kuberoute 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.
@@ -0,0 +1,22 @@
1
+ .venv/
2
+ bin/
3
+ *.out
4
+ *.log
5
+ .env
6
+ .env.local
7
+ node_modules/
8
+ .next/
9
+ dist/
10
+ __pycache__/
11
+ *.pyc
12
+ .pytest_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .terraform/
16
+ *.tfstate
17
+ *.tfstate.*
18
+ .terraform.lock.hcl
19
+ *.zip
20
+ .DS_Store
21
+ .pids/
22
+ logs/
@@ -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,82 @@
1
+ # Kuberoute Python SDK
2
+
3
+ Official Python SDK for [Kuberoute AI](https://kuberoute.ai) — Intelligent Kubernetes Workload Placement & Cost Optimization.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install kuberoute
9
+ ```
10
+
11
+ Or install in editable mode from this monorepo:
12
+
13
+ ```bash
14
+ pip install -e packages/sdk-python
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ### Synchronous Usage
20
+
21
+ ```python
22
+ from kuberoute import KubeRoute
23
+
24
+ # Initialize the client
25
+ client = KubeRoute(
26
+ base_url="http://localhost:8080", # or https://api.kuberoute.ai
27
+ token="your-jwt-token" # or KUBEROUTE_TOKEN env var
28
+ )
29
+
30
+ # In local dev mode, you can log in directly:
31
+ client.login_dev()
32
+
33
+ # Check health
34
+ health = client.health()
35
+ print(f"Server status: {health.status}")
36
+
37
+ # List registered Kubernetes clusters
38
+ clusters = client.clusters.list()
39
+ for cluster in clusters.clusters:
40
+ print(f"Cluster: {cluster.name} ({cluster.provider}/{cluster.region})")
41
+
42
+ # Register a new cluster
43
+ new_cluster = client.clusters.create(
44
+ name="production-eks",
45
+ provider="aws",
46
+ region="us-west-2"
47
+ )
48
+ print(f"Registered cluster ID: {new_cluster.id}")
49
+
50
+ # Fetch routing & placement decisions
51
+ decisions = client.decisions.list(cluster_id=new_cluster.id)
52
+ for d in decisions.decisions:
53
+ print(f"Decision: {d.id} | Savings: ${d.predicted_cost_savings}/mo | Reason: {d.reason}")
54
+ if d.confidence_score > 0.9:
55
+ client.decisions.apply(cluster_id=new_cluster.id, decision_id=d.id)
56
+ print("Applied decision!")
57
+
58
+ # Get aggregate infrastructure costs and savings
59
+ costs = client.costs.get_total()
60
+ print(f"Total: ${costs.total} | Optimized: ${costs.optimized} | Savings: ${costs.savings}")
61
+ ```
62
+
63
+ ### Asynchronous Usage (`AsyncKubeRoute`)
64
+
65
+ ```python
66
+ import asyncio
67
+ from kuberoute import AsyncKubeRoute
68
+
69
+ async def main():
70
+ async with AsyncKubeRoute(base_url="http://localhost:8080", token="your-token") as client:
71
+ clusters = await client.clusters.list()
72
+ print(f"Found {clusters.total} clusters")
73
+
74
+ asyncio.run(main())
75
+ ```
76
+
77
+ ## Resources
78
+
79
+ - **`client.clusters`**: `list()`, `get(id)`, `create(name, provider, region)`, `delete(id)`
80
+ - **`client.decisions`**: `list(cluster_id, status, page, limit)`, `get(cluster_id, decision_id)`, `apply(...)`, `reject(...)`, `revert(...)`
81
+ - **`client.costs`**: `get_total()`, `get_cluster(cluster_id)`
82
+ - **`client.me()`**: Get current authenticated user and tenant credit stats.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kuberoute"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for Kuberoute AI Intelligent Routing & Orchestration"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Kuberoute AI Team", email = "vikas.budde@hotmail.com" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: System Administrators",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: System :: Distributed Computing",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ]
29
+ dependencies = [
30
+ "httpx>=0.24.0",
31
+ "pydantic>=2.0.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=7.0.0",
37
+ "pytest-asyncio>=0.21.0",
38
+ "pytest-mock>=3.10.0",
39
+ "respx>=0.20.0",
40
+ ]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/kuberoute"]
@@ -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()
@@ -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()
@@ -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
@@ -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,251 @@
1
+ """Unit tests for Kuberoute Python SDK using built-in httpx.MockTransport."""
2
+
3
+ import json
4
+ import pytest
5
+ import httpx
6
+ from kuberoute import (
7
+ KubeRoute,
8
+ AsyncKubeRoute,
9
+ AuthenticationError,
10
+ NotFoundError,
11
+ ValidationError,
12
+ ServerError,
13
+ )
14
+
15
+
16
+ def create_mock_transport():
17
+ def handler(request: httpx.Request) -> httpx.Response:
18
+ url_path = request.url.path
19
+ method = request.method
20
+
21
+ if url_path == "/health":
22
+ return httpx.Response(200, json={"status": "ok"})
23
+
24
+ if url_path == "/auth/dev":
25
+ return httpx.Response(
26
+ 200,
27
+ json={
28
+ "user": {
29
+ "id": "11111111-1111-1111-1111-111111111111",
30
+ "email": "dev@kuberoute.local",
31
+ "name": "Dev User",
32
+ "role": "admin",
33
+ "tenant_id": "22222222-2222-2222-2222-222222222222",
34
+ },
35
+ "token": "jwt-token-12345",
36
+ },
37
+ )
38
+
39
+ if url_path == "/api/v1/clusters":
40
+ if method == "GET":
41
+ return httpx.Response(
42
+ 200,
43
+ json={
44
+ "clusters": [
45
+ {
46
+ "id": "c111",
47
+ "name": "prod-us-east",
48
+ "provider": "aws",
49
+ "region": "us-east-1",
50
+ "status": "active",
51
+ "node_count": 5,
52
+ }
53
+ ],
54
+ "total": 1,
55
+ "page": 1,
56
+ "limit": 20,
57
+ },
58
+ )
59
+ elif method == "POST":
60
+ body = json.loads(request.content)
61
+ return httpx.Response(
62
+ 201,
63
+ json={
64
+ "id": "c222",
65
+ "name": body.get("name"),
66
+ "provider": body.get("provider"),
67
+ "region": body.get("region"),
68
+ "status": "active",
69
+ "node_count": 3,
70
+ },
71
+ )
72
+
73
+ if url_path == "/api/v1/clusters/c222":
74
+ if method == "GET":
75
+ return httpx.Response(
76
+ 200,
77
+ json={
78
+ "id": "c222",
79
+ "name": "staging-eu",
80
+ "provider": "gcp",
81
+ "region": "europe-west1",
82
+ "status": "active",
83
+ "node_count": 3,
84
+ },
85
+ )
86
+ elif method == "DELETE":
87
+ return httpx.Response(204)
88
+
89
+ if url_path == "/api/v1/clusters/c111/decisions":
90
+ return httpx.Response(
91
+ 200,
92
+ json={
93
+ "decisions": [
94
+ {
95
+ "id": "d111",
96
+ "cluster_id": "c111",
97
+ "workload_id": "w1",
98
+ "decision_type": "placement",
99
+ "target_node_id": "node-spot-1",
100
+ "confidence_score": 0.95,
101
+ "predicted_cost_savings": 42.50,
102
+ "predicted_latency_ms": 12,
103
+ "status": "pending",
104
+ }
105
+ ],
106
+ "total": 1,
107
+ "page": 1,
108
+ "limit": 20,
109
+ },
110
+ )
111
+
112
+ if url_path == "/api/v1/clusters/c111/decisions/d111/apply":
113
+ return httpx.Response(200, json={"status": "applied"})
114
+
115
+ if url_path == "/api/v1/clusters/c111/decisions/d111/reject":
116
+ return httpx.Response(200, json={"status": "rejected"})
117
+
118
+ if url_path == "/api/v1/costs":
119
+ return httpx.Response(
120
+ 200,
121
+ json={
122
+ "total": 1500.0,
123
+ "optimized": 1050.0,
124
+ "savings": 450.0,
125
+ "breakdown": {"compute": 300.0, "storage": 150.0},
126
+ },
127
+ )
128
+
129
+ if url_path == "/api/v1/clusters/nonexistent":
130
+ return httpx.Response(404, json={"error": "not found"})
131
+
132
+ if url_path == "/api/v1/me":
133
+ auth = request.headers.get("Authorization")
134
+ if not auth or "invalid" in auth:
135
+ return httpx.Response(401, json={"error": "unauthorized"})
136
+ return httpx.Response(
137
+ 200,
138
+ json={
139
+ "user": {"id": "u1", "email": "me@kuberoute.local"},
140
+ "tenant": {"id": "t1", "plan": "pro", "task_credits": 100, "task_credits_used": 10},
141
+ },
142
+ )
143
+
144
+ return httpx.Response(404, json={"error": f"route not found: {url_path}"})
145
+
146
+ return httpx.MockTransport(handler)
147
+
148
+
149
+ @pytest.fixture
150
+ def sync_client():
151
+ client = KubeRoute(base_url="http://localhost:8080", token="test-token")
152
+ client._client = httpx.Client(transport=create_mock_transport(), timeout=client.timeout)
153
+ yield client
154
+ client.close()
155
+
156
+
157
+ @pytest.fixture
158
+ def async_client():
159
+ client = AsyncKubeRoute(base_url="http://localhost:8080", token="test-token")
160
+ client._client = httpx.AsyncClient(transport=create_mock_transport(), timeout=client.timeout)
161
+ yield client
162
+
163
+
164
+ def test_health_check(sync_client):
165
+ resp = sync_client.health()
166
+ assert resp.status == "ok"
167
+
168
+
169
+ def test_dev_login(sync_client):
170
+ res = sync_client.login_dev()
171
+ assert res.token == "jwt-token-12345"
172
+ assert sync_client.token == "jwt-token-12345"
173
+ assert res.user.email == "dev@kuberoute.local"
174
+
175
+
176
+ def test_clusters_crud(sync_client):
177
+ # List
178
+ clusters_resp = sync_client.clusters.list()
179
+ assert clusters_resp.total == 1
180
+ assert len(clusters_resp.clusters) == 1
181
+ assert clusters_resp.clusters[0].name == "prod-us-east"
182
+
183
+ # Create
184
+ new_cluster = sync_client.clusters.create(name="staging-eu", provider="gcp", region="europe-west1")
185
+ assert new_cluster.id == "c222"
186
+ assert new_cluster.name == "staging-eu"
187
+
188
+ # Get
189
+ fetched = sync_client.clusters.get("c222")
190
+ assert fetched.id == "c222"
191
+
192
+ # Delete
193
+ sync_client.clusters.delete("c222")
194
+
195
+
196
+ def test_decisions_workflow(sync_client):
197
+ cluster_id = "c111"
198
+ decision_id = "d111"
199
+
200
+ # List decisions
201
+ decisions = sync_client.decisions.list(cluster_id)
202
+ assert len(decisions.decisions) == 1
203
+ assert decisions.decisions[0].predicted_cost_savings == 42.50
204
+
205
+ # Apply decision
206
+ assert sync_client.decisions.apply(cluster_id, decision_id) is True
207
+
208
+ # Reject decision
209
+ assert sync_client.decisions.reject(cluster_id, decision_id) is True
210
+
211
+
212
+ def test_costs(sync_client):
213
+ costs = sync_client.costs.get_total()
214
+ assert costs.total == 1500.0
215
+ assert costs.savings == 450.0
216
+ assert costs.breakdown["compute"] == 300.0
217
+
218
+
219
+ def test_me(sync_client):
220
+ me = sync_client.me()
221
+ assert me.user.email == "me@kuberoute.local"
222
+ assert me.tenant.plan == "pro"
223
+
224
+
225
+ def test_error_handling(sync_client):
226
+ with pytest.raises(NotFoundError) as exc_info:
227
+ sync_client.clusters.get("nonexistent")
228
+ assert exc_info.value.status_code == 404
229
+
230
+ sync_client.token = "invalid"
231
+ with pytest.raises(AuthenticationError) as exc_info:
232
+ sync_client.me()
233
+ assert exc_info.value.status_code == 401
234
+
235
+
236
+ def test_async_client():
237
+ import asyncio
238
+
239
+ async def _test():
240
+ client = AsyncKubeRoute(base_url="http://localhost:8080", token="test-token")
241
+ client._client = httpx.AsyncClient(transport=create_mock_transport(), timeout=client.timeout)
242
+
243
+ resp = await client.health()
244
+ assert resp.status == "ok"
245
+
246
+ clusters = await client.clusters.list()
247
+ assert len(clusters.clusters) == 1
248
+ assert clusters.clusters[0].name == "prod-us-east"
249
+ await client.close()
250
+
251
+ asyncio.run(_test())