kestrel-workflows 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kestrel_workflows-0.2.0/LICENSE +17 -0
- kestrel_workflows-0.2.0/PKG-INFO +107 -0
- kestrel_workflows-0.2.0/README.md +75 -0
- kestrel_workflows-0.2.0/pyproject.toml +47 -0
- kestrel_workflows-0.2.0/setup.cfg +4 -0
- kestrel_workflows-0.2.0/src/kestrel/__init__.py +39 -0
- kestrel_workflows-0.2.0/src/kestrel/async_client.py +266 -0
- kestrel_workflows-0.2.0/src/kestrel/auth.py +54 -0
- kestrel_workflows-0.2.0/src/kestrel/client.py +357 -0
- kestrel_workflows-0.2.0/src/kestrel/exceptions.py +22 -0
- kestrel_workflows-0.2.0/src/kestrel/models.py +168 -0
- kestrel_workflows-0.2.0/src/kestrel/workflows/__init__.py +28 -0
- kestrel_workflows-0.2.0/src/kestrel/workflows/actions.py +693 -0
- kestrel_workflows-0.2.0/src/kestrel/workflows/approvals.py +70 -0
- kestrel_workflows-0.2.0/src/kestrel/workflows/builder.py +188 -0
- kestrel_workflows-0.2.0/src/kestrel/workflows/conditions.py +63 -0
- kestrel_workflows-0.2.0/src/kestrel/workflows/triggers.py +343 -0
- kestrel_workflows-0.2.0/src/kestrel/workflows/types.py +34 -0
- kestrel_workflows-0.2.0/src/kestrel_workflows.egg-info/PKG-INFO +107 -0
- kestrel_workflows-0.2.0/src/kestrel_workflows.egg-info/SOURCES.txt +22 -0
- kestrel_workflows-0.2.0/src/kestrel_workflows.egg-info/dependency_links.txt +1 -0
- kestrel_workflows-0.2.0/src/kestrel_workflows.egg-info/requires.txt +7 -0
- kestrel_workflows-0.2.0/src/kestrel_workflows.egg-info/top_level.txt +1 -0
- kestrel_workflows-0.2.0/tests/test_client.py +189 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2024-2026 Kestrel AI, Inc.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kestrel-workflows
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for Kestrel — AI Agents for Cloud Operations
|
|
5
|
+
Author-email: Kestrel AI <support@usekestrel.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://usekestrel.ai
|
|
8
|
+
Project-URL: Documentation, https://docs.usekestrel.ai/workflows/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/KestrelAI/kestrel-sdk
|
|
10
|
+
Keywords: kestrel,workflows,kubernetes,incident-response,automation,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: pydantic>=2.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
30
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Kestrel SDK
|
|
34
|
+
|
|
35
|
+
Python SDK for [Kestrel](https://usekestrel.ai) — AI Agents for Cloud Operations.
|
|
36
|
+
|
|
37
|
+
Build, deploy, and manage workflows programmatically with a typed, fluent API.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install kestrel-workflows
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from kestrel import KestrelClient
|
|
49
|
+
from kestrel.workflows import Workflow, Trigger, Action
|
|
50
|
+
|
|
51
|
+
client = KestrelClient(api_key="kestrel_sk_...")
|
|
52
|
+
|
|
53
|
+
wf = (
|
|
54
|
+
Workflow("Pod Crash RCA + Jira")
|
|
55
|
+
.description("Run RCA on pod crash, create Jira ticket")
|
|
56
|
+
.trigger(
|
|
57
|
+
Trigger.k8s_pod_status()
|
|
58
|
+
.reasons("CrashLoopBackOff")
|
|
59
|
+
.namespace("production")
|
|
60
|
+
)
|
|
61
|
+
.cooldown(hours=24)
|
|
62
|
+
.then(Action.kestrel_trigger_rca().label("Run RCA"))
|
|
63
|
+
.then(Action.jira_create_ticket()
|
|
64
|
+
.project("KAN")
|
|
65
|
+
.title("{{incident.title}}")
|
|
66
|
+
.priority("High")
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
created = client.workflows.deploy(wf, activate=True)
|
|
71
|
+
print(f"Deployed: {created.id}")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Async Support
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from kestrel import AsyncKestrelClient
|
|
78
|
+
|
|
79
|
+
async with AsyncKestrelClient(api_key="kestrel_sk_...") as client:
|
|
80
|
+
workflows = await client.workflows.list()
|
|
81
|
+
execution = await client.workflows.test(workflows[0].id)
|
|
82
|
+
result = await client.executions.wait(execution.id)
|
|
83
|
+
print(f"Result: {result.status}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Authentication
|
|
87
|
+
|
|
88
|
+
Create an API key in the Kestrel platform under **Workflows > API Keys**.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# API key (recommended)
|
|
92
|
+
client = KestrelClient(api_key="kestrel_sk_...")
|
|
93
|
+
|
|
94
|
+
# From CLI login
|
|
95
|
+
client = KestrelClient.from_config()
|
|
96
|
+
|
|
97
|
+
# Async
|
|
98
|
+
client = AsyncKestrelClient(api_key="kestrel_sk_...")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Documentation
|
|
102
|
+
|
|
103
|
+
Full SDK documentation: [docs.usekestrel.ai/workflows/sdk](https://docs.usekestrel.ai/workflows/sdk)
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
Apache 2.0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Kestrel SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for [Kestrel](https://usekestrel.ai) — AI Agents for Cloud Operations.
|
|
4
|
+
|
|
5
|
+
Build, deploy, and manage workflows programmatically with a typed, fluent API.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install kestrel-workflows
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from kestrel import KestrelClient
|
|
17
|
+
from kestrel.workflows import Workflow, Trigger, Action
|
|
18
|
+
|
|
19
|
+
client = KestrelClient(api_key="kestrel_sk_...")
|
|
20
|
+
|
|
21
|
+
wf = (
|
|
22
|
+
Workflow("Pod Crash RCA + Jira")
|
|
23
|
+
.description("Run RCA on pod crash, create Jira ticket")
|
|
24
|
+
.trigger(
|
|
25
|
+
Trigger.k8s_pod_status()
|
|
26
|
+
.reasons("CrashLoopBackOff")
|
|
27
|
+
.namespace("production")
|
|
28
|
+
)
|
|
29
|
+
.cooldown(hours=24)
|
|
30
|
+
.then(Action.kestrel_trigger_rca().label("Run RCA"))
|
|
31
|
+
.then(Action.jira_create_ticket()
|
|
32
|
+
.project("KAN")
|
|
33
|
+
.title("{{incident.title}}")
|
|
34
|
+
.priority("High")
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
created = client.workflows.deploy(wf, activate=True)
|
|
39
|
+
print(f"Deployed: {created.id}")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Async Support
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from kestrel import AsyncKestrelClient
|
|
46
|
+
|
|
47
|
+
async with AsyncKestrelClient(api_key="kestrel_sk_...") as client:
|
|
48
|
+
workflows = await client.workflows.list()
|
|
49
|
+
execution = await client.workflows.test(workflows[0].id)
|
|
50
|
+
result = await client.executions.wait(execution.id)
|
|
51
|
+
print(f"Result: {result.status}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
Create an API key in the Kestrel platform under **Workflows > API Keys**.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# API key (recommended)
|
|
60
|
+
client = KestrelClient(api_key="kestrel_sk_...")
|
|
61
|
+
|
|
62
|
+
# From CLI login
|
|
63
|
+
client = KestrelClient.from_config()
|
|
64
|
+
|
|
65
|
+
# Async
|
|
66
|
+
client = AsyncKestrelClient(api_key="kestrel_sk_...")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
Full SDK documentation: [docs.usekestrel.ai/workflows/sdk](https://docs.usekestrel.ai/workflows/sdk)
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Apache 2.0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kestrel-workflows"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python SDK for Kestrel — AI Agents for Cloud Operations"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Kestrel AI", email = "support@usekestrel.ai"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["kestrel", "workflows", "kubernetes", "incident-response", "automation", "sdk"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software 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 :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: System :: Systems Administration",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.27",
|
|
31
|
+
"pydantic>=2.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://usekestrel.ai"
|
|
36
|
+
Documentation = "https://docs.usekestrel.ai/workflows/sdk"
|
|
37
|
+
Repository = "https://github.com/KestrelAI/kestrel-sdk"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.0",
|
|
42
|
+
"pytest-asyncio>=0.23",
|
|
43
|
+
"respx>=0.21",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Kestrel Python SDK — programmatic access to Kestrel Workflows."""
|
|
2
|
+
|
|
3
|
+
from .client import KestrelClient
|
|
4
|
+
from .async_client import AsyncKestrelClient
|
|
5
|
+
from .exceptions import AuthError, ConflictError, KestrelError, NotFoundError, ServerError
|
|
6
|
+
from .models import (
|
|
7
|
+
Approval,
|
|
8
|
+
Catalog,
|
|
9
|
+
Execution,
|
|
10
|
+
ExecutionList,
|
|
11
|
+
GenerateResult,
|
|
12
|
+
IntegrationStatus,
|
|
13
|
+
RequestResult,
|
|
14
|
+
SuggestedWorkflow,
|
|
15
|
+
Workflow,
|
|
16
|
+
WorkflowRequest,
|
|
17
|
+
WorkflowStats,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"KestrelClient",
|
|
22
|
+
"AsyncKestrelClient",
|
|
23
|
+
"KestrelError",
|
|
24
|
+
"AuthError",
|
|
25
|
+
"NotFoundError",
|
|
26
|
+
"ConflictError",
|
|
27
|
+
"ServerError",
|
|
28
|
+
"Workflow",
|
|
29
|
+
"GenerateResult",
|
|
30
|
+
"WorkflowStats",
|
|
31
|
+
"Execution",
|
|
32
|
+
"ExecutionList",
|
|
33
|
+
"Approval",
|
|
34
|
+
"WorkflowRequest",
|
|
35
|
+
"RequestResult",
|
|
36
|
+
"SuggestedWorkflow",
|
|
37
|
+
"Catalog",
|
|
38
|
+
"IntegrationStatus",
|
|
39
|
+
]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Async Kestrel client — mirrors KestrelClient with async/await."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .auth import Config, load_config
|
|
10
|
+
from .exceptions import AuthError, ConflictError, KestrelError, NotFoundError, ServerError
|
|
11
|
+
from .models import (
|
|
12
|
+
Approval,
|
|
13
|
+
Catalog,
|
|
14
|
+
Execution,
|
|
15
|
+
ExecutionList,
|
|
16
|
+
GenerateResult,
|
|
17
|
+
IntegrationStatus,
|
|
18
|
+
RequestResult,
|
|
19
|
+
SuggestedWorkflow,
|
|
20
|
+
Workflow,
|
|
21
|
+
WorkflowRequest,
|
|
22
|
+
WorkflowStats,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _AsyncWorkflowsNamespace:
|
|
27
|
+
def __init__(self, client: AsyncKestrelClient):
|
|
28
|
+
self._c = client
|
|
29
|
+
|
|
30
|
+
async def list(self, *, status: str | None = None) -> list[Workflow]:
|
|
31
|
+
params = {}
|
|
32
|
+
if status:
|
|
33
|
+
params["status"] = status
|
|
34
|
+
data = await self._c._get("/api/workflows", params=params)
|
|
35
|
+
return [Workflow.model_validate(w) for w in data]
|
|
36
|
+
|
|
37
|
+
async def get(self, workflow_id: str) -> Workflow:
|
|
38
|
+
return Workflow.model_validate(await self._c._get(f"/api/workflows/{workflow_id}"))
|
|
39
|
+
|
|
40
|
+
async def create(self, *, name: str, description: str = "", definition: dict[str, Any] | None = None,
|
|
41
|
+
trigger_config: dict[str, Any] | None = None, nl_prompt: str = "",
|
|
42
|
+
alert_config: dict[str, Any] | None = None) -> Workflow:
|
|
43
|
+
body: dict[str, Any] = {"name": name, "description": description}
|
|
44
|
+
if definition is not None:
|
|
45
|
+
body["definition"] = definition
|
|
46
|
+
if trigger_config is not None:
|
|
47
|
+
body["trigger_config"] = trigger_config
|
|
48
|
+
if nl_prompt:
|
|
49
|
+
body["nl_prompt"] = nl_prompt
|
|
50
|
+
if alert_config is not None:
|
|
51
|
+
body["alert_config"] = alert_config
|
|
52
|
+
return Workflow.model_validate(await self._c._post("/api/workflows", json=body))
|
|
53
|
+
|
|
54
|
+
async def update(self, workflow_id: str, *, name: str | None = None, description: str | None = None,
|
|
55
|
+
definition: dict[str, Any] | None = None, trigger_config: dict[str, Any] | None = None,
|
|
56
|
+
nl_prompt: str | None = None) -> Workflow:
|
|
57
|
+
current = await self.get(workflow_id)
|
|
58
|
+
body: dict[str, Any] = {
|
|
59
|
+
"name": name if name is not None else current.name,
|
|
60
|
+
"description": description if description is not None else current.description,
|
|
61
|
+
"definition": definition if definition is not None else current.definition,
|
|
62
|
+
"trigger_config": trigger_config if trigger_config is not None else current.trigger_config,
|
|
63
|
+
"nl_prompt": nl_prompt if nl_prompt is not None else current.nl_prompt,
|
|
64
|
+
}
|
|
65
|
+
return Workflow.model_validate(await self._c._put(f"/api/workflows/{workflow_id}", json=body))
|
|
66
|
+
|
|
67
|
+
async def delete(self, workflow_id: str) -> None:
|
|
68
|
+
await self._c._delete(f"/api/workflows/{workflow_id}")
|
|
69
|
+
|
|
70
|
+
async def activate(self, workflow_id: str) -> None:
|
|
71
|
+
await self._c._post(f"/api/workflows/{workflow_id}/activate")
|
|
72
|
+
|
|
73
|
+
async def pause(self, workflow_id: str) -> None:
|
|
74
|
+
await self._c._post(f"/api/workflows/{workflow_id}/pause")
|
|
75
|
+
|
|
76
|
+
async def duplicate(self, workflow_id: str, *, name: str = "") -> Workflow:
|
|
77
|
+
return Workflow.model_validate(
|
|
78
|
+
await self._c._post(f"/api/workflows/{workflow_id}/duplicate", json={"name": name})
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def deploy(self, workflow: Any, *, activate: bool = False) -> Workflow:
|
|
82
|
+
"""Create a workflow from a builder and optionally activate it."""
|
|
83
|
+
definition, trigger_config = workflow.build()
|
|
84
|
+
body: dict[str, Any] = {
|
|
85
|
+
"name": workflow._name,
|
|
86
|
+
"description": workflow._description,
|
|
87
|
+
"definition": definition,
|
|
88
|
+
"trigger_config": trigger_config,
|
|
89
|
+
}
|
|
90
|
+
if workflow._alert_config:
|
|
91
|
+
body["alert_config"] = workflow._alert_config
|
|
92
|
+
created = Workflow.model_validate(await self._c._post("/api/workflows", json=body))
|
|
93
|
+
if activate:
|
|
94
|
+
await self.activate(created.id)
|
|
95
|
+
created.status = "active"
|
|
96
|
+
return created
|
|
97
|
+
|
|
98
|
+
async def test(self, workflow_id: str) -> Execution:
|
|
99
|
+
return Execution.model_validate(await self._c._post(f"/api/workflows/{workflow_id}/test"))
|
|
100
|
+
|
|
101
|
+
async def generate(self, prompt: str) -> GenerateResult:
|
|
102
|
+
return GenerateResult.model_validate(
|
|
103
|
+
await self._c._post("/api/workflows/generate", json={"prompt": prompt})
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def stats(self) -> WorkflowStats:
|
|
107
|
+
return WorkflowStats.model_validate(await self._c._get("/api/workflows/stats"))
|
|
108
|
+
|
|
109
|
+
async def executions(self, workflow_id: str, *, page: int = 1, page_size: int = 20) -> ExecutionList:
|
|
110
|
+
return ExecutionList.model_validate(
|
|
111
|
+
await self._c._get(f"/api/workflows/{workflow_id}/executions",
|
|
112
|
+
params={"page": str(page), "page_size": str(page_size)})
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def catalog(self) -> Catalog:
|
|
116
|
+
return Catalog.model_validate(await self._c._get("/api/workflows/catalog"))
|
|
117
|
+
|
|
118
|
+
async def integrations(self) -> list[IntegrationStatus]:
|
|
119
|
+
data = await self._c._get("/api/workflows/integrations/status")
|
|
120
|
+
return [IntegrationStatus.model_validate(i) for i in data]
|
|
121
|
+
|
|
122
|
+
async def suggestions(self) -> list[SuggestedWorkflow]:
|
|
123
|
+
data = await self._c._get("/api/workflows/suggestions")
|
|
124
|
+
return [SuggestedWorkflow.model_validate(s) for s in data]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _AsyncExecutionsNamespace:
|
|
128
|
+
def __init__(self, client: AsyncKestrelClient):
|
|
129
|
+
self._c = client
|
|
130
|
+
|
|
131
|
+
async def get(self, execution_id: str) -> Execution:
|
|
132
|
+
return Execution.model_validate(
|
|
133
|
+
await self._c._get(f"/api/workflow-executions/{execution_id}")
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def cancel(self, execution_id: str) -> None:
|
|
137
|
+
await self._c._post(f"/api/workflow-executions/{execution_id}/cancel")
|
|
138
|
+
|
|
139
|
+
async def wait(self, execution_id: str, *, poll_interval: float = 2.0, timeout: float = 300.0) -> Execution:
|
|
140
|
+
"""Poll until execution completes, fails, or times out."""
|
|
141
|
+
import asyncio
|
|
142
|
+
import time
|
|
143
|
+
deadline = time.monotonic() + timeout
|
|
144
|
+
while time.monotonic() < deadline:
|
|
145
|
+
ex = await self.get(execution_id)
|
|
146
|
+
if ex.status in ("completed", "failed", "cancelled"):
|
|
147
|
+
return ex
|
|
148
|
+
await asyncio.sleep(poll_interval)
|
|
149
|
+
raise TimeoutError(f"Execution {execution_id} did not complete within {timeout}s")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _AsyncApprovalsNamespace:
|
|
153
|
+
def __init__(self, client: AsyncKestrelClient):
|
|
154
|
+
self._c = client
|
|
155
|
+
|
|
156
|
+
async def list_pending(self) -> list[Approval]:
|
|
157
|
+
data = await self._c._get("/api/workflow-approvals/pending")
|
|
158
|
+
return [Approval.model_validate(a) for a in data]
|
|
159
|
+
|
|
160
|
+
async def approve(self, approval_id: str, *, justification: str | None = None) -> None:
|
|
161
|
+
body = {"justification": justification} if justification else None
|
|
162
|
+
await self._c._post(f"/api/workflow-approvals/{approval_id}/approve", json=body)
|
|
163
|
+
|
|
164
|
+
async def reject(self, approval_id: str) -> None:
|
|
165
|
+
await self._c._post(f"/api/workflow-approvals/{approval_id}/reject")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class _AsyncRequestsNamespace:
|
|
169
|
+
def __init__(self, client: AsyncKestrelClient):
|
|
170
|
+
self._c = client
|
|
171
|
+
|
|
172
|
+
async def list(self) -> list[WorkflowRequest]:
|
|
173
|
+
data = await self._c._get("/api/workflow-requests")
|
|
174
|
+
items = data.get("requests", []) if isinstance(data, dict) else data
|
|
175
|
+
results = [WorkflowRequest.model_validate(r) for r in items]
|
|
176
|
+
return [r for r in results if r.status in ("no_workflow", "approved", "rejected")]
|
|
177
|
+
|
|
178
|
+
async def approve(self, request_id: str) -> None:
|
|
179
|
+
await self._c._post(f"/api/workflow-requests/{request_id}/approve")
|
|
180
|
+
|
|
181
|
+
async def reject(self, request_id: str) -> None:
|
|
182
|
+
await self._c._post(f"/api/workflow-requests/{request_id}/reject")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class AsyncKestrelClient:
|
|
186
|
+
"""Async entry point for the Kestrel Python SDK.
|
|
187
|
+
|
|
188
|
+
Usage::
|
|
189
|
+
|
|
190
|
+
async with AsyncKestrelClient(api_key="kestrel_sk_...") as client:
|
|
191
|
+
workflows = await client.workflows.list()
|
|
192
|
+
execution = await client.workflows.test(workflows[0].id)
|
|
193
|
+
result = await client.executions.wait(execution.id)
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
DEFAULT_SERVER = "https://platform.usekestrel.ai"
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
server: str = DEFAULT_SERVER,
|
|
201
|
+
*,
|
|
202
|
+
api_key: str | None = None,
|
|
203
|
+
session_token: str | None = None,
|
|
204
|
+
timeout: float = 120.0,
|
|
205
|
+
):
|
|
206
|
+
if api_key is None and session_token is None:
|
|
207
|
+
raise AuthError("Provide either api_key or session_token.")
|
|
208
|
+
|
|
209
|
+
server = server.rstrip("/")
|
|
210
|
+
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
211
|
+
if api_key is not None:
|
|
212
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
213
|
+
else:
|
|
214
|
+
assert session_token is not None
|
|
215
|
+
headers["X-Session-Token"] = session_token
|
|
216
|
+
|
|
217
|
+
self._config = Config(server_url=server, session_token=session_token or "")
|
|
218
|
+
self._http = httpx.AsyncClient(base_url=server, headers=headers, timeout=timeout)
|
|
219
|
+
self.workflows = _AsyncWorkflowsNamespace(self)
|
|
220
|
+
self.executions = _AsyncExecutionsNamespace(self)
|
|
221
|
+
self.approvals = _AsyncApprovalsNamespace(self)
|
|
222
|
+
self.requests = _AsyncRequestsNamespace(self)
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def from_config(cls) -> AsyncKestrelClient:
|
|
226
|
+
cfg = load_config()
|
|
227
|
+
if not cfg.is_logged_in:
|
|
228
|
+
raise AuthError("Not logged in. Run `kestrel login` or use AsyncKestrelClient(api_key=...).")
|
|
229
|
+
return cls(server=cfg.server_url, session_token=cfg.session_token)
|
|
230
|
+
|
|
231
|
+
async def close(self) -> None:
|
|
232
|
+
await self._http.aclose()
|
|
233
|
+
|
|
234
|
+
async def __aenter__(self) -> AsyncKestrelClient:
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
238
|
+
await self.close()
|
|
239
|
+
|
|
240
|
+
async def _get(self, path: str, *, params: dict[str, str] | None = None) -> Any:
|
|
241
|
+
return self._handle(await self._http.get(path, params=params))
|
|
242
|
+
|
|
243
|
+
async def _post(self, path: str, *, json: Any = None) -> Any:
|
|
244
|
+
return self._handle(await self._http.post(path, json=json))
|
|
245
|
+
|
|
246
|
+
async def _put(self, path: str, *, json: Any = None) -> Any:
|
|
247
|
+
return self._handle(await self._http.put(path, json=json))
|
|
248
|
+
|
|
249
|
+
async def _delete(self, path: str) -> Any:
|
|
250
|
+
return self._handle(await self._http.delete(path))
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _handle(resp: httpx.Response) -> Any:
|
|
254
|
+
if resp.status_code == 401:
|
|
255
|
+
raise AuthError("Session expired or invalid. Re-authenticate.", status_code=401)
|
|
256
|
+
if resp.status_code == 404:
|
|
257
|
+
raise NotFoundError(resp.text[:200], status_code=404)
|
|
258
|
+
if resp.status_code == 409:
|
|
259
|
+
raise ConflictError(resp.text[:200], status_code=409)
|
|
260
|
+
if resp.status_code >= 500:
|
|
261
|
+
raise ServerError(resp.text[:200], status_code=resp.status_code)
|
|
262
|
+
if resp.status_code >= 400:
|
|
263
|
+
raise KestrelError(resp.text[:200], status_code=resp.status_code)
|
|
264
|
+
if not resp.text:
|
|
265
|
+
return None
|
|
266
|
+
return resp.json()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Config:
|
|
11
|
+
server_url: str = ""
|
|
12
|
+
session_token: str = ""
|
|
13
|
+
user_id: str = ""
|
|
14
|
+
email: str = ""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def is_logged_in(self) -> bool:
|
|
18
|
+
return bool(self.server_url and self.session_token)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_CONFIG_DIR = Path.home() / ".kestrel"
|
|
22
|
+
_CONFIG_PATH = _CONFIG_DIR / "config.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def config_path() -> Path:
|
|
26
|
+
return _CONFIG_PATH
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_config() -> Config:
|
|
30
|
+
if not _CONFIG_PATH.exists():
|
|
31
|
+
return Config()
|
|
32
|
+
raw = json.loads(_CONFIG_PATH.read_text())
|
|
33
|
+
return Config(
|
|
34
|
+
server_url=raw.get("server_url", ""),
|
|
35
|
+
session_token=raw.get("session_token", ""),
|
|
36
|
+
user_id=raw.get("user_id", ""),
|
|
37
|
+
email=raw.get("email", ""),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_config(cfg: Config) -> None:
|
|
42
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
_CONFIG_PATH.write_text(
|
|
44
|
+
json.dumps(
|
|
45
|
+
{
|
|
46
|
+
"server_url": cfg.server_url,
|
|
47
|
+
"session_token": cfg.session_token,
|
|
48
|
+
"user_id": cfg.user_id,
|
|
49
|
+
"email": cfg.email,
|
|
50
|
+
},
|
|
51
|
+
indent=2,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
os.chmod(_CONFIG_PATH, 0o600)
|