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,250 @@
1
+ """File management commands for kctl-api.
2
+
3
+ Upload, download, list, and inspect files.
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
+ from kctl_api.core.utils import human_size
16
+
17
+ app = typer.Typer(name="files", help="File management — upload, download, list, delete.", no_args_is_help=True)
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # list
22
+ # ---------------------------------------------------------------------------
23
+ @app.command(name="list")
24
+ def list_files(
25
+ ctx: typer.Context,
26
+ page: Annotated[int, typer.Option("--page", "-p", help="Page number.")] = 1,
27
+ per_page: Annotated[int, typer.Option("--per-page", "-n", help="Items per page.")] = 20,
28
+ ) -> None:
29
+ """List uploaded files with pagination.
30
+
31
+ Note: Requires a file listing endpoint on api-main. If not available,
32
+ falls back to a warning.
33
+ """
34
+ actx: AppContext = ctx.obj
35
+ out = actx.output
36
+
37
+ try:
38
+ data = actx.client.get("/api/v1/files", params={"page": page, "per_page": per_page})
39
+ except APIError as e:
40
+ if hasattr(e, "status_code") and e.status_code == 404:
41
+ out.warn("File listing endpoint (GET /api/v1/files) is not available on this server.")
42
+ out.info("Use 'kctl-api files info <id>' to look up individual files.")
43
+ return
44
+ out.error(str(e))
45
+ raise typer.Exit(1) from None
46
+ except (AuthenticationError, KctlConnectionError) as e:
47
+ out.error(str(e))
48
+ raise typer.Exit(1) from None
49
+
50
+ items = data.get("items", []) if isinstance(data, dict) else []
51
+ total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
52
+
53
+ rows: list[list[str]] = []
54
+ for f in items:
55
+ size = f.get("size", 0)
56
+ rows.append(
57
+ [
58
+ str(f.get("id", "")),
59
+ f.get("filename", f.get("name", "")),
60
+ f.get("content_type", ""),
61
+ human_size(size) if isinstance(size, (int, float)) else str(size),
62
+ str(f.get("created_at", "")),
63
+ ]
64
+ )
65
+
66
+ out.table(
67
+ title=f"Files (page {page}, {total} total)",
68
+ columns=[
69
+ ("ID", "bold"),
70
+ ("Filename", ""),
71
+ ("Type", ""),
72
+ ("Size", ""),
73
+ ("Created", "dim"),
74
+ ],
75
+ rows=rows,
76
+ data_for_json=items,
77
+ )
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # info
82
+ # ---------------------------------------------------------------------------
83
+ @app.command()
84
+ def info(
85
+ ctx: typer.Context,
86
+ file_id: Annotated[str, typer.Argument(help="File ID.")],
87
+ ) -> None:
88
+ """Get file metadata by ID."""
89
+ actx: AppContext = ctx.obj
90
+ out = actx.output
91
+
92
+ try:
93
+ data = actx.client.get(f"/api/v1/files/{file_id}")
94
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
95
+ out.error(str(e))
96
+ raise typer.Exit(1) from None
97
+
98
+ if not data:
99
+ out.error(f"File not found: {file_id}")
100
+ raise typer.Exit(1)
101
+
102
+ size = data.get("size", 0)
103
+
104
+ out.detail(
105
+ title=f"File: {data.get('filename', file_id)}",
106
+ sections=[
107
+ (
108
+ "Metadata",
109
+ [
110
+ ("ID", str(data.get("id", ""))),
111
+ ("Filename", data.get("filename", data.get("name", ""))),
112
+ ("Content-Type", data.get("content_type", "")),
113
+ ("Size", human_size(size) if isinstance(size, (int, float)) else str(size)),
114
+ ("Owner ID", str(data.get("owner_id", ""))),
115
+ ],
116
+ ),
117
+ (
118
+ "Timestamps",
119
+ [
120
+ ("Created", str(data.get("created_at", ""))),
121
+ ("Updated", str(data.get("updated_at", ""))),
122
+ ],
123
+ ),
124
+ ],
125
+ data_for_json=data,
126
+ )
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # upload
131
+ # ---------------------------------------------------------------------------
132
+ @app.command()
133
+ def upload(
134
+ ctx: typer.Context,
135
+ file_path: Annotated[str, typer.Argument(help="Local file path to upload.")],
136
+ ) -> None:
137
+ """Upload a file via POST /api/v1/files/upload."""
138
+ actx: AppContext = ctx.obj
139
+ out = actx.output
140
+
141
+ try:
142
+ result = actx.client.upload("/api/v1/files/upload", file_path)
143
+ except FileNotFoundError:
144
+ out.error(f"File not found: {file_path}")
145
+ raise typer.Exit(1) from None
146
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
147
+ out.error(str(e))
148
+ raise typer.Exit(1) from None
149
+
150
+ out.success(f"File uploaded: {result.get('filename', file_path)}")
151
+ if actx.json_mode:
152
+ out.raw_json(result)
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # download
157
+ # ---------------------------------------------------------------------------
158
+ @app.command()
159
+ def download(
160
+ ctx: typer.Context,
161
+ file_id: Annotated[str, typer.Argument(help="File ID to download.")],
162
+ output_path: Annotated[str | None, typer.Option("--output", "-o", help="Output file path.")] = None,
163
+ ) -> None:
164
+ """Download a file by ID via GET /api/v1/files/{id}/download."""
165
+ actx: AppContext = ctx.obj
166
+ out = actx.output
167
+
168
+ try:
169
+ import httpx
170
+
171
+ url = f"{actx.client.base_url}/api/v1/files/{file_id}/download"
172
+ headers = actx.client._auth_headers()
173
+ with httpx.stream("GET", url, headers=headers, follow_redirects=True) as response:
174
+ response.raise_for_status()
175
+ # Determine output filename
176
+ dest = output_path or f"file_{file_id}"
177
+ content_disp = response.headers.get("content-disposition", "")
178
+ if not output_path and "filename=" in content_disp:
179
+ dest = content_disp.split("filename=")[-1].strip('"')
180
+ with open(dest, "wb") as f:
181
+ for chunk in response.iter_bytes():
182
+ f.write(chunk)
183
+ out.success(f"Downloaded to {dest}")
184
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
185
+ out.error(str(e))
186
+ raise typer.Exit(1) from None
187
+ except Exception as e:
188
+ out.error(f"Download failed: {e}")
189
+ raise typer.Exit(1) from None
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # delete
194
+ # ---------------------------------------------------------------------------
195
+ @app.command()
196
+ def delete(
197
+ ctx: typer.Context,
198
+ file_id: Annotated[str, typer.Argument(help="File ID to delete.")],
199
+ force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation.")] = False,
200
+ ) -> None:
201
+ """Delete a file by ID via DELETE /api/v1/files/{id}."""
202
+ actx: AppContext = ctx.obj
203
+ out = actx.output
204
+
205
+ if not force:
206
+ confirm = typer.confirm(f"Delete file {file_id}?", default=False)
207
+ if not confirm:
208
+ out.info("Cancelled.")
209
+ raise typer.Exit(0)
210
+
211
+ try:
212
+ result = actx.client.delete(f"/api/v1/files/{file_id}")
213
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
214
+ out.error(str(e))
215
+ raise typer.Exit(1) from None
216
+
217
+ out.success(f"File {file_id} deleted.")
218
+ if actx.json_mode:
219
+ out.raw_json(result)
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # stats
224
+ # ---------------------------------------------------------------------------
225
+ @app.command()
226
+ def stats(ctx: typer.Context) -> None:
227
+ """Get file storage statistics via GET /api/v1/files/stats."""
228
+ actx: AppContext = ctx.obj
229
+ out = actx.output
230
+
231
+ try:
232
+ data = actx.client.get("/api/v1/files/stats")
233
+ except (AuthenticationError, KctlConnectionError, APIError) as e:
234
+ out.error(str(e))
235
+ raise typer.Exit(1) from None
236
+
237
+ if not data:
238
+ out.info("No stats available.")
239
+ return
240
+
241
+ out.detail(
242
+ title="File Storage Stats",
243
+ sections=[
244
+ (
245
+ "Summary",
246
+ [(k, str(v)) for k, v in data.items()],
247
+ ),
248
+ ],
249
+ data_for_json=data,
250
+ )
@@ -0,0 +1,58 @@
1
+ """Formatting commands for kctl-api.
2
+
3
+ Run ruff format via project scripts.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+
10
+ import typer
11
+
12
+ from kctl_api.core.callbacks import AppContext
13
+ from kctl_api.core.utils import find_project_root
14
+
15
+ app = typer.Typer(name="fmt", help="Code formatting — run, check.", no_args_is_help=True)
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # run
20
+ # ---------------------------------------------------------------------------
21
+ @app.command()
22
+ def run(ctx: typer.Context) -> None:
23
+ """Format code via scripts/fmt."""
24
+ actx: AppContext = ctx.obj
25
+ out = actx.output
26
+
27
+ root = find_project_root()
28
+ cmd = [str(root / "scripts" / "fmt")]
29
+
30
+ out.info("Formatting code ...")
31
+ result = subprocess.run(cmd, cwd=str(root), capture_output=False)
32
+ if result.returncode != 0:
33
+ out.error("Formatting failed.")
34
+ raise typer.Exit(result.returncode)
35
+ out.success("Code formatted.")
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # check
40
+ # ---------------------------------------------------------------------------
41
+ @app.command()
42
+ def check(ctx: typer.Context) -> None:
43
+ """Check formatting without modifying files (ruff format --check)."""
44
+ actx: AppContext = ctx.obj
45
+ out = actx.output
46
+
47
+ root = find_project_root()
48
+
49
+ out.info("Checking code formatting ...")
50
+ result = subprocess.run(
51
+ ["ruff", "format", "--check", "."],
52
+ cwd=str(root),
53
+ capture_output=False,
54
+ )
55
+ if result.returncode != 0:
56
+ out.error("Code is not properly formatted. Run: kctl-api fmt run")
57
+ raise typer.Exit(result.returncode)
58
+ out.success("Code is properly formatted.")