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.
Files changed (72) hide show
  1. tktl/__init__.py +0 -0
  2. tktl/api/__init__.py +29 -0
  3. tktl/api/client.py +154 -0
  4. tktl/api/comments.py +99 -0
  5. tktl/api/connections.py +86 -0
  6. tktl/api/datasets.py +502 -0
  7. tktl/api/execution.py +186 -0
  8. tktl/api/flows.py +75 -0
  9. tktl/api/folders.py +45 -0
  10. tktl/api/versions.py +383 -0
  11. tktl/cache/__init__.py +12 -0
  12. tktl/cache/freshness.py +33 -0
  13. tktl/cache/index.py +110 -0
  14. tktl/cache/manager.py +197 -0
  15. tktl/cli/__init__.py +0 -0
  16. tktl/cli/_deps.py +79 -0
  17. tktl/cli/_error_handler.py +41 -0
  18. tktl/cli/_json_arg.py +32 -0
  19. tktl/cli/auth.py +112 -0
  20. tktl/cli/cache_cmd.py +48 -0
  21. tktl/cli/comments.py +58 -0
  22. tktl/cli/config_cmd.py +42 -0
  23. tktl/cli/connections.py +122 -0
  24. tktl/cli/datasets.py +309 -0
  25. tktl/cli/flows.py +490 -0
  26. tktl/cli/folders.py +37 -0
  27. tktl/cli/groups.py +173 -0
  28. tktl/cli/init_cmd.py +130 -0
  29. tktl/cli/main.py +85 -0
  30. tktl/cli/nodes.py +245 -0
  31. tktl/cli/output.py +155 -0
  32. tktl/cli/render_cmd.py +68 -0
  33. tktl/cli/run.py +45 -0
  34. tktl/cli/update.py +89 -0
  35. tktl/cli/versions.py +223 -0
  36. tktl/config.py +113 -0
  37. tktl/merge.py +27 -0
  38. tktl/models/__init__.py +24 -0
  39. tktl/models/batch.py +59 -0
  40. tktl/models/connection.py +43 -0
  41. tktl/models/dataset.py +141 -0
  42. tktl/models/diff.py +136 -0
  43. tktl/models/errors.py +43 -0
  44. tktl/models/flow.py +25 -0
  45. tktl/models/folder.py +20 -0
  46. tktl/models/group.py +22 -0
  47. tktl/models/node.py +24 -0
  48. tktl/models/version.py +28 -0
  49. tktl/ops/__init__.py +64 -0
  50. tktl/ops/batch.py +611 -0
  51. tktl/ops/cache_ops.py +121 -0
  52. tktl/ops/comments.py +119 -0
  53. tktl/ops/connections.py +88 -0
  54. tktl/ops/datasets.py +546 -0
  55. tktl/ops/diff.py +668 -0
  56. tktl/ops/explain.py +162 -0
  57. tktl/ops/flows.py +234 -0
  58. tktl/ops/folders.py +45 -0
  59. tktl/ops/graph.py +489 -0
  60. tktl/ops/groups.py +265 -0
  61. tktl/ops/manual_review.py +88 -0
  62. tktl/ops/nodes.py +290 -0
  63. tktl/ops/render.py +903 -0
  64. tktl/ops/rule_parser.py +207 -0
  65. tktl/ops/templates.py +399 -0
  66. tktl/ops/versions.py +575 -0
  67. tktl/resolver.py +121 -0
  68. tktl/telemetry.py +199 -0
  69. tktl_cli-0.1.0a1.dist-info/METADATA +11 -0
  70. tktl_cli-0.1.0a1.dist-info/RECORD +72 -0
  71. tktl_cli-0.1.0a1.dist-info/WHEEL +4 -0
  72. 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
@@ -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}")