recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__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 +810 -480
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +39 -28
- recce/apis/check_func.py +33 -27
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +29 -23
- recce/artifact.py +119 -51
- recce/cli.py +1299 -323
- recce/config.py +42 -33
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.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/media/reload-image.7aa931c7.svg +4 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -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 +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -7
- recce/diff.py +6 -12
- 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 +716 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +131 -28
- recce/pull_request.py +27 -25
- recce/run.py +165 -121
- recce/server.py +303 -111
- 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 +188 -143
- 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 +139 -87
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +87 -85
- recce/util/cll.py +274 -219
- 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 +322 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -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 +76 -0
- recce_cloud/api/gitlab.py +82 -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 +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/conftest.py +9 -5
- tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- 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 +333 -0
- tests/tasks/conftest.py +1 -1
- tests/tasks/test_histogram.py +58 -66
- tests/tasks/test_lineage.py +36 -23
- tests/tasks/test_preset_checks.py +45 -31
- tests/tasks/test_profile.py +339 -15
- tests/tasks/test_query.py +46 -46
- tests/tasks/test_row_count.py +65 -46
- tests/tasks/test_schema.py +65 -42
- tests/tasks/test_top_k.py +22 -18
- tests/tasks/test_valuediff.py +43 -32
- tests/test_cli.py +174 -60
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_config.py +7 -9
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +151 -4
- tests/test_dbt.py +7 -7
- tests/test_mcp_server.py +332 -0
- tests/test_pull_request.py +1 -1
- tests/test_server.py +25 -19
- tests/test_summary.py +29 -17
- recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
- 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/368-7587b306577df275.js +0 -65
- 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/3a92ee20-3b5d922d4157af5e.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/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/6ef81909-694dc38134099299.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/8d700b6a-f0b1f6b9e0d97ce2.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-cee661090afbd6aa.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/state.py +0 -753
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- tests/test_state.py +0 -123
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
recce/server.py
CHANGED
|
@@ -7,30 +7,66 @@ 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
|
-
from typing import
|
|
12
|
-
|
|
13
|
-
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
|
+
)
|
|
14
24
|
from fastapi.middleware.cors import CORSMiddleware
|
|
15
25
|
from fastapi.responses import PlainTextResponse
|
|
16
26
|
from fastapi.staticfiles import StaticFiles
|
|
17
|
-
from pydantic import
|
|
27
|
+
from pydantic import BaseModel, ValidationError
|
|
18
28
|
from pytz import utc
|
|
19
29
|
from starlette.middleware.gzip import GZipMiddleware
|
|
20
30
|
from starlette.middleware.sessions import SessionMiddleware
|
|
21
31
|
from starlette.websockets import WebSocketDisconnect
|
|
22
32
|
|
|
23
|
-
from . import __version__, event,
|
|
33
|
+
from . import __latest_version__, __version__, event, is_recce_cloud_instance
|
|
24
34
|
from .apis.check_api import check_router
|
|
25
35
|
from .apis.run_api import run_router
|
|
26
36
|
from .config import RecceConfig
|
|
27
|
-
from .
|
|
28
|
-
|
|
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
|
+
)
|
|
44
|
+
from .core import RecceContext, default_context, load_context
|
|
45
|
+
from .event import get_recce_api_token, log_api_event, log_single_env_event
|
|
29
46
|
from .exceptions import RecceException
|
|
47
|
+
from .github import is_github_codespace
|
|
48
|
+
from .models.types import CllData
|
|
30
49
|
from .run import load_preset_checks
|
|
31
|
-
from .state import
|
|
50
|
+
from .state import RecceShareStateManager, RecceStateLoader
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("uvicorn")
|
|
53
|
+
|
|
54
|
+
# Idle timeout check interval bounds (in seconds)
|
|
55
|
+
MAX_CHECK_INTERVAL = 30
|
|
56
|
+
MIN_CHECK_INTERVAL = 1
|
|
57
|
+
|
|
32
58
|
|
|
33
|
-
|
|
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"]
|
|
34
70
|
|
|
35
71
|
|
|
36
72
|
@dataclass
|
|
@@ -42,24 +78,82 @@ class AppState:
|
|
|
42
78
|
auth_options: Optional[dict] = None
|
|
43
79
|
lifetime: Optional[int] = None
|
|
44
80
|
lifetime_expired_at: Optional[datetime] = None
|
|
81
|
+
idle_timeout: Optional[int] = None
|
|
82
|
+
last_activity: Optional[dict] = None
|
|
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
|
|
45
88
|
|
|
46
89
|
|
|
47
90
|
def schedule_lifetime_termination(app_state):
|
|
48
91
|
def terminating_server():
|
|
49
92
|
pid = os.getpid()
|
|
50
|
-
logger.info(f"Terminating server process [{pid}] manually")
|
|
93
|
+
logger.info(f"Terminating server process [{pid}] manually due to lifetime expiration")
|
|
51
94
|
os.kill(pid, signal.SIGINT)
|
|
52
95
|
|
|
53
96
|
# Terminate the server process after the specified lifetime
|
|
54
|
-
logger.info(f
|
|
97
|
+
logger.info(f"[Configuration] The lifetime of the server is {app_state.lifetime} seconds")
|
|
55
98
|
app.state.lifetime_expired_at = datetime.now(utc) + timedelta(seconds=app_state.lifetime)
|
|
56
99
|
asyncio.get_running_loop().call_later(app_state.lifetime, terminating_server)
|
|
57
100
|
|
|
58
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
|
+
|
|
59
152
|
def setup_server(app_state: AppState) -> RecceContext:
|
|
60
|
-
from .core import load_context
|
|
61
153
|
from rich.console import Console
|
|
62
154
|
|
|
155
|
+
from .core import load_context
|
|
156
|
+
|
|
63
157
|
console = Console()
|
|
64
158
|
state_loader = app_state.state_loader
|
|
65
159
|
kwargs = app_state.kwargs
|
|
@@ -73,56 +167,87 @@ def setup_server(app_state: AppState) -> RecceContext:
|
|
|
73
167
|
log_single_env_event()
|
|
74
168
|
|
|
75
169
|
# Initialize Recce Config
|
|
76
|
-
config = RecceConfig(config_file=kwargs.get(
|
|
170
|
+
config = RecceConfig(config_file=kwargs.get("config"))
|
|
77
171
|
if state_loader.state is None:
|
|
78
|
-
preset_checks = config.get(
|
|
172
|
+
preset_checks = config.get("checks", [])
|
|
79
173
|
if preset_checks and len(preset_checks) > 0:
|
|
80
174
|
console.rule("Loading Preset Checks")
|
|
81
175
|
load_preset_checks(preset_checks)
|
|
82
176
|
|
|
83
177
|
from recce.event import log_load_state
|
|
84
|
-
log_load_state(command='server', single_env=single_env)
|
|
85
178
|
|
|
86
|
-
|
|
87
|
-
schedule_lifetime_termination(app_state)
|
|
179
|
+
log_load_state(command="server", single_env=single_env)
|
|
88
180
|
|
|
89
181
|
return ctx
|
|
90
182
|
|
|
91
183
|
|
|
92
184
|
def teardown_server(app_state: AppState, ctx: RecceContext):
|
|
93
|
-
|
|
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)
|
|
94
190
|
state_loader.export(ctx.export_state())
|
|
95
|
-
|
|
96
191
|
ctx.stop_monitor_artifacts()
|
|
97
192
|
if app_state.flag.get("single_env_onboarding", False):
|
|
98
193
|
ctx.stop_monitor_base_env()
|
|
99
194
|
|
|
100
195
|
|
|
101
196
|
def setup_ready_only(app_state: AppState):
|
|
102
|
-
|
|
103
|
-
schedule_lifetime_termination(app_state)
|
|
197
|
+
pass
|
|
104
198
|
|
|
105
199
|
|
|
106
200
|
def teardown_ready_only(app_state: AppState):
|
|
107
201
|
pass
|
|
108
202
|
|
|
109
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
|
+
|
|
110
217
|
@asynccontextmanager
|
|
111
218
|
async def lifespan(fastapi: FastAPI):
|
|
112
219
|
ctx = None
|
|
113
220
|
app_state: AppState = app.state
|
|
114
221
|
|
|
115
|
-
if
|
|
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
|
+
|
|
228
|
+
if app_state.command == "server":
|
|
116
229
|
ctx = setup_server(app_state)
|
|
117
|
-
elif app_state.command ==
|
|
230
|
+
elif app_state.command == "read-only":
|
|
118
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)
|
|
119
242
|
|
|
120
243
|
yield
|
|
121
244
|
|
|
122
|
-
if app_state.command ==
|
|
245
|
+
if app_state.command == "server":
|
|
123
246
|
teardown_server(app_state, ctx)
|
|
124
|
-
elif app_state.command ==
|
|
247
|
+
elif app_state.command == "read_only":
|
|
125
248
|
teardown_ready_only(app_state)
|
|
249
|
+
elif app_state.command == "preview":
|
|
250
|
+
teardown_preview(app_state, ctx)
|
|
126
251
|
|
|
127
252
|
|
|
128
253
|
app = FastAPI(lifespan=lifespan)
|
|
@@ -130,7 +255,7 @@ app = FastAPI(lifespan=lifespan)
|
|
|
130
255
|
|
|
131
256
|
def verify_json_file(file_path: str) -> bool:
|
|
132
257
|
try:
|
|
133
|
-
with open(file_path,
|
|
258
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
134
259
|
json.load(f)
|
|
135
260
|
except Exception:
|
|
136
261
|
return False
|
|
@@ -143,19 +268,15 @@ def dbt_artifacts_updated_callback(file_changed_event: Any):
|
|
|
143
268
|
file_name = src_path.name
|
|
144
269
|
|
|
145
270
|
if not verify_json_file(file_changed_event.src_path):
|
|
146
|
-
logger.debug(
|
|
271
|
+
logger.debug("Skip to refresh the artifacts because the file is not updated completely.")
|
|
147
272
|
return
|
|
148
273
|
|
|
149
|
-
logger.info(
|
|
150
|
-
f'Detect {target_type} file {file_changed_event.event_type}: {file_name}')
|
|
274
|
+
logger.info(f"Detect {target_type} file {file_changed_event.event_type}: {file_name}")
|
|
151
275
|
ctx = load_context()
|
|
152
276
|
ctx.refresh_manifest(file_changed_event.src_path)
|
|
153
277
|
broadcast_command = {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'eventType': file_changed_event.event_type,
|
|
157
|
-
'srcPath': file_changed_event.src_path
|
|
158
|
-
}
|
|
278
|
+
"command": "refresh",
|
|
279
|
+
"event": {"eventType": file_changed_event.event_type, "srcPath": file_changed_event.src_path},
|
|
159
280
|
}
|
|
160
281
|
payload = json.dumps(broadcast_command)
|
|
161
282
|
asyncio.run(broadcast(payload))
|
|
@@ -164,7 +285,7 @@ def dbt_artifacts_updated_callback(file_changed_event: Any):
|
|
|
164
285
|
def dbt_env_updated_callback():
|
|
165
286
|
logger.info("Detect 'manifest.json' and 'catalog.json' are generated under 'target-base' directory")
|
|
166
287
|
broadcast_command = {
|
|
167
|
-
|
|
288
|
+
"command": "relaunch",
|
|
168
289
|
}
|
|
169
290
|
payload = json.dumps(broadcast_command)
|
|
170
291
|
asyncio.run(broadcast(payload))
|
|
@@ -191,11 +312,32 @@ app.add_middleware(
|
|
|
191
312
|
)
|
|
192
313
|
|
|
193
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
|
+
|
|
194
336
|
@app.middleware("http")
|
|
195
337
|
async def set_context_by_cookie(request: Request, call_next):
|
|
196
338
|
response = await call_next(request)
|
|
197
339
|
|
|
198
|
-
user_id_in_cookie = request.cookies.get(
|
|
340
|
+
user_id_in_cookie = request.cookies.get("recce_user_id")
|
|
199
341
|
user_id = event.get_user_id()
|
|
200
342
|
|
|
201
343
|
if event.is_anonymous_tracking() is False:
|
|
@@ -203,7 +345,7 @@ async def set_context_by_cookie(request: Request, call_next):
|
|
|
203
345
|
user_id = None
|
|
204
346
|
|
|
205
347
|
if user_id_in_cookie is None or user_id_in_cookie != user_id:
|
|
206
|
-
response.set_cookie(key=
|
|
348
|
+
response.set_cookie(key="recce_user_id", value=user_id)
|
|
207
349
|
return response
|
|
208
350
|
|
|
209
351
|
|
|
@@ -212,8 +354,8 @@ async def disable_cache(request: Request, call_next):
|
|
|
212
354
|
response = await call_next(request)
|
|
213
355
|
|
|
214
356
|
# disable cache for '/' and '/index.html'
|
|
215
|
-
if request.url.path in [
|
|
216
|
-
response.headers[
|
|
357
|
+
if request.url.path in ["/", "/index.html"]:
|
|
358
|
+
response.headers["Cache-Control"] = "no-store"
|
|
217
359
|
|
|
218
360
|
return response
|
|
219
361
|
|
|
@@ -224,24 +366,40 @@ async def health_check(request: Request):
|
|
|
224
366
|
|
|
225
367
|
|
|
226
368
|
class RecceInstanceInfoOut(BaseModel):
|
|
369
|
+
server_mode: RecceServerMode
|
|
227
370
|
read_only: bool
|
|
371
|
+
preview: bool
|
|
372
|
+
single_env: bool
|
|
228
373
|
authed: bool
|
|
374
|
+
cloud_instance: bool
|
|
229
375
|
lifetime_expired_at: Optional[datetime] = None
|
|
376
|
+
share_url: Optional[str] = None
|
|
377
|
+
session_id: Optional[str] = None
|
|
378
|
+
organization_name: Optional[str] = None
|
|
379
|
+
web_url: Optional[str] = None
|
|
230
380
|
|
|
231
381
|
|
|
232
382
|
@app.get("/api/instance-info", response_model=RecceInstanceInfoOut, response_model_exclude_none=True)
|
|
233
383
|
async def recce_instance_info():
|
|
234
384
|
app_state: AppState = app.state
|
|
235
385
|
flag = app_state.flag
|
|
236
|
-
read_only = flag.get(
|
|
386
|
+
read_only = flag.get("read_only", False)
|
|
387
|
+
single_env = flag.get("single_env_onboarding", False)
|
|
237
388
|
|
|
238
|
-
|
|
239
|
-
api_token = auth_options.get('api_token')
|
|
389
|
+
api_token = get_recce_api_token()
|
|
240
390
|
|
|
241
391
|
return {
|
|
392
|
+
"server_mode": app_state.command,
|
|
242
393
|
"read_only": read_only,
|
|
394
|
+
"preview": flag.get("preview", False),
|
|
395
|
+
"single_env": single_env,
|
|
243
396
|
"authed": True if api_token else False,
|
|
397
|
+
"cloud_instance": is_recce_cloud_instance(),
|
|
244
398
|
"lifetime_expired_at": app_state.lifetime_expired_at, # UTC timezone
|
|
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,
|
|
245
403
|
# TODO: Add more instance info which won't change during the instance lifecycle
|
|
246
404
|
# review_mode
|
|
247
405
|
# cloud_mode
|
|
@@ -257,16 +415,9 @@ async def config_flag():
|
|
|
257
415
|
return flag
|
|
258
416
|
|
|
259
417
|
|
|
260
|
-
@app.post("/api/onboarding/completed", status_code=204)
|
|
261
|
-
async def mark_onboarding_completed():
|
|
262
|
-
context = default_context()
|
|
263
|
-
context.mark_onboarding_completed()
|
|
264
|
-
app.state.flag['show_onboarding_guide'] = False
|
|
265
|
-
|
|
266
|
-
|
|
267
418
|
@app.post("/api/relaunch-hint/completed", status_code=204)
|
|
268
419
|
async def mark_relaunch_hint_completed():
|
|
269
|
-
app.state.flag[
|
|
420
|
+
app.state.flag["show_relaunch_hint"] = False
|
|
270
421
|
|
|
271
422
|
|
|
272
423
|
@app.get("/api/info")
|
|
@@ -275,7 +426,8 @@ async def get_info():
|
|
|
275
426
|
Get the information of the current context.
|
|
276
427
|
"""
|
|
277
428
|
context = default_context()
|
|
278
|
-
demo = os.environ.get(
|
|
429
|
+
demo = os.environ.get("DEMO", False)
|
|
430
|
+
is_codespace = is_github_codespace()
|
|
279
431
|
|
|
280
432
|
if demo:
|
|
281
433
|
state = context.export_demo_state()
|
|
@@ -293,25 +445,27 @@ async def get_info():
|
|
|
293
445
|
|
|
294
446
|
try:
|
|
295
447
|
info = {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
448
|
+
"state_metadata": state_metadata,
|
|
449
|
+
"adapter_type": context.adapter_type,
|
|
450
|
+
"review_mode": context.review_mode,
|
|
451
|
+
"git": state.git.to_dict() if state.git else None,
|
|
452
|
+
"pull_request": state.pull_request.to_dict() if state.pull_request else None,
|
|
453
|
+
"lineage": lineage_diff,
|
|
454
|
+
"demo": bool(demo),
|
|
455
|
+
"codespace": bool(is_codespace),
|
|
456
|
+
"cloud_mode": context.state_loader.cloud_mode,
|
|
457
|
+
"file_mode": context.state_loader.state_file is not None,
|
|
458
|
+
"filename": filename,
|
|
459
|
+
"support_tasks": support_tasks,
|
|
307
460
|
}
|
|
308
461
|
|
|
309
|
-
if context.adapter_type ==
|
|
462
|
+
if context.adapter_type == "sqlmesh":
|
|
310
463
|
from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
464
|
+
|
|
311
465
|
sqlmesh_adapter: SqlmeshAdapter = context.adapter
|
|
312
|
-
info[
|
|
313
|
-
|
|
314
|
-
|
|
466
|
+
info["sqlmesh"] = {
|
|
467
|
+
"base_env": sqlmesh_adapter.base_env.name,
|
|
468
|
+
"current_env": sqlmesh_adapter.curr_env.name,
|
|
315
469
|
}
|
|
316
470
|
|
|
317
471
|
return info
|
|
@@ -320,32 +474,40 @@ async def get_info():
|
|
|
320
474
|
|
|
321
475
|
|
|
322
476
|
class CllIn(BaseModel):
|
|
323
|
-
|
|
477
|
+
node_id: Optional[str] = None
|
|
478
|
+
column: Optional[str] = None
|
|
479
|
+
change_analysis: Optional[bool] = False
|
|
480
|
+
no_cll: Optional[bool] = False
|
|
481
|
+
no_upstream: Optional[bool] = False
|
|
482
|
+
no_downstream: Optional[bool] = False
|
|
324
483
|
|
|
325
484
|
|
|
326
485
|
class CllOutput(BaseModel):
|
|
327
|
-
current:
|
|
486
|
+
current: CllData
|
|
328
487
|
|
|
329
488
|
|
|
330
489
|
@app.post("/api/cll", response_model=CllOutput)
|
|
331
490
|
async def column_level_lineage_by_node(cll_input: CllIn):
|
|
332
491
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
333
|
-
dbt_adapter: DbtAdapter = default_context().adapter
|
|
334
492
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
493
|
+
dbt_adapter: DbtAdapter = default_context().adapter
|
|
494
|
+
cll = dbt_adapter.get_cll(
|
|
495
|
+
node_id=cll_input.node_id,
|
|
496
|
+
column=cll_input.column,
|
|
497
|
+
change_analysis=cll_input.change_analysis,
|
|
498
|
+
no_upstream=cll_input.no_upstream,
|
|
499
|
+
no_downstream=cll_input.no_downstream,
|
|
500
|
+
no_cll=cll_input.no_cll,
|
|
501
|
+
)
|
|
340
502
|
|
|
341
|
-
return CllOutput(current=
|
|
503
|
+
return CllOutput(current=cll)
|
|
342
504
|
|
|
343
505
|
|
|
344
506
|
class SelectNodesInput(BaseModel):
|
|
345
507
|
select: Optional[str] = None
|
|
346
508
|
exclude: Optional[str] = None
|
|
347
509
|
packages: Optional[list[str]] = None
|
|
348
|
-
view_mode: Optional[Literal[
|
|
510
|
+
view_mode: Optional[Literal["all", "changed_models"]] = None
|
|
349
511
|
|
|
350
512
|
|
|
351
513
|
class SelectNodesOutput(BaseModel):
|
|
@@ -356,8 +518,8 @@ class SelectNodesOutput(BaseModel):
|
|
|
356
518
|
async def select_nodes(input: SelectNodesInput):
|
|
357
519
|
context = default_context()
|
|
358
520
|
|
|
359
|
-
if context.adapter_type !=
|
|
360
|
-
raise HTTPException(status_code=400, detail=
|
|
521
|
+
if context.adapter_type != "dbt":
|
|
522
|
+
raise HTTPException(status_code=400, detail="Only dbt adapter is supported")
|
|
361
523
|
|
|
362
524
|
try:
|
|
363
525
|
nodes = context.adapter.select_nodes(
|
|
@@ -366,7 +528,7 @@ async def select_nodes(input: SelectNodesInput):
|
|
|
366
528
|
packages=input.packages,
|
|
367
529
|
view_mode=input.view_mode,
|
|
368
530
|
)
|
|
369
|
-
nodes = [node for node in nodes if not node.startswith(
|
|
531
|
+
nodes = [node for node in nodes if not node.startswith("test.")]
|
|
370
532
|
return SelectNodesOutput(nodes=nodes)
|
|
371
533
|
except Exception as e:
|
|
372
534
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -377,9 +539,9 @@ async def get_columns(model_id: str):
|
|
|
377
539
|
context = default_context()
|
|
378
540
|
try:
|
|
379
541
|
return {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
542
|
+
"model": {
|
|
543
|
+
"base": context.get_model(model_id, base=True),
|
|
544
|
+
"current": context.get_model(model_id, base=False),
|
|
383
545
|
}
|
|
384
546
|
}
|
|
385
547
|
except Exception as e:
|
|
@@ -394,12 +556,12 @@ async def save_handler():
|
|
|
394
556
|
try:
|
|
395
557
|
# Sync the state file
|
|
396
558
|
context = default_context()
|
|
397
|
-
log_api_event(
|
|
559
|
+
log_api_event("save", dict(state_loader_mode=context.state_loader_mode()))
|
|
398
560
|
state_loader = context.state_loader
|
|
399
561
|
if not state_loader.cloud_mode and state_loader.state_file is None:
|
|
400
|
-
raise RecceException(
|
|
562
|
+
raise RecceException("Not file mode or cloud mode")
|
|
401
563
|
|
|
402
|
-
context.sync_state(
|
|
564
|
+
context.sync_state("overwrite")
|
|
403
565
|
except RecceException as e:
|
|
404
566
|
raise HTTPException(status_code=400, detail=e.message)
|
|
405
567
|
|
|
@@ -415,33 +577,33 @@ def saveas_or_rename(input: SaveAsOrRenameInput, rename: bool = False):
|
|
|
415
577
|
context = default_context()
|
|
416
578
|
state_loader = context.state_loader
|
|
417
579
|
if state_loader.cloud_mode:
|
|
418
|
-
raise RecceException(
|
|
580
|
+
raise RecceException("Cloud mode does not support rename")
|
|
419
581
|
|
|
420
582
|
new_filename = input.filename
|
|
421
583
|
if os.path.dirname(new_filename):
|
|
422
|
-
raise RecceException(
|
|
423
|
-
if not new_filename.endswith(
|
|
424
|
-
raise RecceException(
|
|
584
|
+
raise RecceException("The new filename should not contain directory")
|
|
585
|
+
if not new_filename.endswith(".json"):
|
|
586
|
+
raise RecceException("The new filename should end with .json")
|
|
425
587
|
|
|
426
588
|
old_path = state_loader.state_file
|
|
427
589
|
if old_path:
|
|
428
590
|
old_dir = os.path.dirname(state_loader.state_file)
|
|
429
591
|
old_filename = os.path.basename(state_loader.state_file)
|
|
430
592
|
if old_filename == new_filename:
|
|
431
|
-
raise RecceException(
|
|
593
|
+
raise RecceException("The new filename is the same as the current filename")
|
|
432
594
|
new_path = os.path.join(old_dir, new_filename)
|
|
433
595
|
else:
|
|
434
596
|
new_path = new_filename
|
|
435
597
|
|
|
436
598
|
if os.path.exists(new_path):
|
|
437
599
|
if os.path.isdir(new_path):
|
|
438
|
-
raise HTTPException(status_code=400, detail=f
|
|
600
|
+
raise HTTPException(status_code=400, detail=f"The file {new_path} exists and is a directory")
|
|
439
601
|
|
|
440
602
|
if not input.overwrite:
|
|
441
|
-
raise HTTPException(status_code=409, detail=f
|
|
603
|
+
raise HTTPException(status_code=409, detail=f"The file {new_filename} already exists")
|
|
442
604
|
|
|
443
605
|
state_loader.state_file = new_path
|
|
444
|
-
context.sync_state(
|
|
606
|
+
context.sync_state("overwrite")
|
|
445
607
|
if rename and os.path.exists(old_path):
|
|
446
608
|
os.remove(old_path)
|
|
447
609
|
|
|
@@ -453,7 +615,7 @@ async def save_as_handler(input: SaveAsOrRenameInput):
|
|
|
453
615
|
"""
|
|
454
616
|
context = default_context()
|
|
455
617
|
try:
|
|
456
|
-
log_api_event(
|
|
618
|
+
log_api_event("saveas", dict(state_loader_mode=context.state_loader_mode()))
|
|
457
619
|
saveas_or_rename(input, rename=False)
|
|
458
620
|
except RecceException as e:
|
|
459
621
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -466,7 +628,7 @@ async def rename_handler(input: SaveAsOrRenameInput):
|
|
|
466
628
|
"""
|
|
467
629
|
context = default_context()
|
|
468
630
|
try:
|
|
469
|
-
log_api_event(
|
|
631
|
+
log_api_event("rename", dict(state_loader_mode=context.state_loader_mode()))
|
|
470
632
|
saveas_or_rename(input, rename=True)
|
|
471
633
|
except RecceException as e:
|
|
472
634
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -479,7 +641,7 @@ async def export_handler():
|
|
|
479
641
|
"""
|
|
480
642
|
context = default_context()
|
|
481
643
|
try:
|
|
482
|
-
log_api_event(
|
|
644
|
+
log_api_event("export", dict(state_loader_mode=context.state_loader_mode()))
|
|
483
645
|
return context.export_state().to_json()
|
|
484
646
|
except RecceException as e:
|
|
485
647
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -487,17 +649,16 @@ async def export_handler():
|
|
|
487
649
|
|
|
488
650
|
@app.post("/api/import", status_code=200)
|
|
489
651
|
async def import_handler(
|
|
490
|
-
file: Annotated[UploadFile, Form()],
|
|
491
|
-
checks_only: Annotated[bool, Form()],
|
|
492
|
-
background_tasks: BackgroundTasks
|
|
652
|
+
file: Annotated[UploadFile, Form()], checks_only: Annotated[bool, Form()], background_tasks: BackgroundTasks
|
|
493
653
|
):
|
|
494
654
|
"""
|
|
495
655
|
Import the recce state from the client.
|
|
496
656
|
"""
|
|
497
657
|
from recce.state import RecceState
|
|
658
|
+
|
|
498
659
|
context = default_context()
|
|
499
660
|
try:
|
|
500
|
-
log_api_event(
|
|
661
|
+
log_api_event("import", dict(state_loader_mode=context.state_loader_mode()))
|
|
501
662
|
content = await file.read()
|
|
502
663
|
state = RecceState.from_json(content)
|
|
503
664
|
|
|
@@ -531,16 +692,19 @@ async def sync_handler(input: SyncStateInput, response: Response, background_tas
|
|
|
531
692
|
context = default_context()
|
|
532
693
|
state_loader = context.state_loader
|
|
533
694
|
method = input.method
|
|
534
|
-
log_api_event(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
695
|
+
log_api_event(
|
|
696
|
+
"sync",
|
|
697
|
+
dict(
|
|
698
|
+
state_loader_mode=context.state_loader_mode(),
|
|
699
|
+
method=method,
|
|
700
|
+
),
|
|
701
|
+
)
|
|
538
702
|
|
|
539
703
|
if not method:
|
|
540
704
|
is_conflict = state_loader.check_conflict()
|
|
541
705
|
if is_conflict:
|
|
542
|
-
raise HTTPException(status_code=409, detail=
|
|
543
|
-
method =
|
|
706
|
+
raise HTTPException(status_code=409, detail="Conflict detected")
|
|
707
|
+
method = "overwrite"
|
|
544
708
|
|
|
545
709
|
is_syncing = state_loader.state_lock.locked()
|
|
546
710
|
if is_syncing:
|
|
@@ -590,7 +754,7 @@ async def share_state():
|
|
|
590
754
|
context = default_context()
|
|
591
755
|
state_loader = context.state_loader
|
|
592
756
|
|
|
593
|
-
file_name =
|
|
757
|
+
file_name = "recce_state.json"
|
|
594
758
|
if state_loader.state_file:
|
|
595
759
|
file_name = os.path.basename(state_loader.state_file)
|
|
596
760
|
|
|
@@ -626,8 +790,8 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
|
626
790
|
try:
|
|
627
791
|
while True:
|
|
628
792
|
data = await websocket.receive_text()
|
|
629
|
-
if data ==
|
|
630
|
-
await websocket.send_text(
|
|
793
|
+
if data == "ping":
|
|
794
|
+
await websocket.send_text("pong")
|
|
631
795
|
except WebSocketDisconnect:
|
|
632
796
|
clients.remove(websocket)
|
|
633
797
|
|
|
@@ -637,9 +801,37 @@ async def broadcast(data: str):
|
|
|
637
801
|
await client.send_text(data)
|
|
638
802
|
|
|
639
803
|
|
|
640
|
-
|
|
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
|
+
|
|
832
|
+
api_prefix = "/api"
|
|
641
833
|
app.include_router(check_router, prefix=api_prefix)
|
|
642
834
|
app.include_router(run_router, prefix=api_prefix)
|
|
643
835
|
|
|
644
|
-
static_folder_path = Path(__file__).parent /
|
|
836
|
+
static_folder_path = Path(__file__).parent / "data"
|
|
645
837
|
app.mount("/", StaticFiles(directory=static_folder_path, html=True), name="static")
|