batchor 0.0.1__tar.gz → 0.0.3__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 (110) hide show
  1. {batchor-0.0.1 → batchor-0.0.3}/.github/workflows/ci.yml +1 -1
  2. {batchor-0.0.1 → batchor-0.0.3}/PKG-INFO +15 -48
  3. {batchor-0.0.1 → batchor-0.0.3}/README.md +13 -47
  4. {batchor-0.0.1 → batchor-0.0.3}/docs/design_docs/ARCHITECTURE.md +40 -9
  5. {batchor-0.0.1 → batchor-0.0.3}/docs/design_docs/OPENAI_BATCHING.md +13 -0
  6. {batchor-0.0.1 → batchor-0.0.3}/docs/design_docs/STORAGE_AND_RUNS.md +27 -2
  7. {batchor-0.0.1 → batchor-0.0.3}/docs/design_docs/STORAGE_MIGRATIONS.md +2 -1
  8. {batchor-0.0.1 → batchor-0.0.3}/docs/getting-started/python-api.md +2 -0
  9. {batchor-0.0.1 → batchor-0.0.3}/docs/smoke-test.md +15 -2
  10. {batchor-0.0.1 → batchor-0.0.3}/pyproject.toml +3 -1
  11. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/core/exceptions.py +8 -2
  12. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/core/models.py +7 -0
  13. batchor-0.0.3/src/batchor/runtime/__init__.py +1 -0
  14. batchor-0.0.3/src/batchor/runtime/artifacts.py +177 -0
  15. batchor-0.0.3/src/batchor/runtime/context.py +164 -0
  16. batchor-0.0.3/src/batchor/runtime/ingestion.py +380 -0
  17. batchor-0.0.3/src/batchor/runtime/polling.py +551 -0
  18. batchor-0.0.3/src/batchor/runtime/results.py +119 -0
  19. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/runtime/retry.py +63 -0
  20. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/runtime/run_handle.py +113 -46
  21. batchor-0.0.3/src/batchor/runtime/runner.py +597 -0
  22. batchor-0.0.3/src/batchor/runtime/submission.py +557 -0
  23. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/runtime/validation.py +3 -1
  24. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/sources/base.py +10 -0
  25. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/sources/composite.py +4 -0
  26. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/sources/files.py +16 -0
  27. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/memory.py +58 -0
  28. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/postgres_store.py +18 -4
  29. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite_lifecycle.py +98 -1
  30. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite_protocol.py +1 -0
  31. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite_queries.py +3 -0
  32. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite_results.py +1 -0
  33. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite_schema.py +2 -1
  34. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/state_models.py +26 -0
  35. {batchor-0.0.1 → batchor-0.0.3}/tests/integration/test_batchor_runner.py +309 -105
  36. batchor-0.0.3/tests/unit/test_batchor_results.py +130 -0
  37. batchor-0.0.3/tests/unit/test_batchor_retry.py +143 -0
  38. batchor-0.0.3/tests/unit/test_batchor_runtime_ingestion.py +935 -0
  39. batchor-0.0.3/tests/unit/test_batchor_runtime_polling.py +416 -0
  40. batchor-0.0.3/tests/unit/test_batchor_runtime_submission.py +333 -0
  41. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_batchor_sources.py +282 -0
  42. batchor-0.0.3/tests/unit/test_batchor_storage.py +55 -0
  43. batchor-0.0.3/tests/unit/test_batchor_storage_contracts.py +438 -0
  44. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_batchor_tokens.py +86 -0
  45. batchor-0.0.3/tests/unit/test_batchor_validation.py +379 -0
  46. {batchor-0.0.1 → batchor-0.0.3}/uv.lock +1 -1
  47. batchor-0.0.1/src/batchor/runtime/__init__.py +0 -1
  48. batchor-0.0.1/src/batchor/runtime/runner.py +0 -1015
  49. batchor-0.0.1/src/batchor/runtime/runner_execution.py +0 -753
  50. batchor-0.0.1/tests/unit/test_batchor_retry.py +0 -29
  51. batchor-0.0.1/tests/unit/test_batchor_storage.py +0 -22
  52. batchor-0.0.1/tests/unit/test_batchor_storage_contracts.py +0 -201
  53. batchor-0.0.1/tests/unit/test_batchor_validation.py +0 -174
  54. {batchor-0.0.1 → batchor-0.0.3}/.agents/plugins/marketplace.json +0 -0
  55. {batchor-0.0.1 → batchor-0.0.3}/.agents/skills/batchor-dev/SKILL.md +0 -0
  56. {batchor-0.0.1 → batchor-0.0.3}/.agents/skills/batchor-dev/agents/openai.yaml +0 -0
  57. {batchor-0.0.1 → batchor-0.0.3}/.github/workflows/docs.yml +0 -0
  58. {batchor-0.0.1 → batchor-0.0.3}/.github/workflows/publish.yml +0 -0
  59. {batchor-0.0.1 → batchor-0.0.3}/.gitignore +0 -0
  60. {batchor-0.0.1 → batchor-0.0.3}/.pre-commit-config.yaml +0 -0
  61. {batchor-0.0.1 → batchor-0.0.3}/AGENTS.md +0 -0
  62. {batchor-0.0.1 → batchor-0.0.3}/CONTRIBUTING.md +0 -0
  63. {batchor-0.0.1 → batchor-0.0.3}/LICENSE +0 -0
  64. {batchor-0.0.1 → batchor-0.0.3}/SUPPORT.md +0 -0
  65. {batchor-0.0.1 → batchor-0.0.3}/VERSIONING.md +0 -0
  66. {batchor-0.0.1 → batchor-0.0.3}/docs/assets/stylesheets/extra.css +0 -0
  67. {batchor-0.0.1 → batchor-0.0.3}/docs/design_docs/BOUNDARY_AND_PHILOSOPHY.md +0 -0
  68. {batchor-0.0.1 → batchor-0.0.3}/docs/design_docs/ROADMAP.md +0 -0
  69. {batchor-0.0.1 → batchor-0.0.3}/docs/doc-map.md +0 -0
  70. {batchor-0.0.1 → batchor-0.0.3}/docs/getting-started/cli.md +0 -0
  71. {batchor-0.0.1 → batchor-0.0.3}/docs/getting-started/installation.md +0 -0
  72. {batchor-0.0.1 → batchor-0.0.3}/docs/getting-started/use-cases.md +0 -0
  73. {batchor-0.0.1 → batchor-0.0.3}/docs/index.md +0 -0
  74. {batchor-0.0.1 → batchor-0.0.3}/docs/policies/contributing.md +0 -0
  75. {batchor-0.0.1 → batchor-0.0.3}/docs/policies/support.md +0 -0
  76. {batchor-0.0.1 → batchor-0.0.3}/docs/policies/versioning.md +0 -0
  77. {batchor-0.0.1 → batchor-0.0.3}/docs/reference/api.md +0 -0
  78. {batchor-0.0.1 → batchor-0.0.3}/mkdocs.yml +0 -0
  79. {batchor-0.0.1 → batchor-0.0.3}/plugins/batchor-agent-tools/.codex-plugin/plugin.json +0 -0
  80. {batchor-0.0.1 → batchor-0.0.3}/plugins/batchor-agent-tools/.mcp.json +0 -0
  81. {batchor-0.0.1 → batchor-0.0.3}/plugins/batchor-agent-tools/scripts/batchor_repo_mcp.py +0 -0
  82. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/__init__.py +0 -0
  83. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/artifacts/__init__.py +0 -0
  84. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/artifacts/base.py +0 -0
  85. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/artifacts/local.py +0 -0
  86. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/cli.py +0 -0
  87. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/core/__init__.py +0 -0
  88. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/core/enums.py +0 -0
  89. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/core/types.py +0 -0
  90. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/providers/__init__.py +0 -0
  91. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/providers/base.py +0 -0
  92. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/providers/openai.py +0 -0
  93. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/providers/registry.py +0 -0
  94. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/py.typed +0 -0
  95. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/runtime/tokens.py +0 -0
  96. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/sources/__init__.py +0 -0
  97. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/__init__.py +0 -0
  98. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/postgres.py +0 -0
  99. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/registry.py +0 -0
  100. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite.py +0 -0
  101. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite_codec.py +0 -0
  102. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/sqlite_store.py +0 -0
  103. {batchor-0.0.1 → batchor-0.0.3}/src/batchor/storage/state.py +0 -0
  104. {batchor-0.0.1 → batchor-0.0.3}/tests/integration/test_batchor_live_openai.py +0 -0
  105. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_agent_tooling.py +0 -0
  106. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_batchor_architecture.py +0 -0
  107. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_batchor_artifacts.py +0 -0
  108. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_batchor_cli.py +0 -0
  109. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_batchor_openai_provider.py +0 -0
  110. {batchor-0.0.1 → batchor-0.0.3}/tests/unit/test_batchor_sqlite_storage_flow.py +0 -0
@@ -46,7 +46,7 @@ jobs:
46
46
  strategy:
47
47
  fail-fast: false
48
48
  matrix:
49
- python-version: ["3.12", "3.13"]
49
+ python-version: ["3.12", "3.13", "3.14"]
50
50
 
51
51
  steps:
52
52
  - uses: actions/checkout@v4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchor
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: Structured-first OpenAI Batch runner with typed Pydantic results.
5
5
  Project-URL: Documentation, https://github.com/AnsonDev42/batchor#readme
6
6
  Project-URL: Repository, https://github.com/AnsonDev42/batchor
@@ -88,6 +88,7 @@ Classifier: Operating System :: OS Independent
88
88
  Classifier: Programming Language :: Python :: 3
89
89
  Classifier: Programming Language :: Python :: 3.12
90
90
  Classifier: Programming Language :: Python :: 3.13
91
+ Classifier: Programming Language :: Python :: 3.14
91
92
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
92
93
  Requires-Python: <4,>=3.12
93
94
  Requires-Dist: openai>=2.7.1
@@ -102,12 +103,15 @@ Description-Content-Type: text/markdown
102
103
 
103
104
  # batchor
104
105
 
106
+ `batchor` grew out of a recurring problem in academic research: running large datasets through LLMs in batch — reliably, reproducibly, and without reinventing the same glue code across every project. The patterns that kept emerging (durable state, typed results, safe resume after failure) were extracted into this library so they do not have to be rebuilt each time.
107
+
105
108
  `batchor` is a durable OpenAI Batch runner for Python teams that want:
106
109
 
107
110
  - typed Pydantic results
108
111
  - resumable durable runs
109
112
  - replayable request artifacts
110
113
  - deterministic source checkpoints
114
+ - provider-side enqueue limit controls to stay within token budgets
111
115
  - library-first run controls
112
116
  - a small operator CLI for CSV and JSONL jobs
113
117
 
@@ -154,6 +158,7 @@ Important constraints:
154
158
  - structured-output rehydration requires an importable module-level Pydantic model
155
159
  - raw output artifacts are retained by default and must be exported before raw pruning
156
160
  - pause/resume/cancel and incremental terminal-result APIs are library-first today
161
+ - OpenAI control-plane or batch-level 429 quota/billing exhaustion auto-pauses the run instead of burning attempts across the remaining backlog
157
162
 
158
163
  ## Mental model
159
164
 
@@ -173,6 +178,7 @@ Durability is split on purpose:
173
178
  - the artifact store keeps replayable request JSONL and downloaded raw batch payloads
174
179
 
175
180
  That split is what allows retries and fresh-process resume without keeping every request inline in the control-plane store.
181
+ On resume, existing active provider batches are polled before `batchor` materializes or submits new local work, so completed batches and retry backoff are reconciled first. Deterministic sources also use stored checkpoint completion metadata to avoid opening a fully materialized Parquet or composite source just to discover there are no rows left.
176
182
 
177
183
  ## Architecture
178
184
 
@@ -256,6 +262,7 @@ Supported Python versions:
256
262
 
257
263
  - `3.12`
258
264
  - `3.13`
265
+ - `3.14`
259
266
 
260
267
  ## Authentication
261
268
 
@@ -334,6 +341,8 @@ run.wait()
334
341
  print(run.results()[0].output)
335
342
  ```
336
343
 
344
+ While waiting, `batchor` polls active batches before submitting more work. If a refresh consumes a completed batch, records terminal item state, or submits another provider batch, `Run.wait()` immediately continues draining instead of sleeping for the next poll interval.
345
+
337
346
  Structured-output models are validated up front against the OpenAI strict-schema subset used by `batchor`.
338
347
 
339
348
  - root schema must be an object
@@ -479,6 +488,8 @@ run.cancel()
479
488
 
480
489
  Run control and incremental terminal-result APIs are Python-first in this release. The CLI does not yet expose `pause`, `resume`, `cancel`, or incremental terminal-result export commands.
481
490
 
491
+ OpenAI insufficient-quota failures during upload/create/polling, or as a batch-level terminal error, are treated as an automatic pause with `summary().control_reason == "openai_insufficient_quota"`. Row-level insufficient-quota records inside a completed batch output stay item-scoped: those rows become retryable without consuming attempts, emit `openai_insufficient_quota` failure telemetry, and use retry backoff before resubmission. Auto-pause does not replace an in-progress cancellation.
492
+
482
493
  ## CLI quickstart
483
494
 
484
495
  The CLI is intentionally narrower than the Python API:
@@ -539,12 +550,13 @@ def observer(event: RunEvent) -> None:
539
550
  runner = BatchRunner(observer=observer)
540
551
  ```
541
552
 
542
- Current events include run creation/resume, item ingestion, batch submission/polling/completion, item completion/failure, and artifact export/prune.
553
+ Current events include run creation/resume, automatic quota pause, item ingestion, batch submission/polling/completion, item completion/failure, and artifact export/prune.
543
554
 
544
555
  ## Storage notes
545
556
 
546
557
  - SQLite remains the default durable backend.
547
- - `PostgresStorage` is available for shared control-plane state, but the CLI remains SQLite-only today.
558
+ - `PostgresStorage` is available for shared control-plane state, but the CLI remains SQLite-only today. Plain `postgresql://` and `postgres://` DSNs are normalized to the package's psycopg v3 driver URL.
559
+ - Item `attempt_count` means consumed provider attempts: successful submitted completions increment it, counted item-level failures increment it, and local or batch-level retry/reset paths that did not consume an item attempt leave it unchanged.
548
560
  - Durable artifacts now go through an `ArtifactStore` seam. The built-in implementation is `LocalArtifactStore`, intended for local disk or a shared mounted volume.
549
561
  - Fresh-process resume requeues any locally claimed but not yet submitted items before continuing, so a process crash after request-artifact persistence does not strand work in `queued_local`.
550
562
 
@@ -591,51 +603,6 @@ The default pytest configuration enforces an `85%` coverage floor.
591
603
 
592
604
  GitHub Actions pull requests run:
593
605
 
594
- - `test (3.12)` and `test (3.13)`: `uv run ty check src` plus `uv run pytest -q`
595
- - `docs`: `uv run mkdocs build --strict`
596
- - `build (3.13)`: `uv build`
597
- - `postgres-contract`: `uv run pytest tests/unit/test_batchor_storage_contracts.py --no-cov -q` with an ephemeral PostgreSQL service
598
-
599
- The live OpenAI smoke remains manual-only and is not part of required CI.
600
-
601
- Manual live OpenAI smoke:
602
-
603
- ```bash
604
- export OPENAI_API_KEY=sk-...
605
- export BATCHOR_RUN_LIVE_TESTS=1
606
- uv run pytest tests/integration/test_batchor_live_openai.py --no-cov -q
607
- ```
608
-
609
- If `BATCHOR_LIVE_OPENAI_MODEL` is unset, the harness defaults to `gpt-5-nano`.
610
- If `BATCHOR_LIVE_OPENAI_REASONING_EFFORT` is unset, no reasoning field is sent.
611
- The live smoke also requires an OpenAI account with Batch API access and available billing quota.
612
-
613
- ## Documentation guide
614
-
615
- - `docs/design_docs/ARCHITECTURE.md`: canonical runtime diagrams and module boundaries
616
- - `docs/design_docs/STORAGE_AND_RUNS.md`: durable run, resume, and artifact lifecycle semantics
617
- - `docs/getting-started/use-cases.md`: practical single-file, multi-file, and pipeline examples
618
- - `docs/getting-started/python-api.md`: Python-first workflows
619
- - `docs/getting-started/cli.md`: operator CLI workflows
620
- - `docs/reference/api.md`: generated API surface
621
- - `docs/design_docs/ARCHITECTURE.md`: package boundaries
622
- - `docs/design_docs/OPENAI_BATCHING.md`: OpenAI-specific behavior
623
- - `docs/design_docs/STORAGE_AND_RUNS.md`: durability, rehydration, and artifacts
624
- - `docs/smoke-test.md`: minimum validation bar
625
-
626
- ## Scope
627
-
628
- Today `batchor` is intentionally narrow:
629
-
630
- - one built-in provider: OpenAI Batch
631
- - one default durable backend: SQLite
632
- - one opt-in durable backend: Postgres
633
- - one ephemeral backend: in-memory storage
634
- - structured output is Python API-first
635
- - CLI job creation is file-backed and supports both text and structured-output jobs
636
-
637
- Anything outside that scope should be treated as out of scope or `TBD`, not implied support.
638
-
639
606
  ## License
640
607
 
641
608
  This project is licensed under PolyForm Noncommercial 1.0.0. See [LICENSE](LICENSE).
@@ -1,11 +1,14 @@
1
1
  # batchor
2
2
 
3
+ `batchor` grew out of a recurring problem in academic research: running large datasets through LLMs in batch — reliably, reproducibly, and without reinventing the same glue code across every project. The patterns that kept emerging (durable state, typed results, safe resume after failure) were extracted into this library so they do not have to be rebuilt each time.
4
+
3
5
  `batchor` is a durable OpenAI Batch runner for Python teams that want:
4
6
 
5
7
  - typed Pydantic results
6
8
  - resumable durable runs
7
9
  - replayable request artifacts
8
10
  - deterministic source checkpoints
11
+ - provider-side enqueue limit controls to stay within token budgets
9
12
  - library-first run controls
10
13
  - a small operator CLI for CSV and JSONL jobs
11
14
 
@@ -52,6 +55,7 @@ Important constraints:
52
55
  - structured-output rehydration requires an importable module-level Pydantic model
53
56
  - raw output artifacts are retained by default and must be exported before raw pruning
54
57
  - pause/resume/cancel and incremental terminal-result APIs are library-first today
58
+ - OpenAI control-plane or batch-level 429 quota/billing exhaustion auto-pauses the run instead of burning attempts across the remaining backlog
55
59
 
56
60
  ## Mental model
57
61
 
@@ -71,6 +75,7 @@ Durability is split on purpose:
71
75
  - the artifact store keeps replayable request JSONL and downloaded raw batch payloads
72
76
 
73
77
  That split is what allows retries and fresh-process resume without keeping every request inline in the control-plane store.
78
+ On resume, existing active provider batches are polled before `batchor` materializes or submits new local work, so completed batches and retry backoff are reconciled first. Deterministic sources also use stored checkpoint completion metadata to avoid opening a fully materialized Parquet or composite source just to discover there are no rows left.
74
79
 
75
80
  ## Architecture
76
81
 
@@ -154,6 +159,7 @@ Supported Python versions:
154
159
 
155
160
  - `3.12`
156
161
  - `3.13`
162
+ - `3.14`
157
163
 
158
164
  ## Authentication
159
165
 
@@ -232,6 +238,8 @@ run.wait()
232
238
  print(run.results()[0].output)
233
239
  ```
234
240
 
241
+ While waiting, `batchor` polls active batches before submitting more work. If a refresh consumes a completed batch, records terminal item state, or submits another provider batch, `Run.wait()` immediately continues draining instead of sleeping for the next poll interval.
242
+
235
243
  Structured-output models are validated up front against the OpenAI strict-schema subset used by `batchor`.
236
244
 
237
245
  - root schema must be an object
@@ -377,6 +385,8 @@ run.cancel()
377
385
 
378
386
  Run control and incremental terminal-result APIs are Python-first in this release. The CLI does not yet expose `pause`, `resume`, `cancel`, or incremental terminal-result export commands.
379
387
 
388
+ OpenAI insufficient-quota failures during upload/create/polling, or as a batch-level terminal error, are treated as an automatic pause with `summary().control_reason == "openai_insufficient_quota"`. Row-level insufficient-quota records inside a completed batch output stay item-scoped: those rows become retryable without consuming attempts, emit `openai_insufficient_quota` failure telemetry, and use retry backoff before resubmission. Auto-pause does not replace an in-progress cancellation.
389
+
380
390
  ## CLI quickstart
381
391
 
382
392
  The CLI is intentionally narrower than the Python API:
@@ -437,12 +447,13 @@ def observer(event: RunEvent) -> None:
437
447
  runner = BatchRunner(observer=observer)
438
448
  ```
439
449
 
440
- Current events include run creation/resume, item ingestion, batch submission/polling/completion, item completion/failure, and artifact export/prune.
450
+ Current events include run creation/resume, automatic quota pause, item ingestion, batch submission/polling/completion, item completion/failure, and artifact export/prune.
441
451
 
442
452
  ## Storage notes
443
453
 
444
454
  - SQLite remains the default durable backend.
445
- - `PostgresStorage` is available for shared control-plane state, but the CLI remains SQLite-only today.
455
+ - `PostgresStorage` is available for shared control-plane state, but the CLI remains SQLite-only today. Plain `postgresql://` and `postgres://` DSNs are normalized to the package's psycopg v3 driver URL.
456
+ - Item `attempt_count` means consumed provider attempts: successful submitted completions increment it, counted item-level failures increment it, and local or batch-level retry/reset paths that did not consume an item attempt leave it unchanged.
446
457
  - Durable artifacts now go through an `ArtifactStore` seam. The built-in implementation is `LocalArtifactStore`, intended for local disk or a shared mounted volume.
447
458
  - Fresh-process resume requeues any locally claimed but not yet submitted items before continuing, so a process crash after request-artifact persistence does not strand work in `queued_local`.
448
459
 
@@ -489,51 +500,6 @@ The default pytest configuration enforces an `85%` coverage floor.
489
500
 
490
501
  GitHub Actions pull requests run:
491
502
 
492
- - `test (3.12)` and `test (3.13)`: `uv run ty check src` plus `uv run pytest -q`
493
- - `docs`: `uv run mkdocs build --strict`
494
- - `build (3.13)`: `uv build`
495
- - `postgres-contract`: `uv run pytest tests/unit/test_batchor_storage_contracts.py --no-cov -q` with an ephemeral PostgreSQL service
496
-
497
- The live OpenAI smoke remains manual-only and is not part of required CI.
498
-
499
- Manual live OpenAI smoke:
500
-
501
- ```bash
502
- export OPENAI_API_KEY=sk-...
503
- export BATCHOR_RUN_LIVE_TESTS=1
504
- uv run pytest tests/integration/test_batchor_live_openai.py --no-cov -q
505
- ```
506
-
507
- If `BATCHOR_LIVE_OPENAI_MODEL` is unset, the harness defaults to `gpt-5-nano`.
508
- If `BATCHOR_LIVE_OPENAI_REASONING_EFFORT` is unset, no reasoning field is sent.
509
- The live smoke also requires an OpenAI account with Batch API access and available billing quota.
510
-
511
- ## Documentation guide
512
-
513
- - `docs/design_docs/ARCHITECTURE.md`: canonical runtime diagrams and module boundaries
514
- - `docs/design_docs/STORAGE_AND_RUNS.md`: durable run, resume, and artifact lifecycle semantics
515
- - `docs/getting-started/use-cases.md`: practical single-file, multi-file, and pipeline examples
516
- - `docs/getting-started/python-api.md`: Python-first workflows
517
- - `docs/getting-started/cli.md`: operator CLI workflows
518
- - `docs/reference/api.md`: generated API surface
519
- - `docs/design_docs/ARCHITECTURE.md`: package boundaries
520
- - `docs/design_docs/OPENAI_BATCHING.md`: OpenAI-specific behavior
521
- - `docs/design_docs/STORAGE_AND_RUNS.md`: durability, rehydration, and artifacts
522
- - `docs/smoke-test.md`: minimum validation bar
523
-
524
- ## Scope
525
-
526
- Today `batchor` is intentionally narrow:
527
-
528
- - one built-in provider: OpenAI Batch
529
- - one default durable backend: SQLite
530
- - one opt-in durable backend: Postgres
531
- - one ephemeral backend: in-memory storage
532
- - structured output is Python API-first
533
- - CLI job creation is file-backed and supports both text and structured-output jobs
534
-
535
- Anything outside that scope should be treated as out of scope or `TBD`, not implied support.
536
-
537
503
  ## License
538
504
 
539
505
  This project is licensed under PolyForm Noncommercial 1.0.0. See [LICENSE](LICENSE).
@@ -133,13 +133,16 @@ Internally that expands to:
133
133
 
134
134
  1. Resolve provider and storage implementations.
135
135
  2. Persist run config and ingest items into durable state.
136
- 3. Claim a bounded submission window from pending items.
137
- 4. Build or replay request JSONL rows.
138
- 5. Persist request artifacts before upload.
139
- 6. Submit one or more OpenAI batch files.
140
- 7. Poll active batches.
141
- 8. Download output/error files.
142
- 9. Parse terminal item results back into the state store.
136
+ 3. On resume, poll any already-active provider batches before ingesting or submitting new work.
137
+ 4. Claim a bounded submission window from pending items.
138
+ 5. Build or replay request JSONL rows.
139
+ 6. Persist request artifacts before upload.
140
+ 7. Submit one or more OpenAI batch files.
141
+ 8. Poll active batches.
142
+ 9. Download output/error files.
143
+ 10. Parse terminal item results back into the state store.
144
+
145
+ When a caller uses `Run.wait()`, the runtime repeats that poll-and-submit pass until the run is terminal. A pass that changes durable work state, such as consuming completed batches or submitting more items, immediately triggers the next pass rather than sleeping for the configured poll interval.
143
146
 
144
147
  ## Execution Sequence
145
148
 
@@ -175,6 +178,9 @@ sequenceDiagram
175
178
  Provider-->>BatchRunner: successes + errors
176
179
  BatchRunner->>StateStore: mark_items_completed(successes)
177
180
  BatchRunner->>StateStore: mark_items_failed(errors)
181
+ opt row-level insufficient quota
182
+ BatchRunner->>StateStore: record_batch_retry_failure(row_insufficient_quota)
183
+ end
178
184
  else status in {failed, cancelled, expired}
179
185
  BatchRunner->>Provider: download_file_content(output_file_id, error_file_id)
180
186
  Provider-->>BatchRunner: output/error JSONL
@@ -182,6 +188,11 @@ sequenceDiagram
182
188
  BatchRunner->>StateStore: record_batch_retry_failure()
183
189
  BatchRunner->>StateStore: reset_batch_items_to_pending()
184
190
  end
191
+
192
+ opt control-plane or batch-level OpenAI insufficient quota detected
193
+ BatchRunner->>StateStore: set_run_control_state(paused, control_reason)
194
+ BatchRunner-->>User: RunPausedError / run_auto_paused event
195
+ end
185
196
  end
186
197
 
187
198
  BatchRunner->>StateStore: claim_items_for_submission(limit)
@@ -217,7 +228,7 @@ stateDiagram-v2
217
228
  QUEUED_LOCAL --> FAILED_PERMANENT : rejected pre-submission (e.g. token budget exceeded, max attempts)
218
229
  QUEUED_LOCAL --> SUBMITTED : batch created and registered with provider
219
230
 
220
- SUBMITTED --> COMPLETED : batch completed, result parsed OK
231
+ SUBMITTED --> COMPLETED : batch completed, result parsed OK — attempt consumed
221
232
  SUBMITTED --> FAILED_RETRYABLE : batch error / item error — attempt count below max
222
233
  SUBMITTED --> FAILED_PERMANENT : batch error / item error — attempt count reached max
223
234
 
@@ -243,7 +254,7 @@ stateDiagram-v2
243
254
 
244
255
  state "Control State" as cs {
245
256
  [*] --> RUNNING2 : run created
246
- RUNNING2 --> PAUSED : pause_run() called
257
+ RUNNING2 --> PAUSED : pause_run() or quota auto-pause
247
258
  PAUSED --> RUNNING2 : resume_run() called
248
259
  RUNNING2 --> CANCEL_REQUESTED : cancel_run() called
249
260
 
@@ -258,6 +269,15 @@ stateDiagram-v2
258
269
  Detailed storage, resume, and artifact-retention semantics live in
259
270
  [`STORAGE_AND_RUNS.md`](STORAGE_AND_RUNS.md).
260
271
 
272
+ Paused runs may include a durable `control_reason`. Manual pauses use
273
+ `"manual"`; control-plane or batch-level OpenAI quota/billing exhaustion uses
274
+ `"openai_insufficient_quota"` and preserves retryable work for later resume.
275
+ Row-level quota records inside completed batch output remain item-level
276
+ retryable failures and use retry backoff without pausing the run.
277
+ Quota auto-pause never replaces `cancel_requested`, and non-checkpointed
278
+ finite iterables continue materializing after auto-pause so unpersisted input
279
+ rows are not lost.
280
+
261
281
  ## Module boundaries
262
282
 
263
283
  ### `core/`
@@ -302,6 +322,8 @@ Owns execution behavior:
302
322
  - `Run`
303
323
  - Typer CLI entrypoint for operator workflows
304
324
  - persisted run control state with `pause`, `resume`, and drain-style `cancel`
325
+ - automatic OpenAI control-plane/batch-level insufficient-quota pause with durable `control_reason`
326
+ - item-level insufficient-quota retries with backoff and no attempt consumption
305
327
  - optional observer callback for provider lifecycle events
306
328
  - token estimation and request chunking
307
329
  - bounded pending-item claim windows before submission
@@ -319,6 +341,14 @@ Owns execution behavior:
319
341
 
320
342
  This is where the durable lifecycle lives. It bridges the domain models, providers, storage, and artifact store.
321
343
 
344
+ The public facade is still `BatchRunner` plus `Run`, but the current internal layering under `runtime/` is intentionally split so execution concerns do not collapse back into one file:
345
+
346
+ - `context.py`: persisted config building, resume config comparison, output-model resolution, and provider-backed `RunContext` creation
347
+ - `ingestion.py`: checkpoint-aware item materialization, ingest checkpoint updates, and resume-aware ingestion flow
348
+ - `submission.py`: pending-item claiming, request replay/build, token-budget gating, request artifact persistence, and batch submission
349
+ - `polling.py`: refresh orchestration, active-batch polling, terminal batch consumption, and batch-failure reset/backoff handling
350
+ - `artifacts.py` and `results.py`: request replay helpers, raw artifact writes/deletes, public result mapping, and JSONL result serialization
351
+
322
352
  ### `sources/`
323
353
 
324
354
  Owns streaming input adapters:
@@ -347,6 +377,7 @@ The storage layer persists:
347
377
 
348
378
  - run config
349
379
  - run control state
380
+ - run control reason
350
381
  - item state and attempts
351
382
  - active batch metadata
352
383
  - ingest checkpoints
@@ -55,6 +55,7 @@ Before request artifacts exist, built-in deterministic sources can resume ingest
55
55
  That currently includes CSV, JSONL, Parquet, and `CompositeItemSource` for ordered composition of checkpointed sources.
56
56
  When `CompositeItemSource` is used, child-source row IDs are auto-namespaced into run-unique `item_id` values, while the original row ID is preserved in lineage metadata.
57
57
  Custom non-file sources must implement a durable checkpoint contract explicitly; arbitrary iterables and live DB cursors are still `TBD`.
58
+ Before resumed ingestion continues, active OpenAI batches are polled once. If that poll consumes terminal batches or records enqueue/backoff failures, those state changes are applied before any new prompt rendering, request replay, or submission happens.
58
59
 
59
60
  ## Raw output retention
60
61
 
@@ -99,6 +100,7 @@ For structured outputs:
99
100
  - the result is validated through the declared Pydantic model
100
101
 
101
102
  Validation failures consume item attempts because they represent item-level response failure, not a transient provider control-plane problem.
103
+ Successful submitted responses also consume one item attempt. A successful first provider response is exported with `attempt_count == 1`; a counted retryable item failure followed by success is exported with the total consumed attempts.
102
104
 
103
105
  ## Token estimation
104
106
 
@@ -125,6 +127,7 @@ From those settings, `batchor` derives:
125
127
  - an effective per-batch token limit
126
128
 
127
129
  Submission is constrained by both token budget and generic chunking rules such as max request count and max request file size.
130
+ During `Run.wait()`, each drain cycle polls active batches before submitting new work, so capacity freed by completed batches is visible before the next submit attempt. If that cycle makes durable progress, the wait loop immediately runs another cycle instead of sleeping.
128
131
 
129
132
  ## Batch splitting
130
133
 
@@ -137,6 +140,7 @@ Splitting considers:
137
140
  - estimated request tokens
138
141
 
139
142
  Submission claims only a bounded pending-item window per refresh. This is intentional: large backlogs should not pay full prompt-build and token-estimation cost long before provider capacity exists to send them.
143
+ The default claim window is still large enough to prepare multiple provider batches per cycle, so runs with high remote enqueue headroom can keep filling capacity without waiting for one local batch at a time.
140
144
 
141
145
  If a single request exceeds the allowed OpenAI token limit by itself, that item is marked as a permanent item failure instead of aborting the whole run.
142
146
 
@@ -150,16 +154,23 @@ Control-plane failures:
150
154
  - retryable control-plane failures do not consume item attempts
151
155
  - batch submit failures can trigger batch-level backoff
152
156
  - transient poll failures do not stall unrelated submissions when other capacity remains
157
+ - resume honors existing batch backoff before materializing more source rows
158
+ - OpenAI 429 insufficient-quota or billing exhaustion during upload/create/polling auto-pauses the run with `control_reason="openai_insufficient_quota"` instead of continuing through the backlog
159
+ - batch-level terminal insufficient-quota failures also auto-pause after submitted rows are reset to pending
153
160
 
154
161
  Item-level failures:
155
162
 
163
+ - successful submitted responses consume attempts
156
164
  - structured-output parse failures consume attempts
157
165
  - validation failures consume attempts
158
166
  - oversized requests become permanent item failures
167
+ - item-level insufficient-quota records inside completed batch output are retryable without consuming attempts
168
+ - row-level insufficient-quota records emit `openai_insufficient_quota` failure telemetry and record retry backoff before resubmission instead of pausing the whole run
159
169
 
160
170
  Cleanup behavior:
161
171
 
162
172
  - if upload succeeds but batch creation fails, `batchor` makes a best-effort attempt to delete the uploaded OpenAI input file
173
+ - if upload or batch creation fails because OpenAI reports insufficient quota, local queued items are released back to pending before the run is paused
163
174
  - if a process dies after local artifact persistence but before durable batch registration, fresh-process resume requeues those items and resubmits from persisted request artifacts
164
175
 
165
176
  ## Run control
@@ -169,6 +180,8 @@ Run control is local control-plane state, not a separate OpenAI provider feature
169
180
  - `pause` stops new ingestion, new submission, and provider polling
170
181
  - `resume` restarts those local activities
171
182
  - `cancel` stops new ingestion/submission, continues polling already-submitted batches, and then marks any remaining local non-terminal items as `run_cancelled`
183
+ - automatic quota pause uses the same `paused` control state as manual pause for control-plane and batch-level quota failures, but records `control_reason="openai_insufficient_quota"` so operators can tell why `wait()` exited
184
+ - automatic quota pause is ignored once `cancel_requested` is set, because cancellation is not reversible
172
185
 
173
186
  Provider-side remote batch cancellation is not implemented in v1.
174
187
 
@@ -18,6 +18,7 @@ The public handle exposes:
18
18
  - `run_id`
19
19
  - cached `status`
20
20
  - cached `control_state`
21
+ - cached `control_reason`
21
22
  - `is_finished`
22
23
  - `refresh()`
23
24
  - `wait()`
@@ -36,7 +37,7 @@ Important semantics:
36
37
 
37
38
  - `status` is cached from the last summary read or refresh
38
39
  - `refresh()` performs one poll-and-submit pass
39
- - `wait()` repeatedly refreshes until the run is terminal
40
+ - `wait()` repeatedly refreshes until the run is terminal, and skips the poll-interval sleep after refreshes that complete items, fail items, or submit additional batches
40
41
  - `results()` and artifact lifecycle operations are terminal-only
41
42
  - terminal currently means either `completed` or `completed_with_failures`
42
43
  - `read_terminal_results()` and `export_terminal_results()` are incremental APIs for already-terminal items and are safe to call before the whole run finishes
@@ -49,6 +50,7 @@ The control plane is the state store:
49
50
 
50
51
  - run config
51
52
  - run control state
53
+ - run control reason
52
54
  - item rows and attempts
53
55
  - active batch rows
54
56
  - parsed outputs and failure records
@@ -80,6 +82,7 @@ Current storage responsibilities include:
80
82
 
81
83
  - persisting public run config
82
84
  - persisting run control state
85
+ - persisting run control reason
83
86
  - persisting deterministic-source ingest checkpoints when available
84
87
  - persisting item state and attempts
85
88
  - persisting terminal result sequence metadata for incremental reads
@@ -111,6 +114,7 @@ For SQLite-backed runs, the default artifact root is a sibling `*_artifacts/` di
111
114
  ## Postgres behavior
112
115
 
113
116
  Postgres exists as an opt-in control-plane backend for cases where SQLite is not enough, such as shared state across processes or hosts.
117
+ `PostgresStorage` uses psycopg v3. Plain `postgresql://` and `postgres://` DSNs are normalized to `postgresql+psycopg://` before engine creation; explicit SQLAlchemy driver schemes are preserved.
114
118
 
115
119
  Important operational rule:
116
120
 
@@ -118,6 +122,16 @@ Important operational rule:
118
122
 
119
123
  Postgres stores the control plane, not the large request/output files themselves.
120
124
 
125
+ ## Attempt accounting
126
+
127
+ Item `attempt_count` records consumed provider attempts.
128
+
129
+ - Successful submitted completions increment the counter once.
130
+ - Counted item-level provider or parse failures increment the counter before retry/permanent-failure decisions.
131
+ - Local pre-submission rejections, control-plane quota pauses, batch-level reset-to-pending paths, and missing output rows marked with `count_attempt=False` do not increment it.
132
+
133
+ This keeps terminal exports aligned with audit expectations: a first-attempt success reports `attempt_count == 1`, and a retryable counted failure followed by success reports `attempt_count == 2`.
134
+
121
135
  ## File-source checkpoints
122
136
 
123
137
  For deterministic built-in sources, storage persists a source checkpoint with:
@@ -163,6 +177,7 @@ Successful rehydration depends on:
163
177
  - credentials being available when a refresh needs to talk to the provider
164
178
 
165
179
  Fresh-process resume also requeues any `queued_local` items back to `pending` before submission resumes.
180
+ When `start(job, run_id=...)` resumes a run with active provider batches, it first performs a poll-only reconciliation pass. Terminal provider batches are consumed, failed batches update retry backoff, and any active backoff prevents new source materialization or submission until the backoff expires.
166
181
 
167
182
  Resume compatibility intentionally ignores non-persisted secret fields such as provider API keys.
168
183
 
@@ -176,6 +191,8 @@ Built-in deterministic sources currently include:
176
191
  - `JsonlItemSource`
177
192
  - `ParquetItemSource`
178
193
 
194
+ For checkpointed ingestion, durable stores append materialized item chunks and advance the ingest checkpoint in one storage operation. Parquet and composite sources can also identify an end-of-source checkpoint from cheap metadata, so a resume after all rows were materialized but before the final completion flag was written can mark ingestion complete without re-reading the source data or re-rendering prompts.
195
+
179
196
  Once an item has a durable request artifact pointer, `batchor` prunes large inline request-building fields from the control-plane store and relies on the artifact for later retries.
180
197
 
181
198
  ## Artifact lifecycle
@@ -222,7 +239,15 @@ Semantics:
222
239
  - `resume` restarts those local activities from persisted state
223
240
  - `cancel` stops new ingestion/submission, keeps polling active provider batches, and then permanently fails remaining local non-terminal items with `error_class="run_cancelled"`
224
241
 
225
- `wait()` fails fast on paused runs instead of sleeping indefinitely.
242
+ Manual pauses record `control_reason="manual"`. OpenAI upload/create/polling and batch-level insufficient-quota pauses record `control_reason="openai_insufficient_quota"`, clear batch-control-plane backoff, and preserve affected items for later retry without consuming attempts.
243
+
244
+ Row-level insufficient-quota records inside completed batch outputs do not pause the run. Those items are stored as `failed_retryable` with `error_class="openai_insufficient_quota"`, do not consume attempts, and rely on retry backoff before they are submitted again.
245
+
246
+ Auto-pause must not overwrite `cancel_requested`. Cancellation is non-reversible, so quota errors observed while draining already-submitted batches leave the run in cancel flow.
247
+
248
+ For non-checkpointed finite iterables, ingestion continues materializing remaining items after an auto-pause so `resume()` cannot silently lose input rows that were not yet persisted. Checkpointed sources may stop on pause because their stored checkpoint can resume materialization later.
249
+
250
+ `wait()` fails fast on paused runs instead of sleeping indefinitely. The raised `RunPausedError` carries the same `control_reason` as the summary.
226
251
  Provider-side remote cancellation is still `TBD`.
227
252
 
228
253
  ## Python API versus CLI
@@ -7,7 +7,8 @@ This document describes the current SQLite schema-versioning story for `batchor`
7
7
  - SQLite remains the default durable backend
8
8
  - the SQLite schema is additive and self-healing for supported columns/tables
9
9
  - storage metadata now persists a `schema_version`
10
- - the current published schema version is `1`
10
+ - the current published schema version is `4`
11
+ - schema version `4` adds the nullable `runs.control_reason` column for durable pause reasons
11
12
 
12
13
  ## Compatibility Rules
13
14
 
@@ -303,6 +303,8 @@ runner = BatchRunner(
303
303
  )
304
304
  ```
305
305
 
306
+ `PostgresStorage` uses psycopg v3. Plain `postgresql://` and `postgres://` DSNs are accepted and normalized to `postgresql+psycopg://`; explicit SQLAlchemy driver schemes are left unchanged.
307
+
306
308
  ## Artifacts, export, prune, and retention
307
309
 
308
310
  `batchor` stores request artifacts for replay and raw output artifacts for audit/export.
@@ -31,6 +31,7 @@ uv run pytest -q
31
31
  This path uses the default pytest configuration, which:
32
32
 
33
33
  - runs in parallel
34
+ - collects this package's `tests/` directory by default, so local reference checkouts are not treated as part of the smoke suite
34
35
  - enforces strict pytest config/marker handling
35
36
  - enforces the `85%` coverage gate
36
37
 
@@ -77,15 +78,21 @@ Expected:
77
78
  - composite deterministic sources can namespace duplicate row IDs across explicit inputs and resume across source boundaries
78
79
  - Parquet source adapters can resume from opaque checkpoints and project only required columns
79
80
  - retry/resume from persisted request artifacts still works for SQLite-backed runs
81
+ - resumed runs reconcile existing active batches and persisted backoff before materializing more source rows
80
82
  - transient batch-poll failures do not block unrelated pending submissions from being sent when capacity remains
81
83
  - paused runs stop polling/submission until resumed
84
+ - OpenAI control-plane and batch-level insufficient-quota provider failures auto-pause with a durable `control_reason` and do not consume item attempts
85
+ - OpenAI row-level insufficient-quota records in completed batch output remain retryable, do not consume attempts, and back off without pausing the run
86
+ - quota auto-pause preserves `cancel_requested` and does not strand non-checkpointed input rows during initial ingestion
82
87
  - cancelled runs drain already-submitted work and mark remaining local items as `run_cancelled`
83
88
  - incremental terminal-result reads/exports remain sequence-based and idempotent across repeated calls
84
89
  - raw output/error artifacts can be exported and require export before raw pruning
85
90
  - raw output/error artifact persistence can be disabled without breaking parsed terminal results or request-artifact replay
86
91
  - terminal runs, including `completed_with_failures`, can prune request artifacts without losing persisted results
87
92
  - shared storage-contract behavior remains aligned across SQLite and opt-in Postgres
93
+ - completed submitted items report consumed attempts consistently across storage backends
88
94
  - OpenAI request splitting and enqueue-limit logic still behave as expected
95
+ - wait-mode refresh cycles keep draining immediately after poll/submission progress, without introducing idle poll sleeps while more local work can be sent
89
96
  - structured-output parsing remains stable
90
97
 
91
98
  Notes:
@@ -127,19 +134,25 @@ This is not part of default CI. Prefer fake-provider tests for automated coverag
127
134
  Manual only. Recommended local flow:
128
135
 
129
136
  ```bash
130
- export OPENAI_API_KEY=sk-...
131
137
  export BATCHOR_RUN_LIVE_TESTS=1
132
138
  export BATCHOR_LIVE_OPENAI_MODEL=gpt-5-nano
133
139
  uv run pytest tests/integration/test_batchor_live_openai.py --no-cov -q
134
140
  ```
135
141
 
142
+ Minimum single-item live smoke:
143
+
144
+ ```bash
145
+ export BATCHOR_RUN_LIVE_TESTS=1
146
+ uv run pytest tests/integration/test_batchor_live_openai.py -k text_job_smoke --no-cov -q
147
+ ```
148
+
136
149
  Behavior:
137
150
 
138
151
  - runs a single-item text smoke and a two-CSV composition smoke against the real OpenAI Batch API
139
152
  - uses SQLite durability and the normal `BatchRunner` flow
140
153
  - defaults to `gpt-5-nano` unless `BATCHOR_LIVE_OPENAI_MODEL` is set
141
154
  - sends no reasoning field unless `BATCHOR_LIVE_OPENAI_REASONING_EFFORT` is set
142
- - loads `.env` when present through the test harness for local use
155
+ - loads `.env` when present through the test harness for local use, so `OPENAI_API_KEY` may come from the shell or `.env`
143
156
  - is skipped unless `BATCHOR_RUN_LIVE_TESTS=1`
144
157
  - requires an OpenAI account with Batch API access and available billing quota
145
158
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "batchor"
7
- version = "0.0.1"
7
+ version = "0.0.3"
8
8
  description = "Structured-first OpenAI Batch runner with typed Pydantic results."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -29,6 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3",
30
30
  "Programming Language :: Python :: 3.12",
31
31
  "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
32
33
  "Topic :: Software Development :: Libraries :: Python Modules",
33
34
  ]
34
35
 
@@ -60,6 +61,7 @@ packages = ["src/batchor"]
60
61
  [tool.pytest.ini_options]
61
62
  addopts = "-q -n auto --strict-config --strict-markers --cov=src --cov-report=term-missing --cov-fail-under=85"
62
63
  pythonpath = ["src"]
64
+ testpaths = ["tests"]
63
65
  markers = [
64
66
  "unit: fast isolated tests",
65
67
  "integration: fake-provider and multi-component tests",