ostruct-cli 0.8.7__tar.gz → 0.8.29__tar.gz

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.
Files changed (72) hide show
  1. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/PKG-INFO +78 -15
  2. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/README.md +73 -10
  3. ostruct_cli-0.8.29/pyproject.toml +160 -0
  4. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/cli.py +30 -0
  5. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/errors.py +16 -1
  6. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/file_info.py +28 -6
  7. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/file_list.py +39 -0
  8. ostruct_cli-0.8.29/src/ostruct/cli/utils.py +63 -0
  9. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/validators.py +17 -0
  10. ostruct_cli-0.8.7/pyproject.toml +0 -149
  11. ostruct_cli-0.8.7/src/ostruct/cli/utils.py +0 -33
  12. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/LICENSE +0 -0
  13. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/__init__.py +0 -0
  14. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/__init__.py +0 -0
  15. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/base_errors.py +0 -0
  16. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/cache_manager.py +0 -0
  17. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/click_options.py +0 -0
  18. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/code_interpreter.py +0 -0
  19. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/commands/__init__.py +0 -0
  20. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/commands/list_models.py +0 -0
  21. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/commands/quick_ref.py +0 -0
  22. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/commands/run.py +0 -0
  23. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/commands/update_registry.py +0 -0
  24. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/config.py +0 -0
  25. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/cost_estimation.py +0 -0
  26. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/exit_codes.py +0 -0
  27. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/explicit_file_processor.py +0 -0
  28. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/field_utils.py +0 -0
  29. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/file_search.py +0 -0
  30. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/file_utils.py +0 -0
  31. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/json_extract.py +0 -0
  32. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/mcp_integration.py +0 -0
  33. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/model_creation.py +0 -0
  34. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/model_validation.py +0 -0
  35. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/path_utils.py +0 -0
  36. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/progress.py +0 -0
  37. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/progress_reporting.py +0 -0
  38. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/registry_updates.py +0 -0
  39. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/runner.py +0 -0
  40. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/schema_utils.py +0 -0
  41. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/schema_validation.py +0 -0
  42. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/__init__.py +0 -0
  43. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/allowed_checker.py +0 -0
  44. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/base.py +0 -0
  45. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/case_manager.py +0 -0
  46. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/errors.py +0 -0
  47. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/normalization.py +0 -0
  48. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/safe_joiner.py +0 -0
  49. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/security_manager.py +0 -0
  50. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/symlink_resolver.py +0 -0
  51. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/types.py +0 -0
  52. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/security/windows_paths.py +0 -0
  53. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/sentinel.py +0 -0
  54. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/serialization.py +0 -0
  55. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/services.py +0 -0
  56. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_debug.py +0 -0
  57. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_debug_help.py +0 -0
  58. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_env.py +0 -0
  59. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_extensions.py +0 -0
  60. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_filters.py +0 -0
  61. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_io.py +0 -0
  62. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_optimizer.py +0 -0
  63. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_processor.py +0 -0
  64. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_rendering.py +0 -0
  65. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_schema.py +0 -0
  66. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_utils.py +0 -0
  67. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/template_validation.py +0 -0
  68. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/token_utils.py +0 -0
  69. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/token_validation.py +0 -0
  70. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/types.py +0 -0
  71. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/cli/unattended_operation.py +0 -0
  72. {ostruct_cli-0.8.7 → ostruct_cli-0.8.29}/src/ostruct/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ostruct-cli
3
- Version: 0.8.7
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://raw.githubusercontent.com/yaniv-golan/ostruct/main/scripts/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
@@ -151,23 +151,84 @@ Break down words into their components, showing their origins, meanings, and hie
151
151
 
152
152
  ## Installation
153
153
 
154
- ### Quick Install (macOS)
154
+ We provide multiple installation methods to suit different user needs. Choose the one that's right for you.
155
155
 
156
- For macOS users, we provide a one-line installer that handles everything automatically:
156
+ <details>
157
+ <summary><strong>Recommended: pipx (Python Users)</strong></summary>
158
+
159
+ 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.
160
+
161
+ 1. **Install pipx**:
162
+
163
+ ```bash
164
+ python3 -m pip install --user pipx
165
+ python3 -m pipx ensurepath
166
+ ```
167
+
168
+ *(Restart your terminal after running `ensurepath` to update your `PATH`)*
169
+
170
+ 2. **Install ostruct-cli**:
171
+
172
+ ```bash
173
+ pipx install ostruct-cli
174
+ ```
175
+
176
+ </details>
177
+
178
+ <details>
179
+ <summary><strong>macOS: Homebrew</strong></summary>
180
+
181
+ If you're on macOS and use Homebrew, you can install `ostruct` with a single command:
182
+
183
+ ```bash
184
+ brew install yaniv-golan/ostruct/ostruct-cli
185
+ ```
186
+
187
+ </details>
188
+
189
+ <details>
190
+ <summary><strong>Standalone Binaries (No Python Required)</strong></summary>
191
+
192
+ We provide pre-compiled .zip archives for macOS, Windows, and Linux that do not require Python to be installed.
193
+
194
+ 1. Go to the [**Latest Release**](https://github.com/yaniv-golan/ostruct/releases/latest) page.
195
+ 2. Download the `.zip` file for your operating system (e.g., `ostruct-macos-latest.zip`, `ostruct-windows-latest.zip`, `ostruct-ubuntu-latest.zip`).
196
+ 3. Extract the `.zip` file. This will create a folder (e.g., `ostruct-macos-amd64`).
197
+ 4. On macOS/Linux, make the executable inside the extracted folder runnable:
198
+
199
+ ```bash
200
+ chmod +x /path/to/ostruct-macos-amd64/ostruct
201
+ ```
202
+
203
+ 5. Run the executable from within the extracted folder, as it depends on bundled libraries in the same directory.
204
+
205
+ </details>
206
+
207
+ <details>
208
+ <summary><strong>Docker</strong></summary>
209
+
210
+ If you prefer to use Docker, you can run `ostruct` from our official container image available on the GitHub Container Registry.
157
211
 
158
212
  ```bash
159
- curl -sSL https://raw.githubusercontent.com/yaniv-golan/ostruct/main/scripts/install-macos.sh | bash
213
+ docker run -it --rm \
214
+ -v "$(pwd)":/app \
215
+ -w /app \
216
+ ghcr.io/yaniv-golan/ostruct:latest \
217
+ run template.j2 schema.json -ft input.txt
160
218
  ```
161
219
 
162
- This script will:
220
+ This command mounts the current directory into the container and runs `ostruct`.
163
221
 
164
- - Install Python 3.10+ if needed (via Homebrew or python.org)
165
- - Install Homebrew if needed
166
- - Install ostruct-cli via pip
167
- - Configure your shell PATH automatically
168
- - Verify the installation works
222
+ </details>
223
+
224
+ ### Uninstallation
169
225
 
170
- After installation, restart your terminal and test with `ostruct --version`.
226
+ To uninstall `ostruct`, use the method corresponding to how you installed it:
227
+
228
+ - **pipx**: `pipx uninstall ostruct-cli`
229
+ - **Homebrew**: `brew uninstall ostruct-cli`
230
+ - **Binaries**: Simply delete the binary file.
231
+ - **Docker**: No uninstallation is needed for the image itself, but you can remove it with `docker rmi ghcr.io/yaniv-golan/ostruct:latest`.
171
232
 
172
233
  ### Manual Installation
173
234
 
@@ -381,6 +442,8 @@ tools:
381
442
 
382
443
  🚀 **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.
383
444
 
445
+ 📝 **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!
446
+
384
447
  📖 **Full Documentation:** <https://ostruct.readthedocs.io/>
385
448
 
386
449
  ### Quick Start
@@ -0,0 +1,160 @@
1
+ [build-system]
2
+ requires = ["poetry-core"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [project]
6
+ name = "ostruct-cli"
7
+ version = "0.8.29"
8
+ description = "CLI for OpenAI Structured Output with Multi-Tool Integration"
9
+ authors = [{name = "Yaniv Golan", email = "yaniv@golan.name"}]
10
+ readme = "README.md"
11
+ requires-python = ">=3.10,<4.0"
12
+ dependencies = [
13
+ "pydantic>=2.6.3,<3.0",
14
+ "jsonschema>=4.23.0,<5.0",
15
+ "chardet>=5.0.0,<6.0",
16
+ "cachetools>=5.3.2,<6.0",
17
+ "ijson>=3.2.3,<4.0",
18
+ "typing-extensions>=4.9.0,<5.0",
19
+ "pyyaml>=6.0.2,<7.0",
20
+ "tomli>=2.0.1,<3.0;python_version<'3.11'",
21
+ "click>=8.1.7,<9.0",
22
+ "werkzeug>=3.1.3,<4.0",
23
+ "openai==1.81.0",
24
+ "tiktoken==0.9.0",
25
+ "pygments>=2.15.0,<3.0",
26
+ "jinja2>=3.1.2,<4.0",
27
+ "openai-model-registry>=0.7.0,<1.0",
28
+ "python-dotenv>=1.0.1,<2.0",
29
+ ]
30
+
31
+ [project.scripts]
32
+ ostruct = "ostruct.cli.cli:main"
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8.3.4,<9.0",
37
+ "pytest-rerunfailures>=12.0,<13.0",
38
+ "flake8>=6.0,<7.0",
39
+ "flake8-pyproject>=1.2.3,<2.0",
40
+ "black==24.8.0",
41
+ "mypy>=1.0,<2.0",
42
+ "pytest-asyncio>=0.25.2,<1.0",
43
+ "pytest-mock>=3.14.0,<4.0",
44
+ "build>=1.2.2.post1,<2.0",
45
+ "twine>=6.0.1,<7.0",
46
+ "python-dotenv>=1.0.1,<2.0",
47
+ "types-jsonschema>=4.23.0.20241208",
48
+ "anyio[trio]==3.7.1",
49
+ "sphinx>=7.0.0,<8.0",
50
+ "types-pyyaml>=6.0.12.20241230",
51
+ "types-pygments>=2.19.0.20250107",
52
+ "types-chardet>=5.0.4.6",
53
+ "pyfakefs>=5.7.4,<6.0",
54
+ "types-cachetools>=5.5.0.20240820",
55
+ "types-click>=7.1.8,<8.0",
56
+ "types-requests>=2.32.0.20241016",
57
+ "pre-commit>=4.1.0,<5.0",
58
+ "psutil>=7.0.0,<8.0",
59
+ "hypothesis>=6.0.0,<7.0",
60
+ "tomli>=2.0.1,<3.0",
61
+ ]
62
+ docs = [
63
+ "sphinx>=7.0.0,<8.0",
64
+ "sphinx-rtd-theme>=1.0,<2.0",
65
+ "myst-parser>=3.0.0,<4.0",
66
+ "sphinx-design>=0.4.1,<1.0",
67
+ "tomli>=2.0.1,<3.0;python_version<'3.11'",
68
+ ]
69
+ examples = [
70
+ "tenacity>=8.2.3,<9.0",
71
+ "asyncio-throttle>=1.0.2,<2.0",
72
+ ]
73
+
74
+ [tool.poetry]
75
+ packages = [{include = "ostruct", from = "src"}]
76
+ include = ["py.typed"]
77
+
78
+ [tool.poetry.group.dev.dependencies]
79
+ hypothesis = "^6.135.4"
80
+ pyinstaller = "^6.13"
81
+
82
+ [tool.flake8]
83
+ max-line-length = 120
84
+ extend-ignore = ["E203"]
85
+
86
+ [tool.mypy]
87
+ plugins = ["pydantic.mypy"]
88
+ python_version = "3.10"
89
+ warn_unused_configs = true
90
+ exclude = ["docs/*", "examples/*"]
91
+ disallow_untyped_defs = false
92
+ check_untyped_defs = true
93
+ warn_return_any = false
94
+ warn_unused_ignores = false
95
+ show_error_codes = true
96
+
97
+ [[tool.mypy.overrides]]
98
+ module = ["openai_model_registry.*", "hypothesis"]
99
+ ignore_missing_imports = true
100
+
101
+ # Stricter settings for source code
102
+ [[tool.mypy.overrides]]
103
+ module = "ostruct.*"
104
+ disallow_untyped_defs = true
105
+ warn_return_any = true
106
+ warn_unused_ignores = true
107
+
108
+ # Special handling for Click-related code
109
+ [[tool.mypy.overrides]]
110
+ module = ["click.*", "ostruct.cli.click_options"]
111
+ disallow_untyped_decorators = false
112
+ warn_return_any = false
113
+
114
+ [tool.black]
115
+ line-length = 79
116
+ target-version = ["py310"]
117
+ include = '.pyi?$'
118
+ preview = false
119
+ required-version = "24.8.0"
120
+
121
+ [tool.pytest.ini_options]
122
+ asyncio_mode = "strict"
123
+ testpaths = ["tests"]
124
+ python_files = ["test_*.py"]
125
+ markers = [
126
+ "live: mark test as a live test that makes real API calls or runs actual ostruct commands",
127
+ "asyncio: mark test as requiring async loop",
128
+ "no_fs: mark test to disable pyfakefs and use real filesystem",
129
+ "slow: mark test as slow performance/stress test",
130
+ "flaky: mark test as flaky (may need reruns)",
131
+ "mock_openai: mark test to use mock OpenAI client"
132
+ ]
133
+ asyncio_default_fixture_loop_scope = "function"
134
+ # By default, skip live tests unless explicitly requested
135
+ addopts = "-m 'not live'"
136
+
137
+ [tool.ruff]
138
+ target-version = "py310"
139
+ fix = true
140
+ line-length = 79 # Match Black's setting
141
+
142
+ [tool.ruff.lint]
143
+ select = [
144
+ "E", # pycodestyle errors
145
+ "W", # pycodestyle warnings
146
+ "F", # Pyflakes
147
+ "I", # isort (import sorting)
148
+ ]
149
+ ignore = [
150
+ "E501", # Line too long (handled by Black)
151
+ ]
152
+
153
+ [tool.ruff.lint.isort]
154
+ # Black-compatible settings
155
+ force-single-line = false
156
+ combine-as-imports = true
157
+ split-on-trailing-comma = true
158
+
159
+ [tool.poetry.dependencies]
160
+ python = ">=3.10,<3.14"
@@ -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
 
@@ -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):
@@ -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
  )
@@ -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."""
@@ -0,0 +1,63 @@
1
+ """Common utilities for the CLI package."""
2
+
3
+ from typing import Tuple
4
+
5
+ from .errors import VariableNameError, VariableValueError
6
+
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
+
38
+ def parse_mapping(mapping: str) -> Tuple[str, str]:
39
+ """Parse a mapping string in the format 'name=value'.
40
+
41
+ Args:
42
+ mapping: Mapping string in format 'name=value'
43
+
44
+ Returns:
45
+ Tuple of (name, value) with whitespace stripped from both parts
46
+
47
+ Raises:
48
+ ValueError: If mapping format is invalid
49
+ VariableNameError: If name part is empty
50
+ VariableValueError: If value part is empty
51
+ """
52
+ if not mapping or "=" not in mapping:
53
+ raise ValueError("Invalid mapping format")
54
+
55
+ name, value = mapping.split("=", 1)
56
+ name = name.strip()
57
+ value = value.strip()
58
+ if not name:
59
+ raise VariableNameError("Empty name in mapping")
60
+ if not value:
61
+ raise VariableValueError("Empty value in mapping")
62
+
63
+ return name, value
@@ -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,149 +0,0 @@
1
- [build-system]
2
- requires = ["poetry-core"]
3
- build-backend = "poetry.core.masonry.api"
4
-
5
- [project]
6
- name = "ostruct-cli"
7
- version = "0.8.7"
8
- description = "CLI for OpenAI Structured Output with Multi-Tool Integration"
9
- authors = [{name = "Yaniv Golan", email = "yaniv@golan.name"}]
10
- readme = "README.md"
11
- requires-python = ">=3.10,<4.0"
12
- dependencies = [
13
- "pydantic>=2.6.3,<3.0",
14
- "jsonschema>=4.23.0,<5.0",
15
- "chardet>=5.0.0,<6.0",
16
- "cachetools>=5.3.2,<6.0",
17
- "ijson>=3.2.3,<4.0",
18
- "typing-extensions>=4.9.0,<5.0",
19
- "pyyaml>=6.0.2,<7.0",
20
- "tomli>=2.0.1,<3.0;python_version<'3.11'",
21
- "click>=8.1.7,<9.0",
22
- "werkzeug>=3.1.3,<4.0",
23
- "openai==1.81.0",
24
- "tiktoken==0.9.0",
25
- "pygments>=2.15.0,<3.0",
26
- "jinja2>=3.1.2,<4.0",
27
- "openai-model-registry>=0.7.0,<1.0",
28
- "python-dotenv>=1.0.1,<2.0",
29
- ]
30
-
31
- [project.scripts]
32
- ostruct = "ostruct.cli.cli:main"
33
-
34
- [project.optional-dependencies]
35
- dev = [
36
- "pytest>=8.3.4,<9.0",
37
- "pytest-rerunfailures>=12.0,<13.0",
38
- "flake8>=6.0,<7.0",
39
- "flake8-pyproject>=1.2.3,<2.0",
40
- "black==24.8.0",
41
- "mypy>=1.0,<2.0",
42
- "pytest-asyncio>=0.25.2,<1.0",
43
- "pytest-mock>=3.14.0,<4.0",
44
- "build>=1.2.2.post1,<2.0",
45
- "twine>=6.0.1,<7.0",
46
- "python-dotenv>=1.0.1,<2.0",
47
- "types-jsonschema>=4.23.0.20241208",
48
- "anyio[trio]==3.7.1",
49
- "sphinx>=7.0,<8.0",
50
- "types-pyyaml>=6.0.12.20241230",
51
- "types-pygments>=2.19.0.20250107",
52
- "types-chardet>=5.0.4.6",
53
- "pyfakefs>=5.7.4,<6.0",
54
- "types-cachetools>=5.5.0.20240820",
55
- "types-click>=7.1.8,<8.0",
56
- "types-requests>=2.32.0.20241016",
57
- "pre-commit>=4.1.0,<5.0",
58
- "psutil>=7.0.0,<8.0",
59
- "hypothesis>=6.0.0,<7.0",
60
- "tomli>=2.0.1,<3.0",
61
- ]
62
- docs = [
63
- "sphinx>=7.0,<8.0",
64
- "sphinx-rtd-theme>=1.0,<2.0",
65
- "myst-parser>=2.0.0,<3.0",
66
- "sphinx-design>=0.5.0,<1.0",
67
- "tomli>=2.0.1,<3.0;python_version<'3.11'",
68
- ]
69
- examples = [
70
- "tenacity>=8.2.3,<9.0",
71
- "asyncio-throttle>=1.0.2,<2.0",
72
- ]
73
-
74
- [tool.poetry]
75
- packages = [{include = "ostruct", from = "src"}]
76
- include = ["py.typed"]
77
-
78
- [tool.flake8]
79
- max-line-length = 120
80
- extend-ignore = ["E203"]
81
-
82
- [tool.mypy]
83
- plugins = ["pydantic.mypy"]
84
- python_version = "3.10"
85
- warn_unused_configs = true
86
- exclude = ["docs/*", "examples/*"]
87
- disallow_untyped_defs = false
88
- check_untyped_defs = true
89
- warn_return_any = false
90
- warn_unused_ignores = false
91
- show_error_codes = true
92
-
93
- # Stricter settings for source code
94
- [[tool.mypy.overrides]]
95
- module = "ostruct.*"
96
- disallow_untyped_defs = true
97
- warn_return_any = true
98
- warn_unused_ignores = true
99
-
100
- # Special handling for Click-related code
101
- [[tool.mypy.overrides]]
102
- module = ["click.*", "ostruct.cli.click_options"]
103
- disallow_untyped_decorators = false
104
- warn_return_any = false
105
-
106
- [tool.black]
107
- line-length = 79
108
- target-version = ["py310"]
109
- include = '\.pyi?$'
110
- preview = false
111
- required-version = "24.8.0"
112
-
113
- [tool.pytest.ini_options]
114
- asyncio_mode = "strict"
115
- testpaths = ["tests"]
116
- python_files = ["test_*.py"]
117
- markers = [
118
- "live: mark test as a live test that makes real API calls or runs actual ostruct commands",
119
- "asyncio: mark test as requiring async loop",
120
- "no_fs: mark test to disable pyfakefs and use real filesystem",
121
- "slow: mark test as slow performance/stress test",
122
- "flaky: mark test as flaky (may need reruns)",
123
- "mock_openai: mark test to use mock OpenAI client"
124
- ]
125
- asyncio_default_fixture_loop_scope = "function"
126
- # By default, skip live tests unless explicitly requested
127
- addopts = "-m 'not live'"
128
-
129
- [tool.ruff]
130
- target-version = "py310"
131
- fix = true
132
- line-length = 79 # Match Black's setting
133
-
134
- [tool.ruff.lint]
135
- select = [
136
- "E", # pycodestyle errors
137
- "W", # pycodestyle warnings
138
- "F", # Pyflakes
139
- "I", # isort (import sorting)
140
- ]
141
- ignore = [
142
- "E501", # Line too long (handled by Black)
143
- ]
144
-
145
- [tool.ruff.lint.isort]
146
- # Black-compatible settings
147
- force-single-line = false
148
- combine-as-imports = true
149
- split-on-trailing-comma = true
@@ -1,33 +0,0 @@
1
- """Common utilities for the CLI package."""
2
-
3
- from typing import Tuple
4
-
5
- from .errors import VariableNameError, VariableValueError
6
-
7
-
8
- def parse_mapping(mapping: str) -> Tuple[str, str]:
9
- """Parse a mapping string in the format 'name=value'.
10
-
11
- Args:
12
- mapping: Mapping string in format 'name=value'
13
-
14
- Returns:
15
- Tuple of (name, value) with whitespace stripped from both parts
16
-
17
- Raises:
18
- ValueError: If mapping format is invalid
19
- VariableNameError: If name part is empty
20
- VariableValueError: If value part is empty
21
- """
22
- if not mapping or "=" not in mapping:
23
- raise ValueError("Invalid mapping format")
24
-
25
- name, value = mapping.split("=", 1)
26
- name = name.strip()
27
- value = value.strip()
28
- if not name:
29
- raise VariableNameError("Empty name in mapping")
30
- if not value:
31
- raise VariableValueError("Empty value in mapping")
32
-
33
- return name, value
File without changes