contrastapi 1.23.0__tar.gz → 2.0.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.
- contrastapi-2.0.0/.gitignore +8 -0
- contrastapi-2.0.0/LICENSE +21 -0
- contrastapi-2.0.0/PKG-INFO +233 -0
- contrastapi-2.0.0/README.md +203 -0
- contrastapi-2.0.0/contrastapi/__init__.py +32 -0
- contrastapi-2.0.0/contrastapi/_client.py +649 -0
- contrastapi-2.0.0/contrastapi/_operations.py +2208 -0
- contrastapi-2.0.0/contrastapi/_version.py +1 -0
- contrastapi-2.0.0/contrastapi/exceptions.py +131 -0
- {contrastapi-1.23.0 → contrastapi-2.0.0}/pyproject.toml +11 -8
- contrastapi-2.0.0/tests/conftest.py +157 -0
- contrastapi-2.0.0/tests/test_operations.py +198 -0
- contrastapi-2.0.0/tests/test_parity.py +580 -0
- contrastapi-2.0.0/tests/test_request_shape.py +283 -0
- contrastapi-2.0.0/tests/test_responses.py +229 -0
- contrastapi-2.0.0/tests/test_retry.py +294 -0
- contrastapi-1.23.0/.gitignore +0 -34
- contrastapi-1.23.0/PKG-INFO +0 -195
- contrastapi-1.23.0/README.md +0 -165
- contrastapi-1.23.0/contrastapi/__init__.py +0 -39
- contrastapi-1.23.0/contrastapi/_transport.py +0 -170
- contrastapi-1.23.0/contrastapi/_version.py +0 -1
- contrastapi-1.23.0/contrastapi/client.py +0 -174
- contrastapi-1.23.0/contrastapi/exceptions.py +0 -177
- contrastapi-1.23.0/contrastapi/models.py +0 -718
- contrastapi-1.23.0/contrastapi/namespaces/atlas.py +0 -134
- contrastapi-1.23.0/contrastapi/namespaces/cve.py +0 -143
- contrastapi-1.23.0/contrastapi/namespaces/d3fend.py +0 -124
- contrastapi-1.23.0/contrastapi/namespaces/domain.py +0 -159
- contrastapi-1.23.0/contrastapi/namespaces/intel.py +0 -166
- contrastapi-1.23.0/contrastapi/namespaces/ioc.py +0 -61
- contrastapi-1.23.0/contrastapi/namespaces/security.py +0 -94
- contrastapi-1.23.0/contrastapi/namespaces/sigma.py +0 -42
- contrastapi-1.23.0/contrastapi/shortcuts.py +0 -182
- contrastapi-1.23.0/tests/__init__.py +0 -0
- contrastapi-1.23.0/tests/conftest.py +0 -15
- contrastapi-1.23.0/tests/test_namespaces.py +0 -325
- contrastapi-1.23.0/tests/test_remaining_namespaces.py +0 -330
- contrastapi-1.23.0/tests/test_review_fixes.py +0 -177
- contrastapi-1.23.0/tests/test_review_round2_fixes.py +0 -371
- contrastapi-1.23.0/tests/test_shortcuts.py +0 -274
- contrastapi-1.23.0/tests/test_transport.py +0 -289
- contrastapi-1.23.0/tests/test_v1_23_endpoints.py +0 -152
- /contrastapi-1.23.0/contrastapi/namespaces/__init__.py → /contrastapi-2.0.0/contrastapi/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UPinar
|
|
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,233 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: contrastapi
|
|
3
|
+
Version: 2.0.0
|
|
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/quickstart
|
|
7
|
+
Project-URL: Repository, https://github.com/UPinar/contrastapi-clients/tree/master/sdk/python
|
|
8
|
+
Project-URL: Issues, https://github.com/UPinar/contrastapi-clients/issues
|
|
9
|
+
Author-email: UPinar <contact@contrastcyber.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-security,api,attack,cve,cwe,d3fend,dns,domain,kev,mcp,mitre,osint,security,ssl,threat-intelligence,vulnerability,whois
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
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: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx<1.0,>=0.25
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# ContrastAPI Python SDK
|
|
32
|
+
|
|
33
|
+
Official Python SDK for [ContrastAPI](https://api.contrastcyber.com), security intelligence for developers and AI agents.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install contrastapi
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It needs Python 3.10 or newer. The only dependency is `httpx>=0.25,<1.0`.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from contrastapi import ContrastAPI
|
|
47
|
+
|
|
48
|
+
with ContrastAPI() as client:
|
|
49
|
+
# A typed method for one operation of the REST API.
|
|
50
|
+
cve = client.cve_lookup(cve_id="CVE-2021-44228")
|
|
51
|
+
|
|
52
|
+
# The same operation by its id.
|
|
53
|
+
same = client.call("cve_lookup", {"cve_id": "CVE-2021-44228"})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each method takes keyword arguments named like the parameters of the REST API.
|
|
57
|
+
The client checks the names and the required values before it sends anything.
|
|
58
|
+
A call with an unknown name or a missing required value fails with `TypeError` and spends no credit.
|
|
59
|
+
A method returns the parsed JSON body.
|
|
60
|
+
|
|
61
|
+
The methods and their argument types are generated from the OpenAPI document of the server.
|
|
62
|
+
The package is typed (`py.typed`).
|
|
63
|
+
|
|
64
|
+
### Raw requests
|
|
65
|
+
|
|
66
|
+
`request()` sends a request to any path. It uses the same key policy, errors, retries and credit reading as the methods.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
health = client.request("GET", "/health")
|
|
70
|
+
found = client.request("GET", "/cve_search", query={"product": "apache"})
|
|
71
|
+
bulk = client.request("POST", "/cve_lookup_bulk", body={"cve_ids": ["CVE-2021-44228"]})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`request()` sends a JSON body or none. It cannot send raw bytes.
|
|
75
|
+
|
|
76
|
+
### Not in the SDK
|
|
77
|
+
|
|
78
|
+
`/image_inspect` takes a raw image body (`application/octet-stream`). The SDK sends JSON bodies only, so it has no method for it, and `request()` is no way around that. Call that endpoint with `httpx` or another HTTP client.
|
|
79
|
+
|
|
80
|
+
### Async
|
|
81
|
+
|
|
82
|
+
`AsyncContrastAPI` has the same operation methods, `call()` and `request()`. Each one is a coroutine.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import asyncio
|
|
86
|
+
from contrastapi import AsyncContrastAPI
|
|
87
|
+
|
|
88
|
+
async def main():
|
|
89
|
+
async with AsyncContrastAPI() as client:
|
|
90
|
+
cve = await client.cve_lookup(cve_id="CVE-2021-44228")
|
|
91
|
+
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Close a client with `close()` (sync) or `await aclose()` (async), or use `with` / `async with`.
|
|
96
|
+
|
|
97
|
+
## API key
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
client = ContrastAPI(api_key="your-api-key")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Without `api_key`, the client reads the `CONTRASTAPI_API_KEY` environment variable and trims ASCII white space from both ends. An empty string sends no key.
|
|
104
|
+
A key may hold only printable ASCII without spaces; any other key raises `ValueError`, and the message does not repeat it.
|
|
105
|
+
The client keeps the key only when it will send it.
|
|
106
|
+
|
|
107
|
+
The client sends the key in the `X-API-Key` header. It sends it only to `api.contrastcyber.com`, `localhost`, `127.0.0.1` and `[::1]`. For any other host, pass `trust_host=True`.
|
|
108
|
+
|
|
109
|
+
`base_url` must use https. Plain http is accepted only for `localhost`, `127.0.0.1` and `[::1]`, written that way.
|
|
110
|
+
`base_url` must not hold user info, a query, a fragment, a backslash, a space or an ASCII control character.
|
|
111
|
+
Its path must not hold a `.` or `..` segment.
|
|
112
|
+
The client does not follow redirects: any 3xx answer fails the call with `APIConnectionError("redirect refused")`.
|
|
113
|
+
Before each request, the client checks that the URL keeps the scheme, host and port of `base_url` and stays under its path.
|
|
114
|
+
A URL that leaves it raises `ValueError` and sends nothing.
|
|
115
|
+
A request path with a `.` or `..` segment also raises `ValueError` and sends nothing.
|
|
116
|
+
This covers a raw path and a path value of a method, such as `cve_id=".."`.
|
|
117
|
+
In a raw path, a dot written as `%2e` or `%2E` counts as a dot.
|
|
118
|
+
A method encodes `%` in a path value, so a value of `%2e` is sent.
|
|
119
|
+
The query is not checked.
|
|
120
|
+
The key is not shown in `repr()` of the client.
|
|
121
|
+
|
|
122
|
+
Get a key at [api.contrastcyber.com/pricing](https://api.contrastcyber.com/pricing).
|
|
123
|
+
Without a key, each client address has a smaller hourly credit budget than a key has. The pricing page lists both limits.
|
|
124
|
+
|
|
125
|
+
## Credits
|
|
126
|
+
|
|
127
|
+
Each operation has a price in credits, and some operations are free. The docstring of an operation method states its price.
|
|
128
|
+
|
|
129
|
+
After each response that carries the rate-limit headers, the client keeps the reading:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
client.cve_lookup(cve_id="CVE-2021-44228")
|
|
133
|
+
info = client.last_rate_limit
|
|
134
|
+
if info is not None:
|
|
135
|
+
print(info.remaining, info.reset_human)
|
|
136
|
+
|
|
137
|
+
watched = ContrastAPI(on_rate_limit=lambda info: print(info.remaining, "of", info.limit, "credits left"))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`RateLimitInfo` is a frozen dataclass with `limit`, `remaining`, `reset_at` (a UTC `datetime`) and `reset_human`.
|
|
141
|
+
`last_rate_limit` is `None` until a response has carried the headers.
|
|
142
|
+
An exception that `on_rate_limit` raises is ignored.
|
|
143
|
+
|
|
144
|
+
## Errors
|
|
145
|
+
|
|
146
|
+
Every error extends `ContrastAPIError`. The server answers an error as `{"error": {"code", "message"}}`, and the client reads both.
|
|
147
|
+
|
|
148
|
+
| Class | When |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `BadRequestError` | HTTP 400 |
|
|
151
|
+
| `AuthenticationError` | HTTP 401 |
|
|
152
|
+
| `NotFoundError` | HTTP 404 |
|
|
153
|
+
| `RateLimitError` | HTTP 429 |
|
|
154
|
+
| `UpstreamError` | HTTP 502, 503 or 504 |
|
|
155
|
+
| `ContrastAPIError` | any other status outside 200-399; a 2xx body that is not a UTF-8 JSON object or array; a body over 16 MiB; an answer whose `Content-Encoding` is not `identity` |
|
|
156
|
+
| `APIConnectionError` | no usable answer: network failure or refused redirect (`status` is `None`) |
|
|
157
|
+
| `APITimeoutError` | `ContrastAPI`: a read ran past `timeout`, or the body was still arriving `timeout` seconds after the request started. `AsyncContrastAPI`: the whole exchange ran past `timeout`. It extends `APIConnectionError` |
|
|
158
|
+
|
|
159
|
+
An error has these attributes: `message`, `status`, `code`, `retry_after` (seconds from the `Retry-After` header, at most 3600, or `None`) and `rate_limit` (a `RateLimitInfo` or `None`).
|
|
160
|
+
The message, the code and `rate_limit.reset_human` are stripped of control characters (C0, DEL and C1), format characters (Unicode Cf, such as U+202E) and lone surrogates. The message is cut to 300 characters.
|
|
161
|
+
An error holds neither the request nor the response, and its text never repeats the key.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from contrastapi import NotFoundError, RateLimitError
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
client.cve_lookup(cve_id="CVE-9999-0000")
|
|
168
|
+
except NotFoundError as err:
|
|
169
|
+
print(err.status, err.code, err.message)
|
|
170
|
+
except RateLimitError as err:
|
|
171
|
+
print("retry after", err.retry_after)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A wrong argument raises `TypeError` (wrong type) or `ValueError` (wrong value) before any request.
|
|
175
|
+
A path or query value must be a `str`, an `int`, a finite `float` or a `bool`; a float is written as JavaScript writes it (`7.0` is `7`). A query value of `None` is left out.
|
|
176
|
+
A body is written with `json.dumps(allow_nan=False)` before any request. A NaN or an infinity raises `ValueError`.
|
|
177
|
+
A value that JSON has no form for raises `TypeError`. Examples are a `datetime`, a `set`, `bytes` and a `Decimal`; write a `datetime` as an ISO string yourself.
|
|
178
|
+
A tuple is written as a list.
|
|
179
|
+
A body argument of `None` is left out, like a query value. A `None` nested inside a body value is written as `null`.
|
|
180
|
+
|
|
181
|
+
## Retries
|
|
182
|
+
|
|
183
|
+
| Option | Default | Meaning |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `upstream_busy_retries` | 2 | retries after a 503 with the code `upstream_busy` or `raw_body_busy` |
|
|
186
|
+
| `max_retries` | 0 | retries after a 429; off unless you set it |
|
|
187
|
+
| `max_retry_wait` | 60.0 | a finite number >= 0; the client does not retry when the wait is longer than this many seconds |
|
|
188
|
+
|
|
189
|
+
The wait is the `Retry-After` header, in seconds. A busy 503 without the header waits one second. A 429 without the header is not retried.
|
|
190
|
+
|
|
191
|
+
## Configuration
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
client = ContrastAPI(
|
|
195
|
+
api_key="...", # default: CONTRASTAPI_API_KEY
|
|
196
|
+
base_url="https://api.contrastcyber.com", # default
|
|
197
|
+
trust_host=False, # True sends the key to other hosts
|
|
198
|
+
timeout=30.0, # seconds; default; clamped to 1.0-120.0
|
|
199
|
+
max_retries=0,
|
|
200
|
+
upstream_busy_retries=2,
|
|
201
|
+
max_retry_wait=60.0,
|
|
202
|
+
on_rate_limit=None,
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The client sends `User-Agent: contrastapi-python/<version>`.
|
|
207
|
+
|
|
208
|
+
The client sends `Accept-Encoding: identity`. It refuses an answer with any other `Content-Encoding` and does not read its body.
|
|
209
|
+
It reads the raw bytes, so httpx decodes nothing.
|
|
210
|
+
|
|
211
|
+
In `ContrastAPI`, `timeout` bounds each read, not the header phase as a whole. A server that sends its headers slowly, each read within `timeout`, can hold a call longer.
|
|
212
|
+
`AsyncContrastAPI` bounds the whole exchange by `timeout`, the headers included.
|
|
213
|
+
|
|
214
|
+
The client uses httpx's default `trust_env=True`, so proxy environment variables (`HTTPS_PROXY` and the like) apply.
|
|
215
|
+
|
|
216
|
+
## Regenerating the methods
|
|
217
|
+
|
|
218
|
+
The methods come from `sdk/openapi.json`, the output of `make -s openapi` in the server repository.
|
|
219
|
+
After a new document, run this from the root of the repository that holds `sdk/`:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
python3 sdk/gen/gen_sdk.py # rewrite the generated files
|
|
223
|
+
python3 sdk/gen/gen_sdk.py --check # report a difference, exit 1
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Upgrading from 1.x
|
|
227
|
+
|
|
228
|
+
Version 1.x called `/v1/...` paths. The OpenAPI document of the current server has no such path.
|
|
229
|
+
Version 2.0.0 is a rewrite. The [CHANGELOG](https://github.com/UPinar/contrastapi-clients/blob/master/sdk/python/CHANGELOG.md) in the source repository, [UPinar/contrastapi-clients](https://github.com/UPinar/contrastapi-clients), lists the breaking changes.
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT. The LICENSE file is part of the package.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# ContrastAPI Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [ContrastAPI](https://api.contrastcyber.com), security intelligence for developers and AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install contrastapi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It needs Python 3.10 or newer. The only dependency is `httpx>=0.25,<1.0`.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from contrastapi import ContrastAPI
|
|
17
|
+
|
|
18
|
+
with ContrastAPI() as client:
|
|
19
|
+
# A typed method for one operation of the REST API.
|
|
20
|
+
cve = client.cve_lookup(cve_id="CVE-2021-44228")
|
|
21
|
+
|
|
22
|
+
# The same operation by its id.
|
|
23
|
+
same = client.call("cve_lookup", {"cve_id": "CVE-2021-44228"})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each method takes keyword arguments named like the parameters of the REST API.
|
|
27
|
+
The client checks the names and the required values before it sends anything.
|
|
28
|
+
A call with an unknown name or a missing required value fails with `TypeError` and spends no credit.
|
|
29
|
+
A method returns the parsed JSON body.
|
|
30
|
+
|
|
31
|
+
The methods and their argument types are generated from the OpenAPI document of the server.
|
|
32
|
+
The package is typed (`py.typed`).
|
|
33
|
+
|
|
34
|
+
### Raw requests
|
|
35
|
+
|
|
36
|
+
`request()` sends a request to any path. It uses the same key policy, errors, retries and credit reading as the methods.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
health = client.request("GET", "/health")
|
|
40
|
+
found = client.request("GET", "/cve_search", query={"product": "apache"})
|
|
41
|
+
bulk = client.request("POST", "/cve_lookup_bulk", body={"cve_ids": ["CVE-2021-44228"]})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`request()` sends a JSON body or none. It cannot send raw bytes.
|
|
45
|
+
|
|
46
|
+
### Not in the SDK
|
|
47
|
+
|
|
48
|
+
`/image_inspect` takes a raw image body (`application/octet-stream`). The SDK sends JSON bodies only, so it has no method for it, and `request()` is no way around that. Call that endpoint with `httpx` or another HTTP client.
|
|
49
|
+
|
|
50
|
+
### Async
|
|
51
|
+
|
|
52
|
+
`AsyncContrastAPI` has the same operation methods, `call()` and `request()`. Each one is a coroutine.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
from contrastapi import AsyncContrastAPI
|
|
57
|
+
|
|
58
|
+
async def main():
|
|
59
|
+
async with AsyncContrastAPI() as client:
|
|
60
|
+
cve = await client.cve_lookup(cve_id="CVE-2021-44228")
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Close a client with `close()` (sync) or `await aclose()` (async), or use `with` / `async with`.
|
|
66
|
+
|
|
67
|
+
## API key
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
client = ContrastAPI(api_key="your-api-key")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Without `api_key`, the client reads the `CONTRASTAPI_API_KEY` environment variable and trims ASCII white space from both ends. An empty string sends no key.
|
|
74
|
+
A key may hold only printable ASCII without spaces; any other key raises `ValueError`, and the message does not repeat it.
|
|
75
|
+
The client keeps the key only when it will send it.
|
|
76
|
+
|
|
77
|
+
The client sends the key in the `X-API-Key` header. It sends it only to `api.contrastcyber.com`, `localhost`, `127.0.0.1` and `[::1]`. For any other host, pass `trust_host=True`.
|
|
78
|
+
|
|
79
|
+
`base_url` must use https. Plain http is accepted only for `localhost`, `127.0.0.1` and `[::1]`, written that way.
|
|
80
|
+
`base_url` must not hold user info, a query, a fragment, a backslash, a space or an ASCII control character.
|
|
81
|
+
Its path must not hold a `.` or `..` segment.
|
|
82
|
+
The client does not follow redirects: any 3xx answer fails the call with `APIConnectionError("redirect refused")`.
|
|
83
|
+
Before each request, the client checks that the URL keeps the scheme, host and port of `base_url` and stays under its path.
|
|
84
|
+
A URL that leaves it raises `ValueError` and sends nothing.
|
|
85
|
+
A request path with a `.` or `..` segment also raises `ValueError` and sends nothing.
|
|
86
|
+
This covers a raw path and a path value of a method, such as `cve_id=".."`.
|
|
87
|
+
In a raw path, a dot written as `%2e` or `%2E` counts as a dot.
|
|
88
|
+
A method encodes `%` in a path value, so a value of `%2e` is sent.
|
|
89
|
+
The query is not checked.
|
|
90
|
+
The key is not shown in `repr()` of the client.
|
|
91
|
+
|
|
92
|
+
Get a key at [api.contrastcyber.com/pricing](https://api.contrastcyber.com/pricing).
|
|
93
|
+
Without a key, each client address has a smaller hourly credit budget than a key has. The pricing page lists both limits.
|
|
94
|
+
|
|
95
|
+
## Credits
|
|
96
|
+
|
|
97
|
+
Each operation has a price in credits, and some operations are free. The docstring of an operation method states its price.
|
|
98
|
+
|
|
99
|
+
After each response that carries the rate-limit headers, the client keeps the reading:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
client.cve_lookup(cve_id="CVE-2021-44228")
|
|
103
|
+
info = client.last_rate_limit
|
|
104
|
+
if info is not None:
|
|
105
|
+
print(info.remaining, info.reset_human)
|
|
106
|
+
|
|
107
|
+
watched = ContrastAPI(on_rate_limit=lambda info: print(info.remaining, "of", info.limit, "credits left"))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`RateLimitInfo` is a frozen dataclass with `limit`, `remaining`, `reset_at` (a UTC `datetime`) and `reset_human`.
|
|
111
|
+
`last_rate_limit` is `None` until a response has carried the headers.
|
|
112
|
+
An exception that `on_rate_limit` raises is ignored.
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
Every error extends `ContrastAPIError`. The server answers an error as `{"error": {"code", "message"}}`, and the client reads both.
|
|
117
|
+
|
|
118
|
+
| Class | When |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `BadRequestError` | HTTP 400 |
|
|
121
|
+
| `AuthenticationError` | HTTP 401 |
|
|
122
|
+
| `NotFoundError` | HTTP 404 |
|
|
123
|
+
| `RateLimitError` | HTTP 429 |
|
|
124
|
+
| `UpstreamError` | HTTP 502, 503 or 504 |
|
|
125
|
+
| `ContrastAPIError` | any other status outside 200-399; a 2xx body that is not a UTF-8 JSON object or array; a body over 16 MiB; an answer whose `Content-Encoding` is not `identity` |
|
|
126
|
+
| `APIConnectionError` | no usable answer: network failure or refused redirect (`status` is `None`) |
|
|
127
|
+
| `APITimeoutError` | `ContrastAPI`: a read ran past `timeout`, or the body was still arriving `timeout` seconds after the request started. `AsyncContrastAPI`: the whole exchange ran past `timeout`. It extends `APIConnectionError` |
|
|
128
|
+
|
|
129
|
+
An error has these attributes: `message`, `status`, `code`, `retry_after` (seconds from the `Retry-After` header, at most 3600, or `None`) and `rate_limit` (a `RateLimitInfo` or `None`).
|
|
130
|
+
The message, the code and `rate_limit.reset_human` are stripped of control characters (C0, DEL and C1), format characters (Unicode Cf, such as U+202E) and lone surrogates. The message is cut to 300 characters.
|
|
131
|
+
An error holds neither the request nor the response, and its text never repeats the key.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from contrastapi import NotFoundError, RateLimitError
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
client.cve_lookup(cve_id="CVE-9999-0000")
|
|
138
|
+
except NotFoundError as err:
|
|
139
|
+
print(err.status, err.code, err.message)
|
|
140
|
+
except RateLimitError as err:
|
|
141
|
+
print("retry after", err.retry_after)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A wrong argument raises `TypeError` (wrong type) or `ValueError` (wrong value) before any request.
|
|
145
|
+
A path or query value must be a `str`, an `int`, a finite `float` or a `bool`; a float is written as JavaScript writes it (`7.0` is `7`). A query value of `None` is left out.
|
|
146
|
+
A body is written with `json.dumps(allow_nan=False)` before any request. A NaN or an infinity raises `ValueError`.
|
|
147
|
+
A value that JSON has no form for raises `TypeError`. Examples are a `datetime`, a `set`, `bytes` and a `Decimal`; write a `datetime` as an ISO string yourself.
|
|
148
|
+
A tuple is written as a list.
|
|
149
|
+
A body argument of `None` is left out, like a query value. A `None` nested inside a body value is written as `null`.
|
|
150
|
+
|
|
151
|
+
## Retries
|
|
152
|
+
|
|
153
|
+
| Option | Default | Meaning |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `upstream_busy_retries` | 2 | retries after a 503 with the code `upstream_busy` or `raw_body_busy` |
|
|
156
|
+
| `max_retries` | 0 | retries after a 429; off unless you set it |
|
|
157
|
+
| `max_retry_wait` | 60.0 | a finite number >= 0; the client does not retry when the wait is longer than this many seconds |
|
|
158
|
+
|
|
159
|
+
The wait is the `Retry-After` header, in seconds. A busy 503 without the header waits one second. A 429 without the header is not retried.
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
client = ContrastAPI(
|
|
165
|
+
api_key="...", # default: CONTRASTAPI_API_KEY
|
|
166
|
+
base_url="https://api.contrastcyber.com", # default
|
|
167
|
+
trust_host=False, # True sends the key to other hosts
|
|
168
|
+
timeout=30.0, # seconds; default; clamped to 1.0-120.0
|
|
169
|
+
max_retries=0,
|
|
170
|
+
upstream_busy_retries=2,
|
|
171
|
+
max_retry_wait=60.0,
|
|
172
|
+
on_rate_limit=None,
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The client sends `User-Agent: contrastapi-python/<version>`.
|
|
177
|
+
|
|
178
|
+
The client sends `Accept-Encoding: identity`. It refuses an answer with any other `Content-Encoding` and does not read its body.
|
|
179
|
+
It reads the raw bytes, so httpx decodes nothing.
|
|
180
|
+
|
|
181
|
+
In `ContrastAPI`, `timeout` bounds each read, not the header phase as a whole. A server that sends its headers slowly, each read within `timeout`, can hold a call longer.
|
|
182
|
+
`AsyncContrastAPI` bounds the whole exchange by `timeout`, the headers included.
|
|
183
|
+
|
|
184
|
+
The client uses httpx's default `trust_env=True`, so proxy environment variables (`HTTPS_PROXY` and the like) apply.
|
|
185
|
+
|
|
186
|
+
## Regenerating the methods
|
|
187
|
+
|
|
188
|
+
The methods come from `sdk/openapi.json`, the output of `make -s openapi` in the server repository.
|
|
189
|
+
After a new document, run this from the root of the repository that holds `sdk/`:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
python3 sdk/gen/gen_sdk.py # rewrite the generated files
|
|
193
|
+
python3 sdk/gen/gen_sdk.py --check # report a difference, exit 1
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Upgrading from 1.x
|
|
197
|
+
|
|
198
|
+
Version 1.x called `/v1/...` paths. The OpenAPI document of the current server has no such path.
|
|
199
|
+
Version 2.0.0 is a rewrite. The [CHANGELOG](https://github.com/UPinar/contrastapi-clients/blob/master/sdk/python/CHANGELOG.md) in the source repository, [UPinar/contrastapi-clients](https://github.com/UPinar/contrastapi-clients), lists the breaking changes.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT. The LICENSE file is part of the package.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Official Python SDK for ContrastAPI."""
|
|
2
|
+
|
|
3
|
+
from ._client import AsyncContrastAPI, ContrastAPI, RateLimitInfo
|
|
4
|
+
from ._operations import PackageItem, PackageItemOptionalVersion
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
APIConnectionError,
|
|
8
|
+
APITimeoutError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
BadRequestError,
|
|
11
|
+
ContrastAPIError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
UpstreamError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"APIConnectionError",
|
|
19
|
+
"APITimeoutError",
|
|
20
|
+
"AsyncContrastAPI",
|
|
21
|
+
"AuthenticationError",
|
|
22
|
+
"BadRequestError",
|
|
23
|
+
"ContrastAPI",
|
|
24
|
+
"ContrastAPIError",
|
|
25
|
+
"NotFoundError",
|
|
26
|
+
"PackageItem",
|
|
27
|
+
"PackageItemOptionalVersion",
|
|
28
|
+
"RateLimitError",
|
|
29
|
+
"RateLimitInfo",
|
|
30
|
+
"UpstreamError",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|