griptape-nodes 0.61.0__py3-none-any.whl → 0.62.1__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.
- griptape_nodes/common/macro_parser/core.py +4 -4
- griptape_nodes/common/macro_parser/exceptions.py +3 -3
- griptape_nodes/common/macro_parser/resolution.py +2 -2
- griptape_nodes/common/project_templates/default_project_template.py +5 -10
- griptape_nodes/common/project_templates/directory.py +5 -5
- griptape_nodes/common/project_templates/loader.py +8 -7
- griptape_nodes/common/project_templates/project.py +1 -1
- griptape_nodes/common/project_templates/situation.py +5 -17
- griptape_nodes/common/project_templates/validation.py +3 -3
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +2 -2
- griptape_nodes/drivers/storage/local_storage_driver.py +2 -2
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/project_events.py +208 -93
- griptape_nodes/retained_mode/managers/event_manager.py +24 -9
- griptape_nodes/retained_mode/managers/library_manager.py +12 -21
- griptape_nodes/retained_mode/managers/os_manager.py +54 -6
- griptape_nodes/retained_mode/managers/project_manager.py +709 -259
- griptape_nodes/retained_mode/managers/static_files_manager.py +1 -5
- griptape_nodes/retained_mode/managers/sync_manager.py +4 -1
- griptape_nodes/retained_mode/managers/workflow_manager.py +2 -10
- griptape_nodes/traits/button.py +2 -1
- {griptape_nodes-0.61.0.dist-info → griptape_nodes-0.62.1.dist-info}/METADATA +1 -1
- {griptape_nodes-0.61.0.dist-info → griptape_nodes-0.62.1.dist-info}/RECORD +25 -26
- {griptape_nodes-0.61.0.dist-info → griptape_nodes-0.62.1.dist-info}/WHEEL +1 -1
- griptape_nodes/common/project_templates/defaults/project_template.yml +0 -89
- {griptape_nodes-0.61.0.dist-info → griptape_nodes-0.62.1.dist-info}/entry_points.txt +0 -0
|
@@ -5,87 +5,133 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
9
9
|
|
|
10
10
|
from griptape_nodes.common.macro_parser import (
|
|
11
11
|
MacroMatchFailure,
|
|
12
12
|
MacroMatchFailureReason,
|
|
13
|
-
MacroParseFailure,
|
|
14
|
-
MacroParseFailureReason,
|
|
15
13
|
MacroResolutionError,
|
|
16
14
|
MacroResolutionFailureReason,
|
|
17
|
-
MacroSyntaxError,
|
|
18
15
|
ParsedMacro,
|
|
19
16
|
)
|
|
20
17
|
from griptape_nodes.common.project_templates import (
|
|
21
18
|
DEFAULT_PROJECT_TEMPLATE,
|
|
19
|
+
DirectoryDefinition,
|
|
22
20
|
ProjectTemplate,
|
|
23
21
|
ProjectValidationInfo,
|
|
24
22
|
ProjectValidationStatus,
|
|
23
|
+
SituationTemplate,
|
|
25
24
|
load_project_template_from_yaml,
|
|
26
25
|
)
|
|
27
|
-
from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
|
|
26
|
+
from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
|
|
28
27
|
from griptape_nodes.retained_mode.events.os_events import ReadFileRequest, ReadFileResultSuccess
|
|
29
28
|
from griptape_nodes.retained_mode.events.project_events import (
|
|
29
|
+
AttemptMapAbsolutePathToProjectRequest,
|
|
30
|
+
AttemptMapAbsolutePathToProjectResultFailure,
|
|
31
|
+
AttemptMapAbsolutePathToProjectResultSuccess,
|
|
32
|
+
AttemptMatchPathAgainstMacroRequest,
|
|
33
|
+
AttemptMatchPathAgainstMacroResultFailure,
|
|
34
|
+
AttemptMatchPathAgainstMacroResultSuccess,
|
|
30
35
|
GetAllSituationsForProjectRequest,
|
|
31
36
|
GetAllSituationsForProjectResultFailure,
|
|
32
37
|
GetAllSituationsForProjectResultSuccess,
|
|
33
38
|
GetCurrentProjectRequest,
|
|
39
|
+
GetCurrentProjectResultFailure,
|
|
34
40
|
GetCurrentProjectResultSuccess,
|
|
35
|
-
GetMacroForSituationRequest,
|
|
36
|
-
GetMacroForSituationResultFailure,
|
|
37
|
-
GetMacroForSituationResultSuccess,
|
|
38
41
|
GetPathForMacroRequest,
|
|
39
42
|
GetPathForMacroResultFailure,
|
|
40
43
|
GetPathForMacroResultSuccess,
|
|
41
44
|
GetProjectTemplateRequest,
|
|
42
45
|
GetProjectTemplateResultFailure,
|
|
43
46
|
GetProjectTemplateResultSuccess,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
GetSituationRequest,
|
|
48
|
+
GetSituationResultFailure,
|
|
49
|
+
GetSituationResultSuccess,
|
|
50
|
+
GetStateForMacroRequest,
|
|
51
|
+
GetStateForMacroResultFailure,
|
|
52
|
+
GetStateForMacroResultSuccess,
|
|
53
|
+
ListProjectTemplatesRequest,
|
|
54
|
+
ListProjectTemplatesResultSuccess,
|
|
47
55
|
LoadProjectTemplateRequest,
|
|
48
56
|
LoadProjectTemplateResultFailure,
|
|
49
57
|
LoadProjectTemplateResultSuccess,
|
|
50
|
-
MatchPathAgainstMacroRequest,
|
|
51
|
-
MatchPathAgainstMacroResultFailure,
|
|
52
|
-
MatchPathAgainstMacroResultSuccess,
|
|
53
58
|
PathResolutionFailureReason,
|
|
59
|
+
ProjectTemplateInfo,
|
|
54
60
|
SaveProjectTemplateRequest,
|
|
55
61
|
SaveProjectTemplateResultFailure,
|
|
56
62
|
SetCurrentProjectRequest,
|
|
57
63
|
SetCurrentProjectResultSuccess,
|
|
58
|
-
ValidateMacroSyntaxRequest,
|
|
59
|
-
ValidateMacroSyntaxResultFailure,
|
|
60
|
-
ValidateMacroSyntaxResultSuccess,
|
|
61
64
|
)
|
|
65
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
62
66
|
|
|
63
67
|
if TYPE_CHECKING:
|
|
64
|
-
from griptape_nodes.retained_mode.events.base_events import ResultPayload
|
|
65
68
|
from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
|
|
66
69
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
67
70
|
from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
|
|
68
71
|
|
|
69
72
|
logger = logging.getLogger("griptape_nodes")
|
|
70
73
|
|
|
71
|
-
#
|
|
72
|
-
|
|
74
|
+
# Type alias for project identifiers
|
|
75
|
+
# Usually constructed from file path, but kept opaque to prevent abuse
|
|
76
|
+
ProjectID = str
|
|
77
|
+
|
|
78
|
+
# Synthetic identifier for the system default project template
|
|
79
|
+
SYSTEM_DEFAULTS_KEY: ProjectID = "<system-defaults>"
|
|
80
|
+
|
|
81
|
+
# Builtin variable name constants
|
|
82
|
+
BUILTIN_PROJECT_DIR = "project_dir"
|
|
83
|
+
BUILTIN_PROJECT_NAME = "project_name"
|
|
84
|
+
BUILTIN_WORKSPACE_DIR = "workspace_dir"
|
|
85
|
+
BUILTIN_WORKFLOW_NAME = "workflow_name"
|
|
86
|
+
BUILTIN_WORKFLOW_DIR = "workflow_dir"
|
|
73
87
|
|
|
74
88
|
|
|
75
89
|
@dataclass(frozen=True)
|
|
76
|
-
class
|
|
77
|
-
"""
|
|
90
|
+
class BuiltinVariableInfo:
|
|
91
|
+
"""Metadata about a builtin variable.
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
Attributes:
|
|
94
|
+
name: The variable name (e.g., "project_dir")
|
|
95
|
+
is_directory: Whether this variable represents a directory path
|
|
96
|
+
"""
|
|
81
97
|
|
|
98
|
+
name: str
|
|
99
|
+
is_directory: bool
|
|
82
100
|
|
|
83
|
-
@dataclass(frozen=True)
|
|
84
|
-
class DirectoryMacroKey:
|
|
85
|
-
"""Key for caching parsed directory schema macros."""
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
# Builtin variable definitions with metadata
|
|
103
|
+
_BUILTIN_VARIABLE_DEFINITIONS = [
|
|
104
|
+
BuiltinVariableInfo(name=BUILTIN_PROJECT_DIR, is_directory=True),
|
|
105
|
+
BuiltinVariableInfo(name=BUILTIN_PROJECT_NAME, is_directory=False),
|
|
106
|
+
BuiltinVariableInfo(name=BUILTIN_WORKSPACE_DIR, is_directory=True),
|
|
107
|
+
BuiltinVariableInfo(name=BUILTIN_WORKFLOW_NAME, is_directory=False),
|
|
108
|
+
BuiltinVariableInfo(name=BUILTIN_WORKFLOW_DIR, is_directory=True),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# Map of variable name to metadata
|
|
112
|
+
_BUILTIN_VARIABLE_INFO: dict[str, BuiltinVariableInfo] = {var.name: var for var in _BUILTIN_VARIABLE_DEFINITIONS}
|
|
113
|
+
|
|
114
|
+
# Builtin variables available in all macros (read-only)
|
|
115
|
+
BUILTIN_VARIABLES = frozenset(var.name for var in _BUILTIN_VARIABLE_DEFINITIONS)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class ProjectInfo:
|
|
120
|
+
"""Consolidated information about a loaded project.
|
|
121
|
+
|
|
122
|
+
Stores all project-related data including template, validation,
|
|
123
|
+
file paths, and cached parsed macros.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
project_id: ProjectID
|
|
127
|
+
project_file_path: Path | None # None for system defaults or non-file sources
|
|
128
|
+
project_base_dir: Path # Directory for resolving relative paths ({project_dir})
|
|
129
|
+
template: ProjectTemplate
|
|
130
|
+
validation: ProjectValidationInfo
|
|
131
|
+
|
|
132
|
+
# Cached parsed macros (populated during load for performance)
|
|
133
|
+
parsed_situation_schemas: dict[str, ParsedMacro] # situation_name -> ParsedMacro
|
|
134
|
+
parsed_directory_schemas: dict[str, ParsedMacro] # directory_name -> ParsedMacro
|
|
89
135
|
|
|
90
136
|
|
|
91
137
|
class ProjectManager:
|
|
@@ -119,21 +165,16 @@ class ProjectManager:
|
|
|
119
165
|
config_manager: ConfigManager instance for accessing configuration
|
|
120
166
|
secrets_manager: SecretsManager instance for macro resolution
|
|
121
167
|
"""
|
|
122
|
-
self.
|
|
123
|
-
self.
|
|
124
|
-
|
|
125
|
-
# Track validation status for ALL load attempts (including MISSING/UNUSABLE)
|
|
126
|
-
self.registered_template_status: dict[Path, ProjectValidationInfo] = {}
|
|
168
|
+
self._config_manager = config_manager
|
|
169
|
+
self._secrets_manager = secrets_manager
|
|
127
170
|
|
|
128
|
-
#
|
|
129
|
-
self.
|
|
171
|
+
# Consolidated project information storage
|
|
172
|
+
self._successfully_loaded_project_templates: dict[ProjectID, ProjectInfo] = {}
|
|
173
|
+
self._current_project_id: ProjectID | None = None
|
|
130
174
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
# Track which project.yml user has selected
|
|
136
|
-
self.current_project_path: Path | None = None
|
|
175
|
+
# Track validation status for ALL load attempts (including MISSING/UNUSABLE)
|
|
176
|
+
# This allows UI to query why a project failed to load
|
|
177
|
+
self._registered_template_status: dict[Path, ProjectValidationInfo] = {}
|
|
137
178
|
|
|
138
179
|
# Register event handlers
|
|
139
180
|
if event_manager is not None:
|
|
@@ -144,8 +185,9 @@ class ProjectManager:
|
|
|
144
185
|
GetProjectTemplateRequest, self.on_get_project_template_request
|
|
145
186
|
)
|
|
146
187
|
event_manager.assign_manager_to_request_type(
|
|
147
|
-
|
|
188
|
+
ListProjectTemplatesRequest, self.on_list_project_templates_request
|
|
148
189
|
)
|
|
190
|
+
event_manager.assign_manager_to_request_type(GetSituationRequest, self.on_get_situation_request)
|
|
149
191
|
event_manager.assign_manager_to_request_type(GetPathForMacroRequest, self.on_get_path_for_macro_request)
|
|
150
192
|
event_manager.assign_manager_to_request_type(SetCurrentProjectRequest, self.on_set_current_project_request)
|
|
151
193
|
event_manager.assign_manager_to_request_type(GetCurrentProjectRequest, self.on_get_current_project_request)
|
|
@@ -153,30 +195,25 @@ class ProjectManager:
|
|
|
153
195
|
SaveProjectTemplateRequest, self.on_save_project_template_request
|
|
154
196
|
)
|
|
155
197
|
event_manager.assign_manager_to_request_type(
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
event_manager.assign_manager_to_request_type(
|
|
159
|
-
GetVariablesForMacroRequest, self.on_get_variables_for_macro_request
|
|
198
|
+
AttemptMatchPathAgainstMacroRequest, self.on_match_path_against_macro_request
|
|
160
199
|
)
|
|
200
|
+
event_manager.assign_manager_to_request_type(GetStateForMacroRequest, self.on_get_state_for_macro_request)
|
|
161
201
|
event_manager.assign_manager_to_request_type(
|
|
162
|
-
|
|
202
|
+
GetAllSituationsForProjectRequest, self.on_get_all_situations_for_project_request
|
|
163
203
|
)
|
|
164
204
|
event_manager.assign_manager_to_request_type(
|
|
165
|
-
|
|
205
|
+
AttemptMapAbsolutePathToProjectRequest, self.on_attempt_map_absolute_path_to_project_request
|
|
166
206
|
)
|
|
167
207
|
|
|
168
208
|
# Register app initialization listener
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# AppInitializationComplete,
|
|
174
|
-
# self.on_app_initialization_complete,
|
|
175
|
-
# )
|
|
176
|
-
|
|
177
|
-
# Event handler methods (public)
|
|
209
|
+
event_manager.add_listener_to_app_event(
|
|
210
|
+
AppInitializationComplete,
|
|
211
|
+
self.on_app_initialization_complete,
|
|
212
|
+
)
|
|
178
213
|
|
|
179
|
-
def on_load_project_template_request(
|
|
214
|
+
def on_load_project_template_request(
|
|
215
|
+
self, request: LoadProjectTemplateRequest
|
|
216
|
+
) -> LoadProjectTemplateResultSuccess | LoadProjectTemplateResultFailure:
|
|
180
217
|
"""Load user's project.yml and merge with system defaults.
|
|
181
218
|
|
|
182
219
|
Flow:
|
|
@@ -187,10 +224,6 @@ class ProjectManager:
|
|
|
187
224
|
5. If usable, cache template in successful_templates
|
|
188
225
|
6. Return LoadProjectTemplateResultSuccess or LoadProjectTemplateResultFailure
|
|
189
226
|
"""
|
|
190
|
-
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
191
|
-
|
|
192
|
-
logger.debug("Loading project template: %s", request.project_path)
|
|
193
|
-
|
|
194
227
|
read_request = ReadFileRequest(
|
|
195
228
|
file_path=str(request.project_path),
|
|
196
229
|
encoding="utf-8",
|
|
@@ -200,119 +233,182 @@ class ProjectManager:
|
|
|
200
233
|
|
|
201
234
|
if read_result.failed():
|
|
202
235
|
validation = ProjectValidationInfo(status=ProjectValidationStatus.MISSING)
|
|
203
|
-
self.
|
|
236
|
+
self._registered_template_status[request.project_path] = validation
|
|
204
237
|
|
|
205
238
|
return LoadProjectTemplateResultFailure(
|
|
206
|
-
project_path=request.project_path,
|
|
207
239
|
validation=validation,
|
|
208
|
-
result_details=f"
|
|
240
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because file not found",
|
|
209
241
|
)
|
|
210
242
|
|
|
211
243
|
if not isinstance(read_result, ReadFileResultSuccess):
|
|
212
244
|
validation = ProjectValidationInfo(status=ProjectValidationStatus.UNUSABLE)
|
|
213
|
-
self.
|
|
245
|
+
self._registered_template_status[request.project_path] = validation
|
|
214
246
|
|
|
215
247
|
return LoadProjectTemplateResultFailure(
|
|
216
|
-
project_path=request.project_path,
|
|
217
248
|
validation=validation,
|
|
218
|
-
result_details="
|
|
249
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because file read returned unexpected result type",
|
|
219
250
|
)
|
|
220
251
|
|
|
221
252
|
yaml_text = read_result.content
|
|
222
253
|
if not isinstance(yaml_text, str):
|
|
223
254
|
validation = ProjectValidationInfo(status=ProjectValidationStatus.UNUSABLE)
|
|
224
|
-
self.
|
|
255
|
+
self._registered_template_status[request.project_path] = validation
|
|
225
256
|
|
|
226
257
|
return LoadProjectTemplateResultFailure(
|
|
227
|
-
project_path=request.project_path,
|
|
228
258
|
validation=validation,
|
|
229
|
-
result_details="
|
|
259
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because template must be text, got binary content",
|
|
230
260
|
)
|
|
231
261
|
|
|
232
262
|
validation = ProjectValidationInfo(status=ProjectValidationStatus.GOOD)
|
|
233
263
|
template = load_project_template_from_yaml(yaml_text, validation)
|
|
234
264
|
|
|
235
265
|
if template is None:
|
|
236
|
-
self.
|
|
266
|
+
self._registered_template_status[request.project_path] = validation
|
|
237
267
|
return LoadProjectTemplateResultFailure(
|
|
238
|
-
project_path=request.project_path,
|
|
239
268
|
validation=validation,
|
|
240
|
-
result_details="
|
|
269
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because YAML could not be parsed",
|
|
241
270
|
)
|
|
242
271
|
|
|
272
|
+
# Generate project_id from file path
|
|
273
|
+
project_file_path = Path(request.project_path)
|
|
274
|
+
project_id = str(project_file_path)
|
|
275
|
+
project_base_dir = project_file_path.parent
|
|
276
|
+
|
|
277
|
+
# Parse all macros BEFORE creating ProjectInfo - collect ALL errors
|
|
278
|
+
situation_schemas = self._parse_situation_macros(template.situations, validation)
|
|
279
|
+
directory_schemas = self._parse_directory_macros(template.directories, validation)
|
|
280
|
+
|
|
281
|
+
# Now check if validation is usable after collecting all errors
|
|
243
282
|
if not validation.is_usable():
|
|
244
|
-
self.
|
|
283
|
+
self._registered_template_status[request.project_path] = validation
|
|
245
284
|
return LoadProjectTemplateResultFailure(
|
|
246
|
-
project_path=request.project_path,
|
|
247
285
|
validation=validation,
|
|
248
|
-
result_details=f"
|
|
286
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because template is not usable (status: {validation.status})",
|
|
249
287
|
)
|
|
250
288
|
|
|
251
|
-
|
|
289
|
+
# Create consolidated ProjectInfo with fully populated macro caches
|
|
290
|
+
project_info = ProjectInfo(
|
|
291
|
+
project_id=project_id,
|
|
292
|
+
project_file_path=project_file_path,
|
|
293
|
+
project_base_dir=project_base_dir,
|
|
294
|
+
template=template,
|
|
295
|
+
validation=validation,
|
|
296
|
+
parsed_situation_schemas=situation_schemas,
|
|
297
|
+
parsed_directory_schemas=directory_schemas,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Store in new consolidated dict
|
|
301
|
+
self._successfully_loaded_project_templates[project_id] = project_info
|
|
252
302
|
|
|
253
|
-
|
|
254
|
-
self.
|
|
303
|
+
# Track validation status for all load attempts (for UI display)
|
|
304
|
+
self._registered_template_status[request.project_path] = validation
|
|
255
305
|
|
|
256
306
|
return LoadProjectTemplateResultSuccess(
|
|
257
|
-
|
|
307
|
+
project_id=project_id,
|
|
258
308
|
template=template,
|
|
259
309
|
validation=validation,
|
|
260
310
|
result_details=f"Template loaded successfully with status: {validation.status}",
|
|
261
311
|
)
|
|
262
312
|
|
|
263
|
-
def on_get_project_template_request(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)
|
|
313
|
+
def on_get_project_template_request(
|
|
314
|
+
self, request: GetProjectTemplateRequest
|
|
315
|
+
) -> GetProjectTemplateResultSuccess | GetProjectTemplateResultFailure:
|
|
316
|
+
"""Get cached template for a project ID."""
|
|
317
|
+
project_info = self._successfully_loaded_project_templates.get(request.project_id)
|
|
269
318
|
|
|
270
|
-
|
|
271
|
-
template = self.successful_templates.get(request.project_path)
|
|
272
|
-
|
|
273
|
-
if template is None:
|
|
319
|
+
if project_info is None:
|
|
274
320
|
return GetProjectTemplateResultFailure(
|
|
275
|
-
result_details=f"
|
|
321
|
+
result_details=f"Attempted to get project template for '{request.project_id}'. Failed because template not loaded yet",
|
|
276
322
|
)
|
|
277
323
|
|
|
278
324
|
return GetProjectTemplateResultSuccess(
|
|
279
|
-
template=template,
|
|
280
|
-
validation=validation,
|
|
281
|
-
result_details="
|
|
325
|
+
template=project_info.template,
|
|
326
|
+
validation=project_info.validation,
|
|
327
|
+
result_details=f"Successfully retrieved project template for '{request.project_id}'. Status: {project_info.validation.status}",
|
|
282
328
|
)
|
|
283
329
|
|
|
284
|
-
def
|
|
285
|
-
|
|
330
|
+
def on_list_project_templates_request(
|
|
331
|
+
self, request: ListProjectTemplatesRequest
|
|
332
|
+
) -> ListProjectTemplatesResultSuccess:
|
|
333
|
+
"""List all project templates that have been loaded or attempted to load.
|
|
334
|
+
|
|
335
|
+
Returns separate lists for successfully loaded and failed templates.
|
|
336
|
+
"""
|
|
337
|
+
successfully_loaded: list[ProjectTemplateInfo] = []
|
|
338
|
+
failed_to_load: list[ProjectTemplateInfo] = []
|
|
339
|
+
|
|
340
|
+
# Gather successfully loaded templates from _successfully_loaded_project_templates
|
|
341
|
+
for project_id, project_info in self._successfully_loaded_project_templates.items():
|
|
342
|
+
# Skip system builtins unless requested
|
|
343
|
+
if not request.include_system_builtins and project_id == SYSTEM_DEFAULTS_KEY:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
successfully_loaded.append(ProjectTemplateInfo(project_id=project_id, validation=project_info.validation))
|
|
347
|
+
|
|
348
|
+
# Gather failed templates from _registered_template_status
|
|
349
|
+
# These are tracked by Path, not ProjectID
|
|
350
|
+
for template_path, validation in self._registered_template_status.items():
|
|
351
|
+
project_id = str(template_path)
|
|
352
|
+
|
|
353
|
+
# Skip if already in successfully loaded (validation status might be FLAWED but still loaded)
|
|
354
|
+
if project_id in self._successfully_loaded_project_templates:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
# Skip system builtins unless requested
|
|
358
|
+
if not request.include_system_builtins and project_id == SYSTEM_DEFAULTS_KEY:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
# Only include if status indicates failure (UNUSABLE or MISSING)
|
|
362
|
+
if not validation.is_usable():
|
|
363
|
+
failed_to_load.append(ProjectTemplateInfo(project_id=project_id, validation=validation))
|
|
364
|
+
|
|
365
|
+
return ListProjectTemplatesResultSuccess(
|
|
366
|
+
successfully_loaded=successfully_loaded,
|
|
367
|
+
failed_to_load=failed_to_load,
|
|
368
|
+
result_details=f"Successfully listed project templates. Loaded: {len(successfully_loaded)}, Failed: {len(failed_to_load)}",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def on_get_situation_request(
|
|
372
|
+
self, request: GetSituationRequest
|
|
373
|
+
) -> GetSituationResultSuccess | GetSituationResultFailure:
|
|
374
|
+
"""Get the complete situation template for a specific situation.
|
|
375
|
+
|
|
376
|
+
Returns the full SituationTemplate including macro and policy.
|
|
286
377
|
|
|
287
378
|
Flow:
|
|
288
|
-
1. Get
|
|
289
|
-
2. Get
|
|
290
|
-
3.
|
|
379
|
+
1. Get current project
|
|
380
|
+
2. Get template from successful_templates
|
|
381
|
+
3. Get situation from template
|
|
382
|
+
4. Return complete SituationTemplate
|
|
291
383
|
"""
|
|
292
|
-
|
|
384
|
+
current_project_request = GetCurrentProjectRequest()
|
|
385
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
293
386
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
result_details=f"Project template not loaded: {request.project_path}",
|
|
387
|
+
if not isinstance(current_project_result, GetCurrentProjectResultSuccess):
|
|
388
|
+
return GetSituationResultFailure(
|
|
389
|
+
result_details=f"Attempted to get situation '{request.situation_name}'. Failed because no current project is set or template not loaded",
|
|
298
390
|
)
|
|
299
391
|
|
|
392
|
+
template = current_project_result.project_info.template
|
|
393
|
+
|
|
300
394
|
situation = template.situations.get(request.situation_name)
|
|
301
395
|
if situation is None:
|
|
302
|
-
return
|
|
303
|
-
result_details=f"
|
|
396
|
+
return GetSituationResultFailure(
|
|
397
|
+
result_details=f"Attempted to get situation '{request.situation_name}'. Failed because situation not found",
|
|
304
398
|
)
|
|
305
399
|
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
result_details=f"
|
|
400
|
+
return GetSituationResultSuccess(
|
|
401
|
+
situation=situation,
|
|
402
|
+
result_details=f"Successfully retrieved situation '{request.situation_name}'. Macro: {situation.macro}, Policy: create_dirs={situation.policy.create_dirs}, on_collision={situation.policy.on_collision}",
|
|
309
403
|
)
|
|
310
404
|
|
|
311
|
-
def on_get_path_for_macro_request(
|
|
405
|
+
def on_get_path_for_macro_request( # noqa: C901, PLR0911, PLR0912, PLR0915
|
|
406
|
+
self, request: GetPathForMacroRequest
|
|
407
|
+
) -> GetPathForMacroResultSuccess | GetPathForMacroResultFailure:
|
|
312
408
|
"""Resolve ANY macro schema with variables to final Path.
|
|
313
409
|
|
|
314
410
|
Flow:
|
|
315
|
-
1.
|
|
411
|
+
1. Get current project
|
|
316
412
|
2. Get variables from ParsedMacro.get_variables()
|
|
317
413
|
3. For each variable:
|
|
318
414
|
- If in directories dict → resolve directory, add to resolution bag
|
|
@@ -323,48 +419,75 @@ class ProjectManager:
|
|
|
323
419
|
5. Resolve macro with complete variable bag
|
|
324
420
|
6. Return resolved Path
|
|
325
421
|
"""
|
|
326
|
-
|
|
422
|
+
current_project_request = GetCurrentProjectRequest()
|
|
423
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
327
424
|
|
|
328
|
-
|
|
329
|
-
parsed_macro = ParsedMacro(request.macro_schema)
|
|
330
|
-
except MacroSyntaxError as e:
|
|
425
|
+
if not isinstance(current_project_result, GetCurrentProjectResultSuccess):
|
|
331
426
|
return GetPathForMacroResultFailure(
|
|
332
427
|
failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
|
|
333
|
-
|
|
334
|
-
result_details=f"Invalid macro syntax: {e}",
|
|
428
|
+
result_details="Attempted to resolve macro path. Failed because no current project is set or template not loaded",
|
|
335
429
|
)
|
|
336
430
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return GetPathForMacroResultFailure(
|
|
340
|
-
failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
|
|
341
|
-
error_details="Project template not loaded",
|
|
342
|
-
result_details=f"Project template not loaded: {request.project_path}",
|
|
343
|
-
)
|
|
431
|
+
project_info = current_project_result.project_info
|
|
432
|
+
template = project_info.template
|
|
344
433
|
|
|
345
|
-
variable_infos = parsed_macro.get_variables()
|
|
434
|
+
variable_infos = request.parsed_macro.get_variables()
|
|
346
435
|
directory_names = set(template.directories.keys())
|
|
347
436
|
user_provided_names = set(request.variables.keys())
|
|
348
437
|
|
|
438
|
+
# Check for directory/user variable name conflicts
|
|
349
439
|
conflicting = directory_names & user_provided_names
|
|
350
440
|
if conflicting:
|
|
351
441
|
return GetPathForMacroResultFailure(
|
|
352
442
|
failure_reason=PathResolutionFailureReason.DIRECTORY_OVERRIDE_ATTEMPTED,
|
|
353
|
-
conflicting_variables=
|
|
354
|
-
result_details=f"
|
|
443
|
+
conflicting_variables=conflicting,
|
|
444
|
+
result_details=f"Attempted to resolve macro path. Failed because variables conflict with directory names: {', '.join(sorted(conflicting))}",
|
|
355
445
|
)
|
|
356
446
|
|
|
357
447
|
resolution_bag: dict[str, str | int] = {}
|
|
448
|
+
disallowed_overrides: set[str] = set()
|
|
358
449
|
|
|
359
450
|
for var_info in variable_infos:
|
|
360
451
|
var_name = var_info.name
|
|
361
452
|
|
|
362
453
|
if var_name in directory_names:
|
|
363
454
|
directory_def = template.directories[var_name]
|
|
364
|
-
resolution_bag[var_name] = directory_def.
|
|
455
|
+
resolution_bag[var_name] = directory_def.path_macro
|
|
365
456
|
elif var_name in user_provided_names:
|
|
366
457
|
resolution_bag[var_name] = request.variables[var_name]
|
|
367
458
|
|
|
459
|
+
if var_name in BUILTIN_VARIABLES:
|
|
460
|
+
try:
|
|
461
|
+
builtin_value = self._get_builtin_variable_value(var_name, project_info)
|
|
462
|
+
except (RuntimeError, NotImplementedError) as e:
|
|
463
|
+
return GetPathForMacroResultFailure(
|
|
464
|
+
failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
|
|
465
|
+
result_details=f"Attempted to resolve macro path. Failed because builtin variable '{var_name}' cannot be resolved: {e}",
|
|
466
|
+
)
|
|
467
|
+
# Confirm no monkey business with trying to override builtin values
|
|
468
|
+
existing = resolution_bag.get(var_name)
|
|
469
|
+
if existing is not None:
|
|
470
|
+
# For directory builtin variables, compare as resolved paths
|
|
471
|
+
builtin_info = _BUILTIN_VARIABLE_INFO.get(var_name)
|
|
472
|
+
if builtin_info and builtin_info.is_directory:
|
|
473
|
+
os_manager = GriptapeNodes.OSManager()
|
|
474
|
+
resolved_existing = os_manager.resolve_path_safely(Path(str(existing)))
|
|
475
|
+
resolved_builtin = os_manager.resolve_path_safely(Path(builtin_value))
|
|
476
|
+
if resolved_existing != resolved_builtin:
|
|
477
|
+
disallowed_overrides.add(var_name)
|
|
478
|
+
elif str(existing) != builtin_value:
|
|
479
|
+
disallowed_overrides.add(var_name)
|
|
480
|
+
else:
|
|
481
|
+
resolution_bag[var_name] = builtin_value
|
|
482
|
+
|
|
483
|
+
# Check if user tried to override builtins with different values
|
|
484
|
+
if disallowed_overrides:
|
|
485
|
+
return GetPathForMacroResultFailure(
|
|
486
|
+
failure_reason=PathResolutionFailureReason.DIRECTORY_OVERRIDE_ATTEMPTED,
|
|
487
|
+
conflicting_variables=disallowed_overrides,
|
|
488
|
+
result_details=f"Attempted to resolve macro path. Failed because cannot override builtin variables: {', '.join(sorted(disallowed_overrides))}",
|
|
489
|
+
)
|
|
490
|
+
|
|
368
491
|
required_vars = {v.name for v in variable_infos if v.is_required}
|
|
369
492
|
provided_vars = set(resolution_bag.keys())
|
|
370
493
|
missing = required_vars - provided_vars
|
|
@@ -372,19 +495,18 @@ class ProjectManager:
|
|
|
372
495
|
if missing:
|
|
373
496
|
return GetPathForMacroResultFailure(
|
|
374
497
|
failure_reason=PathResolutionFailureReason.MISSING_REQUIRED_VARIABLES,
|
|
375
|
-
missing_variables=
|
|
376
|
-
result_details=f"
|
|
498
|
+
missing_variables=missing,
|
|
499
|
+
result_details=f"Attempted to resolve macro path. Failed because missing required variables: {', '.join(sorted(missing))}",
|
|
377
500
|
)
|
|
378
501
|
|
|
379
|
-
if self.
|
|
502
|
+
if self._secrets_manager is None:
|
|
380
503
|
return GetPathForMacroResultFailure(
|
|
381
504
|
failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
|
|
382
|
-
|
|
383
|
-
result_details="SecretsManager not available",
|
|
505
|
+
result_details="Attempted to resolve macro path. Failed because SecretsManager is not available",
|
|
384
506
|
)
|
|
385
507
|
|
|
386
508
|
try:
|
|
387
|
-
resolved_string = parsed_macro.resolve(resolution_bag, self.
|
|
509
|
+
resolved_string = request.parsed_macro.resolve(resolution_bag, self._secrets_manager)
|
|
388
510
|
except MacroResolutionError as e:
|
|
389
511
|
if e.failure_reason == MacroResolutionFailureReason.MISSING_REQUIRED_VARIABLES:
|
|
390
512
|
path_failure_reason = PathResolutionFailureReason.MISSING_REQUIRED_VARIABLES
|
|
@@ -394,38 +516,57 @@ class ProjectManager:
|
|
|
394
516
|
return GetPathForMacroResultFailure(
|
|
395
517
|
failure_reason=path_failure_reason,
|
|
396
518
|
missing_variables=e.missing_variables,
|
|
397
|
-
|
|
398
|
-
result_details=f"Macro resolution failed: {e}",
|
|
519
|
+
result_details=f"Attempted to resolve macro path. Failed because macro resolution error: {e}",
|
|
399
520
|
)
|
|
400
521
|
|
|
401
522
|
resolved_path = Path(resolved_string)
|
|
402
523
|
|
|
524
|
+
# Make absolute path by resolving against project base directory
|
|
525
|
+
if resolved_path.is_absolute():
|
|
526
|
+
absolute_path = resolved_path
|
|
527
|
+
else:
|
|
528
|
+
absolute_path = project_info.project_base_dir / resolved_path
|
|
529
|
+
|
|
403
530
|
return GetPathForMacroResultSuccess(
|
|
404
531
|
resolved_path=resolved_path,
|
|
405
|
-
|
|
532
|
+
absolute_path=absolute_path,
|
|
533
|
+
result_details=f"Successfully resolved macro path. Result: {resolved_path}",
|
|
406
534
|
)
|
|
407
535
|
|
|
408
|
-
def on_set_current_project_request(self, request: SetCurrentProjectRequest) ->
|
|
409
|
-
"""Set which project
|
|
410
|
-
self.
|
|
536
|
+
def on_set_current_project_request(self, request: SetCurrentProjectRequest) -> SetCurrentProjectResultSuccess:
|
|
537
|
+
"""Set which project user has selected."""
|
|
538
|
+
self._current_project_id = request.project_id
|
|
411
539
|
|
|
412
|
-
if request.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
540
|
+
if request.project_id is None:
|
|
541
|
+
return SetCurrentProjectResultSuccess(
|
|
542
|
+
result_details="Successfully set current project. No project selected",
|
|
543
|
+
)
|
|
416
544
|
|
|
417
545
|
return SetCurrentProjectResultSuccess(
|
|
418
|
-
result_details="
|
|
546
|
+
result_details=f"Successfully set current project. ID: {request.project_id}",
|
|
419
547
|
)
|
|
420
548
|
|
|
421
|
-
def on_get_current_project_request(
|
|
422
|
-
|
|
549
|
+
def on_get_current_project_request(
|
|
550
|
+
self, _request: GetCurrentProjectRequest
|
|
551
|
+
) -> GetCurrentProjectResultSuccess | GetCurrentProjectResultFailure:
|
|
552
|
+
"""Get currently selected project with template info."""
|
|
553
|
+
if self._current_project_id is None:
|
|
554
|
+
return GetCurrentProjectResultFailure(
|
|
555
|
+
result_details="Attempted to get current project. Failed because no project is currently set"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
project_info = self._successfully_loaded_project_templates.get(self._current_project_id)
|
|
559
|
+
if project_info is None:
|
|
560
|
+
return GetCurrentProjectResultFailure(
|
|
561
|
+
result_details=f"Attempted to get current project. Failed because project not found for ID: '{self._current_project_id}'"
|
|
562
|
+
)
|
|
563
|
+
|
|
423
564
|
return GetCurrentProjectResultSuccess(
|
|
424
|
-
|
|
425
|
-
result_details="
|
|
565
|
+
project_info=project_info,
|
|
566
|
+
result_details=f"Successfully retrieved current project. ID: {self._current_project_id}",
|
|
426
567
|
)
|
|
427
568
|
|
|
428
|
-
def on_save_project_template_request(self, request: SaveProjectTemplateRequest) ->
|
|
569
|
+
def on_save_project_template_request(self, request: SaveProjectTemplateRequest) -> SaveProjectTemplateResultFailure:
|
|
429
570
|
"""Save user customizations to project.yml.
|
|
430
571
|
|
|
431
572
|
Flow:
|
|
@@ -436,128 +577,126 @@ class ProjectManager:
|
|
|
436
577
|
|
|
437
578
|
TODO: Implement saving logic when template system merges
|
|
438
579
|
"""
|
|
439
|
-
logger.debug("Saving project template: %s", request.project_path)
|
|
440
|
-
|
|
441
580
|
return SaveProjectTemplateResultFailure(
|
|
442
|
-
|
|
443
|
-
result_details="Template saving not yet implemented (stub)",
|
|
581
|
+
result_details=f"Attempted to save project template to '{request.project_path}'. Failed because template saving not yet implemented",
|
|
444
582
|
)
|
|
445
583
|
|
|
446
|
-
def on_match_path_against_macro_request(
|
|
447
|
-
|
|
584
|
+
def on_match_path_against_macro_request(
|
|
585
|
+
self, request: AttemptMatchPathAgainstMacroRequest
|
|
586
|
+
) -> AttemptMatchPathAgainstMacroResultSuccess | AttemptMatchPathAgainstMacroResultFailure:
|
|
587
|
+
"""Attempt to match a path against a macro schema and extract variables.
|
|
448
588
|
|
|
449
589
|
Flow:
|
|
450
|
-
1.
|
|
590
|
+
1. Check secrets manager is available (failure = true error)
|
|
451
591
|
2. Call ParsedMacro.extract_variables() with path and known variables
|
|
452
|
-
3. If match succeeds, return
|
|
453
|
-
4. If match fails, return
|
|
592
|
+
3. If match succeeds, return success with extracted_variables
|
|
593
|
+
4. If match fails, return success with match_failure (not an error)
|
|
454
594
|
"""
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return MatchPathAgainstMacroResultFailure(
|
|
459
|
-
match_failure=MacroMatchFailure(
|
|
460
|
-
failure_reason=MacroMatchFailureReason.INVALID_MACRO_SYNTAX,
|
|
461
|
-
expected_pattern=request.macro_schema,
|
|
462
|
-
known_variables_used=request.known_variables,
|
|
463
|
-
error_details="SecretsManager not available",
|
|
464
|
-
),
|
|
465
|
-
result_details="SecretsManager not available for macro matching",
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
try:
|
|
469
|
-
parsed_macro = ParsedMacro(request.macro_schema)
|
|
470
|
-
except MacroSyntaxError as err:
|
|
471
|
-
return MatchPathAgainstMacroResultFailure(
|
|
472
|
-
match_failure=MacroMatchFailure(
|
|
473
|
-
failure_reason=MacroMatchFailureReason.INVALID_MACRO_SYNTAX,
|
|
474
|
-
expected_pattern=request.macro_schema,
|
|
475
|
-
known_variables_used=request.known_variables,
|
|
476
|
-
error_details=str(err),
|
|
477
|
-
),
|
|
478
|
-
result_details=f"Invalid macro syntax: {err}",
|
|
595
|
+
if self._secrets_manager is None:
|
|
596
|
+
return AttemptMatchPathAgainstMacroResultFailure(
|
|
597
|
+
result_details=f"Attempted to match path '{request.file_path}' against macro '{request.parsed_macro.template}'. Failed because SecretsManager not available",
|
|
479
598
|
)
|
|
480
599
|
|
|
481
|
-
extracted = parsed_macro.extract_variables(
|
|
600
|
+
extracted = request.parsed_macro.extract_variables(
|
|
482
601
|
request.file_path,
|
|
483
602
|
request.known_variables,
|
|
484
|
-
self.
|
|
603
|
+
self._secrets_manager,
|
|
485
604
|
)
|
|
486
605
|
|
|
487
606
|
if extracted is None:
|
|
488
|
-
|
|
607
|
+
# Pattern didn't match - this is a normal outcome, not an error
|
|
608
|
+
return AttemptMatchPathAgainstMacroResultSuccess(
|
|
609
|
+
extracted_variables=None,
|
|
489
610
|
match_failure=MacroMatchFailure(
|
|
490
611
|
failure_reason=MacroMatchFailureReason.STATIC_TEXT_MISMATCH,
|
|
491
|
-
expected_pattern=request.
|
|
612
|
+
expected_pattern=request.parsed_macro.template,
|
|
492
613
|
known_variables_used=request.known_variables,
|
|
493
614
|
error_details=f"Path '{request.file_path}' does not match macro pattern",
|
|
494
615
|
),
|
|
495
|
-
result_details="
|
|
616
|
+
result_details=f"Attempted to match path '{request.file_path}' against macro '{request.parsed_macro.template}'. Pattern did not match",
|
|
496
617
|
)
|
|
497
618
|
|
|
498
|
-
|
|
619
|
+
# Pattern matched successfully
|
|
620
|
+
return AttemptMatchPathAgainstMacroResultSuccess(
|
|
499
621
|
extracted_variables=extracted,
|
|
500
|
-
|
|
622
|
+
match_failure=None,
|
|
623
|
+
result_details=f"Successfully matched path '{request.file_path}' against macro '{request.parsed_macro.template}'. Extracted {len(extracted)} variables",
|
|
501
624
|
)
|
|
502
625
|
|
|
503
|
-
def
|
|
504
|
-
|
|
626
|
+
def on_get_state_for_macro_request( # noqa: C901
|
|
627
|
+
self, request: GetStateForMacroRequest
|
|
628
|
+
) -> GetStateForMacroResultSuccess | GetStateForMacroResultFailure:
|
|
629
|
+
"""Analyze a macro and return comprehensive state information.
|
|
505
630
|
|
|
506
631
|
Flow:
|
|
507
|
-
1.
|
|
508
|
-
2.
|
|
509
|
-
3.
|
|
632
|
+
1. Get current project via GetCurrentProjectRequest
|
|
633
|
+
2. Get template from current project
|
|
634
|
+
3. For each variable, determine if it's:
|
|
635
|
+
- A directory (from template)
|
|
636
|
+
- User-provided (from request)
|
|
637
|
+
- A builtin
|
|
638
|
+
4. Check for conflicts:
|
|
639
|
+
- User providing directory name
|
|
640
|
+
- User overriding builtin with different value
|
|
641
|
+
5. Calculate what's satisfied vs missing
|
|
642
|
+
6. Determine if resolution would succeed
|
|
510
643
|
"""
|
|
511
|
-
|
|
644
|
+
current_project_request = GetCurrentProjectRequest()
|
|
645
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
512
646
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return GetVariablesForMacroResultFailure(
|
|
517
|
-
parse_failure=MacroParseFailure(
|
|
518
|
-
failure_reason=err.failure_reason or MacroParseFailureReason.UNEXPECTED_SEGMENT_TYPE,
|
|
519
|
-
error_position=err.error_position,
|
|
520
|
-
error_details=str(err),
|
|
521
|
-
),
|
|
522
|
-
result_details=f"Failed to parse macro: {err}",
|
|
647
|
+
if not isinstance(current_project_result, GetCurrentProjectResultSuccess):
|
|
648
|
+
return GetStateForMacroResultFailure(
|
|
649
|
+
result_details="Attempted to analyze macro state. Failed because no current project is set or template not loaded",
|
|
523
650
|
)
|
|
524
651
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return GetVariablesForMacroResultSuccess(
|
|
528
|
-
variables=variables,
|
|
529
|
-
result_details=f"Found {len(variables)} variables in macro",
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
def on_validate_macro_syntax_request(self, request: ValidateMacroSyntaxRequest) -> ResultPayload:
|
|
533
|
-
"""Validate a macro schema string for syntax errors.
|
|
652
|
+
project_info = current_project_result.project_info
|
|
653
|
+
template = project_info.template
|
|
534
654
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
3. If syntax error, return MacroParseFailure with details
|
|
539
|
-
"""
|
|
540
|
-
logger.debug("Validating macro syntax: %s", request.macro_schema)
|
|
655
|
+
all_variables = request.parsed_macro.get_variables()
|
|
656
|
+
directory_names = set(template.directories.keys())
|
|
657
|
+
user_provided_names = set(request.variables.keys())
|
|
541
658
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return ValidateMacroSyntaxResultFailure(
|
|
546
|
-
parse_failure=MacroParseFailure(
|
|
547
|
-
failure_reason=err.failure_reason or MacroParseFailureReason.UNEXPECTED_SEGMENT_TYPE,
|
|
548
|
-
error_position=err.error_position,
|
|
549
|
-
error_details=str(err),
|
|
550
|
-
),
|
|
551
|
-
partial_variables=[],
|
|
552
|
-
result_details=f"Syntax validation failed: {err}",
|
|
553
|
-
)
|
|
659
|
+
satisfied_variables: set[str] = set()
|
|
660
|
+
missing_required_variables: set[str] = set()
|
|
661
|
+
conflicting_variables: set[str] = set()
|
|
554
662
|
|
|
555
|
-
|
|
663
|
+
for var_info in all_variables:
|
|
664
|
+
var_name = var_info.name
|
|
556
665
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
666
|
+
if var_name in directory_names:
|
|
667
|
+
satisfied_variables.add(var_name)
|
|
668
|
+
if var_name in user_provided_names:
|
|
669
|
+
conflicting_variables.add(var_name)
|
|
670
|
+
|
|
671
|
+
if var_name in user_provided_names:
|
|
672
|
+
satisfied_variables.add(var_name)
|
|
673
|
+
|
|
674
|
+
if var_name in BUILTIN_VARIABLES:
|
|
675
|
+
try:
|
|
676
|
+
builtin_value = self._get_builtin_variable_value(var_name, project_info)
|
|
677
|
+
except (RuntimeError, NotImplementedError) as e:
|
|
678
|
+
return GetStateForMacroResultFailure(
|
|
679
|
+
result_details=f"Attempted to analyze macro state. Failed because builtin variable '{var_name}' cannot be resolved: {e}",
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
satisfied_variables.add(var_name)
|
|
683
|
+
if var_name in user_provided_names:
|
|
684
|
+
user_value = str(request.variables[var_name])
|
|
685
|
+
if user_value != builtin_value:
|
|
686
|
+
conflicting_variables.add(var_name)
|
|
687
|
+
|
|
688
|
+
if var_info.is_required and var_name not in satisfied_variables:
|
|
689
|
+
missing_required_variables.add(var_name)
|
|
690
|
+
|
|
691
|
+
can_resolve = len(missing_required_variables) == 0 and len(conflicting_variables) == 0
|
|
692
|
+
|
|
693
|
+
return GetStateForMacroResultSuccess(
|
|
694
|
+
all_variables=all_variables,
|
|
695
|
+
satisfied_variables=satisfied_variables,
|
|
696
|
+
missing_required_variables=missing_required_variables,
|
|
697
|
+
conflicting_variables=conflicting_variables,
|
|
698
|
+
can_resolve=can_resolve,
|
|
699
|
+
result_details=f"Analyzed macro with {len(all_variables)} variables: {len(satisfied_variables)} satisfied, {len(missing_required_variables)} missing, {len(conflicting_variables)} conflicting",
|
|
561
700
|
)
|
|
562
701
|
|
|
563
702
|
async def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
|
|
@@ -565,12 +704,10 @@ class ProjectManager:
|
|
|
565
704
|
|
|
566
705
|
Called by EventManager after all libraries are loaded.
|
|
567
706
|
"""
|
|
568
|
-
logger.debug("ProjectManager: Loading system default project template")
|
|
569
|
-
|
|
570
707
|
self._load_system_defaults()
|
|
571
708
|
|
|
572
709
|
# Set as current project (using synthetic key for system defaults)
|
|
573
|
-
set_request = SetCurrentProjectRequest(
|
|
710
|
+
set_request = SetCurrentProjectRequest(project_id=SYSTEM_DEFAULTS_KEY)
|
|
574
711
|
result = self.on_set_current_project_request(set_request)
|
|
575
712
|
|
|
576
713
|
if result.failed():
|
|
@@ -578,26 +715,310 @@ class ProjectManager:
|
|
|
578
715
|
else:
|
|
579
716
|
logger.debug("Successfully loaded default project template")
|
|
580
717
|
|
|
581
|
-
def on_get_all_situations_for_project_request(
|
|
582
|
-
|
|
583
|
-
|
|
718
|
+
def on_get_all_situations_for_project_request(
|
|
719
|
+
self, _request: GetAllSituationsForProjectRequest
|
|
720
|
+
) -> GetAllSituationsForProjectResultSuccess | GetAllSituationsForProjectResultFailure:
|
|
721
|
+
"""Get all situation names and schemas from current project template."""
|
|
722
|
+
current_project_request = GetCurrentProjectRequest()
|
|
723
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
584
724
|
|
|
585
|
-
|
|
725
|
+
if not isinstance(current_project_result, GetCurrentProjectResultSuccess):
|
|
726
|
+
return GetAllSituationsForProjectResultFailure(
|
|
727
|
+
result_details="Attempted to get all situations. Failed because no current project is set or template not loaded"
|
|
728
|
+
)
|
|
586
729
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
template_info = self.registered_template_status.get(request.project_path)
|
|
730
|
+
template = current_project_result.project_info.template
|
|
731
|
+
situations = {situation_name: situation.macro for situation_name, situation in template.situations.items()}
|
|
590
732
|
|
|
591
|
-
|
|
592
|
-
|
|
733
|
+
return GetAllSituationsForProjectResultSuccess(
|
|
734
|
+
situations=situations,
|
|
735
|
+
result_details=f"Successfully retrieved all situations. Found {len(situations)} situations",
|
|
736
|
+
)
|
|
593
737
|
|
|
594
|
-
|
|
595
|
-
|
|
738
|
+
def on_attempt_map_absolute_path_to_project_request(
|
|
739
|
+
self, request: AttemptMapAbsolutePathToProjectRequest
|
|
740
|
+
) -> AttemptMapAbsolutePathToProjectResultSuccess | AttemptMapAbsolutePathToProjectResultFailure:
|
|
741
|
+
"""Find out if an absolute path exists anywhere within a Project directory.
|
|
596
742
|
|
|
597
|
-
|
|
598
|
-
|
|
743
|
+
Returns Success with mapped_path if inside project (macro form returned).
|
|
744
|
+
Returns Success with None if outside project (valid answer: "not in project").
|
|
745
|
+
Returns Failure if operation cannot be performed (no project, no secrets manager).
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
request: Request containing the absolute path to check
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Success with mapped_path if path is inside project
|
|
752
|
+
Success with None if path is outside project
|
|
753
|
+
Failure if operation cannot be performed
|
|
754
|
+
"""
|
|
755
|
+
# Check prerequisites - return Failure if missing
|
|
756
|
+
current_project_request = GetCurrentProjectRequest()
|
|
757
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
758
|
+
|
|
759
|
+
if not isinstance(current_project_result, GetCurrentProjectResultSuccess):
|
|
760
|
+
return AttemptMapAbsolutePathToProjectResultFailure(
|
|
761
|
+
result_details="Attempted to map absolute path. Failed because no current project is set"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
if self._secrets_manager is None:
|
|
765
|
+
return AttemptMapAbsolutePathToProjectResultFailure(
|
|
766
|
+
result_details="Attempted to map absolute path. Failed because SecretsManager not available"
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
project_info = current_project_result.project_info
|
|
770
|
+
|
|
771
|
+
# Try to map the path
|
|
772
|
+
try:
|
|
773
|
+
mapped_path = self._absolute_path_to_macro_path(request.absolute_path, project_info)
|
|
774
|
+
except (RuntimeError, NotImplementedError) as e:
|
|
775
|
+
# Variable resolution failed - this is a Failure (can't complete the operation)
|
|
776
|
+
return AttemptMapAbsolutePathToProjectResultFailure(
|
|
777
|
+
result_details=f"Attempted to map absolute path '{request.absolute_path}'. Failed because: {e}"
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Path successfully checked
|
|
781
|
+
if mapped_path is None:
|
|
782
|
+
# Success: we successfully determined the path is outside project
|
|
783
|
+
return AttemptMapAbsolutePathToProjectResultSuccess(
|
|
784
|
+
mapped_path=None,
|
|
785
|
+
result_details=f"Attempted to map absolute path '{request.absolute_path}'. Path is outside all project directories",
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# Success: path mapped to macro form
|
|
789
|
+
return AttemptMapAbsolutePathToProjectResultSuccess(
|
|
790
|
+
mapped_path=mapped_path,
|
|
791
|
+
result_details=f"Successfully mapped absolute path to '{mapped_path}'",
|
|
599
792
|
)
|
|
600
793
|
|
|
794
|
+
# Helper methods (private)
|
|
795
|
+
|
|
796
|
+
@staticmethod
|
|
797
|
+
def _parse_situation_macros(
|
|
798
|
+
situations: dict[str, SituationTemplate], validation: ProjectValidationInfo
|
|
799
|
+
) -> dict[str, ParsedMacro]:
|
|
800
|
+
"""Parse all situation macros.
|
|
801
|
+
|
|
802
|
+
This is called BEFORE creating ProjectInfo to ensure all macros are valid.
|
|
803
|
+
Collects all parsing errors into the validation object instead of raising.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
situations: Dictionary of situation templates to parse
|
|
807
|
+
validation: Validation object to collect errors
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
Dictionary mapping situation_name to ParsedMacro (only for successfully parsed macros)
|
|
811
|
+
"""
|
|
812
|
+
situation_schemas: dict[str, ParsedMacro] = {}
|
|
813
|
+
|
|
814
|
+
for situation_name, situation in situations.items():
|
|
815
|
+
try:
|
|
816
|
+
situation_schemas[situation_name] = ParsedMacro(situation.macro)
|
|
817
|
+
except Exception as e:
|
|
818
|
+
validation.add_error(f"situations.{situation_name}.macro", f"Failed to parse macro: {e}")
|
|
819
|
+
|
|
820
|
+
return situation_schemas
|
|
821
|
+
|
|
822
|
+
@staticmethod
|
|
823
|
+
def _parse_directory_macros(
|
|
824
|
+
directories: dict[str, DirectoryDefinition], validation: ProjectValidationInfo
|
|
825
|
+
) -> dict[str, ParsedMacro]:
|
|
826
|
+
"""Parse all directory macros.
|
|
827
|
+
|
|
828
|
+
This is called BEFORE creating ProjectInfo to ensure all macros are valid.
|
|
829
|
+
Collects all parsing errors into the validation object instead of raising.
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
directories: Dictionary of directory definitions to parse
|
|
833
|
+
validation: Validation object to collect errors
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Dictionary mapping directory_name to ParsedMacro (only for successfully parsed macros)
|
|
837
|
+
"""
|
|
838
|
+
directory_schemas: dict[str, ParsedMacro] = {}
|
|
839
|
+
|
|
840
|
+
for directory_name, directory_def in directories.items():
|
|
841
|
+
try:
|
|
842
|
+
directory_schemas[directory_name] = ParsedMacro(directory_def.path_macro)
|
|
843
|
+
except Exception as e:
|
|
844
|
+
validation.add_error(f"directories.{directory_name}.path_macro", f"Failed to parse macro: {e}")
|
|
845
|
+
|
|
846
|
+
return directory_schemas
|
|
847
|
+
|
|
848
|
+
def _get_builtin_variable_value(self, var_name: str, project_info: ProjectInfo) -> str:
|
|
849
|
+
"""Get the value of a single builtin variable.
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
var_name: Name of the builtin variable
|
|
853
|
+
project_info: Information about the current project
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
String value of the builtin variable
|
|
857
|
+
|
|
858
|
+
Raises:
|
|
859
|
+
ValueError: If var_name is not a recognized builtin variable
|
|
860
|
+
NotImplementedError: If builtin variable is not yet implemented
|
|
861
|
+
"""
|
|
862
|
+
match var_name:
|
|
863
|
+
case "project_dir":
|
|
864
|
+
return str(project_info.project_base_dir)
|
|
865
|
+
|
|
866
|
+
case "project_name":
|
|
867
|
+
msg = f"{BUILTIN_PROJECT_NAME} not yet implemented"
|
|
868
|
+
raise NotImplementedError(msg)
|
|
869
|
+
|
|
870
|
+
case "workspace_dir":
|
|
871
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
872
|
+
workspace_dir = config_manager.get_config_value("workspace_directory")
|
|
873
|
+
if workspace_dir is None:
|
|
874
|
+
msg = "Attempted to resolve builtin variable '{workspace_dir}'. Failed because 'workspace_directory' config value was None"
|
|
875
|
+
raise RuntimeError(msg)
|
|
876
|
+
return str(workspace_dir)
|
|
877
|
+
|
|
878
|
+
case "workflow_name":
|
|
879
|
+
context_manager = GriptapeNodes.ContextManager()
|
|
880
|
+
if not context_manager.has_current_workflow():
|
|
881
|
+
msg = "No current workflow"
|
|
882
|
+
raise RuntimeError(msg)
|
|
883
|
+
return context_manager.get_current_workflow_name()
|
|
884
|
+
|
|
885
|
+
case "workflow_dir":
|
|
886
|
+
msg = f"{BUILTIN_WORKFLOW_DIR} not yet implemented"
|
|
887
|
+
raise NotImplementedError(msg)
|
|
888
|
+
|
|
889
|
+
case _:
|
|
890
|
+
msg = f"Unknown builtin variable: {var_name}"
|
|
891
|
+
raise ValueError(msg)
|
|
892
|
+
|
|
893
|
+
def _absolute_path_to_macro_path( # noqa: C901, PLR0912
|
|
894
|
+
self, absolute_path: Path, project_info: ProjectInfo
|
|
895
|
+
) -> str | None:
|
|
896
|
+
"""Convert an absolute path to macro form using longest prefix matching.
|
|
897
|
+
|
|
898
|
+
Resolves all project directories at runtime (to support env vars and macros),
|
|
899
|
+
then checks if the absolute path is within any of them.
|
|
900
|
+
Uses longest prefix matching to find the best match.
|
|
901
|
+
|
|
902
|
+
Args:
|
|
903
|
+
absolute_path: Absolute path to convert (e.g., /Users/james/project/outputs/file.png)
|
|
904
|
+
project_info: Information about the current project
|
|
905
|
+
|
|
906
|
+
Returns:
|
|
907
|
+
Macro-ified path (e.g., {outputs}/file.png) if inside a project directory,
|
|
908
|
+
or None if outside all project directories
|
|
909
|
+
|
|
910
|
+
Raises:
|
|
911
|
+
RuntimeError: If directory resolution fails or builtin variable cannot be resolved
|
|
912
|
+
NotImplementedError: If a required builtin variable is not yet implemented
|
|
913
|
+
|
|
914
|
+
Examples:
|
|
915
|
+
/Users/james/project/outputs/renders/file.png → "{outputs}/renders/file.png"
|
|
916
|
+
/Users/james/project/outputs/inputs/file.png → "{outputs}/inputs/file.png"
|
|
917
|
+
/Users/james/Downloads/file.png → None
|
|
918
|
+
"""
|
|
919
|
+
# Normalize paths for consistent cross-platform comparison
|
|
920
|
+
os_manager = GriptapeNodes.OSManager()
|
|
921
|
+
absolute_path = os_manager.resolve_path_safely(absolute_path)
|
|
922
|
+
|
|
923
|
+
template = project_info.template
|
|
924
|
+
project_base_dir = os_manager.resolve_path_safely(project_info.project_base_dir)
|
|
925
|
+
|
|
926
|
+
# Secrets manager must be available (checked by caller)
|
|
927
|
+
if self._secrets_manager is None:
|
|
928
|
+
msg = "SecretsManager not available"
|
|
929
|
+
raise RuntimeError(msg)
|
|
930
|
+
secrets_manager = self._secrets_manager
|
|
931
|
+
|
|
932
|
+
# Collect all variables used across ALL directory macros
|
|
933
|
+
variables_needed: set[str] = set()
|
|
934
|
+
for parsed_macro in project_info.parsed_directory_schemas.values():
|
|
935
|
+
variable_infos = parsed_macro.get_variables()
|
|
936
|
+
variables_needed.update(var_info.name for var_info in variable_infos)
|
|
937
|
+
|
|
938
|
+
# Build builtin variables dict - only resolve variables actually needed by the macros
|
|
939
|
+
# If a required variable fails to resolve, let the error propagate (will be caught by handler)
|
|
940
|
+
builtin_vars: dict[str, str | int] = {}
|
|
941
|
+
for var_name in variables_needed:
|
|
942
|
+
if var_name in BUILTIN_VARIABLES:
|
|
943
|
+
builtin_vars[var_name] = self._get_builtin_variable_value(var_name, project_info)
|
|
944
|
+
|
|
945
|
+
# Find all matching directories (where absolute_path is inside the directory)
|
|
946
|
+
class DirectoryMatch(NamedTuple):
|
|
947
|
+
directory_name: str
|
|
948
|
+
resolved_path: Path
|
|
949
|
+
prefix_length: int
|
|
950
|
+
|
|
951
|
+
matches: list[DirectoryMatch] = []
|
|
952
|
+
|
|
953
|
+
for directory_name in template.directories:
|
|
954
|
+
# Get parsed macro from project info cache
|
|
955
|
+
parsed_macro = project_info.parsed_directory_schemas.get(directory_name)
|
|
956
|
+
if parsed_macro is None:
|
|
957
|
+
msg = f"Directory '{directory_name}' not found in parsed schemas"
|
|
958
|
+
raise RuntimeError(msg)
|
|
959
|
+
|
|
960
|
+
try:
|
|
961
|
+
resolved_path_str = parsed_macro.resolve(builtin_vars, secrets_manager)
|
|
962
|
+
except MacroResolutionError as e:
|
|
963
|
+
msg = f"Failed to resolve directory '{directory_name}' macro: {e}"
|
|
964
|
+
raise RuntimeError(msg) from e
|
|
965
|
+
|
|
966
|
+
# Make absolute (resolve relative paths against project base directory)
|
|
967
|
+
resolved_dir_path = Path(resolved_path_str)
|
|
968
|
+
if not resolved_dir_path.is_absolute():
|
|
969
|
+
resolved_dir_path = project_base_dir / resolved_dir_path
|
|
970
|
+
# Normalize for consistent cross-platform comparison
|
|
971
|
+
resolved_dir_path = os_manager.resolve_path_safely(resolved_dir_path)
|
|
972
|
+
|
|
973
|
+
# Check if absolute_path is inside this directory
|
|
974
|
+
try:
|
|
975
|
+
# relative_to will raise ValueError if not a subpath
|
|
976
|
+
_ = absolute_path.relative_to(resolved_dir_path)
|
|
977
|
+
# Track the match with its prefix length (for longest match)
|
|
978
|
+
matches.append(
|
|
979
|
+
DirectoryMatch(
|
|
980
|
+
directory_name=directory_name,
|
|
981
|
+
resolved_path=resolved_dir_path,
|
|
982
|
+
prefix_length=len(resolved_dir_path.parts),
|
|
983
|
+
)
|
|
984
|
+
)
|
|
985
|
+
except ValueError:
|
|
986
|
+
# Not a subpath, skip
|
|
987
|
+
continue
|
|
988
|
+
|
|
989
|
+
# If no defined directories matched, try {project_dir} as fallback
|
|
990
|
+
if not matches:
|
|
991
|
+
# Check if path is inside project_base_dir
|
|
992
|
+
try:
|
|
993
|
+
relative_path = absolute_path.relative_to(project_base_dir)
|
|
994
|
+
|
|
995
|
+
# Convert to {project_dir} macro form
|
|
996
|
+
if str(relative_path) == ".":
|
|
997
|
+
return "{project_dir}"
|
|
998
|
+
return f"{{project_dir}}/{relative_path.as_posix()}"
|
|
999
|
+
except ValueError:
|
|
1000
|
+
# Not inside project_base_dir either
|
|
1001
|
+
return None
|
|
1002
|
+
|
|
1003
|
+
# Use longest prefix match (most specific directory)
|
|
1004
|
+
best_match = matches[0]
|
|
1005
|
+
for match in matches:
|
|
1006
|
+
if match.prefix_length > best_match.prefix_length:
|
|
1007
|
+
best_match = match
|
|
1008
|
+
|
|
1009
|
+
# Calculate relative path from the matched directory
|
|
1010
|
+
relative_path = absolute_path.relative_to(best_match.resolved_path)
|
|
1011
|
+
|
|
1012
|
+
# Convert to macro form
|
|
1013
|
+
if str(relative_path) == ".":
|
|
1014
|
+
# File is directly in the directory root
|
|
1015
|
+
# Example: /Users/james/project/outputs → {outputs}
|
|
1016
|
+
return f"{{{best_match.directory_name}}}"
|
|
1017
|
+
|
|
1018
|
+
# File is in a subdirectory
|
|
1019
|
+
# Example: /Users/james/project/outputs/renders/final.png → {outputs}/renders/final.png
|
|
1020
|
+
return f"{{{best_match.directory_name}}}/{relative_path.as_posix()}"
|
|
1021
|
+
|
|
601
1022
|
# Private helper methods
|
|
602
1023
|
|
|
603
1024
|
def _load_system_defaults(self) -> None:
|
|
@@ -611,7 +1032,36 @@ class ProjectManager:
|
|
|
611
1032
|
# Create validation info to track that defaults were loaded
|
|
612
1033
|
validation = ProjectValidationInfo(status=ProjectValidationStatus.GOOD)
|
|
613
1034
|
|
|
614
|
-
|
|
1035
|
+
# System defaults use workspace directory as the base directory
|
|
1036
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
1037
|
+
workspace_dir_value = config_manager.get_config_value("workspace_directory")
|
|
1038
|
+
if workspace_dir_value is None:
|
|
1039
|
+
msg = "Attempted to load Project Manager's default project schema. Failed because 'workspace_directory' config value was None"
|
|
1040
|
+
raise RuntimeError(msg)
|
|
1041
|
+
|
|
1042
|
+
try:
|
|
1043
|
+
workspace_dir = Path(workspace_dir_value)
|
|
1044
|
+
except (TypeError, ValueError) as e:
|
|
1045
|
+
msg = f"Attempted to load Project Manager's default project schema with workspace_directory='{workspace_dir_value}'. Failed due to {e}"
|
|
1046
|
+
logger.error(msg)
|
|
1047
|
+
raise RuntimeError(msg) from e
|
|
1048
|
+
|
|
1049
|
+
# Parse all macros BEFORE creating ProjectInfo (system defaults should always be valid)
|
|
1050
|
+
situation_schemas = self._parse_situation_macros(DEFAULT_PROJECT_TEMPLATE.situations, validation)
|
|
1051
|
+
directory_schemas = self._parse_directory_macros(DEFAULT_PROJECT_TEMPLATE.directories, validation)
|
|
1052
|
+
|
|
1053
|
+
# Create consolidated ProjectInfo with fully populated macro caches
|
|
1054
|
+
project_info = ProjectInfo(
|
|
1055
|
+
project_id=SYSTEM_DEFAULTS_KEY,
|
|
1056
|
+
project_file_path=None, # No actual file for system defaults
|
|
1057
|
+
project_base_dir=workspace_dir, # Use workspace as base
|
|
1058
|
+
template=DEFAULT_PROJECT_TEMPLATE,
|
|
1059
|
+
validation=validation,
|
|
1060
|
+
parsed_situation_schemas=situation_schemas,
|
|
1061
|
+
parsed_directory_schemas=directory_schemas,
|
|
1062
|
+
)
|
|
615
1063
|
|
|
616
|
-
|
|
617
|
-
self.
|
|
1064
|
+
# Store in new consolidated dict
|
|
1065
|
+
self._successfully_loaded_project_templates[SYSTEM_DEFAULTS_KEY] = project_info
|
|
1066
|
+
|
|
1067
|
+
logger.debug("System defaults loaded successfully")
|