fulkruma 0.1.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.
- fulkruma-0.1.0/.gitignore +6 -0
- fulkruma-0.1.0/PKG-INFO +117 -0
- fulkruma-0.1.0/README.md +94 -0
- fulkruma-0.1.0/fulkruma/__init__.py +18 -0
- fulkruma-0.1.0/fulkruma/client.py +237 -0
- fulkruma-0.1.0/fulkruma/errors.py +33 -0
- fulkruma-0.1.0/fulkruma/resources.py +351 -0
- fulkruma-0.1.0/fulkruma/webhooks.py +112 -0
- fulkruma-0.1.0/pyproject.toml +38 -0
- fulkruma-0.1.0/tests/__init__.py +0 -0
- fulkruma-0.1.0/tests/test_client.py +402 -0
fulkruma-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fulkruma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Fulkruma — stock + warehouses + shipping + licenses + deliveries. HMAC-signed requests with full parity to the Node SDK.
|
|
5
|
+
Project-URL: Homepage, https://fulkruma.com
|
|
6
|
+
Project-URL: Source, https://github.com/hachimi-cat/saas-fulkruma/tree/master/sdk/python
|
|
7
|
+
Project-URL: Issues, https://github.com/hachimi-cat/saas-fulkruma/issues
|
|
8
|
+
Author-email: Forjio <hello@fulkruma.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: biteship,forjio,fulkruma,licenses,shipping,stock,warehouses
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# fulkruma (Python)
|
|
25
|
+
|
|
26
|
+
Official Python SDK for [Fulkruma](https://fulkruma.com) — stock,
|
|
27
|
+
warehouses, shipping, licenses, deliveries. Mirrors the Node SDK
|
|
28
|
+
(`@forjio/fulkruma-node`) API surface 1:1.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install fulkruma
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from fulkruma import FulkrumaClient
|
|
40
|
+
|
|
41
|
+
client = FulkrumaClient(
|
|
42
|
+
key_id="AKIAFULK...",
|
|
43
|
+
secret="sk_...",
|
|
44
|
+
base_url="https://fulkruma.com", # default
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Create a warehouse
|
|
48
|
+
wh = client.warehouses.create({"name": "Jakarta DC", "city": "Jakarta"})
|
|
49
|
+
|
|
50
|
+
# Adjust stock
|
|
51
|
+
client.stock.adjust({
|
|
52
|
+
"variantId": "var_1",
|
|
53
|
+
"warehouseId": wh["warehouse"]["id"],
|
|
54
|
+
"delta": 50,
|
|
55
|
+
"reason": "initial_stock",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
# List shipments
|
|
59
|
+
shipments = client.shipments.list(status="in_transit")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Platform-admin scope
|
|
63
|
+
|
|
64
|
+
Keys with the `fulkruma:platform:admin` scope can operate against any
|
|
65
|
+
merchant via the `X-Fulkruma-On-Behalf-Of` header. Either pass it on the
|
|
66
|
+
client:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
admin = FulkrumaClient(key_id="...", secret="...", on_behalf_of="acc_merchant")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
…or scope a single call:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
client.shipments.list(on_behalf_of="acc_merchant")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`for_merchant(account_id)` returns a cloned client locked to one merchant.
|
|
79
|
+
|
|
80
|
+
## Webhook verification
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from fulkruma import verify_webhook
|
|
84
|
+
|
|
85
|
+
# Flask
|
|
86
|
+
@app.post("/webhooks/fulkruma")
|
|
87
|
+
def hook():
|
|
88
|
+
raw = request.get_data() # bytes — verify the raw body, NOT parsed JSON
|
|
89
|
+
sig = request.headers.get("Fulkruma-Signature", "")
|
|
90
|
+
event = verify_webhook(raw_body=raw, signature=sig, secret=os.environ["WHSEC"])
|
|
91
|
+
# event["type"], event["data"], etc.
|
|
92
|
+
return "", 204
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Resource namespaces
|
|
96
|
+
|
|
97
|
+
| Namespace | Methods |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `products` | `create`, `get`, `list`, `update`, `archive`, `add_variant`, `update_variant`, `archive_variant` |
|
|
100
|
+
| `warehouses` | `create`, `list`, `update`, `archive` |
|
|
101
|
+
| `stock` | `levels`, `movements`, `reservations`, `adjust` |
|
|
102
|
+
| `addresses` | `list`, `create`, `delete` |
|
|
103
|
+
| `shipments` | `list`, `get`, `create` |
|
|
104
|
+
| `shipping` | `couriers`, `origin`, `set_origin`, `rates` |
|
|
105
|
+
| `licenses` | `list`, `issue`, `revoke`, `activate`, `deactivate`, `validate` |
|
|
106
|
+
| `deliveries` | `list`, `get`, `create` |
|
|
107
|
+
| `api_keys` | `list`, `create`, `revoke` |
|
|
108
|
+
| `audit_log` | `list` |
|
|
109
|
+
| `billing` | `plans`, `current_plan`, `subscription`, `usage`, `invoices`, `checkout`, `cancel` |
|
|
110
|
+
| `integrations` | `status` |
|
|
111
|
+
| `stats` | `overview` |
|
|
112
|
+
| `webhooks` | `list_endpoints`, `create_endpoint`, `update_endpoint`, `delete_endpoint`, `list_events` |
|
|
113
|
+
| `admin` | `provision_workspace`, `get_workspace`, `partner_usage` |
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
fulkruma-0.1.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# fulkruma (Python)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [Fulkruma](https://fulkruma.com) — stock,
|
|
4
|
+
warehouses, shipping, licenses, deliveries. Mirrors the Node SDK
|
|
5
|
+
(`@forjio/fulkruma-node`) API surface 1:1.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install fulkruma
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from fulkruma import FulkrumaClient
|
|
17
|
+
|
|
18
|
+
client = FulkrumaClient(
|
|
19
|
+
key_id="AKIAFULK...",
|
|
20
|
+
secret="sk_...",
|
|
21
|
+
base_url="https://fulkruma.com", # default
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Create a warehouse
|
|
25
|
+
wh = client.warehouses.create({"name": "Jakarta DC", "city": "Jakarta"})
|
|
26
|
+
|
|
27
|
+
# Adjust stock
|
|
28
|
+
client.stock.adjust({
|
|
29
|
+
"variantId": "var_1",
|
|
30
|
+
"warehouseId": wh["warehouse"]["id"],
|
|
31
|
+
"delta": 50,
|
|
32
|
+
"reason": "initial_stock",
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
# List shipments
|
|
36
|
+
shipments = client.shipments.list(status="in_transit")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Platform-admin scope
|
|
40
|
+
|
|
41
|
+
Keys with the `fulkruma:platform:admin` scope can operate against any
|
|
42
|
+
merchant via the `X-Fulkruma-On-Behalf-Of` header. Either pass it on the
|
|
43
|
+
client:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
admin = FulkrumaClient(key_id="...", secret="...", on_behalf_of="acc_merchant")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
…or scope a single call:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
client.shipments.list(on_behalf_of="acc_merchant")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`for_merchant(account_id)` returns a cloned client locked to one merchant.
|
|
56
|
+
|
|
57
|
+
## Webhook verification
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from fulkruma import verify_webhook
|
|
61
|
+
|
|
62
|
+
# Flask
|
|
63
|
+
@app.post("/webhooks/fulkruma")
|
|
64
|
+
def hook():
|
|
65
|
+
raw = request.get_data() # bytes — verify the raw body, NOT parsed JSON
|
|
66
|
+
sig = request.headers.get("Fulkruma-Signature", "")
|
|
67
|
+
event = verify_webhook(raw_body=raw, signature=sig, secret=os.environ["WHSEC"])
|
|
68
|
+
# event["type"], event["data"], etc.
|
|
69
|
+
return "", 204
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Resource namespaces
|
|
73
|
+
|
|
74
|
+
| Namespace | Methods |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `products` | `create`, `get`, `list`, `update`, `archive`, `add_variant`, `update_variant`, `archive_variant` |
|
|
77
|
+
| `warehouses` | `create`, `list`, `update`, `archive` |
|
|
78
|
+
| `stock` | `levels`, `movements`, `reservations`, `adjust` |
|
|
79
|
+
| `addresses` | `list`, `create`, `delete` |
|
|
80
|
+
| `shipments` | `list`, `get`, `create` |
|
|
81
|
+
| `shipping` | `couriers`, `origin`, `set_origin`, `rates` |
|
|
82
|
+
| `licenses` | `list`, `issue`, `revoke`, `activate`, `deactivate`, `validate` |
|
|
83
|
+
| `deliveries` | `list`, `get`, `create` |
|
|
84
|
+
| `api_keys` | `list`, `create`, `revoke` |
|
|
85
|
+
| `audit_log` | `list` |
|
|
86
|
+
| `billing` | `plans`, `current_plan`, `subscription`, `usage`, `invoices`, `checkout`, `cancel` |
|
|
87
|
+
| `integrations` | `status` |
|
|
88
|
+
| `stats` | `overview` |
|
|
89
|
+
| `webhooks` | `list_endpoints`, `create_endpoint`, `update_endpoint`, `delete_endpoint`, `list_events` |
|
|
90
|
+
| `admin` | `provision_workspace`, `get_workspace`, `partner_usage` |
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Official Python SDK for Fulkruma.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK (`@forjio/fulkruma-node`) API surface — HMAC-signed
|
|
4
|
+
requests, 14 resource namespaces, optional `X-Fulkruma-On-Behalf-Of`
|
|
5
|
+
platform-admin scoping, plus webhook signature verification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import FulkrumaClient
|
|
9
|
+
from .errors import FulkrumaError
|
|
10
|
+
from .webhooks import verify_webhook
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FulkrumaClient",
|
|
14
|
+
"FulkrumaError",
|
|
15
|
+
"verify_webhook",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""High-level Fulkruma HMAC client.
|
|
2
|
+
|
|
3
|
+
Mirrors `FulkrumaClient` from the Node SDK 1:1:
|
|
4
|
+
|
|
5
|
+
- ``Authorization: Fulkruma-HMAC-SHA256 keyId=…, scope=*, signature=…``
|
|
6
|
+
- ``X-Fulkruma-Timestamp: <unix>``
|
|
7
|
+
- ``X-Fulkruma-On-Behalf-Of: <accountId>`` for platform-admin keys
|
|
8
|
+
- ``Idempotency-Key`` on every mutation that needs replay safety
|
|
9
|
+
- Standard ``{ data, error, meta }`` envelope unwrapping
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import hmac
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
from urllib.parse import urlencode
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from .errors import FulkrumaError
|
|
25
|
+
from .resources import (
|
|
26
|
+
AddressesResources,
|
|
27
|
+
AdminResources,
|
|
28
|
+
ApiKeysResources,
|
|
29
|
+
AuditLogResources,
|
|
30
|
+
BillingResources,
|
|
31
|
+
DeliveriesResources,
|
|
32
|
+
IntegrationsResources,
|
|
33
|
+
LicensesResources,
|
|
34
|
+
ProductsResources,
|
|
35
|
+
ShipmentsResources,
|
|
36
|
+
ShippingResources,
|
|
37
|
+
StatsResources,
|
|
38
|
+
StockResources,
|
|
39
|
+
WarehousesResources,
|
|
40
|
+
WebhooksResources,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FulkrumaClient:
|
|
45
|
+
"""HMAC-signed client for the Fulkruma REST API.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
key_id:
|
|
50
|
+
HMAC access key id, e.g. ``AKIAFULK...``.
|
|
51
|
+
secret:
|
|
52
|
+
HMAC secret.
|
|
53
|
+
base_url:
|
|
54
|
+
Default ``https://fulkruma.com``. Trailing slashes are stripped.
|
|
55
|
+
on_behalf_of:
|
|
56
|
+
Optional merchant ``accountId`` — forwarded as
|
|
57
|
+
``X-Fulkruma-On-Behalf-Of``. Only allowed when ``key_id`` holds
|
|
58
|
+
the ``fulkruma:platform:admin`` scope.
|
|
59
|
+
timeout_ms:
|
|
60
|
+
Per-request timeout in milliseconds. Default 30 000.
|
|
61
|
+
http:
|
|
62
|
+
Inject a pre-configured ``httpx.Client`` (e.g. with a custom
|
|
63
|
+
transport for tests). Caller owns its lifecycle.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
key_id: str,
|
|
70
|
+
secret: str,
|
|
71
|
+
base_url: str = "https://fulkruma.com",
|
|
72
|
+
on_behalf_of: Optional[str] = None,
|
|
73
|
+
timeout_ms: int = 30_000,
|
|
74
|
+
http: Optional[httpx.Client] = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
if not key_id or not secret:
|
|
77
|
+
raise ValueError("FulkrumaClient: key_id and secret are required")
|
|
78
|
+
self.key_id = key_id
|
|
79
|
+
self._secret = secret
|
|
80
|
+
self.base_url = base_url.rstrip("/")
|
|
81
|
+
self._default_obo = on_behalf_of
|
|
82
|
+
self.timeout_ms = timeout_ms
|
|
83
|
+
self._http = http or httpx.Client(timeout=timeout_ms / 1000.0)
|
|
84
|
+
self._owns_http = http is None
|
|
85
|
+
|
|
86
|
+
# Resource namespaces — match the Node SDK property names.
|
|
87
|
+
self.products = ProductsResources(self)
|
|
88
|
+
self.warehouses = WarehousesResources(self)
|
|
89
|
+
self.stock = StockResources(self)
|
|
90
|
+
self.addresses = AddressesResources(self)
|
|
91
|
+
self.shipments = ShipmentsResources(self)
|
|
92
|
+
self.shipping = ShippingResources(self)
|
|
93
|
+
self.licenses = LicensesResources(self)
|
|
94
|
+
self.deliveries = DeliveriesResources(self)
|
|
95
|
+
self.api_keys = ApiKeysResources(self)
|
|
96
|
+
self.audit_log = AuditLogResources(self)
|
|
97
|
+
self.billing = BillingResources(self)
|
|
98
|
+
self.integrations = IntegrationsResources(self)
|
|
99
|
+
self.stats = StatsResources(self)
|
|
100
|
+
self.webhooks = WebhooksResources(self)
|
|
101
|
+
self.admin = AdminResources(self)
|
|
102
|
+
|
|
103
|
+
# ─── Lifecycle ───────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
def close(self) -> None:
|
|
106
|
+
if self._owns_http:
|
|
107
|
+
self._http.close()
|
|
108
|
+
|
|
109
|
+
def __enter__(self) -> "FulkrumaClient":
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def __exit__(self, *_: Any) -> None:
|
|
113
|
+
self.close()
|
|
114
|
+
|
|
115
|
+
def for_merchant(self, account_id: str) -> "FulkrumaClient":
|
|
116
|
+
"""Clone scoped to a specific merchant. For platform-admin keys."""
|
|
117
|
+
return FulkrumaClient(
|
|
118
|
+
key_id=self.key_id,
|
|
119
|
+
secret=self._secret,
|
|
120
|
+
base_url=self.base_url,
|
|
121
|
+
on_behalf_of=account_id,
|
|
122
|
+
timeout_ms=self.timeout_ms,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# ─── Internal helpers ────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _qs(params: Dict[str, Any]) -> str:
|
|
129
|
+
entries = [(k, v) for k, v in params.items() if v is not None]
|
|
130
|
+
if not entries:
|
|
131
|
+
return ""
|
|
132
|
+
# str(bool) → "True"/"False"; Node SDK emits the JS coercion ("true"/"false")
|
|
133
|
+
norm = []
|
|
134
|
+
for k, v in entries:
|
|
135
|
+
if isinstance(v, bool):
|
|
136
|
+
norm.append((k, "true" if v else "false"))
|
|
137
|
+
else:
|
|
138
|
+
norm.append((k, str(v)))
|
|
139
|
+
return "?" + urlencode(norm)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _gen_idem() -> str:
|
|
143
|
+
return f"idem_{uuid.uuid4()}"
|
|
144
|
+
|
|
145
|
+
def _sign(
|
|
146
|
+
self,
|
|
147
|
+
*,
|
|
148
|
+
method: str,
|
|
149
|
+
path: str,
|
|
150
|
+
body: Optional[str],
|
|
151
|
+
idempotency_key: Optional[str],
|
|
152
|
+
) -> Dict[str, str]:
|
|
153
|
+
ts = str(int(time.time()))
|
|
154
|
+
body_hash = hashlib.sha256((body or "").encode("utf-8")).hexdigest()
|
|
155
|
+
idem = f"\n{idempotency_key}" if idempotency_key else ""
|
|
156
|
+
string_to_sign = f"{method.upper()}\n{path}\n{ts}\n{body_hash}{idem}"
|
|
157
|
+
signature = hmac.new(
|
|
158
|
+
self._secret.encode("utf-8"),
|
|
159
|
+
string_to_sign.encode("utf-8"),
|
|
160
|
+
hashlib.sha256,
|
|
161
|
+
).hexdigest()
|
|
162
|
+
return {"signature": signature, "timestamp": ts}
|
|
163
|
+
|
|
164
|
+
# ─── Low-level request ───────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def request(
|
|
167
|
+
self,
|
|
168
|
+
method: str,
|
|
169
|
+
path: str,
|
|
170
|
+
*,
|
|
171
|
+
body: Any = None,
|
|
172
|
+
idempotency_key: Optional[str] = None,
|
|
173
|
+
on_behalf_of: Optional[str] = None,
|
|
174
|
+
) -> Any:
|
|
175
|
+
body_json = json.dumps(body, separators=(",", ":")) if body is not None else None
|
|
176
|
+
signed = self._sign(
|
|
177
|
+
method=method, path=path, body=body_json, idempotency_key=idempotency_key,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
headers: Dict[str, str] = {
|
|
181
|
+
"Accept": "application/json",
|
|
182
|
+
"Authorization": (
|
|
183
|
+
f"Fulkruma-HMAC-SHA256 keyId={self.key_id}, scope=*, "
|
|
184
|
+
f"signature={signed['signature']}"
|
|
185
|
+
),
|
|
186
|
+
"X-Fulkruma-Timestamp": signed["timestamp"],
|
|
187
|
+
}
|
|
188
|
+
if body_json is not None:
|
|
189
|
+
headers["Content-Type"] = "application/json"
|
|
190
|
+
if idempotency_key:
|
|
191
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
192
|
+
effective_obo = on_behalf_of if on_behalf_of is not None else self._default_obo
|
|
193
|
+
if effective_obo:
|
|
194
|
+
headers["X-Fulkruma-On-Behalf-Of"] = effective_obo
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
res = self._http.request(
|
|
198
|
+
method.upper(),
|
|
199
|
+
f"{self.base_url}{path}",
|
|
200
|
+
headers=headers,
|
|
201
|
+
content=body_json,
|
|
202
|
+
)
|
|
203
|
+
except httpx.TimeoutException as exc:
|
|
204
|
+
raise FulkrumaError(
|
|
205
|
+
0, "timeout", f"Fulkruma request timed out after {self.timeout_ms}ms",
|
|
206
|
+
) from exc
|
|
207
|
+
except httpx.HTTPError as exc:
|
|
208
|
+
raise FulkrumaError(0, "network_error", str(exc)) from exc
|
|
209
|
+
|
|
210
|
+
text = res.text
|
|
211
|
+
try:
|
|
212
|
+
env = json.loads(text) if text else {}
|
|
213
|
+
except ValueError:
|
|
214
|
+
raise FulkrumaError(
|
|
215
|
+
res.status_code, "invalid_response", f"Non-JSON response: {text[:200]}",
|
|
216
|
+
) from None
|
|
217
|
+
|
|
218
|
+
request_id = None
|
|
219
|
+
if isinstance(env, dict):
|
|
220
|
+
meta = env.get("meta")
|
|
221
|
+
if isinstance(meta, dict):
|
|
222
|
+
request_id = meta.get("requestId")
|
|
223
|
+
err = env.get("error")
|
|
224
|
+
else:
|
|
225
|
+
err = None
|
|
226
|
+
|
|
227
|
+
if res.status_code >= 400 or err:
|
|
228
|
+
if not err:
|
|
229
|
+
err = {"code": "unknown", "message": f"HTTP {res.status_code}"}
|
|
230
|
+
raise FulkrumaError(
|
|
231
|
+
res.status_code,
|
|
232
|
+
err.get("code", "unknown"),
|
|
233
|
+
err.get("message", f"HTTP {res.status_code}"),
|
|
234
|
+
request_id,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return env.get("data") if isinstance(env, dict) else env
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Typed error class for the Fulkruma SDK.
|
|
2
|
+
|
|
3
|
+
Mirrors `FulkrumaError` from the Node SDK: every non-2xx response and
|
|
4
|
+
every enveloped `{ error: { code, message } }` payload raises this with
|
|
5
|
+
the upstream code preserved.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FulkrumaError(Exception):
|
|
14
|
+
"""Raised on any non-success response from a Fulkruma endpoint."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
status: int,
|
|
19
|
+
code: str,
|
|
20
|
+
message: str,
|
|
21
|
+
request_id: Optional[str] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.status = status
|
|
25
|
+
self.code = code
|
|
26
|
+
self.message = message
|
|
27
|
+
self.request_id = request_id
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return (
|
|
31
|
+
f"FulkrumaError(status={self.status}, code={self.code!r}, "
|
|
32
|
+
f"message={self.message!r}, request_id={self.request_id!r})"
|
|
33
|
+
)
|