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.
- kctl_api/__init__.py +3 -0
- kctl_api/__main__.py +5 -0
- kctl_api/cli.py +238 -0
- kctl_api/commands/__init__.py +1 -0
- kctl_api/commands/ai.py +250 -0
- kctl_api/commands/aliases.py +84 -0
- kctl_api/commands/apps.py +172 -0
- kctl_api/commands/auth.py +313 -0
- kctl_api/commands/automation.py +242 -0
- kctl_api/commands/build_cmd.py +87 -0
- kctl_api/commands/clean.py +182 -0
- kctl_api/commands/config_cmd.py +443 -0
- kctl_api/commands/dashboard.py +139 -0
- kctl_api/commands/db.py +599 -0
- kctl_api/commands/deploy.py +84 -0
- kctl_api/commands/deps.py +289 -0
- kctl_api/commands/dev.py +136 -0
- kctl_api/commands/docker_cmd.py +252 -0
- kctl_api/commands/doctor_cmd.py +286 -0
- kctl_api/commands/env.py +289 -0
- kctl_api/commands/files.py +250 -0
- kctl_api/commands/fmt_cmd.py +58 -0
- kctl_api/commands/health.py +479 -0
- kctl_api/commands/jobs.py +169 -0
- kctl_api/commands/lint_cmd.py +81 -0
- kctl_api/commands/logs.py +258 -0
- kctl_api/commands/marketplace.py +316 -0
- kctl_api/commands/monitor_cmd.py +243 -0
- kctl_api/commands/notifications.py +132 -0
- kctl_api/commands/odoo_proxy.py +182 -0
- kctl_api/commands/openapi.py +299 -0
- kctl_api/commands/perf.py +307 -0
- kctl_api/commands/rate_limit.py +223 -0
- kctl_api/commands/realtime.py +100 -0
- kctl_api/commands/redis_cmd.py +609 -0
- kctl_api/commands/routes_cmd.py +277 -0
- kctl_api/commands/saas.py +145 -0
- kctl_api/commands/scaffold.py +362 -0
- kctl_api/commands/security_cmd.py +350 -0
- kctl_api/commands/services.py +191 -0
- kctl_api/commands/shell.py +197 -0
- kctl_api/commands/skill_cmd.py +58 -0
- kctl_api/commands/streams.py +309 -0
- kctl_api/commands/stripe_cmd.py +105 -0
- kctl_api/commands/tenant_ai.py +169 -0
- kctl_api/commands/test_cmd.py +95 -0
- kctl_api/commands/users.py +302 -0
- kctl_api/commands/webhooks.py +56 -0
- kctl_api/commands/workflows.py +127 -0
- kctl_api/commands/ws.py +323 -0
- kctl_api/core/__init__.py +1 -0
- kctl_api/core/async_client.py +120 -0
- kctl_api/core/callbacks.py +88 -0
- kctl_api/core/client.py +190 -0
- kctl_api/core/config.py +260 -0
- kctl_api/core/db.py +65 -0
- kctl_api/core/exceptions.py +43 -0
- kctl_api/core/output.py +5 -0
- kctl_api/core/plugins.py +26 -0
- kctl_api/core/redis.py +35 -0
- kctl_api/core/resolve.py +47 -0
- kctl_api/core/utils.py +109 -0
- kctl_api-0.2.0.dist-info/METADATA +34 -0
- kctl_api-0.2.0.dist-info/RECORD +66 -0
- kctl_api-0.2.0.dist-info/WHEEL +4 -0
- 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.")
|