griptape-nodes 0.60.4__py3-none-any.whl → 0.62.0__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/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
- griptape_nodes/common/macro_parser/__init__.py +16 -1
- griptape_nodes/common/macro_parser/core.py +19 -7
- griptape_nodes/common/macro_parser/exceptions.py +99 -0
- griptape_nodes/common/macro_parser/formats.py +13 -4
- griptape_nodes/common/macro_parser/matching.py +5 -2
- griptape_nodes/common/macro_parser/parsing.py +48 -8
- griptape_nodes/common/macro_parser/resolution.py +23 -5
- griptape_nodes/common/project_templates/__init__.py +49 -0
- griptape_nodes/common/project_templates/default_project_template.py +87 -0
- griptape_nodes/common/project_templates/defaults/README.md +36 -0
- griptape_nodes/common/project_templates/directory.py +67 -0
- griptape_nodes/common/project_templates/loader.py +342 -0
- griptape_nodes/common/project_templates/project.py +252 -0
- griptape_nodes/common/project_templates/situation.py +143 -0
- griptape_nodes/common/project_templates/validation.py +140 -0
- griptape_nodes/exe_types/core_types.py +36 -3
- griptape_nodes/exe_types/node_types.py +4 -2
- griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
- griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
- griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
- griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
- griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/execution_events.py +41 -0
- griptape_nodes/retained_mode/events/node_events.py +90 -1
- griptape_nodes/retained_mode/events/os_events.py +108 -0
- griptape_nodes/retained_mode/events/parameter_events.py +1 -1
- griptape_nodes/retained_mode/events/project_events.py +528 -0
- griptape_nodes/retained_mode/events/workflow_events.py +19 -1
- griptape_nodes/retained_mode/griptape_nodes.py +9 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
- griptape_nodes/retained_mode/managers/event_manager.py +6 -9
- griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
- griptape_nodes/retained_mode/managers/library_manager.py +55 -42
- griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
- griptape_nodes/retained_mode/managers/node_manager.py +232 -0
- griptape_nodes/retained_mode/managers/os_manager.py +399 -6
- griptape_nodes/retained_mode/managers/project_manager.py +1067 -0
- griptape_nodes/retained_mode/managers/settings.py +6 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +4 -1
- griptape_nodes/retained_mode/managers/workflow_manager.py +8 -79
- griptape_nodes/traits/button.py +19 -0
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/METADATA +5 -3
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/RECORD +47 -32
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
"""ProjectManager - Manages project templates and file save situations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
9
|
+
|
|
10
|
+
from griptape_nodes.common.macro_parser import (
|
|
11
|
+
MacroMatchFailure,
|
|
12
|
+
MacroMatchFailureReason,
|
|
13
|
+
MacroResolutionError,
|
|
14
|
+
MacroResolutionFailureReason,
|
|
15
|
+
ParsedMacro,
|
|
16
|
+
)
|
|
17
|
+
from griptape_nodes.common.project_templates import (
|
|
18
|
+
DEFAULT_PROJECT_TEMPLATE,
|
|
19
|
+
DirectoryDefinition,
|
|
20
|
+
ProjectTemplate,
|
|
21
|
+
ProjectValidationInfo,
|
|
22
|
+
ProjectValidationStatus,
|
|
23
|
+
SituationTemplate,
|
|
24
|
+
load_project_template_from_yaml,
|
|
25
|
+
)
|
|
26
|
+
from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
|
|
27
|
+
from griptape_nodes.retained_mode.events.os_events import ReadFileRequest, ReadFileResultSuccess
|
|
28
|
+
from griptape_nodes.retained_mode.events.project_events import (
|
|
29
|
+
AttemptMapAbsolutePathToProjectRequest,
|
|
30
|
+
AttemptMapAbsolutePathToProjectResultFailure,
|
|
31
|
+
AttemptMapAbsolutePathToProjectResultSuccess,
|
|
32
|
+
AttemptMatchPathAgainstMacroRequest,
|
|
33
|
+
AttemptMatchPathAgainstMacroResultFailure,
|
|
34
|
+
AttemptMatchPathAgainstMacroResultSuccess,
|
|
35
|
+
GetAllSituationsForProjectRequest,
|
|
36
|
+
GetAllSituationsForProjectResultFailure,
|
|
37
|
+
GetAllSituationsForProjectResultSuccess,
|
|
38
|
+
GetCurrentProjectRequest,
|
|
39
|
+
GetCurrentProjectResultFailure,
|
|
40
|
+
GetCurrentProjectResultSuccess,
|
|
41
|
+
GetPathForMacroRequest,
|
|
42
|
+
GetPathForMacroResultFailure,
|
|
43
|
+
GetPathForMacroResultSuccess,
|
|
44
|
+
GetProjectTemplateRequest,
|
|
45
|
+
GetProjectTemplateResultFailure,
|
|
46
|
+
GetProjectTemplateResultSuccess,
|
|
47
|
+
GetSituationRequest,
|
|
48
|
+
GetSituationResultFailure,
|
|
49
|
+
GetSituationResultSuccess,
|
|
50
|
+
GetStateForMacroRequest,
|
|
51
|
+
GetStateForMacroResultFailure,
|
|
52
|
+
GetStateForMacroResultSuccess,
|
|
53
|
+
ListProjectTemplatesRequest,
|
|
54
|
+
ListProjectTemplatesResultSuccess,
|
|
55
|
+
LoadProjectTemplateRequest,
|
|
56
|
+
LoadProjectTemplateResultFailure,
|
|
57
|
+
LoadProjectTemplateResultSuccess,
|
|
58
|
+
PathResolutionFailureReason,
|
|
59
|
+
ProjectTemplateInfo,
|
|
60
|
+
SaveProjectTemplateRequest,
|
|
61
|
+
SaveProjectTemplateResultFailure,
|
|
62
|
+
SetCurrentProjectRequest,
|
|
63
|
+
SetCurrentProjectResultSuccess,
|
|
64
|
+
)
|
|
65
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
66
|
+
|
|
67
|
+
if TYPE_CHECKING:
|
|
68
|
+
from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
|
|
69
|
+
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
70
|
+
from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
|
|
71
|
+
|
|
72
|
+
logger = logging.getLogger("griptape_nodes")
|
|
73
|
+
|
|
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"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class BuiltinVariableInfo:
|
|
91
|
+
"""Metadata about a builtin variable.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
name: The variable name (e.g., "project_dir")
|
|
95
|
+
is_directory: Whether this variable represents a directory path
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
name: str
|
|
99
|
+
is_directory: bool
|
|
100
|
+
|
|
101
|
+
|
|
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
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ProjectManager:
|
|
138
|
+
"""Manages project templates, validation, and file path resolution.
|
|
139
|
+
|
|
140
|
+
Responsibilities:
|
|
141
|
+
- Load and cache project templates (system defaults + user customizations)
|
|
142
|
+
- Track validation status for all load attempts (including MISSING files)
|
|
143
|
+
- Parse and cache macro schemas for performance
|
|
144
|
+
- Resolve file paths using situation templates and variable substitution
|
|
145
|
+
- Manage current project selection
|
|
146
|
+
- Handle project.yml file I/O via OSManager events
|
|
147
|
+
|
|
148
|
+
State tracking uses two dicts:
|
|
149
|
+
- registered_template_status: ALL load attempts (Path -> ProjectValidationInfo)
|
|
150
|
+
- successful_templates: Only usable templates (Path -> ProjectTemplate)
|
|
151
|
+
|
|
152
|
+
This allows UI to query validation status even when template failed to load.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
event_manager: EventManager | None = None,
|
|
158
|
+
config_manager: ConfigManager | None = None,
|
|
159
|
+
secrets_manager: SecretsManager | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Initialize the ProjectManager.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
event_manager: The EventManager instance to use for event handling
|
|
165
|
+
config_manager: ConfigManager instance for accessing configuration
|
|
166
|
+
secrets_manager: SecretsManager instance for macro resolution
|
|
167
|
+
"""
|
|
168
|
+
self._config_manager = config_manager
|
|
169
|
+
self._secrets_manager = secrets_manager
|
|
170
|
+
|
|
171
|
+
# Consolidated project information storage
|
|
172
|
+
self._successfully_loaded_project_templates: dict[ProjectID, ProjectInfo] = {}
|
|
173
|
+
self._current_project_id: ProjectID | None = None
|
|
174
|
+
|
|
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] = {}
|
|
178
|
+
|
|
179
|
+
# Register event handlers
|
|
180
|
+
if event_manager is not None:
|
|
181
|
+
event_manager.assign_manager_to_request_type(
|
|
182
|
+
LoadProjectTemplateRequest, self.on_load_project_template_request
|
|
183
|
+
)
|
|
184
|
+
event_manager.assign_manager_to_request_type(
|
|
185
|
+
GetProjectTemplateRequest, self.on_get_project_template_request
|
|
186
|
+
)
|
|
187
|
+
event_manager.assign_manager_to_request_type(
|
|
188
|
+
ListProjectTemplatesRequest, self.on_list_project_templates_request
|
|
189
|
+
)
|
|
190
|
+
event_manager.assign_manager_to_request_type(GetSituationRequest, self.on_get_situation_request)
|
|
191
|
+
event_manager.assign_manager_to_request_type(GetPathForMacroRequest, self.on_get_path_for_macro_request)
|
|
192
|
+
event_manager.assign_manager_to_request_type(SetCurrentProjectRequest, self.on_set_current_project_request)
|
|
193
|
+
event_manager.assign_manager_to_request_type(GetCurrentProjectRequest, self.on_get_current_project_request)
|
|
194
|
+
event_manager.assign_manager_to_request_type(
|
|
195
|
+
SaveProjectTemplateRequest, self.on_save_project_template_request
|
|
196
|
+
)
|
|
197
|
+
event_manager.assign_manager_to_request_type(
|
|
198
|
+
AttemptMatchPathAgainstMacroRequest, self.on_match_path_against_macro_request
|
|
199
|
+
)
|
|
200
|
+
event_manager.assign_manager_to_request_type(GetStateForMacroRequest, self.on_get_state_for_macro_request)
|
|
201
|
+
event_manager.assign_manager_to_request_type(
|
|
202
|
+
GetAllSituationsForProjectRequest, self.on_get_all_situations_for_project_request
|
|
203
|
+
)
|
|
204
|
+
event_manager.assign_manager_to_request_type(
|
|
205
|
+
AttemptMapAbsolutePathToProjectRequest, self.on_attempt_map_absolute_path_to_project_request
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Register app initialization listener
|
|
209
|
+
event_manager.add_listener_to_app_event(
|
|
210
|
+
AppInitializationComplete,
|
|
211
|
+
self.on_app_initialization_complete,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def on_load_project_template_request(
|
|
215
|
+
self, request: LoadProjectTemplateRequest
|
|
216
|
+
) -> LoadProjectTemplateResultSuccess | LoadProjectTemplateResultFailure:
|
|
217
|
+
"""Load user's project.yml and merge with system defaults.
|
|
218
|
+
|
|
219
|
+
Flow:
|
|
220
|
+
1. Issue ReadFileRequest to OSManager (for proper Windows long path handling)
|
|
221
|
+
2. Parse YAML and load partial template (overlay) using load_partial_project_template()
|
|
222
|
+
3. Merge with system defaults using ProjectTemplate.merge()
|
|
223
|
+
4. Cache validation in registered_template_status
|
|
224
|
+
5. If usable, cache template in successful_templates
|
|
225
|
+
6. Return LoadProjectTemplateResultSuccess or LoadProjectTemplateResultFailure
|
|
226
|
+
"""
|
|
227
|
+
read_request = ReadFileRequest(
|
|
228
|
+
file_path=str(request.project_path),
|
|
229
|
+
encoding="utf-8",
|
|
230
|
+
workspace_only=False,
|
|
231
|
+
)
|
|
232
|
+
read_result = GriptapeNodes.handle_request(read_request)
|
|
233
|
+
|
|
234
|
+
if read_result.failed():
|
|
235
|
+
validation = ProjectValidationInfo(status=ProjectValidationStatus.MISSING)
|
|
236
|
+
self._registered_template_status[request.project_path] = validation
|
|
237
|
+
|
|
238
|
+
return LoadProjectTemplateResultFailure(
|
|
239
|
+
validation=validation,
|
|
240
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because file not found",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not isinstance(read_result, ReadFileResultSuccess):
|
|
244
|
+
validation = ProjectValidationInfo(status=ProjectValidationStatus.UNUSABLE)
|
|
245
|
+
self._registered_template_status[request.project_path] = validation
|
|
246
|
+
|
|
247
|
+
return LoadProjectTemplateResultFailure(
|
|
248
|
+
validation=validation,
|
|
249
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because file read returned unexpected result type",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
yaml_text = read_result.content
|
|
253
|
+
if not isinstance(yaml_text, str):
|
|
254
|
+
validation = ProjectValidationInfo(status=ProjectValidationStatus.UNUSABLE)
|
|
255
|
+
self._registered_template_status[request.project_path] = validation
|
|
256
|
+
|
|
257
|
+
return LoadProjectTemplateResultFailure(
|
|
258
|
+
validation=validation,
|
|
259
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because template must be text, got binary content",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
validation = ProjectValidationInfo(status=ProjectValidationStatus.GOOD)
|
|
263
|
+
template = load_project_template_from_yaml(yaml_text, validation)
|
|
264
|
+
|
|
265
|
+
if template is None:
|
|
266
|
+
self._registered_template_status[request.project_path] = validation
|
|
267
|
+
return LoadProjectTemplateResultFailure(
|
|
268
|
+
validation=validation,
|
|
269
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because YAML could not be parsed",
|
|
270
|
+
)
|
|
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
|
|
282
|
+
if not validation.is_usable():
|
|
283
|
+
self._registered_template_status[request.project_path] = validation
|
|
284
|
+
return LoadProjectTemplateResultFailure(
|
|
285
|
+
validation=validation,
|
|
286
|
+
result_details=f"Attempted to load project template from '{request.project_path}'. Failed because template is not usable (status: {validation.status})",
|
|
287
|
+
)
|
|
288
|
+
|
|
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
|
|
302
|
+
|
|
303
|
+
# Track validation status for all load attempts (for UI display)
|
|
304
|
+
self._registered_template_status[request.project_path] = validation
|
|
305
|
+
|
|
306
|
+
return LoadProjectTemplateResultSuccess(
|
|
307
|
+
project_id=project_id,
|
|
308
|
+
template=template,
|
|
309
|
+
validation=validation,
|
|
310
|
+
result_details=f"Template loaded successfully with status: {validation.status}",
|
|
311
|
+
)
|
|
312
|
+
|
|
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)
|
|
318
|
+
|
|
319
|
+
if project_info is None:
|
|
320
|
+
return GetProjectTemplateResultFailure(
|
|
321
|
+
result_details=f"Attempted to get project template for '{request.project_id}'. Failed because template not loaded yet",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return GetProjectTemplateResultSuccess(
|
|
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}",
|
|
328
|
+
)
|
|
329
|
+
|
|
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.
|
|
377
|
+
|
|
378
|
+
Flow:
|
|
379
|
+
1. Get current project
|
|
380
|
+
2. Get template from successful_templates
|
|
381
|
+
3. Get situation from template
|
|
382
|
+
4. Return complete SituationTemplate
|
|
383
|
+
"""
|
|
384
|
+
current_project_request = GetCurrentProjectRequest()
|
|
385
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
386
|
+
|
|
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",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
template = current_project_result.project_info.template
|
|
393
|
+
|
|
394
|
+
situation = template.situations.get(request.situation_name)
|
|
395
|
+
if situation is None:
|
|
396
|
+
return GetSituationResultFailure(
|
|
397
|
+
result_details=f"Attempted to get situation '{request.situation_name}'. Failed because situation not found",
|
|
398
|
+
)
|
|
399
|
+
|
|
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}",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def on_get_path_for_macro_request( # noqa: C901, PLR0911, PLR0912, PLR0915
|
|
406
|
+
self, request: GetPathForMacroRequest
|
|
407
|
+
) -> GetPathForMacroResultSuccess | GetPathForMacroResultFailure:
|
|
408
|
+
"""Resolve ANY macro schema with variables to final Path.
|
|
409
|
+
|
|
410
|
+
Flow:
|
|
411
|
+
1. Get current project
|
|
412
|
+
2. Get variables from ParsedMacro.get_variables()
|
|
413
|
+
3. For each variable:
|
|
414
|
+
- If in directories dict → resolve directory, add to resolution bag
|
|
415
|
+
- Else if in user_supplied_vars → use user value
|
|
416
|
+
- If in BOTH → ERROR: DIRECTORY_OVERRIDE_ATTEMPTED
|
|
417
|
+
- Else → collect as missing
|
|
418
|
+
4. If any missing → ERROR: MISSING_REQUIRED_VARIABLES
|
|
419
|
+
5. Resolve macro with complete variable bag
|
|
420
|
+
6. Return resolved Path
|
|
421
|
+
"""
|
|
422
|
+
current_project_request = GetCurrentProjectRequest()
|
|
423
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
424
|
+
|
|
425
|
+
if not isinstance(current_project_result, GetCurrentProjectResultSuccess):
|
|
426
|
+
return GetPathForMacroResultFailure(
|
|
427
|
+
failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
|
|
428
|
+
result_details="Attempted to resolve macro path. Failed because no current project is set or template not loaded",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
project_info = current_project_result.project_info
|
|
432
|
+
template = project_info.template
|
|
433
|
+
|
|
434
|
+
variable_infos = request.parsed_macro.get_variables()
|
|
435
|
+
directory_names = set(template.directories.keys())
|
|
436
|
+
user_provided_names = set(request.variables.keys())
|
|
437
|
+
|
|
438
|
+
# Check for directory/user variable name conflicts
|
|
439
|
+
conflicting = directory_names & user_provided_names
|
|
440
|
+
if conflicting:
|
|
441
|
+
return GetPathForMacroResultFailure(
|
|
442
|
+
failure_reason=PathResolutionFailureReason.DIRECTORY_OVERRIDE_ATTEMPTED,
|
|
443
|
+
conflicting_variables=conflicting,
|
|
444
|
+
result_details=f"Attempted to resolve macro path. Failed because variables conflict with directory names: {', '.join(sorted(conflicting))}",
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
resolution_bag: dict[str, str | int] = {}
|
|
448
|
+
disallowed_overrides: set[str] = set()
|
|
449
|
+
|
|
450
|
+
for var_info in variable_infos:
|
|
451
|
+
var_name = var_info.name
|
|
452
|
+
|
|
453
|
+
if var_name in directory_names:
|
|
454
|
+
directory_def = template.directories[var_name]
|
|
455
|
+
resolution_bag[var_name] = directory_def.path_macro
|
|
456
|
+
elif var_name in user_provided_names:
|
|
457
|
+
resolution_bag[var_name] = request.variables[var_name]
|
|
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
|
+
|
|
491
|
+
required_vars = {v.name for v in variable_infos if v.is_required}
|
|
492
|
+
provided_vars = set(resolution_bag.keys())
|
|
493
|
+
missing = required_vars - provided_vars
|
|
494
|
+
|
|
495
|
+
if missing:
|
|
496
|
+
return GetPathForMacroResultFailure(
|
|
497
|
+
failure_reason=PathResolutionFailureReason.MISSING_REQUIRED_VARIABLES,
|
|
498
|
+
missing_variables=missing,
|
|
499
|
+
result_details=f"Attempted to resolve macro path. Failed because missing required variables: {', '.join(sorted(missing))}",
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if self._secrets_manager is None:
|
|
503
|
+
return GetPathForMacroResultFailure(
|
|
504
|
+
failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
|
|
505
|
+
result_details="Attempted to resolve macro path. Failed because SecretsManager is not available",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
resolved_string = request.parsed_macro.resolve(resolution_bag, self._secrets_manager)
|
|
510
|
+
except MacroResolutionError as e:
|
|
511
|
+
if e.failure_reason == MacroResolutionFailureReason.MISSING_REQUIRED_VARIABLES:
|
|
512
|
+
path_failure_reason = PathResolutionFailureReason.MISSING_REQUIRED_VARIABLES
|
|
513
|
+
else:
|
|
514
|
+
path_failure_reason = PathResolutionFailureReason.MACRO_RESOLUTION_ERROR
|
|
515
|
+
|
|
516
|
+
return GetPathForMacroResultFailure(
|
|
517
|
+
failure_reason=path_failure_reason,
|
|
518
|
+
missing_variables=e.missing_variables,
|
|
519
|
+
result_details=f"Attempted to resolve macro path. Failed because macro resolution error: {e}",
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
resolved_path = Path(resolved_string)
|
|
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
|
+
|
|
530
|
+
return GetPathForMacroResultSuccess(
|
|
531
|
+
resolved_path=resolved_path,
|
|
532
|
+
absolute_path=absolute_path,
|
|
533
|
+
result_details=f"Successfully resolved macro path. Result: {resolved_path}",
|
|
534
|
+
)
|
|
535
|
+
|
|
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
|
|
539
|
+
|
|
540
|
+
if request.project_id is None:
|
|
541
|
+
return SetCurrentProjectResultSuccess(
|
|
542
|
+
result_details="Successfully set current project. No project selected",
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return SetCurrentProjectResultSuccess(
|
|
546
|
+
result_details=f"Successfully set current project. ID: {request.project_id}",
|
|
547
|
+
)
|
|
548
|
+
|
|
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
|
+
|
|
564
|
+
return GetCurrentProjectResultSuccess(
|
|
565
|
+
project_info=project_info,
|
|
566
|
+
result_details=f"Successfully retrieved current project. ID: {self._current_project_id}",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def on_save_project_template_request(self, request: SaveProjectTemplateRequest) -> SaveProjectTemplateResultFailure:
|
|
570
|
+
"""Save user customizations to project.yml.
|
|
571
|
+
|
|
572
|
+
Flow:
|
|
573
|
+
1. Convert template_data to YAML format
|
|
574
|
+
2. Issue WriteFileRequest to OSManager
|
|
575
|
+
3. Handle write result
|
|
576
|
+
4. Invalidate cache (force reload on next access)
|
|
577
|
+
|
|
578
|
+
TODO: Implement saving logic when template system merges
|
|
579
|
+
"""
|
|
580
|
+
return SaveProjectTemplateResultFailure(
|
|
581
|
+
result_details=f"Attempted to save project template to '{request.project_path}'. Failed because template saving not yet implemented",
|
|
582
|
+
)
|
|
583
|
+
|
|
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.
|
|
588
|
+
|
|
589
|
+
Flow:
|
|
590
|
+
1. Check secrets manager is available (failure = true error)
|
|
591
|
+
2. Call ParsedMacro.extract_variables() with path and known variables
|
|
592
|
+
3. If match succeeds, return success with extracted_variables
|
|
593
|
+
4. If match fails, return success with match_failure (not an error)
|
|
594
|
+
"""
|
|
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",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
extracted = request.parsed_macro.extract_variables(
|
|
601
|
+
request.file_path,
|
|
602
|
+
request.known_variables,
|
|
603
|
+
self._secrets_manager,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
if extracted is None:
|
|
607
|
+
# Pattern didn't match - this is a normal outcome, not an error
|
|
608
|
+
return AttemptMatchPathAgainstMacroResultSuccess(
|
|
609
|
+
extracted_variables=None,
|
|
610
|
+
match_failure=MacroMatchFailure(
|
|
611
|
+
failure_reason=MacroMatchFailureReason.STATIC_TEXT_MISMATCH,
|
|
612
|
+
expected_pattern=request.parsed_macro.template,
|
|
613
|
+
known_variables_used=request.known_variables,
|
|
614
|
+
error_details=f"Path '{request.file_path}' does not match macro pattern",
|
|
615
|
+
),
|
|
616
|
+
result_details=f"Attempted to match path '{request.file_path}' against macro '{request.parsed_macro.template}'. Pattern did not match",
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Pattern matched successfully
|
|
620
|
+
return AttemptMatchPathAgainstMacroResultSuccess(
|
|
621
|
+
extracted_variables=extracted,
|
|
622
|
+
match_failure=None,
|
|
623
|
+
result_details=f"Successfully matched path '{request.file_path}' against macro '{request.parsed_macro.template}'. Extracted {len(extracted)} variables",
|
|
624
|
+
)
|
|
625
|
+
|
|
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.
|
|
630
|
+
|
|
631
|
+
Flow:
|
|
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
|
|
643
|
+
"""
|
|
644
|
+
current_project_request = GetCurrentProjectRequest()
|
|
645
|
+
current_project_result = self.on_get_current_project_request(current_project_request)
|
|
646
|
+
|
|
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",
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
project_info = current_project_result.project_info
|
|
653
|
+
template = project_info.template
|
|
654
|
+
|
|
655
|
+
all_variables = request.parsed_macro.get_variables()
|
|
656
|
+
directory_names = set(template.directories.keys())
|
|
657
|
+
user_provided_names = set(request.variables.keys())
|
|
658
|
+
|
|
659
|
+
satisfied_variables: set[str] = set()
|
|
660
|
+
missing_required_variables: set[str] = set()
|
|
661
|
+
conflicting_variables: set[str] = set()
|
|
662
|
+
|
|
663
|
+
for var_info in all_variables:
|
|
664
|
+
var_name = var_info.name
|
|
665
|
+
|
|
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",
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
async def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
|
|
703
|
+
"""Load system default project template when app initializes.
|
|
704
|
+
|
|
705
|
+
Called by EventManager after all libraries are loaded.
|
|
706
|
+
"""
|
|
707
|
+
self._load_system_defaults()
|
|
708
|
+
|
|
709
|
+
# Set as current project (using synthetic key for system defaults)
|
|
710
|
+
set_request = SetCurrentProjectRequest(project_id=SYSTEM_DEFAULTS_KEY)
|
|
711
|
+
result = self.on_set_current_project_request(set_request)
|
|
712
|
+
|
|
713
|
+
if result.failed():
|
|
714
|
+
logger.error("Failed to set default project as current: %s", result.result_details)
|
|
715
|
+
else:
|
|
716
|
+
logger.debug("Successfully loaded default project template")
|
|
717
|
+
|
|
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)
|
|
724
|
+
|
|
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
|
+
)
|
|
729
|
+
|
|
730
|
+
template = current_project_result.project_info.template
|
|
731
|
+
situations = {situation_name: situation.macro for situation_name, situation in template.situations.items()}
|
|
732
|
+
|
|
733
|
+
return GetAllSituationsForProjectResultSuccess(
|
|
734
|
+
situations=situations,
|
|
735
|
+
result_details=f"Successfully retrieved all situations. Found {len(situations)} situations",
|
|
736
|
+
)
|
|
737
|
+
|
|
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.
|
|
742
|
+
|
|
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}'",
|
|
792
|
+
)
|
|
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
|
+
|
|
1022
|
+
# Private helper methods
|
|
1023
|
+
|
|
1024
|
+
def _load_system_defaults(self) -> None:
|
|
1025
|
+
"""Load bundled system default template.
|
|
1026
|
+
|
|
1027
|
+
System defaults are now defined in Python as DEFAULT_PROJECT_TEMPLATE.
|
|
1028
|
+
This is always valid by construction.
|
|
1029
|
+
"""
|
|
1030
|
+
logger.debug("Loading system default template")
|
|
1031
|
+
|
|
1032
|
+
# Create validation info to track that defaults were loaded
|
|
1033
|
+
validation = ProjectValidationInfo(status=ProjectValidationStatus.GOOD)
|
|
1034
|
+
|
|
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
|
+
)
|
|
1063
|
+
|
|
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")
|