ostruct-cli 0.1.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.
- ostruct/__init__.py +0 -0
- ostruct/cli/__init__.py +19 -0
- ostruct/cli/cache_manager.py +175 -0
- ostruct/cli/cli.py +2033 -0
- ostruct/cli/errors.py +329 -0
- ostruct/cli/file_info.py +316 -0
- ostruct/cli/file_list.py +151 -0
- ostruct/cli/file_utils.py +518 -0
- ostruct/cli/path_utils.py +123 -0
- ostruct/cli/progress.py +105 -0
- ostruct/cli/security.py +311 -0
- ostruct/cli/security_types.py +49 -0
- ostruct/cli/template_env.py +55 -0
- ostruct/cli/template_extensions.py +51 -0
- ostruct/cli/template_filters.py +650 -0
- ostruct/cli/template_io.py +261 -0
- ostruct/cli/template_rendering.py +347 -0
- ostruct/cli/template_schema.py +565 -0
- ostruct/cli/template_utils.py +288 -0
- ostruct/cli/template_validation.py +375 -0
- ostruct/cli/utils.py +31 -0
- ostruct/py.typed +0 -0
- ostruct_cli-0.1.0.dist-info/LICENSE +21 -0
- ostruct_cli-0.1.0.dist-info/METADATA +182 -0
- ostruct_cli-0.1.0.dist-info/RECORD +27 -0
- ostruct_cli-0.1.0.dist-info/WHEEL +4 -0
- ostruct_cli-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,565 @@
|
|
1
|
+
"""Schema validation and proxy objects for template processing.
|
2
|
+
|
3
|
+
Provides proxy objects that validate attribute/key access during template validation
|
4
|
+
by checking against actual data structures passed to the proxies.
|
5
|
+
|
6
|
+
Classes:
|
7
|
+
- ValidationProxy: Base proxy class for validation
|
8
|
+
- FileInfoProxy: Proxy for file information objects with standard attributes
|
9
|
+
- DictProxy: Proxy for dictionary objects that validates against actual structure
|
10
|
+
- ListProxy: Proxy for list/iterable objects that validates indices and content
|
11
|
+
- StdinProxy: Proxy for lazy stdin access
|
12
|
+
- LazyValidationProxy: Proxy that delays attribute access until string conversion
|
13
|
+
- DotDict: Dictionary wrapper that supports both dot notation and dictionary access
|
14
|
+
|
15
|
+
Examples:
|
16
|
+
Create validation context with actual data:
|
17
|
+
>>> data = {
|
18
|
+
... 'config': {'debug': True, 'settings': {'mode': 'test'}},
|
19
|
+
... 'source_file': FileInfo('test.txt')
|
20
|
+
... }
|
21
|
+
>>> context = create_validation_context(data)
|
22
|
+
>>> # config will be a DictProxy validating against actual structure
|
23
|
+
>>> # source_file will be a FileInfoProxy with standard attributes
|
24
|
+
|
25
|
+
Access validation:
|
26
|
+
>>> # Valid access patterns:
|
27
|
+
>>> config = context['config']
|
28
|
+
>>> debug_value = config.debug # OK - debug exists in data
|
29
|
+
>>> mode = config.settings.mode # OK - settings.mode exists in data
|
30
|
+
>>>
|
31
|
+
>>> # Invalid access raises ValueError:
|
32
|
+
>>> config.invalid # Raises ValueError - key doesn't exist
|
33
|
+
>>> config.settings.invalid # Raises ValueError - nested key doesn't exist
|
34
|
+
|
35
|
+
File info validation:
|
36
|
+
>>> file = context['source_file']
|
37
|
+
>>> name = file.name # OK - standard attribute
|
38
|
+
>>> content = file.content # OK - standard attribute
|
39
|
+
>>> file.invalid # Raises ValueError - invalid attribute
|
40
|
+
|
41
|
+
Notes:
|
42
|
+
- DictProxy validates against actual dictionary structure
|
43
|
+
- FileInfoProxy validates standard file attributes
|
44
|
+
- ListProxy validates indices and returns appropriate proxies
|
45
|
+
- All invalid attribute/key access raises ValueError with details
|
46
|
+
"""
|
47
|
+
|
48
|
+
import logging
|
49
|
+
import sys
|
50
|
+
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
|
51
|
+
|
52
|
+
from .file_utils import FileInfo
|
53
|
+
|
54
|
+
logger = logging.getLogger(__name__)
|
55
|
+
|
56
|
+
|
57
|
+
class ValidationProxy:
|
58
|
+
"""Proxy object that validates attribute/key access during template validation."""
|
59
|
+
|
60
|
+
def __init__(
|
61
|
+
self,
|
62
|
+
var_name: str,
|
63
|
+
value: Any = None,
|
64
|
+
valid_attrs: Optional[Set[str]] = None,
|
65
|
+
nested_attrs: Optional[Dict[str, Any]] = None,
|
66
|
+
allow_nested: bool = True,
|
67
|
+
) -> None:
|
68
|
+
"""Initialize the proxy.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
var_name: The name of the variable being proxied.
|
72
|
+
value: The value being proxied (optional).
|
73
|
+
valid_attrs: Set of valid attribute names.
|
74
|
+
nested_attrs: Dictionary of nested attributes and their allowed values.
|
75
|
+
allow_nested: Whether to allow nested attribute access.
|
76
|
+
"""
|
77
|
+
self._var_name = var_name
|
78
|
+
self._value = value
|
79
|
+
self._valid_attrs = valid_attrs or set()
|
80
|
+
self._nested_attrs = nested_attrs or {}
|
81
|
+
self._allow_nested = allow_nested
|
82
|
+
self._accessed_attributes: Set[str] = set()
|
83
|
+
logger.debug(
|
84
|
+
"Created ValidationProxy for %s with valid_attrs=%s, nested_attrs=%s, allow_nested=%s",
|
85
|
+
var_name,
|
86
|
+
valid_attrs,
|
87
|
+
nested_attrs,
|
88
|
+
allow_nested,
|
89
|
+
)
|
90
|
+
|
91
|
+
def __getattr__(self, name: str) -> Union["ValidationProxy", "DictProxy"]:
|
92
|
+
"""Validate attribute access during template validation."""
|
93
|
+
logger.debug("\n=== ValidationProxy.__getattr__ ===")
|
94
|
+
logger.debug("Called for: %s.%s", self._var_name, name)
|
95
|
+
logger.debug(
|
96
|
+
"State: valid_attrs=%s, nested_attrs=%s, allow_nested=%s",
|
97
|
+
self._valid_attrs,
|
98
|
+
self._nested_attrs,
|
99
|
+
self._allow_nested,
|
100
|
+
)
|
101
|
+
|
102
|
+
self._accessed_attributes.add(name)
|
103
|
+
|
104
|
+
# Allow HTML escaping attributes for all variables
|
105
|
+
if name in {"__html__", "__html_format__"}:
|
106
|
+
logger.debug(
|
107
|
+
"Allowing HTML escape attribute %s for %s",
|
108
|
+
name,
|
109
|
+
self._var_name,
|
110
|
+
)
|
111
|
+
return ValidationProxy(f"{self._var_name}.{name}", value="")
|
112
|
+
|
113
|
+
# Check nested attributes
|
114
|
+
if name in self._nested_attrs:
|
115
|
+
nested_value = self._nested_attrs[name]
|
116
|
+
if isinstance(nested_value, dict):
|
117
|
+
return ValidationProxy(
|
118
|
+
f"{self._var_name}.{name}",
|
119
|
+
nested_attrs=nested_value,
|
120
|
+
allow_nested=True,
|
121
|
+
)
|
122
|
+
elif isinstance(nested_value, set):
|
123
|
+
if (
|
124
|
+
not nested_value
|
125
|
+
): # Empty set means "any nested keys allowed"
|
126
|
+
return ValidationProxy(
|
127
|
+
f"{self._var_name}.{name}", allow_nested=True
|
128
|
+
)
|
129
|
+
return ValidationProxy(
|
130
|
+
f"{self._var_name}.{name}",
|
131
|
+
valid_attrs=nested_value,
|
132
|
+
allow_nested=False,
|
133
|
+
)
|
134
|
+
|
135
|
+
# Validate against valid_attrs if present
|
136
|
+
if self._valid_attrs:
|
137
|
+
if name not in self._valid_attrs:
|
138
|
+
raise ValueError(
|
139
|
+
f"Task template uses undefined attribute '{self._var_name}.{name}'"
|
140
|
+
)
|
141
|
+
return ValidationProxy(
|
142
|
+
f"{self._var_name}.{name}", allow_nested=False
|
143
|
+
)
|
144
|
+
|
145
|
+
# Check nesting allowance and get actual value
|
146
|
+
if not self._allow_nested:
|
147
|
+
raise ValueError(
|
148
|
+
f"Task template uses undefined attribute '{self._var_name}.{name}'"
|
149
|
+
)
|
150
|
+
|
151
|
+
value = None
|
152
|
+
if self._value is not None and hasattr(self._value, name):
|
153
|
+
value = getattr(self._value, name)
|
154
|
+
|
155
|
+
return ValidationProxy(
|
156
|
+
f"{self._var_name}.{name}", value=value, allow_nested=True
|
157
|
+
)
|
158
|
+
|
159
|
+
def __getitem__(self, key: Any) -> Union["ValidationProxy", "DictProxy"]:
|
160
|
+
"""Support item access for validation."""
|
161
|
+
key_str = f"['{key}']" if isinstance(key, str) else f"[{key}]"
|
162
|
+
return ValidationProxy(
|
163
|
+
f"{self._var_name}{key_str}",
|
164
|
+
valid_attrs=self._valid_attrs,
|
165
|
+
allow_nested=self._allow_nested,
|
166
|
+
)
|
167
|
+
|
168
|
+
def __str__(self) -> str:
|
169
|
+
"""Convert the proxy value to a string."""
|
170
|
+
return str(self._value) if self._value is not None else ""
|
171
|
+
|
172
|
+
def __html__(self) -> str:
|
173
|
+
"""Return HTML representation."""
|
174
|
+
return str(self)
|
175
|
+
|
176
|
+
def __html_format__(self, format_spec: str) -> str:
|
177
|
+
"""Return formatted HTML representation."""
|
178
|
+
return str(self)
|
179
|
+
|
180
|
+
def __iter__(self) -> Iterator[Union["ValidationProxy", "DictProxy"]]:
|
181
|
+
"""Support iteration for validation."""
|
182
|
+
yield ValidationProxy(
|
183
|
+
f"{self._var_name}[0]", valid_attrs=self._valid_attrs
|
184
|
+
)
|
185
|
+
|
186
|
+
def get_accessed_attributes(self) -> Set[str]:
|
187
|
+
"""Get the set of accessed attributes."""
|
188
|
+
return self._accessed_attributes.copy()
|
189
|
+
|
190
|
+
|
191
|
+
class FileInfoProxy:
|
192
|
+
"""Proxy for FileInfo that provides validation during template rendering.
|
193
|
+
|
194
|
+
This class wraps FileInfo to provide validation during template rendering.
|
195
|
+
It ensures that only valid attributes are accessed and returns empty strings
|
196
|
+
for content to support filtering in templates.
|
197
|
+
|
198
|
+
Attributes:
|
199
|
+
_var_name: Base variable name for error messages
|
200
|
+
_value: The wrapped FileInfo object
|
201
|
+
_accessed_attrs: Set of attributes that have been accessed
|
202
|
+
_valid_attrs: Set of valid attribute names
|
203
|
+
"""
|
204
|
+
|
205
|
+
def __init__(self, var_name: str, value: FileInfo) -> None:
|
206
|
+
"""Initialize FileInfoProxy.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
var_name: Base variable name for error messages
|
210
|
+
value: FileInfo object to validate
|
211
|
+
"""
|
212
|
+
self._var_name = var_name
|
213
|
+
self._value = value
|
214
|
+
self._accessed_attrs: Set[str] = set()
|
215
|
+
self._valid_attrs = {
|
216
|
+
"name",
|
217
|
+
"path",
|
218
|
+
"content",
|
219
|
+
"ext",
|
220
|
+
"basename",
|
221
|
+
"dirname",
|
222
|
+
"abs_path",
|
223
|
+
"exists",
|
224
|
+
"is_file",
|
225
|
+
"is_dir",
|
226
|
+
"size",
|
227
|
+
"mtime",
|
228
|
+
"encoding",
|
229
|
+
"hash",
|
230
|
+
"extension",
|
231
|
+
"parent",
|
232
|
+
"stem",
|
233
|
+
"suffix",
|
234
|
+
"__html__",
|
235
|
+
"__html_format__",
|
236
|
+
}
|
237
|
+
|
238
|
+
def __getattr__(self, name: str) -> str:
|
239
|
+
"""Get attribute value with validation.
|
240
|
+
|
241
|
+
Args:
|
242
|
+
name: Attribute name to get
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
Empty string for content, actual value for other attributes
|
246
|
+
|
247
|
+
Raises:
|
248
|
+
ValueError: If attribute name is not valid
|
249
|
+
"""
|
250
|
+
if name not in self._valid_attrs:
|
251
|
+
raise ValueError(
|
252
|
+
f"undefined attribute '{name}' for file {self._var_name}"
|
253
|
+
)
|
254
|
+
|
255
|
+
self._accessed_attrs.add(name)
|
256
|
+
|
257
|
+
# Return empty string for content and HTML methods to support filtering
|
258
|
+
if name in ("content", "__html__", "__html_format__"):
|
259
|
+
return ""
|
260
|
+
|
261
|
+
# Return actual value for all other attributes
|
262
|
+
return str(getattr(self._value, name))
|
263
|
+
|
264
|
+
def __str__(self) -> str:
|
265
|
+
"""Convert to string.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
Empty string to support filtering
|
269
|
+
"""
|
270
|
+
return ""
|
271
|
+
|
272
|
+
def __html__(self) -> str:
|
273
|
+
"""Convert to HTML-safe string.
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
Empty string to support filtering
|
277
|
+
"""
|
278
|
+
return ""
|
279
|
+
|
280
|
+
|
281
|
+
class DictProxy:
|
282
|
+
"""Proxy for dictionary access during validation.
|
283
|
+
|
284
|
+
Validates all attribute/key access against the actual dictionary structure.
|
285
|
+
Provides standard dictionary methods (get, items, keys, values).
|
286
|
+
Supports HTML escaping for Jinja2 compatibility.
|
287
|
+
"""
|
288
|
+
|
289
|
+
def __init__(self, name: str, value: Dict[str, Any]) -> None:
|
290
|
+
"""Initialize the proxy.
|
291
|
+
|
292
|
+
Args:
|
293
|
+
name: The name of the variable being proxied.
|
294
|
+
value: The dictionary being proxied.
|
295
|
+
"""
|
296
|
+
self._name = name
|
297
|
+
self._value = value
|
298
|
+
|
299
|
+
def __getattr__(self, name: str) -> Union["DictProxy", ValidationProxy]:
|
300
|
+
"""Validate attribute access against actual dictionary structure."""
|
301
|
+
if name in {"get", "items", "keys", "values"}:
|
302
|
+
return cast(
|
303
|
+
Union["DictProxy", ValidationProxy], getattr(self, f"_{name}")
|
304
|
+
)
|
305
|
+
|
306
|
+
if name not in self._value:
|
307
|
+
raise ValueError(
|
308
|
+
f"Task template uses undefined attribute '{self._name}.{name}'"
|
309
|
+
)
|
310
|
+
|
311
|
+
if isinstance(self._value[name], dict):
|
312
|
+
return DictProxy(f"{self._name}.{name}", self._value[name])
|
313
|
+
return ValidationProxy(f"{self._name}.{name}")
|
314
|
+
|
315
|
+
def __getitem__(self, key: Any) -> Union["DictProxy", ValidationProxy]:
|
316
|
+
"""Validate dictionary key access."""
|
317
|
+
if isinstance(key, int):
|
318
|
+
key = str(key)
|
319
|
+
|
320
|
+
if key not in self._value:
|
321
|
+
raise ValueError(
|
322
|
+
f"Task template uses undefined key '{self._name}['{key}']'"
|
323
|
+
)
|
324
|
+
|
325
|
+
if isinstance(self._value[key], dict):
|
326
|
+
return DictProxy(f"{self._name}['{key}']", self._value[key])
|
327
|
+
return ValidationProxy(f"{self._name}['{key}']")
|
328
|
+
|
329
|
+
def __contains__(self, key: Any) -> bool:
|
330
|
+
"""Support 'in' operator for validation."""
|
331
|
+
if isinstance(key, int):
|
332
|
+
key = str(key)
|
333
|
+
return key in self._value
|
334
|
+
|
335
|
+
def _get(self, key: str, default: Any = None) -> Any:
|
336
|
+
"""Implement dict.get() method."""
|
337
|
+
try:
|
338
|
+
return self[key]
|
339
|
+
except ValueError:
|
340
|
+
return default
|
341
|
+
|
342
|
+
def _items(
|
343
|
+
self,
|
344
|
+
) -> Iterator[Tuple[str, Union["DictProxy", ValidationProxy]]]:
|
345
|
+
"""Implement dict.items() method."""
|
346
|
+
for key, value in self._value.items():
|
347
|
+
if isinstance(value, dict):
|
348
|
+
yield (key, DictProxy(f"{self._name}['{key}']", value))
|
349
|
+
else:
|
350
|
+
yield (key, ValidationProxy(f"{self._name}['{key}']"))
|
351
|
+
|
352
|
+
def _keys(self) -> Iterator[str]:
|
353
|
+
"""Implement dict.keys() method."""
|
354
|
+
for key in self._value.keys():
|
355
|
+
yield key
|
356
|
+
|
357
|
+
def _values(self) -> Iterator[Union["DictProxy", ValidationProxy]]:
|
358
|
+
"""Implement dict.values() method."""
|
359
|
+
for key, value in self._value.items():
|
360
|
+
if isinstance(value, dict):
|
361
|
+
yield DictProxy(f"{self._name}['{key}']", value)
|
362
|
+
else:
|
363
|
+
yield ValidationProxy(f"{self._name}['{key}']")
|
364
|
+
|
365
|
+
def __html__(self) -> str:
|
366
|
+
"""Support HTML escaping."""
|
367
|
+
return ""
|
368
|
+
|
369
|
+
def __html_format__(self, spec: str) -> str:
|
370
|
+
"""Support HTML formatting."""
|
371
|
+
return ""
|
372
|
+
|
373
|
+
|
374
|
+
class ListProxy(ValidationProxy):
|
375
|
+
"""Proxy for list/iterable objects during validation.
|
376
|
+
|
377
|
+
For file lists (from --files or --dir), validates that only valid file attributes
|
378
|
+
are accessed. For other lists, validates indices and returns appropriate proxies
|
379
|
+
based on the actual content type.
|
380
|
+
"""
|
381
|
+
|
382
|
+
def __init__(self, var_name: str, value: List[Any]) -> None:
|
383
|
+
"""Initialize the proxy.
|
384
|
+
|
385
|
+
Args:
|
386
|
+
var_name: The name of the variable being proxied.
|
387
|
+
value: The list being proxied.
|
388
|
+
"""
|
389
|
+
super().__init__(var_name)
|
390
|
+
self._value = value
|
391
|
+
# Determine if this is a list of files
|
392
|
+
self._is_file_list = value and all(
|
393
|
+
isinstance(item, FileInfo) for item in value
|
394
|
+
)
|
395
|
+
self._file_attrs = {
|
396
|
+
"name",
|
397
|
+
"path",
|
398
|
+
"abs_path",
|
399
|
+
"content",
|
400
|
+
"size",
|
401
|
+
"extension",
|
402
|
+
"exists",
|
403
|
+
"mtime",
|
404
|
+
"encoding",
|
405
|
+
"dir",
|
406
|
+
"hash",
|
407
|
+
"is_file",
|
408
|
+
"is_dir",
|
409
|
+
"parent",
|
410
|
+
"stem",
|
411
|
+
"suffix",
|
412
|
+
}
|
413
|
+
|
414
|
+
def __len__(self) -> int:
|
415
|
+
"""Support len() for validation."""
|
416
|
+
return len(self._value)
|
417
|
+
|
418
|
+
def __iter__(self) -> Iterator[Union[ValidationProxy, DictProxy]]:
|
419
|
+
"""Support iteration, returning appropriate proxies."""
|
420
|
+
if self._is_file_list:
|
421
|
+
# For file lists, return FileInfoProxy for validation
|
422
|
+
for i in range(len(self._value)):
|
423
|
+
yield cast(
|
424
|
+
Union[ValidationProxy, DictProxy],
|
425
|
+
FileInfoProxy(f"{self._var_name}[{i}]", self._value[i]),
|
426
|
+
)
|
427
|
+
else:
|
428
|
+
# For other lists, return basic ValidationProxy
|
429
|
+
for i in range(len(self._value)):
|
430
|
+
if isinstance(self._value[i], dict):
|
431
|
+
yield DictProxy(f"{self._var_name}[{i}]", self._value[i])
|
432
|
+
else:
|
433
|
+
yield ValidationProxy(f"{self._var_name}[{i}]")
|
434
|
+
|
435
|
+
def __getitem__(self, key: Any) -> Union[ValidationProxy, DictProxy]:
|
436
|
+
"""Validate list index access and return appropriate proxy."""
|
437
|
+
if isinstance(key, int) and (key < 0 or key >= len(self._value)):
|
438
|
+
raise ValueError(
|
439
|
+
f"List index {key} out of range for {self._var_name}"
|
440
|
+
)
|
441
|
+
|
442
|
+
key_str = f"['{key}']" if isinstance(key, str) else f"[{key}]"
|
443
|
+
|
444
|
+
if self._is_file_list:
|
445
|
+
return cast(
|
446
|
+
Union[ValidationProxy, DictProxy],
|
447
|
+
FileInfoProxy(f"{self._var_name}{key_str}", self._value[key]),
|
448
|
+
)
|
449
|
+
else:
|
450
|
+
value = self._value[key]
|
451
|
+
if isinstance(value, dict):
|
452
|
+
return DictProxy(f"{self._var_name}{key_str}", value)
|
453
|
+
return ValidationProxy(f"{self._var_name}{key_str}")
|
454
|
+
|
455
|
+
|
456
|
+
class StdinProxy:
|
457
|
+
"""Proxy for lazy stdin access.
|
458
|
+
|
459
|
+
This proxy only reads from stdin when the content is actually accessed.
|
460
|
+
This prevents unnecessary stdin reads when the template doesn't use stdin.
|
461
|
+
"""
|
462
|
+
|
463
|
+
def __init__(self) -> None:
|
464
|
+
"""Initialize the proxy."""
|
465
|
+
self._content: Optional[str] = None
|
466
|
+
|
467
|
+
def __str__(self) -> str:
|
468
|
+
"""Return stdin content when converted to string."""
|
469
|
+
if self._content is None:
|
470
|
+
if sys.stdin.isatty():
|
471
|
+
raise ValueError("No input available on stdin")
|
472
|
+
self._content = sys.stdin.read()
|
473
|
+
return self._content or ""
|
474
|
+
|
475
|
+
def __html__(self) -> str:
|
476
|
+
"""Support HTML escaping."""
|
477
|
+
return str(self)
|
478
|
+
|
479
|
+
def __html_format__(self, spec: str) -> str:
|
480
|
+
"""Support HTML formatting."""
|
481
|
+
return str(self)
|
482
|
+
|
483
|
+
|
484
|
+
class DotDict:
|
485
|
+
"""Dictionary wrapper that supports both dot notation and dictionary access."""
|
486
|
+
|
487
|
+
def __init__(self, data: Dict[str, Any]):
|
488
|
+
self._data = data
|
489
|
+
|
490
|
+
def __getattr__(self, name: str) -> Any:
|
491
|
+
try:
|
492
|
+
value = self._data[name]
|
493
|
+
return DotDict(value) if isinstance(value, dict) else value
|
494
|
+
except KeyError:
|
495
|
+
raise AttributeError(f"'DotDict' object has no attribute '{name}'")
|
496
|
+
|
497
|
+
def __getitem__(self, key: str) -> Any:
|
498
|
+
value = self._data[key]
|
499
|
+
return DotDict(value) if isinstance(value, dict) else value
|
500
|
+
|
501
|
+
def __contains__(self, key: str) -> bool:
|
502
|
+
return key in self._data
|
503
|
+
|
504
|
+
def get(self, key: str, default: Any = None) -> Any:
|
505
|
+
value = self._data.get(key, default)
|
506
|
+
return DotDict(value) if isinstance(value, dict) else value
|
507
|
+
|
508
|
+
def items(self) -> List[Tuple[str, Any]]:
|
509
|
+
return [
|
510
|
+
(k, DotDict(v) if isinstance(v, dict) else v)
|
511
|
+
for k, v in self._data.items()
|
512
|
+
]
|
513
|
+
|
514
|
+
def keys(self) -> List[str]:
|
515
|
+
return list(self._data.keys())
|
516
|
+
|
517
|
+
def values(self) -> List[Any]:
|
518
|
+
return [
|
519
|
+
DotDict(v) if isinstance(v, dict) else v
|
520
|
+
for v in self._data.values()
|
521
|
+
]
|
522
|
+
|
523
|
+
|
524
|
+
def create_validation_context(
|
525
|
+
template_context: Dict[str, Any]
|
526
|
+
) -> Dict[str, Any]:
|
527
|
+
"""Create validation context with proxy objects.
|
528
|
+
|
529
|
+
Creates appropriate proxy objects based on the actual type and content
|
530
|
+
of each value in the mappings. Validates all attribute/key access
|
531
|
+
against the actual data structures.
|
532
|
+
|
533
|
+
Args:
|
534
|
+
template_context: Original template context with actual values
|
535
|
+
|
536
|
+
Returns:
|
537
|
+
Dictionary with proxy objects for validation
|
538
|
+
|
539
|
+
Example:
|
540
|
+
>>> data = {'config': {'debug': True}, 'files': [FileInfo('test.txt')]}
|
541
|
+
>>> context = create_validation_context(data)
|
542
|
+
>>> # context['config'] will be DictProxy validating against {'debug': True}
|
543
|
+
>>> # context['files'] will be ListProxy validating file attributes
|
544
|
+
"""
|
545
|
+
logger.debug("Creating validation context for: %s", template_context)
|
546
|
+
validation_context: Dict[str, Any] = {}
|
547
|
+
|
548
|
+
# Add stdin proxy by default - it will only read if accessed
|
549
|
+
validation_context["stdin"] = StdinProxy()
|
550
|
+
|
551
|
+
for name, value in template_context.items():
|
552
|
+
if isinstance(value, FileInfo):
|
553
|
+
validation_context[name] = FileInfoProxy(name, value)
|
554
|
+
elif isinstance(value, dict):
|
555
|
+
validation_context[name] = DictProxy(name, value)
|
556
|
+
elif isinstance(value, list):
|
557
|
+
validation_context[name] = ListProxy(name, value)
|
558
|
+
else:
|
559
|
+
# For primitive values, create a ValidationProxy that disallows attribute access
|
560
|
+
validation_context[name] = ValidationProxy(
|
561
|
+
name, value=value, allow_nested=False
|
562
|
+
)
|
563
|
+
|
564
|
+
logger.debug("Created validation context: %s", validation_context)
|
565
|
+
return validation_context
|