ostruct-cli 0.8.8__py3-none-any.whl → 0.8.29__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/cli/cli.py CHANGED
@@ -18,6 +18,33 @@ from .errors import (
18
18
  )
19
19
  from .exit_codes import ExitCode
20
20
  from .registry_updates import get_update_notification
21
+ from .utils import fix_surrogate_escapes
22
+
23
+
24
+ def fix_argv_encoding() -> None:
25
+ """Fix UTF-8 encoding issues in sys.argv.
26
+
27
+ This function addresses the surrogate escape issue where Python's sys.argv
28
+ contains surrogate characters (e.g., a backslash followed by 'udce2')
29
+ when processing command line arguments with non-ASCII characters. This
30
+ commonly happens with filenames containing characters like en dash (–) or
31
+ other Unicode characters.
32
+
33
+ The fix detects arguments containing surrogate escapes and converts them
34
+ back to proper UTF-8 strings.
35
+ """
36
+ try:
37
+ fixed_argv = []
38
+ for arg in sys.argv:
39
+ fixed_argv.append(fix_surrogate_escapes(arg))
40
+
41
+ # Replace sys.argv with the fixed version
42
+ sys.argv = fixed_argv
43
+
44
+ except Exception:
45
+ # If anything goes wrong with the encoding fix, continue with original argv
46
+ # This ensures the CLI doesn't break even if the fix fails
47
+ pass
21
48
 
22
49
 
23
50
  def create_cli_group() -> click.Group:
@@ -108,6 +135,9 @@ def create_cli() -> click.Command:
108
135
 
109
136
  def main() -> None:
110
137
  """Main entry point for the CLI."""
138
+ # Fix UTF-8 encoding issues in command line arguments before processing
139
+ fix_argv_encoding()
140
+
111
141
  # Load environment variables from .env file
112
142
  load_dotenv()
113
143
 
ostruct/cli/errors.py CHANGED
@@ -291,7 +291,22 @@ class TemplateValidationError(TaskTemplateError):
291
291
  class SystemPromptError(TaskTemplateError):
292
292
  """Raised when there are issues with system prompt loading or processing."""
293
293
 
294
- pass
294
+ def __init__(
295
+ self,
296
+ message: str,
297
+ context: Optional[Dict[str, Any]] = None,
298
+ ) -> None:
299
+ """Initialize system prompt error.
300
+
301
+ Args:
302
+ message: Error message
303
+ context: Additional error context
304
+ """
305
+ super().__init__(
306
+ message,
307
+ context=context,
308
+ exit_code=ExitCode.VALIDATION_ERROR,
309
+ )
295
310
 
296
311
 
297
312
  class SchemaError(CLIError):
ostruct/cli/file_info.py CHANGED
@@ -156,23 +156,45 @@ class FileInfo:
156
156
  Returns a path relative to the security manager's base directory.
157
157
  This ensures consistent path handling across the entire codebase.
158
158
 
159
+ For paths outside the base directory but within allowed directories,
160
+ returns the absolute path.
161
+
159
162
  Example:
160
163
  security_manager = SecurityManager(base_dir="/base")
161
164
  file_info = FileInfo("/base/file.txt", security_manager)
162
165
  print(file_info.path) # Outputs: "file.txt"
163
166
 
167
+ # With allowed directory outside base:
168
+ file_info = FileInfo("/tmp/file.txt", security_manager)
169
+ print(file_info.path) # Outputs: "/tmp/file.txt"
170
+
164
171
  Returns:
165
- str: Path relative to security manager's base directory
172
+ str: Path relative to security manager's base directory, or absolute path
173
+ if outside base directory but within allowed directories
166
174
 
167
175
  Raises:
168
- ValueError: If the path is not within the base directory
176
+ ValueError: If the path is not within the base directory or allowed directories
169
177
  """
178
+ abs_path = Path(self.abs_path)
179
+ base_dir = Path(self.__security_manager.base_dir)
180
+
170
181
  try:
171
- abs_path = Path(self.abs_path)
172
- base_dir = Path(self.__security_manager.base_dir)
173
182
  return str(abs_path.relative_to(base_dir))
174
- except ValueError as e:
175
- logger.error("Error making path relative: %s", e)
183
+ except ValueError:
184
+ # Path is outside base_dir, check if it's in allowed directories
185
+ if self.__security_manager.is_path_allowed(abs_path):
186
+ logger.debug(
187
+ "Path outside base_dir but allowed, returning absolute path: %s",
188
+ abs_path,
189
+ )
190
+ return str(abs_path)
191
+
192
+ # Should never reach here if SecurityManager validation was done properly
193
+ logger.error(
194
+ "Error making path relative: %s is not within base directory %s",
195
+ abs_path,
196
+ base_dir,
197
+ )
176
198
  raise ValueError(
177
199
  f"Path {abs_path} must be within base directory {base_dir}"
178
200
  )
ostruct/cli/file_list.py CHANGED
@@ -320,6 +320,45 @@ class FileInfoList(List[FileInfo]):
320
320
  # Single file from file mapping
321
321
  return self[0].name
322
322
 
323
+ @property
324
+ def extension(self) -> str:
325
+ """Get the file extension of a single file without dot.
326
+
327
+ Returns:
328
+ str: Extension of the single file from file mapping.
329
+
330
+ Raises:
331
+ ValueError: If the list is empty or contains multiple files.
332
+ """
333
+ with self._lock:
334
+ if not self:
335
+ var_name = self._var_alias or "file_list"
336
+ raise ValueError(
337
+ f"No files in '{var_name}'. Cannot access .extension property."
338
+ )
339
+
340
+ # Check for multiple files or directory mapping
341
+ if len(self) > 1:
342
+ var_name = self._var_alias or "file_list"
343
+ raise ValueError(
344
+ f"'{var_name}' contains {len(self)} files. "
345
+ f"Use '{{{{ {var_name}[0].extension }}}}' for the first file, "
346
+ f"'{{{{ {var_name}|single.extension }}}}' if expecting exactly one file, "
347
+ f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.extension }}}}{{%% endfor %%}}'."
348
+ )
349
+
350
+ if self._from_dir:
351
+ var_name = self._var_alias or "file_list"
352
+ raise ValueError(
353
+ f"'{var_name}' contains files from directory mapping. "
354
+ f"Use '{{{{ {var_name}[0].extension }}}}' for the first file, "
355
+ f"'{{{{ {var_name}|single.extension }}}}' if expecting exactly one file, "
356
+ f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.extension }}}}{{%% endfor %%}}'."
357
+ )
358
+
359
+ # Single file from file mapping
360
+ return self[0].extension
361
+
323
362
  @property
324
363
  def names(self) -> List[str]:
325
364
  """Get all filenames as a list."""
ostruct/cli/utils.py CHANGED
@@ -5,6 +5,36 @@ from typing import Tuple
5
5
  from .errors import VariableNameError, VariableValueError
6
6
 
7
7
 
8
+ def fix_surrogate_escapes(text: str) -> str:
9
+ """Fix UTF-8 encoding issues caused by surrogate escapes.
10
+
11
+ This function addresses the issue where Python's sys.argv contains
12
+ surrogate characters (e.g., a backslash followed by 'udce2') when
13
+ processing command line arguments with non-ASCII characters. This
14
+ commonly happens with filenames containing characters like en dash (–)
15
+ or other Unicode characters.
16
+
17
+ Args:
18
+ text: String that may contain surrogate escape characters
19
+
20
+ Returns:
21
+ String with surrogate escapes converted back to proper UTF-8
22
+ """
23
+ try:
24
+ # Check if the text contains surrogate characters
25
+ if any(0xDC00 <= ord(c) <= 0xDFFF for c in text):
26
+ # Convert surrogate escapes back to bytes, then decode as UTF-8
27
+ # This handles the case where Python used surrogateescape error handling
28
+ byte_data = text.encode("utf-8", "surrogateescape")
29
+ return byte_data.decode("utf-8")
30
+ else:
31
+ # No surrogate characters, return as-is
32
+ return text
33
+ except (UnicodeError, UnicodeDecodeError, UnicodeEncodeError):
34
+ # If conversion fails, return the original text
35
+ return text
36
+
37
+
8
38
  def parse_mapping(mapping: str) -> Tuple[str, str]:
9
39
  """Parse a mapping string in the format 'name=value'.
10
40
 
ostruct/cli/validators.py CHANGED
@@ -26,6 +26,7 @@ from .template_processor import (
26
26
  )
27
27
  from .template_utils import validate_json_schema
28
28
  from .types import CLIParams
29
+ from .utils import fix_surrogate_escapes
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
@@ -145,6 +146,9 @@ def validate_variable(
145
146
 
146
147
  result = []
147
148
  for var in value:
149
+ # Fix any surrogate escape issues in the variable string
150
+ var = fix_surrogate_escapes(var)
151
+
148
152
  if "=" not in var:
149
153
  raise click.BadParameter(
150
154
  f"Variable must be in format name=value: {var}"
@@ -152,6 +156,11 @@ def validate_variable(
152
156
  name, val = var.split("=", 1)
153
157
  name = name.strip()
154
158
  val = val.strip()
159
+
160
+ # Fix surrogate escapes in both name and value
161
+ name = fix_surrogate_escapes(name)
162
+ val = fix_surrogate_escapes(val)
163
+
155
164
  if not name.isidentifier():
156
165
  raise click.BadParameter(f"Invalid variable name: {name}")
157
166
  result.append((name, val))
@@ -179,6 +188,9 @@ def validate_json_variable(
179
188
 
180
189
  result = []
181
190
  for var in value:
191
+ # Fix any surrogate escape issues in the variable string
192
+ var = fix_surrogate_escapes(var)
193
+
182
194
  if "=" not in var:
183
195
  raise InvalidJSONError(
184
196
  f"JSON variable must be in format name='{'json':\"value\"}': {var}"
@@ -186,6 +198,11 @@ def validate_json_variable(
186
198
  name, json_str = var.split("=", 1)
187
199
  name = name.strip()
188
200
  json_str = json_str.strip()
201
+
202
+ # Fix surrogate escapes in both name and JSON string
203
+ name = fix_surrogate_escapes(name)
204
+ json_str = fix_surrogate_escapes(json_str)
205
+
189
206
  if not name.isidentifier():
190
207
  raise VariableNameError(f"Invalid variable name: {name}")
191
208
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ostruct-cli
3
- Version: 0.8.8
3
+ Version: 0.8.29
4
4
  Summary: CLI for OpenAI Structured Output with Multi-Tool Integration
5
5
  Author: Yaniv Golan
6
6
  Author-email: yaniv@golan.name
@@ -27,7 +27,7 @@ Requires-Dist: ijson (>=3.2.3,<4.0)
27
27
  Requires-Dist: jinja2 (>=3.1.2,<4.0)
28
28
  Requires-Dist: jsonschema (>=4.23.0,<5.0)
29
29
  Requires-Dist: mypy (>=1.0,<2.0) ; extra == "dev"
30
- Requires-Dist: myst-parser (>=2.0.0,<3.0) ; extra == "docs"
30
+ Requires-Dist: myst-parser (>=3.0.0,<4.0) ; extra == "docs"
31
31
  Requires-Dist: openai (==1.81.0)
32
32
  Requires-Dist: openai-model-registry (>=0.7.0,<1.0)
33
33
  Requires-Dist: pre-commit (>=4.1.0,<5.0) ; extra == "dev"
@@ -42,9 +42,9 @@ Requires-Dist: pytest-rerunfailures (>=12.0,<13.0) ; extra == "dev"
42
42
  Requires-Dist: python-dotenv (>=1.0.1,<2.0)
43
43
  Requires-Dist: python-dotenv (>=1.0.1,<2.0) ; extra == "dev"
44
44
  Requires-Dist: pyyaml (>=6.0.2,<7.0)
45
- Requires-Dist: sphinx (>=7.0,<8.0) ; extra == "dev"
46
- Requires-Dist: sphinx (>=7.0,<8.0) ; extra == "docs"
47
- Requires-Dist: sphinx-design (>=0.5.0,<1.0) ; extra == "docs"
45
+ Requires-Dist: sphinx (>=7.0.0,<8.0) ; extra == "dev"
46
+ Requires-Dist: sphinx (>=7.0.0,<8.0) ; extra == "docs"
47
+ Requires-Dist: sphinx-design (>=0.4.1,<1.0) ; extra == "docs"
48
48
  Requires-Dist: sphinx-rtd-theme (>=1.0,<2.0) ; extra == "docs"
49
49
  Requires-Dist: tenacity (>=8.2.3,<9.0) ; extra == "examples"
50
50
  Requires-Dist: tiktoken (==0.9.0)
@@ -216,23 +216,84 @@ Break down words into their components, showing their origins, meanings, and hie
216
216
 
217
217
  ## Installation
218
218
 
219
- ### Quick Install (macOS)
219
+ We provide multiple installation methods to suit different user needs. Choose the one that's right for you.
220
220
 
221
- For macOS users, we provide a one-line installer that handles everything automatically:
221
+ <details>
222
+ <summary><strong>Recommended: pipx (Python Users)</strong></summary>
223
+
224
+ For users who have Python installed, `pipx` is the recommended installation method. It installs `ostruct` in an isolated environment, preventing conflicts with other Python packages.
225
+
226
+ 1. **Install pipx**:
227
+
228
+ ```bash
229
+ python3 -m pip install --user pipx
230
+ python3 -m pipx ensurepath
231
+ ```
232
+
233
+ *(Restart your terminal after running `ensurepath` to update your `PATH`)*
234
+
235
+ 2. **Install ostruct-cli**:
236
+
237
+ ```bash
238
+ pipx install ostruct-cli
239
+ ```
240
+
241
+ </details>
242
+
243
+ <details>
244
+ <summary><strong>macOS: Homebrew</strong></summary>
245
+
246
+ If you're on macOS and use Homebrew, you can install `ostruct` with a single command:
247
+
248
+ ```bash
249
+ brew install yaniv-golan/ostruct/ostruct-cli
250
+ ```
251
+
252
+ </details>
253
+
254
+ <details>
255
+ <summary><strong>Standalone Binaries (No Python Required)</strong></summary>
256
+
257
+ We provide pre-compiled .zip archives for macOS, Windows, and Linux that do not require Python to be installed.
258
+
259
+ 1. Go to the [**Latest Release**](https://github.com/yaniv-golan/ostruct/releases/latest) page.
260
+ 2. Download the `.zip` file for your operating system (e.g., `ostruct-macos-latest.zip`, `ostruct-windows-latest.zip`, `ostruct-ubuntu-latest.zip`).
261
+ 3. Extract the `.zip` file. This will create a folder (e.g., `ostruct-macos-amd64`).
262
+ 4. On macOS/Linux, make the executable inside the extracted folder runnable:
263
+
264
+ ```bash
265
+ chmod +x /path/to/ostruct-macos-amd64/ostruct
266
+ ```
267
+
268
+ 5. Run the executable from within the extracted folder, as it depends on bundled libraries in the same directory.
269
+
270
+ </details>
271
+
272
+ <details>
273
+ <summary><strong>Docker</strong></summary>
274
+
275
+ If you prefer to use Docker, you can run `ostruct` from our official container image available on the GitHub Container Registry.
222
276
 
223
277
  ```bash
224
- curl -sSL https://github.com/yaniv-golan/ostruct/releases/latest/download/install-macos.sh | bash
278
+ docker run -it --rm \
279
+ -v "$(pwd)":/app \
280
+ -w /app \
281
+ ghcr.io/yaniv-golan/ostruct:latest \
282
+ run template.j2 schema.json -ft input.txt
225
283
  ```
226
284
 
227
- This script will:
285
+ This command mounts the current directory into the container and runs `ostruct`.
228
286
 
229
- - Install Python 3.10+ if needed (via Homebrew or python.org)
230
- - Install Homebrew if needed
231
- - Install ostruct-cli via pip
232
- - Configure your shell PATH automatically
233
- - Verify the installation works
287
+ </details>
288
+
289
+ ### Uninstallation
234
290
 
235
- After installation, restart your terminal and test with `ostruct --version`.
291
+ To uninstall `ostruct`, use the method corresponding to how you installed it:
292
+
293
+ - **pipx**: `pipx uninstall ostruct-cli`
294
+ - **Homebrew**: `brew uninstall ostruct-cli`
295
+ - **Binaries**: Simply delete the binary file.
296
+ - **Docker**: No uninstallation is needed for the image itself, but you can remove it with `docker rmi ghcr.io/yaniv-golan/ostruct:latest`.
236
297
 
237
298
  ### Manual Installation
238
299
 
@@ -446,6 +507,8 @@ tools:
446
507
 
447
508
  🚀 **New to ostruct?** Follow our [step-by-step quickstart guide](https://ostruct.readthedocs.io/en/latest/user-guide/quickstart.html) featuring Juno the beagle for a hands-on introduction.
448
509
 
510
+ 📝 **Template Scripting:** Learn ostruct's templating capabilities with the [template scripting guide](https://ostruct.readthedocs.io/en/latest/user-guide/ostruct_template_scripting_guide.html) - no prior Jinja2 knowledge required!
511
+
449
512
  📖 **Full Documentation:** <https://ostruct.readthedocs.io/>
450
513
 
451
514
  ### Quick Start
@@ -2,7 +2,7 @@ ostruct/__init__.py,sha256=X6zo6V7ZNMv731Wi388aTVQngD1410ExGwGx4J6lpyo,187
2
2
  ostruct/cli/__init__.py,sha256=e-DtWRviCr3fIAJH4cB4UAvles3-rqnhJaTOlBn9TKs,871
3
3
  ostruct/cli/base_errors.py,sha256=o-877bJJA8yJWISRPy0KyL6wDu1-_avddmQIfVePuFM,5989
4
4
  ostruct/cli/cache_manager.py,sha256=ej3KrRfkKKZ_lEp2JswjbJ5bW2ncsvna9NeJu81cqqs,5192
5
- ostruct/cli/cli.py,sha256=pzOzKMvWQMRS1I9w4raAUi-O7IN6OKViNTcOeJGEqhI,4196
5
+ ostruct/cli/cli.py,sha256=QtWoNskOvus9gYvxjSxdhXRpbdwIYwA9Di1f93bkVpE,5266
6
6
  ostruct/cli/click_options.py,sha256=UWLBIkfVMXcIMXInJFWNYNO9Ieg8YrJEzRfkKungy48,31730
7
7
  ostruct/cli/code_interpreter.py,sha256=lnnyEvUh2pGObN9ENpr-X4p0C0oIWiyG1CFQWW4akBQ,16726
8
8
  ostruct/cli/commands/__init__.py,sha256=3NHz-WZ9XqrnWWMksoV2MpYpHnjA-EO9lsrBOYeHcjY,723
@@ -12,12 +12,12 @@ ostruct/cli/commands/run.py,sha256=Cm9Yuf0DLt5CqKfgAYubhQRcvLdK1vqIIuz_ynjfhQ4,6
12
12
  ostruct/cli/commands/update_registry.py,sha256=7DQrPlCJScPVgE2HbFAM7UMap-EdYu58AQWfpI-H7Gw,2483
13
13
  ostruct/cli/config.py,sha256=7tKI1gWLpTISn5OorGWIx66N1J7XW2aq30hPNISZzQ0,9958
14
14
  ostruct/cli/cost_estimation.py,sha256=08hyE-kM5QYzj7y-KB3lMD_RxCMoM_Ve3-IQlSpJAo4,4483
15
- ostruct/cli/errors.py,sha256=sVOM_ZKvrtnmT8_WR-DfX2crs6HmvmwrbNQlxm-5_Ew,24669
15
+ ostruct/cli/errors.py,sha256=Akw-_pAuvy18PKrlYdkpF0sNBrc82M4rGKtDWuPQpQg,25065
16
16
  ostruct/cli/exit_codes.py,sha256=gdvB1dpu0LalEUZrofpk5K6aTQ24n5AfkAK5umludHU,365
17
17
  ostruct/cli/explicit_file_processor.py,sha256=B6yUPbyn6MVd81GcyMVpORFwyaHFFESLwFixp2B6M5w,19767
18
18
  ostruct/cli/field_utils.py,sha256=bcRi1qQ0Ac2UCfrKSQ677_yu-VzaShm_zN7QLf98qc0,1939
19
- ostruct/cli/file_info.py,sha256=s8AHPtyU3__2LRJCCIEPuODYM43is4Y2Q_VDhBOl3XU,17395
20
- ostruct/cli/file_list.py,sha256=alRAguq4tj1zH0_qlWaRoyHo1G1Xmxqu9Xd4QP-zYP0,20268
19
+ ostruct/cli/file_info.py,sha256=B5mFX9AI9-H6yM2FhcdVtwibFS6NbZplXPW9vYjozOs,18336
20
+ ostruct/cli/file_list.py,sha256=upaXaQVmuxwjdmXzTRlvttMZjnpv_B2JzvmhoWTvsUg,21978
21
21
  ostruct/cli/file_search.py,sha256=N12mkji2ttvItLVPyAWE3KEfhTv8hV5IXPrQME2UFdE,15313
22
22
  ostruct/cli/file_utils.py,sha256=JZprQ-1LHQzI3eBfeCIS6VmxTa2fGUZHygGC8gcwpJM,24367
23
23
  ostruct/cli/json_extract.py,sha256=ZleIxat8g-JnA9VVqWgJaKxN7QzL25itQ8aP0Gb5e4Q,2650
@@ -61,11 +61,11 @@ ostruct/cli/token_utils.py,sha256=r4KPEO3Sec18Q6mU0aClK6XGShvusgUggXEQgEPPlaA,13
61
61
  ostruct/cli/token_validation.py,sha256=gmyPJ7B2gC_jSx_1wKZq87DEoFulj23X1XnVpO_aRNA,9930
62
62
  ostruct/cli/types.py,sha256=6nARJ4MF5HIank0t6KGU-PPHC0VpFh3R8fNJZBXwgbA,2903
63
63
  ostruct/cli/unattended_operation.py,sha256=kI95SSVJC_taxORXQYrce_qLEnuKc6edwn9tMOye-qs,9383
64
- ostruct/cli/utils.py,sha256=uY7c0NaINHWfnl77FcPE3TmYUXv3RqEeUTjrCMDij9A,922
65
- ostruct/cli/validators.py,sha256=lbxAUUVS5TPJ7HdYZ5yB7gUjJqfcClZCuh0oktoq0E0,15291
64
+ ostruct/cli/utils.py,sha256=DV8KAx46rl3GgXLydVhrP6A1xY7ofyir3hEKUslSfek,2149
65
+ ostruct/cli/validators.py,sha256=VbafaT7QyKAqpnHbqpibE3xkuaZ74e6zQ7Ih3WbzyRI,15844
66
66
  ostruct/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
- ostruct_cli-0.8.8.dist-info/LICENSE,sha256=DmGAkaYzhrdzTB9Y2Rvfzd3mJiF9ZrTOhxN8t6wrfHA,1098
68
- ostruct_cli-0.8.8.dist-info/METADATA,sha256=IHYyaz_fnJAXqPA8HFqdJuX-b5BEdPlFBaRthMbIE80,25918
69
- ostruct_cli-0.8.8.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
70
- ostruct_cli-0.8.8.dist-info/entry_points.txt,sha256=NFq9IuqHVTem0j9zKjV8C1si_zGcP1RL6Wbvt9fUDXw,48
71
- ostruct_cli-0.8.8.dist-info/RECORD,,
67
+ ostruct_cli-0.8.29.dist-info/LICENSE,sha256=DmGAkaYzhrdzTB9Y2Rvfzd3mJiF9ZrTOhxN8t6wrfHA,1098
68
+ ostruct_cli-0.8.29.dist-info/METADATA,sha256=t-6EX_pD42Nt54cN-XFCc9yQVbbF5NasLGUwnempZL0,28124
69
+ ostruct_cli-0.8.29.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
70
+ ostruct_cli-0.8.29.dist-info/entry_points.txt,sha256=NFq9IuqHVTem0j9zKjV8C1si_zGcP1RL6Wbvt9fUDXw,48
71
+ ostruct_cli-0.8.29.dist-info/RECORD,,