recce-nightly 1.3.0.20250507__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 +103 -89
  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 +147 -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.3.0.20250507.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.3.0.20250507.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-92f13c8fad9fae3d.js +0 -1
  87. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  88. recce_nightly-1.3.0.20250507.dist-info/RECORD +0 -142
  89. /recce/data/_next/static/{K5iKlCYhdcpq8Ea6ck9J_ → E_HPXsXdrqHg2YEHmU3mK}/_buildManifest.js +0 -0
  90. /recce/data/_next/static/{K5iKlCYhdcpq8Ea6ck9J_ → E_HPXsXdrqHg2YEHmU3mK}/_ssgManifest.js +0 -0
  91. {recce_nightly-1.3.0.20250507.dist-info → recce_nightly-1.4.0.20250514.dist-info}/entry_points.txt +0 -0
  92. {recce_nightly-1.3.0.20250507.dist-info → recce_nightly-1.4.0.20250514.dist-info}/licenses/LICENSE +0 -0
  93. {recce_nightly-1.3.0.20250507.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
 
@@ -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('read_only', False)
238
- single_env = flag.get('single_env_onboarding', False)
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('api_token')
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['show_onboarding_guide'] = False
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['show_relaunch_hint'] = False
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('DEMO', False)
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
- 'state_metadata': state_metadata,
300
- 'adapter_type': context.adapter_type,
301
- 'review_mode': context.review_mode,
302
- 'git': state.git.to_dict() if state.git else None,
303
- 'pull_request': state.pull_request.to_dict() if state.pull_request else None,
304
- 'lineage': lineage_diff,
305
- 'demo': bool(demo),
306
- 'cloud_mode': context.state_loader.cloud_mode,
307
- 'file_mode': context.state_loader.state_file is not None,
308
- 'filename': filename,
309
- '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,
310
320
  }
311
321
 
312
- if context.adapter_type == 'sqlmesh':
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['sqlmesh'] = {
316
- 'base_env': sqlmesh_adapter.base_env.name,
317
- '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,
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('node_id'))
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['all', 'changed_models']] = None
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 != 'dbt':
363
- 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")
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('test.')]
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
- 'model': {
384
- 'base': context.get_model(model_id, base=True),
385
- '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),
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('save', dict(state_loader_mode=context.state_loader_mode()))
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('Not file mode or cloud mode')
415
+ raise RecceException("Not file mode or cloud mode")
404
416
 
405
- context.sync_state('overwrite')
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('Cloud mode does not support rename')
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('The new filename should not contain directory')
426
- if not new_filename.endswith('.json'):
427
- 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")
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('The new filename is the same as the current filename')
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'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")
442
454
 
443
455
  if not input.overwrite:
444
- 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")
445
457
 
446
458
  state_loader.state_file = new_path
447
- context.sync_state('overwrite')
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('saveas', dict(state_loader_mode=context.state_loader_mode()))
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('rename', dict(state_loader_mode=context.state_loader_mode()))
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('export', dict(state_loader_mode=context.state_loader_mode()))
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('import', dict(state_loader_mode=context.state_loader_mode()))
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('sync', dict(
538
- state_loader_mode=context.state_loader_mode(),
539
- method=method,
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='Conflict detected')
546
- method = 'overwrite'
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 = 'recce_state.json'
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 == 'ping':
633
- await websocket.send_text('pong')
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 = '/api'
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 / 'data'
661
+ static_folder_path = Path(__file__).parent / "data"
648
662
  app.mount("/", StaticFiles(directory=static_folder_path, html=True), name="static")