tidegap 0.18.0__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 (46) hide show
  1. tidegap-0.18.0/.gitignore +69 -0
  2. tidegap-0.18.0/PKG-INFO +445 -0
  3. tidegap-0.18.0/README.md +405 -0
  4. tidegap-0.18.0/pyproject.toml +85 -0
  5. tidegap-0.18.0/src/tidegap/__init__.py +226 -0
  6. tidegap-0.18.0/src/tidegap/_data.py +245 -0
  7. tidegap-0.18.0/src/tidegap/_env.py +43 -0
  8. tidegap-0.18.0/src/tidegap/budget.py +297 -0
  9. tidegap-0.18.0/src/tidegap/energy.py +259 -0
  10. tidegap-0.18.0/src/tidegap/errors.py +112 -0
  11. tidegap-0.18.0/src/tidegap/grid.py +1283 -0
  12. tidegap-0.18.0/src/tidegap/paths.py +257 -0
  13. tidegap-0.18.0/src/tidegap/providers/__init__.py +37 -0
  14. tidegap-0.18.0/src/tidegap/providers/anthropic.py +304 -0
  15. tidegap-0.18.0/src/tidegap/providers/base.py +209 -0
  16. tidegap-0.18.0/src/tidegap/providers/gemini.py +167 -0
  17. tidegap-0.18.0/src/tidegap/providers/ollama.py +143 -0
  18. tidegap-0.18.0/src/tidegap/providers/openai.py +319 -0
  19. tidegap-0.18.0/src/tidegap/recommend.py +481 -0
  20. tidegap-0.18.0/src/tidegap/routing.py +456 -0
  21. tidegap-0.18.0/src/tidegap/scheduler.py +3562 -0
  22. tidegap-0.18.0/src/tidegap/sign.py +468 -0
  23. tidegap-0.18.0/src/tidegap/types.py +575 -0
  24. tidegap-0.18.0/tests/__init__.py +0 -0
  25. tidegap-0.18.0/tests/conftest.py +20 -0
  26. tidegap-0.18.0/tests/test_audit_2026_07.py +1116 -0
  27. tidegap-0.18.0/tests/test_batch.py +414 -0
  28. tidegap-0.18.0/tests/test_budget.py +339 -0
  29. tidegap-0.18.0/tests/test_energy.py +225 -0
  30. tidegap-0.18.0/tests/test_energy_parity.py +52 -0
  31. tidegap-0.18.0/tests/test_grid.py +151 -0
  32. tidegap-0.18.0/tests/test_grid_feeds_fixtures.py +457 -0
  33. tidegap-0.18.0/tests/test_paths.py +214 -0
  34. tidegap-0.18.0/tests/test_providers.py +628 -0
  35. tidegap-0.18.0/tests/test_providers_gemini_ollama.py +239 -0
  36. tidegap-0.18.0/tests/test_receipt_v011.py +301 -0
  37. tidegap-0.18.0/tests/test_recommend.py +312 -0
  38. tidegap-0.18.0/tests/test_routing.py +409 -0
  39. tidegap-0.18.0/tests/test_sampling_parity.py +31 -0
  40. tidegap-0.18.0/tests/test_scheduler.py +539 -0
  41. tidegap-0.18.0/tests/test_select_window.py +313 -0
  42. tidegap-0.18.0/tests/test_sign.py +298 -0
  43. tidegap-0.18.0/tests/test_sign_vectors.py +180 -0
  44. tidegap-0.18.0/tests/test_task_visibility.py +224 -0
  45. tidegap-0.18.0/tests/test_tick.py +575 -0
  46. tidegap-0.18.0/tests/test_version.py +12 -0
@@ -0,0 +1,69 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # Build output
6
+ dist/
7
+ # The @ebb-ai/cli 0.17.1 shim is hand-written, and 0.17 daemon units pin dist/index.js.
8
+ !compat/npm/cli/dist/
9
+ build/
10
+ *.tsbuildinfo
11
+ .next/
12
+ out/
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.py[cod]
17
+ *$py.class
18
+ .venv/
19
+ venv/
20
+ *.egg-info/
21
+ .pytest_cache/
22
+ .ruff_cache/
23
+
24
+ # Environment
25
+ .env
26
+ .env.local
27
+ .env.*.local
28
+ !.env.example
29
+
30
+ # Logs
31
+ *.log
32
+ npm-debug.log*
33
+ pnpm-debug.log*
34
+
35
+ # IDE
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+ *.swo
40
+
41
+ # OS
42
+ .DS_Store
43
+ Thumbs.db
44
+
45
+ # Local data (SQLite scheduler queue)
46
+ *.db
47
+ *.db-journal
48
+ *.sqlite
49
+ *.sqlite3
50
+ ebb-queue.db
51
+ tidegap-queue.db
52
+
53
+ # Coverage
54
+ coverage/
55
+ *.lcov
56
+
57
+ # Temporary
58
+ tmp/
59
+ .cache/
60
+ development/
61
+
62
+ # Vercel CLI link (local, per-machine)
63
+ .vercel/
64
+ packages/core-py/uv.lock
65
+
66
+ # Local agent/tool artifacts
67
+ .worktrees/
68
+ .playwright-mcp/
69
+ packages/openclaw-plugin/reports/
@@ -0,0 +1,445 @@
1
+ Metadata-Version: 2.5
2
+ Name: tidegap
3
+ Version: 0.18.0
4
+ Summary: Run non-urgent LLM tasks at the cleanest grid hour before a deadline, via Batch APIs when allowed.
5
+ Project-URL: Homepage, https://github.com/tidegap/tidegap
6
+ Project-URL: Issues, https://github.com/tidegap/tidegap/issues
7
+ Project-URL: Repository, https://github.com/tidegap/tidegap
8
+ Author: Vitalii Borovyk
9
+ License: Apache-2.0
10
+ Keywords: agents,ai,carbon-aware,llm,mcp,scheduling,sustainability
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: aiosqlite>=0.20
25
+ Requires-Dist: httpx>=0.27
26
+ Provides-Extra: anthropic
27
+ Requires-Dist: anthropic<2,>=0.39; extra == 'anthropic'
28
+ Provides-Extra: dev
29
+ Requires-Dist: anthropic<2,>=0.39; extra == 'dev'
30
+ Requires-Dist: cryptography>=42; extra == 'dev'
31
+ Requires-Dist: openai<4,>=1.50; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
33
+ Requires-Dist: pytest>=8; extra == 'dev'
34
+ Requires-Dist: ruff>=0.7; extra == 'dev'
35
+ Provides-Extra: openai
36
+ Requires-Dist: openai<4,>=1.50; extra == 'openai'
37
+ Provides-Extra: signing
38
+ Requires-Dist: cryptography>=42; extra == 'signing'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # Tidegap
42
+
43
+ > Renamed from `ebb-ai` (import `ebb_ai`) in 0.18.0: `pip install tidegap`, `import tidegap`. See [Renamed from Ebb AI](https://github.com/tidegap/tidegap#renamed-from-ebb-ai).
44
+
45
+ **Run non-urgent LLM tasks at the cleanest grid hour before your deadline — Python library.**
46
+
47
+ Tidegap takes LLM tasks that can wait, runs them at the lowest-carbon grid hour before a deadline you set, and sends them through the provider Batch API when the deadline allows.
48
+
49
+ - `recommend_window()` picks the hour from a live 72-hour carbon-intensity forecast: 5 real-data feeds, 31 regions (measured).
50
+ - `defer()` queues the task in SQLite. Work with a deadline over 24 hours goes through the Anthropic and OpenAI Batch APIs, at 50 % of the sync list price (projected, provider list price).
51
+ - With the `signing` extra, every dispatch writes an Ed25519-signed carbon receipt.
52
+
53
+ ```bash
54
+ pip install "tidegap[anthropic,openai,signing]" # Python 3.11+
55
+ ```
56
+
57
+ ```python
58
+ import asyncio; from datetime import datetime, timedelta, timezone
59
+ from tidegap import recommend_window, uk_carbon_intensity_feed
60
+ deadline = datetime.now(timezone.utc) + timedelta(hours=24)
61
+ plan = asyncio.run(recommend_window(region="GB", deadline=deadline, feed=uk_carbon_intensity_feed(), rng=lambda: 0))
62
+ print(plan.scheduled_for, plan.intensity_g_co2_per_kwh, plan.band)
63
+ ```
64
+
65
+ Without `rng`, Tidegap picks at random among the cleanest band of near-equal hours, so repeated runs spread load and can return different hours; an `rng` that returns 0 gives the strict minimum.
66
+
67
+ Proof: one real GB run, 2026-09-30. Forecast peak 183 gCO2/kWh vs recommended window 88 gCO2/kWh, about 52 % lower for this run only (forecast, projected). Same API as the TypeScript package [@tidegap/core](https://www.npmjs.com/package/@tidegap/core). Source: https://github.com/tidegap/tidegap
68
+
69
+ ---
70
+
71
+ ## Why this exists
72
+
73
+ Modern AI agents call LLM APIs synchronously by default. Three costs follow:
74
+
75
+ - **Carbon.** Grid carbon intensity swings substantially over a
76
+ single day across the major US ISOs as generation mix shifts.
77
+ Inference at 2 p.m. on a hot day can be materially dirtier than the
78
+ same call at 3 a.m.
79
+ - **Dollars.** Anthropic and OpenAI both offer batch APIs at a flat
80
+ 50% discount (projected, provider list price) for tasks that can
81
+ wait up to 24 hours. Almost no agent code uses them by default
82
+ because it requires rewriting the call site.
83
+ - **Latency, honestly.** Off-peak *sync* execution is sometimes
84
+ faster because providers throttle and queue at peak. **Batch API
85
+ is *not* faster** — it trades latency (up to 24h SLA) for the
86
+ 50% discount (projected, provider list price).
87
+
88
+ `tidegap` fixes all three for any task that is not "answer me right now."
89
+
90
+ ---
91
+
92
+ ## Environment variables
93
+
94
+ The Python package reads provider credentials from the environment
95
+ (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, …) as a convenience — you are the host
96
+ here: you import `tidegap` directly into your own script, notebook or service.
97
+
98
+ > **Deliberate asymmetry with the TypeScript packages.** `@tidegap/core` (the TS
99
+ > mirror of this library) is *environment-pure*: it reads no environment
100
+ > variables and takes every credential as an explicit argument, because it gets
101
+ > bundled into the `@vitalini/tidegap` OpenClaw plugin, and ClawHub's ClawScan flags
102
+ > any ambient-environment read in a bundle that also makes network calls. This
103
+ > Python package is never bundled into a third-party plugin, so the ergonomic
104
+ > fallbacks stay. Passing credentials explicitly (`AnthropicAdapter(api_key=…)`)
105
+ > works here too and is preferred in library code.
106
+ >
107
+ > Environment variables configure the **`tidegap` CLI** and the **`@tidegap/mcp`
108
+ > server**; the **OpenClaw plugin** is configured through OpenClaw plugin config
109
+ > (`plugins.entries.tidegap.config`), where each field names the variable it
110
+ > replaced.
111
+
112
+ ---
113
+
114
+ ## Install
115
+
116
+ ```bash
117
+ pip install tidegap
118
+
119
+ # with vendor extras and receipt signing
120
+ pip install "tidegap[anthropic,openai,signing]"
121
+
122
+ # from source, for contributors (in a clone of https://github.com/tidegap/tidegap)
123
+ pip install -e "packages/core-py[dev,anthropic,openai]"
124
+ ```
125
+
126
+ Python 3.11+. The core library only requires `httpx` and `aiosqlite`;
127
+ the Anthropic and OpenAI extras pull in the official vendor SDKs.
128
+
129
+ ---
130
+
131
+ ## Quick start
132
+
133
+ ### 1. The five-line version
134
+
135
+ ```python
136
+ import asyncio
137
+ from tidegap import defer
138
+
139
+ async def main():
140
+ result = await defer(
141
+ lambda: "do the work here",
142
+ deadline="2026-05-13T08:00:00-04:00", # "by 8am tomorrow my time"
143
+ carbon_budget_g=5, # optional max grams CO2e
144
+ region="US-CAL-CISO", # optional grid region
145
+ )
146
+ print(result)
147
+
148
+ asyncio.run(main())
149
+ ```
150
+
151
+ `defer()` returns whatever the callable returns. The task body can be
152
+ sync or async; both work.
153
+
154
+ ### 2. Hand-built scheduler with SQLite persistence
155
+
156
+ For long-running services you want a single, explicitly-constructed
157
+ scheduler with a durable queue:
158
+
159
+ ```python
160
+ import asyncio
161
+ from tidegap import Scheduler, DeferOptions
162
+
163
+ async def main():
164
+ async with Scheduler(db_path="/var/lib/tidegap/queue.sqlite") as scheduler:
165
+ await scheduler.defer(
166
+ lambda: do_research_run(),
167
+ DeferOptions(
168
+ deadline="2026-05-13T08:00:00-04:00",
169
+ carbon_budget_g=5,
170
+ region="US-CAL-CISO",
171
+ task_id="user:42:weekly-research",
172
+ ),
173
+ )
174
+
175
+ asyncio.run(main())
176
+ ```
177
+
178
+ If the process restarts, the next `Scheduler(db_path=…)` will see the
179
+ completed receipts via `await scheduler.list_persisted_tasks()`.
180
+
181
+ ### 3. Anthropic Batch API
182
+
183
+ ```python
184
+ import asyncio
185
+ from tidegap import Scheduler, DeferOptions
186
+ from tidegap.providers import AnthropicAdapter
187
+
188
+ async def main():
189
+ adapter = AnthropicAdapter() # reads ANTHROPIC_API_KEY
190
+
191
+ async def work():
192
+ result = await adapter.dispatch(
193
+ "claude-sonnet-5",
194
+ "Summarize today's git commits.",
195
+ )
196
+ return result.text
197
+
198
+ async with Scheduler() as scheduler:
199
+ summary = await scheduler.defer(
200
+ work,
201
+ DeferOptions(
202
+ deadline="2026-05-13T08:00:00-04:00",
203
+ region="US-CAL-CISO",
204
+ ),
205
+ )
206
+ print(summary)
207
+
208
+ asyncio.run(main())
209
+ ```
210
+
211
+ For genuinely batched work (50+ prompts at once), use the Batch API
212
+ directly:
213
+
214
+ ```python
215
+ handle = await adapter.dispatch_batch(
216
+ "claude-sonnet-5",
217
+ ["prompt 1", "prompt 2", "prompt 3"],
218
+ )
219
+ print(handle.batch_id) # poll via the Anthropic SDK
220
+ ```
221
+
222
+ ### 4. OpenAI Batch API
223
+
224
+ ```python
225
+ import asyncio
226
+ from tidegap.providers import OpenAIAdapter
227
+
228
+ async def main():
229
+ adapter = OpenAIAdapter() # reads OPENAI_API_KEY
230
+ handle = await adapter.dispatch_batch(
231
+ "gpt-6-sol",
232
+ ["prompt 1", "prompt 2"],
233
+ )
234
+ print(handle.batch_id)
235
+ ```
236
+
237
+ `OpenAIAdapter` builds the required JSONL input file in memory,
238
+ uploads it via `files.create(purpose="batch")`, and submits the batch
239
+ job — the standard OpenAI batch flow.
240
+
241
+ ---
242
+
243
+ ## API reference
244
+
245
+ ### Top-level exports
246
+
247
+ | Name | Kind | Description |
248
+ |---|---|---|
249
+ | `defer(task, *, deadline, carbon_budget_g, region, task_id)` | async function | Defers on the process-wide default scheduler. |
250
+ | `Scheduler` | class | Explicit scheduler; supports SQLite persistence. |
251
+ | `mock_grid_feed()` | function | Deterministic synthetic feed for dev/tests. |
252
+ | `electricity_maps_feed(api_key=None)` | function | Electricity Maps free-tier API client. |
253
+ | `watttime_feed(username=None, password=None, *, fallback=None)` | function | WattTime v3 marginal (co2_moer) forecast feed for US ISOs (`WATTTIME_USERNAME`/`WATTTIME_PASSWORD`). Takes precedence over EIA where covered; falls through to `fallback` otherwise. |
254
+ | `pick_best_window(entries, deadline)` | pure function | Picks the lowest-intensity entry inside `[now, deadline]`. |
255
+ | `normalize_deadline(d)` | function | Parses + validates a user-supplied deadline. |
256
+ | `CarbonBudgetExceededError` | exception | Raised when no candidate window meets the user budget. |
257
+ | `InvalidDeadlineError` | exception (`ValueError`) | Raised when the deadline is unparseable or in the past. |
258
+ | `TaskRecord`, `CarbonReceipt`, `GridForecast`, `GridForecastEntry`, `DeferOptions` | dataclasses | Public record types. |
259
+
260
+ ### `Scheduler`
261
+
262
+ ```python
263
+ Scheduler(*, feed: GridFeed | None = None,
264
+ default_region: str = "US-CAL-CISO",
265
+ db_path: str | None = None)
266
+ ```
267
+
268
+ Methods:
269
+
270
+ - `await scheduler.connect()` — opens the SQLite connection (no-op
271
+ when `db_path` is unset).
272
+ - `await scheduler.defer(task, opts)` — deferred await. Returns the
273
+ task's eventual result.
274
+ - `scheduler.enqueue(task, opts)` — fire-and-forget. Returns the
275
+ `TaskRecord` immediately with `status="queued"`.
276
+ - `scheduler.get_task(task_id)` / `scheduler.list_tasks()` — in-memory
277
+ snapshots.
278
+ - `await scheduler.load_persisted_task(task_id)` /
279
+ `await scheduler.list_persisted_tasks()` — reads from SQLite.
280
+ - `await scheduler.shutdown()` — cancels in-flight schedule tasks and
281
+ closes the DB. The class also supports `async with`.
282
+
283
+ ### Carbon-receipt schema
284
+
285
+ Every completed task has a `receipt` field:
286
+
287
+ ```python
288
+ @dataclass
289
+ class CarbonReceipt:
290
+ task_id: str
291
+ ran_at: str
292
+ region: str
293
+ estimated_carbon_g_co2: float
294
+ provider: str | None = None
295
+ model: str | None = None
296
+ duration_ms: float | None = None
297
+ ```
298
+
299
+ The `TaskRecord` also carries `intensity_source`: `"scored"` if the
300
+ receipt used the forecast entry the scheduler chose against,
301
+ `"current"` if we had to fall back to a freshly-fetched current-hour
302
+ intensity at dispatch time.
303
+
304
+ The receipt's `estimated_carbon_g_co2` is computed via
305
+ `estimate_energy_kwh(model=..., input_tokens=..., output_tokens=...)
306
+ * grid_intensity_g_co2_per_kwh`. As of v0.6 the energy module ships
307
+ per-model Wh/token coefficients from the published research
308
+ (Patterson et al. 2021; Luccioni, Jernite, Strubell 2024; Hugging
309
+ Face AI Energy Score). When no model is specified the function
310
+ returns the pre-v0.6 flat `0.0015` kWh estimate to preserve
311
+ backwards compatibility. See `tidegap.energy` for the table and
312
+ citation metadata.
313
+
314
+ ### Grid feeds
315
+
316
+ Both feeds implement `GridFeed.fetch_forecast(region, hours)`:
317
+
318
+ - `mock_grid_feed()` — deterministic intraday sinusoid with regional
319
+ floors. Useful for dev and CI without an API key.
320
+ - `electricity_maps_feed(api_key=None)` — hits
321
+ `api.electricitymap.org/v3/carbon-intensity/forecast` with a 5 s
322
+ hard timeout. Falls back to the mock feed (and logs a warning) on
323
+ any failure, mirroring the TypeScript port.
324
+
325
+ Supported region codes match Electricity Maps' zone codes
326
+ (`US-CAL-CISO`, `US-TEX-ERCO`, `US-NE-ISNE`, `US-NY-NYIS`,
327
+ `US-MIDA-PJM`, `US-MIDW-MISO`, `FR`, `DE`, `GB`, ...).
328
+
329
+ ### Provider adapters
330
+
331
+ ```python
332
+ from tidegap.providers import (
333
+ AnthropicAdapter,
334
+ OpenAIAdapter,
335
+ GeminiAdapter,
336
+ OllamaAdapter,
337
+ DispatchOptions,
338
+ )
339
+ ```
340
+
341
+ Every adapter implements the sync path:
342
+
343
+ ```python
344
+ async def dispatch(model: str, prompt: str,
345
+ options: DispatchOptions | None = None) -> DispatchResult: ...
346
+ ```
347
+
348
+ `AnthropicAdapter` and `OpenAIAdapter` are additionally batch-capable
349
+ (`dispatch_batch` + `retrieve_batch`, a flat 50% discount — projected,
350
+ provider list price — 24h SLA). `GeminiAdapter`
351
+ and `OllamaAdapter` are sync-only and inherit the base batch stubs — the
352
+ scheduler feature-detects that and keeps their tasks on the sync path:
353
+
354
+ - **`GeminiAdapter`** — Google's Generative Language API
355
+ (`generativelanguage.googleapis.com`). Reads `GEMINI_API_KEY`, falling back to
356
+ `GOOGLE_API_KEY`. Batch is deliberately unsupported: Gemini's batch modes do
357
+ not map onto the submit → poll → results contract (Vertex batch needs
358
+ GCS/BigQuery; the Developer-API batch is a long-running operation keyed by an
359
+ operation name, not a batch id).
360
+ - **`OllamaAdapter`** — a local Ollama server over HTTP (default
361
+ `http://localhost:11434`, override with `OLLAMA_HOST`). Keyless. Uses
362
+ `/api/chat` and reports `prompt_eval_count` / `eval_count` token counts. No
363
+ batch (local inference).
364
+
365
+ The Anthropic / OpenAI modules import cleanly even when the vendor SDK isn't
366
+ installed — construction is what raises a clear error. Gemini and Ollama use
367
+ `httpx` directly (already a core dependency), so they need no extra SDK.
368
+
369
+ ---
370
+
371
+ ## Persistence (Python-only in v0.2)
372
+
373
+ The Python port ships SQLite-backed durability from day one. To enable
374
+ it, construct the `Scheduler` with a `db_path`:
375
+
376
+ ```python
377
+ async with Scheduler(db_path="/var/lib/tidegap/queue.sqlite") as scheduler:
378
+ ...
379
+ ```
380
+
381
+ Schema:
382
+
383
+ ```sql
384
+ CREATE TABLE tasks (
385
+ task_id TEXT PRIMARY KEY,
386
+ status TEXT NOT NULL,
387
+ enqueued_at TEXT NOT NULL,
388
+ scheduled_for TEXT,
389
+ completed_at TEXT,
390
+ region TEXT NOT NULL,
391
+ carbon_budget_g REAL,
392
+ result_json TEXT,
393
+ error TEXT,
394
+ receipt_json TEXT,
395
+ intensity_source TEXT
396
+ );
397
+ ```
398
+
399
+ Receipts and results are stored as JSON. Non-JSON results (e.g. an
400
+ Anthropic `Message` object) are stored as `{"_repr": "<repr>"}` so
401
+ audit trails survive odd value types.
402
+
403
+ Multi-writer support (multiple schedulers pointed at one DB) requires
404
+ WAL mode and is on the v0.3 roadmap.
405
+
406
+ ---
407
+
408
+ ## Testing
409
+
410
+ ```bash
411
+ cd packages/core-py
412
+ python3 -m venv .venv
413
+ . .venv/bin/activate
414
+ pip install -e ".[dev,anthropic,openai]"
415
+ pytest -v
416
+ ```
417
+
418
+ The test suite covers:
419
+
420
+ - Mock grid feed shape and intraday curve.
421
+ - `pick_best_window` correctness (empty, past, in-window).
422
+ - Scheduler accounting: queued/scheduled/running/completed
423
+ transitions.
424
+ - Deadline validation: unparseable strings, past dates,
425
+ `datetime` instances.
426
+ - Carbon-budget enforcement (US-MIDW-MISO floor exceeds a 0.1 g cap).
427
+ - SQLite round-trips, including reopening the DB after shutdown.
428
+ - Provider adapter request shapes and response parsing for both
429
+ Anthropic and OpenAI (mocked SDKs — no live calls).
430
+
431
+ ---
432
+
433
+ ## Changes
434
+
435
+ Release notes: https://github.com/tidegap/tidegap/blob/main/CHANGELOG.md
436
+
437
+ ---
438
+
439
+ ## License
440
+
441
+ [Apache License 2.0](https://github.com/tidegap/tidegap/blob/main/LICENSE) — patent grant included.
442
+
443
+ ---
444
+
445
+ *Built by [Vitalii Borovyk](https://github.com/Vitalini).*