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,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.")
|