recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.4.0.20250514__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 +22 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +355 -316
- 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 +44 -49
- recce/cli.py +484 -285
- recce/config.py +42 -33
- recce/core.py +52 -44
- recce/data/404.html +1 -1
- recce/data/_next/static/chunks/{368-7587b306577df275.js → 778-aef312bffb4c0312.js} +15 -15
- recce/data/_next/static/chunks/8d700b6a.ed11a130057c7a47.js +1 -0
- recce/data/_next/static/chunks/app/layout-c713a2829d3279e4.js +1 -0
- recce/data/_next/static/chunks/app/page-7086764277331fcb.js +1 -0
- recce/data/_next/static/chunks/{cd9f8d63-cf0d5a7b0f7a92e8.js → cd9f8d63-e020f408095ed77c.js} +3 -3
- recce/data/_next/static/chunks/webpack-b787cb1a4f2293de.js +1 -0
- recce/data/_next/static/css/88b8abc134cfd59a.css +3 -0
- recce/data/index.html +2 -2
- recce/data/index.txt +2 -2
- recce/diff.py +6 -12
- recce/event/__init__.py +74 -72
- recce/event/collector.py +27 -20
- recce/event/track.py +39 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/models/__init__.py +1 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +27 -27
- recce/pull_request.py +26 -24
- recce/run.py +148 -111
- recce/server.py +105 -88
- recce/state.py +209 -177
- recce/summary.py +168 -143
- recce/tasks/__init__.py +3 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +19 -17
- 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 +33 -30
- recce/tasks/schema.py +14 -14
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/breaking.py +77 -84
- recce/util/cll.py +55 -51
- recce/util/io.py +19 -17
- recce/util/logger.py +1 -1
- recce/util/recce_cloud.py +70 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/METADATA +5 -2
- recce_nightly-1.4.0.20250514.dist-info/RECORD +143 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/WHEEL +1 -1
- tests/adapter/dbt_adapter/conftest.py +1 -0
- tests/adapter/dbt_adapter/dbt_test_helper.py +28 -18
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +39 -32
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- 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 +340 -15
- tests/tasks/test_query.py +40 -40
- 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 +71 -58
- tests/test_config.py +7 -9
- tests/test_core.py +5 -3
- tests/test_dbt.py +7 -7
- tests/test_pull_request.py +1 -1
- tests/test_server.py +19 -13
- tests/test_state.py +40 -27
- tests/test_summary.py +18 -14
- recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.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/webpack-567d72f0bc0820d5.js +0 -1
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → E_HPXsXdrqHg2YEHmU3mK}/_buildManifest.js +0 -0
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → E_HPXsXdrqHg2YEHmU3mK}/_ssgManifest.js +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/licenses/LICENSE +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/top_level.txt +0 -0
recce/server.py
CHANGED
|
@@ -8,29 +8,38 @@ from contextlib import asynccontextmanager
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from datetime import datetime, timedelta
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
12
|
-
|
|
13
|
-
from fastapi import
|
|
11
|
+
from typing import Annotated, Any, Dict, Literal, Optional, Set
|
|
12
|
+
|
|
13
|
+
from fastapi import (
|
|
14
|
+
BackgroundTasks,
|
|
15
|
+
FastAPI,
|
|
16
|
+
Form,
|
|
17
|
+
HTTPException,
|
|
18
|
+
Request,
|
|
19
|
+
Response,
|
|
20
|
+
UploadFile,
|
|
21
|
+
WebSocket,
|
|
22
|
+
)
|
|
14
23
|
from fastapi.middleware.cors import CORSMiddleware
|
|
15
24
|
from fastapi.responses import PlainTextResponse
|
|
16
25
|
from fastapi.staticfiles import StaticFiles
|
|
17
|
-
from pydantic import
|
|
26
|
+
from pydantic import BaseModel, ValidationError
|
|
18
27
|
from pytz import utc
|
|
19
28
|
from starlette.middleware.gzip import GZipMiddleware
|
|
20
29
|
from starlette.middleware.sessions import SessionMiddleware
|
|
21
30
|
from starlette.websockets import WebSocketDisconnect
|
|
22
31
|
|
|
23
|
-
from . import __version__, event
|
|
32
|
+
from . import __latest_version__, __version__, event
|
|
24
33
|
from .apis.check_api import check_router
|
|
25
34
|
from .apis.run_api import run_router
|
|
26
35
|
from .config import RecceConfig
|
|
27
|
-
from .core import
|
|
36
|
+
from .core import RecceContext, default_context, load_context
|
|
28
37
|
from .event import log_api_event, log_single_env_event
|
|
29
38
|
from .exceptions import RecceException
|
|
30
39
|
from .run import load_preset_checks
|
|
31
|
-
from .state import
|
|
40
|
+
from .state import RecceShareStateManager, RecceStateLoader
|
|
32
41
|
|
|
33
|
-
logger = logging.getLogger(
|
|
42
|
+
logger = logging.getLogger("uvicorn")
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
@dataclass
|
|
@@ -42,6 +51,7 @@ class AppState:
|
|
|
42
51
|
auth_options: Optional[dict] = None
|
|
43
52
|
lifetime: Optional[int] = None
|
|
44
53
|
lifetime_expired_at: Optional[datetime] = None
|
|
54
|
+
share_url: Optional[str] = None
|
|
45
55
|
|
|
46
56
|
|
|
47
57
|
def schedule_lifetime_termination(app_state):
|
|
@@ -51,15 +61,16 @@ def schedule_lifetime_termination(app_state):
|
|
|
51
61
|
os.kill(pid, signal.SIGINT)
|
|
52
62
|
|
|
53
63
|
# Terminate the server process after the specified lifetime
|
|
54
|
-
logger.info(f
|
|
64
|
+
logger.info(f"[Configuration] The lifetime of the server is {app_state.lifetime} seconds")
|
|
55
65
|
app.state.lifetime_expired_at = datetime.now(utc) + timedelta(seconds=app_state.lifetime)
|
|
56
66
|
asyncio.get_running_loop().call_later(app_state.lifetime, terminating_server)
|
|
57
67
|
|
|
58
68
|
|
|
59
69
|
def setup_server(app_state: AppState) -> RecceContext:
|
|
60
|
-
from .core import load_context
|
|
61
70
|
from rich.console import Console
|
|
62
71
|
|
|
72
|
+
from .core import load_context
|
|
73
|
+
|
|
63
74
|
console = Console()
|
|
64
75
|
state_loader = app_state.state_loader
|
|
65
76
|
kwargs = app_state.kwargs
|
|
@@ -73,15 +84,16 @@ def setup_server(app_state: AppState) -> RecceContext:
|
|
|
73
84
|
log_single_env_event()
|
|
74
85
|
|
|
75
86
|
# Initialize Recce Config
|
|
76
|
-
config = RecceConfig(config_file=kwargs.get(
|
|
87
|
+
config = RecceConfig(config_file=kwargs.get("config"))
|
|
77
88
|
if state_loader.state is None:
|
|
78
|
-
preset_checks = config.get(
|
|
89
|
+
preset_checks = config.get("checks", [])
|
|
79
90
|
if preset_checks and len(preset_checks) > 0:
|
|
80
91
|
console.rule("Loading Preset Checks")
|
|
81
92
|
load_preset_checks(preset_checks)
|
|
82
93
|
|
|
83
94
|
from recce.event import log_load_state
|
|
84
|
-
|
|
95
|
+
|
|
96
|
+
log_load_state(command="server", single_env=single_env)
|
|
85
97
|
|
|
86
98
|
if app_state.lifetime is not None and app_state.lifetime > 0:
|
|
87
99
|
schedule_lifetime_termination(app_state)
|
|
@@ -112,16 +124,16 @@ async def lifespan(fastapi: FastAPI):
|
|
|
112
124
|
ctx = None
|
|
113
125
|
app_state: AppState = app.state
|
|
114
126
|
|
|
115
|
-
if app_state.command ==
|
|
127
|
+
if app_state.command == "server":
|
|
116
128
|
ctx = setup_server(app_state)
|
|
117
|
-
elif app_state.command ==
|
|
129
|
+
elif app_state.command == "read_only":
|
|
118
130
|
setup_ready_only(app_state)
|
|
119
131
|
|
|
120
132
|
yield
|
|
121
133
|
|
|
122
|
-
if app_state.command ==
|
|
134
|
+
if app_state.command == "server":
|
|
123
135
|
teardown_server(app_state, ctx)
|
|
124
|
-
elif app_state.command ==
|
|
136
|
+
elif app_state.command == "read_only":
|
|
125
137
|
teardown_ready_only(app_state)
|
|
126
138
|
|
|
127
139
|
|
|
@@ -130,7 +142,7 @@ app = FastAPI(lifespan=lifespan)
|
|
|
130
142
|
|
|
131
143
|
def verify_json_file(file_path: str) -> bool:
|
|
132
144
|
try:
|
|
133
|
-
with open(file_path,
|
|
145
|
+
with open(file_path, "r") as f:
|
|
134
146
|
json.load(f)
|
|
135
147
|
except Exception:
|
|
136
148
|
return False
|
|
@@ -143,19 +155,15 @@ def dbt_artifacts_updated_callback(file_changed_event: Any):
|
|
|
143
155
|
file_name = src_path.name
|
|
144
156
|
|
|
145
157
|
if not verify_json_file(file_changed_event.src_path):
|
|
146
|
-
logger.debug(
|
|
158
|
+
logger.debug("Skip to refresh the artifacts because the file is not updated completely.")
|
|
147
159
|
return
|
|
148
160
|
|
|
149
|
-
logger.info(
|
|
150
|
-
f'Detect {target_type} file {file_changed_event.event_type}: {file_name}')
|
|
161
|
+
logger.info(f"Detect {target_type} file {file_changed_event.event_type}: {file_name}")
|
|
151
162
|
ctx = load_context()
|
|
152
163
|
ctx.refresh_manifest(file_changed_event.src_path)
|
|
153
164
|
broadcast_command = {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'eventType': file_changed_event.event_type,
|
|
157
|
-
'srcPath': file_changed_event.src_path
|
|
158
|
-
}
|
|
165
|
+
"command": "refresh",
|
|
166
|
+
"event": {"eventType": file_changed_event.event_type, "srcPath": file_changed_event.src_path},
|
|
159
167
|
}
|
|
160
168
|
payload = json.dumps(broadcast_command)
|
|
161
169
|
asyncio.run(broadcast(payload))
|
|
@@ -164,7 +172,7 @@ def dbt_artifacts_updated_callback(file_changed_event: Any):
|
|
|
164
172
|
def dbt_env_updated_callback():
|
|
165
173
|
logger.info("Detect 'manifest.json' and 'catalog.json' are generated under 'target-base' directory")
|
|
166
174
|
broadcast_command = {
|
|
167
|
-
|
|
175
|
+
"command": "relaunch",
|
|
168
176
|
}
|
|
169
177
|
payload = json.dumps(broadcast_command)
|
|
170
178
|
asyncio.run(broadcast(payload))
|
|
@@ -195,7 +203,7 @@ app.add_middleware(
|
|
|
195
203
|
async def set_context_by_cookie(request: Request, call_next):
|
|
196
204
|
response = await call_next(request)
|
|
197
205
|
|
|
198
|
-
user_id_in_cookie = request.cookies.get(
|
|
206
|
+
user_id_in_cookie = request.cookies.get("recce_user_id")
|
|
199
207
|
user_id = event.get_user_id()
|
|
200
208
|
|
|
201
209
|
if event.is_anonymous_tracking() is False:
|
|
@@ -203,7 +211,7 @@ async def set_context_by_cookie(request: Request, call_next):
|
|
|
203
211
|
user_id = None
|
|
204
212
|
|
|
205
213
|
if user_id_in_cookie is None or user_id_in_cookie != user_id:
|
|
206
|
-
response.set_cookie(key=
|
|
214
|
+
response.set_cookie(key="recce_user_id", value=user_id)
|
|
207
215
|
return response
|
|
208
216
|
|
|
209
217
|
|
|
@@ -212,8 +220,8 @@ async def disable_cache(request: Request, call_next):
|
|
|
212
220
|
response = await call_next(request)
|
|
213
221
|
|
|
214
222
|
# disable cache for '/' and '/index.html'
|
|
215
|
-
if request.url.path in [
|
|
216
|
-
response.headers[
|
|
223
|
+
if request.url.path in ["/", "/index.html"]:
|
|
224
|
+
response.headers["Cache-Control"] = "no-store"
|
|
217
225
|
|
|
218
226
|
return response
|
|
219
227
|
|
|
@@ -225,23 +233,28 @@ async def health_check(request: Request):
|
|
|
225
233
|
|
|
226
234
|
class RecceInstanceInfoOut(BaseModel):
|
|
227
235
|
read_only: bool
|
|
236
|
+
single_env: bool
|
|
228
237
|
authed: bool
|
|
229
238
|
lifetime_expired_at: Optional[datetime] = None
|
|
239
|
+
share_url: Optional[str] = None
|
|
230
240
|
|
|
231
241
|
|
|
232
242
|
@app.get("/api/instance-info", response_model=RecceInstanceInfoOut, response_model_exclude_none=True)
|
|
233
243
|
async def recce_instance_info():
|
|
234
244
|
app_state: AppState = app.state
|
|
235
245
|
flag = app_state.flag
|
|
236
|
-
read_only = flag.get(
|
|
246
|
+
read_only = flag.get("read_only", False)
|
|
247
|
+
single_env = flag.get("single_env_onboarding", False)
|
|
237
248
|
|
|
238
249
|
auth_options = app_state.auth_options or {}
|
|
239
|
-
api_token = auth_options.get(
|
|
250
|
+
api_token = auth_options.get("api_token")
|
|
240
251
|
|
|
241
252
|
return {
|
|
242
253
|
"read_only": read_only,
|
|
254
|
+
"single_env": single_env,
|
|
243
255
|
"authed": True if api_token else False,
|
|
244
256
|
"lifetime_expired_at": app_state.lifetime_expired_at, # UTC timezone
|
|
257
|
+
"share_url": app_state.share_url,
|
|
245
258
|
# TODO: Add more instance info which won't change during the instance lifecycle
|
|
246
259
|
# review_mode
|
|
247
260
|
# cloud_mode
|
|
@@ -261,12 +274,12 @@ async def config_flag():
|
|
|
261
274
|
async def mark_onboarding_completed():
|
|
262
275
|
context = default_context()
|
|
263
276
|
context.mark_onboarding_completed()
|
|
264
|
-
app.state.flag[
|
|
277
|
+
app.state.flag["show_onboarding_guide"] = False
|
|
265
278
|
|
|
266
279
|
|
|
267
280
|
@app.post("/api/relaunch-hint/completed", status_code=204)
|
|
268
281
|
async def mark_relaunch_hint_completed():
|
|
269
|
-
app.state.flag[
|
|
282
|
+
app.state.flag["show_relaunch_hint"] = False
|
|
270
283
|
|
|
271
284
|
|
|
272
285
|
@app.get("/api/info")
|
|
@@ -275,7 +288,7 @@ async def get_info():
|
|
|
275
288
|
Get the information of the current context.
|
|
276
289
|
"""
|
|
277
290
|
context = default_context()
|
|
278
|
-
demo = os.environ.get(
|
|
291
|
+
demo = os.environ.get("DEMO", False)
|
|
279
292
|
|
|
280
293
|
if demo:
|
|
281
294
|
state = context.export_demo_state()
|
|
@@ -293,25 +306,26 @@ async def get_info():
|
|
|
293
306
|
|
|
294
307
|
try:
|
|
295
308
|
info = {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
"state_metadata": state_metadata,
|
|
310
|
+
"adapter_type": context.adapter_type,
|
|
311
|
+
"review_mode": context.review_mode,
|
|
312
|
+
"git": state.git.to_dict() if state.git else None,
|
|
313
|
+
"pull_request": state.pull_request.to_dict() if state.pull_request else None,
|
|
314
|
+
"lineage": lineage_diff,
|
|
315
|
+
"demo": bool(demo),
|
|
316
|
+
"cloud_mode": context.state_loader.cloud_mode,
|
|
317
|
+
"file_mode": context.state_loader.state_file is not None,
|
|
318
|
+
"filename": filename,
|
|
319
|
+
"support_tasks": support_tasks,
|
|
307
320
|
}
|
|
308
321
|
|
|
309
|
-
if context.adapter_type ==
|
|
322
|
+
if context.adapter_type == "sqlmesh":
|
|
310
323
|
from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
324
|
+
|
|
311
325
|
sqlmesh_adapter: SqlmeshAdapter = context.adapter
|
|
312
|
-
info[
|
|
313
|
-
|
|
314
|
-
|
|
326
|
+
info["sqlmesh"] = {
|
|
327
|
+
"base_env": sqlmesh_adapter.base_env.name,
|
|
328
|
+
"current_env": sqlmesh_adapter.curr_env.name,
|
|
315
329
|
}
|
|
316
330
|
|
|
317
331
|
return info
|
|
@@ -330,11 +344,12 @@ class CllOutput(BaseModel):
|
|
|
330
344
|
@app.post("/api/cll", response_model=CllOutput)
|
|
331
345
|
async def column_level_lineage_by_node(cll_input: CllIn):
|
|
332
346
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
347
|
+
|
|
333
348
|
dbt_adapter: DbtAdapter = default_context().adapter
|
|
334
349
|
|
|
335
350
|
try:
|
|
336
351
|
# TODO: Add support for by the node and column
|
|
337
|
-
result = dbt_adapter.get_cll_by_node_id(cll_input.params.get(
|
|
352
|
+
result = dbt_adapter.get_cll_by_node_id(cll_input.params.get("node_id"))
|
|
338
353
|
except Exception as e:
|
|
339
354
|
raise HTTPException(status_code=400, detail=str(e))
|
|
340
355
|
|
|
@@ -345,7 +360,7 @@ class SelectNodesInput(BaseModel):
|
|
|
345
360
|
select: Optional[str] = None
|
|
346
361
|
exclude: Optional[str] = None
|
|
347
362
|
packages: Optional[list[str]] = None
|
|
348
|
-
view_mode: Optional[Literal[
|
|
363
|
+
view_mode: Optional[Literal["all", "changed_models"]] = None
|
|
349
364
|
|
|
350
365
|
|
|
351
366
|
class SelectNodesOutput(BaseModel):
|
|
@@ -356,8 +371,8 @@ class SelectNodesOutput(BaseModel):
|
|
|
356
371
|
async def select_nodes(input: SelectNodesInput):
|
|
357
372
|
context = default_context()
|
|
358
373
|
|
|
359
|
-
if context.adapter_type !=
|
|
360
|
-
raise HTTPException(status_code=400, detail=
|
|
374
|
+
if context.adapter_type != "dbt":
|
|
375
|
+
raise HTTPException(status_code=400, detail="Only dbt adapter is supported")
|
|
361
376
|
|
|
362
377
|
try:
|
|
363
378
|
nodes = context.adapter.select_nodes(
|
|
@@ -366,7 +381,7 @@ async def select_nodes(input: SelectNodesInput):
|
|
|
366
381
|
packages=input.packages,
|
|
367
382
|
view_mode=input.view_mode,
|
|
368
383
|
)
|
|
369
|
-
nodes = [node for node in nodes if not node.startswith(
|
|
384
|
+
nodes = [node for node in nodes if not node.startswith("test.")]
|
|
370
385
|
return SelectNodesOutput(nodes=nodes)
|
|
371
386
|
except Exception as e:
|
|
372
387
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -377,9 +392,9 @@ async def get_columns(model_id: str):
|
|
|
377
392
|
context = default_context()
|
|
378
393
|
try:
|
|
379
394
|
return {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
395
|
+
"model": {
|
|
396
|
+
"base": context.get_model(model_id, base=True),
|
|
397
|
+
"current": context.get_model(model_id, base=False),
|
|
383
398
|
}
|
|
384
399
|
}
|
|
385
400
|
except Exception as e:
|
|
@@ -394,12 +409,12 @@ async def save_handler():
|
|
|
394
409
|
try:
|
|
395
410
|
# Sync the state file
|
|
396
411
|
context = default_context()
|
|
397
|
-
log_api_event(
|
|
412
|
+
log_api_event("save", dict(state_loader_mode=context.state_loader_mode()))
|
|
398
413
|
state_loader = context.state_loader
|
|
399
414
|
if not state_loader.cloud_mode and state_loader.state_file is None:
|
|
400
|
-
raise RecceException(
|
|
415
|
+
raise RecceException("Not file mode or cloud mode")
|
|
401
416
|
|
|
402
|
-
context.sync_state(
|
|
417
|
+
context.sync_state("overwrite")
|
|
403
418
|
except RecceException as e:
|
|
404
419
|
raise HTTPException(status_code=400, detail=e.message)
|
|
405
420
|
|
|
@@ -415,33 +430,33 @@ def saveas_or_rename(input: SaveAsOrRenameInput, rename: bool = False):
|
|
|
415
430
|
context = default_context()
|
|
416
431
|
state_loader = context.state_loader
|
|
417
432
|
if state_loader.cloud_mode:
|
|
418
|
-
raise RecceException(
|
|
433
|
+
raise RecceException("Cloud mode does not support rename")
|
|
419
434
|
|
|
420
435
|
new_filename = input.filename
|
|
421
436
|
if os.path.dirname(new_filename):
|
|
422
|
-
raise RecceException(
|
|
423
|
-
if not new_filename.endswith(
|
|
424
|
-
raise RecceException(
|
|
437
|
+
raise RecceException("The new filename should not contain directory")
|
|
438
|
+
if not new_filename.endswith(".json"):
|
|
439
|
+
raise RecceException("The new filename should end with .json")
|
|
425
440
|
|
|
426
441
|
old_path = state_loader.state_file
|
|
427
442
|
if old_path:
|
|
428
443
|
old_dir = os.path.dirname(state_loader.state_file)
|
|
429
444
|
old_filename = os.path.basename(state_loader.state_file)
|
|
430
445
|
if old_filename == new_filename:
|
|
431
|
-
raise RecceException(
|
|
446
|
+
raise RecceException("The new filename is the same as the current filename")
|
|
432
447
|
new_path = os.path.join(old_dir, new_filename)
|
|
433
448
|
else:
|
|
434
449
|
new_path = new_filename
|
|
435
450
|
|
|
436
451
|
if os.path.exists(new_path):
|
|
437
452
|
if os.path.isdir(new_path):
|
|
438
|
-
raise HTTPException(status_code=400, detail=f
|
|
453
|
+
raise HTTPException(status_code=400, detail=f"The file {new_path} exists and is a directory")
|
|
439
454
|
|
|
440
455
|
if not input.overwrite:
|
|
441
|
-
raise HTTPException(status_code=409, detail=f
|
|
456
|
+
raise HTTPException(status_code=409, detail=f"The file {new_filename} already exists")
|
|
442
457
|
|
|
443
458
|
state_loader.state_file = new_path
|
|
444
|
-
context.sync_state(
|
|
459
|
+
context.sync_state("overwrite")
|
|
445
460
|
if rename and os.path.exists(old_path):
|
|
446
461
|
os.remove(old_path)
|
|
447
462
|
|
|
@@ -453,7 +468,7 @@ async def save_as_handler(input: SaveAsOrRenameInput):
|
|
|
453
468
|
"""
|
|
454
469
|
context = default_context()
|
|
455
470
|
try:
|
|
456
|
-
log_api_event(
|
|
471
|
+
log_api_event("saveas", dict(state_loader_mode=context.state_loader_mode()))
|
|
457
472
|
saveas_or_rename(input, rename=False)
|
|
458
473
|
except RecceException as e:
|
|
459
474
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -466,7 +481,7 @@ async def rename_handler(input: SaveAsOrRenameInput):
|
|
|
466
481
|
"""
|
|
467
482
|
context = default_context()
|
|
468
483
|
try:
|
|
469
|
-
log_api_event(
|
|
484
|
+
log_api_event("rename", dict(state_loader_mode=context.state_loader_mode()))
|
|
470
485
|
saveas_or_rename(input, rename=True)
|
|
471
486
|
except RecceException as e:
|
|
472
487
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -479,7 +494,7 @@ async def export_handler():
|
|
|
479
494
|
"""
|
|
480
495
|
context = default_context()
|
|
481
496
|
try:
|
|
482
|
-
log_api_event(
|
|
497
|
+
log_api_event("export", dict(state_loader_mode=context.state_loader_mode()))
|
|
483
498
|
return context.export_state().to_json()
|
|
484
499
|
except RecceException as e:
|
|
485
500
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -487,17 +502,16 @@ async def export_handler():
|
|
|
487
502
|
|
|
488
503
|
@app.post("/api/import", status_code=200)
|
|
489
504
|
async def import_handler(
|
|
490
|
-
file: Annotated[UploadFile, Form()],
|
|
491
|
-
checks_only: Annotated[bool, Form()],
|
|
492
|
-
background_tasks: BackgroundTasks
|
|
505
|
+
file: Annotated[UploadFile, Form()], checks_only: Annotated[bool, Form()], background_tasks: BackgroundTasks
|
|
493
506
|
):
|
|
494
507
|
"""
|
|
495
508
|
Import the recce state from the client.
|
|
496
509
|
"""
|
|
497
510
|
from recce.state import RecceState
|
|
511
|
+
|
|
498
512
|
context = default_context()
|
|
499
513
|
try:
|
|
500
|
-
log_api_event(
|
|
514
|
+
log_api_event("import", dict(state_loader_mode=context.state_loader_mode()))
|
|
501
515
|
content = await file.read()
|
|
502
516
|
state = RecceState.from_json(content)
|
|
503
517
|
|
|
@@ -531,16 +545,19 @@ async def sync_handler(input: SyncStateInput, response: Response, background_tas
|
|
|
531
545
|
context = default_context()
|
|
532
546
|
state_loader = context.state_loader
|
|
533
547
|
method = input.method
|
|
534
|
-
log_api_event(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
548
|
+
log_api_event(
|
|
549
|
+
"sync",
|
|
550
|
+
dict(
|
|
551
|
+
state_loader_mode=context.state_loader_mode(),
|
|
552
|
+
method=method,
|
|
553
|
+
),
|
|
554
|
+
)
|
|
538
555
|
|
|
539
556
|
if not method:
|
|
540
557
|
is_conflict = state_loader.check_conflict()
|
|
541
558
|
if is_conflict:
|
|
542
|
-
raise HTTPException(status_code=409, detail=
|
|
543
|
-
method =
|
|
559
|
+
raise HTTPException(status_code=409, detail="Conflict detected")
|
|
560
|
+
method = "overwrite"
|
|
544
561
|
|
|
545
562
|
is_syncing = state_loader.state_lock.locked()
|
|
546
563
|
if is_syncing:
|
|
@@ -590,7 +607,7 @@ async def share_state():
|
|
|
590
607
|
context = default_context()
|
|
591
608
|
state_loader = context.state_loader
|
|
592
609
|
|
|
593
|
-
file_name =
|
|
610
|
+
file_name = "recce_state.json"
|
|
594
611
|
if state_loader.state_file:
|
|
595
612
|
file_name = os.path.basename(state_loader.state_file)
|
|
596
613
|
|
|
@@ -626,8 +643,8 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
|
626
643
|
try:
|
|
627
644
|
while True:
|
|
628
645
|
data = await websocket.receive_text()
|
|
629
|
-
if data ==
|
|
630
|
-
await websocket.send_text(
|
|
646
|
+
if data == "ping":
|
|
647
|
+
await websocket.send_text("pong")
|
|
631
648
|
except WebSocketDisconnect:
|
|
632
649
|
clients.remove(websocket)
|
|
633
650
|
|
|
@@ -637,9 +654,9 @@ async def broadcast(data: str):
|
|
|
637
654
|
await client.send_text(data)
|
|
638
655
|
|
|
639
656
|
|
|
640
|
-
api_prefix =
|
|
657
|
+
api_prefix = "/api"
|
|
641
658
|
app.include_router(check_router, prefix=api_prefix)
|
|
642
659
|
app.include_router(run_router, prefix=api_prefix)
|
|
643
660
|
|
|
644
|
-
static_folder_path = Path(__file__).parent /
|
|
661
|
+
static_folder_path = Path(__file__).parent / "data"
|
|
645
662
|
app.mount("/", StaticFiles(directory=static_folder_path, html=True), name="static")
|