setta 0.0.8.dev0__py3-none-any.whl → 0.0.9.dev0__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.
setta/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.0.8.dev0"
1
+ __version__ = "0.0.9.dev0"
@@ -549,7 +549,7 @@ class ExporterForInMemoryFn:
549
549
  C.LIST_ROOT,
550
550
  C.DICT_ROOT,
551
551
  C.GROUP,
552
- # C.IMAGE,
552
+ C.IMAGE,
553
553
  # C.CHART,
554
554
  C.DRAW,
555
555
  C.CHAT,
@@ -588,9 +588,11 @@ class ExporterForInMemoryFn:
588
588
  value[child_name] = self.export_section(
589
589
  c, f'{name}["{child_name}"]'
590
590
  )
591
- # elif type in [C.IMAGE, C.CHART]:
592
- # value = get_artifacts(self.p, id)
593
- # self.create_var_mapping((id, "artifact"), name)
591
+ elif type in [C.IMAGE]:
592
+ artifacts = get_artifacts(self.p, id)
593
+ img = artifacts[0]["value"] if len(artifacts) > 0 else None
594
+ value = {"image": img}
595
+ self.create_var_mapping((id, "image"), f'{name}["image"]')
594
596
  elif type == C.DRAW:
595
597
  value = {"drawing": get_drawing(self.p, id)}
596
598
  self.create_var_mapping((id, "drawing"), f'{name}["drawing"]')
@@ -210,7 +210,11 @@ def load_json_sources_into_data_structures(
210
210
  if v["jsonSource"] and ((not section_ids) or k in section_ids)
211
211
  }
212
212
  for s in sections.values():
213
+ logger.debug(
214
+ f'Attempting to read {s["jsonSource"]} with keys {s["jsonSourceKeys"]}'
215
+ )
213
216
  new_data = load_json_source(s["jsonSource"], s["jsonSourceKeys"])
217
+ logger.debug(f"Loaded json: {new_data}")
214
218
  for filename, data in new_data.items():
215
219
  codeInfo.update(data["codeInfo"])
216
220
  variantId = None
@@ -244,6 +248,9 @@ def load_json_sources_into_data_structures(
244
248
  for s in sections.values():
245
249
  for vid in s["variantIds"]:
246
250
  if sectionVariants[vid]["name"] not in filenames_loaded:
251
+ logger.debug(
252
+ f'Removing variant {sectionVariants[vid]["name"]} because the associated json was not found'
253
+ )
247
254
  to_delete.append(vid)
248
255
 
249
256
  for vid in to_delete:
@@ -255,14 +262,22 @@ def load_json_sources_into_data_structures(
255
262
  s["jsonSourceMissing"] = False
256
263
  s["variantIds"] = [v for v in s["variantIds"] if v in sectionVariants]
257
264
  if len(s["variantIds"]) == 0:
265
+ logger.debug("Section has no variantIds. Creating new section variant.")
258
266
  variantId, variant = new_section_variant()
259
- s["variantIds"].append(variantId)
260
267
  sectionVariants[variantId] = variant
268
+ s["variantId"] = variantId
269
+ s["variantIds"].append(variantId)
261
270
  s["jsonSourceMissing"] = True
262
271
  elif s["variantId"] not in s["variantIds"]:
272
+ logger.debug(
273
+ "Selected variantId is not in list of variantIds. Changing selected variantId"
274
+ )
263
275
  s["variantId"] = s["variantIds"][0]
264
276
 
265
277
  if s["defaultVariantId"] not in s["variantIds"]:
278
+ logger.debug(
279
+ "Default variantId is not in list of variantIds. Changing default variantId"
280
+ )
266
281
  s["defaultVariantId"] = s["variantId"]
267
282
 
268
283
 
setta/lsp/file_watcher.py CHANGED
@@ -53,22 +53,6 @@ class LSPFileWatcher:
53
53
  self.observer.stop()
54
54
  self.observer.join()
55
55
 
56
- def is_watching(self, path: str) -> bool:
57
- """
58
- Check if a path is being watched.
59
-
60
- Args:
61
- path: Path to check
62
-
63
- Returns:
64
- bool: True if the path is being watched
65
- """
66
- absolute_path = os.path.abspath(path)
67
- return any(
68
- absolute_path.startswith(watched_path)
69
- for watched_path in self.watched_paths
70
- )
71
-
72
56
 
73
57
  class LSPEventHandler(FileSystemEventHandler):
74
58
  def __init__(self, callback, loop):
@@ -0,0 +1,249 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ from typing import Dict, List, Set
5
+
6
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
7
+ from watchdog.observers import Observer
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SpecificFileWatcher:
13
+ """File watcher that monitors specific files and notifies via a single callback."""
14
+
15
+ def __init__(self, callback):
16
+ """
17
+ Initialize the file watcher for specific files.
18
+
19
+ Args:
20
+ callback: Function to call when any watched file changes
21
+ Callback receives (file_path, event_type) where event_type is
22
+ one of 'created', 'modified', 'deleted', or 'moved'
23
+ """
24
+ self.observer = Observer()
25
+ self.watched_files: Set[str] = set()
26
+ self.handler = SpecificFileEventHandler(
27
+ callback, asyncio.get_event_loop(), self.watched_files
28
+ )
29
+
30
+ def add_file(self, file_path: str) -> bool:
31
+ """
32
+ Add a specific file to watch.
33
+
34
+ Args:
35
+ file_path: Absolute path to the file to watch
36
+
37
+ Returns:
38
+ bool: True if the file was added, False if it doesn't exist
39
+ """
40
+ if not os.path.exists(file_path):
41
+ return False
42
+
43
+ absolute_path = os.path.abspath(file_path)
44
+ dir_path = os.path.dirname(absolute_path)
45
+
46
+ # Add file to watched files if not already there
47
+ if absolute_path not in self.watched_files:
48
+ self.watched_files.add(absolute_path)
49
+ else:
50
+ # Already watching this file
51
+ return True
52
+
53
+ # Schedule the directory for watching if needed
54
+ if not self.observer.emitters:
55
+ # No directories are being watched yet
56
+ self.observer.schedule(self.handler, dir_path, recursive=False)
57
+ else:
58
+ # Check if the directory is already being watched
59
+ is_dir_watched = False
60
+ for emitter in self.observer.emitters:
61
+ if emitter.watch.path == dir_path:
62
+ is_dir_watched = True
63
+ break
64
+
65
+ if not is_dir_watched:
66
+ self.observer.schedule(self.handler, dir_path, recursive=False)
67
+
68
+ return True
69
+
70
+ def remove_file(self, file_path: str) -> None:
71
+ """
72
+ Remove a file from being watched.
73
+
74
+ Args:
75
+ file_path: Path to the file
76
+ """
77
+ absolute_path = os.path.abspath(file_path)
78
+
79
+ if absolute_path in self.watched_files:
80
+ self.watched_files.remove(absolute_path)
81
+
82
+ def update_watch_list(self, file_paths: List[str]) -> Dict[str, List[str]]:
83
+ """
84
+ Update the entire list of files being watched with a single function call.
85
+ This efficiently handles adding new files and removing files that are no longer needed.
86
+
87
+ Args:
88
+ file_paths: List of file paths that should be watched
89
+
90
+ Returns:
91
+ Dict containing 'added' and 'removed' lists of file paths
92
+ """
93
+ # Convert all input paths to absolute paths (only if they exist)
94
+ absolute_paths = [
95
+ os.path.abspath(path) for path in file_paths if os.path.exists(path)
96
+ ]
97
+ absolute_paths_set = set(absolute_paths)
98
+
99
+ # Calculate differences
100
+ files_to_add = absolute_paths_set - self.watched_files
101
+ files_to_remove = self.watched_files - absolute_paths_set
102
+
103
+ # Track which files were successfully added
104
+ added_files = []
105
+
106
+ # Add new files
107
+ for file_path in files_to_add:
108
+ success = self.add_file(file_path)
109
+ if success:
110
+ added_files.append(file_path)
111
+
112
+ # Remove files no longer in the list
113
+ for file_path in files_to_remove:
114
+ self.remove_file(file_path)
115
+
116
+ # Return information about what changed
117
+ return {"added": added_files, "removed": list(files_to_remove)}
118
+
119
+ def start(self) -> None:
120
+ """Start the file watcher."""
121
+ self.observer.start()
122
+ self.started = True
123
+
124
+ def stop(self) -> None:
125
+ """Stop the file watcher."""
126
+ self.observer.stop()
127
+ self.observer.join()
128
+ self.started = False
129
+
130
+
131
+ class SpecificFileEventHandler(FileSystemEventHandler):
132
+ """Event handler for specific file events."""
133
+
134
+ def __init__(self, callback, loop, watched_files_ref):
135
+ """
136
+ Initialize the event handler.
137
+
138
+ Args:
139
+ callback: Function to call when a watched file changes
140
+ loop: Asyncio event loop for async callbacks
141
+ watched_files_ref: Reference to the set of files being watched
142
+ """
143
+ self.callback = callback
144
+ self.loop = loop
145
+ self.watched_files_ref = watched_files_ref
146
+
147
+ def on_created(self, event: FileSystemEvent):
148
+ """Handle file creation event."""
149
+ if not event.is_directory:
150
+ file_path = os.path.abspath(event.src_path)
151
+ if file_path in self.watched_files_ref:
152
+ self._send_event(event, "created")
153
+
154
+ def on_modified(self, event: FileSystemEvent):
155
+ """Handle file modification event."""
156
+ if not event.is_directory:
157
+ file_path = os.path.abspath(event.src_path)
158
+ if file_path in self.watched_files_ref:
159
+ self._send_event(event, "modified")
160
+
161
+ def on_deleted(self, event: FileSystemEvent):
162
+ """Handle file deletion event."""
163
+ if not event.is_directory:
164
+ file_path = os.path.abspath(event.src_path)
165
+ if file_path in self.watched_files_ref:
166
+ self._send_event(event, "deleted")
167
+
168
+ def on_moved(self, event: FileSystemEvent):
169
+ """Handle file move event."""
170
+ if not event.is_directory:
171
+ # For moved events, we need to check both source and destination
172
+ src_path = os.path.abspath(event.src_path)
173
+ dest_path = (
174
+ os.path.abspath(event.dest_path)
175
+ if hasattr(event, "dest_path")
176
+ else None
177
+ )
178
+
179
+ # If the source was being watched, notify about the move
180
+ if src_path in self.watched_files_ref:
181
+ self._send_event(event, "moved")
182
+
183
+ # If the destination is also being watched, notify about modification
184
+ if (
185
+ dest_path
186
+ and dest_path in self.watched_files_ref
187
+ and dest_path != src_path
188
+ ):
189
+ # Create a modified event for the destination
190
+ self._send_event(event, "modified")
191
+
192
+ def _send_event(self, event: FileSystemEvent, event_type: str):
193
+ """
194
+ Process and send the event to the callback.
195
+
196
+ Args:
197
+ event: The file system event
198
+ event_type: The type of event ('created', 'modified', 'deleted', 'moved')
199
+ """
200
+ logger.debug("_send_event")
201
+ # Get absolute path
202
+ abs_path = os.path.abspath(event.src_path)
203
+
204
+ # Get relative path to current working directory
205
+ rel_path = os.path.relpath(abs_path)
206
+
207
+ # Get file contents for created and modified events
208
+ file_content = None
209
+ if event_type in ("created", "modified"):
210
+ try:
211
+ with open(abs_path, "r", encoding="utf-8") as f:
212
+ file_content = f.read()
213
+ except Exception as e:
214
+ logger.debug(f"Error reading file {abs_path}: {e}")
215
+ file_content = None
216
+
217
+ # For moved events, get the content of the destination file
218
+ dest_abs_path = None
219
+ dest_rel_path = None
220
+ if event_type == "moved" and hasattr(event, "dest_path"):
221
+ dest_abs_path = os.path.abspath(event.dest_path)
222
+ dest_rel_path = os.path.relpath(dest_abs_path)
223
+ try:
224
+ with open(dest_abs_path, "r", encoding="utf-8") as f:
225
+ file_content = f.read()
226
+ except Exception as e:
227
+ logger.debug(f"Error reading destination file {dest_abs_path}: {e}")
228
+ file_content = None
229
+
230
+ # Prepare event info object
231
+ event_info = {
232
+ "absPath": abs_path,
233
+ "relPath": rel_path,
234
+ "eventType": event_type,
235
+ "fileContent": file_content,
236
+ }
237
+
238
+ # Add destination paths for moved events
239
+ if event_type == "moved" and dest_abs_path:
240
+ event_info["destAbsPath"] = dest_abs_path
241
+ event_info["destRelPath"] = dest_rel_path
242
+
243
+ logger.debug(f"will send {event_info}")
244
+ if asyncio.iscoroutinefunction(self.callback):
245
+ self.loop.call_soon_threadsafe(
246
+ lambda: self.loop.create_task(self.callback(event_info))
247
+ )
248
+ else:
249
+ self.callback(event_info)
setta/lsp/utils.py CHANGED
@@ -1,8 +1,15 @@
1
+ import logging
2
+
3
+ from setta.utils.constants import C
4
+
1
5
  from .file_watcher import LSPFileWatcher
2
6
  from .reader import LanguageServerReader
3
7
  from .server import LanguageServer
8
+ from .specific_file_watcher import SpecificFileWatcher
4
9
  from .writer import LanguageServerWriter
5
10
 
11
+ logger = logging.getLogger(__name__)
12
+
6
13
 
7
14
  def create_lsps(
8
15
  workspace_folder,
@@ -42,6 +49,19 @@ def create_file_watcher(lsps, lsp_writers):
42
49
  return file_watcher
43
50
 
44
51
 
52
+ def create_specific_file_watcher(websocket_manager):
53
+ async def callback(event_info):
54
+ logger.debug(f"callback {event_info}")
55
+ await websocket_manager.broadcast(
56
+ {
57
+ "content": event_info,
58
+ "messageType": C.WS_SPECIFIC_FILE_WATCHER_UPDATE,
59
+ }
60
+ )
61
+
62
+ return SpecificFileWatcher(callback)
63
+
64
+
45
65
  async def start_lsps(lsps, lsp_readers, lsp_writers):
46
66
  for k, v in lsps.items():
47
67
  await v.start_server()
@@ -47,3 +47,7 @@ def get_lsp_writers(request: Request):
47
47
 
48
48
  def get_lsp_writers_from_websocket(websocket: WebSocket):
49
49
  return websocket.app.state.lsp_writers
50
+
51
+
52
+ def get_specific_file_watcher(request: Request):
53
+ return request.app.state.specific_file_watcher
setta/routers/sections.py CHANGED
@@ -2,7 +2,7 @@ import os
2
2
  from pathlib import Path
3
3
  from typing import Dict, List, Optional
4
4
 
5
- from fastapi import APIRouter, HTTPException, status
5
+ from fastapi import APIRouter, Depends, HTTPException, status
6
6
  from pydantic import BaseModel
7
7
 
8
8
  from setta.code_gen.export_selected import (
@@ -21,6 +21,7 @@ from setta.database.db.sections.copy import (
21
21
  )
22
22
  from setta.database.db.sections.jsonSource import save_json_source_data
23
23
  from setta.database.db.sections.load import load_json_sources_into_data_structures
24
+ from setta.routers.dependencies import get_specific_file_watcher
24
25
  from setta.utils.constants import C
25
26
  from setta.utils.generate_new_filename import generate_new_filename
26
27
 
@@ -75,6 +76,10 @@ class DeleteFileRequest(BaseModel):
75
76
  filepath: str
76
77
 
77
78
 
79
+ class FileWatchListRequest(BaseModel):
80
+ filepaths: List[str]
81
+
82
+
78
83
  @router.post(C.ROUTE_COPY_SECTIONS)
79
84
  def route_sections_make_copy(x: SectionsMakeCopyRequest):
80
85
  output = copy_sections_and_other_info(x.sectionsAndOtherInfo)
@@ -172,3 +177,11 @@ def route_delete_file(x: DeleteFileRequest):
172
177
  status_code=status.HTTP_404_NOT_FOUND,
173
178
  detail=f"Failed to delete file: {str(e)}",
174
179
  )
180
+
181
+
182
+ @router.post(C.ROUTE_FILE_WATCH_LIST)
183
+ def route_file_watch_list(
184
+ x: FileWatchListRequest, specific_file_watcher=Depends(get_specific_file_watcher)
185
+ ):
186
+ # x.filepaths is the current list of file paths that should be watched
187
+ specific_file_watcher.update_watch_list(x.filepaths)
setta/server.py CHANGED
@@ -16,6 +16,7 @@ from setta.lsp.utils import (
16
16
  create_lsp_readers,
17
17
  create_lsp_writers,
18
18
  create_lsps,
19
+ create_specific_file_watcher,
19
20
  kill_lsps,
20
21
  start_lsps,
21
22
  )
@@ -52,6 +53,9 @@ async def lifespan(app: FastAPI):
52
53
  app.state.file_watcher = create_file_watcher(app.state.lsps, app.state.lsp_writers)
53
54
  app.state.terminal_websockets = TerminalWebsockets()
54
55
  app.state.websocket_manager = WebsocketManager()
56
+ app.state.specific_file_watcher = create_specific_file_watcher(
57
+ app.state.websocket_manager
58
+ )
55
59
  app.state.tasks = Tasks(app.state.lsp_writers)
56
60
  app.state.lsp_readers = create_lsp_readers(
57
61
  app.state.lsps, app.state.websocket_manager
@@ -73,6 +77,7 @@ async def lifespan(app: FastAPI):
73
77
  app.state.lsp_writers,
74
78
  )
75
79
  app.state.file_watcher.start()
80
+ app.state.specific_file_watcher.start()
76
81
 
77
82
  if not is_dev_mode():
78
83
  # Mount the 'frontend/dist' directory at '/static'
@@ -98,6 +103,7 @@ async def lifespan(app: FastAPI):
98
103
  finally:
99
104
  app.state.tasks.close()
100
105
  app.state.file_watcher.stop()
106
+ app.state.specific_file_watcher.stop()
101
107
  await kill_lsps(app.state.lsps, app.state.lsp_readers)
102
108
 
103
109
 
@@ -93,6 +93,7 @@
93
93
  "ROUTE_CHECK_IF_FILE_EXISTS": "/checkIfFileExists",
94
94
  "ROUTE_LOAD_ARTIFACT_FROM_DISK": "/loadArtifactFromDisk",
95
95
  "ROUTE_RESTART_LANGUAGE_SERVER": "/restartLanguageServer",
96
+ "ROUTE_FILE_WATCH_LIST": "/fileWatchList",
96
97
  "JSON_SOURCE_PREFIX": "JSON-",
97
98
  "NESTED_PARAM": "NESTED_PARAM",
98
99
  "ARGS_PREFIX": "__",
@@ -105,6 +106,7 @@
105
106
  "WS_TERMINAL_RESIZE": "terminalResize",
106
107
  "WS_LSP_STATUS": "lspStatus",
107
108
  "WS_IN_MEMORY_FN_AVG_RUN_TIME": "inMemoryFnAvgRunTime",
109
+ "WS_SPECIFIC_FILE_WATCHER_UPDATE": "specificFileWatcherUpdate",
108
110
  "SETTA_GENERATED_PYTHON": "SETTA_GENERATED_PYTHON",
109
111
  "SETTA_GENERATED_PYTHON_IMPORTS": "SETTA_GENERATED_PYTHON_IMPORTS",
110
112
  "TEMPLATE_VAR_IMPORT_PATH_SUFFIX": "import_path",