tktl-cli 0.1.0a1__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.
- tktl/__init__.py +0 -0
- tktl/api/__init__.py +29 -0
- tktl/api/client.py +154 -0
- tktl/api/comments.py +99 -0
- tktl/api/connections.py +86 -0
- tktl/api/datasets.py +502 -0
- tktl/api/execution.py +186 -0
- tktl/api/flows.py +75 -0
- tktl/api/folders.py +45 -0
- tktl/api/versions.py +383 -0
- tktl/cache/__init__.py +12 -0
- tktl/cache/freshness.py +33 -0
- tktl/cache/index.py +110 -0
- tktl/cache/manager.py +197 -0
- tktl/cli/__init__.py +0 -0
- tktl/cli/_deps.py +79 -0
- tktl/cli/_error_handler.py +41 -0
- tktl/cli/_json_arg.py +32 -0
- tktl/cli/auth.py +112 -0
- tktl/cli/cache_cmd.py +48 -0
- tktl/cli/comments.py +58 -0
- tktl/cli/config_cmd.py +42 -0
- tktl/cli/connections.py +122 -0
- tktl/cli/datasets.py +309 -0
- tktl/cli/flows.py +490 -0
- tktl/cli/folders.py +37 -0
- tktl/cli/groups.py +173 -0
- tktl/cli/init_cmd.py +130 -0
- tktl/cli/main.py +85 -0
- tktl/cli/nodes.py +245 -0
- tktl/cli/output.py +155 -0
- tktl/cli/render_cmd.py +68 -0
- tktl/cli/run.py +45 -0
- tktl/cli/update.py +89 -0
- tktl/cli/versions.py +223 -0
- tktl/config.py +113 -0
- tktl/merge.py +27 -0
- tktl/models/__init__.py +24 -0
- tktl/models/batch.py +59 -0
- tktl/models/connection.py +43 -0
- tktl/models/dataset.py +141 -0
- tktl/models/diff.py +136 -0
- tktl/models/errors.py +43 -0
- tktl/models/flow.py +25 -0
- tktl/models/folder.py +20 -0
- tktl/models/group.py +22 -0
- tktl/models/node.py +24 -0
- tktl/models/version.py +28 -0
- tktl/ops/__init__.py +64 -0
- tktl/ops/batch.py +611 -0
- tktl/ops/cache_ops.py +121 -0
- tktl/ops/comments.py +119 -0
- tktl/ops/connections.py +88 -0
- tktl/ops/datasets.py +546 -0
- tktl/ops/diff.py +668 -0
- tktl/ops/explain.py +162 -0
- tktl/ops/flows.py +234 -0
- tktl/ops/folders.py +45 -0
- tktl/ops/graph.py +489 -0
- tktl/ops/groups.py +265 -0
- tktl/ops/manual_review.py +88 -0
- tktl/ops/nodes.py +290 -0
- tktl/ops/render.py +903 -0
- tktl/ops/rule_parser.py +207 -0
- tktl/ops/templates.py +399 -0
- tktl/ops/versions.py +575 -0
- tktl/resolver.py +121 -0
- tktl/telemetry.py +199 -0
- tktl_cli-0.1.0a1.dist-info/METADATA +11 -0
- tktl_cli-0.1.0a1.dist-info/RECORD +72 -0
- tktl_cli-0.1.0a1.dist-info/WHEEL +4 -0
- tktl_cli-0.1.0a1.dist-info/entry_points.txt +2 -0
tktl/__init__.py
ADDED
|
File without changes
|
tktl/api/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Taktile API client layer."""
|
|
2
|
+
|
|
3
|
+
from tktl.api.client import TktlClient
|
|
4
|
+
from tktl.api.execution import run_flow
|
|
5
|
+
from tktl.api.flows import create_flow, get_flow_by_slug, list_flows
|
|
6
|
+
from tktl.api.versions import (
|
|
7
|
+
create_version,
|
|
8
|
+
delete_version,
|
|
9
|
+
duplicate_version,
|
|
10
|
+
get_changes,
|
|
11
|
+
get_version,
|
|
12
|
+
patch_version,
|
|
13
|
+
put_version,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"TktlClient",
|
|
18
|
+
"create_flow",
|
|
19
|
+
"create_version",
|
|
20
|
+
"delete_version",
|
|
21
|
+
"duplicate_version",
|
|
22
|
+
"get_changes",
|
|
23
|
+
"get_flow_by_slug",
|
|
24
|
+
"get_version",
|
|
25
|
+
"list_flows",
|
|
26
|
+
"patch_version",
|
|
27
|
+
"put_version",
|
|
28
|
+
"run_flow",
|
|
29
|
+
]
|
tktl/api/client.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Thin HTTP client wrapping httpx with auth, base URL, and retry logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from tktl.config import Config, Credentials, _DEFAULT_CONFIG_DIR
|
|
12
|
+
from tktl.models.errors import AuthError, NetworkError
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://flow-api.taktile.com"
|
|
15
|
+
|
|
16
|
+
# Retry settings for network errors
|
|
17
|
+
_MAX_RETRIES = 3
|
|
18
|
+
_BACKOFF_DELAYS = [1, 2, 4] # seconds
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TktlClient:
|
|
22
|
+
"""Synchronous HTTP client for the Taktile Flow API.
|
|
23
|
+
|
|
24
|
+
Reads config and credentials from disk (or accepts explicit overrides).
|
|
25
|
+
Attaches ``X-Api-Key`` header to every request.
|
|
26
|
+
Retries network errors with exponential backoff.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
config_dir: Path = _DEFAULT_CONFIG_DIR,
|
|
32
|
+
*,
|
|
33
|
+
base_url: str | None = None,
|
|
34
|
+
api_key: str | None = None,
|
|
35
|
+
transport: httpx.BaseTransport | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
# Load config from disk
|
|
38
|
+
config = Config(config_dir)
|
|
39
|
+
config.load()
|
|
40
|
+
|
|
41
|
+
# Determine base URL
|
|
42
|
+
if base_url is not None:
|
|
43
|
+
self._base_url = base_url
|
|
44
|
+
else:
|
|
45
|
+
self._base_url = config.get("flow_api_url") or DEFAULT_BASE_URL
|
|
46
|
+
|
|
47
|
+
# Determine API key
|
|
48
|
+
if api_key is not None:
|
|
49
|
+
self._api_key = api_key
|
|
50
|
+
self._user_id: str | None = None
|
|
51
|
+
else:
|
|
52
|
+
creds = Credentials(config_dir)
|
|
53
|
+
creds.load()
|
|
54
|
+
if not creds.api_key:
|
|
55
|
+
raise AuthError("No API key found. Run `tktl auth login` to authenticate.")
|
|
56
|
+
self._api_key = creds.api_key
|
|
57
|
+
self._user_id = creds.user_id
|
|
58
|
+
|
|
59
|
+
self._config = config
|
|
60
|
+
|
|
61
|
+
# Build the httpx client
|
|
62
|
+
self._http = httpx.Client(
|
|
63
|
+
base_url=self._base_url,
|
|
64
|
+
headers={"X-Api-Key": self._api_key},
|
|
65
|
+
transport=transport,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def base_url(self) -> str:
|
|
70
|
+
return self._base_url
|
|
71
|
+
|
|
72
|
+
# ---- HTTP verbs ----
|
|
73
|
+
|
|
74
|
+
def get(
|
|
75
|
+
self,
|
|
76
|
+
path: str,
|
|
77
|
+
*,
|
|
78
|
+
params: dict[str, Any] | None = None,
|
|
79
|
+
headers: dict[str, str] | None = None,
|
|
80
|
+
) -> httpx.Response:
|
|
81
|
+
return self._request("GET", path, params=params, headers=headers)
|
|
82
|
+
|
|
83
|
+
def post(
|
|
84
|
+
self,
|
|
85
|
+
path: str,
|
|
86
|
+
*,
|
|
87
|
+
json: Any = None,
|
|
88
|
+
params: dict[str, Any] | None = None,
|
|
89
|
+
headers: dict[str, str] | None = None,
|
|
90
|
+
) -> httpx.Response:
|
|
91
|
+
return self._request("POST", path, json=json, params=params, headers=headers)
|
|
92
|
+
|
|
93
|
+
def patch(
|
|
94
|
+
self,
|
|
95
|
+
path: str,
|
|
96
|
+
*,
|
|
97
|
+
json: Any = None,
|
|
98
|
+
params: dict[str, Any] | None = None,
|
|
99
|
+
headers: dict[str, str] | None = None,
|
|
100
|
+
) -> httpx.Response:
|
|
101
|
+
return self._request("PATCH", path, json=json, params=params, headers=headers)
|
|
102
|
+
|
|
103
|
+
def put(
|
|
104
|
+
self,
|
|
105
|
+
path: str,
|
|
106
|
+
*,
|
|
107
|
+
json: Any = None,
|
|
108
|
+
params: dict[str, Any] | None = None,
|
|
109
|
+
headers: dict[str, str] | None = None,
|
|
110
|
+
) -> httpx.Response:
|
|
111
|
+
return self._request("PUT", path, json=json, params=params, headers=headers)
|
|
112
|
+
|
|
113
|
+
def delete(
|
|
114
|
+
self,
|
|
115
|
+
path: str,
|
|
116
|
+
*,
|
|
117
|
+
json: Any = None,
|
|
118
|
+
params: dict[str, Any] | None = None,
|
|
119
|
+
headers: dict[str, str] | None = None,
|
|
120
|
+
) -> httpx.Response:
|
|
121
|
+
return self._request("DELETE", path, json=json, params=params, headers=headers)
|
|
122
|
+
|
|
123
|
+
# ---- internal ----
|
|
124
|
+
|
|
125
|
+
def _request(
|
|
126
|
+
self,
|
|
127
|
+
method: str,
|
|
128
|
+
path: str,
|
|
129
|
+
*,
|
|
130
|
+
json: Any = None,
|
|
131
|
+
params: dict[str, Any] | None = None,
|
|
132
|
+
headers: dict[str, str] | None = None,
|
|
133
|
+
) -> httpx.Response:
|
|
134
|
+
"""Send an HTTP request with retry logic for network errors."""
|
|
135
|
+
last_exc: Exception | None = None
|
|
136
|
+
|
|
137
|
+
for attempt in range(_MAX_RETRIES + 1):
|
|
138
|
+
try:
|
|
139
|
+
return self._http.request(
|
|
140
|
+
method,
|
|
141
|
+
path,
|
|
142
|
+
json=json,
|
|
143
|
+
params=params,
|
|
144
|
+
headers=headers,
|
|
145
|
+
)
|
|
146
|
+
except httpx.HTTPError as exc:
|
|
147
|
+
last_exc = exc
|
|
148
|
+
if attempt < _MAX_RETRIES:
|
|
149
|
+
time.sleep(_BACKOFF_DELAYS[attempt])
|
|
150
|
+
continue
|
|
151
|
+
raise NetworkError(str(exc)) from exc
|
|
152
|
+
|
|
153
|
+
# Should not reach here, but satisfy type checker
|
|
154
|
+
raise NetworkError(str(last_exc)) # pragma: no cover
|
tktl/api/comments.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Comment API functions.
|
|
2
|
+
|
|
3
|
+
Comments are per-flow-version and can be attached to nodes or the version itself.
|
|
4
|
+
Create uses PUT /api/v1/comments/{comment_id} (client-generated ID).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from tktl.api.client import TktlClient
|
|
13
|
+
from tktl.models.errors import NotFoundError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_comments(
|
|
17
|
+
client: TktlClient,
|
|
18
|
+
flow_version_id: str,
|
|
19
|
+
*,
|
|
20
|
+
resource_type: str | None = None,
|
|
21
|
+
resource_id: str | None = None,
|
|
22
|
+
) -> list[dict[str, Any]]:
|
|
23
|
+
"""List comments for a flow version.
|
|
24
|
+
|
|
25
|
+
GET /api/v1/comments?flow_version_id=...
|
|
26
|
+
"""
|
|
27
|
+
params: dict[str, str] = {"flow_version_id": flow_version_id}
|
|
28
|
+
if resource_type is not None:
|
|
29
|
+
params["resource_type"] = resource_type
|
|
30
|
+
if resource_id is not None:
|
|
31
|
+
params["resource_id"] = resource_id
|
|
32
|
+
|
|
33
|
+
resp = client.get("/api/v1/comments", params=params)
|
|
34
|
+
if resp.status_code == 200:
|
|
35
|
+
return resp.json()
|
|
36
|
+
resp.raise_for_status()
|
|
37
|
+
return [] # pragma: no cover
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_comment(
|
|
41
|
+
client: TktlClient,
|
|
42
|
+
flow_version_id: str,
|
|
43
|
+
content: str,
|
|
44
|
+
*,
|
|
45
|
+
resource_type: str | None = None,
|
|
46
|
+
resource_id: str | None = None,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""Create a comment on a flow version or node.
|
|
49
|
+
|
|
50
|
+
PUT /api/v1/comments/{comment_id} (client-generated UUID)
|
|
51
|
+
"""
|
|
52
|
+
comment_id = str(uuid.uuid4())
|
|
53
|
+
body: dict[str, Any] = {
|
|
54
|
+
"content": content,
|
|
55
|
+
"flow_version": flow_version_id,
|
|
56
|
+
}
|
|
57
|
+
if resource_type is not None:
|
|
58
|
+
body["resource_type"] = resource_type
|
|
59
|
+
if resource_id is not None:
|
|
60
|
+
body["resource_id"] = resource_id
|
|
61
|
+
|
|
62
|
+
resp = client.put(f"/api/v1/comments/{comment_id}", json=body)
|
|
63
|
+
if resp.status_code == 200:
|
|
64
|
+
return resp.json()
|
|
65
|
+
if resp.status_code == 404:
|
|
66
|
+
raise NotFoundError(f"Flow version '{flow_version_id}' not found")
|
|
67
|
+
resp.raise_for_status()
|
|
68
|
+
return {} # pragma: no cover
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def delete_comment(
|
|
72
|
+
client: TktlClient,
|
|
73
|
+
comment_id: str,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Delete a comment.
|
|
76
|
+
|
|
77
|
+
DELETE /api/v1/comments/{comment_id}
|
|
78
|
+
"""
|
|
79
|
+
resp = client.delete(f"/api/v1/comments/{comment_id}")
|
|
80
|
+
if resp.status_code == 404:
|
|
81
|
+
raise NotFoundError(f"Comment '{comment_id}' not found")
|
|
82
|
+
resp.raise_for_status()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_comment_summary(
|
|
86
|
+
client: TktlClient,
|
|
87
|
+
flow_version_id: str,
|
|
88
|
+
) -> list[dict[str, Any]]:
|
|
89
|
+
"""Get per-node comment counts for a flow version.
|
|
90
|
+
|
|
91
|
+
GET /api/v1/comments/summary/{flow_version_id}
|
|
92
|
+
"""
|
|
93
|
+
resp = client.get(f"/api/v1/comments/summary/{flow_version_id}")
|
|
94
|
+
if resp.status_code == 200:
|
|
95
|
+
return resp.json()
|
|
96
|
+
if resp.status_code == 404:
|
|
97
|
+
raise NotFoundError(f"Flow version '{flow_version_id}' not found")
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
return [] # pragma: no cover
|
tktl/api/connections.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Workspace connections API.
|
|
2
|
+
|
|
3
|
+
The Connect API is per-workspace and lives on the workspace base URL:
|
|
4
|
+
GET {base_url}/connect/api/v1/connections — list all connections
|
|
5
|
+
POST {base_url}/connect/api/v1/connections — create a connection
|
|
6
|
+
POST {base_url}/connect/api/v1/connections/{id}/resources — create a resource config
|
|
7
|
+
PATCH {base_url}/connect/api/v1/connections/{id}/secrets — update secrets
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from tktl.models.errors import NetworkError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def list_connections(api_key: str, base_url: str) -> list[dict[str, Any]]:
|
|
20
|
+
"""List all connections in the workspace.
|
|
21
|
+
|
|
22
|
+
GET {base_url}/connect/api/v1/connections
|
|
23
|
+
"""
|
|
24
|
+
url = f"{base_url}/connect/api/v1/connections"
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
resp = httpx.get(url, headers={"X-Api-Key": api_key}, timeout=15.0)
|
|
28
|
+
except httpx.HTTPError as exc:
|
|
29
|
+
raise NetworkError(f"Failed to list connections: {exc}") from exc
|
|
30
|
+
|
|
31
|
+
if resp.status_code == 200:
|
|
32
|
+
return resp.json()
|
|
33
|
+
raise NetworkError(f"Failed to list connections: HTTP {resp.status_code}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_connection(api_key: str, base_url: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
37
|
+
"""Create a new connection.
|
|
38
|
+
|
|
39
|
+
POST {base_url}/connect/api/v1/connections
|
|
40
|
+
"""
|
|
41
|
+
url = f"{base_url}/connect/api/v1/connections"
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
resp = httpx.post(url, json=body, headers={"X-Api-Key": api_key}, timeout=15.0)
|
|
45
|
+
except httpx.HTTPError as exc:
|
|
46
|
+
raise NetworkError(f"Failed to create connection: {exc}") from exc
|
|
47
|
+
|
|
48
|
+
if resp.status_code in (200, 201):
|
|
49
|
+
return resp.json()
|
|
50
|
+
raise NetworkError(f"Failed to create connection: HTTP {resp.status_code}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_resource_config(api_key: str, base_url: str, connection_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
54
|
+
"""Create a resource config on a connection.
|
|
55
|
+
|
|
56
|
+
POST {base_url}/connect/api/v1/connections/{connection_id}/resources
|
|
57
|
+
"""
|
|
58
|
+
url = f"{base_url}/connect/api/v1/connections/{connection_id}/resources"
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
resp = httpx.post(url, json=body, headers={"X-Api-Key": api_key}, timeout=15.0)
|
|
62
|
+
except httpx.HTTPError as exc:
|
|
63
|
+
raise NetworkError(f"Failed to create resource config: {exc}") from exc
|
|
64
|
+
|
|
65
|
+
if resp.status_code in (200, 201):
|
|
66
|
+
return resp.json()
|
|
67
|
+
raise NetworkError(f"Failed to create resource config: HTTP {resp.status_code}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def update_secrets(
|
|
71
|
+
api_key: str, base_url: str, connection_id: str, secrets: list[dict[str, Any]]
|
|
72
|
+
) -> list[dict[str, Any]]:
|
|
73
|
+
"""Update secrets on a connection.
|
|
74
|
+
|
|
75
|
+
PATCH {base_url}/connect/api/v1/connections/{connection_id}/secrets
|
|
76
|
+
"""
|
|
77
|
+
url = f"{base_url}/connect/api/v1/connections/{connection_id}/secrets"
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
resp = httpx.patch(url, json=secrets, headers={"X-Api-Key": api_key}, timeout=15.0)
|
|
81
|
+
except httpx.HTTPError as exc:
|
|
82
|
+
raise NetworkError(f"Failed to update secrets: {exc}") from exc
|
|
83
|
+
|
|
84
|
+
if resp.status_code == 200:
|
|
85
|
+
return resp.json()
|
|
86
|
+
raise NetworkError(f"Failed to update secrets: HTTP {resp.status_code}")
|