blacksands-bursar 0.2.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.
- blacksands_bursar-0.2.0/PKG-INFO +210 -0
- blacksands_bursar-0.2.0/README.md +197 -0
- blacksands_bursar-0.2.0/pyproject.toml +14 -0
- blacksands_bursar-0.2.0/setup.cfg +4 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/__init__.py +21 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/client.py +212 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/exceptions.py +30 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/__init__.py +29 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/apps.py +35 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/base.py +20 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/certs.py +23 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/compliance.py +21 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/dns.py +25 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/endpoints.py +37 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/manifests.py +22 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/operations.py +45 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/orgs.py +22 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/policies.py +37 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/sessions.py +16 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar/resources/verify.py +16 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar.egg-info/PKG-INFO +210 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar.egg-info/SOURCES.txt +24 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar.egg-info/dependency_links.txt +1 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar.egg-info/requires.txt +6 -0
- blacksands_bursar-0.2.0/src/blacksands_bursar.egg-info/top_level.txt +1 -0
- blacksands_bursar-0.2.0/tests/test_resources.py +402 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blacksands-bursar
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for the Blacksands Bursar API
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.28.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
12
|
+
Requires-Dist: responses>=0.23; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# Blacksands Bursar Python SDK
|
|
15
|
+
|
|
16
|
+
Python client for the Blacksands Bursar API -- zero-trust network security provisioning, certificate management, compliance, and posture verification.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install blacksands-shield
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For development:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from blacksands_shield import ShieldClient
|
|
34
|
+
|
|
35
|
+
client = ShieldClient(
|
|
36
|
+
client_cert="/path/to/client.crt",
|
|
37
|
+
client_key="/path/to/client.key",
|
|
38
|
+
ca_cert="/path/to/blacksands-ca.crt", # optional
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Create an organisation
|
|
42
|
+
org = client.orgs.create("Acme Corp", "admin@acme.com", tier="professional")
|
|
43
|
+
org_id = org["data"]["id"]
|
|
44
|
+
|
|
45
|
+
# Register an application
|
|
46
|
+
app = client.apps.register(org_id, "web-frontend", framework="react")
|
|
47
|
+
app_id = app["data"]["id"]
|
|
48
|
+
|
|
49
|
+
# Generate a certificate
|
|
50
|
+
cert = client.certs.generate(app_id)
|
|
51
|
+
|
|
52
|
+
# Provision an endpoint
|
|
53
|
+
ep = client.endpoints.provision(app_id, "ingress", "api.acme.com", 443)
|
|
54
|
+
|
|
55
|
+
# Run a verification scan and wait for results
|
|
56
|
+
scan = client.verify.scan(app_id)
|
|
57
|
+
result = client.operations.poll_until_complete(scan["data"]["operationId"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Authentication
|
|
61
|
+
|
|
62
|
+
The Shield SDK uses mTLS client certificates exclusively. API-key authentication is not supported. An mTLS certificate bundle is issued by a Blacksands administrator through Overwatch or SysAdmin and delivered to the caller out-of-band.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
client = ShieldClient(
|
|
66
|
+
client_cert="/path/to/client.crt",
|
|
67
|
+
client_key="/path/to/client.key",
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The cert and key are attached to the underlying `requests` session via `session.cert`.
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
client = ShieldClient(
|
|
77
|
+
client_cert="/path/to/client.crt",
|
|
78
|
+
client_key="/path/to/client.key",
|
|
79
|
+
ca_cert="/path/to/blacksands-ca.crt", # optional: pin the server cert
|
|
80
|
+
base_url="https://shield.blacksands.io/v1", # default
|
|
81
|
+
timeout=30, # seconds, default
|
|
82
|
+
max_retries=3, # retries on 5xx / connection errors, default
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### Organisations -- `client.orgs`
|
|
89
|
+
|
|
90
|
+
| Method | Description |
|
|
91
|
+
|--------|-------------|
|
|
92
|
+
| `create(name, contact_email, tier="essentials")` | Create a new organisation |
|
|
93
|
+
| `get(org_id)` | Get organisation by ID |
|
|
94
|
+
| `list(limit=20, offset=0)` | List organisations |
|
|
95
|
+
| `update(org_id, **kwargs)` | Update organisation fields |
|
|
96
|
+
|
|
97
|
+
### Applications -- `client.apps`
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
|--------|-------------|
|
|
101
|
+
| `register(org_id, name, version=None, framework=None, runtime=None)` | Register a new app |
|
|
102
|
+
| `get(app_id)` | Get application by ID |
|
|
103
|
+
| `list(org_id, limit=20, offset=0)` | List apps for an org |
|
|
104
|
+
| `decommission(app_id)` | Decommission an app |
|
|
105
|
+
|
|
106
|
+
### Certificates -- `client.certs`
|
|
107
|
+
|
|
108
|
+
| Method | Description |
|
|
109
|
+
|--------|-------------|
|
|
110
|
+
| `generate(app_id, algorithm="RSA-2048")` | Generate a new certificate |
|
|
111
|
+
| `rotate(app_id, cert_id)` | Rotate a certificate |
|
|
112
|
+
| `revoke(app_id, cert_id)` | Revoke a certificate |
|
|
113
|
+
| `list(app_id, limit=20, offset=0)` | List certificates for an app |
|
|
114
|
+
|
|
115
|
+
### Endpoints -- `client.endpoints`
|
|
116
|
+
|
|
117
|
+
| Method | Description |
|
|
118
|
+
|--------|-------------|
|
|
119
|
+
| `provision(app_id, type, hostname, port, protocol="https")` | Provision an endpoint |
|
|
120
|
+
| `list(app_id, limit=20, offset=0)` | List endpoints |
|
|
121
|
+
| `verify(app_id, endpoint_id)` | Verify an endpoint |
|
|
122
|
+
| `deprovision(app_id, endpoint_id)` | Deprovision an endpoint |
|
|
123
|
+
|
|
124
|
+
### Policies -- `client.policies`
|
|
125
|
+
|
|
126
|
+
| Method | Description |
|
|
127
|
+
|--------|-------------|
|
|
128
|
+
| `create(app_id, type, name, rules, priority=1)` | Create a policy |
|
|
129
|
+
| `update(app_id, policy_id, **kwargs)` | Update a policy |
|
|
130
|
+
| `apply(app_id)` | Apply all policies for an app |
|
|
131
|
+
| `list(app_id, limit=20, offset=0)` | List policies |
|
|
132
|
+
|
|
133
|
+
### DNS -- `client.dns`
|
|
134
|
+
|
|
135
|
+
| Method | Description |
|
|
136
|
+
|--------|-------------|
|
|
137
|
+
| `allow(app_id, domain, reason=None)` | Allow a domain |
|
|
138
|
+
| `block(app_id, domain, reason=None)` | Block a domain |
|
|
139
|
+
| `scan(app_id)` | Trigger a DNS scan |
|
|
140
|
+
| `report(app_id)` | Get DNS report |
|
|
141
|
+
|
|
142
|
+
### Manifests -- `client.manifests`
|
|
143
|
+
|
|
144
|
+
| Method | Description |
|
|
145
|
+
|--------|-------------|
|
|
146
|
+
| `submit(org_id, manifest)` | Submit a deployment manifest |
|
|
147
|
+
| `get(manifest_id)` | Get manifest by ID |
|
|
148
|
+
| `provision(manifest_id)` | Provision from manifest |
|
|
149
|
+
| `diff(manifest_id, compare_version)` | Diff against a version |
|
|
150
|
+
|
|
151
|
+
### Verify -- `client.verify`
|
|
152
|
+
|
|
153
|
+
| Method | Description |
|
|
154
|
+
|--------|-------------|
|
|
155
|
+
| `scan(app_id)` | Trigger a posture scan |
|
|
156
|
+
| `get_result(operation_id)` | Get scan result |
|
|
157
|
+
| `get_latest(app_id)` | Get latest posture |
|
|
158
|
+
|
|
159
|
+
### Compliance -- `client.compliance`
|
|
160
|
+
|
|
161
|
+
| Method | Description |
|
|
162
|
+
|--------|-------------|
|
|
163
|
+
| `get_status(app_id)` | Get compliance status |
|
|
164
|
+
| `generate_report(app_id, framework="SOC2")` | Generate a report |
|
|
165
|
+
| `get_controls(framework)` | List controls for a framework |
|
|
166
|
+
| `get_history(app_id)` | Get compliance history |
|
|
167
|
+
|
|
168
|
+
### Operations -- `client.operations`
|
|
169
|
+
|
|
170
|
+
| Method | Description |
|
|
171
|
+
|--------|-------------|
|
|
172
|
+
| `get_status(operation_id)` | Get operation status |
|
|
173
|
+
| `poll_until_complete(operation_id, timeout=120, interval=3)` | Block until done |
|
|
174
|
+
|
|
175
|
+
### Sessions -- `client.sessions`
|
|
176
|
+
|
|
177
|
+
| Method | Description |
|
|
178
|
+
|--------|-------------|
|
|
179
|
+
| `list(app_id)` | List active sessions |
|
|
180
|
+
| `get(session_id)` | Get session details |
|
|
181
|
+
| `terminate(session_id)` | Terminate a session |
|
|
182
|
+
|
|
183
|
+
## Error Handling
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from blacksands_shield.exceptions import (
|
|
187
|
+
ShieldAPIError,
|
|
188
|
+
AuthenticationError,
|
|
189
|
+
RateLimitError,
|
|
190
|
+
NotFoundError,
|
|
191
|
+
ValidationError,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
client.orgs.get("nonexistent")
|
|
196
|
+
except NotFoundError as e:
|
|
197
|
+
print(f"Not found: {e} (HTTP {e.status_code})")
|
|
198
|
+
except AuthenticationError:
|
|
199
|
+
print("Check your API key")
|
|
200
|
+
except RateLimitError:
|
|
201
|
+
print("Slow down -- rate limit hit")
|
|
202
|
+
except ShieldAPIError as e:
|
|
203
|
+
print(f"API error: {e}")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
All exceptions carry `status_code` and `response` attributes.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Blacksands Bursar Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for the Blacksands Bursar API -- zero-trust network security provisioning, certificate management, compliance, and posture verification.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install blacksands-shield
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For development:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e ".[dev]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from blacksands_shield import ShieldClient
|
|
21
|
+
|
|
22
|
+
client = ShieldClient(
|
|
23
|
+
client_cert="/path/to/client.crt",
|
|
24
|
+
client_key="/path/to/client.key",
|
|
25
|
+
ca_cert="/path/to/blacksands-ca.crt", # optional
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Create an organisation
|
|
29
|
+
org = client.orgs.create("Acme Corp", "admin@acme.com", tier="professional")
|
|
30
|
+
org_id = org["data"]["id"]
|
|
31
|
+
|
|
32
|
+
# Register an application
|
|
33
|
+
app = client.apps.register(org_id, "web-frontend", framework="react")
|
|
34
|
+
app_id = app["data"]["id"]
|
|
35
|
+
|
|
36
|
+
# Generate a certificate
|
|
37
|
+
cert = client.certs.generate(app_id)
|
|
38
|
+
|
|
39
|
+
# Provision an endpoint
|
|
40
|
+
ep = client.endpoints.provision(app_id, "ingress", "api.acme.com", 443)
|
|
41
|
+
|
|
42
|
+
# Run a verification scan and wait for results
|
|
43
|
+
scan = client.verify.scan(app_id)
|
|
44
|
+
result = client.operations.poll_until_complete(scan["data"]["operationId"])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Authentication
|
|
48
|
+
|
|
49
|
+
The Shield SDK uses mTLS client certificates exclusively. API-key authentication is not supported. An mTLS certificate bundle is issued by a Blacksands administrator through Overwatch or SysAdmin and delivered to the caller out-of-band.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
client = ShieldClient(
|
|
53
|
+
client_cert="/path/to/client.crt",
|
|
54
|
+
client_key="/path/to/client.key",
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The cert and key are attached to the underlying `requests` session via `session.cert`.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
client = ShieldClient(
|
|
64
|
+
client_cert="/path/to/client.crt",
|
|
65
|
+
client_key="/path/to/client.key",
|
|
66
|
+
ca_cert="/path/to/blacksands-ca.crt", # optional: pin the server cert
|
|
67
|
+
base_url="https://shield.blacksands.io/v1", # default
|
|
68
|
+
timeout=30, # seconds, default
|
|
69
|
+
max_retries=3, # retries on 5xx / connection errors, default
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### Organisations -- `client.orgs`
|
|
76
|
+
|
|
77
|
+
| Method | Description |
|
|
78
|
+
|--------|-------------|
|
|
79
|
+
| `create(name, contact_email, tier="essentials")` | Create a new organisation |
|
|
80
|
+
| `get(org_id)` | Get organisation by ID |
|
|
81
|
+
| `list(limit=20, offset=0)` | List organisations |
|
|
82
|
+
| `update(org_id, **kwargs)` | Update organisation fields |
|
|
83
|
+
|
|
84
|
+
### Applications -- `client.apps`
|
|
85
|
+
|
|
86
|
+
| Method | Description |
|
|
87
|
+
|--------|-------------|
|
|
88
|
+
| `register(org_id, name, version=None, framework=None, runtime=None)` | Register a new app |
|
|
89
|
+
| `get(app_id)` | Get application by ID |
|
|
90
|
+
| `list(org_id, limit=20, offset=0)` | List apps for an org |
|
|
91
|
+
| `decommission(app_id)` | Decommission an app |
|
|
92
|
+
|
|
93
|
+
### Certificates -- `client.certs`
|
|
94
|
+
|
|
95
|
+
| Method | Description |
|
|
96
|
+
|--------|-------------|
|
|
97
|
+
| `generate(app_id, algorithm="RSA-2048")` | Generate a new certificate |
|
|
98
|
+
| `rotate(app_id, cert_id)` | Rotate a certificate |
|
|
99
|
+
| `revoke(app_id, cert_id)` | Revoke a certificate |
|
|
100
|
+
| `list(app_id, limit=20, offset=0)` | List certificates for an app |
|
|
101
|
+
|
|
102
|
+
### Endpoints -- `client.endpoints`
|
|
103
|
+
|
|
104
|
+
| Method | Description |
|
|
105
|
+
|--------|-------------|
|
|
106
|
+
| `provision(app_id, type, hostname, port, protocol="https")` | Provision an endpoint |
|
|
107
|
+
| `list(app_id, limit=20, offset=0)` | List endpoints |
|
|
108
|
+
| `verify(app_id, endpoint_id)` | Verify an endpoint |
|
|
109
|
+
| `deprovision(app_id, endpoint_id)` | Deprovision an endpoint |
|
|
110
|
+
|
|
111
|
+
### Policies -- `client.policies`
|
|
112
|
+
|
|
113
|
+
| Method | Description |
|
|
114
|
+
|--------|-------------|
|
|
115
|
+
| `create(app_id, type, name, rules, priority=1)` | Create a policy |
|
|
116
|
+
| `update(app_id, policy_id, **kwargs)` | Update a policy |
|
|
117
|
+
| `apply(app_id)` | Apply all policies for an app |
|
|
118
|
+
| `list(app_id, limit=20, offset=0)` | List policies |
|
|
119
|
+
|
|
120
|
+
### DNS -- `client.dns`
|
|
121
|
+
|
|
122
|
+
| Method | Description |
|
|
123
|
+
|--------|-------------|
|
|
124
|
+
| `allow(app_id, domain, reason=None)` | Allow a domain |
|
|
125
|
+
| `block(app_id, domain, reason=None)` | Block a domain |
|
|
126
|
+
| `scan(app_id)` | Trigger a DNS scan |
|
|
127
|
+
| `report(app_id)` | Get DNS report |
|
|
128
|
+
|
|
129
|
+
### Manifests -- `client.manifests`
|
|
130
|
+
|
|
131
|
+
| Method | Description |
|
|
132
|
+
|--------|-------------|
|
|
133
|
+
| `submit(org_id, manifest)` | Submit a deployment manifest |
|
|
134
|
+
| `get(manifest_id)` | Get manifest by ID |
|
|
135
|
+
| `provision(manifest_id)` | Provision from manifest |
|
|
136
|
+
| `diff(manifest_id, compare_version)` | Diff against a version |
|
|
137
|
+
|
|
138
|
+
### Verify -- `client.verify`
|
|
139
|
+
|
|
140
|
+
| Method | Description |
|
|
141
|
+
|--------|-------------|
|
|
142
|
+
| `scan(app_id)` | Trigger a posture scan |
|
|
143
|
+
| `get_result(operation_id)` | Get scan result |
|
|
144
|
+
| `get_latest(app_id)` | Get latest posture |
|
|
145
|
+
|
|
146
|
+
### Compliance -- `client.compliance`
|
|
147
|
+
|
|
148
|
+
| Method | Description |
|
|
149
|
+
|--------|-------------|
|
|
150
|
+
| `get_status(app_id)` | Get compliance status |
|
|
151
|
+
| `generate_report(app_id, framework="SOC2")` | Generate a report |
|
|
152
|
+
| `get_controls(framework)` | List controls for a framework |
|
|
153
|
+
| `get_history(app_id)` | Get compliance history |
|
|
154
|
+
|
|
155
|
+
### Operations -- `client.operations`
|
|
156
|
+
|
|
157
|
+
| Method | Description |
|
|
158
|
+
|--------|-------------|
|
|
159
|
+
| `get_status(operation_id)` | Get operation status |
|
|
160
|
+
| `poll_until_complete(operation_id, timeout=120, interval=3)` | Block until done |
|
|
161
|
+
|
|
162
|
+
### Sessions -- `client.sessions`
|
|
163
|
+
|
|
164
|
+
| Method | Description |
|
|
165
|
+
|--------|-------------|
|
|
166
|
+
| `list(app_id)` | List active sessions |
|
|
167
|
+
| `get(session_id)` | Get session details |
|
|
168
|
+
| `terminate(session_id)` | Terminate a session |
|
|
169
|
+
|
|
170
|
+
## Error Handling
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from blacksands_shield.exceptions import (
|
|
174
|
+
ShieldAPIError,
|
|
175
|
+
AuthenticationError,
|
|
176
|
+
RateLimitError,
|
|
177
|
+
NotFoundError,
|
|
178
|
+
ValidationError,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
client.orgs.get("nonexistent")
|
|
183
|
+
except NotFoundError as e:
|
|
184
|
+
print(f"Not found: {e} (HTTP {e.status_code})")
|
|
185
|
+
except AuthenticationError:
|
|
186
|
+
print("Check your API key")
|
|
187
|
+
except RateLimitError:
|
|
188
|
+
print("Slow down -- rate limit hit")
|
|
189
|
+
except ShieldAPIError as e:
|
|
190
|
+
print(f"API error: {e}")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
All exceptions carry `status_code` and `response` attributes.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "blacksands-bursar"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python SDK for the Blacksands Bursar API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = ["requests>=2.28.0"]
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = ["pytest>=7.0", "pytest-mock", "responses>=0.23"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Blacksands Bursar Python SDK."""
|
|
2
|
+
|
|
3
|
+
from blacksands_bursar.client import BursarClient
|
|
4
|
+
from blacksands_bursar.exceptions import (
|
|
5
|
+
BursarAPIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__version__ = "0.2.0"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BursarClient",
|
|
15
|
+
"BursarAPIError",
|
|
16
|
+
"AuthenticationError",
|
|
17
|
+
"RateLimitError",
|
|
18
|
+
"NotFoundError",
|
|
19
|
+
"ValidationError",
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Main Bursar API client, ported from shieldApiClient.js."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from blacksands_bursar.exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
BursarAPIError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from blacksands_bursar.resources.apps import AppResource
|
|
15
|
+
from blacksands_bursar.resources.certs import CertResource
|
|
16
|
+
from blacksands_bursar.resources.compliance import ComplianceResource
|
|
17
|
+
from blacksands_bursar.resources.dns import DnsResource
|
|
18
|
+
from blacksands_bursar.resources.endpoints import EndpointResource
|
|
19
|
+
from blacksands_bursar.resources.manifests import ManifestResource
|
|
20
|
+
from blacksands_bursar.resources.operations import OperationResource
|
|
21
|
+
from blacksands_bursar.resources.orgs import OrgResource
|
|
22
|
+
from blacksands_bursar.resources.policies import PolicyResource
|
|
23
|
+
from blacksands_bursar.resources.sessions import SessionResource
|
|
24
|
+
from blacksands_bursar.resources.verify import VerifyResource
|
|
25
|
+
|
|
26
|
+
_ERROR_MAP = {
|
|
27
|
+
400: ValidationError,
|
|
28
|
+
401: AuthenticationError,
|
|
29
|
+
403: AuthenticationError,
|
|
30
|
+
404: NotFoundError,
|
|
31
|
+
422: ValidationError,
|
|
32
|
+
429: RateLimitError,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DEFAULT_BASE_URL = "https://shield.blacksands.io/v1"
|
|
36
|
+
DEFAULT_TIMEOUT = 30
|
|
37
|
+
DEFAULT_MAX_RETRIES = 3
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BursarClient:
|
|
41
|
+
"""Python client for the Blacksands Bursar API.
|
|
42
|
+
|
|
43
|
+
Authentication is mTLS only. API keys are not supported.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
client_cert: Path to the PEM client certificate file.
|
|
47
|
+
client_key: Path to the PEM client private key file.
|
|
48
|
+
ca_cert: Optional path to a PEM CA bundle to pin the server cert.
|
|
49
|
+
base_url: Override the default API base URL.
|
|
50
|
+
timeout: Request timeout in seconds.
|
|
51
|
+
max_retries: Number of retries for transient (5xx) failures.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
client_cert: str,
|
|
57
|
+
client_key: str,
|
|
58
|
+
ca_cert: str | None = None,
|
|
59
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
60
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
61
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
62
|
+
):
|
|
63
|
+
if not client_cert or not client_key:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"BursarClient requires client_cert and client_key. "
|
|
66
|
+
"mTLS is the only supported authentication method."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self.base_url = base_url.rstrip("/")
|
|
70
|
+
self.timeout = timeout
|
|
71
|
+
self.max_retries = max_retries
|
|
72
|
+
|
|
73
|
+
self.session = requests.Session()
|
|
74
|
+
self.session.headers.update({"Content-Type": "application/json"})
|
|
75
|
+
self.session.cert = (client_cert, client_key)
|
|
76
|
+
if ca_cert:
|
|
77
|
+
self.session.verify = ca_cert
|
|
78
|
+
|
|
79
|
+
# Lazy-initialised resource singletons
|
|
80
|
+
self._orgs = None
|
|
81
|
+
self._apps = None
|
|
82
|
+
self._certs = None
|
|
83
|
+
self._endpoints = None
|
|
84
|
+
self._policies = None
|
|
85
|
+
self._dns = None
|
|
86
|
+
self._manifests = None
|
|
87
|
+
self._verify = None
|
|
88
|
+
self._compliance = None
|
|
89
|
+
self._operations = None
|
|
90
|
+
self._sessions = None
|
|
91
|
+
|
|
92
|
+
# -- resource properties --------------------------------------------------
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def orgs(self) -> OrgResource:
|
|
96
|
+
if self._orgs is None:
|
|
97
|
+
self._orgs = OrgResource(self)
|
|
98
|
+
return self._orgs
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def apps(self) -> AppResource:
|
|
102
|
+
if self._apps is None:
|
|
103
|
+
self._apps = AppResource(self)
|
|
104
|
+
return self._apps
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def certs(self) -> CertResource:
|
|
108
|
+
if self._certs is None:
|
|
109
|
+
self._certs = CertResource(self)
|
|
110
|
+
return self._certs
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def endpoints(self) -> EndpointResource:
|
|
114
|
+
if self._endpoints is None:
|
|
115
|
+
self._endpoints = EndpointResource(self)
|
|
116
|
+
return self._endpoints
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def policies(self) -> PolicyResource:
|
|
120
|
+
if self._policies is None:
|
|
121
|
+
self._policies = PolicyResource(self)
|
|
122
|
+
return self._policies
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def dns(self) -> DnsResource:
|
|
126
|
+
if self._dns is None:
|
|
127
|
+
self._dns = DnsResource(self)
|
|
128
|
+
return self._dns
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def manifests(self) -> ManifestResource:
|
|
132
|
+
if self._manifests is None:
|
|
133
|
+
self._manifests = ManifestResource(self)
|
|
134
|
+
return self._manifests
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def verify(self) -> VerifyResource:
|
|
138
|
+
if self._verify is None:
|
|
139
|
+
self._verify = VerifyResource(self)
|
|
140
|
+
return self._verify
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def compliance(self) -> ComplianceResource:
|
|
144
|
+
if self._compliance is None:
|
|
145
|
+
self._compliance = ComplianceResource(self)
|
|
146
|
+
return self._compliance
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def operations(self) -> OperationResource:
|
|
150
|
+
if self._operations is None:
|
|
151
|
+
self._operations = OperationResource(self)
|
|
152
|
+
return self._operations
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def sessions(self) -> SessionResource:
|
|
156
|
+
if self._sessions is None:
|
|
157
|
+
self._sessions = SessionResource(self)
|
|
158
|
+
return self._sessions
|
|
159
|
+
|
|
160
|
+
# -- core request plumbing ------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def _request(self, method: str, path: str, **kwargs):
|
|
163
|
+
"""Send an HTTP request with retry + exponential back-off.
|
|
164
|
+
|
|
165
|
+
Mirrors the JS ``_request(method, path, data, retries)`` pattern:
|
|
166
|
+
- Retries up to ``max_retries`` on 5xx / connection errors.
|
|
167
|
+
- Maps 4xx status codes to typed exceptions immediately (no retry).
|
|
168
|
+
"""
|
|
169
|
+
url = f"{self.base_url}{path}"
|
|
170
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
171
|
+
|
|
172
|
+
last_exc = None
|
|
173
|
+
for attempt in range(1, self.max_retries + 1):
|
|
174
|
+
try:
|
|
175
|
+
resp = self.session.request(method, url, **kwargs)
|
|
176
|
+
|
|
177
|
+
if resp.status_code >= 400:
|
|
178
|
+
error_body = {}
|
|
179
|
+
try:
|
|
180
|
+
error_body = resp.json()
|
|
181
|
+
except ValueError:
|
|
182
|
+
pass
|
|
183
|
+
message = error_body.get("error", resp.text or "Unknown error")
|
|
184
|
+
|
|
185
|
+
exc_cls = _ERROR_MAP.get(resp.status_code, BursarAPIError)
|
|
186
|
+
|
|
187
|
+
# Do not retry client errors (4xx) except 429
|
|
188
|
+
if resp.status_code < 500 and resp.status_code != 429:
|
|
189
|
+
raise exc_cls(message, status_code=resp.status_code, response=resp)
|
|
190
|
+
|
|
191
|
+
# 429 and 5xx are retryable
|
|
192
|
+
last_exc = exc_cls(message, status_code=resp.status_code, response=resp)
|
|
193
|
+
if attempt == self.max_retries:
|
|
194
|
+
raise last_exc
|
|
195
|
+
|
|
196
|
+
time.sleep(2**attempt)
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Success
|
|
200
|
+
try:
|
|
201
|
+
return resp.json()
|
|
202
|
+
except ValueError:
|
|
203
|
+
return resp.text
|
|
204
|
+
|
|
205
|
+
except (requests.ConnectionError, requests.Timeout) as exc:
|
|
206
|
+
last_exc = BursarAPIError(str(exc))
|
|
207
|
+
if attempt == self.max_retries:
|
|
208
|
+
raise last_exc from exc
|
|
209
|
+
time.sleep(2**attempt)
|
|
210
|
+
|
|
211
|
+
# Should not reach here, but just in case
|
|
212
|
+
raise last_exc # pragma: no cover
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Exception classes for the Blacksands Bursar SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BursarAPIError(Exception):
|
|
5
|
+
"""Base exception for Bursar API errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message, status_code=None, response=None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.response = response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(BursarAPIError):
|
|
14
|
+
"""Raised when API authentication fails (401)."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RateLimitError(BursarAPIError):
|
|
19
|
+
"""Raised when the API rate limit is exceeded (429)."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NotFoundError(BursarAPIError):
|
|
24
|
+
"""Raised when a requested resource is not found (404)."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValidationError(BursarAPIError):
|
|
29
|
+
"""Raised when request validation fails (400/422)."""
|
|
30
|
+
pass
|