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.

Files changed (93) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +22 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +355 -316
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +39 -28
  8. recce/apis/check_func.py +33 -27
  9. recce/apis/run_api.py +25 -19
  10. recce/apis/run_func.py +29 -23
  11. recce/artifact.py +44 -49
  12. recce/cli.py +484 -285
  13. recce/config.py +42 -33
  14. recce/core.py +52 -44
  15. recce/data/404.html +1 -1
  16. recce/data/_next/static/chunks/{368-7587b306577df275.js → 778-aef312bffb4c0312.js} +15 -15
  17. recce/data/_next/static/chunks/8d700b6a.ed11a130057c7a47.js +1 -0
  18. recce/data/_next/static/chunks/app/layout-c713a2829d3279e4.js +1 -0
  19. recce/data/_next/static/chunks/app/page-7086764277331fcb.js +1 -0
  20. recce/data/_next/static/chunks/{cd9f8d63-cf0d5a7b0f7a92e8.js → cd9f8d63-e020f408095ed77c.js} +3 -3
  21. recce/data/_next/static/chunks/webpack-b787cb1a4f2293de.js +1 -0
  22. recce/data/_next/static/css/88b8abc134cfd59a.css +3 -0
  23. recce/data/index.html +2 -2
  24. recce/data/index.txt +2 -2
  25. recce/diff.py +6 -12
  26. recce/event/__init__.py +74 -72
  27. recce/event/collector.py +27 -20
  28. recce/event/track.py +39 -27
  29. recce/exceptions.py +1 -1
  30. recce/git.py +7 -7
  31. recce/github.py +57 -53
  32. recce/models/__init__.py +1 -1
  33. recce/models/check.py +6 -7
  34. recce/models/run.py +1 -0
  35. recce/models/types.py +27 -27
  36. recce/pull_request.py +26 -24
  37. recce/run.py +148 -111
  38. recce/server.py +105 -88
  39. recce/state.py +209 -177
  40. recce/summary.py +168 -143
  41. recce/tasks/__init__.py +3 -3
  42. recce/tasks/core.py +11 -13
  43. recce/tasks/dataframe.py +19 -17
  44. recce/tasks/histogram.py +69 -34
  45. recce/tasks/lineage.py +2 -2
  46. recce/tasks/profile.py +152 -86
  47. recce/tasks/query.py +139 -87
  48. recce/tasks/rowcount.py +33 -30
  49. recce/tasks/schema.py +14 -14
  50. recce/tasks/top_k.py +35 -35
  51. recce/tasks/valuediff.py +216 -152
  52. recce/util/breaking.py +77 -84
  53. recce/util/cll.py +55 -51
  54. recce/util/io.py +19 -17
  55. recce/util/logger.py +1 -1
  56. recce/util/recce_cloud.py +70 -72
  57. recce/util/singleton.py +4 -4
  58. recce/yaml/__init__.py +7 -10
  59. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/METADATA +5 -2
  60. recce_nightly-1.4.0.20250514.dist-info/RECORD +143 -0
  61. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/WHEEL +1 -1
  62. tests/adapter/dbt_adapter/conftest.py +1 -0
  63. tests/adapter/dbt_adapter/dbt_test_helper.py +28 -18
  64. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
  65. tests/adapter/dbt_adapter/test_dbt_cll.py +39 -32
  66. tests/adapter/dbt_adapter/test_selector.py +22 -21
  67. tests/tasks/test_histogram.py +58 -66
  68. tests/tasks/test_lineage.py +36 -23
  69. tests/tasks/test_preset_checks.py +45 -31
  70. tests/tasks/test_profile.py +340 -15
  71. tests/tasks/test_query.py +40 -40
  72. tests/tasks/test_row_count.py +65 -46
  73. tests/tasks/test_schema.py +65 -42
  74. tests/tasks/test_top_k.py +22 -18
  75. tests/tasks/test_valuediff.py +43 -32
  76. tests/test_cli.py +71 -58
  77. tests/test_config.py +7 -9
  78. tests/test_core.py +5 -3
  79. tests/test_dbt.py +7 -7
  80. tests/test_pull_request.py +1 -1
  81. tests/test_server.py +19 -13
  82. tests/test_state.py +40 -27
  83. tests/test_summary.py +18 -14
  84. recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
  85. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  86. recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
  87. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  88. recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
  89. /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → E_HPXsXdrqHg2YEHmU3mK}/_buildManifest.js +0 -0
  90. /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → E_HPXsXdrqHg2YEHmU3mK}/_ssgManifest.js +0 -0
  91. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/entry_points.txt +0 -0
  92. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.4.0.20250514.dist-info}/licenses/LICENSE +0 -0
  93. {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 Optional, Any, Set, Annotated, Literal, Dict
12
-
13
- from fastapi import FastAPI, HTTPException, Request, WebSocket, UploadFile, Response, BackgroundTasks, Form
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 ValidationError, BaseModel
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, __latest_version__
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 load_context, default_context, RecceContext
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 RecceStateLoader, RecceShareStateManager
40
+ from .state import RecceShareStateManager, RecceStateLoader
32
41
 
33
- logger = logging.getLogger('uvicorn')
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'[Configuration] The lifetime of the server is {app_state.lifetime} seconds')
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('config'))
87
+ config = RecceConfig(config_file=kwargs.get("config"))
77
88
  if state_loader.state is None:
78
- preset_checks = config.get('checks', [])
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
- log_load_state(command='server', single_env=single_env)
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 == 'server':
127
+ if app_state.command == "server":
116
128
  ctx = setup_server(app_state)
117
- elif app_state.command == 'read_only':
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 == 'server':
134
+ if app_state.command == "server":
123
135
  teardown_server(app_state, ctx)
124
- elif app_state.command == 'read_only':
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, 'r') as f:
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('Skip to refresh the artifacts because the file is not updated completely.')
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
- 'command': 'refresh',
155
- 'event': {
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
- 'command': 'relaunch',
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('recce_user_id')
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='recce_user_id', value=user_id)
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 ['/', '/index.html']:
216
- response.headers['Cache-Control'] = 'no-store'
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('read_only', False)
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('api_token')
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['show_onboarding_guide'] = False
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['show_relaunch_hint'] = False
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('DEMO', False)
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
- 'state_metadata': state_metadata,
297
- 'adapter_type': context.adapter_type,
298
- 'review_mode': context.review_mode,
299
- 'git': state.git.to_dict() if state.git else None,
300
- 'pull_request': state.pull_request.to_dict() if state.pull_request else None,
301
- 'lineage': lineage_diff,
302
- 'demo': bool(demo),
303
- 'cloud_mode': context.state_loader.cloud_mode,
304
- 'file_mode': context.state_loader.state_file is not None,
305
- 'filename': filename,
306
- 'support_tasks': support_tasks,
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 == 'sqlmesh':
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['sqlmesh'] = {
313
- 'base_env': sqlmesh_adapter.base_env.name,
314
- 'current_env': sqlmesh_adapter.curr_env.name,
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('node_id'))
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['all', 'changed_models']] = None
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 != 'dbt':
360
- raise HTTPException(status_code=400, detail='Only dbt adapter is supported')
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('test.')]
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
- 'model': {
381
- 'base': context.get_model(model_id, base=True),
382
- 'current': context.get_model(model_id, base=False)
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('save', dict(state_loader_mode=context.state_loader_mode()))
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('Not file mode or cloud mode')
415
+ raise RecceException("Not file mode or cloud mode")
401
416
 
402
- context.sync_state('overwrite')
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('Cloud mode does not support rename')
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('The new filename should not contain directory')
423
- if not new_filename.endswith('.json'):
424
- raise RecceException('The new filename should end with .json')
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('The new filename is the same as the current filename')
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'The file {new_path} exists and is a directory')
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'The file {new_filename} already exists')
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('overwrite')
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('saveas', dict(state_loader_mode=context.state_loader_mode()))
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('rename', dict(state_loader_mode=context.state_loader_mode()))
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('export', dict(state_loader_mode=context.state_loader_mode()))
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('import', dict(state_loader_mode=context.state_loader_mode()))
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('sync', dict(
535
- state_loader_mode=context.state_loader_mode(),
536
- method=method,
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='Conflict detected')
543
- method = 'overwrite'
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 = 'recce_state.json'
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 == 'ping':
630
- await websocket.send_text('pong')
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 = '/api'
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 / 'data'
661
+ static_folder_path = Path(__file__).parent / "data"
645
662
  app.mount("/", StaticFiles(directory=static_folder_path, html=True), name="static")