options-analysis-suite 0.1.0a1__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 (26) hide show
  1. options_analysis_suite-0.1.0a1/.gitignore +24 -0
  2. options_analysis_suite-0.1.0a1/CHANGELOG.md +75 -0
  3. options_analysis_suite-0.1.0a1/LICENSE +21 -0
  4. options_analysis_suite-0.1.0a1/Makefile +40 -0
  5. options_analysis_suite-0.1.0a1/PKG-INFO +221 -0
  6. options_analysis_suite-0.1.0a1/README.md +187 -0
  7. options_analysis_suite-0.1.0a1/pyproject.toml +100 -0
  8. options_analysis_suite-0.1.0a1/scripts/gen_models.py +113 -0
  9. options_analysis_suite-0.1.0a1/src/oas/__init__.py +49 -0
  10. options_analysis_suite-0.1.0a1/src/oas/_generated/__init__.py +0 -0
  11. options_analysis_suite-0.1.0a1/src/oas/_generated/models.py +1068 -0
  12. options_analysis_suite-0.1.0a1/src/oas/_manifest.py +110 -0
  13. options_analysis_suite-0.1.0a1/src/oas/_transport.py +113 -0
  14. options_analysis_suite-0.1.0a1/src/oas/calibration.py +275 -0
  15. options_analysis_suite-0.1.0a1/src/oas/client.py +994 -0
  16. options_analysis_suite-0.1.0a1/src/oas/credentials.py +57 -0
  17. options_analysis_suite-0.1.0a1/src/oas/errors.py +129 -0
  18. options_analysis_suite-0.1.0a1/tests/__init__.py +0 -0
  19. options_analysis_suite-0.1.0a1/tests/conftest.py +49 -0
  20. options_analysis_suite-0.1.0a1/tests/fixtures/openapi.snapshot.json +1 -0
  21. options_analysis_suite-0.1.0a1/tests/test_calibration.py +297 -0
  22. options_analysis_suite-0.1.0a1/tests/test_client_live.py +26 -0
  23. options_analysis_suite-0.1.0a1/tests/test_client_mocked.py +223 -0
  24. options_analysis_suite-0.1.0a1/tests/test_drift.py +108 -0
  25. options_analysis_suite-0.1.0a1/tests/test_fixture_freshness.py +50 -0
  26. options_analysis_suite-0.1.0a1/tests/test_methods_mocked.py +273 -0
@@ -0,0 +1,24 @@
1
+ # Build artifacts
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+ .eggs/
6
+
7
+ # Virtual envs
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Caches
13
+ __pycache__/
14
+ *.py[cod]
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+
23
+ # OS
24
+ .DS_Store
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `options-analysis-suite` Python SDK are documented
4
+ in this file.
5
+
6
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
+ and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ While the SDK is in `0.x`, the public surface may change between minor
10
+ releases — pin to a specific `0.0.x` if you need stability. Breaking changes
11
+ will be called out in this file with **Breaking** at the start of the bullet.
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.1.0a1] — 2026-04-28
16
+
17
+ First public alpha. Architecture decided, full surface implemented, drift
18
+ checked against the deployed OpenAPI spec.
19
+
20
+ ### Added
21
+
22
+ - `OASClient` with 49 typed methods covering every typed `/v1/*` operationId
23
+ from the data-api OpenAPI spec.
24
+ - **Compute (9):** `price`, `greeks`, `exposure`, `scenario`, `sensitivity`,
25
+ `max_pain`, `expected_move`, `probability` (+ `probability_simple` /
26
+ `probability_full` typed helpers), `calibrate`.
27
+ - **Data (40):** snapshot, metrics, regime, IV surface, Greeks history,
28
+ scanner, market structure (ATS/OTC/blocks), calendars (economic/IPO/
29
+ dividend/split), corporate actions, news, history, FRED, Treasury
30
+ auctions, bond ETF + TRACE.
31
+ - `Calibration` domain helper that wraps a `/v1/compute/calibrate` result.
32
+ - `cal.price(...)` / `cal.greeks(...)` forward `modelParams=cal.params`
33
+ automatically.
34
+ - `cal.save("path.json")` / `Calibration.from_json("path.json", client=...)`
35
+ for durable persistence; the 30-second-only `calibrationId` is
36
+ intentionally not surfaced.
37
+ - `_format_version=1` written to disk; loading newer files raises
38
+ `ValueError`.
39
+ - Symbol forwarding to `/price` / `/greeks` is suppressed only when the
40
+ caller supplies full numeric inputs **including** numeric `t`, so
41
+ compute-only API keys don't trip the server's data-scope check.
42
+ - `iter_metrics(symbols, batch_size=50)` — chunks a large symbol list across
43
+ `/v1/data/metrics/batch` calls.
44
+ - Typed exceptions: `OASError`, `AuthenticationError`, `ValidationError`,
45
+ `PermissionDeniedError` (with `.required_scope`), `NotFoundError`,
46
+ `RateLimitError` (with `.retry_after`, `.bucket`), `CalibrationQuotaError`
47
+ (with `.resets_at`), `ConcurrencyLimitError` (with `.current`, `.max`),
48
+ `ServerError`.
49
+ - `BrokerCredentials` BYOK helpers: `TradierCredentials(token=...)`,
50
+ `TastytradeCredentials(refresh_token=..., client_secret=...)`.
51
+ - Generated Pydantic v2 response models in `oas._generated.models`
52
+ (88 classes from the OpenAPI spec). Post-processed to use
53
+ `extra='ignore'` so additive server fields don't break older SDK
54
+ versions.
55
+ - Drift-test suite (`tests/test_drift.py`) gates SDK coverage against
56
+ `/openapi.json`. Strict-equality gate enabled.
57
+ - Pinned spec fixture at `tests/fixtures/openapi.snapshot.json` for offline
58
+ test runs; `make test-live` checks freshness against deployed prod.
59
+
60
+ ### Notes
61
+
62
+ - `py.typed` is intentionally **not** shipped in this release: the
63
+ hand-written surface (client/errors/credentials/transport) is mypy-strict
64
+ clean, but the auto-generated module has type-check warnings that won't
65
+ resolve until `datamodel-code-generator` produces fully mypy-clean output.
66
+ Methods still return typed Pydantic models at runtime.
67
+ - `Typing :: Typed` classifier withheld for the same reason.
68
+ - ListResponse-backed methods return `dict[str, Any]` so server-side
69
+ top-level metadata (date, page, count, direction, etc.) is preserved.
70
+ These will tighten to typed models as endpoint-specific schemas land
71
+ upstream.
72
+ - Sync-only client. `AsyncOASClient` deferred until there's a user need.
73
+
74
+ [Unreleased]: https://github.com/Options-Analysis-Suite/options-analysis-suite-python/compare/v0.1.0a1...HEAD
75
+ [0.1.0a1]: https://github.com/Options-Analysis-Suite/options-analysis-suite-python/releases/tag/v0.1.0a1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Options Analysis Suite
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,40 @@
1
+ .PHONY: install gen lint typecheck test test-live build clean
2
+
3
+ # Lint/typecheck/test targets route through ./.venv/bin/* so they work
4
+ # without activating the virtualenv first. `make install` provisions .venv
5
+ # via uv. `make build` calls `uv build` directly (no .venv lookup needed).
6
+ PY := .venv/bin/python
7
+ PYTEST := .venv/bin/pytest
8
+ RUFF := .venv/bin/ruff
9
+ MYPY := .venv/bin/mypy
10
+
11
+ install:
12
+ uv venv --python 3.12 .venv
13
+ uv pip install --python .venv -e ".[dev,codegen]"
14
+
15
+ # Re-derive Pydantic models from the live OpenAPI spec. Requires datamodel-codegen.
16
+ gen:
17
+ $(PY) scripts/gen_models.py
18
+
19
+ lint:
20
+ $(RUFF) check src tests scripts
21
+
22
+ typecheck:
23
+ $(MYPY)
24
+
25
+ # Unit + drift tests (no network). Reads tests/fixtures/openapi.snapshot.json by
26
+ # default; set OAS_OPENAPI_PATH=- to force a live fetch instead.
27
+ test:
28
+ $(PYTEST) -m "not live" -v
29
+
30
+ # Integration test against the deployed API. Requires OAS_API_KEY in env.
31
+ # Also runs the fixture-freshness check that diffs the pinned snapshot against
32
+ # the live /openapi.json — refresh tests/fixtures/openapi.snapshot.json if it fails.
33
+ test-live:
34
+ $(PYTEST) -m live -v
35
+
36
+ build:
37
+ uv build
38
+
39
+ clean:
40
+ rm -rf build dist *.egg-info src/oas.egg-info .pytest_cache .mypy_cache .ruff_cache
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: options-analysis-suite
3
+ Version: 0.1.0a1
4
+ Summary: Python SDK for the Options Analysis Suite API: 17 pricing models, auto-calibration, GEX/DEX exposure, IV surfaces, and pre-computed market data.
5
+ Project-URL: Homepage, https://optionsanalysissuite.com
6
+ Project-URL: Documentation, https://data.optionsanalysissuite.com/docs
7
+ Project-URL: Repository, https://github.com/Options-Analysis-Suite/options-analysis-suite-python
8
+ Project-URL: Changelog, https://github.com/Options-Analysis-Suite/options-analysis-suite-python/blob/main/CHANGELOG.md
9
+ Author: Options Analysis Suite
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: calibration,exposure,greeks,heston,options,quant,sabr,trading
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Office/Business :: Financial :: Investment
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx<1.0,>=0.27
24
+ Requires-Dist: pydantic<3.0,>=2.5
25
+ Provides-Extra: codegen
26
+ Requires-Dist: datamodel-code-generator>=0.26; extra == 'codegen'
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.8; extra == 'dev'
29
+ Requires-Dist: pytest>=7.4; extra == 'dev'
30
+ Requires-Dist: respx>=0.20; extra == 'dev'
31
+ Requires-Dist: ruff>=0.5; extra == 'dev'
32
+ Requires-Dist: twine>=5.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Options Analysis Suite — Python SDK
36
+
37
+ Type-safe Python client for the [Options Analysis Suite API](https://optionsanalysissuite.com).
38
+
39
+ > **Status: alpha (`0.1.0a1`)** — full coverage of every typed `/v1/*`
40
+ > operationId, plus a `Calibration` domain helper. Drift-checked against
41
+ > the deployed OpenAPI spec.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install options-analysis-suite
47
+ ```
48
+
49
+ > Available on PyPI after the first `v*` tag is published. Until then,
50
+ > install from source with `pip install -e .` from a repo checkout.
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from oas import OASClient, TradierCredentials
56
+
57
+ with OASClient(api_key="oas_live_...") as client:
58
+ # Data: cached EOD analytics
59
+ snap = client.snapshot("SPY")
60
+ print(snap.atmIv, snap.netGex, snap.maxPain)
61
+
62
+ if snap.maxPainCurve:
63
+ for row in snap.maxPainCurve:
64
+ print(row.strike, row.totalPain)
65
+
66
+ # Compute: 17 pricing models, full Greeks, exposure, expected move...
67
+ price = client.price(model="bs", is_call=True, S=650, K=650, r=0.05,
68
+ q=0.012, sigma=0.15, t=0.25)
69
+ greeks = client.greeks(model="heston", is_call=True, S=650, K=650, r=0.05,
70
+ q=0.012, sigma=0.15, t=0.25)
71
+
72
+ # Calibrate once, persist, reuse — never re-touches the calibrationId TTL.
73
+ cal = client.calibrate(
74
+ "SPY", model="heston",
75
+ broker=TradierCredentials(token="..."),
76
+ )
77
+ cal.save("spy_heston.json")
78
+
79
+ # Evaluate the calibrated model anywhere across the chain.
80
+ fair = cal.price(is_call=True, K=655, expiry="2026-06-19")
81
+
82
+ # Stream batched metrics without manually paging.
83
+ for m in client.iter_metrics(["SPY", "QQQ", "IWM", "DIA"], batch_size=50):
84
+ print(m.symbol, m.ivRank)
85
+ ```
86
+
87
+ ## Calibration round-trip
88
+
89
+ A `Calibration` is the durable wrapper around a `/v1/compute/calibrate` result.
90
+ The fitted `params` dict survives a JSON round-trip; the 30-second-only
91
+ `calibrationId` is intentionally not surfaced.
92
+
93
+ ```python
94
+ # Load a saved calibration in another process / hours later.
95
+ from oas import Calibration, OASClient
96
+
97
+ cal = Calibration.from_json("spy_heston.json")
98
+ with OASClient(api_key="oas_live_...") as client:
99
+ cal.bind(client) # attach so cal.price() / cal.greeks() can fire HTTP
100
+ price = cal.price(is_call=True, K=650, S=650, r=0.05, q=0.012,
101
+ sigma=0.15, t=0.25)
102
+ ```
103
+
104
+ ## Errors
105
+
106
+ Every error subclass carries the HTTP status, the server's structured `code`
107
+ field (when present), and any extra fields the server returned.
108
+
109
+ ```python
110
+ from oas.errors import NotFoundError, RateLimitError, CalibrationQuotaError
111
+
112
+ try:
113
+ snap = client.snapshot("UNKNOWN")
114
+ except NotFoundError as e:
115
+ print(f"warehouse miss: {e}")
116
+ except RateLimitError as e:
117
+ print(f"slow down — retry in {e.retry_after}s (bucket: {e.bucket})")
118
+ except CalibrationQuotaError as e:
119
+ print(f"calibration quota exhausted; resets at {e.resets_at}")
120
+ ```
121
+
122
+ See [`src/oas/errors.py`](src/oas/errors.py) for the full hierarchy.
123
+
124
+ ## Models
125
+
126
+ Response models live in `oas._generated.models` and are auto-generated from
127
+ the OpenAPI spec at build time. The generated classes use `extra='ignore'`
128
+ so additive server fields (e.g., a new metric in `MetricsResponse`) don't
129
+ break older SDK versions — older SDKs will simply omit unknown fields.
130
+
131
+ To regenerate against the latest spec:
132
+
133
+ ```bash
134
+ make gen
135
+ ```
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ # Set up dev env
141
+ make install
142
+
143
+ # Lint / typecheck / test
144
+ make lint
145
+ make typecheck
146
+ make test # unit + drift, no network. Uses tests/fixtures/openapi.snapshot.json
147
+ make test-live # integration (needs OAS_API_KEY env var). Also re-checks the
148
+ # pinned spec fixture against the deployed /openapi.json.
149
+ # Refresh the fixture with:
150
+ # curl -s https://data.optionsanalysissuite.com/openapi.json \
151
+ # > tests/fixtures/openapi.snapshot.json
152
+
153
+ # Build distribution
154
+ make build
155
+ ```
156
+
157
+ ## Drift gate
158
+
159
+ The SDK's `_manifest.py` registers every operationId → `OASClient` method
160
+ pairing. `tests/test_drift.py` runs four checks against the spec:
161
+
162
+ 1. Every `ENDPOINTS` operationId actually exists in `/openapi.json`
163
+ 2. Every registered `Endpoint.sdk_method` resolves to a real `OASClient` attr
164
+ 3. Every registered `Endpoint.path` is a path in the spec
165
+ 4. **Strict gate**: spec ops − ignored − expected-missing == `ENDPOINTS` keys.
166
+ Catches both unfinished stubs left in the allowlist and surprise spec
167
+ additions.
168
+
169
+ The `EXPECTED_MISSING_OPERATION_IDS` allowlist is **empty** — the SDK
170
+ covers every typed operation. The `live` test suite double-checks that
171
+ the pinned `tests/fixtures/openapi.snapshot.json` still matches deployed
172
+ prod.
173
+
174
+ ## Release process (maintainers)
175
+
176
+ Releases are tag-driven via the `publish.yml` GitHub Actions workflow.
177
+ The publish job uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
178
+ (OIDC) — no API tokens are stored anywhere.
179
+
180
+ **One-time setup (on PyPI):**
181
+
182
+ 1. Visit https://pypi.org/manage/account/publishing/
183
+ 2. Add a pending publisher with:
184
+ - Owner: `Options-Analysis-Suite`
185
+ - Repository: `options-analysis-suite-python`
186
+ - Workflow filename: `publish.yml`
187
+ - Environment: `pypi`
188
+ 3. (After the first publish, the project will exist on PyPI and the
189
+ pending publisher becomes a regular trusted publisher.)
190
+
191
+ **One-time setup (on GitHub):**
192
+
193
+ The publish job runs in a GitHub environment named `pypi` (see
194
+ `environment: pypi` in `publish.yml`). Create it under **Settings →
195
+ Environments → New environment**. Optional but recommended: require
196
+ manual approval, restrict to the `main` branch, and add reviewers so a
197
+ stray tag push can't ship to PyPI without a human in the loop.
198
+
199
+ **Per-release:**
200
+
201
+ 1. Bump `version` in `pyproject.toml` to the new version
202
+ (e.g. `0.1.0a1` → `0.1.0`).
203
+ 2. Update `CHANGELOG.md` under the new version heading; move
204
+ `[Unreleased]` items into it.
205
+ 3. Commit (`chore: release vX.Y.Z`).
206
+ 4. Tag the commit: `git tag vX.Y.Z` (the `v` prefix is required — the
207
+ publish workflow only triggers on tags matching `v*`).
208
+ 5. Push the tag: `git push origin vX.Y.Z`.
209
+
210
+ The workflow:
211
+ - Verifies the tag's version matches `pyproject.toml`
212
+ - Runs lint + typecheck + offline tests
213
+ - Builds sdist + wheel via `uv build`
214
+ - Publishes to PyPI through the OIDC trusted publisher
215
+
216
+ If the tag-vs-pyproject guard fails, fix `pyproject.toml`, retag with
217
+ `git tag -d` + recreate, and re-push.
218
+
219
+ ## License
220
+
221
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,187 @@
1
+ # Options Analysis Suite — Python SDK
2
+
3
+ Type-safe Python client for the [Options Analysis Suite API](https://optionsanalysissuite.com).
4
+
5
+ > **Status: alpha (`0.1.0a1`)** — full coverage of every typed `/v1/*`
6
+ > operationId, plus a `Calibration` domain helper. Drift-checked against
7
+ > the deployed OpenAPI spec.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install options-analysis-suite
13
+ ```
14
+
15
+ > Available on PyPI after the first `v*` tag is published. Until then,
16
+ > install from source with `pip install -e .` from a repo checkout.
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ from oas import OASClient, TradierCredentials
22
+
23
+ with OASClient(api_key="oas_live_...") as client:
24
+ # Data: cached EOD analytics
25
+ snap = client.snapshot("SPY")
26
+ print(snap.atmIv, snap.netGex, snap.maxPain)
27
+
28
+ if snap.maxPainCurve:
29
+ for row in snap.maxPainCurve:
30
+ print(row.strike, row.totalPain)
31
+
32
+ # Compute: 17 pricing models, full Greeks, exposure, expected move...
33
+ price = client.price(model="bs", is_call=True, S=650, K=650, r=0.05,
34
+ q=0.012, sigma=0.15, t=0.25)
35
+ greeks = client.greeks(model="heston", is_call=True, S=650, K=650, r=0.05,
36
+ q=0.012, sigma=0.15, t=0.25)
37
+
38
+ # Calibrate once, persist, reuse — never re-touches the calibrationId TTL.
39
+ cal = client.calibrate(
40
+ "SPY", model="heston",
41
+ broker=TradierCredentials(token="..."),
42
+ )
43
+ cal.save("spy_heston.json")
44
+
45
+ # Evaluate the calibrated model anywhere across the chain.
46
+ fair = cal.price(is_call=True, K=655, expiry="2026-06-19")
47
+
48
+ # Stream batched metrics without manually paging.
49
+ for m in client.iter_metrics(["SPY", "QQQ", "IWM", "DIA"], batch_size=50):
50
+ print(m.symbol, m.ivRank)
51
+ ```
52
+
53
+ ## Calibration round-trip
54
+
55
+ A `Calibration` is the durable wrapper around a `/v1/compute/calibrate` result.
56
+ The fitted `params` dict survives a JSON round-trip; the 30-second-only
57
+ `calibrationId` is intentionally not surfaced.
58
+
59
+ ```python
60
+ # Load a saved calibration in another process / hours later.
61
+ from oas import Calibration, OASClient
62
+
63
+ cal = Calibration.from_json("spy_heston.json")
64
+ with OASClient(api_key="oas_live_...") as client:
65
+ cal.bind(client) # attach so cal.price() / cal.greeks() can fire HTTP
66
+ price = cal.price(is_call=True, K=650, S=650, r=0.05, q=0.012,
67
+ sigma=0.15, t=0.25)
68
+ ```
69
+
70
+ ## Errors
71
+
72
+ Every error subclass carries the HTTP status, the server's structured `code`
73
+ field (when present), and any extra fields the server returned.
74
+
75
+ ```python
76
+ from oas.errors import NotFoundError, RateLimitError, CalibrationQuotaError
77
+
78
+ try:
79
+ snap = client.snapshot("UNKNOWN")
80
+ except NotFoundError as e:
81
+ print(f"warehouse miss: {e}")
82
+ except RateLimitError as e:
83
+ print(f"slow down — retry in {e.retry_after}s (bucket: {e.bucket})")
84
+ except CalibrationQuotaError as e:
85
+ print(f"calibration quota exhausted; resets at {e.resets_at}")
86
+ ```
87
+
88
+ See [`src/oas/errors.py`](src/oas/errors.py) for the full hierarchy.
89
+
90
+ ## Models
91
+
92
+ Response models live in `oas._generated.models` and are auto-generated from
93
+ the OpenAPI spec at build time. The generated classes use `extra='ignore'`
94
+ so additive server fields (e.g., a new metric in `MetricsResponse`) don't
95
+ break older SDK versions — older SDKs will simply omit unknown fields.
96
+
97
+ To regenerate against the latest spec:
98
+
99
+ ```bash
100
+ make gen
101
+ ```
102
+
103
+ ## Development
104
+
105
+ ```bash
106
+ # Set up dev env
107
+ make install
108
+
109
+ # Lint / typecheck / test
110
+ make lint
111
+ make typecheck
112
+ make test # unit + drift, no network. Uses tests/fixtures/openapi.snapshot.json
113
+ make test-live # integration (needs OAS_API_KEY env var). Also re-checks the
114
+ # pinned spec fixture against the deployed /openapi.json.
115
+ # Refresh the fixture with:
116
+ # curl -s https://data.optionsanalysissuite.com/openapi.json \
117
+ # > tests/fixtures/openapi.snapshot.json
118
+
119
+ # Build distribution
120
+ make build
121
+ ```
122
+
123
+ ## Drift gate
124
+
125
+ The SDK's `_manifest.py` registers every operationId → `OASClient` method
126
+ pairing. `tests/test_drift.py` runs four checks against the spec:
127
+
128
+ 1. Every `ENDPOINTS` operationId actually exists in `/openapi.json`
129
+ 2. Every registered `Endpoint.sdk_method` resolves to a real `OASClient` attr
130
+ 3. Every registered `Endpoint.path` is a path in the spec
131
+ 4. **Strict gate**: spec ops − ignored − expected-missing == `ENDPOINTS` keys.
132
+ Catches both unfinished stubs left in the allowlist and surprise spec
133
+ additions.
134
+
135
+ The `EXPECTED_MISSING_OPERATION_IDS` allowlist is **empty** — the SDK
136
+ covers every typed operation. The `live` test suite double-checks that
137
+ the pinned `tests/fixtures/openapi.snapshot.json` still matches deployed
138
+ prod.
139
+
140
+ ## Release process (maintainers)
141
+
142
+ Releases are tag-driven via the `publish.yml` GitHub Actions workflow.
143
+ The publish job uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
144
+ (OIDC) — no API tokens are stored anywhere.
145
+
146
+ **One-time setup (on PyPI):**
147
+
148
+ 1. Visit https://pypi.org/manage/account/publishing/
149
+ 2. Add a pending publisher with:
150
+ - Owner: `Options-Analysis-Suite`
151
+ - Repository: `options-analysis-suite-python`
152
+ - Workflow filename: `publish.yml`
153
+ - Environment: `pypi`
154
+ 3. (After the first publish, the project will exist on PyPI and the
155
+ pending publisher becomes a regular trusted publisher.)
156
+
157
+ **One-time setup (on GitHub):**
158
+
159
+ The publish job runs in a GitHub environment named `pypi` (see
160
+ `environment: pypi` in `publish.yml`). Create it under **Settings →
161
+ Environments → New environment**. Optional but recommended: require
162
+ manual approval, restrict to the `main` branch, and add reviewers so a
163
+ stray tag push can't ship to PyPI without a human in the loop.
164
+
165
+ **Per-release:**
166
+
167
+ 1. Bump `version` in `pyproject.toml` to the new version
168
+ (e.g. `0.1.0a1` → `0.1.0`).
169
+ 2. Update `CHANGELOG.md` under the new version heading; move
170
+ `[Unreleased]` items into it.
171
+ 3. Commit (`chore: release vX.Y.Z`).
172
+ 4. Tag the commit: `git tag vX.Y.Z` (the `v` prefix is required — the
173
+ publish workflow only triggers on tags matching `v*`).
174
+ 5. Push the tag: `git push origin vX.Y.Z`.
175
+
176
+ The workflow:
177
+ - Verifies the tag's version matches `pyproject.toml`
178
+ - Runs lint + typecheck + offline tests
179
+ - Builds sdist + wheel via `uv build`
180
+ - Publishes to PyPI through the OIDC trusted publisher
181
+
182
+ If the tag-vs-pyproject guard fails, fix `pyproject.toml`, retag with
183
+ `git tag -d` + recreate, and re-push.
184
+
185
+ ## License
186
+
187
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,100 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "options-analysis-suite"
7
+ version = "0.1.0a1"
8
+ description = "Python SDK for the Options Analysis Suite API: 17 pricing models, auto-calibration, GEX/DEX exposure, IV surfaces, and pre-computed market data."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Options Analysis Suite" }]
13
+ keywords = ["options", "trading", "quant", "greeks", "exposure", "calibration", "heston", "sabr"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Office/Business :: Financial :: Investment",
24
+ ]
25
+ # Note: no Typing :: Typed classifier and no `py.typed` marker until the
26
+ # datamodel-codegen output is mypy-clean. The hand-written surface
27
+ # (client.py, errors.py, credentials.py, _transport.py) is fully typed and
28
+ # passes `mypy --strict`; the generated module is excluded.
29
+ dependencies = [
30
+ "httpx>=0.27,<1.0",
31
+ "pydantic>=2.5,<3.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=7.4",
37
+ "respx>=0.20",
38
+ "ruff>=0.5",
39
+ "mypy>=1.8",
40
+ "twine>=5.0",
41
+ ]
42
+ codegen = [
43
+ "datamodel-code-generator>=0.26",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://optionsanalysissuite.com"
48
+ Documentation = "https://data.optionsanalysissuite.com/docs"
49
+ Repository = "https://github.com/Options-Analysis-Suite/options-analysis-suite-python"
50
+ Changelog = "https://github.com/Options-Analysis-Suite/options-analysis-suite-python/blob/main/CHANGELOG.md"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/oas"]
54
+
55
+ [tool.hatch.build.targets.sdist]
56
+ include = [
57
+ "src/oas",
58
+ "tests",
59
+ "scripts",
60
+ "README.md",
61
+ "CHANGELOG.md",
62
+ "LICENSE",
63
+ "Makefile",
64
+ "pyproject.toml",
65
+ ]
66
+
67
+ [tool.ruff]
68
+ line-length = 100
69
+ target-version = "py310"
70
+ # Generated models are not hand-written; lint them only via the codegen script's
71
+ # post-processing — not by hand-editing the file.
72
+ extend-exclude = ["src/oas/_generated"]
73
+
74
+ [tool.ruff.lint]
75
+ select = ["E", "F", "W", "I", "B", "UP", "N"]
76
+
77
+ [tool.ruff.lint.per-file-ignores]
78
+ # Pricing-convention argument names: S=spot, K=strike, etc. Allowed everywhere
79
+ # the math actually demands them; renaming to lowercase would obscure the
80
+ # Black-Scholes correspondence.
81
+ "src/oas/client.py" = ["N803"]
82
+ "src/oas/calibration.py" = ["N803"]
83
+ # _manifest.py is a hand-aligned table mapping operationIds → Endpoint(...)
84
+ # entries. The visual alignment is more valuable than a 100-char wrap.
85
+ "src/oas/_manifest.py" = ["E501"]
86
+
87
+ [tool.pytest.ini_options]
88
+ testpaths = ["tests"]
89
+ markers = [
90
+ "live: integration tests that hit the deployed API (require OAS_API_KEY env var)",
91
+ ]
92
+
93
+ [tool.mypy]
94
+ python_version = "3.10"
95
+ strict = true
96
+ files = ["src/oas"]
97
+ # datamodel-codegen output uses old-style `confloat(...)` / `constr(...)` annotations
98
+ # that mypy doesn't accept as type expressions even though Pydantic handles them at
99
+ # runtime. The generated file is mechanically produced, so don't gate dev on it.
100
+ exclude = ["src/oas/_generated/.*"]