ostruct-cli 0.2.0__py3-none-any.whl → 0.4.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.
@@ -0,0 +1,404 @@
1
+ """Windows path handling and validation.
2
+
3
+ This module provides functions for handling Windows-specific path features:
4
+ - Device paths (r"\\\\?\\", r"\\\\.")
5
+ - Drive-relative paths (C:folder)
6
+ - Reserved names (CON, PRN, etc.)
7
+ - UNC paths (r"\\\\server\\share")
8
+ - Alternate Data Streams (file.txt:stream)
9
+
10
+ Security Design Choices:
11
+ 1. Device Paths:
12
+ - Explicitly blocked for security
13
+ - No support for extended-length paths
14
+ - No direct device access allowed
15
+
16
+ 2. Drive Paths:
17
+ - Drive-relative paths must include separator
18
+ - Drive absolute paths are allowed
19
+ - Drive letters must be A-Z (case insensitive)
20
+
21
+ 3. Reserved Names:
22
+ - All Windows reserved names blocked
23
+ - Case-insensitive matching
24
+ - Blocked with or without extensions
25
+
26
+ 4. UNC Paths:
27
+ - Must be complete (server and share)
28
+ - No device paths in UNC format
29
+ - Normalized to forward slashes
30
+
31
+ 5. Alternate Data Streams:
32
+ - All ADS access is blocked
33
+ - No exceptions for Zone.Identifier
34
+ - Blocks both read and write
35
+
36
+ Known Limitations:
37
+ 1. Path Length:
38
+ - No extended-length path support
39
+ - Standard Windows MAX_PATH limits
40
+ - No workarounds for long paths
41
+
42
+ 2. Network:
43
+ - No special handling for DFS
44
+ - No support for administrative shares
45
+ - Basic UNC validation only
46
+
47
+ 3. Security:
48
+ - Some rare path formats may bypass checks
49
+ - Complex NTFS features not handled
50
+ - Limited reparse point support
51
+ """
52
+
53
+ import logging
54
+ import os
55
+ import re
56
+ from pathlib import Path, WindowsPath
57
+ from typing import Optional, Union
58
+
59
+ from .errors import PathSecurityError, SecurityErrorReasons
60
+
61
+ logger = logging.getLogger(__name__)
62
+
63
+ # Windows path length limits
64
+ MAX_PATH = 260
65
+ EXTENDED_MAX_PATH = 32767
66
+
67
+ # Regex patterns for Windows path features
68
+ _WINDOWS_DEVICE_PATH = re.compile(
69
+ r"^(?:\\\\|//)[?.](?:\\|/)(?!UNC(?:\\|/))", # Match device paths but exclude UNC
70
+ flags=re.IGNORECASE,
71
+ )
72
+
73
+ _WINDOWS_DRIVE_RELATIVE = re.compile(
74
+ r"(?:^|[/\\])[A-Za-z]:(?![/\\])|" # C:folder or \C:folder but not C:\folder
75
+ r"^/[A-Za-z]:(?![/\\])" # /C:folder variants
76
+ )
77
+
78
+ _WINDOWS_RESERVED_NAMES = re.compile(
79
+ r"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])" # Base names
80
+ r"(\.[^\\/:*?\"<>|]*)?$", # Optional extension
81
+ re.IGNORECASE,
82
+ )
83
+
84
+ _WINDOWS_UNC = re.compile(
85
+ r"^\\\\[^?.\\/][^\\/]*\\[^\\/]+(?:\\.*)?|" # \\server\share[\anything]
86
+ r"^//[^?./][^/]*/[^/]+(?:/.*)?$" # //server/share[/anything]
87
+ )
88
+
89
+ _WINDOWS_INCOMPLETE_UNC = re.compile(
90
+ r"^\\\\[^?.\\/][^\\/]*(?:\\[^\\/]+)?$|" # \\server or \\server\incomplete
91
+ r"^//[^?./][^/]*(?:/[^/]+)?$" # //server or //server/incomplete variants
92
+ )
93
+
94
+ _WINDOWS_ADS = re.compile(
95
+ r":[^/\\<>:\"|?*]+$|" # Basic ADS
96
+ r":Zone\.Identifier$|" # Zone.Identifier
97
+ r":[^/\\<>:\"|?*]+:[^/\\]+$" # Multiple stream segments
98
+ )
99
+
100
+ _WINDOWS_INVALID_CHARS = re.compile(
101
+ r'[<>"|?*]|' # Standard invalid chars except colon
102
+ r"(?<!^[A-Za-z]):|" # Colon except after drive letter at start
103
+ r"[\x00-\x1F]" # Control chars
104
+ )
105
+
106
+ _WINDOWS_TRAILING = re.compile(r"[. ]+$") # Trailing dots/spaces
107
+
108
+
109
+ def is_windows_path(path: Union[str, Path]) -> bool:
110
+ """Check if path uses Windows-specific features.
111
+
112
+ Security Note:
113
+ - Detects device paths (r"\\?\\" and r"\\.\\") in both slash formats
114
+ - Case insensitive to handle drive letters
115
+ """
116
+ path_str = str(path)
117
+
118
+ # Normalize slashes for consistent matching
119
+ normalized_path = path_str.replace("\\", "/")
120
+
121
+ # Check for device paths first before any processing
122
+ if _WINDOWS_DEVICE_PATH.match(path_str) or _WINDOWS_DEVICE_PATH.match(
123
+ normalized_path
124
+ ):
125
+ logger.debug("Windows device path detected: %r", path_str)
126
+ return True
127
+
128
+ # Rest of the function remains unchanged
129
+ basename = os.path.basename(path_str)
130
+ is_drive_relative = bool(_WINDOWS_DRIVE_RELATIVE.search(path_str))
131
+ is_unc = bool(_WINDOWS_UNC.search(path_str))
132
+ is_ads = bool(_WINDOWS_ADS.search(path_str))
133
+ is_reserved = bool(_WINDOWS_RESERVED_NAMES.match(basename))
134
+
135
+ logger.debug(
136
+ "Windows path check for %r: drive_relative=%s, unc=%s, ads=%s, reserved=%s",
137
+ path_str,
138
+ is_drive_relative,
139
+ is_unc,
140
+ is_ads,
141
+ is_reserved,
142
+ )
143
+
144
+ return bool(is_drive_relative or is_unc or is_ads or is_reserved)
145
+
146
+
147
+ def normalize_windows_path(path: Union[str, Path]) -> Path:
148
+ """Normalize a path using Windows-specific rules.
149
+
150
+ This function:
151
+ 1. Converts to Path with Windows semantics
152
+ 2. Resolves to absolute path
153
+ 3. Normalizes separators and case
154
+ 4. Removes redundant separators and dots
155
+
156
+ Args:
157
+ path: Path to normalize
158
+
159
+ Returns:
160
+ Normalized Path
161
+
162
+ Raises:
163
+ PathSecurityError: If path cannot be normalized
164
+ """
165
+ try:
166
+ logger.debug("Normalizing Windows path: %r", path)
167
+
168
+ # Convert to string and normalize all slashes to forward slashes first
169
+ path_str = str(path)
170
+ # Replace all backslashes with forward slashes for consistent handling
171
+ path_str = path_str.replace("\\", "/")
172
+ # Collapse multiple slashes to single slash, except for UNC prefixes
173
+ path_str = re.sub(r"(?<!^)//+", "/", path_str)
174
+ logger.debug("Normalized slashes: %r", path_str)
175
+
176
+ # Use regular Path on non-Windows systems
177
+ path_cls = WindowsPath if os.name == "nt" else Path
178
+
179
+ # Convert back to backslashes for Windows path handling
180
+ if os.name == "nt":
181
+ path_str = path_str.replace("/", "\\")
182
+ # Preserve UNC path double backslashes
183
+ if path_str.startswith("\\") and not path_str.startswith("\\\\"):
184
+ path_str = "\\" + path_str
185
+
186
+ normalized = path_cls(path_str)
187
+ logger.debug(
188
+ "Created path object: %r (class=%s)", normalized, path_cls.__name__
189
+ )
190
+
191
+ if os.name == "nt":
192
+ # Check if resolve() would exceed MAX_PATH
193
+ resolved = normalized.resolve()
194
+ resolved_str = str(resolved)
195
+ # If resolve() added \\?\ prefix or path is too long, reject it
196
+ if (
197
+ resolved_str.startswith("\\\\?\\")
198
+ or len(resolved_str) > MAX_PATH
199
+ ):
200
+ raise PathSecurityError(
201
+ f"Path would exceed maximum length of {MAX_PATH} characters after resolution",
202
+ path=str(path),
203
+ context={
204
+ "reason": SecurityErrorReasons.NORMALIZATION_ERROR
205
+ },
206
+ )
207
+ normalized = resolved
208
+ logger.debug("Resolved on Windows: %r", normalized)
209
+ else:
210
+ # On non-Windows, just normalize the path
211
+ normalized = Path(os.path.normpath(path_str))
212
+ logger.debug("Normalized on non-Windows: %r", normalized)
213
+
214
+ return normalized
215
+ except PathSecurityError:
216
+ raise
217
+ except Exception as e:
218
+ logger.error(
219
+ "Failed to normalize Windows path %r: %s", path, e, exc_info=True
220
+ )
221
+ raise PathSecurityError(
222
+ f"Failed to normalize Windows path: {e}",
223
+ path=str(path),
224
+ context={"reason": SecurityErrorReasons.NORMALIZATION_ERROR},
225
+ )
226
+
227
+
228
+ def validate_windows_path(path: Union[str, Path]) -> Optional[str]:
229
+ """Validate a path for Windows-specific security issues.
230
+
231
+ Performs checks in order:
232
+ 1. Device paths (blocked)
233
+ 2. Path normalization
234
+ 3. Other Windows-specific checks
235
+
236
+ Returns an error message if the path:
237
+ - Uses device paths (r"\\\\?\\", r"\\\\.")
238
+ - Uses drive-relative paths (C:folder)
239
+ - Contains reserved names (CON, PRN, etc.)
240
+ - Uses UNC paths (r"\\\\server\\share")
241
+ - Contains Alternate Data Streams (file.txt:stream)
242
+ - Exceeds maximum path length
243
+ - Contains invalid characters
244
+ - Has trailing dots or spaces
245
+
246
+ Returns None if the path is valid.
247
+ """
248
+ logger.debug("Validating Windows path: %r", path)
249
+
250
+ # Initial checks on raw path string
251
+ path_str = str(path)
252
+
253
+ # Normalize slashes for consistent matching
254
+ normalized_path = path_str.replace("\\", "/")
255
+
256
+ # Check for device paths before any processing
257
+ if _WINDOWS_DEVICE_PATH.match(path_str) or _WINDOWS_DEVICE_PATH.match(
258
+ normalized_path
259
+ ):
260
+ logger.debug("Device path detected in original path: %r", path_str)
261
+ return "Device paths not allowed"
262
+
263
+ # Check for incomplete UNC paths before normalization
264
+ if _WINDOWS_INCOMPLETE_UNC.search(path_str):
265
+ logger.debug("Incomplete UNC path detected: %r", path_str)
266
+ return "Incomplete UNC path"
267
+
268
+ # Then normalize the path for other checks
269
+ try:
270
+ normalized_str = str(normalize_windows_path(path))
271
+ logger.debug("Normalized path: %r", normalized_str)
272
+
273
+ # Check for device paths again after normalization
274
+ if _WINDOWS_DEVICE_PATH.match(normalized_str):
275
+ logger.debug(
276
+ "Device path detected after normalization: %r", normalized_str
277
+ )
278
+ return "Device paths not allowed"
279
+
280
+ except PathSecurityError as e:
281
+ logger.debug("Path normalization failed: %s", e)
282
+ return str(e)
283
+
284
+ # Check path length
285
+ if len(normalized_str) > MAX_PATH:
286
+ msg = f"Path exceeds maximum length of {MAX_PATH} characters"
287
+ logger.debug("Path too long: %s", msg)
288
+ return msg
289
+
290
+ if _WINDOWS_DRIVE_RELATIVE.search(normalized_str):
291
+ logger.debug("Drive-relative path detected: %r", normalized_str)
292
+ return "Drive-relative paths must include separator"
293
+
294
+ # Check for complete UNC paths
295
+ if _WINDOWS_UNC.search(normalized_str):
296
+ logger.debug("UNC path detected: %r", normalized_str)
297
+ return "UNC paths not allowed"
298
+
299
+ if _WINDOWS_ADS.search(normalized_str):
300
+ logger.debug("Alternate Data Stream detected: %r", normalized_str)
301
+ return "Alternate Data Streams not allowed"
302
+
303
+ # Check each path component
304
+ try:
305
+ parts = (
306
+ Path(normalized_str).parts
307
+ if os.name != "nt"
308
+ else WindowsPath(normalized_str).parts
309
+ )
310
+ logger.debug("Path components: %r", parts)
311
+
312
+ for part in parts:
313
+ # Check for reserved names
314
+ if _WINDOWS_RESERVED_NAMES.match(part):
315
+ logger.debug("Reserved name detected: %r", part)
316
+ return "Windows reserved names not allowed"
317
+
318
+ # Check for invalid characters
319
+ if _WINDOWS_INVALID_CHARS.search(part):
320
+ msg = f"Invalid characters in path component '{part}'"
321
+ logger.debug("Invalid characters: %s", msg)
322
+ return msg
323
+
324
+ # Check for trailing dots/spaces
325
+ if _WINDOWS_TRAILING.search(part):
326
+ msg = f"Trailing dots or spaces not allowed in '{part}'"
327
+ logger.debug("Trailing dots/spaces: %s", msg)
328
+ return msg
329
+ except Exception as e:
330
+ logger.error("Failed to check path components: %s", e, exc_info=True)
331
+ return f"Failed to validate path components: {e}"
332
+
333
+ logger.debug("Path validation successful: %r", normalized_str)
334
+ return None
335
+
336
+
337
+ def resolve_windows_symlink(path: Path) -> Optional[Path]:
338
+ """Resolve a Windows symlink or reparse point.
339
+
340
+ This is a Windows-specific helper for symlink resolution that handles:
341
+ - NTFS symbolic links
342
+ - NTFS junction points
343
+ - NTFS mount points
344
+ - Other reparse points
345
+
346
+ Args:
347
+ path: The path to resolve.
348
+
349
+ Returns:
350
+ Resolved Path if successful, None if not a Windows symlink.
351
+
352
+ Note:
353
+ This function requires Windows and elevated privileges for some
354
+ reparse point operations.
355
+
356
+ Security Note:
357
+ By default, this function only handles regular symlinks.
358
+ For security reasons, other reparse points (junctions, mount points)
359
+ are not resolved by default as they can bypass directory restrictions.
360
+ If you need to handle these, implement proper security checks in the
361
+ calling code.
362
+ """
363
+ if os.name != "nt":
364
+ return None
365
+
366
+ try:
367
+ # Try to resolve as a regular symlink first
368
+ if path.is_symlink():
369
+ target = Path(os.readlink(path))
370
+ logger.debug("Resolved symlink %r to %r", path, target)
371
+ return target
372
+
373
+ # Check if it's a reparse point but not a symlink
374
+ # This requires using Windows APIs, so we just warn about it
375
+ if hasattr(path, "is_mount") and path.is_mount():
376
+ logger.warning(
377
+ "Path %r is a mount point/junction - not resolving for security",
378
+ path,
379
+ )
380
+ return None
381
+
382
+ # For any other reparse points, log a warning
383
+ try:
384
+ import ctypes
385
+
386
+ attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path)) # type: ignore[attr-defined]
387
+ is_reparse = bool(
388
+ attrs != -1 and attrs & 0x400
389
+ ) # FILE_ATTRIBUTE_REPARSE_POINT
390
+ if is_reparse:
391
+ logger.warning(
392
+ "Path %r is a reparse point - not resolving for security",
393
+ path,
394
+ )
395
+ return None
396
+ except Exception:
397
+ # If we can't check reparse attributes, assume it's not a reparse point
398
+ pass
399
+
400
+ return None
401
+
402
+ except OSError as e:
403
+ logger.debug("Failed to resolve Windows symlink %r: %s", path, e)
404
+ return None
@@ -544,9 +544,9 @@ def format_code(
544
544
  """Format code with syntax highlighting.
545
545
 
546
546
  Args:
547
- text (str): The code text to format
548
- output_format (str): The output format ('terminal', 'html', or 'plain')
549
- language (str): The programming language for syntax highlighting
547
+ text: The code text to format
548
+ output_format: The output format ('terminal', 'html', or 'plain')
549
+ language: The programming language for syntax highlighting
550
550
 
551
551
  Returns:
552
552
  str: Formatted code string
@@ -580,10 +580,13 @@ def format_code(
580
580
  else: # plain
581
581
  formatter = NullFormatter[str]()
582
582
 
583
- return highlight(text, lexer, formatter)
583
+ result = highlight(text, lexer, formatter)
584
+ if isinstance(result, bytes):
585
+ return result.decode("utf-8")
586
+ return str(result)
584
587
  except Exception as e:
585
588
  logger.error(f"Error formatting code: {e}")
586
- return text
589
+ return str(text)
587
590
 
588
591
 
589
592
  def register_template_filters(env: Environment) -> None:
@@ -97,8 +97,10 @@ def read_file(
97
97
  if security_manager is None:
98
98
  from .security import SecurityManager
99
99
 
100
- security_manager = SecurityManager()
101
- logger.debug("Created default SecurityManager")
100
+ security_manager = SecurityManager(base_dir=os.getcwd())
101
+ logger.debug(
102
+ "Created default SecurityManager with base_dir=%s", os.getcwd()
103
+ )
102
104
 
103
105
  # Create progress context
104
106
  with ProgressContext(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ostruct-cli
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: CLI for OpenAI Structured Output
5
5
  Author: Yaniv Golan
6
6
  Author-email: yaniv@golan.name
@@ -13,21 +13,24 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: cachetools (>=5.3.2,<6.0.0)
15
15
  Requires-Dist: chardet (>=5.0.0,<6.0.0)
16
+ Requires-Dist: click (>=8.1.7,<9.0.0)
16
17
  Requires-Dist: ijson (>=3.2.3,<4.0.0)
17
18
  Requires-Dist: jsonschema (>=4.23.0,<5.0.0)
18
- Requires-Dist: openai-structured (>=1.0.0,<2.0.0)
19
+ Requires-Dist: openai (>=1.0.0,<2.0.0)
20
+ Requires-Dist: openai-structured (>=1.3.0,<2.0.0)
19
21
  Requires-Dist: pydantic (>=2.6.3,<3.0.0)
20
22
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
21
- Requires-Dist: tiktoken (>=0.6.0,<0.7.0)
23
+ Requires-Dist: tiktoken (>=0.8.0,<0.9.0)
22
24
  Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
23
25
  Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
26
+ Requires-Dist: werkzeug (>=3.1.3,<4.0.0)
24
27
  Description-Content-Type: text/markdown
25
28
 
26
29
  # ostruct-cli
27
30
 
28
31
  [![PyPI version](https://badge.fury.io/py/ostruct-cli.svg)](https://badge.fury.io/py/ostruct-cli)
29
- [![Python Versions](https://img.shields.io/pypi/pyversions/ostruct-cli.svg)](https://pypi.org/project/ostruct-cli/)
30
- [![Documentation Status](https://readthedocs.org/projects/ostruct-cli/badge/?version=latest)](https://ostruct-cli.readthedocs.io/en/latest/?badge=latest)
32
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ostruct-cli.svg)](https://pypi.org/project/ostruct-cli)
33
+ [![Documentation Status](https://readthedocs.org/projects/ostruct/badge/?version=latest)](https://ostruct.readthedocs.io/en/latest/?badge=latest)
31
34
  [![CI](https://github.com/yaniv-golan/ostruct/actions/workflows/ci.yml/badge.svg)](https://github.com/yaniv-golan/ostruct/actions/workflows/ci.yml)
32
35
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
33
36
 
@@ -124,7 +127,7 @@ All debug and error logs are written to:
124
127
  - `~/.ostruct/logs/ostruct.log`: General application logs
125
128
  - `~/.ostruct/logs/openai_stream.log`: OpenAI streaming operations logs
126
129
 
127
- For more detailed documentation and examples, visit our [documentation](https://ostruct-cli.readthedocs.io/).
130
+ For more detailed documentation and examples, visit our [documentation](https://ostruct.readthedocs.io/).
128
131
 
129
132
  ## Development
130
133
 
@@ -0,0 +1,36 @@
1
+ ostruct/__init__.py,sha256=X6zo6V7ZNMv731Wi388aTVQngD1410ExGwGx4J6lpyo,187
2
+ ostruct/cli/__init__.py,sha256=sYHKT6o1kFy1acbXejzAvVm8Cy8U91Yf1l4DlzquHKg,409
3
+ ostruct/cli/cache_manager.py,sha256=ej3KrRfkKKZ_lEp2JswjbJ5bW2ncsvna9NeJu81cqqs,5192
4
+ ostruct/cli/cli.py,sha256=kh2_P8O7BAbu7qWB64d1qH0rgWSJZuyxn4MdXd13h4I,65574
5
+ ostruct/cli/click_options.py,sha256=rrx04tiZmOu2103k0WJMNtc4ZYGpDftD9DjtGCVaNW0,7551
6
+ ostruct/cli/errors.py,sha256=lr8c4nkRCZZ5QoWHMZxX4B22CNoaoL1JJx8Y8WTt53Y,10448
7
+ ostruct/cli/file_info.py,sha256=xafeONqhUC7D9OPVuDIlc-44KcrPjdEBVmHN69CEtzs,13838
8
+ ostruct/cli/file_list.py,sha256=OiOl0NfkrWipZEdtRAf4eDJGDo2kyWdurxmU2k4KaZ0,11438
9
+ ostruct/cli/file_utils.py,sha256=On5zjqTx0qKlcZBbt1SNaq7HXtEON5_vyeTFE4UsCFg,22075
10
+ ostruct/cli/path_utils.py,sha256=RzGO-QOrp__NtDcIfAjfKl71NSXz3m_pb07UybgF8ro,3681
11
+ ostruct/cli/progress.py,sha256=rj9nVEco5UeZORMbzd7mFJpFGJjbH9KbBFh5oTE5Anw,3415
12
+ ostruct/cli/security/__init__.py,sha256=CQpkCgTFYlA1p6atpQeNgIKtE4LZGUKt4EbytbGKpCs,846
13
+ ostruct/cli/security/allowed_checker.py,sha256=y_R1UIJeGr1Ah1jlsg8t6aO28DnOfLqSH0wqmlVhx5A,1369
14
+ ostruct/cli/security/case_manager.py,sha256=I_ZJSyntLuGx5qVzze559CI-OxsaNPSibkAN8zZ7PvE,2345
15
+ ostruct/cli/security/errors.py,sha256=hDYKxo7V600ibXtXbrDoCbLNL7MEkarZEQbO2QYfTnI,5859
16
+ ostruct/cli/security/normalization.py,sha256=qevvxW3hHDtD1cVvDym8LJEQD1AKenVB-0ZvjCYjn5E,5242
17
+ ostruct/cli/security/safe_joiner.py,sha256=PHowCeBAkfHfPqRwuO5Com0OemGuq3cHkdu2p9IYNT0,7107
18
+ ostruct/cli/security/security_manager.py,sha256=KkI-fApKoDfRD7HSlz_H5LrIMbM5Rz9aP727t4Q_b5g,12770
19
+ ostruct/cli/security/symlink_resolver.py,sha256=wtZdJ_T_0FOy6B1P5ty1odEXQk9vr8BzlWeAFD4huJE,16744
20
+ ostruct/cli/security/types.py,sha256=15yuG_T4CXyAFFFdSWLjVS7ACmDGIPXhQpZ8awcDwCQ,2991
21
+ ostruct/cli/security/windows_paths.py,sha256=qxC2H2kLwtmQ7YePYde3UrmOJcGnsLEebDLh242sUaI,13453
22
+ ostruct/cli/template_env.py,sha256=S2ZvxuMQMicodSVqUhrw0kOzbNmlpQjSHtWlOwjXCms,1538
23
+ ostruct/cli/template_extensions.py,sha256=tJN3HGAS2yzGI8Up6STPday8NVL0VV6UCClBrtDKYr0,1623
24
+ ostruct/cli/template_filters.py,sha256=wZiR08e2_2SW28B7_tTU3wiij_KTCx3CCvlg-P2q7mk,19126
25
+ ostruct/cli/template_io.py,sha256=yUWO-8rZnSdX97DTMSEX8fG9CP1ISsOhm2NZN3Fab9A,8821
26
+ ostruct/cli/template_rendering.py,sha256=GrQAcKpGe6QEjSVQkOjpegMcor9LzVUikGmmEVgiWCE,12391
27
+ ostruct/cli/template_schema.py,sha256=ckH4rUZnEgfm_BHS9LnMGr8LtDxRmZ0C6UBVrSp8KTc,19604
28
+ ostruct/cli/template_utils.py,sha256=QGgewxU_Tgn81J5U-Y4xfi67CkN2dEqXI7PsaNiI9es,7812
29
+ ostruct/cli/template_validation.py,sha256=q3ACw4TscdekJb3Z3CTYw0YPEYttqjKjm74ap4lWtU4,11737
30
+ ostruct/cli/utils.py,sha256=1UCl4rHjBWKR5EKugvlVGHiHjO3XXmqvkgeAUSyIPDU,831
31
+ ostruct/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ ostruct_cli-0.4.0.dist-info/LICENSE,sha256=QUOY6QCYVxAiH8vdrUTDqe3i9hQ5bcNczppDSVpLTjk,1068
33
+ ostruct_cli-0.4.0.dist-info/METADATA,sha256=Khg3pmpYFixNMW_RrpF9bfD_ZRJDjAqP4VDWDdMcBhY,5405
34
+ ostruct_cli-0.4.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
35
+ ostruct_cli-0.4.0.dist-info/entry_points.txt,sha256=NFq9IuqHVTem0j9zKjV8C1si_zGcP1RL6Wbvt9fUDXw,48
36
+ ostruct_cli-0.4.0.dist-info/RECORD,,