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,362 @@
|
|
|
1
|
+
"""Scaffolding commands for kctl-api.
|
|
2
|
+
|
|
3
|
+
Generate new apps, endpoints, models, and webhooks.
|
|
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="scaffold", help="Scaffold — new app, endpoint, model, webhook.", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# app
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
@app.command(name="app")
|
|
23
|
+
def scaffold_app(
|
|
24
|
+
ctx: typer.Context,
|
|
25
|
+
name: Annotated[str, typer.Argument(help="New app name.")],
|
|
26
|
+
app_type: Annotated[
|
|
27
|
+
str, typer.Option("--type", "-t", help="App type: webhook, events, integration, agent, etl, scheduler, stream.")
|
|
28
|
+
] = "webhook",
|
|
29
|
+
port: Annotated[int | None, typer.Option("--port", "-p", help="Port number.")] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Scaffold a new deployable app via scripts/new-app."""
|
|
32
|
+
actx: AppContext = ctx.obj
|
|
33
|
+
out = actx.output
|
|
34
|
+
|
|
35
|
+
root = find_project_root()
|
|
36
|
+
cmd = [str(root / "scripts" / "new-app"), name, "--type", app_type]
|
|
37
|
+
if port:
|
|
38
|
+
cmd.extend(["--port", str(port)])
|
|
39
|
+
|
|
40
|
+
out.info(f"Scaffolding app: {name} (type={app_type})")
|
|
41
|
+
result = subprocess.run(cmd, cwd=str(root), capture_output=False)
|
|
42
|
+
if result.returncode != 0:
|
|
43
|
+
out.error("Scaffolding failed.")
|
|
44
|
+
raise typer.Exit(result.returncode)
|
|
45
|
+
out.success(f"App '{name}' scaffolded successfully.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# endpoint
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
@app.command()
|
|
52
|
+
def endpoint(
|
|
53
|
+
ctx: typer.Context,
|
|
54
|
+
app_name: Annotated[str, typer.Argument(help="Target app name.")],
|
|
55
|
+
name: Annotated[str, typer.Argument(help="Endpoint/resource name.")],
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Scaffold a new API endpoint in an existing app."""
|
|
58
|
+
actx: AppContext = ctx.obj
|
|
59
|
+
out = actx.output
|
|
60
|
+
|
|
61
|
+
root = find_project_root()
|
|
62
|
+
script = root / "scripts" / "new-endpoint"
|
|
63
|
+
|
|
64
|
+
if script.exists():
|
|
65
|
+
result = subprocess.run([str(script), app_name, name], cwd=str(root), capture_output=False)
|
|
66
|
+
if result.returncode != 0:
|
|
67
|
+
out.error("Scaffolding failed.")
|
|
68
|
+
raise typer.Exit(result.returncode)
|
|
69
|
+
out.success(f"Endpoint '{name}' scaffolded in {app_name}.")
|
|
70
|
+
else:
|
|
71
|
+
out.info(f"Not yet implemented. Will scaffold endpoint '{name}' in {app_name}.")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# model
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
@app.command()
|
|
78
|
+
def model(
|
|
79
|
+
ctx: typer.Context,
|
|
80
|
+
name: Annotated[str, typer.Argument(help="Model name (PascalCase).")],
|
|
81
|
+
table: Annotated[str | None, typer.Option("--table", help="Table name override.")] = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Scaffold a new SQLAlchemy model with Alembic migration."""
|
|
84
|
+
actx: AppContext = ctx.obj
|
|
85
|
+
out = actx.output
|
|
86
|
+
|
|
87
|
+
root = find_project_root()
|
|
88
|
+
script = root / "scripts" / "new-model"
|
|
89
|
+
|
|
90
|
+
if script.exists():
|
|
91
|
+
cmd = [str(script), name]
|
|
92
|
+
if table:
|
|
93
|
+
cmd.extend(["--table", table])
|
|
94
|
+
result = subprocess.run(cmd, cwd=str(root), capture_output=False)
|
|
95
|
+
if result.returncode != 0:
|
|
96
|
+
out.error("Scaffolding failed.")
|
|
97
|
+
raise typer.Exit(result.returncode)
|
|
98
|
+
out.success(f"Model '{name}' scaffolded.")
|
|
99
|
+
else:
|
|
100
|
+
out.info(f"Not yet implemented. Will scaffold model '{name}'.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# webhook
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
@app.command()
|
|
107
|
+
def webhook(
|
|
108
|
+
ctx: typer.Context,
|
|
109
|
+
name: Annotated[str, typer.Argument(help="Webhook app name.")],
|
|
110
|
+
port: Annotated[int | None, typer.Option("--port", "-p", help="Port number.")] = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Scaffold a new standalone webhook receiver app."""
|
|
113
|
+
actx: AppContext = ctx.obj
|
|
114
|
+
out = actx.output
|
|
115
|
+
|
|
116
|
+
root = find_project_root()
|
|
117
|
+
script = root / "scripts" / "new-webhook"
|
|
118
|
+
|
|
119
|
+
if script.exists():
|
|
120
|
+
cmd = [str(script), name]
|
|
121
|
+
if port:
|
|
122
|
+
cmd.extend(["--port", str(port)])
|
|
123
|
+
result = subprocess.run(cmd, cwd=str(root), capture_output=False)
|
|
124
|
+
if result.returncode != 0:
|
|
125
|
+
out.error("Scaffolding failed.")
|
|
126
|
+
raise typer.Exit(result.returncode)
|
|
127
|
+
out.success(f"Webhook '{name}' scaffolded.")
|
|
128
|
+
else:
|
|
129
|
+
# Fall back to new-app with webhook type
|
|
130
|
+
cmd = [str(root / "scripts" / "new-app"), name, "--type", "webhook"]
|
|
131
|
+
if port:
|
|
132
|
+
cmd.extend(["--port", str(port)])
|
|
133
|
+
result = subprocess.run(cmd, cwd=str(root), capture_output=False)
|
|
134
|
+
if result.returncode != 0:
|
|
135
|
+
out.error("Scaffolding failed.")
|
|
136
|
+
raise typer.Exit(result.returncode)
|
|
137
|
+
out.success(f"Webhook '{name}' scaffolded.")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# service
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
@app.command()
|
|
144
|
+
def service(
|
|
145
|
+
ctx: typer.Context,
|
|
146
|
+
name: Annotated[str, typer.Argument(help="Service class name (PascalCase, e.g. UserService).")],
|
|
147
|
+
app_name: Annotated[str | None, typer.Option("--app", "-a", help="Target app name.")] = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Scaffold a service class boilerplate file."""
|
|
150
|
+
actx: AppContext = ctx.obj
|
|
151
|
+
out = actx.output
|
|
152
|
+
|
|
153
|
+
root = find_project_root()
|
|
154
|
+
snake_name = "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")
|
|
155
|
+
snake_name = snake_name.replace("__", "_")
|
|
156
|
+
|
|
157
|
+
# Determine target directory
|
|
158
|
+
if app_name:
|
|
159
|
+
target_dir = root / "apps" / app_name / "src" / f"kodemeio_{app_name.replace('-', '_')}" / "services"
|
|
160
|
+
else:
|
|
161
|
+
target_dir = root / "services"
|
|
162
|
+
|
|
163
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
target_file = target_dir / f"{snake_name}.py"
|
|
165
|
+
|
|
166
|
+
if target_file.exists():
|
|
167
|
+
out.error(f"File already exists: {target_file}")
|
|
168
|
+
raise typer.Exit(1)
|
|
169
|
+
|
|
170
|
+
content = f'''"""Service layer for {name}."""
|
|
171
|
+
|
|
172
|
+
from __future__ import annotations
|
|
173
|
+
|
|
174
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
175
|
+
|
|
176
|
+
from kodemeio_core.utils.query import paginate
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class {name}:
|
|
180
|
+
"""Business logic for {name.replace("Service", "")} operations."""
|
|
181
|
+
|
|
182
|
+
def __init__(self, db: AsyncSession) -> None:
|
|
183
|
+
self.db = db
|
|
184
|
+
|
|
185
|
+
async def list(self, params: object) -> object:
|
|
186
|
+
"""List {name.replace("Service", "").lower()} resources with pagination."""
|
|
187
|
+
raise NotImplementedError
|
|
188
|
+
|
|
189
|
+
async def get(self, resource_id: str) -> object | None:
|
|
190
|
+
"""Get a single {name.replace("Service", "").lower()} by ID."""
|
|
191
|
+
raise NotImplementedError
|
|
192
|
+
|
|
193
|
+
async def create(self, data: object) -> object:
|
|
194
|
+
"""Create a new {name.replace("Service", "").lower()} resource."""
|
|
195
|
+
raise NotImplementedError
|
|
196
|
+
|
|
197
|
+
async def update(self, resource_id: str, data: object) -> object:
|
|
198
|
+
"""Update an existing {name.replace("Service", "").lower()} resource."""
|
|
199
|
+
raise NotImplementedError
|
|
200
|
+
|
|
201
|
+
async def delete(self, resource_id: str) -> bool:
|
|
202
|
+
"""Soft-delete a {name.replace("Service", "").lower()} resource."""
|
|
203
|
+
raise NotImplementedError
|
|
204
|
+
'''
|
|
205
|
+
|
|
206
|
+
target_file.write_text(content)
|
|
207
|
+
out.success(f"Service '{name}' scaffolded at {target_file.relative_to(root)}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# middleware
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
@app.command(name="middleware")
|
|
214
|
+
def scaffold_middleware(
|
|
215
|
+
ctx: typer.Context,
|
|
216
|
+
name: Annotated[str, typer.Argument(help="Middleware name (e.g. RequestLogging).")],
|
|
217
|
+
app_name: Annotated[str | None, typer.Option("--app", "-a", help="Target app name.")] = None,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Scaffold a Starlette/FastAPI middleware boilerplate."""
|
|
220
|
+
actx: AppContext = ctx.obj
|
|
221
|
+
out = actx.output
|
|
222
|
+
|
|
223
|
+
root = find_project_root()
|
|
224
|
+
snake_name = "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")
|
|
225
|
+
snake_name = snake_name.replace("__", "_")
|
|
226
|
+
|
|
227
|
+
if app_name:
|
|
228
|
+
target_dir = root / "apps" / app_name / "src" / f"kodemeio_{app_name.replace('-', '_')}" / "middleware"
|
|
229
|
+
else:
|
|
230
|
+
target_dir = root / "packages" / "core" / "src" / "kodemeio_core" / "middleware"
|
|
231
|
+
|
|
232
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
target_file = target_dir / f"{snake_name}.py"
|
|
234
|
+
|
|
235
|
+
if target_file.exists():
|
|
236
|
+
out.error(f"File already exists: {target_file}")
|
|
237
|
+
raise typer.Exit(1)
|
|
238
|
+
|
|
239
|
+
content = f'''"""{name} middleware."""
|
|
240
|
+
|
|
241
|
+
from __future__ import annotations
|
|
242
|
+
|
|
243
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
244
|
+
from starlette.requests import Request
|
|
245
|
+
from starlette.responses import Response
|
|
246
|
+
|
|
247
|
+
from kodemeio_core.logging import get_logger
|
|
248
|
+
|
|
249
|
+
logger = get_logger(__name__)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class {name}Middleware(BaseHTTPMiddleware):
|
|
253
|
+
"""{name} middleware — add description here."""
|
|
254
|
+
|
|
255
|
+
def __init__(self, app: object, **kwargs: object) -> None:
|
|
256
|
+
super().__init__(app) # type: ignore[arg-type]
|
|
257
|
+
|
|
258
|
+
async def dispatch(self, request: Request, call_next: object) -> Response:
|
|
259
|
+
# Pre-processing
|
|
260
|
+
logger.debug("request", path=request.url.path, method=request.method)
|
|
261
|
+
|
|
262
|
+
response = await call_next(request) # type: ignore[misc]
|
|
263
|
+
|
|
264
|
+
# Post-processing
|
|
265
|
+
return response
|
|
266
|
+
'''
|
|
267
|
+
|
|
268
|
+
target_file.write_text(content)
|
|
269
|
+
out.success(f"Middleware '{name}Middleware' scaffolded at {target_file.relative_to(root)}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# background-job
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
@app.command(name="background-job")
|
|
276
|
+
def background_job(
|
|
277
|
+
ctx: typer.Context,
|
|
278
|
+
name: Annotated[str, typer.Argument(help="Job function name (snake_case, e.g. send_email).")],
|
|
279
|
+
app_name: Annotated[str | None, typer.Option("--app", "-a", help="Target app name.")] = None,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Scaffold an ARQ background job boilerplate."""
|
|
282
|
+
actx: AppContext = ctx.obj
|
|
283
|
+
out = actx.output
|
|
284
|
+
|
|
285
|
+
root = find_project_root()
|
|
286
|
+
|
|
287
|
+
if app_name:
|
|
288
|
+
target_dir = root / "apps" / app_name / "src" / f"kodemeio_{app_name.replace('-', '_')}" / "jobs"
|
|
289
|
+
else:
|
|
290
|
+
target_dir = root / "apps" / "api-main" / "src" / "kodemeio_api_main" / "jobs"
|
|
291
|
+
|
|
292
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
293
|
+
target_file = target_dir / f"{name}.py"
|
|
294
|
+
|
|
295
|
+
if target_file.exists():
|
|
296
|
+
out.error(f"File already exists: {target_file}")
|
|
297
|
+
raise typer.Exit(1)
|
|
298
|
+
|
|
299
|
+
content = f'''"""ARQ background job: {name}."""
|
|
300
|
+
|
|
301
|
+
from __future__ import annotations
|
|
302
|
+
|
|
303
|
+
from arq import ArqRedis
|
|
304
|
+
from kodemeio_core.logging import get_logger
|
|
305
|
+
|
|
306
|
+
logger = get_logger(__name__)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def {name}(ctx: dict, **kwargs: object) -> dict:
|
|
310
|
+
"""Background job: {name.replace("_", " ")}.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
ctx: ARQ job context (contains redis, job_id, etc.)
|
|
314
|
+
**kwargs: Job-specific parameters
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Result dict with status and any relevant data.
|
|
318
|
+
"""
|
|
319
|
+
job_id = ctx.get("job_id", "unknown")
|
|
320
|
+
logger.info("job_started", job="{name}", job_id=job_id, params=kwargs)
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
# TODO: implement job logic here
|
|
324
|
+
result = {{"status": "completed", "job_id": job_id}}
|
|
325
|
+
logger.info("job_completed", job="{name}", job_id=job_id)
|
|
326
|
+
return result
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error("job_failed", job="{name}", job_id=job_id, error=str(e))
|
|
329
|
+
raise
|
|
330
|
+
'''
|
|
331
|
+
|
|
332
|
+
target_file.write_text(content)
|
|
333
|
+
out.success(f"Background job '{name}' scaffolded at {target_file.relative_to(root)}")
|
|
334
|
+
out.info("Enqueue via: POST /api/v1/jobs/enqueue (admin only)")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# migration
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
@app.command()
|
|
341
|
+
def migration(
|
|
342
|
+
ctx: typer.Context,
|
|
343
|
+
message: Annotated[str, typer.Argument(help="Migration message.")],
|
|
344
|
+
autogenerate: Annotated[bool, typer.Option("--auto", help="Auto-generate from model changes.")] = True,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Generate an Alembic migration file."""
|
|
347
|
+
actx: AppContext = ctx.obj
|
|
348
|
+
out = actx.output
|
|
349
|
+
|
|
350
|
+
root = find_project_root()
|
|
351
|
+
script = root / "scripts" / "db"
|
|
352
|
+
|
|
353
|
+
cmd = [str(script), "generate", message]
|
|
354
|
+
if not autogenerate:
|
|
355
|
+
cmd.append("--no-autogenerate")
|
|
356
|
+
|
|
357
|
+
out.info(f"Generating migration: {message}")
|
|
358
|
+
result = subprocess.run(cmd, cwd=str(root), capture_output=False)
|
|
359
|
+
if result.returncode != 0:
|
|
360
|
+
out.error("Migration generation failed.")
|
|
361
|
+
raise typer.Exit(result.returncode)
|
|
362
|
+
out.success(f"Migration generated: {message}")
|