recce-nightly 1.3.0.20250507__py3-none-any.whl → 1.4.0.20250515__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 +103 -89
- 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 +147 -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.3.0.20250507.dist-info → recce_nightly-1.4.0.20250515.dist-info}/METADATA +5 -2
- recce_nightly-1.4.0.20250515.dist-info/RECORD +143 -0
- {recce_nightly-1.3.0.20250507.dist-info → recce_nightly-1.4.0.20250515.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-92f13c8fad9fae3d.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce_nightly-1.3.0.20250507.dist-info/RECORD +0 -142
- /recce/data/_next/static/{K5iKlCYhdcpq8Ea6ck9J_ → q0Xsc9Sd6PDuo1lshYpLu}/_buildManifest.js +0 -0
- /recce/data/_next/static/{K5iKlCYhdcpq8Ea6ck9J_ → q0Xsc9Sd6PDuo1lshYpLu}/_ssgManifest.js +0 -0
- {recce_nightly-1.3.0.20250507.dist-info → recce_nightly-1.4.0.20250515.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.3.0.20250507.dist-info → recce_nightly-1.4.0.20250515.dist-info}/licenses/LICENSE +0 -0
- {recce_nightly-1.3.0.20250507.dist-info → recce_nightly-1.4.0.20250515.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
|
|
|
@@ -228,23 +236,25 @@ class RecceInstanceInfoOut(BaseModel):
|
|
|
228
236
|
single_env: bool
|
|
229
237
|
authed: bool
|
|
230
238
|
lifetime_expired_at: Optional[datetime] = None
|
|
239
|
+
share_url: Optional[str] = None
|
|
231
240
|
|
|
232
241
|
|
|
233
242
|
@app.get("/api/instance-info", response_model=RecceInstanceInfoOut, response_model_exclude_none=True)
|
|
234
243
|
async def recce_instance_info():
|
|
235
244
|
app_state: AppState = app.state
|
|
236
245
|
flag = app_state.flag
|
|
237
|
-
read_only = flag.get(
|
|
238
|
-
single_env = flag.get(
|
|
246
|
+
read_only = flag.get("read_only", False)
|
|
247
|
+
single_env = flag.get("single_env_onboarding", False)
|
|
239
248
|
|
|
240
249
|
auth_options = app_state.auth_options or {}
|
|
241
|
-
api_token = auth_options.get(
|
|
250
|
+
api_token = auth_options.get("api_token")
|
|
242
251
|
|
|
243
252
|
return {
|
|
244
253
|
"read_only": read_only,
|
|
245
254
|
"single_env": single_env,
|
|
246
255
|
"authed": True if api_token else False,
|
|
247
256
|
"lifetime_expired_at": app_state.lifetime_expired_at, # UTC timezone
|
|
257
|
+
"share_url": app_state.share_url,
|
|
248
258
|
# TODO: Add more instance info which won't change during the instance lifecycle
|
|
249
259
|
# review_mode
|
|
250
260
|
# cloud_mode
|
|
@@ -264,12 +274,12 @@ async def config_flag():
|
|
|
264
274
|
async def mark_onboarding_completed():
|
|
265
275
|
context = default_context()
|
|
266
276
|
context.mark_onboarding_completed()
|
|
267
|
-
app.state.flag[
|
|
277
|
+
app.state.flag["show_onboarding_guide"] = False
|
|
268
278
|
|
|
269
279
|
|
|
270
280
|
@app.post("/api/relaunch-hint/completed", status_code=204)
|
|
271
281
|
async def mark_relaunch_hint_completed():
|
|
272
|
-
app.state.flag[
|
|
282
|
+
app.state.flag["show_relaunch_hint"] = False
|
|
273
283
|
|
|
274
284
|
|
|
275
285
|
@app.get("/api/info")
|
|
@@ -278,7 +288,7 @@ async def get_info():
|
|
|
278
288
|
Get the information of the current context.
|
|
279
289
|
"""
|
|
280
290
|
context = default_context()
|
|
281
|
-
demo = os.environ.get(
|
|
291
|
+
demo = os.environ.get("DEMO", False)
|
|
282
292
|
|
|
283
293
|
if demo:
|
|
284
294
|
state = context.export_demo_state()
|
|
@@ -296,25 +306,26 @@ async def get_info():
|
|
|
296
306
|
|
|
297
307
|
try:
|
|
298
308
|
info = {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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,
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
if context.adapter_type ==
|
|
322
|
+
if context.adapter_type == "sqlmesh":
|
|
313
323
|
from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
324
|
+
|
|
314
325
|
sqlmesh_adapter: SqlmeshAdapter = context.adapter
|
|
315
|
-
info[
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
info["sqlmesh"] = {
|
|
327
|
+
"base_env": sqlmesh_adapter.base_env.name,
|
|
328
|
+
"current_env": sqlmesh_adapter.curr_env.name,
|
|
318
329
|
}
|
|
319
330
|
|
|
320
331
|
return info
|
|
@@ -333,11 +344,12 @@ class CllOutput(BaseModel):
|
|
|
333
344
|
@app.post("/api/cll", response_model=CllOutput)
|
|
334
345
|
async def column_level_lineage_by_node(cll_input: CllIn):
|
|
335
346
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
347
|
+
|
|
336
348
|
dbt_adapter: DbtAdapter = default_context().adapter
|
|
337
349
|
|
|
338
350
|
try:
|
|
339
351
|
# TODO: Add support for by the node and column
|
|
340
|
-
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"))
|
|
341
353
|
except Exception as e:
|
|
342
354
|
raise HTTPException(status_code=400, detail=str(e))
|
|
343
355
|
|
|
@@ -348,7 +360,7 @@ class SelectNodesInput(BaseModel):
|
|
|
348
360
|
select: Optional[str] = None
|
|
349
361
|
exclude: Optional[str] = None
|
|
350
362
|
packages: Optional[list[str]] = None
|
|
351
|
-
view_mode: Optional[Literal[
|
|
363
|
+
view_mode: Optional[Literal["all", "changed_models"]] = None
|
|
352
364
|
|
|
353
365
|
|
|
354
366
|
class SelectNodesOutput(BaseModel):
|
|
@@ -359,8 +371,8 @@ class SelectNodesOutput(BaseModel):
|
|
|
359
371
|
async def select_nodes(input: SelectNodesInput):
|
|
360
372
|
context = default_context()
|
|
361
373
|
|
|
362
|
-
if context.adapter_type !=
|
|
363
|
-
raise HTTPException(status_code=400, detail=
|
|
374
|
+
if context.adapter_type != "dbt":
|
|
375
|
+
raise HTTPException(status_code=400, detail="Only dbt adapter is supported")
|
|
364
376
|
|
|
365
377
|
try:
|
|
366
378
|
nodes = context.adapter.select_nodes(
|
|
@@ -369,7 +381,7 @@ async def select_nodes(input: SelectNodesInput):
|
|
|
369
381
|
packages=input.packages,
|
|
370
382
|
view_mode=input.view_mode,
|
|
371
383
|
)
|
|
372
|
-
nodes = [node for node in nodes if not node.startswith(
|
|
384
|
+
nodes = [node for node in nodes if not node.startswith("test.")]
|
|
373
385
|
return SelectNodesOutput(nodes=nodes)
|
|
374
386
|
except Exception as e:
|
|
375
387
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -380,9 +392,9 @@ async def get_columns(model_id: str):
|
|
|
380
392
|
context = default_context()
|
|
381
393
|
try:
|
|
382
394
|
return {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
"model": {
|
|
396
|
+
"base": context.get_model(model_id, base=True),
|
|
397
|
+
"current": context.get_model(model_id, base=False),
|
|
386
398
|
}
|
|
387
399
|
}
|
|
388
400
|
except Exception as e:
|
|
@@ -397,12 +409,12 @@ async def save_handler():
|
|
|
397
409
|
try:
|
|
398
410
|
# Sync the state file
|
|
399
411
|
context = default_context()
|
|
400
|
-
log_api_event(
|
|
412
|
+
log_api_event("save", dict(state_loader_mode=context.state_loader_mode()))
|
|
401
413
|
state_loader = context.state_loader
|
|
402
414
|
if not state_loader.cloud_mode and state_loader.state_file is None:
|
|
403
|
-
raise RecceException(
|
|
415
|
+
raise RecceException("Not file mode or cloud mode")
|
|
404
416
|
|
|
405
|
-
context.sync_state(
|
|
417
|
+
context.sync_state("overwrite")
|
|
406
418
|
except RecceException as e:
|
|
407
419
|
raise HTTPException(status_code=400, detail=e.message)
|
|
408
420
|
|
|
@@ -418,33 +430,33 @@ def saveas_or_rename(input: SaveAsOrRenameInput, rename: bool = False):
|
|
|
418
430
|
context = default_context()
|
|
419
431
|
state_loader = context.state_loader
|
|
420
432
|
if state_loader.cloud_mode:
|
|
421
|
-
raise RecceException(
|
|
433
|
+
raise RecceException("Cloud mode does not support rename")
|
|
422
434
|
|
|
423
435
|
new_filename = input.filename
|
|
424
436
|
if os.path.dirname(new_filename):
|
|
425
|
-
raise RecceException(
|
|
426
|
-
if not new_filename.endswith(
|
|
427
|
-
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")
|
|
428
440
|
|
|
429
441
|
old_path = state_loader.state_file
|
|
430
442
|
if old_path:
|
|
431
443
|
old_dir = os.path.dirname(state_loader.state_file)
|
|
432
444
|
old_filename = os.path.basename(state_loader.state_file)
|
|
433
445
|
if old_filename == new_filename:
|
|
434
|
-
raise RecceException(
|
|
446
|
+
raise RecceException("The new filename is the same as the current filename")
|
|
435
447
|
new_path = os.path.join(old_dir, new_filename)
|
|
436
448
|
else:
|
|
437
449
|
new_path = new_filename
|
|
438
450
|
|
|
439
451
|
if os.path.exists(new_path):
|
|
440
452
|
if os.path.isdir(new_path):
|
|
441
|
-
raise HTTPException(status_code=400, detail=f
|
|
453
|
+
raise HTTPException(status_code=400, detail=f"The file {new_path} exists and is a directory")
|
|
442
454
|
|
|
443
455
|
if not input.overwrite:
|
|
444
|
-
raise HTTPException(status_code=409, detail=f
|
|
456
|
+
raise HTTPException(status_code=409, detail=f"The file {new_filename} already exists")
|
|
445
457
|
|
|
446
458
|
state_loader.state_file = new_path
|
|
447
|
-
context.sync_state(
|
|
459
|
+
context.sync_state("overwrite")
|
|
448
460
|
if rename and os.path.exists(old_path):
|
|
449
461
|
os.remove(old_path)
|
|
450
462
|
|
|
@@ -456,7 +468,7 @@ async def save_as_handler(input: SaveAsOrRenameInput):
|
|
|
456
468
|
"""
|
|
457
469
|
context = default_context()
|
|
458
470
|
try:
|
|
459
|
-
log_api_event(
|
|
471
|
+
log_api_event("saveas", dict(state_loader_mode=context.state_loader_mode()))
|
|
460
472
|
saveas_or_rename(input, rename=False)
|
|
461
473
|
except RecceException as e:
|
|
462
474
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -469,7 +481,7 @@ async def rename_handler(input: SaveAsOrRenameInput):
|
|
|
469
481
|
"""
|
|
470
482
|
context = default_context()
|
|
471
483
|
try:
|
|
472
|
-
log_api_event(
|
|
484
|
+
log_api_event("rename", dict(state_loader_mode=context.state_loader_mode()))
|
|
473
485
|
saveas_or_rename(input, rename=True)
|
|
474
486
|
except RecceException as e:
|
|
475
487
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -482,7 +494,7 @@ async def export_handler():
|
|
|
482
494
|
"""
|
|
483
495
|
context = default_context()
|
|
484
496
|
try:
|
|
485
|
-
log_api_event(
|
|
497
|
+
log_api_event("export", dict(state_loader_mode=context.state_loader_mode()))
|
|
486
498
|
return context.export_state().to_json()
|
|
487
499
|
except RecceException as e:
|
|
488
500
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -490,17 +502,16 @@ async def export_handler():
|
|
|
490
502
|
|
|
491
503
|
@app.post("/api/import", status_code=200)
|
|
492
504
|
async def import_handler(
|
|
493
|
-
file: Annotated[UploadFile, Form()],
|
|
494
|
-
checks_only: Annotated[bool, Form()],
|
|
495
|
-
background_tasks: BackgroundTasks
|
|
505
|
+
file: Annotated[UploadFile, Form()], checks_only: Annotated[bool, Form()], background_tasks: BackgroundTasks
|
|
496
506
|
):
|
|
497
507
|
"""
|
|
498
508
|
Import the recce state from the client.
|
|
499
509
|
"""
|
|
500
510
|
from recce.state import RecceState
|
|
511
|
+
|
|
501
512
|
context = default_context()
|
|
502
513
|
try:
|
|
503
|
-
log_api_event(
|
|
514
|
+
log_api_event("import", dict(state_loader_mode=context.state_loader_mode()))
|
|
504
515
|
content = await file.read()
|
|
505
516
|
state = RecceState.from_json(content)
|
|
506
517
|
|
|
@@ -534,16 +545,19 @@ async def sync_handler(input: SyncStateInput, response: Response, background_tas
|
|
|
534
545
|
context = default_context()
|
|
535
546
|
state_loader = context.state_loader
|
|
536
547
|
method = input.method
|
|
537
|
-
log_api_event(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
548
|
+
log_api_event(
|
|
549
|
+
"sync",
|
|
550
|
+
dict(
|
|
551
|
+
state_loader_mode=context.state_loader_mode(),
|
|
552
|
+
method=method,
|
|
553
|
+
),
|
|
554
|
+
)
|
|
541
555
|
|
|
542
556
|
if not method:
|
|
543
557
|
is_conflict = state_loader.check_conflict()
|
|
544
558
|
if is_conflict:
|
|
545
|
-
raise HTTPException(status_code=409, detail=
|
|
546
|
-
method =
|
|
559
|
+
raise HTTPException(status_code=409, detail="Conflict detected")
|
|
560
|
+
method = "overwrite"
|
|
547
561
|
|
|
548
562
|
is_syncing = state_loader.state_lock.locked()
|
|
549
563
|
if is_syncing:
|
|
@@ -593,7 +607,7 @@ async def share_state():
|
|
|
593
607
|
context = default_context()
|
|
594
608
|
state_loader = context.state_loader
|
|
595
609
|
|
|
596
|
-
file_name =
|
|
610
|
+
file_name = "recce_state.json"
|
|
597
611
|
if state_loader.state_file:
|
|
598
612
|
file_name = os.path.basename(state_loader.state_file)
|
|
599
613
|
|
|
@@ -629,8 +643,8 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
|
629
643
|
try:
|
|
630
644
|
while True:
|
|
631
645
|
data = await websocket.receive_text()
|
|
632
|
-
if data ==
|
|
633
|
-
await websocket.send_text(
|
|
646
|
+
if data == "ping":
|
|
647
|
+
await websocket.send_text("pong")
|
|
634
648
|
except WebSocketDisconnect:
|
|
635
649
|
clients.remove(websocket)
|
|
636
650
|
|
|
@@ -640,9 +654,9 @@ async def broadcast(data: str):
|
|
|
640
654
|
await client.send_text(data)
|
|
641
655
|
|
|
642
656
|
|
|
643
|
-
api_prefix =
|
|
657
|
+
api_prefix = "/api"
|
|
644
658
|
app.include_router(check_router, prefix=api_prefix)
|
|
645
659
|
app.include_router(run_router, prefix=api_prefix)
|
|
646
660
|
|
|
647
|
-
static_folder_path = Path(__file__).parent /
|
|
661
|
+
static_folder_path = Path(__file__).parent / "data"
|
|
648
662
|
app.mount("/", StaticFiles(directory=static_folder_path, html=True), name="static")
|