contrastapi 1.22.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.
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ *.db
6
+ *.db-wal
7
+ *.db-shm
8
+ venv/
9
+ .env
10
+ test_*.db
11
+ .mcpregistry_*
12
+ scripts/*
13
+ !scripts/backfill_mitre_fields.py
14
+ plans/
15
+ .npmrc
16
+ node_modules/
17
+ research_landing*.md
18
+ .coverage
19
+ deploy/
20
+ .aider*
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: contrastapi
3
+ Version: 1.22.3
4
+ Summary: Official Python SDK for ContrastAPI — security intelligence for developers and AI agents
5
+ Project-URL: Homepage, https://contrastcyber.com
6
+ Project-URL: Documentation, https://api.contrastcyber.com/docs
7
+ Project-URL: Repository, https://github.com/UPinar/contrastapi
8
+ Project-URL: Issues, https://github.com/UPinar/contrastapi/issues
9
+ Author-email: ContrastCyber <contact@contrastcyber.com>
10
+ License-Expression: MIT
11
+ Keywords: ai-security,api,atlas,attack,cve,cwe,d3fend,dns,domain,kev,mcp,mitre,osint,security,ssl,threat-intelligence,vulnerability,whois
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx<1.0,>=0.25
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
27
+ Requires-Dist: pytest>=7; extra == 'test'
28
+ Requires-Dist: respx>=0.20; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # ContrastAPI Python SDK
32
+
33
+ Official Python client for [ContrastAPI](https://api.contrastcyber.com) — security intelligence for developers and AI agents.
34
+
35
+ 42 MCP tools / 50+ HTTP endpoints: CVE / EPSS / KEV / CWE, MITRE ATLAS (AI/ML attacks) + bulk drill, MITRE D3FEND defenses, domain audit, IOC + threat intel, OSINT, code-security checks, and more. No API key required for the free tier (100 req/hr).
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install contrastapi
41
+ ```
42
+
43
+ Requires Python 3.10+. Depends only on `httpx>=0.25`.
44
+
45
+ ## Quickstart
46
+
47
+ ### Sync
48
+
49
+ ```python
50
+ from contrastapi import ContrastAPI
51
+ from contrastapi.models import CveResponse, AuditResponse
52
+
53
+ with ContrastAPI() as client: # keyless, free tier
54
+ cve: CveResponse = client.cve.lookup("CVE-2021-44228")
55
+ print(cve["kev"]["in_kev"]) # True (IDE autocompletes "kev", "epss"...)
56
+
57
+ techs = client.atlas.bulk_technique_lookup(["AML.T0051", "AML.T0043"])
58
+ print(techs["successful"]) # 2
59
+
60
+ audit: AuditResponse = client.domain.audit("example.com")
61
+ print(audit["score"])
62
+ ```
63
+
64
+ Every response method has a typed return — `client.cve.lookup(...)` returns `CveResponse`, `client.atlas.technique(...)` returns `AtlasTechniqueResponse`, etc. You never need to write the type annotation; IDEs (VSCode/PyCharm) infer it from the method signature and offer autocomplete on response keys. Import from `contrastapi.models` only if you want to annotate explicitly or pass responses across function boundaries.
65
+
66
+ ### Async
67
+
68
+ ```python
69
+ import asyncio
70
+ from contrastapi import AsyncContrastAPI
71
+
72
+ async def main():
73
+ async with AsyncContrastAPI(api_key="cc_...") as client:
74
+ defenses = await client.d3fend.defense_for_attack("T1059")
75
+ print(len(defenses["defenses"]))
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ ### Shortcuts (multi-call helpers)
81
+
82
+ ```python
83
+ from contrastapi import ContrastAPI, audit_full, enrich_batch, triage_ioc
84
+
85
+ with ContrastAPI() as client:
86
+ # Auto-route IP/hash/domain to the right enrichment leg
87
+ report = triage_ioc(client, "8.8.8.8") # ioc + threat_report
88
+
89
+ # Audit + subdomains + tech + per-subdomain SSL (capped)
90
+ audit = audit_full(client, "example.com", ssl_subdomains=5)
91
+
92
+ # Auto-detect CVE vs IOC and bulk-route
93
+ enriched = enrich_batch(client, ["CVE-2021-44228", "8.8.8.8", "evil.com"])
94
+ ```
95
+
96
+ Shortcuts swallow per-leg `ContrastAPIError` so partial failures still return whatever succeeded — see `result["errors"]` for the failure map.
97
+
98
+ ## Authentication
99
+
100
+ Pass an API key as the first positional argument or as `api_key=`:
101
+
102
+ ```python
103
+ client = ContrastAPI("cc_<your-key>") # 1000 req/hr (Pro tier)
104
+ ```
105
+
106
+ Get a key at [contrastcyber.com/pricing](https://contrastcyber.com/pricing).
107
+
108
+ ## Exception model
109
+
110
+ The SDK maps server error codes (v1.22.2+ wire envelope) to typed exceptions:
111
+
112
+ | Exception | Status | Server code |
113
+ | --- | --- | --- |
114
+ | `InvalidArgumentError` | 400, 422 | `invalid_argument` |
115
+ | `AuthRequiredError` | 401 | `auth_required` |
116
+ | `TierLimitError` | 403 | `tier_limit` |
117
+ | `NotFoundError` | 404 | `not_found` |
118
+ | `RateLimitError` | 429 | `rate_limit_exceeded` |
119
+ | `UpstreamError` | 502 | `upstream_error` |
120
+ | `UpstreamTimeoutError` | 504 | `upstream_timeout` |
121
+ | `TransportError` | n/a | (network failure, before HTTP) |
122
+ | `ContrastAPIError` | * | base / unknown |
123
+
124
+ Every exception carries the parsed envelope:
125
+
126
+ ```python
127
+ from contrastapi import ContrastAPI, RateLimitError
128
+
129
+ try:
130
+ client.cve.lookup("CVE-2021-44228")
131
+ except RateLimitError as exc:
132
+ print(exc.message) # "Hourly limit reached"
133
+ print(exc.retry_after_seconds) # 60 (capped at 3600)
134
+ print(exc.upgrade_url) # "https://contrastcyber.com/pricing"
135
+ print(exc.extras) # back-compat top-level fields (tier, limit, ...)
136
+ ```
137
+
138
+ ## Namespaces
139
+
140
+ | Namespace | Methods |
141
+ | --- | --- |
142
+ | `cve` | `lookup`, `search`, `leading`, `kev`, `exploit`, `bulk` |
143
+ | `cwe` | `lookup` |
144
+ | `ioc` | `lookup`, `hash`, `phishing`, `bulk` |
145
+ | `atlas` | `technique`, `technique_search`, `bulk_technique_lookup`, `case_study`, `case_study_search` |
146
+ | `d3fend` | `defense`, `defense_search`, `defense_for_attack`, `coverage` |
147
+ | `domain` | `report`, `dns`, `whois`, `subdomains`, `certs`, `ssl`, `tech`, `threat`, `monitor`, `vulns`, `audit`, `wayback`, `bulk` |
148
+ | `ip` | `lookup`, `threat_report` |
149
+ | `asn` | `lookup` |
150
+ | `email` | `mx`, `disposable` |
151
+ | `phone` | `lookup` |
152
+ | `password` | `check` (k-anonymity SHA-1 prefix) |
153
+ | `username` | `lookup` |
154
+ | `check` | `secrets`, `injection`, `headers`, `dependencies` |
155
+ | `scan` | `headers` (live HTTP scan) |
156
+
157
+ The async client (`AsyncContrastAPI`) exposes the same namespace surface 1:1 — every method is `async def`.
158
+
159
+ ## Parity with the Node SDK
160
+
161
+ | Surface | Node SDK | Python SDK |
162
+ | --- | --- | --- |
163
+ | Sync | ✅ (Promise-based) | ✅ (`ContrastAPI`) |
164
+ | Async | (Promise model) | ✅ (`AsyncContrastAPI`) |
165
+ | Namespace count | 13 | 14 (adds `username`) |
166
+ | `bulk_technique_lookup` (ATLAS) | (added in v1.4.0) | ✅ |
167
+ | `wayback` archive lookup | (added in v1.4.0) | ✅ |
168
+ | Typed errors | `Error` subclasses | full hierarchy with envelope fields |
169
+ | Shortcuts | — | ✅ `triage_ioc`, `audit_full`, `enrich_batch` |
170
+ | Response models | `Promise<any>` | ✅ `TypedDict` (IDE autocomplete, no runtime cost) |
171
+
172
+ ## Configuration
173
+
174
+ ```python
175
+ client = ContrastAPI(
176
+ api_key="cc_...", # optional; keyless = free tier
177
+ base_url="https://api.contrastcyber.com", # override for self-host
178
+ timeout=30.0, # seconds; clamped to [1, 120]
179
+ allow_insecure=False, # set True to allow http:// (dev only)
180
+ )
181
+ ```
182
+
183
+ The transport hard-caps response bodies at 10 MB, sends a `User-Agent: contrastapi-python/<version>` header, and refuses to send your API key over plaintext HTTP even when `allow_insecure=True`.
184
+
185
+ ## Links
186
+
187
+ - API docs: https://api.contrastcyber.com/docs
188
+ - Pricing & API key: https://contrastcyber.com/pricing
189
+ - Source: https://github.com/UPinar/contrastapi
190
+ - Issues: https://github.com/UPinar/contrastapi/issues
191
+
192
+ ## License
193
+
194
+ MIT — see [LICENSE](../../LICENSE).
@@ -0,0 +1,164 @@
1
+ # ContrastAPI Python SDK
2
+
3
+ Official Python client for [ContrastAPI](https://api.contrastcyber.com) — security intelligence for developers and AI agents.
4
+
5
+ 42 MCP tools / 50+ HTTP endpoints: CVE / EPSS / KEV / CWE, MITRE ATLAS (AI/ML attacks) + bulk drill, MITRE D3FEND defenses, domain audit, IOC + threat intel, OSINT, code-security checks, and more. No API key required for the free tier (100 req/hr).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install contrastapi
11
+ ```
12
+
13
+ Requires Python 3.10+. Depends only on `httpx>=0.25`.
14
+
15
+ ## Quickstart
16
+
17
+ ### Sync
18
+
19
+ ```python
20
+ from contrastapi import ContrastAPI
21
+ from contrastapi.models import CveResponse, AuditResponse
22
+
23
+ with ContrastAPI() as client: # keyless, free tier
24
+ cve: CveResponse = client.cve.lookup("CVE-2021-44228")
25
+ print(cve["kev"]["in_kev"]) # True (IDE autocompletes "kev", "epss"...)
26
+
27
+ techs = client.atlas.bulk_technique_lookup(["AML.T0051", "AML.T0043"])
28
+ print(techs["successful"]) # 2
29
+
30
+ audit: AuditResponse = client.domain.audit("example.com")
31
+ print(audit["score"])
32
+ ```
33
+
34
+ Every response method has a typed return — `client.cve.lookup(...)` returns `CveResponse`, `client.atlas.technique(...)` returns `AtlasTechniqueResponse`, etc. You never need to write the type annotation; IDEs (VSCode/PyCharm) infer it from the method signature and offer autocomplete on response keys. Import from `contrastapi.models` only if you want to annotate explicitly or pass responses across function boundaries.
35
+
36
+ ### Async
37
+
38
+ ```python
39
+ import asyncio
40
+ from contrastapi import AsyncContrastAPI
41
+
42
+ async def main():
43
+ async with AsyncContrastAPI(api_key="cc_...") as client:
44
+ defenses = await client.d3fend.defense_for_attack("T1059")
45
+ print(len(defenses["defenses"]))
46
+
47
+ asyncio.run(main())
48
+ ```
49
+
50
+ ### Shortcuts (multi-call helpers)
51
+
52
+ ```python
53
+ from contrastapi import ContrastAPI, audit_full, enrich_batch, triage_ioc
54
+
55
+ with ContrastAPI() as client:
56
+ # Auto-route IP/hash/domain to the right enrichment leg
57
+ report = triage_ioc(client, "8.8.8.8") # ioc + threat_report
58
+
59
+ # Audit + subdomains + tech + per-subdomain SSL (capped)
60
+ audit = audit_full(client, "example.com", ssl_subdomains=5)
61
+
62
+ # Auto-detect CVE vs IOC and bulk-route
63
+ enriched = enrich_batch(client, ["CVE-2021-44228", "8.8.8.8", "evil.com"])
64
+ ```
65
+
66
+ Shortcuts swallow per-leg `ContrastAPIError` so partial failures still return whatever succeeded — see `result["errors"]` for the failure map.
67
+
68
+ ## Authentication
69
+
70
+ Pass an API key as the first positional argument or as `api_key=`:
71
+
72
+ ```python
73
+ client = ContrastAPI("cc_<your-key>") # 1000 req/hr (Pro tier)
74
+ ```
75
+
76
+ Get a key at [contrastcyber.com/pricing](https://contrastcyber.com/pricing).
77
+
78
+ ## Exception model
79
+
80
+ The SDK maps server error codes (v1.22.2+ wire envelope) to typed exceptions:
81
+
82
+ | Exception | Status | Server code |
83
+ | --- | --- | --- |
84
+ | `InvalidArgumentError` | 400, 422 | `invalid_argument` |
85
+ | `AuthRequiredError` | 401 | `auth_required` |
86
+ | `TierLimitError` | 403 | `tier_limit` |
87
+ | `NotFoundError` | 404 | `not_found` |
88
+ | `RateLimitError` | 429 | `rate_limit_exceeded` |
89
+ | `UpstreamError` | 502 | `upstream_error` |
90
+ | `UpstreamTimeoutError` | 504 | `upstream_timeout` |
91
+ | `TransportError` | n/a | (network failure, before HTTP) |
92
+ | `ContrastAPIError` | * | base / unknown |
93
+
94
+ Every exception carries the parsed envelope:
95
+
96
+ ```python
97
+ from contrastapi import ContrastAPI, RateLimitError
98
+
99
+ try:
100
+ client.cve.lookup("CVE-2021-44228")
101
+ except RateLimitError as exc:
102
+ print(exc.message) # "Hourly limit reached"
103
+ print(exc.retry_after_seconds) # 60 (capped at 3600)
104
+ print(exc.upgrade_url) # "https://contrastcyber.com/pricing"
105
+ print(exc.extras) # back-compat top-level fields (tier, limit, ...)
106
+ ```
107
+
108
+ ## Namespaces
109
+
110
+ | Namespace | Methods |
111
+ | --- | --- |
112
+ | `cve` | `lookup`, `search`, `leading`, `kev`, `exploit`, `bulk` |
113
+ | `cwe` | `lookup` |
114
+ | `ioc` | `lookup`, `hash`, `phishing`, `bulk` |
115
+ | `atlas` | `technique`, `technique_search`, `bulk_technique_lookup`, `case_study`, `case_study_search` |
116
+ | `d3fend` | `defense`, `defense_search`, `defense_for_attack`, `coverage` |
117
+ | `domain` | `report`, `dns`, `whois`, `subdomains`, `certs`, `ssl`, `tech`, `threat`, `monitor`, `vulns`, `audit`, `wayback`, `bulk` |
118
+ | `ip` | `lookup`, `threat_report` |
119
+ | `asn` | `lookup` |
120
+ | `email` | `mx`, `disposable` |
121
+ | `phone` | `lookup` |
122
+ | `password` | `check` (k-anonymity SHA-1 prefix) |
123
+ | `username` | `lookup` |
124
+ | `check` | `secrets`, `injection`, `headers`, `dependencies` |
125
+ | `scan` | `headers` (live HTTP scan) |
126
+
127
+ The async client (`AsyncContrastAPI`) exposes the same namespace surface 1:1 — every method is `async def`.
128
+
129
+ ## Parity with the Node SDK
130
+
131
+ | Surface | Node SDK | Python SDK |
132
+ | --- | --- | --- |
133
+ | Sync | ✅ (Promise-based) | ✅ (`ContrastAPI`) |
134
+ | Async | (Promise model) | ✅ (`AsyncContrastAPI`) |
135
+ | Namespace count | 13 | 14 (adds `username`) |
136
+ | `bulk_technique_lookup` (ATLAS) | (added in v1.4.0) | ✅ |
137
+ | `wayback` archive lookup | (added in v1.4.0) | ✅ |
138
+ | Typed errors | `Error` subclasses | full hierarchy with envelope fields |
139
+ | Shortcuts | — | ✅ `triage_ioc`, `audit_full`, `enrich_batch` |
140
+ | Response models | `Promise<any>` | ✅ `TypedDict` (IDE autocomplete, no runtime cost) |
141
+
142
+ ## Configuration
143
+
144
+ ```python
145
+ client = ContrastAPI(
146
+ api_key="cc_...", # optional; keyless = free tier
147
+ base_url="https://api.contrastcyber.com", # override for self-host
148
+ timeout=30.0, # seconds; clamped to [1, 120]
149
+ allow_insecure=False, # set True to allow http:// (dev only)
150
+ )
151
+ ```
152
+
153
+ The transport hard-caps response bodies at 10 MB, sends a `User-Agent: contrastapi-python/<version>` header, and refuses to send your API key over plaintext HTTP even when `allow_insecure=True`.
154
+
155
+ ## Links
156
+
157
+ - API docs: https://api.contrastcyber.com/docs
158
+ - Pricing & API key: https://contrastcyber.com/pricing
159
+ - Source: https://github.com/UPinar/contrastapi
160
+ - Issues: https://github.com/UPinar/contrastapi/issues
161
+
162
+ ## License
163
+
164
+ MIT — see [LICENSE](../../LICENSE).
@@ -0,0 +1,39 @@
1
+ """ContrastAPI Python SDK.
2
+
3
+ >>> from contrastapi import ContrastAPI
4
+ >>> client = ContrastAPI()
5
+ >>> client.cve.lookup("CVE-2021-44228")
6
+ """
7
+
8
+ from ._version import __version__
9
+ from .client import AsyncContrastAPI, ContrastAPI
10
+ from .exceptions import (
11
+ AuthRequiredError,
12
+ ContrastAPIError,
13
+ InvalidArgumentError,
14
+ NotFoundError,
15
+ RateLimitError,
16
+ TierLimitError,
17
+ TransportError,
18
+ UpstreamError,
19
+ UpstreamTimeoutError,
20
+ )
21
+ from .shortcuts import audit_full, enrich_batch, triage_ioc
22
+
23
+ __all__ = [
24
+ "AsyncContrastAPI",
25
+ "AuthRequiredError",
26
+ "ContrastAPI",
27
+ "ContrastAPIError",
28
+ "InvalidArgumentError",
29
+ "NotFoundError",
30
+ "RateLimitError",
31
+ "TierLimitError",
32
+ "TransportError",
33
+ "UpstreamError",
34
+ "UpstreamTimeoutError",
35
+ "__version__",
36
+ "audit_full",
37
+ "enrich_batch",
38
+ "triage_ioc",
39
+ ]
@@ -0,0 +1,170 @@
1
+ """HTTP transport for ContrastAPI SDK — sync (httpx.Client) and async (httpx.AsyncClient).
2
+
3
+ Both transports share:
4
+ * Base URL + API key handling (HTTPS-only unless allow_insecure=True).
5
+ * Error envelope parsing via exceptions._parse_error.
6
+ * Response size cap (10 MB) to mirror Node SDK behaviour.
7
+ * Identical User-Agent (`contrastapi-python/<version>`).
8
+
9
+ Design note: keeping sync and async classes side-by-side in one module avoids
10
+ the duplicated-stub problem and makes the parity guarantee obvious. Namespace
11
+ classes accept the transport instance and call `transport.get(...)` /
12
+ `transport.post(...)` (or `aget` / `apost` for the async variant) — the rest is
13
+ just URL construction.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from typing import Any
20
+ from urllib.parse import urlsplit
21
+
22
+ import httpx
23
+
24
+ from ._version import __version__
25
+ from .exceptions import ContrastAPIError, TransportError, _parse_error
26
+
27
+ DEFAULT_BASE_URL = "https://api.contrastcyber.com"
28
+ DEFAULT_TIMEOUT = 30.0
29
+ MIN_TIMEOUT = 1.0
30
+ MAX_TIMEOUT = 120.0
31
+ MAX_RESPONSE_BYTES = 10 * 1024 * 1024 # 10 MB — parity with Node SDK
32
+ USER_AGENT = f"contrastapi-python/{__version__}"
33
+
34
+
35
+ def _normalize_base_url(base_url: str, *, allow_insecure: bool) -> str:
36
+ parts = urlsplit(base_url)
37
+ if parts.scheme not in ("https", "http"):
38
+ raise ValueError(f"Unsupported scheme: {parts.scheme!r}")
39
+ if parts.scheme == "http" and not allow_insecure:
40
+ raise ValueError("Only HTTPS is allowed. Pass allow_insecure=True to override.")
41
+ return base_url.rstrip("/")
42
+
43
+
44
+ def _build_headers(api_key: str | None, base_scheme: str, has_body: bool) -> dict[str, str]:
45
+ headers = {"Accept": "application/json", "User-Agent": USER_AGENT}
46
+ if api_key:
47
+ if base_scheme != "https":
48
+ raise ValueError("Refusing to send API key over insecure connection")
49
+ headers["X-API-Key"] = api_key
50
+ if has_body:
51
+ headers["Content-Type"] = "application/json"
52
+ return headers
53
+
54
+
55
+ def _clamp_timeout(value: float | None) -> float:
56
+ if value is None:
57
+ return DEFAULT_TIMEOUT
58
+ return max(MIN_TIMEOUT, min(float(value), MAX_TIMEOUT))
59
+
60
+
61
+ def _decode_response(response: httpx.Response) -> Any:
62
+ """Parse JSON body with size cap. Raises ContrastAPIError on bad JSON or oversize."""
63
+ content = response.content
64
+ if len(content) > MAX_RESPONSE_BYTES:
65
+ raise ContrastAPIError(
66
+ f"Response too large ({len(content)} bytes > {MAX_RESPONSE_BYTES})",
67
+ status_code=response.status_code,
68
+ )
69
+ try:
70
+ return json.loads(content) if content else None
71
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
72
+ raise ContrastAPIError(
73
+ f"Invalid JSON response (HTTP {response.status_code}): {exc}",
74
+ status_code=response.status_code,
75
+ ) from exc
76
+
77
+
78
+ def _handle_response(response: httpx.Response) -> Any:
79
+ body = _decode_response(response)
80
+ if response.status_code >= 400:
81
+ raise _parse_error(response.status_code, body)
82
+ return body
83
+
84
+
85
+ class _SyncTransport:
86
+ def __init__(
87
+ self,
88
+ *,
89
+ base_url: str = DEFAULT_BASE_URL,
90
+ api_key: str | None = None,
91
+ timeout: float | None = None,
92
+ allow_insecure: bool = False,
93
+ client: httpx.Client | None = None,
94
+ ) -> None:
95
+ self.base_url = _normalize_base_url(base_url, allow_insecure=allow_insecure)
96
+ self._scheme = urlsplit(self.base_url).scheme
97
+ self.api_key = api_key
98
+ self.timeout = _clamp_timeout(timeout)
99
+ self._owns_client = client is None
100
+ self._client = client or httpx.Client(timeout=self.timeout)
101
+
102
+ def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
103
+ headers = _build_headers(self.api_key, self._scheme, has_body=False)
104
+ try:
105
+ response = self._client.get(self.base_url + path, params=params, headers=headers)
106
+ except httpx.HTTPError as exc:
107
+ raise TransportError(str(exc)) from exc
108
+ return _handle_response(response)
109
+
110
+ def post(self, path: str, *, json_body: Any | None = None) -> Any:
111
+ headers = _build_headers(self.api_key, self._scheme, has_body=json_body is not None)
112
+ try:
113
+ response = self._client.post(self.base_url + path, json=json_body, headers=headers)
114
+ except httpx.HTTPError as exc:
115
+ raise TransportError(str(exc)) from exc
116
+ return _handle_response(response)
117
+
118
+ def close(self) -> None:
119
+ if self._owns_client:
120
+ self._client.close()
121
+
122
+ def __enter__(self) -> _SyncTransport:
123
+ return self
124
+
125
+ def __exit__(self, *exc_info: Any) -> None:
126
+ self.close()
127
+
128
+
129
+ class _AsyncTransport:
130
+ def __init__(
131
+ self,
132
+ *,
133
+ base_url: str = DEFAULT_BASE_URL,
134
+ api_key: str | None = None,
135
+ timeout: float | None = None,
136
+ allow_insecure: bool = False,
137
+ client: httpx.AsyncClient | None = None,
138
+ ) -> None:
139
+ self.base_url = _normalize_base_url(base_url, allow_insecure=allow_insecure)
140
+ self._scheme = urlsplit(self.base_url).scheme
141
+ self.api_key = api_key
142
+ self.timeout = _clamp_timeout(timeout)
143
+ self._owns_client = client is None
144
+ self._client = client or httpx.AsyncClient(timeout=self.timeout)
145
+
146
+ async def aget(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
147
+ headers = _build_headers(self.api_key, self._scheme, has_body=False)
148
+ try:
149
+ response = await self._client.get(self.base_url + path, params=params, headers=headers)
150
+ except httpx.HTTPError as exc:
151
+ raise TransportError(str(exc)) from exc
152
+ return _handle_response(response)
153
+
154
+ async def apost(self, path: str, *, json_body: Any | None = None) -> Any:
155
+ headers = _build_headers(self.api_key, self._scheme, has_body=json_body is not None)
156
+ try:
157
+ response = await self._client.post(self.base_url + path, json=json_body, headers=headers)
158
+ except httpx.HTTPError as exc:
159
+ raise TransportError(str(exc)) from exc
160
+ return _handle_response(response)
161
+
162
+ async def aclose(self) -> None:
163
+ if self._owns_client:
164
+ await self._client.aclose()
165
+
166
+ async def __aenter__(self) -> _AsyncTransport:
167
+ return self
168
+
169
+ async def __aexit__(self, *exc_info: Any) -> None:
170
+ await self.aclose()
@@ -0,0 +1 @@
1
+ __version__ = "1.22.3"