From 70d876d162527c68dba607dc1ce6ed9e4da1d31f Mon Sep 17 00:00:00 2001 From: Stephen Romano Date: Mon, 26 Oct 2026 12:48:22 -0400 Subject: [PATCH] feat(client)!: enforce the beta 100-contract batch ceiling --- contracts/openapi.json | 12 ++++++++---- contracts/provenance.json | 6 +++--- docs/API.md | 2 +- docs/BATCH.md | 7 ++++++- docs/CONTRACT.md | 11 +++++++++-- docs/WORKFLOWS.md | 2 +- src/morphiq_sdk/models.py | 2 +- tests/fixtures/analytics.json | 8 +++++--- tests/fixtures/batch.json | 8 +++++--- tests/fixtures/greeks.json | 8 +++++--- tests/fixtures/implied_volatility.json | 8 +++++--- tests/fixtures/pricing.json | 8 +++++--- tests/test_batch.py | 7 +++++-- tests/test_workflows.py | 4 ++-- 14 files changed, 61 insertions(+), 32 deletions(-) diff --git a/contracts/openapi.json b/contracts/openapi.json index b32e839..4848760 100644 --- a/contracts/openapi.json +++ b/contracts/openapi.json @@ -3033,7 +3033,8 @@ "items": { "$ref": "#/components/schemas/FusedAnalyticsRequest" }, - "maxItems": 1024, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, "minItems": 1 } }, @@ -3131,7 +3132,8 @@ "items": { "$ref": "#/components/schemas/GreeksAllTenRequest" }, - "maxItems": 1024, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, "minItems": 1 } }, @@ -3229,7 +3231,8 @@ "items": { "$ref": "#/components/schemas/ImpliedVolatilityRequest" }, - "maxItems": 1024, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, "minItems": 1 } }, @@ -3328,7 +3331,8 @@ "items": { "$ref": "#/components/schemas/PriceRequest" }, - "maxItems": 1024, + "description": "One to 100 contracts; a lower account limit still applies.", + "maxItems": 100, "minItems": 1 } }, diff --git a/contracts/provenance.json b/contracts/provenance.json index 1bf43b7..a637a54 100644 --- a/contracts/provenance.json +++ b/contracts/provenance.json @@ -1,10 +1,10 @@ { "repository": "https://gitlab.com/prophetizo/morphiq-labs/sites/morphiq-api", - "source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", + "source_commit": "d15a738fd13d79671d65b14358795908a9949f1f", "source_path": "services/api/openapi.json", "contract_version": "0.1.0", - "sha256": "d9a746af609cba68d768c4b743061fd3e91cbe04cd4242c954e48c01f286031d", - "import_method": "Exact immutable API commit from MR !183 copied byte-for-byte; candidate pin pending producer review and merge before SDK merge.", + "sha256": "57ff021f7eb7d3932ac0ef2612c0221b86f30c3612baed4c9d0ec134ee6cc0cf", + "import_method": "Exact immutable API beta-limits candidate copied byte-for-byte. Producer review/merge and coordinated deployment remain pending before SDK release.", "covered_operations": [ "POST /v1/price", "POST /v1/regime", diff --git a/docs/API.md b/docs/API.md index 4bafa1e..b1ce6f8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -109,7 +109,7 @@ updates do not themselves validate changed values. ## Batch and regime requests Every batch envelope requires `requests`, a list of the corresponding single -request type. The reviewed wire schema accepts 1–1024 items; service/account +request type. The reviewed wire schema accepts 1–100 items; service/account limits can be lower. Empty/oversized lists and invalid list edits fail locally. The SDK preserves item order and single-request omission/null semantics. See [all four batch construction examples](BATCH.md#constructing-each-batch-type). diff --git a/docs/BATCH.md b/docs/BATCH.md index c6cb28f..90a2cc8 100644 --- a/docs/BATCH.md +++ b/docs/BATCH.md @@ -14,7 +14,7 @@ request models. One call sends one HTTP request. These methods do not split batc create concurrent work, retry rejected requests, or replay failed items. For caller-sized chunks, optional async concurrency and per-request costs, use an explicit [batch workflow](WORKFLOWS.md). -The reviewed wire schema allows 1–1024 items; the service applies the account's +The reviewed wire schema allows 1–100 items; the service applies the account's actual limit, which may be lower. The SDK does not hardcode plan limits. Set `MORPHIQ_API_KEY`, then run a self-contained example: @@ -151,3 +151,8 @@ remain unknown; use the request ID and account records to reconcile them. Use the optional [pandas adapter](PANDAS.md) to convert batch results into ordered analysis rows and a separate per-request charge table. Core SDK calls do not require pandas. + +The beta service's default compute deadline is 10 seconds, including time waiting +for a worker. The SDK's default timeout remains 30 seconds to allow for transport. +A valid batch size does not guarantee completion before the deadline. A timeout +after execution starts can retain the full CU charge; workflows do not retry it. diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md index 707c2c9..3700450 100644 --- a/docs/CONTRACT.md +++ b/docs/CONTRACT.md @@ -9,8 +9,9 @@ coordinates the producer/import workflow with [API #106](https://gitlab.com/prophetizo/morphiq-labs/sites/morphiq-api/-/work_items/106). contracts/openapi.json now contains the reviewed snapshot byte-for-byte. contracts/provenance.json records its digest, API source commit and contract -version. It was copied from immutable reviewed main source; automated published -artifact delivery remains #2. SDK tests and builds use the offline snapshot +version. This branch pins immutable API candidate `d15a738fd13d79671d65b14358795908a9949f1f` +for the 100-item beta limit; producer review and merge remain pending. Automated +published artifact delivery remains #2. SDK tests and builds use the offline snapshot without private repository or numerical engine access. The SDK covers POST /v1/price, POST /v1/greeks, POST /v1/implied-volatility, @@ -44,3 +45,9 @@ Greeks, implied-volatility and analytics fixtures contain synthetic inputs evalu native service built from the pinned API commit. Tests verify all ten unscaled Greeks, IV solver diagnostics and residual bases, model identity, metadata, error evidence and sync/async parity against native JSON. + +The source-generated schema caps every option batch at 100 items. Fixture +numerical values retain their original provenance in `original_source_commit` +and are replayed against the candidate; no numerical engine or CU price changes +are introduced by this SDK update. The service compute deadline is ten seconds; +the client timeout stays at 30 seconds. diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index ad7a88e..8f69d6f 100644 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -14,7 +14,7 @@ Both `MorphIQ` and `AsyncMorphIQ` provide these factories: | `analytics_workflow()` | `AnalyticsRequest` | `BatchAnalyticsResponse` | Every factory requires `chunk_size=`. Choose a positive integer allowed by your -account; the SDK checks the reviewed wire ceiling of 1024, not customer plan +account; the SDK checks the reviewed wire ceiling of 100, not customer plan limits. A smaller final chunk is allowed. Empty input completes without HTTP requests and has total cost zero. Execution is sequential by default. Async workflows accept an explicit diff --git a/src/morphiq_sdk/models.py b/src/morphiq_sdk/models.py index 7b18295..92b7906 100644 --- a/src/morphiq_sdk/models.py +++ b/src/morphiq_sdk/models.py @@ -384,7 +384,7 @@ class ErrorEnvelope(_Response): class _BatchRequest(_Request, Generic[_OptionRequestType]): # This is the reviewed wire limit, not an account/plan limit. - requests: list[_OptionRequestType] = Field(min_length=1, max_length=1024) + requests: list[_OptionRequestType] = Field(min_length=1, max_length=100) def to_wire(self) -> dict[str, Any]: validated = type(self).model_validate({"requests": self.requests}) diff --git a/tests/fixtures/analytics.json b/tests/fixtures/analytics.json index 7ad33f8..d1fe914 100644 --- a/tests/fixtures/analytics.json +++ b/tests/fixtures/analytics.json @@ -1,6 +1,6 @@ { - "source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", - "contract_sha256": "d9a746af609cba68d768c4b743061fd3e91cbe04cd4242c954e48c01f286031d", + "source_commit": "d15a738fd13d79671d65b14358795908a9949f1f", + "contract_sha256": "57ff021f7eb7d3932ac0ef2612c0221b86f30c3612baed4c9d0ec134ee6cc0cf", "admission": "open", "note": "Synthetic inputs evaluated by the native API built with --locked from the pinned source commit. Served OpenAPI digest verified. Request IDs normalized; open mode does not report commercial CU. No Python numerical implementation.", "cases": [ @@ -216,5 +216,7 @@ } } } - ] + ], + "original_source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", + "replay_note": "Original synthetic numerical values retained; replayed against the beta-limits candidate. Only contract provenance changes." } diff --git a/tests/fixtures/batch.json b/tests/fixtures/batch.json index 6975c40..f2e3e4b 100644 --- a/tests/fixtures/batch.json +++ b/tests/fixtures/batch.json @@ -1,6 +1,6 @@ { - "source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", - "contract_sha256": "d9a746af609cba68d768c4b743061fd3e91cbe04cd4242c954e48c01f286031d", + "source_commit": "d15a738fd13d79671d65b14358795908a9949f1f", + "contract_sha256": "57ff021f7eb7d3932ac0ef2612c0221b86f30c3612baed4c9d0ec134ee6cc0cf", "admission": "open", "note": "Synthetic inputs evaluated by the native API built with --locked from the pinned source commit. Served OpenAPI digest verified. Request IDs normalized; open mode does not report commercial CU. No Python numerical implementation.", "cases": [ @@ -720,5 +720,7 @@ ] } } - ] + ], + "original_source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", + "replay_note": "Original synthetic numerical values retained; replayed against the beta-limits candidate. Only contract provenance changes." } diff --git a/tests/fixtures/greeks.json b/tests/fixtures/greeks.json index 6d52465..6018b19 100644 --- a/tests/fixtures/greeks.json +++ b/tests/fixtures/greeks.json @@ -1,6 +1,6 @@ { - "source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", - "contract_sha256": "d9a746af609cba68d768c4b743061fd3e91cbe04cd4242c954e48c01f286031d", + "source_commit": "d15a738fd13d79671d65b14358795908a9949f1f", + "contract_sha256": "57ff021f7eb7d3932ac0ef2612c0221b86f30c3612baed4c9d0ec134ee6cc0cf", "admission": "open", "note": "Synthetic inputs evaluated by the native API built with --locked from the pinned source commit. Served OpenAPI digest verified. Request IDs normalized; open mode does not report commercial CU. No Python numerical implementation.", "cases": [ @@ -100,5 +100,7 @@ } } } - ] + ], + "original_source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", + "replay_note": "Original synthetic numerical values retained; replayed against the beta-limits candidate. Only contract provenance changes." } diff --git a/tests/fixtures/implied_volatility.json b/tests/fixtures/implied_volatility.json index f60b05b..89f26f8 100644 --- a/tests/fixtures/implied_volatility.json +++ b/tests/fixtures/implied_volatility.json @@ -1,6 +1,6 @@ { - "source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", - "contract_sha256": "d9a746af609cba68d768c4b743061fd3e91cbe04cd4242c954e48c01f286031d", + "source_commit": "d15a738fd13d79671d65b14358795908a9949f1f", + "contract_sha256": "57ff021f7eb7d3932ac0ef2612c0221b86f30c3612baed4c9d0ec134ee6cc0cf", "admission": "open", "note": "Synthetic inputs evaluated by the native API built with --locked from the pinned source commit. Served OpenAPI digest verified. Request IDs normalized; open mode does not report commercial CU. No Python numerical implementation.", "cases": [ @@ -84,5 +84,7 @@ } } } - ] + ], + "original_source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", + "replay_note": "Original synthetic numerical values retained; replayed against the beta-limits candidate. Only contract provenance changes." } diff --git a/tests/fixtures/pricing.json b/tests/fixtures/pricing.json index 4f8e17d..08c2014 100644 --- a/tests/fixtures/pricing.json +++ b/tests/fixtures/pricing.json @@ -1,6 +1,6 @@ { - "source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", - "contract_sha256": "d9a746af609cba68d768c4b743061fd3e91cbe04cd4242c954e48c01f286031d", + "source_commit": "d15a738fd13d79671d65b14358795908a9949f1f", + "contract_sha256": "57ff021f7eb7d3932ac0ef2612c0221b86f30c3612baed4c9d0ec134ee6cc0cf", "admission": "open", "note": "Synthetic inputs evaluated by the pinned native API. Request IDs normalized. Commercial CU metadata is absent in open mode.", "cases": [ @@ -78,5 +78,7 @@ } } } - ] + ], + "original_source_commit": "a2e2f597a3ccb36222d27157cb938aa813a634d2", + "replay_note": "Original synthetic numerical values retained; replayed against the beta-limits candidate. Only contract provenance changes." } diff --git a/tests/test_batch.py b/tests/test_batch.py index cfb9309..1d05990 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -102,8 +102,11 @@ def test_batch_inputs_revalidate_edits_without_plan_assumptions(index, batch_cas with pytest.raises(ValidationError): cls(requests=[]) with pytest.raises(ValidationError): - cls(requests=[values[0]] * 1025) - assert len(cls(requests=[values[0]] * 1024).to_wire()["requests"]) == 1024 + cls(requests=[values[0]] * 101) + schema = json.loads((ROOT / "contracts/openapi.json").read_text()) + wire_name = OPERATIONS[index][3] + "Request" + assert schema["components"]["schemas"][wire_name]["properties"]["requests"]["maxItems"] == 100 + assert len(cls(requests=[values[0]] * 100).to_wire()["requests"]) == 100 request = cls(requests=values) request.requests[0].dividend_schedule.append( {"ex_time_years": float("nan"), "cash_amount": 1.0} diff --git a/tests/test_workflows.py b/tests/test_workflows.py index c906efe..9bb7d44 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -239,10 +239,10 @@ def test_validate_entire_plan_before_network_and_snapshot_inputs(operation, batc factory, request_type = OPERATIONS[operation] requests = request_type(**batch_cases[operation]["request"]).requests with MorphIQ(api_key="workflow-test") as client: - for size in [0, -1, True, 1.5, 1025]: + for size in [0, -1, True, 1.5, 101]: with pytest.raises(ValueError): getattr(client, factory)(requests, chunk_size=size) - assert getattr(client, factory)(requests, chunk_size=1024).planned_chunk_count == 1 + assert getattr(client, factory)(requests, chunk_size=100).planned_chunk_count == 1 # Invalid later chunk fails preparation; no earlier request has been made. requests[-1].dividend_schedule.append({"ex_time_years": float("nan"), "cash_amount": 1}) with pytest.raises(ValidationError):