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