ostruct-cli 0.8.2__tar.gz → 0.8.4__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.
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/PKG-INFO +96 -2
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/README.md +91 -1
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/pyproject.toml +12 -4
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/cli.py +4 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/click_options.py +113 -16
- ostruct_cli-0.8.4/src/ostruct/cli/code_interpreter.py +431 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/run.py +56 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/config.py +20 -1
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/errors.py +2 -30
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_info.py +55 -20
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_utils.py +19 -3
- ostruct_cli-0.8.4/src/ostruct/cli/json_extract.py +75 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/model_creation.py +1 -1
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/runner.py +476 -195
- ostruct_cli-0.8.4/src/ostruct/cli/sentinel.py +29 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_optimizer.py +11 -7
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_processor.py +243 -115
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_rendering.py +41 -1
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_validation.py +41 -3
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/types.py +14 -1
- ostruct_cli-0.8.2/src/ostruct/cli/code_interpreter.py +0 -238
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/LICENSE +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/__init__.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/__init__.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/base_errors.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/cache_manager.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/__init__.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/list_models.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/quick_ref.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/update_registry.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/cost_estimation.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/exit_codes.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/explicit_file_processor.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/field_utils.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_list.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_search.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/mcp_integration.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/model_validation.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/path_utils.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/progress.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/progress_reporting.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/registry_updates.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/schema_utils.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/schema_validation.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/__init__.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/allowed_checker.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/base.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/case_manager.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/errors.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/normalization.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/safe_joiner.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/security_manager.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/symlink_resolver.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/types.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/windows_paths.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/serialization.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/services.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_debug.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_debug_help.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_env.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_extensions.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_filters.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_io.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_schema.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_utils.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/token_utils.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/token_validation.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/unattended_operation.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/utils.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/validators.py +0 -0
- {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/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.
|
3
|
+
Version: 0.8.4
|
4
4
|
Summary: CLI for OpenAI Structured Output with Multi-Tool Integration
|
5
5
|
Author: Yaniv Golan
|
6
6
|
Author-email: yaniv@golan.name
|
@@ -22,6 +22,7 @@ Requires-Dist: chardet (>=5.0.0,<6.0)
|
|
22
22
|
Requires-Dist: click (>=8.1.7,<9.0)
|
23
23
|
Requires-Dist: flake8 (>=6.0,<7.0) ; extra == "dev"
|
24
24
|
Requires-Dist: flake8-pyproject (>=1.2.3,<2.0) ; extra == "dev"
|
25
|
+
Requires-Dist: hypothesis (>=6.0.0,<7.0) ; extra == "dev"
|
25
26
|
Requires-Dist: ijson (>=3.2.3,<4.0)
|
26
27
|
Requires-Dist: jinja2 (>=3.1.2,<4.0)
|
27
28
|
Requires-Dist: jsonschema (>=4.23.0,<5.0)
|
@@ -37,6 +38,8 @@ Requires-Dist: pygments (>=2.15.0,<3.0)
|
|
37
38
|
Requires-Dist: pytest (>=8.3.4,<9.0) ; extra == "dev"
|
38
39
|
Requires-Dist: pytest-asyncio (>=0.25.2,<1.0) ; extra == "dev"
|
39
40
|
Requires-Dist: pytest-mock (>=3.14.0,<4.0) ; extra == "dev"
|
41
|
+
Requires-Dist: pytest-rerunfailures (>=12.0,<13.0) ; extra == "dev"
|
42
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0)
|
40
43
|
Requires-Dist: python-dotenv (>=1.0.1,<2.0) ; extra == "dev"
|
41
44
|
Requires-Dist: pyyaml (>=6.0.2,<7.0)
|
42
45
|
Requires-Dist: sphinx (>=7.0,<8.0) ; extra == "dev"
|
@@ -46,6 +49,7 @@ Requires-Dist: sphinx-rtd-theme (>=1.0,<2.0) ; extra == "docs"
|
|
46
49
|
Requires-Dist: tenacity (>=8.2.3,<9.0) ; extra == "examples"
|
47
50
|
Requires-Dist: tiktoken (==0.9.0)
|
48
51
|
Requires-Dist: tomli (>=2.0.1,<3.0) ; (python_version < "3.11") and (extra == "docs")
|
52
|
+
Requires-Dist: tomli (>=2.0.1,<3.0) ; extra == "dev"
|
49
53
|
Requires-Dist: tomli (>=2.0.1,<3.0) ; python_version < "3.11"
|
50
54
|
Requires-Dist: twine (>=6.0.1,<7.0) ; extra == "dev"
|
51
55
|
Requires-Dist: types-cachetools (>=5.5.0.20240820) ; extra == "dev"
|
@@ -235,6 +239,8 @@ ostruct-cli respects the following environment variables:
|
|
235
239
|
- `OSTRUCT_DISABLE_REGISTRY_UPDATE_CHECKS`: Set to "1", "true", or "yes" to disable automatic registry update checks
|
236
240
|
- `MCP_<NAME>_URL`: Custom MCP server URLs (e.g., `MCP_STRIPE_URL=https://mcp.stripe.com`)
|
237
241
|
|
242
|
+
**💡 Tip**: ostruct automatically loads `.env` files from the current directory. Environment variables take precedence over `.env` file values.
|
243
|
+
|
238
244
|
<details>
|
239
245
|
<summary><strong>Shell Completion Setup</strong> (Click to expand)</summary>
|
240
246
|
|
@@ -302,7 +308,7 @@ ostruct run analysis.j2 schema.json -fc data.csv
|
|
302
308
|
ostruct run search.j2 schema.json -fs documentation.pdf
|
303
309
|
|
304
310
|
# Web Search (real-time information)
|
305
|
-
ostruct run research.j2 schema.json --web-search -V topic="latest AI developments"
|
311
|
+
ostruct run research.j2 schema.json --enable-tool web-search -V topic="latest AI developments"
|
306
312
|
|
307
313
|
# Multiple tools with one file
|
308
314
|
ostruct run template.j2 schema.json --file-for code-interpreter shared.json --file-for file-search shared.json
|
@@ -370,6 +376,7 @@ tools:
|
|
370
376
|
code_interpreter:
|
371
377
|
auto_download: true
|
372
378
|
output_directory: "./output"
|
379
|
+
download_strategy: "two_pass_sentinel" # Enable reliable file downloads
|
373
380
|
|
374
381
|
mcp:
|
375
382
|
custom_server: "https://my-mcp-server.com"
|
@@ -384,6 +391,35 @@ Load custom configuration:
|
|
384
391
|
ostruct --config my-config.yaml run template.j2 schema.json
|
385
392
|
```
|
386
393
|
|
394
|
+
### Code Interpreter File Downloads
|
395
|
+
|
396
|
+
**Important**: If you're using Code Interpreter with structured output (JSON schemas), you may need to enable the two-pass download strategy to ensure files are downloaded reliably.
|
397
|
+
|
398
|
+
#### Option 1: CLI Flags (Recommended for one-off usage)
|
399
|
+
|
400
|
+
```bash
|
401
|
+
# Enable reliable file downloads for this run
|
402
|
+
ostruct run template.j2 schema.json -fc data.csv --enable-feature ci-download-hack
|
403
|
+
|
404
|
+
# Force single-pass mode (override config)
|
405
|
+
ostruct run template.j2 schema.json -fc data.csv --disable-feature ci-download-hack
|
406
|
+
```
|
407
|
+
|
408
|
+
#### Option 2: Configuration File (Recommended for persistent settings)
|
409
|
+
|
410
|
+
```yaml
|
411
|
+
# ostruct.yaml
|
412
|
+
tools:
|
413
|
+
code_interpreter:
|
414
|
+
download_strategy: "two_pass_sentinel" # Enables reliable file downloads
|
415
|
+
auto_download: true
|
416
|
+
output_directory: "./downloads"
|
417
|
+
```
|
418
|
+
|
419
|
+
**Why this is needed**: OpenAI's structured output mode can prevent file download annotations from being generated. The two-pass strategy works around this by making two API calls: one to generate files (without structured output), then another to ensure schema compliance. For detailed technical information, see [docs/known-issues/2025-06-responses-ci-file-output.md](docs/known-issues/2025-06-responses-ci-file-output.md).
|
420
|
+
|
421
|
+
**Performance**: The two-pass strategy approximately doubles token usage but ensures reliable file downloads when using structured output with Code Interpreter.
|
422
|
+
|
387
423
|
## Get Started Quickly
|
388
424
|
|
389
425
|
🚀 **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.
|
@@ -395,7 +431,11 @@ ostruct --config my-config.yaml run template.j2 schema.json
|
|
395
431
|
1. Set your OpenAI API key:
|
396
432
|
|
397
433
|
```bash
|
434
|
+
# Environment variable
|
398
435
|
export OPENAI_API_KEY=your-api-key
|
436
|
+
|
437
|
+
# Or create a .env file
|
438
|
+
echo 'OPENAI_API_KEY=your-api-key' > .env
|
399
439
|
```
|
400
440
|
|
401
441
|
### Example 1: Basic Text Extraction (Simplest)
|
@@ -628,6 +668,60 @@ The registry file is stored at `~/.openai_structured/config/models.yml` and is a
|
|
628
668
|
|
629
669
|
The update command uses HTTP conditional requests (If-Modified-Since headers) to check if the remote registry has changed before downloading, ensuring efficient updates.
|
630
670
|
|
671
|
+
# Testing
|
672
|
+
|
673
|
+
## Running Tests
|
674
|
+
|
675
|
+
The test suite is divided into two categories:
|
676
|
+
|
677
|
+
### Regular Tests (Default)
|
678
|
+
|
679
|
+
```bash
|
680
|
+
# Run all tests (skips live tests by default)
|
681
|
+
pytest
|
682
|
+
|
683
|
+
# Run specific test file
|
684
|
+
pytest tests/test_config.py
|
685
|
+
|
686
|
+
# Run with verbose output
|
687
|
+
pytest -v
|
688
|
+
```
|
689
|
+
|
690
|
+
### Live Tests
|
691
|
+
|
692
|
+
Live tests make real API calls to OpenAI and require a valid API key. They are skipped by default.
|
693
|
+
|
694
|
+
```bash
|
695
|
+
# Run only live tests (requires OPENAI_API_KEY)
|
696
|
+
pytest -m live
|
697
|
+
|
698
|
+
# Run all tests including live tests
|
699
|
+
pytest -m "live or not live"
|
700
|
+
|
701
|
+
# Run specific live test
|
702
|
+
pytest tests/test_responses_annotations.py -m live
|
703
|
+
```
|
704
|
+
|
705
|
+
**Live tests include:**
|
706
|
+
|
707
|
+
- Tests that make actual OpenAI API calls
|
708
|
+
- Tests that run `ostruct` commands via subprocess
|
709
|
+
- Tests that verify real API behavior and file downloads
|
710
|
+
|
711
|
+
**Requirements for live tests:**
|
712
|
+
|
713
|
+
- Valid `OPENAI_API_KEY` environment variable
|
714
|
+
- Internet connection
|
715
|
+
- May incur API costs
|
716
|
+
|
717
|
+
## Test Markers
|
718
|
+
|
719
|
+
- `@pytest.mark.live` - Tests that make real API calls or run actual commands
|
720
|
+
- `@pytest.mark.no_fs` - Tests that need real filesystem (not pyfakefs)
|
721
|
+
- `@pytest.mark.slow` - Performance/stress tests
|
722
|
+
- `@pytest.mark.flaky` - Tests that may need reruns
|
723
|
+
- `@pytest.mark.mock_openai` - Tests using mocked OpenAI client
|
724
|
+
|
631
725
|
<!--
|
632
726
|
MAINTAINER NOTE: After editing this README, please test GitHub rendering by:
|
633
727
|
1. Creating a draft PR or pushing to a test branch
|
@@ -174,6 +174,8 @@ ostruct-cli respects the following environment variables:
|
|
174
174
|
- `OSTRUCT_DISABLE_REGISTRY_UPDATE_CHECKS`: Set to "1", "true", or "yes" to disable automatic registry update checks
|
175
175
|
- `MCP_<NAME>_URL`: Custom MCP server URLs (e.g., `MCP_STRIPE_URL=https://mcp.stripe.com`)
|
176
176
|
|
177
|
+
**💡 Tip**: ostruct automatically loads `.env` files from the current directory. Environment variables take precedence over `.env` file values.
|
178
|
+
|
177
179
|
<details>
|
178
180
|
<summary><strong>Shell Completion Setup</strong> (Click to expand)</summary>
|
179
181
|
|
@@ -241,7 +243,7 @@ ostruct run analysis.j2 schema.json -fc data.csv
|
|
241
243
|
ostruct run search.j2 schema.json -fs documentation.pdf
|
242
244
|
|
243
245
|
# Web Search (real-time information)
|
244
|
-
ostruct run research.j2 schema.json --web-search -V topic="latest AI developments"
|
246
|
+
ostruct run research.j2 schema.json --enable-tool web-search -V topic="latest AI developments"
|
245
247
|
|
246
248
|
# Multiple tools with one file
|
247
249
|
ostruct run template.j2 schema.json --file-for code-interpreter shared.json --file-for file-search shared.json
|
@@ -309,6 +311,7 @@ tools:
|
|
309
311
|
code_interpreter:
|
310
312
|
auto_download: true
|
311
313
|
output_directory: "./output"
|
314
|
+
download_strategy: "two_pass_sentinel" # Enable reliable file downloads
|
312
315
|
|
313
316
|
mcp:
|
314
317
|
custom_server: "https://my-mcp-server.com"
|
@@ -323,6 +326,35 @@ Load custom configuration:
|
|
323
326
|
ostruct --config my-config.yaml run template.j2 schema.json
|
324
327
|
```
|
325
328
|
|
329
|
+
### Code Interpreter File Downloads
|
330
|
+
|
331
|
+
**Important**: If you're using Code Interpreter with structured output (JSON schemas), you may need to enable the two-pass download strategy to ensure files are downloaded reliably.
|
332
|
+
|
333
|
+
#### Option 1: CLI Flags (Recommended for one-off usage)
|
334
|
+
|
335
|
+
```bash
|
336
|
+
# Enable reliable file downloads for this run
|
337
|
+
ostruct run template.j2 schema.json -fc data.csv --enable-feature ci-download-hack
|
338
|
+
|
339
|
+
# Force single-pass mode (override config)
|
340
|
+
ostruct run template.j2 schema.json -fc data.csv --disable-feature ci-download-hack
|
341
|
+
```
|
342
|
+
|
343
|
+
#### Option 2: Configuration File (Recommended for persistent settings)
|
344
|
+
|
345
|
+
```yaml
|
346
|
+
# ostruct.yaml
|
347
|
+
tools:
|
348
|
+
code_interpreter:
|
349
|
+
download_strategy: "two_pass_sentinel" # Enables reliable file downloads
|
350
|
+
auto_download: true
|
351
|
+
output_directory: "./downloads"
|
352
|
+
```
|
353
|
+
|
354
|
+
**Why this is needed**: OpenAI's structured output mode can prevent file download annotations from being generated. The two-pass strategy works around this by making two API calls: one to generate files (without structured output), then another to ensure schema compliance. For detailed technical information, see [docs/known-issues/2025-06-responses-ci-file-output.md](docs/known-issues/2025-06-responses-ci-file-output.md).
|
355
|
+
|
356
|
+
**Performance**: The two-pass strategy approximately doubles token usage but ensures reliable file downloads when using structured output with Code Interpreter.
|
357
|
+
|
326
358
|
## Get Started Quickly
|
327
359
|
|
328
360
|
🚀 **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.
|
@@ -334,7 +366,11 @@ ostruct --config my-config.yaml run template.j2 schema.json
|
|
334
366
|
1. Set your OpenAI API key:
|
335
367
|
|
336
368
|
```bash
|
369
|
+
# Environment variable
|
337
370
|
export OPENAI_API_KEY=your-api-key
|
371
|
+
|
372
|
+
# Or create a .env file
|
373
|
+
echo 'OPENAI_API_KEY=your-api-key' > .env
|
338
374
|
```
|
339
375
|
|
340
376
|
### Example 1: Basic Text Extraction (Simplest)
|
@@ -567,6 +603,60 @@ The registry file is stored at `~/.openai_structured/config/models.yml` and is a
|
|
567
603
|
|
568
604
|
The update command uses HTTP conditional requests (If-Modified-Since headers) to check if the remote registry has changed before downloading, ensuring efficient updates.
|
569
605
|
|
606
|
+
# Testing
|
607
|
+
|
608
|
+
## Running Tests
|
609
|
+
|
610
|
+
The test suite is divided into two categories:
|
611
|
+
|
612
|
+
### Regular Tests (Default)
|
613
|
+
|
614
|
+
```bash
|
615
|
+
# Run all tests (skips live tests by default)
|
616
|
+
pytest
|
617
|
+
|
618
|
+
# Run specific test file
|
619
|
+
pytest tests/test_config.py
|
620
|
+
|
621
|
+
# Run with verbose output
|
622
|
+
pytest -v
|
623
|
+
```
|
624
|
+
|
625
|
+
### Live Tests
|
626
|
+
|
627
|
+
Live tests make real API calls to OpenAI and require a valid API key. They are skipped by default.
|
628
|
+
|
629
|
+
```bash
|
630
|
+
# Run only live tests (requires OPENAI_API_KEY)
|
631
|
+
pytest -m live
|
632
|
+
|
633
|
+
# Run all tests including live tests
|
634
|
+
pytest -m "live or not live"
|
635
|
+
|
636
|
+
# Run specific live test
|
637
|
+
pytest tests/test_responses_annotations.py -m live
|
638
|
+
```
|
639
|
+
|
640
|
+
**Live tests include:**
|
641
|
+
|
642
|
+
- Tests that make actual OpenAI API calls
|
643
|
+
- Tests that run `ostruct` commands via subprocess
|
644
|
+
- Tests that verify real API behavior and file downloads
|
645
|
+
|
646
|
+
**Requirements for live tests:**
|
647
|
+
|
648
|
+
- Valid `OPENAI_API_KEY` environment variable
|
649
|
+
- Internet connection
|
650
|
+
- May incur API costs
|
651
|
+
|
652
|
+
## Test Markers
|
653
|
+
|
654
|
+
- `@pytest.mark.live` - Tests that make real API calls or run actual commands
|
655
|
+
- `@pytest.mark.no_fs` - Tests that need real filesystem (not pyfakefs)
|
656
|
+
- `@pytest.mark.slow` - Performance/stress tests
|
657
|
+
- `@pytest.mark.flaky` - Tests that may need reruns
|
658
|
+
- `@pytest.mark.mock_openai` - Tests using mocked OpenAI client
|
659
|
+
|
570
660
|
<!--
|
571
661
|
MAINTAINER NOTE: After editing this README, please test GitHub rendering by:
|
572
662
|
1. Creating a draft PR or pushing to a test branch
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "ostruct-cli"
|
7
|
-
version = "0.8.
|
7
|
+
version = "0.8.4"
|
8
8
|
description = "CLI for OpenAI Structured Output with Multi-Tool Integration"
|
9
9
|
authors = [{name = "Yaniv Golan", email = "yaniv@golan.name"}]
|
10
10
|
readme = "README.md"
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
25
25
|
"pygments>=2.15.0,<3.0",
|
26
26
|
"jinja2>=3.1.2,<4.0",
|
27
27
|
"openai-model-registry>=0.7.0,<1.0",
|
28
|
+
"python-dotenv>=1.0.1,<2.0",
|
28
29
|
]
|
29
30
|
|
30
31
|
[project.scripts]
|
@@ -33,6 +34,7 @@ ostruct = "ostruct.cli.cli:main"
|
|
33
34
|
[project.optional-dependencies]
|
34
35
|
dev = [
|
35
36
|
"pytest>=8.3.4,<9.0",
|
37
|
+
"pytest-rerunfailures>=12.0,<13.0",
|
36
38
|
"flake8>=6.0,<7.0",
|
37
39
|
"flake8-pyproject>=1.2.3,<2.0",
|
38
40
|
"black==24.8.0",
|
@@ -54,6 +56,8 @@ dev = [
|
|
54
56
|
"types-requests>=2.32.0.20241016",
|
55
57
|
"pre-commit>=4.1.0,<5.0",
|
56
58
|
"psutil>=7.0.0,<8.0",
|
59
|
+
"hypothesis>=6.0.0,<7.0",
|
60
|
+
"tomli>=2.0.1,<3.0",
|
57
61
|
]
|
58
62
|
docs = [
|
59
63
|
"sphinx>=7.0,<8.0",
|
@@ -111,12 +115,16 @@ include = ["py.typed"]
|
|
111
115
|
testpaths = ["tests"]
|
112
116
|
python_files = ["test_*.py"]
|
113
117
|
markers = [
|
114
|
-
"live: mark test as a live test that
|
118
|
+
"live: mark test as a live test that makes real API calls or runs actual ostruct commands",
|
115
119
|
"asyncio: mark test as requiring async loop",
|
116
|
-
"
|
117
|
-
"slow: mark test as slow performance/stress test"
|
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"
|
118
124
|
]
|
119
125
|
asyncio_default_fixture_loop_scope = "function"
|
126
|
+
# By default, skip live tests unless explicitly requested
|
127
|
+
addopts = "-m 'not live'"
|
120
128
|
|
121
129
|
[tool.ruff]
|
122
130
|
target-version = "py310"
|
@@ -4,6 +4,7 @@ import sys
|
|
4
4
|
from typing import Optional
|
5
5
|
|
6
6
|
import click
|
7
|
+
from dotenv import load_dotenv
|
7
8
|
|
8
9
|
from .. import __version__
|
9
10
|
from .commands import create_command_group
|
@@ -107,6 +108,9 @@ def create_cli() -> click.Command:
|
|
107
108
|
|
108
109
|
def main() -> None:
|
109
110
|
"""Main entry point for the CLI."""
|
111
|
+
# Load environment variables from .env file
|
112
|
+
load_dotenv()
|
113
|
+
|
110
114
|
try:
|
111
115
|
cli(standalone_mode=False)
|
112
116
|
except (
|
@@ -29,6 +29,56 @@ CommandDecorator = Callable[[F], Command]
|
|
29
29
|
DecoratedCommand = Union[Command, Callable[..., Any]]
|
30
30
|
|
31
31
|
|
32
|
+
def parse_feature_flags(
|
33
|
+
enabled_features: tuple[str, ...], disabled_features: tuple[str, ...]
|
34
|
+
) -> dict[str, str]:
|
35
|
+
"""Parse feature flags from CLI arguments.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
enabled_features: Tuple of feature names to enable
|
39
|
+
disabled_features: Tuple of feature names to disable
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
Dictionary mapping feature names to "on" or "off"
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
click.BadParameter: If flag format is invalid or conflicts exist
|
46
|
+
"""
|
47
|
+
parsed = {}
|
48
|
+
|
49
|
+
# Process enabled features
|
50
|
+
for feature in enabled_features:
|
51
|
+
feature = feature.strip()
|
52
|
+
if not feature:
|
53
|
+
raise click.BadParameter("Feature name cannot be empty")
|
54
|
+
|
55
|
+
# Validate known feature flags
|
56
|
+
if feature == "ci-download-hack":
|
57
|
+
parsed[feature] = "on"
|
58
|
+
else:
|
59
|
+
raise click.BadParameter(f"Unknown feature: {feature}")
|
60
|
+
|
61
|
+
# Process disabled features
|
62
|
+
for feature in disabled_features:
|
63
|
+
feature = feature.strip()
|
64
|
+
if not feature:
|
65
|
+
raise click.BadParameter("Feature name cannot be empty")
|
66
|
+
|
67
|
+
# Check for conflicts
|
68
|
+
if feature in parsed:
|
69
|
+
raise click.BadParameter(
|
70
|
+
f"Feature '{feature}' cannot be both enabled and disabled"
|
71
|
+
)
|
72
|
+
|
73
|
+
# Validate known feature flags
|
74
|
+
if feature == "ci-download-hack":
|
75
|
+
parsed[feature] = "off"
|
76
|
+
else:
|
77
|
+
raise click.BadParameter(f"Unknown feature: {feature}")
|
78
|
+
|
79
|
+
return parsed
|
80
|
+
|
81
|
+
|
32
82
|
def debug_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
33
83
|
"""Add debug-related CLI options."""
|
34
84
|
# Initial conversion to Command if needed
|
@@ -442,12 +492,13 @@ def api_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
|
442
492
|
environment variable.""",
|
443
493
|
)(cmd)
|
444
494
|
|
495
|
+
# API timeout for OpenAI calls
|
445
496
|
cmd = click.option(
|
446
497
|
"--timeout",
|
447
498
|
type=click.FloatRange(1.0, None),
|
448
499
|
default=60.0,
|
449
500
|
show_default=True,
|
450
|
-
help="
|
501
|
+
help="Timeout in seconds for OpenAI API calls.",
|
451
502
|
)(cmd)
|
452
503
|
|
453
504
|
return cast(Command, cmd)
|
@@ -573,6 +624,31 @@ def code_interpreter_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
|
573
624
|
help="""Clean up uploaded files after execution to save storage quota.""",
|
574
625
|
)(cmd)
|
575
626
|
|
627
|
+
# Feature flags for experimental features
|
628
|
+
cmd = click.option(
|
629
|
+
"--enable-feature",
|
630
|
+
"enabled_features",
|
631
|
+
multiple=True,
|
632
|
+
metavar="<FEATURE>",
|
633
|
+
help="""🔧 [EXPERIMENTAL] Enable experimental features.
|
634
|
+
Available features:
|
635
|
+
• ci-download-hack - Enable two-pass sentinel mode for reliable Code Interpreter
|
636
|
+
file downloads with structured output. Overrides config file setting.
|
637
|
+
Example: --enable-feature ci-download-hack""",
|
638
|
+
)(cmd)
|
639
|
+
|
640
|
+
cmd = click.option(
|
641
|
+
"--disable-feature",
|
642
|
+
"disabled_features",
|
643
|
+
multiple=True,
|
644
|
+
metavar="<FEATURE>",
|
645
|
+
help="""🔧 [EXPERIMENTAL] Disable experimental features.
|
646
|
+
Available features:
|
647
|
+
• ci-download-hack - Force single-pass mode for Code Interpreter downloads.
|
648
|
+
Overrides config file setting.
|
649
|
+
Example: --disable-feature ci-download-hack""",
|
650
|
+
)(cmd)
|
651
|
+
|
576
652
|
return cast(Command, cmd)
|
577
653
|
|
578
654
|
|
@@ -685,13 +761,17 @@ def web_search_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
|
685
761
|
is_flag=True,
|
686
762
|
help="""🌐 [WEB SEARCH] Enable OpenAI web search tool for up-to-date information.
|
687
763
|
Allows the model to search the web for current events, recent updates, and real-time data.
|
688
|
-
Note: Search queries may be sent to external services via OpenAI.
|
764
|
+
Note: Search queries may be sent to external services via OpenAI.
|
765
|
+
|
766
|
+
⚠️ DEPRECATED: Use --enable-tool web-search instead. Will be removed in v0.9.0.""",
|
689
767
|
)(cmd)
|
690
768
|
|
691
769
|
cmd = click.option(
|
692
770
|
"--no-web-search",
|
693
771
|
is_flag=True,
|
694
|
-
help="""Explicitly disable web search even if enabled by default in configuration.
|
772
|
+
help="""Explicitly disable web search even if enabled by default in configuration.
|
773
|
+
|
774
|
+
⚠️ DEPRECATED: Use --disable-tool web-search instead. Will be removed in v0.9.0.""",
|
695
775
|
)(cmd)
|
696
776
|
|
697
777
|
cmd = click.option(
|
@@ -725,6 +805,35 @@ def web_search_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
|
725
805
|
return cast(Command, cmd)
|
726
806
|
|
727
807
|
|
808
|
+
def tool_toggle_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
809
|
+
"""Add universal tool toggle CLI options."""
|
810
|
+
cmd: Any = f if isinstance(f, Command) else f
|
811
|
+
|
812
|
+
cmd = click.option(
|
813
|
+
"--enable-tool",
|
814
|
+
"enabled_tools",
|
815
|
+
multiple=True,
|
816
|
+
metavar="<TOOL>",
|
817
|
+
help="""🔧 [TOOL TOGGLES] Enable a tool for this run (repeatable).
|
818
|
+
Overrides configuration file and implicit activation.
|
819
|
+
Available tools: code-interpreter, file-search, web-search, mcp
|
820
|
+
Example: --enable-tool code-interpreter --enable-tool web-search""",
|
821
|
+
)(cmd)
|
822
|
+
|
823
|
+
cmd = click.option(
|
824
|
+
"--disable-tool",
|
825
|
+
"disabled_tools",
|
826
|
+
multiple=True,
|
827
|
+
metavar="<TOOL>",
|
828
|
+
help="""🔧 [TOOL TOGGLES] Disable a tool for this run (repeatable).
|
829
|
+
Overrides configuration file and implicit activation.
|
830
|
+
Available tools: code-interpreter, file-search, web-search, mcp
|
831
|
+
Example: --disable-tool web-search --disable-tool mcp""",
|
832
|
+
)(cmd)
|
833
|
+
|
834
|
+
return cast(Command, cmd)
|
835
|
+
|
836
|
+
|
728
837
|
def debug_progress_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
729
838
|
"""Add debugging and progress CLI options."""
|
730
839
|
cmd: Any = f if isinstance(f, Command) else f
|
@@ -746,19 +855,6 @@ def debug_progress_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
|
746
855
|
"--verbose", is_flag=True, help="Enable verbose logging"
|
747
856
|
)(cmd)
|
748
857
|
|
749
|
-
cmd = click.option(
|
750
|
-
"--debug-openai-stream",
|
751
|
-
is_flag=True,
|
752
|
-
help="Debug OpenAI streaming process",
|
753
|
-
)(cmd)
|
754
|
-
|
755
|
-
cmd = click.option(
|
756
|
-
"--timeout",
|
757
|
-
type=int,
|
758
|
-
default=3600,
|
759
|
-
help="Operation timeout in seconds (default: 3600 = 1 hour)",
|
760
|
-
)(cmd)
|
761
|
-
|
762
858
|
return cast(Command, cmd)
|
763
859
|
|
764
860
|
|
@@ -777,6 +873,7 @@ def all_options(f: Union[Command, Callable[..., Any]]) -> Command:
|
|
777
873
|
cmd = code_interpreter_options(cmd)
|
778
874
|
cmd = file_search_options(cmd)
|
779
875
|
cmd = web_search_options(cmd)
|
876
|
+
cmd = tool_toggle_options(cmd)
|
780
877
|
cmd = debug_options(cmd)
|
781
878
|
cmd = debug_progress_options(cmd)
|
782
879
|
|