ostruct-cli 0.1.2__py3-none-any.whl → 0.2.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 +10 -0
- ostruct/cli/cli.py +41 -29
- ostruct/cli/file_info.py +9 -1
- ostruct/cli/file_list.py +57 -1
- ostruct/cli/file_utils.py +97 -7
- ostruct/cli/security.py +12 -0
- ostruct/cli/template_extensions.py +1 -1
- ostruct/cli/template_rendering.py +0 -8
- ostruct/cli/template_validation.py +1 -1
- {ostruct_cli-0.1.2.dist-info → ostruct_cli-0.2.0.dist-info}/METADATA +2 -1
- {ostruct_cli-0.1.2.dist-info → ostruct_cli-0.2.0.dist-info}/RECORD +14 -14
- {ostruct_cli-0.1.2.dist-info → ostruct_cli-0.2.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.1.2.dist-info → ostruct_cli-0.2.0.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.1.2.dist-info → ostruct_cli-0.2.0.dist-info}/entry_points.txt +0 -0
ostruct/__init__.py
CHANGED
ostruct/cli/cli.py
CHANGED
@@ -12,7 +12,6 @@ if sys.version_info >= (3, 11):
|
|
12
12
|
from enum import StrEnum
|
13
13
|
|
14
14
|
from datetime import date, datetime, time
|
15
|
-
from importlib.metadata import version
|
16
15
|
from pathlib import Path
|
17
16
|
from typing import (
|
18
17
|
Any,
|
@@ -72,6 +71,7 @@ from pydantic.functional_validators import BeforeValidator
|
|
72
71
|
from pydantic.types import constr
|
73
72
|
from typing_extensions import TypeAlias
|
74
73
|
|
74
|
+
from .. import __version__
|
75
75
|
from .errors import (
|
76
76
|
DirectoryNotFoundError,
|
77
77
|
FieldDefinitionError,
|
@@ -94,6 +94,9 @@ from .security import SecurityManager
|
|
94
94
|
from .template_env import create_jinja_env
|
95
95
|
from .template_utils import SystemPromptError, render_template
|
96
96
|
|
97
|
+
# Constants
|
98
|
+
DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant."
|
99
|
+
|
97
100
|
# Set up logging
|
98
101
|
logger = logging.getLogger(__name__)
|
99
102
|
|
@@ -128,15 +131,6 @@ ostruct_file_handler.setFormatter(
|
|
128
131
|
)
|
129
132
|
logger.addHandler(ostruct_file_handler)
|
130
133
|
|
131
|
-
# Constants
|
132
|
-
DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant."
|
133
|
-
|
134
|
-
# Get package version
|
135
|
-
try:
|
136
|
-
__version__ = version("openai-structured")
|
137
|
-
except Exception:
|
138
|
-
__version__ = "unknown"
|
139
|
-
|
140
134
|
|
141
135
|
class ExitCode(IntEnum):
|
142
136
|
"""Exit codes for the CLI following standard Unix conventions.
|
@@ -212,7 +206,6 @@ def _get_type_with_constraints(
|
|
212
206
|
Returns:
|
213
207
|
Tuple of (type, field)
|
214
208
|
"""
|
215
|
-
field_type = field_schema.get("type")
|
216
209
|
field_kwargs: Dict[str, Any] = {}
|
217
210
|
|
218
211
|
# Add common field metadata
|
@@ -225,21 +218,40 @@ def _get_type_with_constraints(
|
|
225
218
|
if "readOnly" in field_schema:
|
226
219
|
field_kwargs["frozen"] = field_schema["readOnly"]
|
227
220
|
|
221
|
+
field_type = field_schema.get("type")
|
222
|
+
|
228
223
|
# Handle array type
|
229
224
|
if field_type == "array":
|
230
225
|
items_schema = field_schema.get("items", {})
|
231
226
|
if not items_schema:
|
232
227
|
return (List[Any], Field(**field_kwargs))
|
233
228
|
|
234
|
-
# Create nested model
|
235
|
-
|
236
|
-
items_schema,
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
229
|
+
# Create nested model for object items
|
230
|
+
if (
|
231
|
+
isinstance(items_schema, dict)
|
232
|
+
and items_schema.get("type") == "object"
|
233
|
+
):
|
234
|
+
array_item_model = create_dynamic_model(
|
235
|
+
items_schema,
|
236
|
+
base_name=f"{base_name}_{field_name}_Item",
|
237
|
+
show_schema=False,
|
238
|
+
debug_validation=False,
|
239
|
+
)
|
240
|
+
array_type: Type[List[Any]] = List[array_item_model] # type: ignore[valid-type]
|
241
|
+
return (array_type, Field(**field_kwargs))
|
242
|
+
|
243
|
+
# For non-object items, use the type directly
|
244
|
+
item_type = items_schema.get("type", "string")
|
245
|
+
if item_type == "string":
|
246
|
+
return (List[str], Field(**field_kwargs))
|
247
|
+
elif item_type == "integer":
|
248
|
+
return (List[int], Field(**field_kwargs))
|
249
|
+
elif item_type == "number":
|
250
|
+
return (List[float], Field(**field_kwargs))
|
251
|
+
elif item_type == "boolean":
|
252
|
+
return (List[bool], Field(**field_kwargs))
|
253
|
+
else:
|
254
|
+
return (List[Any], Field(**field_kwargs))
|
243
255
|
|
244
256
|
# Handle object type
|
245
257
|
if field_type == "object":
|
@@ -942,11 +954,7 @@ def create_template_context(
|
|
942
954
|
# Add file variables
|
943
955
|
if files:
|
944
956
|
for name, file_list in files.items():
|
945
|
-
|
946
|
-
if len(file_list) == 1:
|
947
|
-
context[name] = file_list[0]
|
948
|
-
else:
|
949
|
-
context[name] = file_list
|
957
|
+
context[name] = file_list # Always keep FileInfoList wrapper
|
950
958
|
|
951
959
|
# Add simple variables
|
952
960
|
if variables:
|
@@ -1834,13 +1842,17 @@ def create_dynamic_model(
|
|
1834
1842
|
logger.info("Schema missing type field, assuming object type")
|
1835
1843
|
schema["type"] = "object"
|
1836
1844
|
|
1837
|
-
#
|
1845
|
+
# For non-object root schemas, create a wrapper model
|
1838
1846
|
if schema["type"] != "object":
|
1839
1847
|
if debug_validation:
|
1840
|
-
logger.
|
1841
|
-
"
|
1848
|
+
logger.info(
|
1849
|
+
"Converting non-object root schema to object wrapper"
|
1842
1850
|
)
|
1843
|
-
|
1851
|
+
schema = {
|
1852
|
+
"type": "object",
|
1853
|
+
"properties": {"value": schema},
|
1854
|
+
"required": ["value"],
|
1855
|
+
}
|
1844
1856
|
|
1845
1857
|
# Create model configuration
|
1846
1858
|
config = ConfigDict(
|
ostruct/cli/file_info.py
CHANGED
@@ -46,6 +46,8 @@ class FileInfo:
|
|
46
46
|
FileNotFoundError: If the file does not exist
|
47
47
|
PathSecurityError: If the path is not allowed
|
48
48
|
"""
|
49
|
+
logger.debug("Creating FileInfo for path: %s", path)
|
50
|
+
|
49
51
|
# Validate path
|
50
52
|
if not path:
|
51
53
|
raise ValueError("Path cannot be empty")
|
@@ -61,6 +63,8 @@ class FileInfo:
|
|
61
63
|
|
62
64
|
# First check if file exists
|
63
65
|
abs_path = os.path.abspath(self.__path)
|
66
|
+
logger.debug("Absolute path for %s: %s", path, abs_path)
|
67
|
+
|
64
68
|
if not os.path.exists(abs_path):
|
65
69
|
raise FileNotFoundError(f"File not found: {path}")
|
66
70
|
if not os.path.isfile(abs_path):
|
@@ -69,7 +73,10 @@ class FileInfo:
|
|
69
73
|
# Then validate security
|
70
74
|
try:
|
71
75
|
# This will raise PathSecurityError if path is not allowed
|
72
|
-
self.abs_path
|
76
|
+
resolved_path = self.abs_path
|
77
|
+
logger.debug(
|
78
|
+
"Security-resolved path for %s: %s", path, resolved_path
|
79
|
+
)
|
73
80
|
except PathSecurityError:
|
74
81
|
raise
|
75
82
|
except Exception as e:
|
@@ -77,6 +84,7 @@ class FileInfo:
|
|
77
84
|
|
78
85
|
# If content/encoding weren't provided, read them now
|
79
86
|
if self.__content is None or self.__encoding is None:
|
87
|
+
logger.debug("Reading content for %s", path)
|
80
88
|
self._read_file()
|
81
89
|
|
82
90
|
@property
|
ostruct/cli/file_list.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
"""FileInfoList implementation providing smart file content access."""
|
2
2
|
|
3
|
-
|
3
|
+
import logging
|
4
|
+
from typing import Iterator, List, SupportsIndex, Union, overload
|
4
5
|
|
5
6
|
from .file_info import FileInfo
|
6
7
|
|
7
8
|
__all__ = ["FileInfoList", "FileInfo"]
|
8
9
|
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
9
12
|
|
10
13
|
class FileInfoList(List[FileInfo]):
|
11
14
|
"""List of FileInfo objects with smart content access.
|
@@ -44,6 +47,11 @@ class FileInfoList(List[FileInfo]):
|
|
44
47
|
files: List of FileInfo objects
|
45
48
|
from_dir: Whether this list was created from a directory mapping
|
46
49
|
"""
|
50
|
+
logger.debug(
|
51
|
+
"Creating FileInfoList with %d files, from_dir=%s",
|
52
|
+
len(files),
|
53
|
+
from_dir,
|
54
|
+
)
|
47
55
|
super().__init__(files)
|
48
56
|
self._from_dir = from_dir
|
49
57
|
|
@@ -59,9 +67,18 @@ class FileInfoList(List[FileInfo]):
|
|
59
67
|
ValueError: If the list is empty
|
60
68
|
"""
|
61
69
|
if not self:
|
70
|
+
logger.debug("FileInfoList.content called but list is empty")
|
62
71
|
raise ValueError("No files in FileInfoList")
|
63
72
|
if len(self) == 1 and not self._from_dir:
|
73
|
+
logger.debug(
|
74
|
+
"FileInfoList.content returning single file content (not from dir)"
|
75
|
+
)
|
64
76
|
return self[0].content
|
77
|
+
logger.debug(
|
78
|
+
"FileInfoList.content returning list of %d contents (from_dir=%s)",
|
79
|
+
len(self),
|
80
|
+
self._from_dir,
|
81
|
+
)
|
65
82
|
return [f.content for f in self]
|
66
83
|
|
67
84
|
@property
|
@@ -149,3 +166,42 @@ class FileInfoList(List[FileInfo]):
|
|
149
166
|
str: Same as str() for consistency
|
150
167
|
"""
|
151
168
|
return str(self)
|
169
|
+
|
170
|
+
def __iter__(self) -> Iterator[FileInfo]:
|
171
|
+
"""Return iterator over files."""
|
172
|
+
logger.debug(
|
173
|
+
"Starting iteration over FileInfoList with %d files", len(self)
|
174
|
+
)
|
175
|
+
return super().__iter__()
|
176
|
+
|
177
|
+
def __len__(self) -> int:
|
178
|
+
"""Return number of files."""
|
179
|
+
return super().__len__()
|
180
|
+
|
181
|
+
def __bool__(self) -> bool:
|
182
|
+
"""Return True if there are files."""
|
183
|
+
return super().__len__() > 0
|
184
|
+
|
185
|
+
@overload
|
186
|
+
def __getitem__(self, index: SupportsIndex, /) -> FileInfo: ...
|
187
|
+
|
188
|
+
@overload
|
189
|
+
def __getitem__(self, index: slice, /) -> "FileInfoList": ...
|
190
|
+
|
191
|
+
def __getitem__(
|
192
|
+
self, index: Union[SupportsIndex, slice], /
|
193
|
+
) -> Union[FileInfo, "FileInfoList"]:
|
194
|
+
"""Get file at index."""
|
195
|
+
logger.debug("Getting file at index %s", index)
|
196
|
+
result = super().__getitem__(index)
|
197
|
+
if isinstance(index, slice):
|
198
|
+
if not isinstance(result, list):
|
199
|
+
raise TypeError(
|
200
|
+
f"Expected list from slice, got {type(result)}"
|
201
|
+
)
|
202
|
+
return FileInfoList(result, self._from_dir)
|
203
|
+
if not isinstance(result, FileInfo):
|
204
|
+
raise TypeError(
|
205
|
+
f"Expected FileInfo from index, got {type(result)}"
|
206
|
+
)
|
207
|
+
return result
|
ostruct/cli/file_utils.py
CHANGED
@@ -132,7 +132,7 @@ def collect_files_from_pattern(
|
|
132
132
|
return []
|
133
133
|
|
134
134
|
# Create FileInfo objects
|
135
|
-
files = []
|
135
|
+
files: List[FileInfo] = []
|
136
136
|
for path in matched_paths:
|
137
137
|
try:
|
138
138
|
file_info = FileInfo.from_path(path, security_manager)
|
@@ -169,37 +169,77 @@ def collect_files_from_directory(
|
|
169
169
|
DirectoryNotFoundError: If directory does not exist
|
170
170
|
PathSecurityError: If directory is not allowed
|
171
171
|
"""
|
172
|
+
logger.debug(
|
173
|
+
"Collecting files from directory: %s (recursive=%s, extensions=%s)",
|
174
|
+
directory,
|
175
|
+
recursive,
|
176
|
+
allowed_extensions,
|
177
|
+
)
|
178
|
+
|
172
179
|
# Validate directory exists and is allowed
|
173
180
|
try:
|
174
181
|
abs_dir = str(security_manager.resolve_path(directory))
|
175
|
-
|
182
|
+
logger.debug("Resolved absolute directory path: %s", abs_dir)
|
183
|
+
logger.debug(
|
184
|
+
"Security manager base_dir: %s", security_manager.base_dir
|
185
|
+
)
|
186
|
+
logger.debug(
|
187
|
+
"Security manager allowed_dirs: %s", security_manager.allowed_dirs
|
188
|
+
)
|
189
|
+
except PathSecurityError as e:
|
190
|
+
logger.debug("PathSecurityError while resolving directory: %s", str(e))
|
176
191
|
# Let the original error propagate
|
177
192
|
raise
|
178
193
|
|
179
194
|
if not os.path.exists(abs_dir):
|
195
|
+
logger.debug("Directory not found: %s (abs: %s)", directory, abs_dir)
|
180
196
|
raise DirectoryNotFoundError(f"Directory not found: {directory}")
|
181
197
|
if not os.path.isdir(abs_dir):
|
198
|
+
logger.debug(
|
199
|
+
"Path is not a directory: %s (abs: %s)", directory, abs_dir
|
200
|
+
)
|
182
201
|
raise DirectoryNotFoundError(f"Path is not a directory: {directory}")
|
183
202
|
|
184
203
|
# Collect files
|
185
|
-
files = []
|
186
|
-
for root,
|
204
|
+
files: List[FileInfo] = []
|
205
|
+
for root, dirs, filenames in os.walk(abs_dir):
|
206
|
+
logger.debug("Walking directory: %s", root)
|
207
|
+
logger.debug("Found subdirectories: %s", dirs)
|
208
|
+
logger.debug("Found files: %s", filenames)
|
209
|
+
|
187
210
|
if not recursive and root != abs_dir:
|
211
|
+
logger.debug(
|
212
|
+
"Skipping subdirectory (non-recursive mode): %s", root
|
213
|
+
)
|
188
214
|
continue
|
189
215
|
|
216
|
+
logger.debug("Scanning directory: %s", root)
|
217
|
+
logger.debug("Current files collected: %d", len(files))
|
190
218
|
for filename in filenames:
|
191
219
|
# Get relative path from base directory
|
192
220
|
abs_path = os.path.join(root, filename)
|
193
221
|
try:
|
194
222
|
rel_path = os.path.relpath(abs_path, security_manager.base_dir)
|
195
|
-
|
223
|
+
logger.debug("Processing file: %s -> %s", abs_path, rel_path)
|
224
|
+
except ValueError as e:
|
196
225
|
# Skip files that can't be made relative
|
226
|
+
logger.debug(
|
227
|
+
"Skipping file that can't be made relative: %s (error: %s)",
|
228
|
+
abs_path,
|
229
|
+
str(e),
|
230
|
+
)
|
197
231
|
continue
|
198
232
|
|
199
233
|
# Check extension if filter is specified
|
200
234
|
if allowed_extensions is not None:
|
201
235
|
ext = os.path.splitext(filename)[1].lstrip(".")
|
202
236
|
if ext not in allowed_extensions:
|
237
|
+
logger.debug(
|
238
|
+
"Skipping file with non-matching extension: %s (ext=%s, allowed=%s)",
|
239
|
+
filename,
|
240
|
+
ext,
|
241
|
+
allowed_extensions,
|
242
|
+
)
|
203
243
|
continue
|
204
244
|
|
205
245
|
try:
|
@@ -207,10 +247,17 @@ def collect_files_from_directory(
|
|
207
247
|
rel_path, security_manager=security_manager, **kwargs
|
208
248
|
)
|
209
249
|
files.append(file_info)
|
210
|
-
|
250
|
+
logger.debug("Added file to list: %s", rel_path)
|
251
|
+
except (FileNotFoundError, PathSecurityError) as e:
|
211
252
|
# Skip files that can't be accessed
|
253
|
+
logger.debug(
|
254
|
+
"Skipping inaccessible file: %s (error: %s)",
|
255
|
+
rel_path,
|
256
|
+
str(e),
|
257
|
+
)
|
212
258
|
continue
|
213
259
|
|
260
|
+
logger.debug("Collected %d files from directory %s", len(files), directory)
|
214
261
|
return files
|
215
262
|
|
216
263
|
|
@@ -272,18 +319,38 @@ def collect_files(
|
|
272
319
|
PathSecurityError: If a path is outside the base directory
|
273
320
|
DirectoryNotFoundError: If a directory is not found
|
274
321
|
"""
|
322
|
+
logger.debug(
|
323
|
+
"Collecting files (recursive=%s, extensions=%s):\n files=%s\n patterns=%s\n dirs=%s",
|
324
|
+
dir_recursive,
|
325
|
+
dir_extensions,
|
326
|
+
file_mappings,
|
327
|
+
pattern_mappings,
|
328
|
+
dir_mappings,
|
329
|
+
)
|
330
|
+
|
275
331
|
if security_manager is None:
|
276
332
|
security_manager = SecurityManager(base_dir=os.getcwd())
|
333
|
+
logger.debug(
|
334
|
+
"Created default security manager with base_dir=%s", os.getcwd()
|
335
|
+
)
|
336
|
+
else:
|
337
|
+
logger.debug(
|
338
|
+
"Using provided security manager with base_dir=%s and allowed_dirs=%s",
|
339
|
+
security_manager.base_dir,
|
340
|
+
security_manager.allowed_dirs,
|
341
|
+
)
|
277
342
|
|
278
343
|
# Normalize extensions by removing leading dots
|
279
344
|
if dir_extensions:
|
280
345
|
dir_extensions = [ext.lstrip(".") for ext in dir_extensions]
|
346
|
+
logger.debug("Normalized extensions: %s", dir_extensions)
|
281
347
|
|
282
348
|
files: Dict[str, FileInfoList] = {}
|
283
349
|
|
284
350
|
# Process file mappings
|
285
351
|
if file_mappings:
|
286
352
|
for mapping in file_mappings:
|
353
|
+
logger.debug("Processing file mapping: %s", mapping)
|
287
354
|
name, path = _validate_and_split_mapping(mapping, "file")
|
288
355
|
if name in files:
|
289
356
|
raise ValueError(f"Duplicate file mapping: {name}")
|
@@ -292,10 +359,12 @@ def collect_files(
|
|
292
359
|
path, security_manager=security_manager, **kwargs
|
293
360
|
)
|
294
361
|
files[name] = FileInfoList([file_info], from_dir=False)
|
362
|
+
logger.debug("Added single file mapping: %s -> %s", name, path)
|
295
363
|
|
296
364
|
# Process pattern mappings
|
297
365
|
if pattern_mappings:
|
298
366
|
for mapping in pattern_mappings:
|
367
|
+
logger.debug("Processing pattern mapping: %s", mapping)
|
299
368
|
name, pattern = _validate_and_split_mapping(mapping, "pattern")
|
300
369
|
if name in files:
|
301
370
|
raise ValueError(f"Duplicate pattern mapping: {name}")
|
@@ -305,6 +374,7 @@ def collect_files(
|
|
305
374
|
pattern, security_manager=security_manager, **kwargs
|
306
375
|
)
|
307
376
|
except PathSecurityError as e:
|
377
|
+
logger.debug("Security error in pattern mapping: %s", str(e))
|
308
378
|
raise PathSecurityError(
|
309
379
|
"Pattern mapping error: Access denied: "
|
310
380
|
f"{pattern} is outside base directory and not in allowed directories"
|
@@ -315,14 +385,24 @@ def collect_files(
|
|
315
385
|
continue
|
316
386
|
|
317
387
|
files[name] = FileInfoList(matched_files, from_dir=False)
|
388
|
+
logger.debug(
|
389
|
+
"Added pattern mapping: %s -> %s (%d files)",
|
390
|
+
name,
|
391
|
+
pattern,
|
392
|
+
len(matched_files),
|
393
|
+
)
|
318
394
|
|
319
395
|
# Process directory mappings
|
320
396
|
if dir_mappings:
|
321
397
|
for mapping in dir_mappings:
|
398
|
+
logger.debug("Processing directory mapping: %s", mapping)
|
322
399
|
name, directory = _validate_and_split_mapping(mapping, "directory")
|
323
400
|
if name in files:
|
324
401
|
raise ValueError(f"Duplicate directory mapping: {name}")
|
325
402
|
|
403
|
+
logger.debug(
|
404
|
+
"Processing directory mapping: %s -> %s", name, directory
|
405
|
+
)
|
326
406
|
try:
|
327
407
|
dir_files = collect_files_from_directory(
|
328
408
|
directory=directory,
|
@@ -332,11 +412,13 @@ def collect_files(
|
|
332
412
|
**kwargs,
|
333
413
|
)
|
334
414
|
except PathSecurityError as e:
|
415
|
+
logger.debug("Security error in directory mapping: %s", str(e))
|
335
416
|
raise PathSecurityError(
|
336
417
|
"Directory mapping error: Access denied: "
|
337
418
|
f"{directory} is outside base directory and not in allowed directories"
|
338
419
|
) from e
|
339
|
-
except DirectoryNotFoundError:
|
420
|
+
except DirectoryNotFoundError as e:
|
421
|
+
logger.debug("Directory not found: %s", str(e))
|
340
422
|
raise DirectoryNotFoundError(
|
341
423
|
f"Directory not found: {directory}"
|
342
424
|
)
|
@@ -346,10 +428,18 @@ def collect_files(
|
|
346
428
|
files[name] = FileInfoList([], from_dir=True)
|
347
429
|
else:
|
348
430
|
files[name] = FileInfoList(dir_files, from_dir=True)
|
431
|
+
logger.debug(
|
432
|
+
"Added directory mapping: %s -> %s (%d files)",
|
433
|
+
name,
|
434
|
+
directory,
|
435
|
+
len(dir_files),
|
436
|
+
)
|
349
437
|
|
350
438
|
if not files:
|
439
|
+
logger.debug("No files found in any mappings")
|
351
440
|
raise ValueError("No files found")
|
352
441
|
|
442
|
+
logger.debug("Collected files total mappings: %d", len(files))
|
353
443
|
return files
|
354
444
|
|
355
445
|
|
ostruct/cli/security.py
CHANGED
@@ -96,10 +96,15 @@ class SecurityManager(SecurityManagerProtocol):
|
|
96
96
|
All paths are normalized using realpath to handle symlinks
|
97
97
|
and relative paths consistently across platforms.
|
98
98
|
"""
|
99
|
+
logger = logging.getLogger("ostruct")
|
100
|
+
logger.debug("Initializing SecurityManager")
|
99
101
|
self._base_dir = Path(os.path.realpath(base_dir or os.getcwd()))
|
102
|
+
logger.debug("Base directory set to: %s", self._base_dir)
|
103
|
+
|
100
104
|
self._allowed_dirs: Set[Path] = set()
|
101
105
|
if allowed_dirs:
|
102
106
|
for directory in allowed_dirs:
|
107
|
+
logger.debug("Adding allowed directory: %s", directory)
|
103
108
|
self.add_allowed_dir(directory)
|
104
109
|
|
105
110
|
@property
|
@@ -121,14 +126,21 @@ class SecurityManager(SecurityManagerProtocol):
|
|
121
126
|
Raises:
|
122
127
|
DirectoryNotFoundError: If directory does not exist
|
123
128
|
"""
|
129
|
+
logger = logging.getLogger("ostruct")
|
130
|
+
logger.debug("Adding allowed directory: %s", directory)
|
124
131
|
real_path = Path(os.path.realpath(directory))
|
132
|
+
logger.debug("Resolved real path: %s", real_path)
|
133
|
+
|
125
134
|
if not real_path.exists():
|
135
|
+
logger.debug("Directory not found: %s", directory)
|
126
136
|
raise DirectoryNotFoundError(f"Directory not found: {directory}")
|
127
137
|
if not real_path.is_dir():
|
138
|
+
logger.debug("Path is not a directory: %s", directory)
|
128
139
|
raise DirectoryNotFoundError(
|
129
140
|
f"Path is not a directory: {directory}"
|
130
141
|
)
|
131
142
|
self._allowed_dirs.add(real_path)
|
143
|
+
logger.debug("Successfully added allowed directory: %s", real_path)
|
132
144
|
|
133
145
|
def add_allowed_dirs_from_file(self, file_path: str) -> None:
|
134
146
|
"""Add allowed directories from a file.
|
@@ -9,7 +9,7 @@ from jinja2.ext import Extension
|
|
9
9
|
from jinja2.parser import Parser
|
10
10
|
|
11
11
|
|
12
|
-
class CommentExtension(Extension):
|
12
|
+
class CommentExtension(Extension):
|
13
13
|
"""Extension that ignores variables inside comment blocks.
|
14
14
|
|
15
15
|
This extension ensures that:
|
@@ -300,14 +300,6 @@ def render_template(
|
|
300
300
|
"=== Rendered result (first 1000 chars) ===\n%s",
|
301
301
|
result[:1000],
|
302
302
|
)
|
303
|
-
if "## File:" not in result:
|
304
|
-
logger.error(
|
305
|
-
"WARNING: File headers missing from rendered output!"
|
306
|
-
)
|
307
|
-
logger.error(
|
308
|
-
"Template string excerpt: %r", template_str[:200]
|
309
|
-
)
|
310
|
-
logger.error("Result excerpt: %r", result[:200])
|
311
303
|
if progress:
|
312
304
|
progress.update(1)
|
313
305
|
return result # type: ignore[no-any-return]
|
@@ -87,7 +87,7 @@ __all__ = [
|
|
87
87
|
]
|
88
88
|
|
89
89
|
|
90
|
-
class SafeUndefined(jinja2.StrictUndefined):
|
90
|
+
class SafeUndefined(jinja2.StrictUndefined):
|
91
91
|
"""A strict Undefined class that validates attribute access during validation."""
|
92
92
|
|
93
93
|
def __getattr__(self, name: str) -> Any:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ostruct-cli
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: CLI for OpenAI Structured Output
|
5
5
|
Author: Yaniv Golan
|
6
6
|
Author-email: yaniv@golan.name
|
@@ -19,6 +19,7 @@ Requires-Dist: openai-structured (>=1.0.0,<2.0.0)
|
|
19
19
|
Requires-Dist: pydantic (>=2.6.3,<3.0.0)
|
20
20
|
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
21
21
|
Requires-Dist: tiktoken (>=0.6.0,<0.7.0)
|
22
|
+
Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
|
22
23
|
Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
|
23
24
|
Description-Content-Type: text/markdown
|
24
25
|
|
@@ -1,27 +1,27 @@
|
|
1
|
-
ostruct/__init__.py,sha256=
|
1
|
+
ostruct/__init__.py,sha256=X6zo6V7ZNMv731Wi388aTVQngD1410ExGwGx4J6lpyo,187
|
2
2
|
ostruct/cli/__init__.py,sha256=dbdUfKRT4ARSHgjsZOmQ-TJT1mHwL7L3xMuilPkhG4Q,411
|
3
3
|
ostruct/cli/cache_manager.py,sha256=ej3KrRfkKKZ_lEp2JswjbJ5bW2ncsvna9NeJu81cqqs,5192
|
4
|
-
ostruct/cli/cli.py,sha256=
|
4
|
+
ostruct/cli/cli.py,sha256=jJ60txGOA4ju-4Z057FA8eRnrpxTHDLBn6bZLNsJy9Q,70170
|
5
5
|
ostruct/cli/errors.py,sha256=vxfjXpUbsarYJoTBaGFpjpa8wXqOMGQryTh-roUhRuU,9454
|
6
|
-
ostruct/cli/file_info.py,sha256=
|
7
|
-
ostruct/cli/file_list.py,sha256=
|
8
|
-
ostruct/cli/file_utils.py,sha256=
|
6
|
+
ostruct/cli/file_info.py,sha256=SdU_V5g4R0o41tOTlobOF5cibZfb9qS12fK7G2XNfRs,10481
|
7
|
+
ostruct/cli/file_list.py,sha256=srrouvhimoklNo69nWjrKyUN-5zTOmtVj_swdBZPBTk,7105
|
8
|
+
ostruct/cli/file_utils.py,sha256=uqEvRoWskzKUd5akA-NtdKtLWByKMTXOiVJiiO7uXus,21153
|
9
9
|
ostruct/cli/path_utils.py,sha256=jhJsvmxsq0_FU5cWwB-JoEymGbvB2JX9Q0a-dUf0s1w,4461
|
10
10
|
ostruct/cli/progress.py,sha256=rj9nVEco5UeZORMbzd7mFJpFGJjbH9KbBFh5oTE5Anw,3415
|
11
|
-
ostruct/cli/security.py,sha256=
|
11
|
+
ostruct/cli/security.py,sha256=j7oRb7UM_Mndl_CvnQlql8eyudcAVgfCkeQuXIQyME4,10945
|
12
12
|
ostruct/cli/security_types.py,sha256=oSNbaKjhZVHB6pQEG_WOUhUYKlw9cl4uGOBx2R9BxRk,1341
|
13
13
|
ostruct/cli/template_env.py,sha256=S2ZvxuMQMicodSVqUhrw0kOzbNmlpQjSHtWlOwjXCms,1538
|
14
|
-
ostruct/cli/template_extensions.py,sha256=
|
14
|
+
ostruct/cli/template_extensions.py,sha256=tJN3HGAS2yzGI8Up6STPday8NVL0VV6UCClBrtDKYr0,1623
|
15
15
|
ostruct/cli/template_filters.py,sha256=SNp7PR4ZbuC9BVUlEgwzd6VZYjI0lsobTabLiJe_sZM,19030
|
16
16
|
ostruct/cli/template_io.py,sha256=6rDw2Wx6czK1VntKGUM6cvyMbMWojt41hUlYRpfQuoc,8749
|
17
|
-
ostruct/cli/template_rendering.py,sha256=
|
17
|
+
ostruct/cli/template_rendering.py,sha256=GrQAcKpGe6QEjSVQkOjpegMcor9LzVUikGmmEVgiWCE,12391
|
18
18
|
ostruct/cli/template_schema.py,sha256=ckH4rUZnEgfm_BHS9LnMGr8LtDxRmZ0C6UBVrSp8KTc,19604
|
19
19
|
ostruct/cli/template_utils.py,sha256=QGgewxU_Tgn81J5U-Y4xfi67CkN2dEqXI7PsaNiI9es,7812
|
20
|
-
ostruct/cli/template_validation.py,sha256=
|
20
|
+
ostruct/cli/template_validation.py,sha256=q3ACw4TscdekJb3Z3CTYw0YPEYttqjKjm74ap4lWtU4,11737
|
21
21
|
ostruct/cli/utils.py,sha256=1UCl4rHjBWKR5EKugvlVGHiHjO3XXmqvkgeAUSyIPDU,831
|
22
22
|
ostruct/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
|
-
ostruct_cli-0.
|
24
|
-
ostruct_cli-0.
|
25
|
-
ostruct_cli-0.
|
26
|
-
ostruct_cli-0.
|
27
|
-
ostruct_cli-0.
|
23
|
+
ostruct_cli-0.2.0.dist-info/LICENSE,sha256=QUOY6QCYVxAiH8vdrUTDqe3i9hQ5bcNczppDSVpLTjk,1068
|
24
|
+
ostruct_cli-0.2.0.dist-info/METADATA,sha256=uduwQEF87qBeSBhzFg9AWdFqnDM1TdRyvdRwq-IKqZQ,5300
|
25
|
+
ostruct_cli-0.2.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
26
|
+
ostruct_cli-0.2.0.dist-info/entry_points.txt,sha256=NFq9IuqHVTem0j9zKjV8C1si_zGcP1RL6Wbvt9fUDXw,48
|
27
|
+
ostruct_cli-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|