kuberoute 0.1.0__py3-none-any.whl
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.
- kuberoute/__init__.py +49 -0
- kuberoute/async_client.py +182 -0
- kuberoute/client.py +216 -0
- kuberoute/exceptions.py +38 -0
- kuberoute/models.py +91 -0
- kuberoute-0.1.0.dist-info/METADATA +110 -0
- kuberoute-0.1.0.dist-info/RECORD +8 -0
- kuberoute-0.1.0.dist-info/WHEEL +4 -0
kuberoute/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Kuberoute Python SDK."""
|
|
2
|
+
|
|
3
|
+
from .client import KubeRoute, ClustersResource, DecisionsResource, CostsResource
|
|
4
|
+
from .async_client import AsyncKubeRoute, AsyncClustersResource, AsyncDecisionsResource, AsyncCostsResource
|
|
5
|
+
from .models import (
|
|
6
|
+
User,
|
|
7
|
+
TenantInfo,
|
|
8
|
+
CurrentUserResponse,
|
|
9
|
+
Cluster,
|
|
10
|
+
ClusterListResponse,
|
|
11
|
+
Decision,
|
|
12
|
+
DecisionListResponse,
|
|
13
|
+
CostSummary,
|
|
14
|
+
HealthResponse,
|
|
15
|
+
AuthResponse,
|
|
16
|
+
)
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
KubeRouteError,
|
|
19
|
+
AuthenticationError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
ServerError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"KubeRoute",
|
|
27
|
+
"AsyncKubeRoute",
|
|
28
|
+
"ClustersResource",
|
|
29
|
+
"DecisionsResource",
|
|
30
|
+
"CostsResource",
|
|
31
|
+
"AsyncClustersResource",
|
|
32
|
+
"AsyncDecisionsResource",
|
|
33
|
+
"AsyncCostsResource",
|
|
34
|
+
"User",
|
|
35
|
+
"TenantInfo",
|
|
36
|
+
"CurrentUserResponse",
|
|
37
|
+
"Cluster",
|
|
38
|
+
"ClusterListResponse",
|
|
39
|
+
"Decision",
|
|
40
|
+
"DecisionListResponse",
|
|
41
|
+
"CostSummary",
|
|
42
|
+
"HealthResponse",
|
|
43
|
+
"AuthResponse",
|
|
44
|
+
"KubeRouteError",
|
|
45
|
+
"AuthenticationError",
|
|
46
|
+
"NotFoundError",
|
|
47
|
+
"ValidationError",
|
|
48
|
+
"ServerError",
|
|
49
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Asynchronous Kuberoute Client."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
CurrentUserResponse,
|
|
9
|
+
Cluster,
|
|
10
|
+
ClusterListResponse,
|
|
11
|
+
Decision,
|
|
12
|
+
DecisionListResponse,
|
|
13
|
+
CostSummary,
|
|
14
|
+
HealthResponse,
|
|
15
|
+
AuthResponse,
|
|
16
|
+
)
|
|
17
|
+
from .client import _handle_response
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncClustersResource:
|
|
21
|
+
"""Async operations on Kubernetes clusters."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, client: "AsyncKubeRoute") -> None:
|
|
24
|
+
self._client = client
|
|
25
|
+
|
|
26
|
+
async def list(self, page: int = 1, limit: int = 20) -> ClusterListResponse:
|
|
27
|
+
"""List all clusters registered with Kuberoute."""
|
|
28
|
+
data = await self._client._get("/api/v1/clusters", params={"page": page, "limit": limit})
|
|
29
|
+
return ClusterListResponse.model_validate(data)
|
|
30
|
+
|
|
31
|
+
async def get(self, cluster_id: str) -> Cluster:
|
|
32
|
+
"""Get details of a specific cluster by ID."""
|
|
33
|
+
data = await self._client._get(f"/api/v1/clusters/{cluster_id}")
|
|
34
|
+
return Cluster.model_validate(data)
|
|
35
|
+
|
|
36
|
+
async def create(self, name: str, provider: Optional[str] = None, region: Optional[str] = None) -> Cluster:
|
|
37
|
+
"""Register a new cluster."""
|
|
38
|
+
payload: Dict[str, Any] = {"name": name}
|
|
39
|
+
if provider:
|
|
40
|
+
payload["provider"] = provider
|
|
41
|
+
if region:
|
|
42
|
+
payload["region"] = region
|
|
43
|
+
data = await self._client._post("/api/v1/clusters", json_data=payload)
|
|
44
|
+
return Cluster.model_validate(data)
|
|
45
|
+
|
|
46
|
+
async def delete(self, cluster_id: str) -> None:
|
|
47
|
+
"""Deregister and delete a cluster."""
|
|
48
|
+
await self._client._delete(f"/api/v1/clusters/{cluster_id}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AsyncDecisionsResource:
|
|
52
|
+
"""Async operations on AI routing decisions."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, client: "AsyncKubeRoute") -> None:
|
|
55
|
+
self._client = client
|
|
56
|
+
|
|
57
|
+
async def list(
|
|
58
|
+
self,
|
|
59
|
+
cluster_id: str,
|
|
60
|
+
status: Optional[str] = None,
|
|
61
|
+
page: int = 1,
|
|
62
|
+
limit: int = 20,
|
|
63
|
+
) -> DecisionListResponse:
|
|
64
|
+
"""List AI placement and routing decisions for a cluster."""
|
|
65
|
+
params: Dict[str, Any] = {"page": page, "limit": limit}
|
|
66
|
+
if status:
|
|
67
|
+
params["status"] = status
|
|
68
|
+
data = await self._client._get(f"/api/v1/clusters/{cluster_id}/decisions", params=params)
|
|
69
|
+
return DecisionListResponse.model_validate(data)
|
|
70
|
+
|
|
71
|
+
async def get(self, cluster_id: str, decision_id: str) -> Decision:
|
|
72
|
+
"""Get a single decision by ID."""
|
|
73
|
+
data = await self._client._get(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}")
|
|
74
|
+
return Decision.model_validate(data)
|
|
75
|
+
|
|
76
|
+
async def apply(self, cluster_id: str, decision_id: str) -> bool:
|
|
77
|
+
"""Apply an AI routing decision to the cluster."""
|
|
78
|
+
data = await self._client._post(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}/apply")
|
|
79
|
+
return data.get("status") == "applied"
|
|
80
|
+
|
|
81
|
+
async def reject(self, cluster_id: str, decision_id: str) -> bool:
|
|
82
|
+
"""Reject an AI routing decision."""
|
|
83
|
+
data = await self._client._post(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}/reject")
|
|
84
|
+
return data.get("status") == "rejected"
|
|
85
|
+
|
|
86
|
+
async def revert(self, cluster_id: str, decision_id: str) -> Decision:
|
|
87
|
+
"""Revert a previously applied decision."""
|
|
88
|
+
data = await self._client._post(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}/revert")
|
|
89
|
+
return Decision.model_validate(data)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AsyncCostsResource:
|
|
93
|
+
"""Async operations on infrastructure cost summaries and analytics."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, client: "AsyncKubeRoute") -> None:
|
|
96
|
+
self._client = client
|
|
97
|
+
|
|
98
|
+
async def get_total(self) -> CostSummary:
|
|
99
|
+
"""Get aggregate cost summary across all registered clusters."""
|
|
100
|
+
data = await self._client._get("/api/v1/costs")
|
|
101
|
+
return CostSummary.model_validate(data)
|
|
102
|
+
|
|
103
|
+
async def get_cluster(self, cluster_id: str) -> CostSummary:
|
|
104
|
+
"""Get cost summary and estimated savings for a specific cluster."""
|
|
105
|
+
data = await self._client._get(f"/api/v1/clusters/{cluster_id}/costs")
|
|
106
|
+
return CostSummary.model_validate(data)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncKubeRoute:
|
|
110
|
+
"""Asynchronous client for Kuberoute AI Orchestrator API."""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
base_url: Optional[str] = None,
|
|
115
|
+
token: Optional[str] = None,
|
|
116
|
+
timeout: float = 30.0,
|
|
117
|
+
) -> None:
|
|
118
|
+
self.base_url = (
|
|
119
|
+
base_url
|
|
120
|
+
or os.environ.get("KUBEROUTE_API_URL")
|
|
121
|
+
or "http://localhost:8080"
|
|
122
|
+
).rstrip("/")
|
|
123
|
+
self.token = token or os.environ.get("KUBEROUTE_TOKEN")
|
|
124
|
+
self.timeout = timeout
|
|
125
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
126
|
+
|
|
127
|
+
self.clusters = AsyncClustersResource(self)
|
|
128
|
+
self.decisions = AsyncDecisionsResource(self)
|
|
129
|
+
self.costs = AsyncCostsResource(self)
|
|
130
|
+
|
|
131
|
+
def _headers(self) -> Dict[str, str]:
|
|
132
|
+
headers = {
|
|
133
|
+
"Accept": "application/json",
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
}
|
|
136
|
+
if self.token:
|
|
137
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
138
|
+
return headers
|
|
139
|
+
|
|
140
|
+
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
141
|
+
url = f"{self.base_url}{path}"
|
|
142
|
+
resp = await self._client.get(url, headers=self._headers(), params=params)
|
|
143
|
+
return _handle_response(resp)
|
|
144
|
+
|
|
145
|
+
async def _post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
146
|
+
url = f"{self.base_url}{path}"
|
|
147
|
+
resp = await self._client.post(url, headers=self._headers(), json=json_data, params=params)
|
|
148
|
+
return _handle_response(resp)
|
|
149
|
+
|
|
150
|
+
async def _delete(self, path: str) -> Any:
|
|
151
|
+
url = f"{self.base_url}{path}"
|
|
152
|
+
resp = await self._client.delete(url, headers=self._headers())
|
|
153
|
+
return _handle_response(resp)
|
|
154
|
+
|
|
155
|
+
async def health(self) -> HealthResponse:
|
|
156
|
+
"""Check Orchestrator health status."""
|
|
157
|
+
data = await self._get("/health")
|
|
158
|
+
return HealthResponse.model_validate(data)
|
|
159
|
+
|
|
160
|
+
async def login_dev(self) -> AuthResponse:
|
|
161
|
+
"""Perform dev authentication (local dev only) and save JWT token."""
|
|
162
|
+
url = f"{self.base_url}/auth/dev?format=json"
|
|
163
|
+
resp = await self._client.post(url, headers={"Accept": "application/json"})
|
|
164
|
+
data = _handle_response(resp)
|
|
165
|
+
auth_resp = AuthResponse.model_validate(data)
|
|
166
|
+
self.token = auth_resp.token
|
|
167
|
+
return auth_resp
|
|
168
|
+
|
|
169
|
+
async def me(self) -> CurrentUserResponse:
|
|
170
|
+
"""Get current authenticated user and tenant info."""
|
|
171
|
+
data = await self._get("/api/v1/me")
|
|
172
|
+
return CurrentUserResponse.model_validate(data)
|
|
173
|
+
|
|
174
|
+
async def close(self) -> None:
|
|
175
|
+
"""Close the underlying HTTP client."""
|
|
176
|
+
await self._client.aclose()
|
|
177
|
+
|
|
178
|
+
async def __aenter__(self) -> "AsyncKubeRoute":
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
182
|
+
await self.close()
|
kuberoute/client.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Synchronous Kuberoute Client."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
User,
|
|
9
|
+
CurrentUserResponse,
|
|
10
|
+
Cluster,
|
|
11
|
+
ClusterListResponse,
|
|
12
|
+
Decision,
|
|
13
|
+
DecisionListResponse,
|
|
14
|
+
CostSummary,
|
|
15
|
+
HealthResponse,
|
|
16
|
+
AuthResponse,
|
|
17
|
+
)
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
KubeRouteError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
NotFoundError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
ServerError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _handle_response(resp: httpx.Response) -> Any:
|
|
28
|
+
if resp.status_code == 204:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
data = resp.json()
|
|
32
|
+
except Exception:
|
|
33
|
+
data = {"raw": resp.text}
|
|
34
|
+
|
|
35
|
+
if resp.status_code >= 400:
|
|
36
|
+
err_msg = data.get("error") if isinstance(data, dict) else str(data)
|
|
37
|
+
if not err_msg:
|
|
38
|
+
err_msg = f"HTTP {resp.status_code}"
|
|
39
|
+
|
|
40
|
+
if resp.status_code in (401, 403):
|
|
41
|
+
raise AuthenticationError(err_msg, status_code=resp.status_code, details=data)
|
|
42
|
+
elif resp.status_code == 404:
|
|
43
|
+
raise NotFoundError(err_msg, status_code=resp.status_code, details=data)
|
|
44
|
+
elif resp.status_code == 400:
|
|
45
|
+
raise ValidationError(err_msg, status_code=resp.status_code, details=data)
|
|
46
|
+
elif resp.status_code >= 500:
|
|
47
|
+
raise ServerError(err_msg, status_code=resp.status_code, details=data)
|
|
48
|
+
else:
|
|
49
|
+
raise KubeRouteError(err_msg, status_code=resp.status_code, details=data)
|
|
50
|
+
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ClustersResource:
|
|
55
|
+
"""Operations on Kubernetes clusters."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, client: "KubeRoute") -> None:
|
|
58
|
+
self._client = client
|
|
59
|
+
|
|
60
|
+
def list(self, page: int = 1, limit: int = 20) -> ClusterListResponse:
|
|
61
|
+
"""List all clusters registered with Kuberoute."""
|
|
62
|
+
data = self._client._get("/api/v1/clusters", params={"page": page, "limit": limit})
|
|
63
|
+
return ClusterListResponse.model_validate(data)
|
|
64
|
+
|
|
65
|
+
def get(self, cluster_id: str) -> Cluster:
|
|
66
|
+
"""Get details of a specific cluster by ID."""
|
|
67
|
+
data = self._client._get(f"/api/v1/clusters/{cluster_id}")
|
|
68
|
+
return Cluster.model_validate(data)
|
|
69
|
+
|
|
70
|
+
def create(self, name: str, provider: Optional[str] = None, region: Optional[str] = None) -> Cluster:
|
|
71
|
+
"""Register a new cluster."""
|
|
72
|
+
payload: Dict[str, Any] = {"name": name}
|
|
73
|
+
if provider:
|
|
74
|
+
payload["provider"] = provider
|
|
75
|
+
if region:
|
|
76
|
+
payload["region"] = region
|
|
77
|
+
data = self._client._post("/api/v1/clusters", json_data=payload)
|
|
78
|
+
return Cluster.model_validate(data)
|
|
79
|
+
|
|
80
|
+
def delete(self, cluster_id: str) -> None:
|
|
81
|
+
"""Deregister and delete a cluster."""
|
|
82
|
+
self._client._delete(f"/api/v1/clusters/{cluster_id}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DecisionsResource:
|
|
86
|
+
"""Operations on AI routing decisions."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, client: "KubeRoute") -> None:
|
|
89
|
+
self._client = client
|
|
90
|
+
|
|
91
|
+
def list(
|
|
92
|
+
self,
|
|
93
|
+
cluster_id: str,
|
|
94
|
+
status: Optional[str] = None,
|
|
95
|
+
page: int = 1,
|
|
96
|
+
limit: int = 20,
|
|
97
|
+
) -> DecisionListResponse:
|
|
98
|
+
"""List AI placement and routing decisions for a cluster."""
|
|
99
|
+
params: Dict[str, Any] = {"page": page, "limit": limit}
|
|
100
|
+
if status:
|
|
101
|
+
params["status"] = status
|
|
102
|
+
data = self._client._get(f"/api/v1/clusters/{cluster_id}/decisions", params=params)
|
|
103
|
+
return DecisionListResponse.model_validate(data)
|
|
104
|
+
|
|
105
|
+
def get(self, cluster_id: str, decision_id: str) -> Decision:
|
|
106
|
+
"""Get a single decision by ID."""
|
|
107
|
+
data = self._client._get(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}")
|
|
108
|
+
return Decision.model_validate(data)
|
|
109
|
+
|
|
110
|
+
def apply(self, cluster_id: str, decision_id: str) -> bool:
|
|
111
|
+
"""Apply an AI routing decision to the cluster."""
|
|
112
|
+
data = self._client._post(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}/apply")
|
|
113
|
+
return data.get("status") == "applied"
|
|
114
|
+
|
|
115
|
+
def reject(self, cluster_id: str, decision_id: str) -> bool:
|
|
116
|
+
"""Reject an AI routing decision."""
|
|
117
|
+
data = self._client._post(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}/reject")
|
|
118
|
+
return data.get("status") == "rejected"
|
|
119
|
+
|
|
120
|
+
def revert(self, cluster_id: str, decision_id: str) -> Decision:
|
|
121
|
+
"""Revert a previously applied decision."""
|
|
122
|
+
data = self._client._post(f"/api/v1/clusters/{cluster_id}/decisions/{decision_id}/revert")
|
|
123
|
+
return Decision.model_validate(data)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class CostsResource:
|
|
127
|
+
"""Operations on infrastructure cost summaries and analytics."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, client: "KubeRoute") -> None:
|
|
130
|
+
self._client = client
|
|
131
|
+
|
|
132
|
+
def get_total(self) -> CostSummary:
|
|
133
|
+
"""Get aggregate cost summary across all registered clusters."""
|
|
134
|
+
data = self._client._get("/api/v1/costs")
|
|
135
|
+
return CostSummary.model_validate(data)
|
|
136
|
+
|
|
137
|
+
def get_cluster(self, cluster_id: str) -> CostSummary:
|
|
138
|
+
"""Get cost summary and estimated savings for a specific cluster."""
|
|
139
|
+
data = self._client._get(f"/api/v1/clusters/{cluster_id}/costs")
|
|
140
|
+
return CostSummary.model_validate(data)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class KubeRoute:
|
|
144
|
+
"""Synchronous client for Kuberoute AI Orchestrator API."""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
base_url: Optional[str] = None,
|
|
149
|
+
token: Optional[str] = None,
|
|
150
|
+
timeout: float = 30.0,
|
|
151
|
+
) -> None:
|
|
152
|
+
self.base_url = (
|
|
153
|
+
base_url
|
|
154
|
+
or os.environ.get("KUBEROUTE_API_URL")
|
|
155
|
+
or "http://localhost:8080"
|
|
156
|
+
).rstrip("/")
|
|
157
|
+
self.token = token or os.environ.get("KUBEROUTE_TOKEN")
|
|
158
|
+
self.timeout = timeout
|
|
159
|
+
self._client = httpx.Client(timeout=self.timeout)
|
|
160
|
+
|
|
161
|
+
self.clusters = ClustersResource(self)
|
|
162
|
+
self.decisions = DecisionsResource(self)
|
|
163
|
+
self.costs = CostsResource(self)
|
|
164
|
+
|
|
165
|
+
def _headers(self) -> Dict[str, str]:
|
|
166
|
+
headers = {
|
|
167
|
+
"Accept": "application/json",
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
}
|
|
170
|
+
if self.token:
|
|
171
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
172
|
+
return headers
|
|
173
|
+
|
|
174
|
+
def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
175
|
+
url = f"{self.base_url}{path}"
|
|
176
|
+
resp = self._client.get(url, headers=self._headers(), params=params)
|
|
177
|
+
return _handle_response(resp)
|
|
178
|
+
|
|
179
|
+
def _post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
180
|
+
url = f"{self.base_url}{path}"
|
|
181
|
+
resp = self._client.post(url, headers=self._headers(), json=json_data, params=params)
|
|
182
|
+
return _handle_response(resp)
|
|
183
|
+
|
|
184
|
+
def _delete(self, path: str) -> Any:
|
|
185
|
+
url = f"{self.base_url}{path}"
|
|
186
|
+
resp = self._client.delete(url, headers=self._headers())
|
|
187
|
+
return _handle_response(resp)
|
|
188
|
+
|
|
189
|
+
def health(self) -> HealthResponse:
|
|
190
|
+
"""Check Orchestrator health status."""
|
|
191
|
+
data = self._get("/health")
|
|
192
|
+
return HealthResponse.model_validate(data)
|
|
193
|
+
|
|
194
|
+
def login_dev(self) -> AuthResponse:
|
|
195
|
+
"""Perform dev authentication (local dev only) and save JWT token."""
|
|
196
|
+
url = f"{self.base_url}/auth/dev?format=json"
|
|
197
|
+
resp = self._client.post(url, headers={"Accept": "application/json"})
|
|
198
|
+
data = _handle_response(resp)
|
|
199
|
+
auth_resp = AuthResponse.model_validate(data)
|
|
200
|
+
self.token = auth_resp.token
|
|
201
|
+
return auth_resp
|
|
202
|
+
|
|
203
|
+
def me(self) -> CurrentUserResponse:
|
|
204
|
+
"""Get current authenticated user and tenant info."""
|
|
205
|
+
data = self._get("/api/v1/me")
|
|
206
|
+
return CurrentUserResponse.model_validate(data)
|
|
207
|
+
|
|
208
|
+
def close(self) -> None:
|
|
209
|
+
"""Close the underlying HTTP client."""
|
|
210
|
+
self._client.close()
|
|
211
|
+
|
|
212
|
+
def __enter__(self) -> "KubeRoute":
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
216
|
+
self.close()
|
kuberoute/exceptions.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Kuberoute SDK Exceptions."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KubeRouteError(Exception):
|
|
7
|
+
"""Base exception for all KubeRoute SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status_code: Optional[int] = None, details: Optional[Any] = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.details = details
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
if self.status_code:
|
|
17
|
+
return f"[{self.status_code}] {self.message}"
|
|
18
|
+
return self.message
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthenticationError(KubeRouteError):
|
|
22
|
+
"""Raised when authentication fails (401 Unauthorized or 403 Forbidden)."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NotFoundError(KubeRouteError):
|
|
27
|
+
"""Raised when a requested resource is not found (404)."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ValidationError(KubeRouteError):
|
|
32
|
+
"""Raised when request payload or parameters fail validation (400 Bad Request)."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ServerError(KubeRouteError):
|
|
37
|
+
"""Raised when the Kuberoute server returns an internal server error (500+)."""
|
|
38
|
+
pass
|
kuberoute/models.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Pydantic Models for Kuberoute SDK."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Dict, List, Optional, Any
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class User(BaseModel):
|
|
9
|
+
id: str
|
|
10
|
+
email: Optional[str] = None
|
|
11
|
+
name: Optional[str] = None
|
|
12
|
+
avatar_url: Optional[str] = None
|
|
13
|
+
role: Optional[str] = None
|
|
14
|
+
tenant_id: Optional[str] = None
|
|
15
|
+
created_at: Optional[datetime] = None
|
|
16
|
+
updated_at: Optional[datetime] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TenantInfo(BaseModel):
|
|
20
|
+
id: str
|
|
21
|
+
plan: str = "free"
|
|
22
|
+
task_credits: int = 0
|
|
23
|
+
task_credits_used: int = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CurrentUserResponse(BaseModel):
|
|
27
|
+
user: User
|
|
28
|
+
tenant: TenantInfo
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Cluster(BaseModel):
|
|
32
|
+
id: str
|
|
33
|
+
tenant_id: Optional[str] = None
|
|
34
|
+
name: str
|
|
35
|
+
provider: Optional[str] = None
|
|
36
|
+
region: Optional[str] = None
|
|
37
|
+
k8s_version: Optional[str] = None
|
|
38
|
+
agent_version: Optional[str] = None
|
|
39
|
+
status: Optional[str] = "active"
|
|
40
|
+
node_count: Optional[int] = 0
|
|
41
|
+
created_at: Optional[datetime] = None
|
|
42
|
+
updated_at: Optional[datetime] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ClusterListResponse(BaseModel):
|
|
46
|
+
clusters: List[Cluster] = Field(default_factory=list)
|
|
47
|
+
total: int = 0
|
|
48
|
+
page: int = 1
|
|
49
|
+
limit: int = 20
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Decision(BaseModel):
|
|
53
|
+
id: str
|
|
54
|
+
cluster_id: str
|
|
55
|
+
workload_id: Optional[str] = None
|
|
56
|
+
decision_type: Optional[str] = None
|
|
57
|
+
target_node_id: Optional[str] = None
|
|
58
|
+
confidence_score: Optional[float] = None
|
|
59
|
+
predicted_cost_savings: Optional[float] = None
|
|
60
|
+
predicted_latency_ms: Optional[int] = None
|
|
61
|
+
reason: Optional[str] = None
|
|
62
|
+
status: Optional[str] = "pending"
|
|
63
|
+
applied: Optional[bool] = False
|
|
64
|
+
applied_at: Optional[datetime] = None
|
|
65
|
+
applied_by: Optional[str] = None
|
|
66
|
+
revert_of: Optional[str] = None
|
|
67
|
+
created_at: Optional[datetime] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DecisionListResponse(BaseModel):
|
|
71
|
+
decisions: List[Decision] = Field(default_factory=list)
|
|
72
|
+
total: int = 0
|
|
73
|
+
page: int = 1
|
|
74
|
+
limit: int = 20
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CostSummary(BaseModel):
|
|
78
|
+
cluster_id: Optional[str] = None
|
|
79
|
+
total: float = 0.0
|
|
80
|
+
optimized: float = 0.0
|
|
81
|
+
savings: float = 0.0
|
|
82
|
+
breakdown: Dict[str, float] = Field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HealthResponse(BaseModel):
|
|
86
|
+
status: str = "ok"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AuthResponse(BaseModel):
|
|
90
|
+
user: User
|
|
91
|
+
token: str
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kuberoute
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Kuberoute AI Intelligent Routing & Orchestration
|
|
5
|
+
Author-email: Kuberoute AI Team <vikas.budde@hotmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: System Administrators
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: httpx>=0.24.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: respx>=0.20.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Kuberoute Python SDK
|
|
30
|
+
|
|
31
|
+
Official Python SDK for [Kuberoute AI](https://kuberoute.ai) — Intelligent Kubernetes Workload Placement & Cost Optimization.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install kuberoute
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or install in editable mode from this monorepo:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install -e packages/sdk-python
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
### Synchronous Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from kuberoute import KubeRoute
|
|
51
|
+
|
|
52
|
+
# Initialize the client
|
|
53
|
+
client = KubeRoute(
|
|
54
|
+
base_url="http://localhost:8080", # or https://api.kuberoute.ai
|
|
55
|
+
token="your-jwt-token" # or KUBEROUTE_TOKEN env var
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# In local dev mode, you can log in directly:
|
|
59
|
+
client.login_dev()
|
|
60
|
+
|
|
61
|
+
# Check health
|
|
62
|
+
health = client.health()
|
|
63
|
+
print(f"Server status: {health.status}")
|
|
64
|
+
|
|
65
|
+
# List registered Kubernetes clusters
|
|
66
|
+
clusters = client.clusters.list()
|
|
67
|
+
for cluster in clusters.clusters:
|
|
68
|
+
print(f"Cluster: {cluster.name} ({cluster.provider}/{cluster.region})")
|
|
69
|
+
|
|
70
|
+
# Register a new cluster
|
|
71
|
+
new_cluster = client.clusters.create(
|
|
72
|
+
name="production-eks",
|
|
73
|
+
provider="aws",
|
|
74
|
+
region="us-west-2"
|
|
75
|
+
)
|
|
76
|
+
print(f"Registered cluster ID: {new_cluster.id}")
|
|
77
|
+
|
|
78
|
+
# Fetch routing & placement decisions
|
|
79
|
+
decisions = client.decisions.list(cluster_id=new_cluster.id)
|
|
80
|
+
for d in decisions.decisions:
|
|
81
|
+
print(f"Decision: {d.id} | Savings: ${d.predicted_cost_savings}/mo | Reason: {d.reason}")
|
|
82
|
+
if d.confidence_score > 0.9:
|
|
83
|
+
client.decisions.apply(cluster_id=new_cluster.id, decision_id=d.id)
|
|
84
|
+
print("Applied decision!")
|
|
85
|
+
|
|
86
|
+
# Get aggregate infrastructure costs and savings
|
|
87
|
+
costs = client.costs.get_total()
|
|
88
|
+
print(f"Total: ${costs.total} | Optimized: ${costs.optimized} | Savings: ${costs.savings}")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Asynchronous Usage (`AsyncKubeRoute`)
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
from kuberoute import AsyncKubeRoute
|
|
96
|
+
|
|
97
|
+
async def main():
|
|
98
|
+
async with AsyncKubeRoute(base_url="http://localhost:8080", token="your-token") as client:
|
|
99
|
+
clusters = await client.clusters.list()
|
|
100
|
+
print(f"Found {clusters.total} clusters")
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Resources
|
|
106
|
+
|
|
107
|
+
- **`client.clusters`**: `list()`, `get(id)`, `create(name, provider, region)`, `delete(id)`
|
|
108
|
+
- **`client.decisions`**: `list(cluster_id, status, page, limit)`, `get(cluster_id, decision_id)`, `apply(...)`, `reject(...)`, `revert(...)`
|
|
109
|
+
- **`client.costs`**: `get_total()`, `get_cluster(cluster_id)`
|
|
110
|
+
- **`client.me()`**: Get current authenticated user and tenant credit stats.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
kuberoute/__init__.py,sha256=hGc1ppd6zRd4p0ubJaTNQbiHryM1g6AjhNsy0BOHoYA,1082
|
|
2
|
+
kuberoute/async_client.py,sha256=Aj_ayYR49vTheNKEo7JGvBlwvc2R_cU4OxBw5tOtC6U,6935
|
|
3
|
+
kuberoute/client.py,sha256=_OiqvioTZJ3iQeP2hsAXLimhaiH1munv2TEP_ooPRmg,7722
|
|
4
|
+
kuberoute/exceptions.py,sha256=zs7-m94NLSNa3ywuHpMkQW7IhB-Aamj61KGl_1RXJWw,1062
|
|
5
|
+
kuberoute/models.py,sha256=eLvUXMvCL14bpzqEdXmjzh_xNhHeCRTtx-v1LU3r4vQ,2262
|
|
6
|
+
kuberoute-0.1.0.dist-info/METADATA,sha256=Gz1VCat7JGUdy4KAR-UV-UQqLymDW1E_iAuGglBK32k,3527
|
|
7
|
+
kuberoute-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
kuberoute-0.1.0.dist-info/RECORD,,
|