xcorecli 1.0.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.
- xcli/__init__.py +0 -0
- xcli/_run.py +14 -0
- xcli/_xcore.py +56 -0
- xcli/init/__init__.py +0 -0
- xcli/init/cli.py +11 -0
- xcli/init/manager.py +141 -0
- xcli/init/upgrade.py +198 -0
- xcli/main.py +64 -0
- xcli/manager/__init__.py +0 -0
- xcli/manager/cli.py +537 -0
- xcli/marketplace/__init__.py +0 -0
- xcli/marketplace/cli.py +44 -0
- xcli/migrations/__init__.py +3 -0
- xcli/migrations/cli.py +292 -0
- xcli/migrations/runtime.py +198 -0
- xcli/plugin/__init__.py +0 -0
- xcli/plugin/cli.py +538 -0
- xcli/plugin/scaffold.py +213 -0
- xcli/sandbox/__init__.py +0 -0
- xcli/sandbox/cli.py +124 -0
- xcli/worker/__init__.py +0 -0
- xcli/worker/cli.py +569 -0
- xcli/worker/worker.py +565 -0
- xcorecli-1.0.0.dist-info/METADATA +11 -0
- xcorecli-1.0.0.dist-info/RECORD +27 -0
- xcorecli-1.0.0.dist-info/WHEEL +4 -0
- xcorecli-1.0.0.dist-info/entry_points.txt +3 -0
xcli/worker/cli.py
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"""
|
|
2
|
+
worker_cmd.py — Commandes CLI pour gérer FastAPI et Celery en processus séparés.
|
|
3
|
+
|
|
4
|
+
Commandes :
|
|
5
|
+
xcore worker start Lance API + Celery ensemble
|
|
6
|
+
xcore worker start api Lance uniquement FastAPI (uvicorn)
|
|
7
|
+
xcore worker start celery Lance uniquement le worker Celery
|
|
8
|
+
xcore worker stop Arrête tous les processus xcore
|
|
9
|
+
xcore worker stop api Arrête uniquement FastAPI
|
|
10
|
+
xcore worker stop celery Arrête uniquement Celery
|
|
11
|
+
xcore worker status État des processus en cours
|
|
12
|
+
xcore worker logs Affiche les dernières lignes de log
|
|
13
|
+
xcore worker inspect Liste les tâches et files d'attente
|
|
14
|
+
xcore worker purge [queue] Vide une file d'attente
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import signal
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
PID_DIR = Path(".xcore/pids")
|
|
29
|
+
LOG_DIR = Path("log")
|
|
30
|
+
PID_API = PID_DIR / "api.pid"
|
|
31
|
+
PID_CELERY = PID_DIR / "celery.pid"
|
|
32
|
+
LOG_API = LOG_DIR / "api.log"
|
|
33
|
+
LOG_CELERY = LOG_DIR / "celery.log"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_dirs() -> None:
|
|
40
|
+
PID_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _write_pid(path: Path, pid: int) -> None:
|
|
45
|
+
path.write_text(str(pid))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_pid(path: Path) -> int | None:
|
|
49
|
+
if not path.exists():
|
|
50
|
+
return None
|
|
51
|
+
try:
|
|
52
|
+
return int(path.read_text().strip())
|
|
53
|
+
except ValueError:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_running(pid: int | None) -> bool:
|
|
58
|
+
if pid is None:
|
|
59
|
+
return False
|
|
60
|
+
try:
|
|
61
|
+
os.kill(pid, 0)
|
|
62
|
+
return True
|
|
63
|
+
except (ProcessLookupError, PermissionError):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _stop_pid(path: Path, label: str) -> bool:
|
|
68
|
+
from rich.console import Console
|
|
69
|
+
|
|
70
|
+
console = Console()
|
|
71
|
+
pid = _read_pid(path)
|
|
72
|
+
if not _is_running(pid):
|
|
73
|
+
console.print(f" [dim]{label} n'est pas en cours d'exécution[/dim]")
|
|
74
|
+
path.unlink(missing_ok=True)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
os.kill(pid, signal.SIGTERM)
|
|
79
|
+
for _ in range(30):
|
|
80
|
+
time.sleep(0.2)
|
|
81
|
+
if not _is_running(pid):
|
|
82
|
+
break
|
|
83
|
+
else:
|
|
84
|
+
os.kill(pid, signal.SIGKILL)
|
|
85
|
+
path.unlink(missing_ok=True)
|
|
86
|
+
console.print(f" [green]✓[/green] {label} arrêté (PID {pid})")
|
|
87
|
+
return True
|
|
88
|
+
except ProcessLookupError:
|
|
89
|
+
path.unlink(missing_ok=True)
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _load_config(config_path: str | None) -> Any:
|
|
94
|
+
try:
|
|
95
|
+
from xcore.configurations.loader import ConfigLoader
|
|
96
|
+
|
|
97
|
+
return ConfigLoader.load(config_path)
|
|
98
|
+
except Exception:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_celery_app() -> str:
|
|
103
|
+
return "xcore.services.xworker.xworker:_celery_worker"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Lancement des processus ────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _start_api(args: Any) -> subprocess.Popen | None:
|
|
110
|
+
from rich.console import Console
|
|
111
|
+
|
|
112
|
+
console = Console()
|
|
113
|
+
|
|
114
|
+
cfg = _load_config(getattr(args, "config", None))
|
|
115
|
+
srv = getattr(cfg.app, "server", None) if cfg else None
|
|
116
|
+
|
|
117
|
+
_arg_app = getattr(args, "app", "main:app")
|
|
118
|
+
_arg_host = getattr(args, "host", "0.0.0.0")
|
|
119
|
+
_arg_port = getattr(args, "port", 8000)
|
|
120
|
+
|
|
121
|
+
app_path = _arg_app if _arg_app != "main:app" else (getattr(srv, "app", _arg_app) if srv else _arg_app)
|
|
122
|
+
host = _arg_host if _arg_host != "0.0.0.0" else (getattr(srv, "host", _arg_host) if srv else _arg_host)
|
|
123
|
+
port = _arg_port if _arg_port != 8000 else (getattr(srv, "port", _arg_port) if srv else _arg_port)
|
|
124
|
+
workers = getattr(args, "workers", None) or (getattr(srv, "workers", 1) if srv else 1)
|
|
125
|
+
reload = getattr(args, "reload", False) or (getattr(srv, "reload", False) if srv else False)
|
|
126
|
+
log_level = (
|
|
127
|
+
args.loglevel.lower()
|
|
128
|
+
if args.loglevel != "info"
|
|
129
|
+
else (getattr(srv, "log_level", "info") if srv else "info")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
cmd = [
|
|
133
|
+
sys.executable,
|
|
134
|
+
"-m",
|
|
135
|
+
"uvicorn",
|
|
136
|
+
app_path,
|
|
137
|
+
"--host",
|
|
138
|
+
host,
|
|
139
|
+
"--port",
|
|
140
|
+
str(port),
|
|
141
|
+
"--log-level",
|
|
142
|
+
log_level,
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
if reload:
|
|
146
|
+
cmd.append("--reload")
|
|
147
|
+
elif workers > 1:
|
|
148
|
+
cmd.extend(["--workers", str(workers)])
|
|
149
|
+
|
|
150
|
+
if getattr(args, "detach", False):
|
|
151
|
+
_ensure_dirs()
|
|
152
|
+
log_file = open(LOG_API, "a")
|
|
153
|
+
proc = subprocess.Popen(
|
|
154
|
+
cmd,
|
|
155
|
+
stdout=log_file,
|
|
156
|
+
stderr=log_file,
|
|
157
|
+
start_new_session=True,
|
|
158
|
+
)
|
|
159
|
+
_write_pid(PID_API, proc.pid)
|
|
160
|
+
console.print(
|
|
161
|
+
f" [green]✓[/green] API démarrée en arrière-plan — PID {proc.pid} "
|
|
162
|
+
f"[dim]log → {LOG_API}[/dim]"
|
|
163
|
+
)
|
|
164
|
+
return proc
|
|
165
|
+
else:
|
|
166
|
+
console.print(f" [cyan]→[/cyan] API [dim]{' '.join(cmd[2:])}[/dim]")
|
|
167
|
+
return subprocess.Popen(cmd)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _start_celery(args: Any) -> subprocess.Popen | None:
|
|
171
|
+
from rich.console import Console
|
|
172
|
+
|
|
173
|
+
console = Console()
|
|
174
|
+
|
|
175
|
+
celery_app = _resolve_celery_app()
|
|
176
|
+
queues = getattr(args, "queues", None)
|
|
177
|
+
concurrency = getattr(args, "concurrency", None)
|
|
178
|
+
log_level = args.loglevel.upper()
|
|
179
|
+
|
|
180
|
+
cfg = _load_config(getattr(args, "config", None))
|
|
181
|
+
if cfg:
|
|
182
|
+
worker_cfg = cfg.services.xworker
|
|
183
|
+
if queues is None:
|
|
184
|
+
queues = ",".join(worker_cfg.queues)
|
|
185
|
+
if concurrency is None:
|
|
186
|
+
concurrency = worker_cfg.concurrency
|
|
187
|
+
|
|
188
|
+
if queues is None:
|
|
189
|
+
queues = "default"
|
|
190
|
+
if concurrency is None:
|
|
191
|
+
concurrency = 4
|
|
192
|
+
|
|
193
|
+
cmd = [
|
|
194
|
+
sys.executable,
|
|
195
|
+
"-m",
|
|
196
|
+
"celery",
|
|
197
|
+
"-A",
|
|
198
|
+
celery_app,
|
|
199
|
+
"worker",
|
|
200
|
+
"--loglevel",
|
|
201
|
+
log_level,
|
|
202
|
+
"-Q",
|
|
203
|
+
queues,
|
|
204
|
+
"--concurrency",
|
|
205
|
+
str(concurrency),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
hostname = getattr(args, "hostname", None)
|
|
209
|
+
if hostname:
|
|
210
|
+
cmd.extend(["-n", hostname])
|
|
211
|
+
|
|
212
|
+
if getattr(args, "detach", False):
|
|
213
|
+
_ensure_dirs()
|
|
214
|
+
log_file = open(LOG_CELERY, "a")
|
|
215
|
+
proc = subprocess.Popen(
|
|
216
|
+
cmd,
|
|
217
|
+
stdout=log_file,
|
|
218
|
+
stderr=log_file,
|
|
219
|
+
start_new_session=True,
|
|
220
|
+
)
|
|
221
|
+
_write_pid(PID_CELERY, proc.pid)
|
|
222
|
+
console.print(
|
|
223
|
+
f" [green]✓[/green] Celery démarré en arrière-plan — PID {proc.pid} "
|
|
224
|
+
f"[dim]log → {LOG_CELERY}[/dim]"
|
|
225
|
+
)
|
|
226
|
+
return proc
|
|
227
|
+
else:
|
|
228
|
+
console.print(f" [cyan]→[/cyan] Celery [dim]{' '.join(cmd[2:])}[/dim]")
|
|
229
|
+
return subprocess.Popen(cmd)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ── Handlers de commandes ─────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _cmd_start(args: Any) -> None:
|
|
236
|
+
from rich.console import Console
|
|
237
|
+
from rich.panel import Panel
|
|
238
|
+
|
|
239
|
+
console = Console()
|
|
240
|
+
target = getattr(args, "target", "all")
|
|
241
|
+
|
|
242
|
+
console.print(
|
|
243
|
+
Panel(
|
|
244
|
+
f"[bold]Démarrage xcore[/bold] [dim]target={target}[/dim]",
|
|
245
|
+
border_style="blue",
|
|
246
|
+
padding=(0, 2),
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if target in ("all", "api"):
|
|
251
|
+
api_proc = _start_api(args)
|
|
252
|
+
else:
|
|
253
|
+
api_proc = None
|
|
254
|
+
|
|
255
|
+
if target in ("all", "celery"):
|
|
256
|
+
celery_proc = _start_celery(args)
|
|
257
|
+
else:
|
|
258
|
+
celery_proc = None
|
|
259
|
+
|
|
260
|
+
if getattr(args, "detach", False):
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
procs = [p for p in (api_proc, celery_proc) if p is not None]
|
|
264
|
+
if not procs:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
console.print("\n[dim]Ctrl+C pour arrêter[/dim]\n")
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
while all(_is_running(p.pid) for p in procs):
|
|
271
|
+
time.sleep(0.5)
|
|
272
|
+
except KeyboardInterrupt:
|
|
273
|
+
console.print("\n[yellow]⚠[/yellow] Arrêt en cours…")
|
|
274
|
+
finally:
|
|
275
|
+
for proc in procs:
|
|
276
|
+
if _is_running(proc.pid):
|
|
277
|
+
proc.terminate()
|
|
278
|
+
for proc in procs:
|
|
279
|
+
try:
|
|
280
|
+
proc.wait(timeout=8)
|
|
281
|
+
except subprocess.TimeoutExpired:
|
|
282
|
+
proc.kill()
|
|
283
|
+
console.print("[green]✓[/green] Tous les processus arrêtés.")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _cmd_stop(args: Any) -> None:
|
|
287
|
+
from rich.console import Console
|
|
288
|
+
from rich.panel import Panel
|
|
289
|
+
|
|
290
|
+
console = Console()
|
|
291
|
+
target = getattr(args, "target", "all")
|
|
292
|
+
|
|
293
|
+
console.print(
|
|
294
|
+
Panel(
|
|
295
|
+
f"[bold]Arrêt xcore[/bold] [dim]target={target}[/dim]",
|
|
296
|
+
border_style="red",
|
|
297
|
+
padding=(0, 2),
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if target in ("all", "api"):
|
|
302
|
+
_stop_pid(PID_API, "API")
|
|
303
|
+
if target in ("all", "celery"):
|
|
304
|
+
_stop_pid(PID_CELERY, "Celery")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _cmd_status(args: Any) -> None:
|
|
308
|
+
from rich.console import Console
|
|
309
|
+
from rich.table import Table
|
|
310
|
+
|
|
311
|
+
console = Console()
|
|
312
|
+
|
|
313
|
+
api_pid = _read_pid(PID_API)
|
|
314
|
+
celery_pid = _read_pid(PID_CELERY)
|
|
315
|
+
|
|
316
|
+
table = Table(title="État des processus xcore", border_style="blue", min_width=55)
|
|
317
|
+
table.add_column("Service", style="bold")
|
|
318
|
+
table.add_column("PID", justify="right")
|
|
319
|
+
table.add_column("État")
|
|
320
|
+
table.add_column("Log")
|
|
321
|
+
|
|
322
|
+
def _status_cell(pid: int | None) -> tuple[str, str]:
|
|
323
|
+
if _is_running(pid):
|
|
324
|
+
return str(pid), "[green]● En cours[/green]"
|
|
325
|
+
return str(pid) if pid else "—", "[dim]○ Arrêté[/dim]"
|
|
326
|
+
|
|
327
|
+
api_pid_str, api_status = _status_cell(api_pid)
|
|
328
|
+
cel_pid_str, cel_status = _status_cell(celery_pid)
|
|
329
|
+
|
|
330
|
+
table.add_row("FastAPI (uvicorn)", api_pid_str, api_status, str(LOG_API))
|
|
331
|
+
table.add_row("Celery worker", cel_pid_str, cel_status, str(LOG_CELERY))
|
|
332
|
+
|
|
333
|
+
console.print(table)
|
|
334
|
+
|
|
335
|
+
if getattr(args, "json", False):
|
|
336
|
+
data = {
|
|
337
|
+
"api": {"pid": api_pid, "running": _is_running(api_pid)},
|
|
338
|
+
"celery": {"pid": celery_pid, "running": _is_running(celery_pid)},
|
|
339
|
+
}
|
|
340
|
+
console.print_json(json.dumps(data))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _cmd_logs(args: Any) -> None:
|
|
344
|
+
from rich.console import Console
|
|
345
|
+
|
|
346
|
+
console = Console()
|
|
347
|
+
target = getattr(args, "target", "all")
|
|
348
|
+
lines = getattr(args, "lines", 50)
|
|
349
|
+
follow = getattr(args, "follow", False)
|
|
350
|
+
|
|
351
|
+
targets: list[tuple[str, Path]] = []
|
|
352
|
+
if target in ("all", "api"):
|
|
353
|
+
targets.append(("API", LOG_API))
|
|
354
|
+
if target in ("all", "celery"):
|
|
355
|
+
targets.append(("Celery", LOG_CELERY))
|
|
356
|
+
|
|
357
|
+
if follow and len(targets) == 1:
|
|
358
|
+
label, log_path = targets[0]
|
|
359
|
+
if not log_path.exists():
|
|
360
|
+
console.print(
|
|
361
|
+
f"[yellow]⚠[/yellow] {log_path} introuvable — le service est-il démarré ?"
|
|
362
|
+
)
|
|
363
|
+
return
|
|
364
|
+
console.print(f"[dim]→ {log_path} (Ctrl+C pour quitter)[/dim]\n")
|
|
365
|
+
try:
|
|
366
|
+
proc = subprocess.Popen(["tail", "-f", "-n", str(lines), str(log_path)])
|
|
367
|
+
proc.wait()
|
|
368
|
+
except KeyboardInterrupt:
|
|
369
|
+
proc.terminate()
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
for label, log_path in targets:
|
|
373
|
+
if not log_path.exists():
|
|
374
|
+
console.print(f"[dim]{label}: {log_path} introuvable[/dim]\n")
|
|
375
|
+
continue
|
|
376
|
+
console.rule(f"[bold]{label}[/bold] [dim]{log_path}[/dim]")
|
|
377
|
+
result = subprocess.run(
|
|
378
|
+
["tail", "-n", str(lines), str(log_path)],
|
|
379
|
+
capture_output=True,
|
|
380
|
+
text=True,
|
|
381
|
+
)
|
|
382
|
+
console.print(result.stdout or "[dim](vide)[/dim]")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _cmd_inspect(_args: Any) -> None:
|
|
386
|
+
from rich.console import Console
|
|
387
|
+
from rich.table import Table
|
|
388
|
+
|
|
389
|
+
console = Console()
|
|
390
|
+
|
|
391
|
+
celery_app_path = _resolve_celery_app()
|
|
392
|
+
module_path, attr = celery_app_path.rsplit(":", 1)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
import importlib
|
|
396
|
+
|
|
397
|
+
mod = importlib.import_module(module_path)
|
|
398
|
+
celery_app = getattr(mod, attr)
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
console.print(f"[red]✗[/red] Impossible de charger l'app Celery : {exc}")
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
task_table = Table(title="Tâches Celery enregistrées", border_style="blue")
|
|
404
|
+
task_table.add_column("Nom", style="cyan")
|
|
405
|
+
task_table.add_column("Module")
|
|
406
|
+
|
|
407
|
+
for name, task in sorted(celery_app.tasks.items()):
|
|
408
|
+
if name.startswith("celery."):
|
|
409
|
+
continue
|
|
410
|
+
module = getattr(task, "__module__", "—")
|
|
411
|
+
task_table.add_row(name, module)
|
|
412
|
+
|
|
413
|
+
console.print(task_table)
|
|
414
|
+
|
|
415
|
+
console.print()
|
|
416
|
+
try:
|
|
417
|
+
inspect = celery_app.control.inspect(timeout=3)
|
|
418
|
+
active = inspect.active() or {}
|
|
419
|
+
reserved = inspect.reserved() or {}
|
|
420
|
+
|
|
421
|
+
if not active and not reserved:
|
|
422
|
+
console.print(
|
|
423
|
+
"[dim]Aucun worker actif trouvé (broker inaccessible ou aucun worker lancé)[/dim]"
|
|
424
|
+
)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
worker_table = Table(title="Workers actifs", border_style="green")
|
|
428
|
+
worker_table.add_column("Worker")
|
|
429
|
+
worker_table.add_column("Tâches actives", justify="right")
|
|
430
|
+
worker_table.add_column("Tâches en attente", justify="right")
|
|
431
|
+
|
|
432
|
+
all_workers = set(active) | set(reserved)
|
|
433
|
+
for worker in sorted(all_workers):
|
|
434
|
+
n_active = len(active.get(worker, []))
|
|
435
|
+
n_reserved = len(reserved.get(worker, []))
|
|
436
|
+
worker_table.add_row(worker, str(n_active), str(n_reserved))
|
|
437
|
+
|
|
438
|
+
console.print(worker_table)
|
|
439
|
+
|
|
440
|
+
except Exception as exc:
|
|
441
|
+
console.print(f"[yellow]⚠[/yellow] Broker inaccessible : {exc}")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _cmd_purge(args: Any) -> None:
|
|
445
|
+
from rich.console import Console
|
|
446
|
+
|
|
447
|
+
console = Console()
|
|
448
|
+
queue = getattr(args, "queue", None) or "default"
|
|
449
|
+
|
|
450
|
+
celery_app_path = _resolve_celery_app()
|
|
451
|
+
|
|
452
|
+
cmd = [
|
|
453
|
+
sys.executable,
|
|
454
|
+
"-m",
|
|
455
|
+
"celery",
|
|
456
|
+
"-A",
|
|
457
|
+
celery_app_path,
|
|
458
|
+
"purge",
|
|
459
|
+
"-Q",
|
|
460
|
+
queue,
|
|
461
|
+
"-f",
|
|
462
|
+
]
|
|
463
|
+
|
|
464
|
+
console.print(f"[yellow]⚠[/yellow] Purge de la file [bold]{queue}[/bold]…")
|
|
465
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
466
|
+
if result.returncode == 0:
|
|
467
|
+
console.print(f"[green]✓[/green] {result.stdout.strip() or 'File vidée.'}")
|
|
468
|
+
else:
|
|
469
|
+
console.print(f"[red]✗[/red] {result.stderr.strip()}")
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _cmd_beat(args: Any) -> None:
|
|
473
|
+
from rich.console import Console
|
|
474
|
+
|
|
475
|
+
console = Console()
|
|
476
|
+
celery_app_path = _resolve_celery_app()
|
|
477
|
+
log_level = args.loglevel.upper()
|
|
478
|
+
|
|
479
|
+
cmd = [
|
|
480
|
+
sys.executable,
|
|
481
|
+
"-m",
|
|
482
|
+
"celery",
|
|
483
|
+
"-A",
|
|
484
|
+
celery_app_path,
|
|
485
|
+
"beat",
|
|
486
|
+
"--loglevel",
|
|
487
|
+
log_level,
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
schedule = getattr(args, "schedule", None)
|
|
491
|
+
if schedule:
|
|
492
|
+
cmd.extend(["--schedule", schedule])
|
|
493
|
+
|
|
494
|
+
if getattr(args, "detach", False):
|
|
495
|
+
_ensure_dirs()
|
|
496
|
+
pid_beat = PID_DIR / "beat.pid"
|
|
497
|
+
log_beat = LOG_DIR / "beat.log"
|
|
498
|
+
log_file = open(log_beat, "a")
|
|
499
|
+
proc = subprocess.Popen(
|
|
500
|
+
cmd,
|
|
501
|
+
stdout=log_file,
|
|
502
|
+
stderr=log_file,
|
|
503
|
+
start_new_session=True,
|
|
504
|
+
)
|
|
505
|
+
_write_pid(pid_beat, proc.pid)
|
|
506
|
+
console.print(
|
|
507
|
+
f" [green]✓[/green] Beat démarré en arrière-plan — PID {proc.pid} "
|
|
508
|
+
f"[dim]log → {log_beat}[/dim]"
|
|
509
|
+
)
|
|
510
|
+
else:
|
|
511
|
+
console.print(f" [cyan]→[/cyan] Beat [dim]{' '.join(cmd[2:])}[/dim]")
|
|
512
|
+
console.print("[dim]Ctrl+C pour arrêter[/dim]\n")
|
|
513
|
+
try:
|
|
514
|
+
proc = subprocess.Popen(cmd)
|
|
515
|
+
proc.wait()
|
|
516
|
+
except KeyboardInterrupt:
|
|
517
|
+
proc.terminate()
|
|
518
|
+
proc.wait()
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ── Point d'entrée principal ──────────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def handle_worker(args: Any) -> None:
|
|
525
|
+
sub = getattr(args, "worker_subcommand", None)
|
|
526
|
+
|
|
527
|
+
if sub == "start":
|
|
528
|
+
_cmd_start(args)
|
|
529
|
+
elif sub == "stop":
|
|
530
|
+
_cmd_stop(args)
|
|
531
|
+
elif sub == "status":
|
|
532
|
+
_cmd_status(args)
|
|
533
|
+
elif sub == "logs":
|
|
534
|
+
_cmd_logs(args)
|
|
535
|
+
elif sub == "inspect":
|
|
536
|
+
_cmd_inspect(args)
|
|
537
|
+
elif sub == "purge":
|
|
538
|
+
_cmd_purge(args)
|
|
539
|
+
elif sub == "beat":
|
|
540
|
+
_cmd_beat(args)
|
|
541
|
+
else:
|
|
542
|
+
from rich.console import Console
|
|
543
|
+
from rich.table import Table
|
|
544
|
+
|
|
545
|
+
console = Console()
|
|
546
|
+
table = Table(
|
|
547
|
+
title="xcore worker — commandes disponibles",
|
|
548
|
+
border_style="blue",
|
|
549
|
+
min_width=65,
|
|
550
|
+
)
|
|
551
|
+
table.add_column("Commande", style="bold cyan")
|
|
552
|
+
table.add_column("Description")
|
|
553
|
+
|
|
554
|
+
rows = [
|
|
555
|
+
("xcore worker start [api|celery]", "Lance API et/ou Celery worker"),
|
|
556
|
+
("xcore worker stop [api|celery]", "Arrête les processus en cours"),
|
|
557
|
+
("xcore worker status", "Affiche l'état des processus"),
|
|
558
|
+
("xcore worker logs [api|celery]", "Affiche les dernières lignes de log"),
|
|
559
|
+
("xcore worker inspect", "Liste les tâches et workers actifs"),
|
|
560
|
+
("xcore worker purge [queue]", "Vide une file d'attente Celery"),
|
|
561
|
+
("xcore worker beat", "Lance le scheduler Celery Beat"),
|
|
562
|
+
]
|
|
563
|
+
for cmd, desc in rows:
|
|
564
|
+
table.add_row(cmd, desc)
|
|
565
|
+
|
|
566
|
+
console.print(table)
|
|
567
|
+
console.print(
|
|
568
|
+
"\n[dim]Exemple : xcore worker start --detach -Q default,emails -c 4[/dim]"
|
|
569
|
+
)
|