kortana-dev-sdk 1.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.
- kortana_dev_sdk-1.0.0/PKG-INFO +113 -0
- kortana_dev_sdk-1.0.0/README.md +87 -0
- kortana_dev_sdk-1.0.0/kortana/__init__.py +67 -0
- kortana_dev_sdk-1.0.0/kortana/auth/__init__.py +1 -0
- kortana_dev_sdk-1.0.0/kortana/auth/authenticator.py +65 -0
- kortana_dev_sdk-1.0.0/kortana/client.py +58 -0
- kortana_dev_sdk-1.0.0/kortana/config.py +75 -0
- kortana_dev_sdk-1.0.0/kortana/exceptions.py +183 -0
- kortana_dev_sdk-1.0.0/kortana/http/__init__.py +1 -0
- kortana_dev_sdk-1.0.0/kortana/http/client.py +179 -0
- kortana_dev_sdk-1.0.0/kortana/modules/__init__.py +26 -0
- kortana_dev_sdk-1.0.0/kortana/modules/banking.py +98 -0
- kortana_dev_sdk-1.0.0/kortana/modules/blockchain.py +91 -0
- kortana_dev_sdk-1.0.0/kortana/modules/cards.py +91 -0
- kortana_dev_sdk-1.0.0/kortana/modules/compliance.py +106 -0
- kortana_dev_sdk-1.0.0/kortana/modules/contracts.py +139 -0
- kortana_dev_sdk-1.0.0/kortana/modules/merchants.py +72 -0
- kortana_dev_sdk-1.0.0/kortana/modules/payments.py +144 -0
- kortana_dev_sdk-1.0.0/kortana/modules/settlement.py +88 -0
- kortana_dev_sdk-1.0.0/kortana/modules/wallets.py +180 -0
- kortana_dev_sdk-1.0.0/kortana/webhooks/__init__.py +8 -0
- kortana_dev_sdk-1.0.0/kortana/webhooks/verifier.py +84 -0
- kortana_dev_sdk-1.0.0/pyproject.toml +49 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kortana-dev-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Kortana SDK for Python
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Kortana Labs
|
|
7
|
+
Author-email: engineering@kortana.io
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: anyio (>=4.0.0,<5.0.0)
|
|
18
|
+
Requires-Dist: cryptography (>=42.0.0,<43.0.0)
|
|
19
|
+
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
|
20
|
+
Requires-Dist: pydantic (>=2.5.0,<3.0.0)
|
|
21
|
+
Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
|
|
22
|
+
Project-URL: Homepage, https://docs.kortana.io/sdk/python
|
|
23
|
+
Project-URL: Repository, https://github.com/kortana-labs/sdk-python
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# kortana-dev-sdk — Official Kortana Python SDK
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/kortana-dev-sdk/)
|
|
29
|
+
[](https://pypi.org/project/kortana-dev-sdk/)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
|
|
32
|
+
The official Python SDK for the [Kortana](https://kortana.io) blockchain payments platform.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install kortana-dev-sdk
|
|
38
|
+
# or
|
|
39
|
+
poetry add kortana-dev-sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import asyncio
|
|
46
|
+
import os
|
|
47
|
+
from kortana import kortana
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
async with kortana(
|
|
51
|
+
api_key=os.environ["KORTANA_API_KEY"],
|
|
52
|
+
environment="mainnet",
|
|
53
|
+
) as client:
|
|
54
|
+
# Check network status
|
|
55
|
+
status = await client.blockchain.get_network_status()
|
|
56
|
+
print(f"Block: {status.block_number}")
|
|
57
|
+
|
|
58
|
+
# Get wallet balance
|
|
59
|
+
balance = await client.wallets.get_balance()
|
|
60
|
+
print(f"Balance: {balance.available} DNR")
|
|
61
|
+
|
|
62
|
+
# Create a payment link
|
|
63
|
+
link = await client.payments.create_link(
|
|
64
|
+
amount=1_000_000_000_000_000_000, # 1 DNR in wei
|
|
65
|
+
currency="DNR",
|
|
66
|
+
description="Invoice #1001",
|
|
67
|
+
)
|
|
68
|
+
print(f"Payment URL: {link.url}")
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Webhook Verification
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from kortana import kortana
|
|
77
|
+
|
|
78
|
+
client = kortana(api_key="kt_live_...", webhook_secret="whsec_...")
|
|
79
|
+
|
|
80
|
+
@app.post("/webhooks")
|
|
81
|
+
async def handle_webhook(request: Request):
|
|
82
|
+
payload = await request.body()
|
|
83
|
+
signature = request.headers.get("X-Kortana-Signature", "")
|
|
84
|
+
event = client.validate_webhook(payload.decode(), signature)
|
|
85
|
+
if event["type"] == "transfer.completed":
|
|
86
|
+
print("Transfer done!", event["data"])
|
|
87
|
+
return {"received": True}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Modules
|
|
91
|
+
|
|
92
|
+
| Module | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `client.blockchain` | Read-only blockchain data (blocks, txs, gas) |
|
|
95
|
+
| `client.contracts` | Deploy and interact with smart contracts |
|
|
96
|
+
| `client.wallets` | HD wallet management and transfers |
|
|
97
|
+
| `client.payments` | Payment intents, invoices, subscriptions |
|
|
98
|
+
| `client.banking` | Customer and bank account management |
|
|
99
|
+
| `client.cards` | Virtual and physical card issuance |
|
|
100
|
+
| `client.compliance` | KYC, KYB, and AML screening |
|
|
101
|
+
| `client.settlement` | Ledger settlement and treasury |
|
|
102
|
+
| `client.merchants` | Merchant onboarding and management |
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
- Python 3.9+
|
|
107
|
+
- `httpx >= 0.27`
|
|
108
|
+
- `pydantic >= 2.5`
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT © Kortana Labs
|
|
113
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# kortana-dev-sdk — Official Kortana Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/kortana-dev-sdk/)
|
|
4
|
+
[](https://pypi.org/project/kortana-dev-sdk/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
The official Python SDK for the [Kortana](https://kortana.io) blockchain payments platform.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install kortana-dev-sdk
|
|
13
|
+
# or
|
|
14
|
+
poetry add kortana-dev-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
import os
|
|
22
|
+
from kortana import kortana
|
|
23
|
+
|
|
24
|
+
async def main():
|
|
25
|
+
async with kortana(
|
|
26
|
+
api_key=os.environ["KORTANA_API_KEY"],
|
|
27
|
+
environment="mainnet",
|
|
28
|
+
) as client:
|
|
29
|
+
# Check network status
|
|
30
|
+
status = await client.blockchain.get_network_status()
|
|
31
|
+
print(f"Block: {status.block_number}")
|
|
32
|
+
|
|
33
|
+
# Get wallet balance
|
|
34
|
+
balance = await client.wallets.get_balance()
|
|
35
|
+
print(f"Balance: {balance.available} DNR")
|
|
36
|
+
|
|
37
|
+
# Create a payment link
|
|
38
|
+
link = await client.payments.create_link(
|
|
39
|
+
amount=1_000_000_000_000_000_000, # 1 DNR in wei
|
|
40
|
+
currency="DNR",
|
|
41
|
+
description="Invoice #1001",
|
|
42
|
+
)
|
|
43
|
+
print(f"Payment URL: {link.url}")
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Webhook Verification
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from kortana import kortana
|
|
52
|
+
|
|
53
|
+
client = kortana(api_key="kt_live_...", webhook_secret="whsec_...")
|
|
54
|
+
|
|
55
|
+
@app.post("/webhooks")
|
|
56
|
+
async def handle_webhook(request: Request):
|
|
57
|
+
payload = await request.body()
|
|
58
|
+
signature = request.headers.get("X-Kortana-Signature", "")
|
|
59
|
+
event = client.validate_webhook(payload.decode(), signature)
|
|
60
|
+
if event["type"] == "transfer.completed":
|
|
61
|
+
print("Transfer done!", event["data"])
|
|
62
|
+
return {"received": True}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Modules
|
|
66
|
+
|
|
67
|
+
| Module | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `client.blockchain` | Read-only blockchain data (blocks, txs, gas) |
|
|
70
|
+
| `client.contracts` | Deploy and interact with smart contracts |
|
|
71
|
+
| `client.wallets` | HD wallet management and transfers |
|
|
72
|
+
| `client.payments` | Payment intents, invoices, subscriptions |
|
|
73
|
+
| `client.banking` | Customer and bank account management |
|
|
74
|
+
| `client.cards` | Virtual and physical card issuance |
|
|
75
|
+
| `client.compliance` | KYC, KYB, and AML screening |
|
|
76
|
+
| `client.settlement` | Ledger settlement and treasury |
|
|
77
|
+
| `client.merchants` | Merchant onboarding and management |
|
|
78
|
+
|
|
79
|
+
## Requirements
|
|
80
|
+
|
|
81
|
+
- Python 3.9+
|
|
82
|
+
- `httpx >= 0.27`
|
|
83
|
+
- `pydantic >= 2.5`
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT © Kortana Labs
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
kortana-dev-sdk — Official Kortana Python SDK.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from kortana import kortana
|
|
6
|
+
|
|
7
|
+
async with kortana(api_key="kt_live_...", environment="mainnet") as client:
|
|
8
|
+
balance = await client.wallets.get_balance()
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from .client import AsyncKortanaClient
|
|
14
|
+
from .config import KortanaConfig
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
KortanaError,
|
|
17
|
+
KortanaConfigError,
|
|
18
|
+
KortanaAuthError,
|
|
19
|
+
KortanaNetworkError,
|
|
20
|
+
KortanaRateLimitError,
|
|
21
|
+
KortanaValidationError,
|
|
22
|
+
KortanaNotFoundError,
|
|
23
|
+
KortanaConflictError,
|
|
24
|
+
KortanaWebhookSignatureError,
|
|
25
|
+
KortanaServerError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "1.0.0"
|
|
29
|
+
__all__ = [
|
|
30
|
+
"kortana",
|
|
31
|
+
"AsyncKortanaClient",
|
|
32
|
+
"KortanaConfig",
|
|
33
|
+
"KortanaError",
|
|
34
|
+
"KortanaConfigError",
|
|
35
|
+
"KortanaAuthError",
|
|
36
|
+
"KortanaNetworkError",
|
|
37
|
+
"KortanaRateLimitError",
|
|
38
|
+
"KortanaValidationError",
|
|
39
|
+
"KortanaNotFoundError",
|
|
40
|
+
"KortanaConflictError",
|
|
41
|
+
"KortanaWebhookSignatureError",
|
|
42
|
+
"KortanaServerError",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def kortana(
|
|
47
|
+
api_key: str,
|
|
48
|
+
environment: str = "mainnet",
|
|
49
|
+
**kwargs: object,
|
|
50
|
+
) -> AsyncKortanaClient:
|
|
51
|
+
"""
|
|
52
|
+
Factory function to create a Kortana client.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
api_key: Your Kortana API key (kt_live_... or kt_test_...).
|
|
56
|
+
environment: 'mainnet' or 'testnet'. Defaults to 'mainnet'.
|
|
57
|
+
**kwargs: Additional config overrides (timeout, max_retries, webhook_secret, etc.).
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
AsyncKortanaClient that supports use as an async context manager.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
async with kortana(api_key=os.environ["KORTANA_API_KEY"]) as client:
|
|
64
|
+
balance = await client.wallets.get_balance()
|
|
65
|
+
"""
|
|
66
|
+
config = KortanaConfig(api_key=api_key, environment=environment, **kwargs) # type: ignore[arg-type]
|
|
67
|
+
return AsyncKortanaClient(config)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Auth sub-package for Kortana SDK."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authenticator — handles API key pass-through and JWT token auto-refresh.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from kortana.config import KortanaConfig
|
|
12
|
+
from kortana.exceptions import KortanaAuthError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Authenticator:
|
|
16
|
+
"""
|
|
17
|
+
Manages authentication tokens for the Kortana API.
|
|
18
|
+
|
|
19
|
+
- API keys (kt_live_... / kt_test_...) are passed through directly.
|
|
20
|
+
- Other credentials trigger JWT token acquisition with auto-refresh
|
|
21
|
+
60 seconds before expiry.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_TOKEN_TTL_SECONDS = 900 # 15 minutes
|
|
25
|
+
_REFRESH_BUFFER_SECONDS = 60
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: KortanaConfig) -> None:
|
|
28
|
+
self._config = config
|
|
29
|
+
self._token: Optional[str] = None
|
|
30
|
+
self._token_expiry: float = 0.0
|
|
31
|
+
|
|
32
|
+
async def get_token(self) -> str:
|
|
33
|
+
"""Return a valid authentication token."""
|
|
34
|
+
if self._config.api_key.startswith("kt_"):
|
|
35
|
+
# Direct API key — pass through as Bearer token
|
|
36
|
+
return self._config.api_key
|
|
37
|
+
|
|
38
|
+
# JWT mode — refresh if expired or expiring soon
|
|
39
|
+
if self._token and time.monotonic() < self._token_expiry - self._REFRESH_BUFFER_SECONDS:
|
|
40
|
+
return self._token
|
|
41
|
+
|
|
42
|
+
return await self._refresh_token()
|
|
43
|
+
|
|
44
|
+
async def _refresh_token(self) -> str:
|
|
45
|
+
"""Obtain a new JWT from the auth endpoint."""
|
|
46
|
+
auth_base = self._config.get_base_url().replace("/v1", "")
|
|
47
|
+
url = f"{auth_base}/auth/token"
|
|
48
|
+
|
|
49
|
+
async with httpx.AsyncClient(timeout=self._config.connect_timeout) as client:
|
|
50
|
+
response = await client.post(
|
|
51
|
+
url,
|
|
52
|
+
headers={
|
|
53
|
+
"Authorization": f"Bearer {self._config.api_key}",
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not response.is_success:
|
|
59
|
+
raise KortanaAuthError("Failed to authenticate with Kortana API")
|
|
60
|
+
|
|
61
|
+
data = response.json()
|
|
62
|
+
token: str = data["data"]["token"]
|
|
63
|
+
self._token = token
|
|
64
|
+
self._token_expiry = time.monotonic() + self._TOKEN_TTL_SECONDS
|
|
65
|
+
return self._token
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Async Kortana client.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from kortana.auth.authenticator import Authenticator
|
|
9
|
+
from kortana.config import KortanaConfig
|
|
10
|
+
from kortana.http.client import AsyncHttpClient
|
|
11
|
+
from kortana.modules.banking import BankingModule
|
|
12
|
+
from kortana.modules.blockchain import BlockchainModule
|
|
13
|
+
from kortana.modules.cards import CardsModule
|
|
14
|
+
from kortana.modules.compliance import ComplianceModule
|
|
15
|
+
from kortana.modules.contracts import ContractsModule
|
|
16
|
+
from kortana.modules.merchants import MerchantsModule
|
|
17
|
+
from kortana.modules.payments import PaymentsModule
|
|
18
|
+
from kortana.modules.settlement import SettlementModule
|
|
19
|
+
from kortana.modules.wallets import WalletsModule
|
|
20
|
+
from kortana.webhooks.verifier import Webhook
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncKortanaClient:
|
|
24
|
+
"""
|
|
25
|
+
Official asynchronous client for the Kortana blockchain platform.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: KortanaConfig) -> None:
|
|
29
|
+
self._config = config
|
|
30
|
+
self._auth = Authenticator(config)
|
|
31
|
+
self._http = AsyncHttpClient(config, self._auth)
|
|
32
|
+
self._webhook_verifier = Webhook(config.webhook_secret)
|
|
33
|
+
|
|
34
|
+
# Initialize all domain modules
|
|
35
|
+
self.blockchain = BlockchainModule(self._http)
|
|
36
|
+
self.wallets = WalletsModule(self._http)
|
|
37
|
+
self.contracts = ContractsModule(self._http)
|
|
38
|
+
self.payments = PaymentsModule(self._http)
|
|
39
|
+
self.merchants = MerchantsModule(self._http)
|
|
40
|
+
self.banking = BankingModule(self._http)
|
|
41
|
+
self.cards = CardsModule(self._http)
|
|
42
|
+
self.compliance = ComplianceModule(self._http)
|
|
43
|
+
self.settlement = SettlementModule(self._http)
|
|
44
|
+
|
|
45
|
+
def validate_webhook(self, payload: str, signature: str) -> dict[str, Any]:
|
|
46
|
+
"""Validate webhook payload and signature."""
|
|
47
|
+
return self._webhook_verifier.verify(payload, signature)
|
|
48
|
+
|
|
49
|
+
async def get_health(self) -> dict[str, Any]:
|
|
50
|
+
"""Get the health status of the API backend."""
|
|
51
|
+
return await self._http.get("/health") # type: ignore[no-any-return]
|
|
52
|
+
|
|
53
|
+
async def __aenter__(self) -> AsyncKortanaClient:
|
|
54
|
+
await self._http.__aenter__()
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
58
|
+
await self._http.__aexit__(*args)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kortana SDK configuration using Pydantic settings.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from pydantic import field_validator, model_validator
|
|
8
|
+
from pydantic_settings import BaseSettings
|
|
9
|
+
|
|
10
|
+
BASE_URLS: dict[str, str] = {
|
|
11
|
+
"mainnet": "https://api.oneworld.kortana.network/api/v1",
|
|
12
|
+
"testnet": "https://api.oneworld.kortana.network/api/v1",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KortanaConfig(BaseSettings):
|
|
17
|
+
"""
|
|
18
|
+
Kortana SDK configuration.
|
|
19
|
+
|
|
20
|
+
All fields can be overridden via environment variables prefixed with KORTANA_.
|
|
21
|
+
Example: KORTANA_API_KEY, KORTANA_ENVIRONMENT, KORTANA_TIMEOUT
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
api_key: str
|
|
25
|
+
environment: str = "mainnet"
|
|
26
|
+
base_url: Optional[str] = None
|
|
27
|
+
timeout: float = 30.0
|
|
28
|
+
connect_timeout: float = 5.0
|
|
29
|
+
max_retries: int = 3
|
|
30
|
+
retry_delay: float = 1.0
|
|
31
|
+
max_retry_delay: float = 32.0
|
|
32
|
+
log_level: str = "info"
|
|
33
|
+
webhook_secret: Optional[str] = None
|
|
34
|
+
user_agent: Optional[str] = None
|
|
35
|
+
telemetry: bool = True
|
|
36
|
+
|
|
37
|
+
model_config = {"env_prefix": "KORTANA_", "populate_by_name": True} # type: ignore[assignment]
|
|
38
|
+
|
|
39
|
+
@field_validator("environment")
|
|
40
|
+
@classmethod
|
|
41
|
+
def validate_environment(cls, v: str) -> str:
|
|
42
|
+
if v not in ("mainnet", "testnet"):
|
|
43
|
+
raise ValueError(f"environment must be 'mainnet' or 'testnet', got {v!r}")
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
@field_validator("api_key")
|
|
47
|
+
@classmethod
|
|
48
|
+
def validate_api_key(cls, v: str) -> str:
|
|
49
|
+
if not v or not v.strip():
|
|
50
|
+
raise ValueError("api_key must not be empty")
|
|
51
|
+
return v.strip()
|
|
52
|
+
|
|
53
|
+
@field_validator("timeout")
|
|
54
|
+
@classmethod
|
|
55
|
+
def validate_timeout(cls, v: float) -> float:
|
|
56
|
+
if v <= 0:
|
|
57
|
+
raise ValueError("timeout must be positive")
|
|
58
|
+
return v
|
|
59
|
+
|
|
60
|
+
@field_validator("max_retries")
|
|
61
|
+
@classmethod
|
|
62
|
+
def validate_max_retries(cls, v: int) -> int:
|
|
63
|
+
if v < 0:
|
|
64
|
+
raise ValueError("max_retries must be non-negative")
|
|
65
|
+
return v
|
|
66
|
+
|
|
67
|
+
def get_base_url(self) -> str:
|
|
68
|
+
"""Return the resolved base URL."""
|
|
69
|
+
if self.base_url:
|
|
70
|
+
return self.base_url.rstrip("/")
|
|
71
|
+
return BASE_URLS[self.environment]
|
|
72
|
+
|
|
73
|
+
def get_user_agent(self) -> str:
|
|
74
|
+
"""Return the User-Agent header value."""
|
|
75
|
+
return self.user_agent or "kortana-sdk-python/1.0.0"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kortana SDK exception hierarchy.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KortanaError(Exception):
|
|
10
|
+
"""Base exception for all Kortana SDK errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
code: str = "UNKNOWN_ERROR",
|
|
16
|
+
request_id: Optional[str] = None,
|
|
17
|
+
docs_url: Optional[str] = None,
|
|
18
|
+
details: Optional[dict[str, Any]] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.code = code
|
|
23
|
+
self.request_id = request_id
|
|
24
|
+
self.docs_url = docs_url
|
|
25
|
+
self.details = details or {}
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return f"{self.__class__.__name__}(code={self.code!r}, message={self.message!r})"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class KortanaConfigError(KortanaError):
|
|
32
|
+
"""Raised when the SDK is misconfigured."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str) -> None:
|
|
35
|
+
super().__init__(
|
|
36
|
+
message,
|
|
37
|
+
code="CONFIG_ERROR",
|
|
38
|
+
docs_url="https://docs.kortana.io/errors/CONFIG_ERROR",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class KortanaAuthError(KortanaError):
|
|
43
|
+
"""Raised on authentication failures (401)."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, request_id: Optional[str] = None) -> None:
|
|
46
|
+
super().__init__(
|
|
47
|
+
message,
|
|
48
|
+
code="AUTH_ERROR",
|
|
49
|
+
request_id=request_id,
|
|
50
|
+
docs_url="https://docs.kortana.io/errors/AUTH_ERROR",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class KortanaNetworkError(KortanaError):
|
|
55
|
+
"""Raised on network-level failures (timeouts, connection errors)."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
message: str,
|
|
60
|
+
cause: Optional[Exception] = None,
|
|
61
|
+
request_id: Optional[str] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__(
|
|
64
|
+
message,
|
|
65
|
+
code="NETWORK_ERROR",
|
|
66
|
+
request_id=request_id,
|
|
67
|
+
docs_url="https://docs.kortana.io/errors/NETWORK_ERROR",
|
|
68
|
+
)
|
|
69
|
+
self.cause = cause
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class KortanaRateLimitError(KortanaError):
|
|
73
|
+
"""Raised when the API rate limit is exceeded (429)."""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
message: str,
|
|
78
|
+
retry_after: int = 60,
|
|
79
|
+
request_id: Optional[str] = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
super().__init__(
|
|
82
|
+
message,
|
|
83
|
+
code="RATE_LIMIT_ERROR",
|
|
84
|
+
request_id=request_id,
|
|
85
|
+
docs_url="https://docs.kortana.io/errors/RATE_LIMIT_ERROR",
|
|
86
|
+
)
|
|
87
|
+
self.retry_after = retry_after
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class KortanaValidationError(KortanaError):
|
|
91
|
+
"""Raised when request validation fails (400)."""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
message: str,
|
|
96
|
+
fields: Optional[list[dict[str, str]]] = None,
|
|
97
|
+
request_id: Optional[str] = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
super().__init__(
|
|
100
|
+
message,
|
|
101
|
+
code="VALIDATION_ERROR",
|
|
102
|
+
request_id=request_id,
|
|
103
|
+
docs_url="https://docs.kortana.io/errors/VALIDATION_ERROR",
|
|
104
|
+
)
|
|
105
|
+
self.fields = fields or []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class KortanaNotFoundError(KortanaError):
|
|
109
|
+
"""Raised when a resource is not found (404)."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, message: str, request_id: Optional[str] = None) -> None:
|
|
112
|
+
super().__init__(
|
|
113
|
+
message,
|
|
114
|
+
code="NOT_FOUND_ERROR",
|
|
115
|
+
request_id=request_id,
|
|
116
|
+
docs_url="https://docs.kortana.io/errors/NOT_FOUND_ERROR",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class KortanaConflictError(KortanaError):
|
|
121
|
+
"""Raised on resource conflicts (409)."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, message: str, request_id: Optional[str] = None) -> None:
|
|
124
|
+
super().__init__(
|
|
125
|
+
message,
|
|
126
|
+
code="CONFLICT_ERROR",
|
|
127
|
+
request_id=request_id,
|
|
128
|
+
docs_url="https://docs.kortana.io/errors/CONFLICT_ERROR",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class KortanaWebhookSignatureError(KortanaError):
|
|
133
|
+
"""Raised when webhook signature verification fails."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, message: str) -> None:
|
|
136
|
+
super().__init__(
|
|
137
|
+
message,
|
|
138
|
+
code="WEBHOOK_SIGNATURE_ERROR",
|
|
139
|
+
docs_url="https://docs.kortana.io/errors/WEBHOOK_SIGNATURE_ERROR",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class KortanaServerError(KortanaError):
|
|
144
|
+
"""Raised on server-side errors (5xx)."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, message: str, request_id: Optional[str] = None) -> None:
|
|
147
|
+
super().__init__(
|
|
148
|
+
message,
|
|
149
|
+
code="SERVER_ERROR",
|
|
150
|
+
request_id=request_id,
|
|
151
|
+
docs_url="https://docs.kortana.io/errors/SERVER_ERROR",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def raise_for_response(
|
|
156
|
+
status_code: int,
|
|
157
|
+
error_body: dict[str, Any],
|
|
158
|
+
headers: dict[str, str],
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Map HTTP status codes and error bodies to specific KortanaError subclasses."""
|
|
161
|
+
error = error_body.get("error", {})
|
|
162
|
+
message = error.get("message", "An unknown error occurred")
|
|
163
|
+
request_id = error.get("request_id")
|
|
164
|
+
fields = error.get("fields")
|
|
165
|
+
|
|
166
|
+
if status_code == 400:
|
|
167
|
+
raise KortanaValidationError(message, fields=fields, request_id=request_id)
|
|
168
|
+
if status_code == 401:
|
|
169
|
+
raise KortanaAuthError(message, request_id=request_id)
|
|
170
|
+
if status_code == 404:
|
|
171
|
+
raise KortanaNotFoundError(message, request_id=request_id)
|
|
172
|
+
if status_code == 409:
|
|
173
|
+
raise KortanaConflictError(message, request_id=request_id)
|
|
174
|
+
if status_code == 429:
|
|
175
|
+
retry_after = int(headers.get("retry-after", "60"))
|
|
176
|
+
raise KortanaRateLimitError(message, retry_after=retry_after, request_id=request_id)
|
|
177
|
+
if status_code >= 500:
|
|
178
|
+
raise KortanaServerError(message, request_id=request_id)
|
|
179
|
+
|
|
180
|
+
code = error.get("code", "UNKNOWN_ERROR")
|
|
181
|
+
docs_url = error.get("docs_url")
|
|
182
|
+
details = error.get("details")
|
|
183
|
+
raise KortanaError(message, code=code, request_id=request_id, docs_url=docs_url, details=details)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP sub-package for Kortana SDK."""
|