griptape-nodes 0.60.4__py3-none-any.whl → 0.61.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 +15 -3
- 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 +92 -0
- griptape_nodes/common/project_templates/defaults/README.md +36 -0
- griptape_nodes/common/project_templates/defaults/project_template.yml +89 -0
- griptape_nodes/common/project_templates/directory.py +67 -0
- griptape_nodes/common/project_templates/loader.py +341 -0
- griptape_nodes/common/project_templates/project.py +252 -0
- griptape_nodes/common/project_templates/situation.py +155 -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 +413 -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 +345 -0
- griptape_nodes/retained_mode/managers/project_manager.py +617 -0
- griptape_nodes/retained_mode/managers/settings.py +6 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +6 -69
- griptape_nodes/traits/button.py +18 -0
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# System Default Project Template
|
|
2
|
+
# Version: 0.1.0
|
|
3
|
+
#
|
|
4
|
+
# This is the default configuration used when no project.yml exists.
|
|
5
|
+
# You can copy this file to your project root and customize it.
|
|
6
|
+
|
|
7
|
+
project_template_schema_version: "0.1.0"
|
|
8
|
+
name: "Default Project"
|
|
9
|
+
description: "System default configuration"
|
|
10
|
+
|
|
11
|
+
# Directory Definitions
|
|
12
|
+
#
|
|
13
|
+
# Define logical directories used in situation schemas.
|
|
14
|
+
# Paths can be:
|
|
15
|
+
# - Relative: "inputs", "outputs/renders"
|
|
16
|
+
# - Absolute: "/tmp/cache", "C:/Projects/assets"
|
|
17
|
+
# - With macros: Path schemas will be resolved by ProjectManager
|
|
18
|
+
directories:
|
|
19
|
+
inputs:
|
|
20
|
+
path_schema: "inputs"
|
|
21
|
+
outputs:
|
|
22
|
+
path_schema: "outputs"
|
|
23
|
+
temp:
|
|
24
|
+
path_schema: "temp"
|
|
25
|
+
previews:
|
|
26
|
+
path_schema: "previews"
|
|
27
|
+
|
|
28
|
+
# Environment Variables (Optional)
|
|
29
|
+
#
|
|
30
|
+
# Define project-specific variables or document which system env vars are used.
|
|
31
|
+
# System automatically detects all env vars - this section is for documentation
|
|
32
|
+
# and project-specific overrides.
|
|
33
|
+
environment: {}
|
|
34
|
+
|
|
35
|
+
# Situation Configurations
|
|
36
|
+
#
|
|
37
|
+
# Each situation defines how files are saved in a specific scenario.
|
|
38
|
+
# The schema uses macro syntax: {variable_name} or {variable_name?} for optional
|
|
39
|
+
situations:
|
|
40
|
+
|
|
41
|
+
# Base situation - others fall back to this
|
|
42
|
+
save_file:
|
|
43
|
+
situation_template_schema_version: "0.1.0"
|
|
44
|
+
description: "Generic file save operation"
|
|
45
|
+
schema: "{file_name_base}{_index:03?}.{file_extension}"
|
|
46
|
+
policy:
|
|
47
|
+
on_collision: "create_new" # Increment {_index}: file_001.jpg, file_002.jpg
|
|
48
|
+
create_dirs: true
|
|
49
|
+
fallback: null
|
|
50
|
+
|
|
51
|
+
# User copies external file to project
|
|
52
|
+
copy_external_file:
|
|
53
|
+
situation_template_schema_version: "0.1.0"
|
|
54
|
+
description: "User copies external file to project"
|
|
55
|
+
schema: "{inputs}/{node_name?:_}{parameter_name?:_}{file_name_base}{_index:03?}.{file_extension}"
|
|
56
|
+
policy:
|
|
57
|
+
on_collision: "create_new"
|
|
58
|
+
create_dirs: true
|
|
59
|
+
fallback: "save_file"
|
|
60
|
+
|
|
61
|
+
# Download file from URL
|
|
62
|
+
download_url:
|
|
63
|
+
situation_template_schema_version: "0.1.0"
|
|
64
|
+
description: "Download file from URL"
|
|
65
|
+
schema: "{inputs}/{sanitized_url}"
|
|
66
|
+
policy:
|
|
67
|
+
on_collision: "overwrite" # Same URL = same content, safe to replace
|
|
68
|
+
create_dirs: true
|
|
69
|
+
fallback: "save_file"
|
|
70
|
+
|
|
71
|
+
# Node generates and saves output
|
|
72
|
+
save_node_output:
|
|
73
|
+
situation_template_schema_version: "0.1.0"
|
|
74
|
+
description: "Node generates and saves output"
|
|
75
|
+
schema: "{outputs}/{node_name?:_}{file_name_base}{_index:03?}.{file_extension}"
|
|
76
|
+
policy:
|
|
77
|
+
on_collision: "create_new"
|
|
78
|
+
create_dirs: true
|
|
79
|
+
fallback: "save_file"
|
|
80
|
+
|
|
81
|
+
# Generate preview/thumbnail
|
|
82
|
+
save_preview:
|
|
83
|
+
situation_template_schema_version: "0.1.0"
|
|
84
|
+
description: "Generate preview/thumbnail"
|
|
85
|
+
schema: "{previews}/{original_file_path}"
|
|
86
|
+
policy:
|
|
87
|
+
on_collision: "overwrite" # Preview should match source, safe to replace
|
|
88
|
+
create_dirs: true
|
|
89
|
+
fallback: "save_file"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Directory definition for logical project directories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from griptape_nodes.common.project_templates.loader import YAMLLineInfo
|
|
11
|
+
from griptape_nodes.common.project_templates.validation import ProjectValidationInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DirectoryDefinition(BaseModel):
|
|
15
|
+
"""Definition of a logical directory in the project."""
|
|
16
|
+
|
|
17
|
+
name: str = Field(description="Logical name (e.g., 'inputs', 'outputs')")
|
|
18
|
+
path_schema: str = Field(description="Path string (may contain macros/env vars)")
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def merge(
|
|
22
|
+
base: DirectoryDefinition,
|
|
23
|
+
overlay_data: dict[str, Any],
|
|
24
|
+
field_path: str,
|
|
25
|
+
validation_info: ProjectValidationInfo,
|
|
26
|
+
line_info: YAMLLineInfo,
|
|
27
|
+
) -> DirectoryDefinition:
|
|
28
|
+
"""Merge overlay fields onto base directory.
|
|
29
|
+
|
|
30
|
+
Field-level merge behavior:
|
|
31
|
+
- path_schema: Use overlay if present, else base
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
base: Complete base directory
|
|
35
|
+
overlay_data: Partial directory dict from overlay
|
|
36
|
+
field_path: Path for validation errors (e.g., "directories.inputs")
|
|
37
|
+
validation_info: Shared validation info
|
|
38
|
+
line_info: Line tracking from overlay
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
New merged DirectoryDefinition
|
|
42
|
+
"""
|
|
43
|
+
# Start with base fields
|
|
44
|
+
merged_data = {"name": base.name, "path_schema": base.path_schema}
|
|
45
|
+
|
|
46
|
+
# Apply overlay if present
|
|
47
|
+
if "path_schema" in overlay_data:
|
|
48
|
+
merged_data["path_schema"] = overlay_data["path_schema"]
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
return DirectoryDefinition.model_validate(merged_data)
|
|
52
|
+
except ValidationError as e:
|
|
53
|
+
# Convert Pydantic validation errors to our validation_info format
|
|
54
|
+
for error in e.errors():
|
|
55
|
+
error_field_path = ".".join(str(loc) for loc in error["loc"])
|
|
56
|
+
full_field_path = f"{field_path}.{error_field_path}"
|
|
57
|
+
message = error["msg"]
|
|
58
|
+
line_number = line_info.get_line(full_field_path)
|
|
59
|
+
|
|
60
|
+
validation_info.add_error(
|
|
61
|
+
field_path=full_field_path,
|
|
62
|
+
message=message,
|
|
63
|
+
line_number=line_number,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Return base on validation error (fault-tolerant)
|
|
67
|
+
return base
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""YAML loading with line number tracking for project templates.
|
|
2
|
+
|
|
3
|
+
Note: This module only handles YAML parsing. File I/O operations should be
|
|
4
|
+
handled by ProjectManager using ReadFileRequest/WriteFileRequest to ensure
|
|
5
|
+
proper long path handling on Windows and consistent error handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
12
|
+
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
from ruamel.yaml import YAML
|
|
15
|
+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
16
|
+
from ruamel.yaml.error import YAMLError
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from griptape_nodes.common.project_templates.project import ProjectTemplate
|
|
20
|
+
from griptape_nodes.common.project_templates.validation import ProjectValidationInfo
|
|
21
|
+
|
|
22
|
+
# Field name constants
|
|
23
|
+
FIELD_NAME = "name"
|
|
24
|
+
FIELD_SITUATIONS = "situations"
|
|
25
|
+
FIELD_DIRECTORIES = "directories"
|
|
26
|
+
FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION = "project_template_schema_version"
|
|
27
|
+
FIELD_ENVIRONMENT = "environment"
|
|
28
|
+
FIELD_DESCRIPTION = "description"
|
|
29
|
+
|
|
30
|
+
# Special constants
|
|
31
|
+
ROOT_FIELD_PATH = "<root>"
|
|
32
|
+
DEFAULT_SCHEMA_VERSION = "0.1.0"
|
|
33
|
+
DEFAULT_PROJECT_NAME = "Invalid Project"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class YAMLLineInfo:
|
|
38
|
+
"""Line number information for YAML fields."""
|
|
39
|
+
|
|
40
|
+
field_path_to_line: dict[str, int] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def get_line(self, field_path: str) -> int | None:
|
|
43
|
+
"""Get line number for a field path, returns None if not tracked."""
|
|
44
|
+
return self.field_path_to_line.get(field_path)
|
|
45
|
+
|
|
46
|
+
def add_mapping(self, field_path: str, line_number: int) -> None:
|
|
47
|
+
"""Add a field path to line number mapping."""
|
|
48
|
+
self.field_path_to_line[field_path] = line_number
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class YAMLParseResult(NamedTuple):
|
|
52
|
+
"""Result of parsing YAML with line tracking.
|
|
53
|
+
|
|
54
|
+
Contains the parsed YAML data and line number tracking information.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
data: dict[str, Any]
|
|
58
|
+
line_info: YAMLLineInfo
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProjectOverlayData(NamedTuple):
|
|
62
|
+
"""Partially validated project template data for merging.
|
|
63
|
+
|
|
64
|
+
Contains raw dicts for situations/directories with basic structural validation.
|
|
65
|
+
Line tracking preserved for error reporting during merge.
|
|
66
|
+
Used as overlay input to ProjectTemplate.merge().
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
project_template_schema_version: str
|
|
71
|
+
situations: dict[str, dict[str, Any]] # situation_name -> raw dict
|
|
72
|
+
directories: dict[str, dict[str, Any]] # directory_name -> raw dict
|
|
73
|
+
environment: dict[str, str]
|
|
74
|
+
description: str | None
|
|
75
|
+
line_info: YAMLLineInfo
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_yaml_with_line_tracking(yaml_text: str) -> YAMLParseResult:
|
|
79
|
+
"""Load YAML preserving line numbers and comments.
|
|
80
|
+
|
|
81
|
+
Uses ruamel.yaml to parse while tracking line numbers for each field.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
- Parsed YAML data as dict
|
|
85
|
+
- Line number tracking info
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
YAMLError: If YAML syntax is invalid (unclosed quotes, invalid structure, etc.)
|
|
89
|
+
"""
|
|
90
|
+
yaml = YAML()
|
|
91
|
+
yaml.preserve_quotes = True
|
|
92
|
+
yaml.default_flow_style = False
|
|
93
|
+
|
|
94
|
+
# Parse YAML with line tracking
|
|
95
|
+
# ruamel.yaml returns CommentedMap/CommentedSeq objects with line info
|
|
96
|
+
data = yaml.load(yaml_text)
|
|
97
|
+
|
|
98
|
+
if not isinstance(data, dict):
|
|
99
|
+
msg = f"Expected YAML root to be a mapping (dict), got {type(data).__name__}"
|
|
100
|
+
raise YAMLError(msg)
|
|
101
|
+
|
|
102
|
+
# Build line number mapping
|
|
103
|
+
line_info = YAMLLineInfo()
|
|
104
|
+
|
|
105
|
+
def track_lines(obj: CommentedMap | CommentedSeq, path: str) -> None:
|
|
106
|
+
"""Recursively track line numbers for YAML objects.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
obj: CommentedMap or CommentedSeq from ruamel.yaml (has lc attribute)
|
|
110
|
+
path: Current field path for tracking
|
|
111
|
+
|
|
112
|
+
Note: Only CommentedMap/CommentedSeq objects have line tracking.
|
|
113
|
+
Plain dicts/lists and scalars don't have lc attributes.
|
|
114
|
+
"""
|
|
115
|
+
# Track line number for this object
|
|
116
|
+
line = obj.lc.line + 1 # Convert to 1-indexed
|
|
117
|
+
line_info.add_mapping(path, line)
|
|
118
|
+
|
|
119
|
+
# Recurse into children
|
|
120
|
+
if isinstance(obj, CommentedMap):
|
|
121
|
+
for key, value in obj.items():
|
|
122
|
+
child_path = f"{path}.{key}" if path else key
|
|
123
|
+
if isinstance(value, CommentedMap | CommentedSeq):
|
|
124
|
+
track_lines(value, child_path)
|
|
125
|
+
elif isinstance(obj, CommentedSeq):
|
|
126
|
+
for i, item in enumerate(obj):
|
|
127
|
+
child_path = f"{path}[{i}]"
|
|
128
|
+
if isinstance(item, CommentedMap | CommentedSeq):
|
|
129
|
+
track_lines(item, child_path)
|
|
130
|
+
|
|
131
|
+
# At runtime, data is a CommentedMap from ruamel.yaml
|
|
132
|
+
if isinstance(data, CommentedMap):
|
|
133
|
+
track_lines(data, "")
|
|
134
|
+
|
|
135
|
+
return YAMLParseResult(data=data, line_info=line_info)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_project_template_from_yaml( # noqa: C901
|
|
139
|
+
yaml_text: str,
|
|
140
|
+
validation_info: ProjectValidationInfo,
|
|
141
|
+
) -> ProjectTemplate | None:
|
|
142
|
+
"""Parse project.yml text into ProjectTemplate.
|
|
143
|
+
|
|
144
|
+
Two-pass approach:
|
|
145
|
+
1. Load raw YAML with line tracking (may raise YAMLError)
|
|
146
|
+
2. Build ProjectTemplate via Pydantic validation, collecting validation problems
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
yaml_text: YAML text to parse
|
|
150
|
+
validation_info: Validation info to populate (caller-owned, will be mutated)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
ProjectTemplate on success, None if fatal errors prevent construction
|
|
154
|
+
"""
|
|
155
|
+
# Import here to avoid circular dependency:
|
|
156
|
+
# - loader.py defines YAMLLineInfo
|
|
157
|
+
# - project.py imports YAMLLineInfo for type hints
|
|
158
|
+
# - loader.py needs ProjectTemplate at runtime
|
|
159
|
+
# TYPE_CHECKING import at top is for type hints only, this is for runtime
|
|
160
|
+
from griptape_nodes.common.project_templates.project import ProjectTemplate
|
|
161
|
+
|
|
162
|
+
# Pass 1: Load YAML with line tracking
|
|
163
|
+
try:
|
|
164
|
+
result = load_yaml_with_line_tracking(yaml_text)
|
|
165
|
+
except YAMLError as e:
|
|
166
|
+
# YAML syntax error - cannot proceed
|
|
167
|
+
validation_info.add_error(
|
|
168
|
+
field_path=ROOT_FIELD_PATH,
|
|
169
|
+
message=f"YAML syntax error: {e}",
|
|
170
|
+
line_number=None,
|
|
171
|
+
)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
# Pass 2: Build ProjectTemplate with Pydantic validation
|
|
175
|
+
data = result.data
|
|
176
|
+
line_info = result.line_info
|
|
177
|
+
|
|
178
|
+
# Add names to situations and directories before validation
|
|
179
|
+
if FIELD_SITUATIONS in data and isinstance(data[FIELD_SITUATIONS], dict):
|
|
180
|
+
for sit_name, sit_data in data[FIELD_SITUATIONS].items():
|
|
181
|
+
if isinstance(sit_data, dict):
|
|
182
|
+
sit_data[FIELD_NAME] = sit_name
|
|
183
|
+
|
|
184
|
+
if FIELD_DIRECTORIES in data and isinstance(data[FIELD_DIRECTORIES], dict):
|
|
185
|
+
for dir_name, dir_data in data[FIELD_DIRECTORIES].items():
|
|
186
|
+
if isinstance(dir_data, dict):
|
|
187
|
+
dir_data[FIELD_NAME] = dir_name
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
template = ProjectTemplate.model_validate(data)
|
|
191
|
+
except ValidationError as e:
|
|
192
|
+
# Convert Pydantic validation errors to our validation_info format
|
|
193
|
+
for error in e.errors():
|
|
194
|
+
# Build field path from error location
|
|
195
|
+
field_path = ".".join(str(loc) for loc in error["loc"])
|
|
196
|
+
message = error["msg"]
|
|
197
|
+
|
|
198
|
+
# Try to get line number from our line tracking
|
|
199
|
+
line_number = line_info.get_line(field_path)
|
|
200
|
+
|
|
201
|
+
validation_info.add_error(
|
|
202
|
+
field_path=field_path,
|
|
203
|
+
message=message,
|
|
204
|
+
line_number=line_number,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return None
|
|
208
|
+
else:
|
|
209
|
+
# Add warnings for schema version mismatches
|
|
210
|
+
if template.project_template_schema_version != ProjectTemplate.LATEST_SCHEMA_VERSION:
|
|
211
|
+
validation_info.add_warning(
|
|
212
|
+
field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
|
|
213
|
+
message=f"Schema version '{template.project_template_schema_version}' differs from latest '{ProjectTemplate.LATEST_SCHEMA_VERSION}'",
|
|
214
|
+
line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return template
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def load_partial_project_template(
|
|
221
|
+
yaml_text: str,
|
|
222
|
+
validation_info: ProjectValidationInfo,
|
|
223
|
+
) -> ProjectOverlayData | None:
|
|
224
|
+
"""Load project template overlay for merging without full construction.
|
|
225
|
+
|
|
226
|
+
Performs minimal structural validation:
|
|
227
|
+
- YAML syntax
|
|
228
|
+
- Required top-level fields (name, project_template_schema_version)
|
|
229
|
+
- Basic type checking (situations is dict, directories is dict, etc.)
|
|
230
|
+
|
|
231
|
+
Does NOT:
|
|
232
|
+
- Construct full SituationTemplate/DirectoryDefinition objects
|
|
233
|
+
- Validate situation schemas or policy values
|
|
234
|
+
- Check directory references
|
|
235
|
+
|
|
236
|
+
Use this for loading user overlay templates before merge.
|
|
237
|
+
After merge, full validation happens during object construction.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
yaml_text: YAML text to parse
|
|
241
|
+
validation_info: Validation info to populate (caller-owned, will be mutated)
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
ProjectOverlayData on success, None if fatal errors prevent construction
|
|
245
|
+
"""
|
|
246
|
+
# Parse YAML with line tracking
|
|
247
|
+
try:
|
|
248
|
+
result = load_yaml_with_line_tracking(yaml_text)
|
|
249
|
+
data = result.data
|
|
250
|
+
line_info = result.line_info
|
|
251
|
+
except YAMLError as e:
|
|
252
|
+
validation_info.add_error(
|
|
253
|
+
field_path=ROOT_FIELD_PATH,
|
|
254
|
+
message=f"YAML syntax error: {e}",
|
|
255
|
+
line_number=None,
|
|
256
|
+
)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# Validate required field: name
|
|
260
|
+
name = data.get(FIELD_NAME)
|
|
261
|
+
if name is None:
|
|
262
|
+
validation_info.add_error(
|
|
263
|
+
field_path=FIELD_NAME,
|
|
264
|
+
message=f"Required field '{FIELD_NAME}' missing",
|
|
265
|
+
line_number=line_info.get_line(FIELD_NAME),
|
|
266
|
+
)
|
|
267
|
+
name = DEFAULT_PROJECT_NAME
|
|
268
|
+
elif not isinstance(name, str):
|
|
269
|
+
validation_info.add_error(
|
|
270
|
+
field_path=FIELD_NAME,
|
|
271
|
+
message=f"Field '{FIELD_NAME}' must be string, got {type(name).__name__}",
|
|
272
|
+
line_number=line_info.get_line(FIELD_NAME),
|
|
273
|
+
)
|
|
274
|
+
name = DEFAULT_PROJECT_NAME
|
|
275
|
+
|
|
276
|
+
# Validate required field: project_template_schema_version
|
|
277
|
+
schema_version = data.get(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION)
|
|
278
|
+
if schema_version is None:
|
|
279
|
+
validation_info.add_error(
|
|
280
|
+
field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
|
|
281
|
+
message=f"Required field '{FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION}' missing",
|
|
282
|
+
line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
|
|
283
|
+
)
|
|
284
|
+
schema_version = DEFAULT_SCHEMA_VERSION
|
|
285
|
+
elif not isinstance(schema_version, str):
|
|
286
|
+
validation_info.add_error(
|
|
287
|
+
field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
|
|
288
|
+
message=f"Must be string, got {type(schema_version).__name__}",
|
|
289
|
+
line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
|
|
290
|
+
)
|
|
291
|
+
schema_version = DEFAULT_SCHEMA_VERSION
|
|
292
|
+
|
|
293
|
+
# Optional field: situations (default to empty dict)
|
|
294
|
+
situations = data.get(FIELD_SITUATIONS, {})
|
|
295
|
+
if not isinstance(situations, dict):
|
|
296
|
+
validation_info.add_error(
|
|
297
|
+
field_path=FIELD_SITUATIONS,
|
|
298
|
+
message=f"Must be dict, got {type(situations).__name__}",
|
|
299
|
+
line_number=line_info.get_line(FIELD_SITUATIONS),
|
|
300
|
+
)
|
|
301
|
+
situations = {}
|
|
302
|
+
|
|
303
|
+
# Optional field: directories (default to empty dict)
|
|
304
|
+
directories = data.get(FIELD_DIRECTORIES, {})
|
|
305
|
+
if not isinstance(directories, dict):
|
|
306
|
+
validation_info.add_error(
|
|
307
|
+
field_path=FIELD_DIRECTORIES,
|
|
308
|
+
message=f"Must be dict, got {type(directories).__name__}",
|
|
309
|
+
line_number=line_info.get_line(FIELD_DIRECTORIES),
|
|
310
|
+
)
|
|
311
|
+
directories = {}
|
|
312
|
+
|
|
313
|
+
# Optional field: environment (default to empty dict)
|
|
314
|
+
environment = data.get(FIELD_ENVIRONMENT, {})
|
|
315
|
+
if not isinstance(environment, dict):
|
|
316
|
+
validation_info.add_error(
|
|
317
|
+
field_path=FIELD_ENVIRONMENT,
|
|
318
|
+
message=f"Must be dict, got {type(environment).__name__}",
|
|
319
|
+
line_number=line_info.get_line(FIELD_ENVIRONMENT),
|
|
320
|
+
)
|
|
321
|
+
environment = {}
|
|
322
|
+
|
|
323
|
+
# Optional field: description
|
|
324
|
+
description = data.get(FIELD_DESCRIPTION)
|
|
325
|
+
if description is not None and not isinstance(description, str):
|
|
326
|
+
validation_info.add_error(
|
|
327
|
+
field_path=FIELD_DESCRIPTION,
|
|
328
|
+
message=f"Must be string, got {type(description).__name__}",
|
|
329
|
+
line_number=line_info.get_line(FIELD_DESCRIPTION),
|
|
330
|
+
)
|
|
331
|
+
description = None
|
|
332
|
+
|
|
333
|
+
return ProjectOverlayData(
|
|
334
|
+
name=name,
|
|
335
|
+
project_template_schema_version=schema_version,
|
|
336
|
+
situations=situations,
|
|
337
|
+
directories=directories,
|
|
338
|
+
environment=environment,
|
|
339
|
+
description=description,
|
|
340
|
+
line_info=line_info,
|
|
341
|
+
)
|