setta 0.0.8.dev0__py3-none-any.whl → 0.0.9__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.
Files changed (33) hide show
  1. setta/__init__.py +1 -1
  2. setta/code_gen/export_selected.py +6 -4
  3. setta/code_gen/python/generate_code.py +2 -0
  4. setta/database/db/codeInfo/copy.py +1 -4
  5. setta/database/db/projects/saveAs.py +0 -3
  6. setta/database/db/projects/utils.py +2 -2
  7. setta/database/db/sectionVariants/copy.py +12 -3
  8. setta/database/db/sections/copy.py +4 -1
  9. setta/database/db/sections/jsonSource.py +109 -42
  10. setta/database/db/sections/load.py +145 -73
  11. setta/database/settings_file.py +1 -1
  12. setta/lsp/file_watcher.py +0 -16
  13. setta/lsp/specific_file_watcher.py +278 -0
  14. setta/lsp/utils.py +20 -0
  15. setta/routers/dependencies.py +4 -0
  16. setta/routers/projects.py +1 -1
  17. setta/routers/sections.py +28 -5
  18. setta/server.py +6 -0
  19. setta/static/constants/constants.json +3 -1
  20. setta/static/constants/defaultValues.json +3 -2
  21. setta/static/constants/settingsProject.json +200 -29
  22. setta/static/frontend/assets/{index-c90491bb.js → index-0693b9a1.js} +165 -165
  23. setta/static/frontend/assets/index-cf887608.css +32 -0
  24. setta/static/frontend/index.html +2 -2
  25. setta/utils/constants.py +1 -4
  26. setta/utils/websocket_manager.py +5 -0
  27. {setta-0.0.8.dev0.dist-info → setta-0.0.9.dist-info}/METADATA +11 -6
  28. {setta-0.0.8.dev0.dist-info → setta-0.0.9.dist-info}/RECORD +32 -31
  29. {setta-0.0.8.dev0.dist-info → setta-0.0.9.dist-info}/WHEEL +1 -1
  30. setta/static/frontend/assets/index-3a6274db.css +0 -32
  31. {setta-0.0.8.dev0.dist-info → setta-0.0.9.dist-info}/LICENSE +0 -0
  32. {setta-0.0.8.dev0.dist-info → setta-0.0.9.dist-info}/entry_points.txt +0 -0
  33. {setta-0.0.8.dev0.dist-info → setta-0.0.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,278 @@
1
+ import asyncio
2
+ import glob
3
+ import logging
4
+ import os
5
+ from typing import Dict, List, Set
6
+
7
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
8
+ from watchdog.observers import Observer
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SpecificFileWatcher:
14
+ """File watcher that monitors specific files and notifies via a single callback."""
15
+
16
+ def __init__(self, callback):
17
+ """
18
+ Initialize the file watcher for specific files.
19
+
20
+ Args:
21
+ callback: Function to call when any watched file changes
22
+ Callback receives (file_path, event_type) where event_type is
23
+ one of 'created', 'modified', 'deleted', or 'moved'
24
+ """
25
+ self.observer = Observer()
26
+ self.watched_files: Set[str] = set()
27
+ self.file_to_patterns = {}
28
+ self.handler = SpecificFileEventHandler(
29
+ callback,
30
+ asyncio.get_event_loop(),
31
+ self.watched_files,
32
+ self.file_to_patterns,
33
+ )
34
+
35
+ def add_file(self, file_path: str) -> bool:
36
+ """
37
+ Add a specific file to watch.
38
+
39
+ Args:
40
+ file_path: Absolute path to the file to watch
41
+
42
+ Returns:
43
+ bool: True if the file was added, False if it doesn't exist
44
+ """
45
+ if not os.path.exists(file_path):
46
+ return False
47
+
48
+ absolute_path = os.path.abspath(file_path)
49
+ dir_path = os.path.dirname(absolute_path)
50
+
51
+ # Add file to watched files if not already there
52
+ if absolute_path not in self.watched_files:
53
+ self.watched_files.add(absolute_path)
54
+ else:
55
+ # Already watching this file
56
+ return True
57
+
58
+ # Schedule the directory for watching if needed
59
+ if not self.observer.emitters:
60
+ # No directories are being watched yet
61
+ self.observer.schedule(self.handler, dir_path, recursive=False)
62
+ else:
63
+ # Check if the directory is already being watched
64
+ is_dir_watched = False
65
+ for emitter in self.observer.emitters:
66
+ if emitter.watch.path == dir_path:
67
+ is_dir_watched = True
68
+ break
69
+
70
+ if not is_dir_watched:
71
+ self.observer.schedule(self.handler, dir_path, recursive=False)
72
+
73
+ return True
74
+
75
+ def remove_file(self, file_path: str) -> None:
76
+ """
77
+ Remove a file from being watched.
78
+
79
+ Args:
80
+ file_path: Path to the file
81
+ """
82
+ absolute_path = os.path.abspath(file_path)
83
+
84
+ if absolute_path in self.watched_files:
85
+ self.watched_files.remove(absolute_path)
86
+
87
+ def update_watch_list(
88
+ self, filepaths_and_glob_patterns: List[str]
89
+ ) -> Dict[str, List[str]]:
90
+ """
91
+ Update the entire list of files being watched with a single function call.
92
+ This efficiently handles adding new files and removing files that are no longer needed.
93
+
94
+ Args:
95
+ filepaths_and_glob_patterns: List of file paths that should be watched
96
+
97
+ Returns:
98
+ Dict containing 'added' and 'removed' lists of file paths
99
+ """
100
+ file_paths = self.get_actual_file_paths_to_watch(filepaths_and_glob_patterns)
101
+
102
+ # Convert all input paths to absolute paths (only if they exist)
103
+ absolute_paths = [
104
+ os.path.abspath(path) for path in file_paths if os.path.exists(path)
105
+ ]
106
+ absolute_paths_set = set(absolute_paths)
107
+
108
+ # Calculate differences
109
+ files_to_add = absolute_paths_set - self.watched_files
110
+ files_to_remove = self.watched_files - absolute_paths_set
111
+
112
+ # Track which files were successfully added
113
+ added_files = []
114
+
115
+ # Add new files
116
+ for file_path in files_to_add:
117
+ success = self.add_file(file_path)
118
+ if success:
119
+ added_files.append(file_path)
120
+
121
+ # Remove files no longer in the list
122
+ for file_path in files_to_remove:
123
+ self.remove_file(file_path)
124
+
125
+ # Return information about what changed
126
+ return {"added": added_files, "removed": list(files_to_remove)}
127
+
128
+ def get_actual_file_paths_to_watch(self, filepaths_and_glob_patterns):
129
+ actual_filepaths = set()
130
+ # Expand glob patterns and build the mapping
131
+ for pattern in filepaths_and_glob_patterns:
132
+ matching_files = glob.glob(pattern)
133
+ for file_path in matching_files:
134
+ # Only add actual files, not directories
135
+ if os.path.isfile(file_path):
136
+ abs_path = os.path.abspath(file_path)
137
+ actual_filepaths.add(abs_path)
138
+
139
+ # Add to the mapping
140
+ if abs_path not in self.file_to_patterns:
141
+ self.file_to_patterns[abs_path] = set()
142
+ self.file_to_patterns[abs_path].add(pattern)
143
+
144
+ return actual_filepaths
145
+
146
+ def start(self) -> None:
147
+ """Start the file watcher."""
148
+ self.observer.start()
149
+ self.started = True
150
+
151
+ def stop(self) -> None:
152
+ """Stop the file watcher."""
153
+ self.observer.stop()
154
+ self.observer.join()
155
+ self.started = False
156
+
157
+
158
+ class SpecificFileEventHandler(FileSystemEventHandler):
159
+ """Event handler for specific file events."""
160
+
161
+ def __init__(self, callback, loop, watched_files_ref, file_to_patterns):
162
+ """
163
+ Initialize the event handler.
164
+
165
+ Args:
166
+ callback: Function to call when a watched file changes
167
+ loop: Asyncio event loop for async callbacks
168
+ watched_files_ref: Reference to the set of files being watched
169
+ """
170
+ self.callback = callback
171
+ self.loop = loop
172
+ self.watched_files_ref = watched_files_ref
173
+ self.file_to_patterns = file_to_patterns
174
+
175
+ def on_created(self, event: FileSystemEvent):
176
+ """Handle file creation event."""
177
+ if not event.is_directory:
178
+ file_path = os.path.abspath(event.src_path)
179
+ if file_path in self.watched_files_ref:
180
+ self._send_event(event, "created")
181
+
182
+ def on_modified(self, event: FileSystemEvent):
183
+ """Handle file modification event."""
184
+ if not event.is_directory:
185
+ file_path = os.path.abspath(event.src_path)
186
+ if file_path in self.watched_files_ref:
187
+ self._send_event(event, "modified")
188
+
189
+ def on_deleted(self, event: FileSystemEvent):
190
+ """Handle file deletion event."""
191
+ if not event.is_directory:
192
+ file_path = os.path.abspath(event.src_path)
193
+ if file_path in self.watched_files_ref:
194
+ self._send_event(event, "deleted")
195
+
196
+ def on_moved(self, event: FileSystemEvent):
197
+ """Handle file move event."""
198
+ if not event.is_directory:
199
+ # For moved events, we need to check both source and destination
200
+ src_path = os.path.abspath(event.src_path)
201
+ dest_path = (
202
+ os.path.abspath(event.dest_path)
203
+ if hasattr(event, "dest_path")
204
+ else None
205
+ )
206
+
207
+ # If the source was being watched, notify about the move
208
+ if src_path in self.watched_files_ref:
209
+ self._send_event(event, "moved")
210
+
211
+ # If the destination is also being watched, notify about modification
212
+ if (
213
+ dest_path
214
+ and dest_path in self.watched_files_ref
215
+ and dest_path != src_path
216
+ ):
217
+ # Create a modified event for the destination
218
+ self._send_event(event, "modified")
219
+
220
+ def _send_event(self, event: FileSystemEvent, event_type: str):
221
+ """
222
+ Process and send the event to the callback.
223
+
224
+ Args:
225
+ event: The file system event
226
+ event_type: The type of event ('created', 'modified', 'deleted', 'moved')
227
+ """
228
+ logger.debug("_send_event")
229
+ # Get absolute path
230
+ abs_path = os.path.abspath(event.src_path)
231
+
232
+ # Get relative path to current working directory
233
+ rel_path = os.path.relpath(abs_path)
234
+
235
+ # Get file contents for created and modified events
236
+ file_content = None
237
+ if event_type in ("created", "modified"):
238
+ try:
239
+ with open(abs_path, "r", encoding="utf-8") as f:
240
+ file_content = f.read()
241
+ except Exception as e:
242
+ logger.debug(f"Error reading file {abs_path}: {e}")
243
+ file_content = None
244
+
245
+ # For moved events, get the content of the destination file
246
+ dest_abs_path = None
247
+ dest_rel_path = None
248
+ if event_type == "moved" and hasattr(event, "dest_path"):
249
+ dest_abs_path = os.path.abspath(event.dest_path)
250
+ dest_rel_path = os.path.relpath(dest_abs_path)
251
+ try:
252
+ with open(dest_abs_path, "r", encoding="utf-8") as f:
253
+ file_content = f.read()
254
+ except Exception as e:
255
+ logger.debug(f"Error reading destination file {dest_abs_path}: {e}")
256
+ file_content = None
257
+
258
+ # Prepare event info object
259
+ event_info = {
260
+ "absPath": abs_path,
261
+ "relPath": rel_path,
262
+ "eventType": event_type,
263
+ "fileContent": file_content,
264
+ "matchingGlobPatterns": list(self.file_to_patterns[abs_path]),
265
+ }
266
+
267
+ # Add destination paths for moved events
268
+ if event_type == "moved" and dest_abs_path:
269
+ event_info["destAbsPath"] = dest_abs_path
270
+ event_info["destRelPath"] = dest_rel_path
271
+
272
+ logger.debug(f"will send {event_info}")
273
+ if asyncio.iscoroutinefunction(self.callback):
274
+ self.loop.call_soon_threadsafe(
275
+ lambda: self.loop.create_task(self.callback(event_info))
276
+ )
277
+ else:
278
+ 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/projects.py CHANGED
@@ -178,7 +178,7 @@ def router_set_as_default_project(x: SetAsDefaultProjectRequest, dbq=Depends(get
178
178
 
179
179
  @router.post(C.ROUTE_FILTER_DATA_FOR_JSON_EXPORT)
180
180
  def router_filter_data_for_json_export(x: FilterDataForJSONExportRequest):
181
- filter_data_for_json_export(x.project, keepCodeInfoThatHaveUITypes=True)
181
+ filter_data_for_json_export(x.project)
182
182
  return x.project
183
183
 
184
184
 
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
 
@@ -53,8 +54,7 @@ class GlobalParamSweepSectionToYamlRequest(BaseModel):
53
54
 
54
55
  class LoadSectionJSONSourceRequest(BaseModel):
55
56
  project: dict
56
- sectionId: str
57
- jsonSource: str
57
+ sectionIdToJSONSource: Dict[str, str]
58
58
 
59
59
 
60
60
  class SaveSectionJSONSourceRequest(BaseModel):
@@ -67,6 +67,10 @@ class NewJSONVersionNameRequest(BaseModel):
67
67
  filenameGlob: str
68
68
 
69
69
 
70
+ class CreateFileRequest(BaseModel):
71
+ filepath: str
72
+
73
+
70
74
  class GetJSONSourcePathToBeDeleted(BaseModel):
71
75
  variantName: str
72
76
 
@@ -75,6 +79,10 @@ class DeleteFileRequest(BaseModel):
75
79
  filepath: str
76
80
 
77
81
 
82
+ class FileWatchListRequest(BaseModel):
83
+ filepaths: List[str]
84
+
85
+
78
86
  @router.post(C.ROUTE_COPY_SECTIONS)
79
87
  def route_sections_make_copy(x: SectionsMakeCopyRequest):
80
88
  output = copy_sections_and_other_info(x.sectionsAndOtherInfo)
@@ -126,13 +134,14 @@ def route_global_param_sweep_section_to_yaml(x: GlobalParamSweepSectionToYamlReq
126
134
  @router.post(C.ROUTE_LOAD_SECTION_JSON_SOURCE)
127
135
  def route_load_section_json_source(x: LoadSectionJSONSourceRequest):
128
136
  p = x.project
129
- p["sections"][x.sectionId]["jsonSource"] = x.jsonSource
137
+ for k, v in x.sectionIdToJSONSource.items():
138
+ p["sections"][k]["jsonSource"] = v
130
139
  load_json_sources_into_data_structures(
131
140
  p["sections"],
132
141
  p["codeInfo"],
133
142
  p["codeInfoCols"],
134
143
  p["sectionVariants"],
135
- section_ids=[x.sectionId],
144
+ section_ids=list(x.sectionIdToJSONSource.keys()),
136
145
  )
137
146
  return {"project": p}
138
147
 
@@ -145,6 +154,12 @@ def route_new_json_version_name(x: NewJSONVersionNameRequest):
145
154
  return new_filename
146
155
 
147
156
 
157
+ @router.post(C.ROUTE_CREATE_FILE)
158
+ def route_create_file(x: CreateFileRequest):
159
+ Path(x.filepath).parent.mkdir(parents=True, exist_ok=True)
160
+ Path(x.filepath).touch()
161
+
162
+
148
163
  @router.post(C.ROUTE_SAVE_SECTION_JSON_SOURCE)
149
164
  def route_save_section_json_source(x: SaveSectionJSONSourceRequest):
150
165
  save_json_source_data(x.project, [x.sectionId], x.forking_from)
@@ -172,3 +187,11 @@ def route_delete_file(x: DeleteFileRequest):
172
187
  status_code=status.HTTP_404_NOT_FOUND,
173
188
  detail=f"Failed to delete file: {str(e)}",
174
189
  )
190
+
191
+
192
+ @router.post(C.ROUTE_FILE_WATCH_LIST)
193
+ def route_file_watch_list(
194
+ x: FileWatchListRequest, specific_file_watcher=Depends(get_specific_file_watcher)
195
+ ):
196
+ # x.filepaths is the current list of file paths or glob patterns that should be watched
197
+ 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
 
@@ -89,11 +89,12 @@
89
89
  "ROUTE_ADD_DEFAULT_DATA_FOR_JSON_IMPORT": "/addDefaultDataForJSONImport",
90
90
  "ROUTE_GET_JSON_SOURCE_PATH_TO_BE_DELETED": "/getJsonSourcePathToBeDeleted",
91
91
  "ROUTE_MAKE_EV_REF_TEMPLATE_VAR_REPLACEMENTS": "/makeEVRefReplacements",
92
+ "ROUTE_CREATE_FILE": "/createFile",
92
93
  "ROUTE_DELETE_FILE": "/deleteFile",
93
94
  "ROUTE_CHECK_IF_FILE_EXISTS": "/checkIfFileExists",
94
95
  "ROUTE_LOAD_ARTIFACT_FROM_DISK": "/loadArtifactFromDisk",
95
96
  "ROUTE_RESTART_LANGUAGE_SERVER": "/restartLanguageServer",
96
- "JSON_SOURCE_PREFIX": "JSON-",
97
+ "ROUTE_FILE_WATCH_LIST": "/fileWatchList",
97
98
  "NESTED_PARAM": "NESTED_PARAM",
98
99
  "ARGS_PREFIX": "__",
99
100
  "TEMPLATE_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",
@@ -65,7 +65,7 @@
65
65
  "aspectRatioExtraWidth": 0,
66
66
  "columnWidth": null,
67
67
  "renderedValue": null,
68
- "subprocessStartMethod": "fork",
68
+ "subprocessStartMethod": "spawn",
69
69
  "headingAsSectionName": false
70
70
  },
71
71
  "sectionVariant": {
@@ -94,7 +94,8 @@
94
94
  "isFrozen": false,
95
95
  "isSelected": false,
96
96
  "evRefs": [],
97
- "ignoreTypeErrors": false
97
+ "ignoreTypeErrors": false,
98
+ "jsonSource": null
98
99
  },
99
100
  "codeInfoCol": {
100
101
  "children": { "null": [] }