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/worker/worker.py ADDED
@@ -0,0 +1,565 @@
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 = cfg.app.server if cfg else None
116
+
117
+ app_path = args.app if args.app != "main:app" else (srv.app if srv else args.app)
118
+ host = args.host if args.host != "0.0.0.0" else (srv.host if srv else args.host)
119
+ port = args.port if args.port != 8000 else (srv.port if srv else args.port)
120
+ workers = getattr(args, "workers", None) or (srv.workers if srv else 1)
121
+ reload = getattr(args, "reload", False) or (srv.reload if srv else False)
122
+ log_level = (
123
+ args.loglevel.lower()
124
+ if args.loglevel != "info"
125
+ else (srv.log_level if srv else "info")
126
+ )
127
+
128
+ cmd = [
129
+ sys.executable,
130
+ "-m",
131
+ "uvicorn",
132
+ app_path,
133
+ "--host",
134
+ host,
135
+ "--port",
136
+ str(port),
137
+ "--log-level",
138
+ log_level,
139
+ ]
140
+
141
+ if reload:
142
+ cmd.append("--reload")
143
+ elif workers > 1:
144
+ cmd.extend(["--workers", str(workers)])
145
+
146
+ if getattr(args, "detach", False):
147
+ _ensure_dirs()
148
+ log_file = open(LOG_API, "a")
149
+ proc = subprocess.Popen(
150
+ cmd,
151
+ stdout=log_file,
152
+ stderr=log_file,
153
+ start_new_session=True,
154
+ )
155
+ _write_pid(PID_API, proc.pid)
156
+ console.print(
157
+ f" [green]✓[/green] API démarrée en arrière-plan — PID {proc.pid} "
158
+ f"[dim]log → {LOG_API}[/dim]"
159
+ )
160
+ return proc
161
+ else:
162
+ console.print(f" [cyan]→[/cyan] API [dim]{' '.join(cmd[2:])}[/dim]")
163
+ return subprocess.Popen(cmd)
164
+
165
+
166
+ def _start_celery(args: Any) -> subprocess.Popen | None:
167
+ from rich.console import Console
168
+
169
+ console = Console()
170
+
171
+ celery_app = _resolve_celery_app()
172
+ queues = getattr(args, "queues", None)
173
+ concurrency = getattr(args, "concurrency", None)
174
+ log_level = args.loglevel.upper()
175
+
176
+ cfg = _load_config(getattr(args, "config", None))
177
+ if cfg:
178
+ worker_cfg = cfg.services.xworker
179
+ if queues is None:
180
+ queues = ",".join(worker_cfg.queues)
181
+ if concurrency is None:
182
+ concurrency = worker_cfg.concurrency
183
+
184
+ if queues is None:
185
+ queues = "default"
186
+ if concurrency is None:
187
+ concurrency = 4
188
+
189
+ cmd = [
190
+ sys.executable,
191
+ "-m",
192
+ "celery",
193
+ "-A",
194
+ celery_app,
195
+ "worker",
196
+ "--loglevel",
197
+ log_level,
198
+ "-Q",
199
+ queues,
200
+ "--concurrency",
201
+ str(concurrency),
202
+ ]
203
+
204
+ hostname = getattr(args, "hostname", None)
205
+ if hostname:
206
+ cmd.extend(["-n", hostname])
207
+
208
+ if getattr(args, "detach", False):
209
+ _ensure_dirs()
210
+ log_file = open(LOG_CELERY, "a")
211
+ proc = subprocess.Popen(
212
+ cmd,
213
+ stdout=log_file,
214
+ stderr=log_file,
215
+ start_new_session=True,
216
+ )
217
+ _write_pid(PID_CELERY, proc.pid)
218
+ console.print(
219
+ f" [green]✓[/green] Celery démarré en arrière-plan — PID {proc.pid} "
220
+ f"[dim]log → {LOG_CELERY}[/dim]"
221
+ )
222
+ return proc
223
+ else:
224
+ console.print(f" [cyan]→[/cyan] Celery [dim]{' '.join(cmd[2:])}[/dim]")
225
+ return subprocess.Popen(cmd)
226
+
227
+
228
+ # ── Handlers de commandes ─────────────────────────────────────────────────────
229
+
230
+
231
+ def _cmd_start(args: Any) -> None:
232
+ from rich.console import Console
233
+ from rich.panel import Panel
234
+
235
+ console = Console()
236
+ target = getattr(args, "target", "all")
237
+
238
+ console.print(
239
+ Panel(
240
+ f"[bold]Démarrage xcore[/bold] [dim]target={target}[/dim]",
241
+ border_style="blue",
242
+ padding=(0, 2),
243
+ )
244
+ )
245
+
246
+ if target in ("all", "api"):
247
+ api_proc = _start_api(args)
248
+ else:
249
+ api_proc = None
250
+
251
+ if target in ("all", "celery"):
252
+ celery_proc = _start_celery(args)
253
+ else:
254
+ celery_proc = None
255
+
256
+ if getattr(args, "detach", False):
257
+ return
258
+
259
+ procs = [p for p in (api_proc, celery_proc) if p is not None]
260
+ if not procs:
261
+ return
262
+
263
+ console.print("\n[dim]Ctrl+C pour arrêter[/dim]\n")
264
+
265
+ try:
266
+ while all(_is_running(p.pid) for p in procs):
267
+ time.sleep(0.5)
268
+ except KeyboardInterrupt:
269
+ console.print("\n[yellow]⚠[/yellow] Arrêt en cours…")
270
+ finally:
271
+ for proc in procs:
272
+ if _is_running(proc.pid):
273
+ proc.terminate()
274
+ for proc in procs:
275
+ try:
276
+ proc.wait(timeout=8)
277
+ except subprocess.TimeoutExpired:
278
+ proc.kill()
279
+ console.print("[green]✓[/green] Tous les processus arrêtés.")
280
+
281
+
282
+ def _cmd_stop(args: Any) -> None:
283
+ from rich.console import Console
284
+ from rich.panel import Panel
285
+
286
+ console = Console()
287
+ target = getattr(args, "target", "all")
288
+
289
+ console.print(
290
+ Panel(
291
+ f"[bold]Arrêt xcore[/bold] [dim]target={target}[/dim]",
292
+ border_style="red",
293
+ padding=(0, 2),
294
+ )
295
+ )
296
+
297
+ if target in ("all", "api"):
298
+ _stop_pid(PID_API, "API")
299
+ if target in ("all", "celery"):
300
+ _stop_pid(PID_CELERY, "Celery")
301
+
302
+
303
+ def _cmd_status(args: Any) -> None:
304
+ from rich.console import Console
305
+ from rich.table import Table
306
+
307
+ console = Console()
308
+
309
+ api_pid = _read_pid(PID_API)
310
+ celery_pid = _read_pid(PID_CELERY)
311
+
312
+ table = Table(title="État des processus xcore", border_style="blue", min_width=55)
313
+ table.add_column("Service", style="bold")
314
+ table.add_column("PID", justify="right")
315
+ table.add_column("État")
316
+ table.add_column("Log")
317
+
318
+ def _status_cell(pid: int | None) -> tuple[str, str]:
319
+ if _is_running(pid):
320
+ return str(pid), "[green]● En cours[/green]"
321
+ return str(pid) if pid else "—", "[dim]○ Arrêté[/dim]"
322
+
323
+ api_pid_str, api_status = _status_cell(api_pid)
324
+ cel_pid_str, cel_status = _status_cell(celery_pid)
325
+
326
+ table.add_row("FastAPI (uvicorn)", api_pid_str, api_status, str(LOG_API))
327
+ table.add_row("Celery worker", cel_pid_str, cel_status, str(LOG_CELERY))
328
+
329
+ console.print(table)
330
+
331
+ if getattr(args, "json", False):
332
+ data = {
333
+ "api": {"pid": api_pid, "running": _is_running(api_pid)},
334
+ "celery": {"pid": celery_pid, "running": _is_running(celery_pid)},
335
+ }
336
+ console.print_json(json.dumps(data))
337
+
338
+
339
+ def _cmd_logs(args: Any) -> None:
340
+ from rich.console import Console
341
+
342
+ console = Console()
343
+ target = getattr(args, "target", "all")
344
+ lines = getattr(args, "lines", 50)
345
+ follow = getattr(args, "follow", False)
346
+
347
+ targets: list[tuple[str, Path]] = []
348
+ if target in ("all", "api"):
349
+ targets.append(("API", LOG_API))
350
+ if target in ("all", "celery"):
351
+ targets.append(("Celery", LOG_CELERY))
352
+
353
+ if follow and len(targets) == 1:
354
+ label, log_path = targets[0]
355
+ if not log_path.exists():
356
+ console.print(
357
+ f"[yellow]⚠[/yellow] {log_path} introuvable — le service est-il démarré ?"
358
+ )
359
+ return
360
+ console.print(f"[dim]→ {log_path} (Ctrl+C pour quitter)[/dim]\n")
361
+ try:
362
+ proc = subprocess.Popen(["tail", "-f", "-n", str(lines), str(log_path)])
363
+ proc.wait()
364
+ except KeyboardInterrupt:
365
+ proc.terminate()
366
+ return
367
+
368
+ for label, log_path in targets:
369
+ if not log_path.exists():
370
+ console.print(f"[dim]{label}: {log_path} introuvable[/dim]\n")
371
+ continue
372
+ console.rule(f"[bold]{label}[/bold] [dim]{log_path}[/dim]")
373
+ result = subprocess.run(
374
+ ["tail", "-n", str(lines), str(log_path)],
375
+ capture_output=True,
376
+ text=True,
377
+ )
378
+ console.print(result.stdout or "[dim](vide)[/dim]")
379
+
380
+
381
+ def _cmd_inspect(_args: Any) -> None:
382
+ from rich.console import Console
383
+ from rich.table import Table
384
+
385
+ console = Console()
386
+
387
+ celery_app_path = _resolve_celery_app()
388
+ module_path, attr = celery_app_path.rsplit(":", 1)
389
+
390
+ try:
391
+ import importlib
392
+
393
+ mod = importlib.import_module(module_path)
394
+ celery_app = getattr(mod, attr)
395
+ except Exception as exc:
396
+ console.print(f"[red]✗[/red] Impossible de charger l'app Celery : {exc}")
397
+ return
398
+
399
+ task_table = Table(title="Tâches Celery enregistrées", border_style="blue")
400
+ task_table.add_column("Nom", style="cyan")
401
+ task_table.add_column("Module")
402
+
403
+ for name, task in sorted(celery_app.tasks.items()):
404
+ if name.startswith("celery."):
405
+ continue
406
+ module = getattr(task, "__module__", "—")
407
+ task_table.add_row(name, module)
408
+
409
+ console.print(task_table)
410
+
411
+ console.print()
412
+ try:
413
+ inspect = celery_app.control.inspect(timeout=3)
414
+ active = inspect.active() or {}
415
+ reserved = inspect.reserved() or {}
416
+
417
+ if not active and not reserved:
418
+ console.print(
419
+ "[dim]Aucun worker actif trouvé (broker inaccessible ou aucun worker lancé)[/dim]"
420
+ )
421
+ return
422
+
423
+ worker_table = Table(title="Workers actifs", border_style="green")
424
+ worker_table.add_column("Worker")
425
+ worker_table.add_column("Tâches actives", justify="right")
426
+ worker_table.add_column("Tâches en attente", justify="right")
427
+
428
+ all_workers = set(active) | set(reserved)
429
+ for worker in sorted(all_workers):
430
+ n_active = len(active.get(worker, []))
431
+ n_reserved = len(reserved.get(worker, []))
432
+ worker_table.add_row(worker, str(n_active), str(n_reserved))
433
+
434
+ console.print(worker_table)
435
+
436
+ except Exception as exc:
437
+ console.print(f"[yellow]⚠[/yellow] Broker inaccessible : {exc}")
438
+
439
+
440
+ def _cmd_purge(args: Any) -> None:
441
+ from rich.console import Console
442
+
443
+ console = Console()
444
+ queue = getattr(args, "queue", None) or "default"
445
+
446
+ celery_app_path = _resolve_celery_app()
447
+
448
+ cmd = [
449
+ sys.executable,
450
+ "-m",
451
+ "celery",
452
+ "-A",
453
+ celery_app_path,
454
+ "purge",
455
+ "-Q",
456
+ queue,
457
+ "-f",
458
+ ]
459
+
460
+ console.print(f"[yellow]⚠[/yellow] Purge de la file [bold]{queue}[/bold]…")
461
+ result = subprocess.run(cmd, capture_output=True, text=True)
462
+ if result.returncode == 0:
463
+ console.print(f"[green]✓[/green] {result.stdout.strip() or 'File vidée.'}")
464
+ else:
465
+ console.print(f"[red]✗[/red] {result.stderr.strip()}")
466
+
467
+
468
+ def _cmd_beat(args: Any) -> None:
469
+ from rich.console import Console
470
+
471
+ console = Console()
472
+ celery_app_path = _resolve_celery_app()
473
+ log_level = args.loglevel.upper()
474
+
475
+ cmd = [
476
+ sys.executable,
477
+ "-m",
478
+ "celery",
479
+ "-A",
480
+ celery_app_path,
481
+ "beat",
482
+ "--loglevel",
483
+ log_level,
484
+ ]
485
+
486
+ schedule = getattr(args, "schedule", None)
487
+ if schedule:
488
+ cmd.extend(["--schedule", schedule])
489
+
490
+ if getattr(args, "detach", False):
491
+ _ensure_dirs()
492
+ pid_beat = PID_DIR / "beat.pid"
493
+ log_beat = LOG_DIR / "beat.log"
494
+ log_file = open(log_beat, "a")
495
+ proc = subprocess.Popen(
496
+ cmd,
497
+ stdout=log_file,
498
+ stderr=log_file,
499
+ start_new_session=True,
500
+ )
501
+ _write_pid(pid_beat, proc.pid)
502
+ console.print(
503
+ f" [green]✓[/green] Beat démarré en arrière-plan — PID {proc.pid} "
504
+ f"[dim]log → {log_beat}[/dim]"
505
+ )
506
+ else:
507
+ console.print(f" [cyan]→[/cyan] Beat [dim]{' '.join(cmd[2:])}[/dim]")
508
+ console.print("[dim]Ctrl+C pour arrêter[/dim]\n")
509
+ try:
510
+ proc = subprocess.Popen(cmd)
511
+ proc.wait()
512
+ except KeyboardInterrupt:
513
+ proc.terminate()
514
+ proc.wait()
515
+
516
+
517
+ # ── Point d'entrée principal ──────────────────────────────────────────────────
518
+
519
+
520
+ def handle_worker(args: Any) -> None:
521
+ sub = getattr(args, "worker_subcommand", None)
522
+
523
+ if sub == "start":
524
+ _cmd_start(args)
525
+ elif sub == "stop":
526
+ _cmd_stop(args)
527
+ elif sub == "status":
528
+ _cmd_status(args)
529
+ elif sub == "logs":
530
+ _cmd_logs(args)
531
+ elif sub == "inspect":
532
+ _cmd_inspect(args)
533
+ elif sub == "purge":
534
+ _cmd_purge(args)
535
+ elif sub == "beat":
536
+ _cmd_beat(args)
537
+ else:
538
+ from rich.console import Console
539
+ from rich.table import Table
540
+
541
+ console = Console()
542
+ table = Table(
543
+ title="xcore worker — commandes disponibles",
544
+ border_style="blue",
545
+ min_width=65,
546
+ )
547
+ table.add_column("Commande", style="bold cyan")
548
+ table.add_column("Description")
549
+
550
+ rows = [
551
+ ("xcore worker start [api|celery]", "Lance API et/ou Celery worker"),
552
+ ("xcore worker stop [api|celery]", "Arrête les processus en cours"),
553
+ ("xcore worker status", "Affiche l'état des processus"),
554
+ ("xcore worker logs [api|celery]", "Affiche les dernières lignes de log"),
555
+ ("xcore worker inspect", "Liste les tâches et workers actifs"),
556
+ ("xcore worker purge [queue]", "Vide une file d'attente Celery"),
557
+ ("xcore worker beat", "Lance le scheduler Celery Beat"),
558
+ ]
559
+ for cmd, desc in rows:
560
+ table.add_row(cmd, desc)
561
+
562
+ console.print(table)
563
+ console.print(
564
+ "\n[dim]Exemple : xcore worker start --detach -Q default,emails -c 4[/dim]"
565
+ )
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: xcorecli
3
+ Version: 1.0.0
4
+ Summary: CLI for xcore — configuration, plugins, deployment
5
+ Requires-Python: <4.0,>=3.12
6
+ Requires-Dist: alembic>=1.18.0
7
+ Requires-Dist: psutil>=5.9
8
+ Requires-Dist: pyyaml>=6.0.1
9
+ Requires-Dist: rich>=14.3.4
10
+ Requires-Dist: sqlalchemy>=2.0.0
11
+ Requires-Dist: typer>=0.25.1
@@ -0,0 +1,27 @@
1
+ xcli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ xcli/_run.py,sha256=B_MTgFcRtflwiJLnWxMgog827Nvi4Nd58VPNiCpqg4o,394
3
+ xcli/_xcore.py,sha256=EErQi8iSRyOmAQeHCWHYosHH0cOEycruFwvlnojuTgg,1429
4
+ xcli/main.py,sha256=C_4zVcuWCFT0D69tKsvXz2Xv5o4ncrKfcijBcTpS_IE,2674
5
+ xcli/init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ xcli/init/cli.py,sha256=almIuwTbEtjSbXTMuX0lrKStSJc7BUp_A56hKtv_API,187
7
+ xcli/init/manager.py,sha256=Hfqd0b0C8BL4QOyjahJEJu_vvXWZqpnxLgiWM9bacxI,3466
8
+ xcli/init/upgrade.py,sha256=VB9FZ0wU-3RvtrhYsR8D1afsnvPRSyNKwJRqWDgOzUw,5861
9
+ xcli/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ xcli/manager/cli.py,sha256=y_WSiAcfsIfa27x9488K-ktllglGfWgkw-_X-LKzbQ0,20166
11
+ xcli/marketplace/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ xcli/marketplace/cli.py,sha256=5DLcXVkVgleQBohG3WRT-rJ1x1VVrjN8yZtLLkydCp0,1260
13
+ xcli/migrations/__init__.py,sha256=ledMrs-h5k_4wssXWv4MBYTDi-HHo3Fv8QAMpqlQI_M,40
14
+ xcli/migrations/cli.py,sha256=D805ljf2x9knugfoiQOu5gaAd1x9oHNL-ZwTVBTDIkI,8611
15
+ xcli/migrations/runtime.py,sha256=gyHn4wwe6JgWFP1GuNiJor3ipjQn692bv_jQnZzgJ1U,5691
16
+ xcli/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ xcli/plugin/cli.py,sha256=aqGM9QoJBD4hOGykfMW5Gjjur_1YzMpL6sa9aWX0k7g,19987
18
+ xcli/plugin/scaffold.py,sha256=fFDWUuOh7iNavBk8bST40Edcx7LbYG_ZMxLpM4WrjmA,6230
19
+ xcli/sandbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ xcli/sandbox/cli.py,sha256=cMOgCZNwYpBcb-lbyUxyXh1vumCdo1Sw5HUBxEjb_Mo,4434
21
+ xcli/worker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ xcli/worker/cli.py,sha256=WDfLg7CgmIwUx2BBYxoDLPWh74LwLao-HAeKLstexc0,17183
23
+ xcli/worker/worker.py,sha256=kNXrPEvKkt2rxDf-jBtrb2Ngz4N29o84-8rQvWO_CU0,16901
24
+ xcorecli-1.0.0.dist-info/METADATA,sha256=dMc-KlI1nIG-p8gDaq33PV5FK6jIIIMS4TXwLYg01rI,320
25
+ xcorecli-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
26
+ xcorecli-1.0.0.dist-info/entry_points.txt,sha256=MIgO5GoxvPnkLFNT0mIsNf-qNS6LE98_4v1jcOpTW7U,65
27
+ xcorecli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ xcli = xcli.main:app
3
+ xcore-cli = xcli.main:app