kctl-api 0.2.0__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 (66) hide show
  1. kctl_api/__init__.py +3 -0
  2. kctl_api/__main__.py +5 -0
  3. kctl_api/cli.py +238 -0
  4. kctl_api/commands/__init__.py +1 -0
  5. kctl_api/commands/ai.py +250 -0
  6. kctl_api/commands/aliases.py +84 -0
  7. kctl_api/commands/apps.py +172 -0
  8. kctl_api/commands/auth.py +313 -0
  9. kctl_api/commands/automation.py +242 -0
  10. kctl_api/commands/build_cmd.py +87 -0
  11. kctl_api/commands/clean.py +182 -0
  12. kctl_api/commands/config_cmd.py +443 -0
  13. kctl_api/commands/dashboard.py +139 -0
  14. kctl_api/commands/db.py +599 -0
  15. kctl_api/commands/deploy.py +84 -0
  16. kctl_api/commands/deps.py +289 -0
  17. kctl_api/commands/dev.py +136 -0
  18. kctl_api/commands/docker_cmd.py +252 -0
  19. kctl_api/commands/doctor_cmd.py +286 -0
  20. kctl_api/commands/env.py +289 -0
  21. kctl_api/commands/files.py +250 -0
  22. kctl_api/commands/fmt_cmd.py +58 -0
  23. kctl_api/commands/health.py +479 -0
  24. kctl_api/commands/jobs.py +169 -0
  25. kctl_api/commands/lint_cmd.py +81 -0
  26. kctl_api/commands/logs.py +258 -0
  27. kctl_api/commands/marketplace.py +316 -0
  28. kctl_api/commands/monitor_cmd.py +243 -0
  29. kctl_api/commands/notifications.py +132 -0
  30. kctl_api/commands/odoo_proxy.py +182 -0
  31. kctl_api/commands/openapi.py +299 -0
  32. kctl_api/commands/perf.py +307 -0
  33. kctl_api/commands/rate_limit.py +223 -0
  34. kctl_api/commands/realtime.py +100 -0
  35. kctl_api/commands/redis_cmd.py +609 -0
  36. kctl_api/commands/routes_cmd.py +277 -0
  37. kctl_api/commands/saas.py +145 -0
  38. kctl_api/commands/scaffold.py +362 -0
  39. kctl_api/commands/security_cmd.py +350 -0
  40. kctl_api/commands/services.py +191 -0
  41. kctl_api/commands/shell.py +197 -0
  42. kctl_api/commands/skill_cmd.py +58 -0
  43. kctl_api/commands/streams.py +309 -0
  44. kctl_api/commands/stripe_cmd.py +105 -0
  45. kctl_api/commands/tenant_ai.py +169 -0
  46. kctl_api/commands/test_cmd.py +95 -0
  47. kctl_api/commands/users.py +302 -0
  48. kctl_api/commands/webhooks.py +56 -0
  49. kctl_api/commands/workflows.py +127 -0
  50. kctl_api/commands/ws.py +323 -0
  51. kctl_api/core/__init__.py +1 -0
  52. kctl_api/core/async_client.py +120 -0
  53. kctl_api/core/callbacks.py +88 -0
  54. kctl_api/core/client.py +190 -0
  55. kctl_api/core/config.py +260 -0
  56. kctl_api/core/db.py +65 -0
  57. kctl_api/core/exceptions.py +43 -0
  58. kctl_api/core/output.py +5 -0
  59. kctl_api/core/plugins.py +26 -0
  60. kctl_api/core/redis.py +35 -0
  61. kctl_api/core/resolve.py +47 -0
  62. kctl_api/core/utils.py +109 -0
  63. kctl_api-0.2.0.dist-info/METADATA +34 -0
  64. kctl_api-0.2.0.dist-info/RECORD +66 -0
  65. kctl_api-0.2.0.dist-info/WHEEL +4 -0
  66. kctl_api-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,242 @@
1
+ """Automation template commands for kctl-api.
2
+
3
+ CRUD for automation templates and manual trigger execution.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from kctl_api.core.callbacks import AppContext
13
+ from kctl_api.core.exceptions import APIError, AuthenticationError
14
+ from kctl_api.core.exceptions import ConnectionError as KctlConnectionError
15
+
16
+ app = typer.Typer(
17
+ name="automation", help="Automation templates — list, create, update, delete, run.", no_args_is_help=True
18
+ )
19
+
20
+ _BASE = "/api/v1/automation/templates"
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # list
25
+ # ---------------------------------------------------------------------------
26
+ @app.command(name="list")
27
+ def list_templates(
28
+ ctx: typer.Context,
29
+ page: Annotated[int, typer.Option("--page", "-p", help="Page number.")] = 1,
30
+ per_page: Annotated[int, typer.Option("--per-page", "-n", help="Items per page.")] = 20,
31
+ ) -> None:
32
+ """List automation templates."""
33
+ actx: AppContext = ctx.obj
34
+ out = actx.output
35
+
36
+ try:
37
+ data = actx.client.get(_BASE, params={"page": page, "per_page": per_page})
38
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
39
+ out.error(str(e))
40
+ raise typer.Exit(1) from None
41
+
42
+ items = data.get("items", []) if isinstance(data, dict) else []
43
+ total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
44
+
45
+ rows: list[list[str]] = []
46
+ for t in items:
47
+ rows.append(
48
+ [
49
+ str(t.get("id", "")),
50
+ t.get("name", ""),
51
+ t.get("trigger_type", ""),
52
+ "[green]yes[/green]" if t.get("enabled") else "[red]no[/red]",
53
+ str(t.get("created_at", "")),
54
+ ]
55
+ )
56
+
57
+ out.table(
58
+ title=f"Automation Templates (page {page}, {total} total)",
59
+ columns=[
60
+ ("ID", "bold"),
61
+ ("Name", ""),
62
+ ("Trigger", ""),
63
+ ("Enabled", ""),
64
+ ("Created", "dim"),
65
+ ],
66
+ rows=rows,
67
+ data_for_json=items,
68
+ )
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # create
73
+ # ---------------------------------------------------------------------------
74
+ @app.command()
75
+ def create(
76
+ ctx: typer.Context,
77
+ name: Annotated[str, typer.Option("--name", "-n", help="Template name.")],
78
+ trigger_type: Annotated[str, typer.Option("--trigger", "-t", help="Trigger type (e.g. cron, webhook, event).")],
79
+ config_json: Annotated[str | None, typer.Option("--config", "-c", help="JSON config payload.")] = None,
80
+ ) -> None:
81
+ """Create a new automation template via POST."""
82
+ actx: AppContext = ctx.obj
83
+ out = actx.output
84
+
85
+ import json
86
+
87
+ payload: dict = {"name": name, "trigger_type": trigger_type}
88
+ if config_json:
89
+ try:
90
+ payload["config"] = json.loads(config_json)
91
+ except json.JSONDecodeError as e:
92
+ out.error(f"Invalid JSON: {e}")
93
+ raise typer.Exit(1) from None
94
+
95
+ try:
96
+ result = actx.client.post(_BASE, json=payload)
97
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
98
+ out.error(str(e))
99
+ raise typer.Exit(1) from None
100
+
101
+ out.success(f"Template created: {name}")
102
+ if actx.json_mode:
103
+ out.raw_json(result)
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # get
108
+ # ---------------------------------------------------------------------------
109
+ @app.command(name="get")
110
+ def get_template(
111
+ ctx: typer.Context,
112
+ template_id: Annotated[str, typer.Argument(help="Template ID.")],
113
+ ) -> None:
114
+ """Get automation template details."""
115
+ actx: AppContext = ctx.obj
116
+ out = actx.output
117
+
118
+ try:
119
+ data = actx.client.get(f"{_BASE}/{template_id}")
120
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
121
+ out.error(str(e))
122
+ raise typer.Exit(1) from None
123
+
124
+ if not data:
125
+ out.error(f"Template not found: {template_id}")
126
+ raise typer.Exit(1)
127
+
128
+ out.detail(
129
+ title=f"Template: {data.get('name', template_id)}",
130
+ sections=[
131
+ ("Details", [(k, str(v)) for k, v in data.items()]),
132
+ ],
133
+ data_for_json=data,
134
+ )
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # update
139
+ # ---------------------------------------------------------------------------
140
+ @app.command()
141
+ def update(
142
+ ctx: typer.Context,
143
+ template_id: Annotated[str, typer.Argument(help="Template ID.")],
144
+ name: Annotated[str | None, typer.Option("--name", "-n", help="New name.")] = None,
145
+ enabled: Annotated[bool | None, typer.Option("--enabled/--disabled", help="Enable or disable.")] = None,
146
+ config_json: Annotated[str | None, typer.Option("--config", "-c", help="JSON config payload.")] = None,
147
+ ) -> None:
148
+ """Update an automation template via PATCH."""
149
+ actx: AppContext = ctx.obj
150
+ out = actx.output
151
+
152
+ import json
153
+
154
+ payload: dict = {}
155
+ if name:
156
+ payload["name"] = name
157
+ if enabled is not None:
158
+ payload["enabled"] = enabled
159
+ if config_json:
160
+ try:
161
+ payload["config"] = json.loads(config_json)
162
+ except json.JSONDecodeError as e:
163
+ out.error(f"Invalid JSON: {e}")
164
+ raise typer.Exit(1) from None
165
+
166
+ if not payload:
167
+ out.warn("No fields to update.")
168
+ raise typer.Exit(0)
169
+
170
+ try:
171
+ result = actx.client.patch(f"{_BASE}/{template_id}", json=payload)
172
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
173
+ out.error(str(e))
174
+ raise typer.Exit(1) from None
175
+
176
+ out.success(f"Template {template_id} updated.")
177
+ if actx.json_mode:
178
+ out.raw_json(result)
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # delete
183
+ # ---------------------------------------------------------------------------
184
+ @app.command()
185
+ def delete(
186
+ ctx: typer.Context,
187
+ template_id: Annotated[str, typer.Argument(help="Template ID.")],
188
+ force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation.")] = False,
189
+ ) -> None:
190
+ """Delete an automation template via DELETE."""
191
+ actx: AppContext = ctx.obj
192
+ out = actx.output
193
+
194
+ if not force:
195
+ confirm = typer.confirm(f"Delete template {template_id}?", default=False)
196
+ if not confirm:
197
+ out.info("Cancelled.")
198
+ raise typer.Exit(0)
199
+
200
+ try:
201
+ result = actx.client.delete(f"{_BASE}/{template_id}")
202
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
203
+ out.error(str(e))
204
+ raise typer.Exit(1) from None
205
+
206
+ out.success(f"Template {template_id} deleted.")
207
+ if actx.json_mode:
208
+ out.raw_json(result)
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # run
213
+ # ---------------------------------------------------------------------------
214
+ @app.command()
215
+ def run(
216
+ ctx: typer.Context,
217
+ template_id: Annotated[str, typer.Argument(help="Template ID to trigger.")],
218
+ data_json: Annotated[str | None, typer.Option("--data", "-d", help="JSON input payload.")] = None,
219
+ ) -> None:
220
+ """Manually trigger an automation template via POST."""
221
+ actx: AppContext = ctx.obj
222
+ out = actx.output
223
+
224
+ import json
225
+
226
+ payload: dict = {}
227
+ if data_json:
228
+ try:
229
+ payload = json.loads(data_json)
230
+ except json.JSONDecodeError as e:
231
+ out.error(f"Invalid JSON: {e}")
232
+ raise typer.Exit(1) from None
233
+
234
+ try:
235
+ result = actx.client.post(f"{_BASE}/{template_id}/run", json=payload)
236
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
237
+ out.error(str(e))
238
+ raise typer.Exit(1) from None
239
+
240
+ out.success(f"Template {template_id} triggered.")
241
+ if actx.json_mode:
242
+ out.raw_json(result)
@@ -0,0 +1,87 @@
1
+ """Build commands for kctl-api.
2
+
3
+ Build Docker images for development and production.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from kctl_api.core.callbacks import AppContext
14
+ from kctl_api.core.utils import find_project_root
15
+
16
+ app = typer.Typer(name="build", help="Docker image builds — dev, prod, all.", no_args_is_help=True)
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # dev
21
+ # ---------------------------------------------------------------------------
22
+ @app.command()
23
+ def dev(
24
+ ctx: typer.Context,
25
+ app_name: Annotated[str, typer.Argument(help="App name or 'all'.")] = "all",
26
+ ) -> None:
27
+ """Build development Docker image(s) via scripts/build --dev."""
28
+ actx: AppContext = ctx.obj
29
+ out = actx.output
30
+
31
+ root = find_project_root()
32
+ cmd = [str(root / "scripts" / "build"), app_name, "--dev"]
33
+
34
+ out.info(f"Building dev image: {app_name}")
35
+ result = subprocess.run(cmd, cwd=str(root), capture_output=False)
36
+ if result.returncode != 0:
37
+ out.error("Build failed.")
38
+ raise typer.Exit(result.returncode)
39
+ out.success(f"Dev build complete: {app_name}")
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # prod
44
+ # ---------------------------------------------------------------------------
45
+ @app.command()
46
+ def prod(
47
+ ctx: typer.Context,
48
+ app_name: Annotated[str, typer.Argument(help="App name or 'all'.")] = "all",
49
+ ) -> None:
50
+ """Build production Docker image(s) via scripts/build."""
51
+ actx: AppContext = ctx.obj
52
+ out = actx.output
53
+
54
+ root = find_project_root()
55
+ cmd = [str(root / "scripts" / "build"), app_name]
56
+
57
+ out.info(f"Building prod image: {app_name}")
58
+ result = subprocess.run(cmd, cwd=str(root), capture_output=False)
59
+ if result.returncode != 0:
60
+ out.error("Build failed.")
61
+ raise typer.Exit(result.returncode)
62
+ out.success(f"Prod build complete: {app_name}")
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # all
67
+ # ---------------------------------------------------------------------------
68
+ @app.command(name="all")
69
+ def build_all(
70
+ ctx: typer.Context,
71
+ dev_mode: Annotated[bool, typer.Option("--dev", help="Build in dev mode.")] = False,
72
+ ) -> None:
73
+ """Build all Docker images via scripts/build all."""
74
+ actx: AppContext = ctx.obj
75
+ out = actx.output
76
+
77
+ root = find_project_root()
78
+ cmd = [str(root / "scripts" / "build"), "all"]
79
+ if dev_mode:
80
+ cmd.append("--dev")
81
+
82
+ out.info("Building all images ...")
83
+ result = subprocess.run(cmd, cwd=str(root), capture_output=False)
84
+ if result.returncode != 0:
85
+ out.error("Build failed.")
86
+ raise typer.Exit(result.returncode)
87
+ out.success("All builds complete.")
@@ -0,0 +1,182 @@
1
+ """Artifact cleanup commands for kctl-api.
2
+
3
+ Clean build artifacts, caches, and __pycache__ directories.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import pathlib
9
+ import shutil
10
+ from typing import Annotated
11
+
12
+ import typer
13
+
14
+ from kctl_api.core.callbacks import AppContext
15
+ from kctl_api.core.utils import find_project_root
16
+
17
+ app = typer.Typer(name="clean", help="Cleanup — all, cache, build, pycache.", no_args_is_help=True)
18
+
19
+
20
+ def _rm_dirs(patterns: list[str], root: pathlib.Path, dry_run: bool = False) -> list[pathlib.Path]:
21
+ """Remove directories matching glob patterns under root. Returns removed paths."""
22
+ removed: list[pathlib.Path] = []
23
+ skip_dirs = {".venv", ".git", "node_modules"}
24
+
25
+ for pattern in patterns:
26
+ for target in root.rglob(pattern):
27
+ # Don't clean inside .venv or .git
28
+ if any(part in skip_dirs for part in target.parts):
29
+ continue
30
+ if target.is_dir():
31
+ removed.append(target)
32
+ if not dry_run:
33
+ shutil.rmtree(target, ignore_errors=True)
34
+ elif target.is_file():
35
+ removed.append(target)
36
+ if not dry_run:
37
+ target.unlink(missing_ok=True)
38
+
39
+ return removed
40
+
41
+
42
+ def _rm_files(patterns: list[str], root: pathlib.Path, dry_run: bool = False) -> list[pathlib.Path]:
43
+ """Remove files matching glob patterns under root."""
44
+ removed: list[pathlib.Path] = []
45
+ skip_dirs = {".venv", ".git", "node_modules"}
46
+
47
+ for pattern in patterns:
48
+ for target in root.rglob(pattern):
49
+ if any(part in skip_dirs for part in target.parts):
50
+ continue
51
+ if target.is_file():
52
+ removed.append(target)
53
+ if not dry_run:
54
+ target.unlink(missing_ok=True)
55
+
56
+ return removed
57
+
58
+
59
+ def _display_removed(out: object, removed: list[pathlib.Path], root: pathlib.Path, dry_run: bool) -> None:
60
+ """Print removed paths."""
61
+ prefix = "[dim](dry-run)[/dim] " if dry_run else ""
62
+ for path in removed[:30]:
63
+ try:
64
+ rel = path.relative_to(root)
65
+ except ValueError:
66
+ rel = path
67
+ out.text(f" {prefix}[red]rm[/red] {rel}") # type: ignore[attr-defined]
68
+ if len(removed) > 30:
69
+ out.text(f" ... and {len(removed) - 30} more") # type: ignore[attr-defined]
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # all
74
+ # ---------------------------------------------------------------------------
75
+ @app.command(name="all")
76
+ def clean_all(
77
+ ctx: typer.Context,
78
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be removed without deleting.")] = False,
79
+ force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation.")] = False,
80
+ ) -> None:
81
+ """Remove all build artifacts, caches, and __pycache__ directories."""
82
+ actx: AppContext = ctx.obj
83
+ out = actx.output
84
+
85
+ if not force and not dry_run:
86
+ confirm = typer.confirm("Remove all build artifacts and caches?", default=False)
87
+ if not confirm:
88
+ out.info("Cancelled.")
89
+ raise typer.Exit(0)
90
+
91
+ root = find_project_root()
92
+ total_removed: list[pathlib.Path] = []
93
+
94
+ cache_dirs = [".ruff_cache", ".mypy_cache", ".pytest_cache", ".hypothesis", ".tox"]
95
+ build_dirs = ["dist", "build", "*.egg-info"]
96
+ pycache_dirs = ["__pycache__"]
97
+ coverage_files = [".coverage", "coverage.xml", "htmlcov"]
98
+
99
+ total_removed += _rm_dirs(cache_dirs, root, dry_run)
100
+ total_removed += _rm_dirs(build_dirs, root, dry_run)
101
+ total_removed += _rm_dirs(pycache_dirs, root, dry_run)
102
+ total_removed += _rm_dirs(coverage_files, root, dry_run)
103
+ total_removed += _rm_files(["*.pyc", "*.pyo"], root, dry_run)
104
+
105
+ if total_removed:
106
+ _display_removed(out, total_removed, root, dry_run)
107
+ action = "Would remove" if dry_run else "Removed"
108
+ out.success(f"{action} {len(total_removed)} items.")
109
+ else:
110
+ out.info("Nothing to clean.")
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # cache
115
+ # ---------------------------------------------------------------------------
116
+ @app.command()
117
+ def cache(
118
+ ctx: typer.Context,
119
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be removed.")] = False,
120
+ ) -> None:
121
+ """Remove .ruff_cache, .mypy_cache, .pytest_cache directories."""
122
+ actx: AppContext = ctx.obj
123
+ out = actx.output
124
+
125
+ root = find_project_root()
126
+ patterns = [".ruff_cache", ".mypy_cache", ".pytest_cache", ".hypothesis"]
127
+ removed = _rm_dirs(patterns, root, dry_run)
128
+
129
+ if removed:
130
+ _display_removed(out, removed, root, dry_run)
131
+ action = "Would remove" if dry_run else "Removed"
132
+ out.success(f"{action} {len(removed)} cache directories.")
133
+ else:
134
+ out.info("No cache directories found.")
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # build
139
+ # ---------------------------------------------------------------------------
140
+ @app.command()
141
+ def build(
142
+ ctx: typer.Context,
143
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be removed.")] = False,
144
+ ) -> None:
145
+ """Remove dist/ and *.egg-info build artifacts."""
146
+ actx: AppContext = ctx.obj
147
+ out = actx.output
148
+
149
+ root = find_project_root()
150
+ patterns = ["dist", "build", "*.egg-info", "*.dist-info"]
151
+ removed = _rm_dirs(patterns, root, dry_run)
152
+
153
+ if removed:
154
+ _display_removed(out, removed, root, dry_run)
155
+ action = "Would remove" if dry_run else "Removed"
156
+ out.success(f"{action} {len(removed)} build artifact directories.")
157
+ else:
158
+ out.info("No build artifacts found.")
159
+
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # pycache
163
+ # ---------------------------------------------------------------------------
164
+ @app.command()
165
+ def pycache(
166
+ ctx: typer.Context,
167
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be removed.")] = False,
168
+ ) -> None:
169
+ """Remove all __pycache__ directories and .pyc/.pyo files."""
170
+ actx: AppContext = ctx.obj
171
+ out = actx.output
172
+
173
+ root = find_project_root()
174
+ removed = _rm_dirs(["__pycache__"], root, dry_run)
175
+ removed += _rm_files(["*.pyc", "*.pyo"], root, dry_run)
176
+
177
+ if removed:
178
+ _display_removed(out, removed, root, dry_run)
179
+ action = "Would remove" if dry_run else "Removed"
180
+ out.success(f"{action} {len(removed)} __pycache__ items.")
181
+ else:
182
+ out.info("No __pycache__ directories found.")