agentplane-sdk 0.0.1__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.
- agentplane_sdk-0.0.1/.gitignore +34 -0
- agentplane_sdk-0.0.1/PKG-INFO +41 -0
- agentplane_sdk-0.0.1/README.md +29 -0
- agentplane_sdk-0.0.1/pyproject.toml +26 -0
- agentplane_sdk-0.0.1/src/agentplane_sdk/__init__.py +43 -0
- agentplane_sdk-0.0.1/src/agentplane_sdk/auth.py +99 -0
- agentplane_sdk-0.0.1/src/agentplane_sdk/cli.py +326 -0
- agentplane_sdk-0.0.1/src/agentplane_sdk/client.py +396 -0
- agentplane_sdk-0.0.1/src/agentplane_sdk/config.py +78 -0
- agentplane_sdk-0.0.1/src/agentplane_sdk/errors.py +54 -0
- agentplane_sdk-0.0.1/tests/conftest.py +21 -0
- agentplane_sdk-0.0.1/tests/test_auth.py +55 -0
- agentplane_sdk-0.0.1/tests/test_cli.py +121 -0
- agentplane_sdk-0.0.1/tests/test_client.py +160 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# internal working documents — not part of the public repo
|
|
2
|
+
CLAUDE.md
|
|
3
|
+
SPEC.md
|
|
4
|
+
.claude/
|
|
5
|
+
|
|
6
|
+
# python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.venv/
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
|
|
14
|
+
# tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
|
|
21
|
+
# local databases and state
|
|
22
|
+
*.db
|
|
23
|
+
registry.db
|
|
24
|
+
runtime.db
|
|
25
|
+
|
|
26
|
+
# environments / secrets
|
|
27
|
+
.env
|
|
28
|
+
.env.*
|
|
29
|
+
|
|
30
|
+
# editors / OS
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
.DS_Store
|
|
34
|
+
Thumbs.db
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentplane-sdk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: agentplane typed client + CLI
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: agentplane-core<0.1.0,>=0.0.1
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: typer>=0.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# agentplane-sdk
|
|
14
|
+
|
|
15
|
+
Thin typed client + CLI for the agentplane platform. Everything the SDK does
|
|
16
|
+
is possible with plain HTTP — the SDK is convenience, not a requirement.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from agentplane_sdk import RuntimeClient
|
|
20
|
+
|
|
21
|
+
async with RuntimeClient("https://api.example/runtime", token="…") as client:
|
|
22
|
+
info = await client.deploy("support-rag")
|
|
23
|
+
print(info.endpoint_url)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
CLI:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
agentplane validate flow.yaml
|
|
30
|
+
agentplane deploy flow.yaml [--draft]
|
|
31
|
+
agentplane undeploy <name>
|
|
32
|
+
agentplane list [--status deployed] [--json]
|
|
33
|
+
agentplane export <name> [-o flow.yaml]
|
|
34
|
+
agentplane search "invoice" [--tags rag] [--semantic]
|
|
35
|
+
agentplane resources list|create -f res.yaml|delete <name>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Config resolution: flags → env (`AGENTPLANE_RUNTIME_URL`,
|
|
39
|
+
`AGENTPLANE_REGISTRY_URL`, `AGENTPLANE_TOKEN` or OIDC vars) →
|
|
40
|
+
`~/.config/agentplane/config.toml`. Exit codes: 0 ok, 1 validation failed,
|
|
41
|
+
2 transport/auth error, 3 not found.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# agentplane-sdk
|
|
2
|
+
|
|
3
|
+
Thin typed client + CLI for the agentplane platform. Everything the SDK does
|
|
4
|
+
is possible with plain HTTP — the SDK is convenience, not a requirement.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from agentplane_sdk import RuntimeClient
|
|
8
|
+
|
|
9
|
+
async with RuntimeClient("https://api.example/runtime", token="…") as client:
|
|
10
|
+
info = await client.deploy("support-rag")
|
|
11
|
+
print(info.endpoint_url)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
CLI:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
agentplane validate flow.yaml
|
|
18
|
+
agentplane deploy flow.yaml [--draft]
|
|
19
|
+
agentplane undeploy <name>
|
|
20
|
+
agentplane list [--status deployed] [--json]
|
|
21
|
+
agentplane export <name> [-o flow.yaml]
|
|
22
|
+
agentplane search "invoice" [--tags rag] [--semantic]
|
|
23
|
+
agentplane resources list|create -f res.yaml|delete <name>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Config resolution: flags → env (`AGENTPLANE_RUNTIME_URL`,
|
|
27
|
+
`AGENTPLANE_REGISTRY_URL`, `AGENTPLANE_TOKEN` or OIDC vars) →
|
|
28
|
+
`~/.config/agentplane/config.toml`. Exit codes: 0 ok, 1 validation failed,
|
|
29
|
+
2 transport/auth error, 3 not found.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agentplane-sdk"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "agentplane typed client + CLI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"agentplane-core>=0.0.1,<0.1.0",
|
|
10
|
+
"httpx>=0.27",
|
|
11
|
+
"typer>=0.12",
|
|
12
|
+
"pyyaml>=6.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
agentplane = "agentplane_sdk.cli:app"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/agentplane_sdk"]
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources]
|
|
26
|
+
agentplane-core = { workspace = true }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""agentplane-sdk: thin typed client + CLI."""
|
|
2
|
+
|
|
3
|
+
from agentplane_sdk.auth import (
|
|
4
|
+
OidcClientCredentialsProvider,
|
|
5
|
+
StaticTokenProvider,
|
|
6
|
+
TokenProvider,
|
|
7
|
+
as_token_provider,
|
|
8
|
+
)
|
|
9
|
+
from agentplane_sdk.client import (
|
|
10
|
+
RegistryClient,
|
|
11
|
+
RuntimeClient,
|
|
12
|
+
SyncRegistryClient,
|
|
13
|
+
SyncRuntimeClient,
|
|
14
|
+
)
|
|
15
|
+
from agentplane_sdk.errors import (
|
|
16
|
+
AgentplaneError,
|
|
17
|
+
ApiError,
|
|
18
|
+
AuthError,
|
|
19
|
+
ConflictError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
TransportError,
|
|
22
|
+
ValidationFailedError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "0.0.1"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AgentplaneError",
|
|
29
|
+
"ApiError",
|
|
30
|
+
"AuthError",
|
|
31
|
+
"ConflictError",
|
|
32
|
+
"NotFoundError",
|
|
33
|
+
"OidcClientCredentialsProvider",
|
|
34
|
+
"RegistryClient",
|
|
35
|
+
"RuntimeClient",
|
|
36
|
+
"StaticTokenProvider",
|
|
37
|
+
"SyncRegistryClient",
|
|
38
|
+
"SyncRuntimeClient",
|
|
39
|
+
"TokenProvider",
|
|
40
|
+
"TransportError",
|
|
41
|
+
"ValidationFailedError",
|
|
42
|
+
"as_token_provider",
|
|
43
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Token providers: static bearer and OIDC client-credentials (SPEC §4.1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from agentplane_sdk.errors import AuthError, TransportError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class TokenProvider(Protocol):
|
|
15
|
+
"""Anything that can produce a bearer token."""
|
|
16
|
+
|
|
17
|
+
async def get_token(self) -> str: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StaticTokenProvider:
|
|
21
|
+
"""Wraps a fixed bearer token."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, token: str) -> None:
|
|
24
|
+
self._token = token
|
|
25
|
+
|
|
26
|
+
async def get_token(self) -> str:
|
|
27
|
+
return self._token
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OidcClientCredentialsProvider:
|
|
31
|
+
"""Fetches tokens via the OIDC client-credentials grant, cached until expiry."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
issuer: str,
|
|
36
|
+
client_id: str,
|
|
37
|
+
client_secret: str,
|
|
38
|
+
*,
|
|
39
|
+
audience: str | None = None,
|
|
40
|
+
leeway_s: float = 30.0,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._issuer = issuer.rstrip("/")
|
|
43
|
+
self._client_id = client_id
|
|
44
|
+
self._client_secret = client_secret
|
|
45
|
+
self._audience = audience
|
|
46
|
+
self._leeway_s = leeway_s
|
|
47
|
+
self._token: str | None = None
|
|
48
|
+
self._expires_at: float = 0.0
|
|
49
|
+
self._token_endpoint: str | None = None
|
|
50
|
+
|
|
51
|
+
async def get_token(self) -> str:
|
|
52
|
+
if self._token is not None and time.monotonic() < self._expires_at:
|
|
53
|
+
return self._token
|
|
54
|
+
try:
|
|
55
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
56
|
+
if self._token_endpoint is None:
|
|
57
|
+
discovery = await client.get(f"{self._issuer}/.well-known/openid-configuration")
|
|
58
|
+
discovery.raise_for_status()
|
|
59
|
+
endpoint = discovery.json().get("token_endpoint")
|
|
60
|
+
if not isinstance(endpoint, str):
|
|
61
|
+
raise AuthError("issuer discovery returned no token_endpoint")
|
|
62
|
+
self._token_endpoint = endpoint
|
|
63
|
+
data = {
|
|
64
|
+
"grant_type": "client_credentials",
|
|
65
|
+
"client_id": self._client_id,
|
|
66
|
+
"client_secret": self._client_secret,
|
|
67
|
+
}
|
|
68
|
+
if self._audience:
|
|
69
|
+
data["audience"] = self._audience
|
|
70
|
+
response = await client.post(self._token_endpoint, data=data)
|
|
71
|
+
except httpx.HTTPError as exc:
|
|
72
|
+
raise TransportError(f"token request failed: {exc}") from exc
|
|
73
|
+
if response.status_code != httpx.codes.OK:
|
|
74
|
+
raise AuthError(f"token endpoint returned {response.status_code}")
|
|
75
|
+
payload = response.json()
|
|
76
|
+
token = payload.get("access_token")
|
|
77
|
+
if not isinstance(token, str):
|
|
78
|
+
raise AuthError("token endpoint returned no access_token")
|
|
79
|
+
expires_in = payload.get("expires_in", 300)
|
|
80
|
+
ttl = float(expires_in) if isinstance(expires_in, int | float) else 300.0
|
|
81
|
+
self._token = token
|
|
82
|
+
self._expires_at = time.monotonic() + max(ttl - self._leeway_s, 5.0)
|
|
83
|
+
return token
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def as_token_provider(token: str | TokenProvider | None) -> TokenProvider | None:
|
|
87
|
+
if token is None:
|
|
88
|
+
return None
|
|
89
|
+
if isinstance(token, str):
|
|
90
|
+
return StaticTokenProvider(token)
|
|
91
|
+
return token
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"OidcClientCredentialsProvider",
|
|
96
|
+
"StaticTokenProvider",
|
|
97
|
+
"TokenProvider",
|
|
98
|
+
"as_token_provider",
|
|
99
|
+
]
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""The ``agentplane`` CLI (SPEC §4.2).
|
|
2
|
+
|
|
3
|
+
Exit codes: 0 ok, 1 validation failed, 2 transport/auth error, 3 not found.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import Coroutine
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated, Any
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
import yaml
|
|
16
|
+
from pydantic import TypeAdapter, ValidationError
|
|
17
|
+
|
|
18
|
+
from agentplane_core import (
|
|
19
|
+
FlowDefinition,
|
|
20
|
+
Resource,
|
|
21
|
+
ValidationResult,
|
|
22
|
+
validate_structure,
|
|
23
|
+
)
|
|
24
|
+
from agentplane_sdk.client import RegistryClient, RuntimeClient
|
|
25
|
+
from agentplane_sdk.config import resolve_config, token_provider_from_config
|
|
26
|
+
from agentplane_sdk.errors import (
|
|
27
|
+
AuthError,
|
|
28
|
+
ConflictError,
|
|
29
|
+
NotFoundError,
|
|
30
|
+
TransportError,
|
|
31
|
+
ValidationFailedError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
app = typer.Typer(name="agentplane", help="agentplane platform CLI", no_args_is_help=True)
|
|
35
|
+
resources_app = typer.Typer(help="Manage runtime resources", no_args_is_help=True)
|
|
36
|
+
app.add_typer(resources_app, name="resources")
|
|
37
|
+
|
|
38
|
+
EXIT_OK = 0
|
|
39
|
+
EXIT_VALIDATION = 1
|
|
40
|
+
EXIT_TRANSPORT = 2
|
|
41
|
+
EXIT_NOT_FOUND = 3
|
|
42
|
+
|
|
43
|
+
_RESOURCE_ADAPTER: TypeAdapter[Resource] = TypeAdapter(Resource)
|
|
44
|
+
|
|
45
|
+
RuntimeUrlOption = Annotated[
|
|
46
|
+
str | None, typer.Option("--runtime-url", envvar="AGENTPLANE_RUNTIME_URL")
|
|
47
|
+
]
|
|
48
|
+
RegistryUrlOption = Annotated[
|
|
49
|
+
str | None, typer.Option("--registry-url", envvar="AGENTPLANE_REGISTRY_URL")
|
|
50
|
+
]
|
|
51
|
+
TokenOption = Annotated[str | None, typer.Option("--token", envvar="AGENTPLANE_TOKEN")]
|
|
52
|
+
JsonFlag = Annotated[bool, typer.Option("--json", help="machine-readable output")]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _fail(message: str, code: int) -> None:
|
|
56
|
+
typer.echo(message, err=True)
|
|
57
|
+
raise typer.Exit(code)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _run[R](coro: Coroutine[Any, Any, R]) -> R:
|
|
61
|
+
try:
|
|
62
|
+
return asyncio.run(coro)
|
|
63
|
+
except ValidationFailedError as exc:
|
|
64
|
+
_print_issues(exc.result, as_json=False)
|
|
65
|
+
raise typer.Exit(EXIT_VALIDATION) from None
|
|
66
|
+
except NotFoundError as exc:
|
|
67
|
+
_fail(f"not found: {exc}", EXIT_NOT_FOUND)
|
|
68
|
+
except (TransportError, AuthError) as exc:
|
|
69
|
+
_fail(str(exc), EXIT_TRANSPORT)
|
|
70
|
+
except ConflictError as exc:
|
|
71
|
+
_fail(f"conflict: {exc}", EXIT_TRANSPORT)
|
|
72
|
+
raise AssertionError("unreachable")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_yaml(path: Path) -> dict[str, object]:
|
|
76
|
+
try:
|
|
77
|
+
with path.open(encoding="utf-8") as fh:
|
|
78
|
+
data = yaml.safe_load(fh)
|
|
79
|
+
except FileNotFoundError:
|
|
80
|
+
_fail(f"file not found: {path}", EXIT_NOT_FOUND)
|
|
81
|
+
except yaml.YAMLError as exc:
|
|
82
|
+
_fail(f"invalid YAML in {path}: {exc}", EXIT_VALIDATION)
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
_fail(f"{path} does not contain a mapping", EXIT_VALIDATION)
|
|
85
|
+
return {str(key): value for key, value in data.items()}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _print_issues(result: ValidationResult, *, as_json: bool) -> None:
|
|
89
|
+
if as_json:
|
|
90
|
+
typer.echo(result.model_dump_json(indent=2))
|
|
91
|
+
return
|
|
92
|
+
for issue in result.issues:
|
|
93
|
+
typer.echo(f"{issue.severity.upper()} {issue.code} {issue.path}: {issue.message}", err=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _runtime_client(runtime_url: str | None, token: str | None) -> RuntimeClient:
|
|
97
|
+
config = resolve_config(runtime_url=runtime_url, token=token)
|
|
98
|
+
if not config.runtime_url:
|
|
99
|
+
_fail(
|
|
100
|
+
"no runtime URL configured (flag --runtime-url / env AGENTPLANE_RUNTIME_URL)",
|
|
101
|
+
EXIT_TRANSPORT,
|
|
102
|
+
)
|
|
103
|
+
assert config.runtime_url is not None
|
|
104
|
+
return RuntimeClient(config.runtime_url, token_provider_from_config(config))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _registry_client(registry_url: str | None, token: str | None) -> RegistryClient:
|
|
108
|
+
config = resolve_config(registry_url=registry_url, token=token)
|
|
109
|
+
if not config.registry_url:
|
|
110
|
+
_fail(
|
|
111
|
+
"no registry URL configured (flag --registry-url / env AGENTPLANE_REGISTRY_URL)",
|
|
112
|
+
EXIT_TRANSPORT,
|
|
113
|
+
)
|
|
114
|
+
assert config.registry_url is not None
|
|
115
|
+
return RegistryClient(config.registry_url, token_provider_from_config(config))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def validate(
|
|
120
|
+
file: Annotated[Path, typer.Argument(help="flow definition YAML")],
|
|
121
|
+
json_output: JsonFlag = False,
|
|
122
|
+
remote: Annotated[
|
|
123
|
+
bool, typer.Option("--remote", help="ask the runtime (authoritative, adds E020-E022)")
|
|
124
|
+
] = False,
|
|
125
|
+
runtime_url: RuntimeUrlOption = None,
|
|
126
|
+
token: TokenOption = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Validate a definition; local by default, --remote for the runtime's answer."""
|
|
129
|
+
raw = _load_yaml(file)
|
|
130
|
+
if remote:
|
|
131
|
+
|
|
132
|
+
async def remote_validate() -> ValidationResult:
|
|
133
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
134
|
+
return await client.validate(raw)
|
|
135
|
+
|
|
136
|
+
result = _run(remote_validate())
|
|
137
|
+
else:
|
|
138
|
+
result = ValidationResult.from_issues(validate_structure(raw))
|
|
139
|
+
_print_issues(result, as_json=json_output)
|
|
140
|
+
raise typer.Exit(EXIT_OK if result.valid else EXIT_VALIDATION)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def deploy(
|
|
145
|
+
file: Annotated[Path, typer.Argument(help="flow definition YAML")],
|
|
146
|
+
draft: Annotated[bool, typer.Option("--draft", help="create/update the draft only")] = False,
|
|
147
|
+
runtime_url: RuntimeUrlOption = None,
|
|
148
|
+
token: TokenOption = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Create-or-update the definition by name; deploys unless --draft."""
|
|
151
|
+
raw = _load_yaml(file)
|
|
152
|
+
try:
|
|
153
|
+
defn = FlowDefinition.model_validate(raw)
|
|
154
|
+
except ValidationError:
|
|
155
|
+
result = ValidationResult.from_issues(validate_structure(raw))
|
|
156
|
+
_print_issues(result, as_json=False)
|
|
157
|
+
raise typer.Exit(EXIT_VALIDATION) from None
|
|
158
|
+
|
|
159
|
+
async def do_deploy() -> str:
|
|
160
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
161
|
+
try:
|
|
162
|
+
await client.create_draft(defn)
|
|
163
|
+
except ConflictError:
|
|
164
|
+
await client.update_draft(defn.name, defn)
|
|
165
|
+
if draft:
|
|
166
|
+
return f"draft saved: {defn.name}"
|
|
167
|
+
info = await client.deploy(defn.name)
|
|
168
|
+
return f"deployed {info.name} v{info.version} -> {info.endpoint_url}"
|
|
169
|
+
|
|
170
|
+
typer.echo(_run(do_deploy()))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def undeploy(
|
|
175
|
+
name: str,
|
|
176
|
+
runtime_url: RuntimeUrlOption = None,
|
|
177
|
+
token: TokenOption = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Stop serving a deployed definition and deregister it."""
|
|
180
|
+
|
|
181
|
+
async def do_undeploy() -> None:
|
|
182
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
183
|
+
await client.undeploy(name)
|
|
184
|
+
|
|
185
|
+
_run(do_undeploy())
|
|
186
|
+
typer.echo(f"undeployed {name}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command(name="list")
|
|
190
|
+
def list_definitions(
|
|
191
|
+
status: Annotated[str | None, typer.Option("--status")] = None,
|
|
192
|
+
json_output: JsonFlag = False,
|
|
193
|
+
runtime_url: RuntimeUrlOption = None,
|
|
194
|
+
token: TokenOption = None,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""List definitions known to the runtime."""
|
|
197
|
+
|
|
198
|
+
async def do_list() -> list[dict[str, object]]:
|
|
199
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
200
|
+
return [i.model_dump(mode="json") for i in await client.list(status)]
|
|
201
|
+
|
|
202
|
+
infos = _run(do_list())
|
|
203
|
+
if json_output:
|
|
204
|
+
typer.echo(json.dumps(infos, indent=2))
|
|
205
|
+
return
|
|
206
|
+
for info in infos:
|
|
207
|
+
endpoint = info.get("endpoint_url") or "-"
|
|
208
|
+
typer.echo(f"{info['name']:<30} {info['status']:<10} {endpoint}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command()
|
|
212
|
+
def export(
|
|
213
|
+
name: str,
|
|
214
|
+
output: Annotated[Path | None, typer.Option("-o", "--output")] = None,
|
|
215
|
+
version: Annotated[int | None, typer.Option("--version")] = None,
|
|
216
|
+
runtime_url: RuntimeUrlOption = None,
|
|
217
|
+
token: TokenOption = None,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Export the canonical serialized definition."""
|
|
220
|
+
|
|
221
|
+
async def do_export() -> FlowDefinition:
|
|
222
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
223
|
+
return await client.export(name, version)
|
|
224
|
+
|
|
225
|
+
defn = _run(do_export())
|
|
226
|
+
text = yaml.safe_dump(defn.canonical_dict(), sort_keys=False, allow_unicode=True)
|
|
227
|
+
if output is None:
|
|
228
|
+
typer.echo(text)
|
|
229
|
+
else:
|
|
230
|
+
output.write_text(text, encoding="utf-8")
|
|
231
|
+
typer.echo(f"wrote {output}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@app.command()
|
|
235
|
+
def search(
|
|
236
|
+
query: Annotated[str, typer.Argument()] = "",
|
|
237
|
+
tags: Annotated[list[str] | None, typer.Option("--tags")] = None,
|
|
238
|
+
semantic: Annotated[bool, typer.Option("--semantic")] = False,
|
|
239
|
+
kind: Annotated[str | None, typer.Option("--kind")] = None,
|
|
240
|
+
json_output: JsonFlag = False,
|
|
241
|
+
registry_url: RegistryUrlOption = None,
|
|
242
|
+
token: TokenOption = None,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Search the registry."""
|
|
245
|
+
|
|
246
|
+
async def do_search() -> list[dict[str, object]]:
|
|
247
|
+
async with _registry_client(registry_url, token) as client:
|
|
248
|
+
page = await client.search(query, tags=tags, kind=kind, semantic=semantic)
|
|
249
|
+
return [e.model_dump(mode="json") for e in page.items]
|
|
250
|
+
|
|
251
|
+
entries = _run(do_search())
|
|
252
|
+
if json_output:
|
|
253
|
+
typer.echo(json.dumps(entries, indent=2))
|
|
254
|
+
return
|
|
255
|
+
for entry in entries:
|
|
256
|
+
card = entry.get("card")
|
|
257
|
+
name = card.get("name", "?") if isinstance(card, dict) else "?"
|
|
258
|
+
typer.echo(f"{name:<30} {entry['kind']:<11} {entry['status']:<9} {entry['url']}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@resources_app.command(name="list")
|
|
262
|
+
def resources_list(
|
|
263
|
+
kind: Annotated[str | None, typer.Option("--kind")] = None,
|
|
264
|
+
json_output: JsonFlag = False,
|
|
265
|
+
runtime_url: RuntimeUrlOption = None,
|
|
266
|
+
token: TokenOption = None,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""List resources."""
|
|
269
|
+
|
|
270
|
+
async def do_list() -> list[dict[str, object]]:
|
|
271
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
272
|
+
resources = await client.list_resources(kind)
|
|
273
|
+
return [_RESOURCE_ADAPTER.dump_python(r, mode="json") for r in resources]
|
|
274
|
+
|
|
275
|
+
resources = _run(do_list())
|
|
276
|
+
if json_output:
|
|
277
|
+
typer.echo(json.dumps(resources, indent=2))
|
|
278
|
+
return
|
|
279
|
+
for resource in resources:
|
|
280
|
+
typer.echo(f"{resource['name']:<30} {resource['kind']}")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@resources_app.command(name="create")
|
|
284
|
+
def resources_create(
|
|
285
|
+
file: Annotated[Path, typer.Option("-f", "--file", help="resource YAML")],
|
|
286
|
+
runtime_url: RuntimeUrlOption = None,
|
|
287
|
+
token: TokenOption = None,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Create a resource from a YAML file."""
|
|
290
|
+
raw = _load_yaml(file)
|
|
291
|
+
try:
|
|
292
|
+
resource = _RESOURCE_ADAPTER.validate_python(raw)
|
|
293
|
+
except ValidationError as exc:
|
|
294
|
+
_fail(f"invalid resource: {exc}", EXIT_VALIDATION)
|
|
295
|
+
raise AssertionError("unreachable") from None
|
|
296
|
+
|
|
297
|
+
async def do_create() -> str:
|
|
298
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
299
|
+
created = await client.create_resource(resource)
|
|
300
|
+
return created.name
|
|
301
|
+
|
|
302
|
+
typer.echo(f"created resource {_run(do_create())}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@resources_app.command(name="delete")
|
|
306
|
+
def resources_delete(
|
|
307
|
+
name: str,
|
|
308
|
+
runtime_url: RuntimeUrlOption = None,
|
|
309
|
+
token: TokenOption = None,
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Delete a resource (refused while referenced)."""
|
|
312
|
+
|
|
313
|
+
async def do_delete() -> None:
|
|
314
|
+
async with _runtime_client(runtime_url, token) as client:
|
|
315
|
+
await client.delete_resource(name)
|
|
316
|
+
|
|
317
|
+
_run(do_delete())
|
|
318
|
+
typer.echo(f"deleted resource {name}")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> None: # pragma: no cover - thin wrapper
|
|
322
|
+
app()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if __name__ == "__main__": # pragma: no cover
|
|
326
|
+
main()
|