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.
Files changed (71) hide show
  1. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/PKG-INFO +96 -2
  2. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/README.md +91 -1
  3. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/pyproject.toml +12 -4
  4. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/cli.py +4 -0
  5. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/click_options.py +113 -16
  6. ostruct_cli-0.8.4/src/ostruct/cli/code_interpreter.py +431 -0
  7. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/run.py +56 -0
  8. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/config.py +20 -1
  9. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/errors.py +2 -30
  10. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_info.py +55 -20
  11. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_utils.py +19 -3
  12. ostruct_cli-0.8.4/src/ostruct/cli/json_extract.py +75 -0
  13. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/model_creation.py +1 -1
  14. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/runner.py +476 -195
  15. ostruct_cli-0.8.4/src/ostruct/cli/sentinel.py +29 -0
  16. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_optimizer.py +11 -7
  17. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_processor.py +243 -115
  18. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_rendering.py +41 -1
  19. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_validation.py +41 -3
  20. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/types.py +14 -1
  21. ostruct_cli-0.8.2/src/ostruct/cli/code_interpreter.py +0 -238
  22. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/LICENSE +0 -0
  23. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/__init__.py +0 -0
  24. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/__init__.py +0 -0
  25. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/base_errors.py +0 -0
  26. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/cache_manager.py +0 -0
  27. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/__init__.py +0 -0
  28. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/list_models.py +0 -0
  29. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/quick_ref.py +0 -0
  30. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/commands/update_registry.py +0 -0
  31. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/cost_estimation.py +0 -0
  32. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/exit_codes.py +0 -0
  33. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/explicit_file_processor.py +0 -0
  34. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/field_utils.py +0 -0
  35. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_list.py +0 -0
  36. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/file_search.py +0 -0
  37. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/mcp_integration.py +0 -0
  38. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/model_validation.py +0 -0
  39. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/path_utils.py +0 -0
  40. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/progress.py +0 -0
  41. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/progress_reporting.py +0 -0
  42. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/registry_updates.py +0 -0
  43. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/schema_utils.py +0 -0
  44. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/schema_validation.py +0 -0
  45. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/__init__.py +0 -0
  46. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/allowed_checker.py +0 -0
  47. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/base.py +0 -0
  48. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/case_manager.py +0 -0
  49. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/errors.py +0 -0
  50. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/normalization.py +0 -0
  51. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/safe_joiner.py +0 -0
  52. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/security_manager.py +0 -0
  53. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/symlink_resolver.py +0 -0
  54. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/types.py +0 -0
  55. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/security/windows_paths.py +0 -0
  56. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/serialization.py +0 -0
  57. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/services.py +0 -0
  58. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_debug.py +0 -0
  59. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_debug_help.py +0 -0
  60. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_env.py +0 -0
  61. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_extensions.py +0 -0
  62. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_filters.py +0 -0
  63. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_io.py +0 -0
  64. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_schema.py +0 -0
  65. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/template_utils.py +0 -0
  66. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/token_utils.py +0 -0
  67. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/token_validation.py +0 -0
  68. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/unattended_operation.py +0 -0
  69. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/utils.py +0 -0
  70. {ostruct_cli-0.8.2 → ostruct_cli-0.8.4}/src/ostruct/cli/validators.py +0 -0
  71. {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.2
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.2"
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 should use real API key",
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
- "no_pyfakefs: mark test to disable pyfakefs",
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="API timeout in seconds.",
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