celery-fastapi 0.1.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.
celery_fastapi/cli.py ADDED
@@ -0,0 +1,787 @@
1
+ """Command-line interface for Celery FastAPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from enum import Enum
8
+ from typing import Annotated, Any
9
+
10
+ try:
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ except ImportError:
15
+ print(
16
+ "CLI dependencies not installed. "
17
+ "Install with: pip install celery-fastapi[cli]"
18
+ )
19
+ sys.exit(1)
20
+
21
+
22
+ class LogLevel(str, Enum):
23
+ """Log level options."""
24
+
25
+ critical = "critical"
26
+ error = "error"
27
+ warning = "warning"
28
+ info = "info"
29
+ debug = "debug"
30
+ trace = "trace"
31
+
32
+
33
+ class LoopType(str, Enum):
34
+ """Event loop implementation options."""
35
+
36
+ auto = "auto"
37
+ asyncio = "asyncio"
38
+ uvloop = "uvloop"
39
+
40
+
41
+ class HttpProtocol(str, Enum):
42
+ """HTTP protocol implementation options."""
43
+
44
+ auto = "auto"
45
+ h11 = "h11"
46
+ httptools = "httptools"
47
+
48
+
49
+ class WsProtocol(str, Enum):
50
+ """WebSocket protocol implementation options."""
51
+
52
+ auto = "auto"
53
+ none = "none"
54
+ websockets = "websockets"
55
+ wsproto = "wsproto"
56
+
57
+
58
+ class InterfaceType(str, Enum):
59
+ """ASGI interface version."""
60
+
61
+ auto = "auto"
62
+ asgi3 = "asgi3"
63
+ asgi2 = "asgi2"
64
+ wsgi = "wsgi"
65
+
66
+
67
+ app = typer.Typer(
68
+ name="celery-fastapi",
69
+ help="Automatic REST API generation for Celery tasks with FastAPI.",
70
+ add_completion=False,
71
+ )
72
+ console = Console()
73
+
74
+
75
+ def version_callback(value: bool) -> None:
76
+ """Print version and exit."""
77
+ if value:
78
+ from celery_fastapi import __version__
79
+
80
+ console.print(f"celery-fastapi version: [bold green]{__version__}[/]")
81
+ raise typer.Exit()
82
+
83
+
84
+ @app.command()
85
+ def serve(
86
+ celery_app: Annotated[
87
+ str,
88
+ typer.Argument(
89
+ help="Path to Celery app (e.g., 'myapp.celery:app' or 'celery_app:celery_app')"
90
+ ),
91
+ ],
92
+ # Server binding options
93
+ host: Annotated[
94
+ str,
95
+ typer.Option("--host", "-h", help="Host to bind the server to"),
96
+ ] = "0.0.0.0",
97
+ port: Annotated[
98
+ int,
99
+ typer.Option("--port", "-p", help="Port to bind the server to"),
100
+ ] = 8000,
101
+ uds: Annotated[
102
+ str | None,
103
+ typer.Option("--uds", help="Unix domain socket path to bind to"),
104
+ ] = None,
105
+ fd: Annotated[
106
+ int | None,
107
+ typer.Option("--fd", help="File descriptor to bind to"),
108
+ ] = None,
109
+ # Development options
110
+ reload: Annotated[
111
+ bool,
112
+ typer.Option("--reload", "-r", help="Enable auto-reload for development"),
113
+ ] = False,
114
+ reload_dir: Annotated[
115
+ list[str] | None,
116
+ typer.Option("--reload-dir", help="Directories to watch for reload"),
117
+ ] = None,
118
+ reload_include: Annotated[
119
+ list[str] | None,
120
+ typer.Option("--reload-include", help="Glob patterns to include for reload"),
121
+ ] = None,
122
+ reload_exclude: Annotated[
123
+ list[str] | None,
124
+ typer.Option("--reload-exclude", help="Glob patterns to exclude for reload"),
125
+ ] = None,
126
+ reload_delay: Annotated[
127
+ float,
128
+ typer.Option("--reload-delay", help="Delay between reload checks"),
129
+ ] = 0.25,
130
+ # Worker options
131
+ workers: Annotated[
132
+ int,
133
+ typer.Option("--workers", "-w", help="Number of worker processes"),
134
+ ] = 1,
135
+ # Logging options
136
+ log_level: Annotated[
137
+ LogLevel,
138
+ typer.Option("--log-level", "-l", help="Logging level"),
139
+ ] = LogLevel.info,
140
+ access_log: Annotated[
141
+ bool,
142
+ typer.Option("--access-log/--no-access-log", help="Enable/disable access log"),
143
+ ] = True,
144
+ log_config: Annotated[
145
+ str | None,
146
+ typer.Option("--log-config", help="Path to logging config file"),
147
+ ] = None,
148
+ # Protocol options
149
+ loop: Annotated[
150
+ LoopType,
151
+ typer.Option("--loop", help="Event loop implementation"),
152
+ ] = LoopType.auto,
153
+ http: Annotated[
154
+ HttpProtocol,
155
+ typer.Option("--http", help="HTTP protocol implementation"),
156
+ ] = HttpProtocol.auto,
157
+ ws: Annotated[
158
+ WsProtocol,
159
+ typer.Option("--ws", help="WebSocket protocol implementation"),
160
+ ] = WsProtocol.auto,
161
+ interface: Annotated[
162
+ InterfaceType,
163
+ typer.Option("--interface", help="ASGI interface version"),
164
+ ] = InterfaceType.auto,
165
+ # Connection options
166
+ limit_concurrency: Annotated[
167
+ int | None,
168
+ typer.Option("--limit-concurrency", help="Max concurrent connections"),
169
+ ] = None,
170
+ limit_max_requests: Annotated[
171
+ int | None,
172
+ typer.Option("--limit-max-requests", help="Max requests before worker restart"),
173
+ ] = None,
174
+ backlog: Annotated[
175
+ int,
176
+ typer.Option("--backlog", help="Max queued connections"),
177
+ ] = 2048,
178
+ # Timeout options
179
+ timeout_keep_alive: Annotated[
180
+ int,
181
+ typer.Option("--timeout-keep-alive", help="Keep-alive timeout in seconds"),
182
+ ] = 5,
183
+ timeout_graceful_shutdown: Annotated[
184
+ int | None,
185
+ typer.Option("--timeout-graceful-shutdown", help="Graceful shutdown timeout"),
186
+ ] = None,
187
+ # SSL options
188
+ ssl_keyfile: Annotated[
189
+ str | None,
190
+ typer.Option("--ssl-keyfile", help="SSL key file path"),
191
+ ] = None,
192
+ ssl_certfile: Annotated[
193
+ str | None,
194
+ typer.Option("--ssl-certfile", help="SSL certificate file path"),
195
+ ] = None,
196
+ ssl_keyfile_password: Annotated[
197
+ str | None,
198
+ typer.Option("--ssl-keyfile-password", help="SSL key file password"),
199
+ ] = None,
200
+ ssl_ca_certs: Annotated[
201
+ str | None,
202
+ typer.Option("--ssl-ca-certs", help="SSL CA certificates file"),
203
+ ] = None,
204
+ # Header options
205
+ server_header: Annotated[
206
+ bool,
207
+ typer.Option(
208
+ "--server-header/--no-server-header", help="Include server header"
209
+ ),
210
+ ] = True,
211
+ date_header: Annotated[
212
+ bool,
213
+ typer.Option("--date-header/--no-date-header", help="Include date header"),
214
+ ] = True,
215
+ forwarded_allow_ips: Annotated[
216
+ str | None,
217
+ typer.Option(
218
+ "--forwarded-allow-ips",
219
+ help="Comma-separated IPs to trust for X-Forwarded headers",
220
+ ),
221
+ ] = None,
222
+ # Proxy headers
223
+ proxy_headers: Annotated[
224
+ bool,
225
+ typer.Option("--proxy-headers/--no-proxy-headers", help="Enable proxy headers"),
226
+ ] = True,
227
+ # FastAPI prefix option
228
+ prefix: Annotated[
229
+ str,
230
+ typer.Option("--prefix", help="URL prefix for all endpoints"),
231
+ ] = "",
232
+ # Root path for proxied setups
233
+ root_path: Annotated[
234
+ str,
235
+ typer.Option("--root-path", help="ASGI root_path for apps behind proxies"),
236
+ ] = "",
237
+ # H11 max incomplete event size
238
+ h11_max_incomplete_event_size: Annotated[
239
+ int | None,
240
+ typer.Option(
241
+ "--h11-max-incomplete-event-size", help="H11 max incomplete event size"
242
+ ),
243
+ ] = None,
244
+ ) -> None:
245
+ """
246
+ Start the FastAPI server with auto-generated Celery task endpoints.
247
+
248
+ Supports all uvicorn options for production deployment including:
249
+ - SSL/TLS configuration
250
+ - Worker process management
251
+ - Protocol customization (HTTP/WebSocket implementations)
252
+ - Proxy header handling
253
+ - Connection limits and timeouts
254
+
255
+ Examples:
256
+ # Development with auto-reload
257
+ celery-fastapi serve myapp.celery:app --reload
258
+
259
+ # Production with multiple workers
260
+ celery-fastapi serve myapp.celery:app -w 4 --host 0.0.0.0
261
+
262
+ # With SSL
263
+ celery-fastapi serve myapp.celery:app --ssl-keyfile key.pem --ssl-certfile cert.pem
264
+
265
+ # Behind a proxy
266
+ celery-fastapi serve myapp.celery:app --proxy-headers --forwarded-allow-ips '*'
267
+ """
268
+ try:
269
+ import uvicorn
270
+ except ImportError:
271
+ console.print(
272
+ "[red]Error:[/] uvicorn not installed. "
273
+ "Install with: pip install celery-fastapi[server]"
274
+ )
275
+ raise typer.Exit(1)
276
+
277
+ from celery_fastapi.app import create_app, load_celery_app
278
+
279
+ console.print(f"[bold blue]Loading Celery app from:[/] {celery_app}")
280
+
281
+ try:
282
+ celery_instance = load_celery_app(celery_app)
283
+ except Exception as e:
284
+ console.print(f"[red]Error loading Celery app:[/] {e}")
285
+ raise typer.Exit(1)
286
+
287
+ console.print(f"[green]✓[/] Loaded Celery app: [bold]{celery_instance.main}[/]")
288
+
289
+ # Create the FastAPI app
290
+ fastapi_app = create_app(
291
+ celery_instance,
292
+ title=f"Celery FastAPI - {celery_instance.main}",
293
+ prefix=prefix,
294
+ fastapi_kwargs={"root_path": root_path} if root_path else None,
295
+ )
296
+
297
+ # Show registered routes
298
+ bridge = fastapi_app.state.celery_bridge
299
+ routes = bridge.get_registered_routes()
300
+
301
+ if routes:
302
+ table = Table(title="Registered Endpoints")
303
+ table.add_column("Method", style="cyan")
304
+ table.add_column("Path", style="green")
305
+
306
+ for route in routes:
307
+ table.add_row(route["method"], route["path"])
308
+
309
+ console.print(table)
310
+ else:
311
+ console.print("[yellow]Warning:[/] No task endpoints registered")
312
+
313
+ # Build uvicorn config
314
+ uvicorn_config: dict[str, Any] = {
315
+ "host": host,
316
+ "port": port,
317
+ "reload": reload,
318
+ "workers": workers if not reload else 1,
319
+ "log_level": log_level.value,
320
+ "access_log": access_log,
321
+ "proxy_headers": proxy_headers,
322
+ "server_header": server_header,
323
+ "date_header": date_header,
324
+ "timeout_keep_alive": timeout_keep_alive,
325
+ "backlog": backlog,
326
+ "reload_delay": reload_delay,
327
+ }
328
+
329
+ # Add optional parameters
330
+ if uds:
331
+ uvicorn_config["uds"] = uds
332
+ if fd is not None:
333
+ uvicorn_config["fd"] = fd
334
+ if reload_dir:
335
+ uvicorn_config["reload_dirs"] = reload_dir
336
+ if reload_include:
337
+ uvicorn_config["reload_includes"] = reload_include
338
+ if reload_exclude:
339
+ uvicorn_config["reload_excludes"] = reload_exclude
340
+ if log_config:
341
+ uvicorn_config["log_config"] = log_config
342
+ if limit_concurrency:
343
+ uvicorn_config["limit_concurrency"] = limit_concurrency
344
+ if limit_max_requests:
345
+ uvicorn_config["limit_max_requests"] = limit_max_requests
346
+ if timeout_graceful_shutdown:
347
+ uvicorn_config["timeout_graceful_shutdown"] = timeout_graceful_shutdown
348
+ if ssl_keyfile:
349
+ uvicorn_config["ssl_keyfile"] = ssl_keyfile
350
+ if ssl_certfile:
351
+ uvicorn_config["ssl_certfile"] = ssl_certfile
352
+ if ssl_keyfile_password:
353
+ uvicorn_config["ssl_keyfile_password"] = ssl_keyfile_password
354
+ if ssl_ca_certs:
355
+ uvicorn_config["ssl_ca_certs"] = ssl_ca_certs
356
+ if forwarded_allow_ips:
357
+ uvicorn_config["forwarded_allow_ips"] = forwarded_allow_ips
358
+ if h11_max_incomplete_event_size:
359
+ uvicorn_config["h11_max_incomplete_event_size"] = h11_max_incomplete_event_size
360
+
361
+ # Protocol options (only if not auto)
362
+ if loop != LoopType.auto:
363
+ uvicorn_config["loop"] = loop.value
364
+ if http != HttpProtocol.auto:
365
+ uvicorn_config["http"] = http.value
366
+ if ws != WsProtocol.auto:
367
+ uvicorn_config["ws"] = ws.value if ws != WsProtocol.none else None
368
+ if interface != InterfaceType.auto:
369
+ uvicorn_config["interface"] = interface.value
370
+
371
+ console.print(
372
+ f"\n[bold green]Starting server at http://{host}:{port}[/]" f"{prefix or ''}"
373
+ )
374
+ console.print("[dim]Press CTRL+C to stop[/]\n")
375
+
376
+ # Run the server
377
+ uvicorn.run(fastapi_app, **uvicorn_config)
378
+
379
+
380
+ @app.command()
381
+ def serve_gunicorn(
382
+ celery_app: Annotated[
383
+ str,
384
+ typer.Argument(help="Path to Celery app (e.g., 'myapp.celery:app')"),
385
+ ],
386
+ # Binding options
387
+ bind: Annotated[
388
+ str,
389
+ typer.Option(
390
+ "--bind", "-b", help="Address to bind to (host:port or unix socket)"
391
+ ),
392
+ ] = "0.0.0.0:8000",
393
+ # Worker options
394
+ workers: Annotated[
395
+ int,
396
+ typer.Option("--workers", "-w", help="Number of worker processes"),
397
+ ] = 1,
398
+ worker_class: Annotated[
399
+ str,
400
+ typer.Option(
401
+ "--worker-class",
402
+ "-k",
403
+ help="Worker class (uvicorn.workers.UvicornWorker, gevent, eventlet, etc.)",
404
+ ),
405
+ ] = "uvicorn.workers.UvicornWorker",
406
+ threads: Annotated[
407
+ int,
408
+ typer.Option(
409
+ "--threads", help="Number of threads per worker (for sync workers)"
410
+ ),
411
+ ] = 1,
412
+ worker_connections: Annotated[
413
+ int,
414
+ typer.Option(
415
+ "--worker-connections", help="Max simultaneous clients per worker"
416
+ ),
417
+ ] = 1000,
418
+ max_requests: Annotated[
419
+ int,
420
+ typer.Option(
421
+ "--max-requests", help="Max requests before worker restart (0=disabled)"
422
+ ),
423
+ ] = 0,
424
+ max_requests_jitter: Annotated[
425
+ int,
426
+ typer.Option("--max-requests-jitter", help="Max jitter to add to max-requests"),
427
+ ] = 0,
428
+ # Timeout options
429
+ timeout: Annotated[
430
+ int,
431
+ typer.Option("--timeout", "-t", help="Worker timeout in seconds"),
432
+ ] = 30,
433
+ graceful_timeout: Annotated[
434
+ int,
435
+ typer.Option("--graceful-timeout", help="Graceful worker timeout"),
436
+ ] = 30,
437
+ keepalive: Annotated[
438
+ int,
439
+ typer.Option("--keep-alive", help="Keep-alive timeout in seconds"),
440
+ ] = 2,
441
+ # Logging options
442
+ log_level: Annotated[
443
+ LogLevel,
444
+ typer.Option("--log-level", "-l", help="Logging level"),
445
+ ] = LogLevel.info,
446
+ access_log: Annotated[
447
+ str | None,
448
+ typer.Option("--access-logfile", help="Access log file path (- for stdout)"),
449
+ ] = "-",
450
+ error_log: Annotated[
451
+ str | None,
452
+ typer.Option("--error-logfile", help="Error log file path (- for stderr)"),
453
+ ] = "-",
454
+ capture_output: Annotated[
455
+ bool,
456
+ typer.Option(
457
+ "--capture-output/--no-capture-output", help="Capture stdout/stderr"
458
+ ),
459
+ ] = False,
460
+ # Process options
461
+ daemon: Annotated[
462
+ bool,
463
+ typer.Option("--daemon", "-D", help="Daemonize the process"),
464
+ ] = False,
465
+ pidfile: Annotated[
466
+ str | None,
467
+ typer.Option("--pid", help="PID file path"),
468
+ ] = None,
469
+ user: Annotated[
470
+ str | None,
471
+ typer.Option(
472
+ "--user", "-u", help="Switch worker processes to run as this user"
473
+ ),
474
+ ] = None,
475
+ group: Annotated[
476
+ str | None,
477
+ typer.Option(
478
+ "--group", "-g", help="Switch worker processes to run as this group"
479
+ ),
480
+ ] = None,
481
+ umask: Annotated[
482
+ int,
483
+ typer.Option("--umask", help="File mode creation mask"),
484
+ ] = 0,
485
+ # SSL options
486
+ keyfile: Annotated[
487
+ str | None,
488
+ typer.Option("--keyfile", help="SSL key file path"),
489
+ ] = None,
490
+ certfile: Annotated[
491
+ str | None,
492
+ typer.Option("--certfile", help="SSL certificate file path"),
493
+ ] = None,
494
+ ca_certs: Annotated[
495
+ str | None,
496
+ typer.Option("--ca-certs", help="CA certificates file"),
497
+ ] = None,
498
+ # Misc options
499
+ preload: Annotated[
500
+ bool,
501
+ typer.Option("--preload/--no-preload", help="Preload application code"),
502
+ ] = False,
503
+ reload: Annotated[
504
+ bool,
505
+ typer.Option("--reload", "-r", help="Enable auto-reload for development"),
506
+ ] = False,
507
+ chdir: Annotated[
508
+ str | None,
509
+ typer.Option("--chdir", help="Change to this directory before loading app"),
510
+ ] = None,
511
+ # FastAPI options
512
+ prefix: Annotated[
513
+ str,
514
+ typer.Option("--prefix", help="URL prefix for all endpoints"),
515
+ ] = "",
516
+ ) -> None:
517
+ """
518
+ Start the FastAPI server using Gunicorn with uvicorn workers.
519
+
520
+ This is recommended for production deployments. Supports all Gunicorn options
521
+ including worker management, SSL, logging, and process control.
522
+
523
+ Examples:
524
+ # Basic production setup
525
+ celery-fastapi serve-gunicorn myapp.celery:app -w 4
526
+
527
+ # With gevent workers
528
+ celery-fastapi serve-gunicorn myapp.celery:app -w 4 -k gevent
529
+
530
+ # As a daemon
531
+ celery-fastapi serve-gunicorn myapp.celery:app -w 4 -D --pid /var/run/celery-fastapi.pid
532
+ """
533
+ try:
534
+ import gunicorn.app.base # noqa: F401
535
+ except ImportError:
536
+ console.print(
537
+ "[red]Error:[/] gunicorn not installed. "
538
+ "Install with: pip install celery-fastapi[gunicorn]"
539
+ )
540
+ raise typer.Exit(1)
541
+
542
+ from celery_fastapi.app import create_app, load_celery_app
543
+ from celery_fastapi.server import GunicornApplication
544
+
545
+ console.print(f"[bold blue]Loading Celery app from:[/] {celery_app}")
546
+
547
+ try:
548
+ celery_instance = load_celery_app(celery_app)
549
+ except Exception as e:
550
+ console.print(f"[red]Error loading Celery app:[/] {e}")
551
+ raise typer.Exit(1)
552
+
553
+ console.print(f"[green]✓[/] Loaded Celery app: [bold]{celery_instance.main}[/]")
554
+
555
+ # Create the FastAPI app
556
+ fastapi_app = create_app(
557
+ celery_instance,
558
+ title=f"Celery FastAPI - {celery_instance.main}",
559
+ prefix=prefix,
560
+ )
561
+
562
+ # Show registered routes
563
+ bridge = fastapi_app.state.celery_bridge
564
+ routes = bridge.get_registered_routes()
565
+
566
+ if routes:
567
+ table = Table(title="Registered Endpoints")
568
+ table.add_column("Method", style="cyan")
569
+ table.add_column("Path", style="green")
570
+
571
+ for route in routes:
572
+ table.add_row(route["method"], route["path"])
573
+
574
+ console.print(table)
575
+
576
+ # Build gunicorn options
577
+ options: dict[str, Any] = {
578
+ "bind": bind,
579
+ "workers": workers,
580
+ "worker_class": worker_class,
581
+ "threads": threads,
582
+ "worker_connections": worker_connections,
583
+ "max_requests": max_requests,
584
+ "max_requests_jitter": max_requests_jitter,
585
+ "timeout": timeout,
586
+ "graceful_timeout": graceful_timeout,
587
+ "keepalive": keepalive,
588
+ "loglevel": log_level.value,
589
+ "capture_output": capture_output,
590
+ "daemon": daemon,
591
+ "umask": umask,
592
+ "preload_app": preload,
593
+ "reload": reload,
594
+ }
595
+
596
+ if access_log:
597
+ options["accesslog"] = access_log
598
+ if error_log:
599
+ options["errorlog"] = error_log
600
+ if pidfile:
601
+ options["pidfile"] = pidfile
602
+ if user:
603
+ options["user"] = user
604
+ if group:
605
+ options["group"] = group
606
+ if keyfile:
607
+ options["keyfile"] = keyfile
608
+ if certfile:
609
+ options["certfile"] = certfile
610
+ if ca_certs:
611
+ options["ca_certs"] = ca_certs
612
+ if chdir:
613
+ options["chdir"] = chdir
614
+
615
+ console.print(f"\n[bold green]Starting Gunicorn server at {bind}[/]")
616
+ console.print("[dim]Press CTRL+C to stop[/]\n")
617
+
618
+ # Run with gunicorn
619
+ GunicornApplication(fastapi_app, options).run()
620
+
621
+
622
+ @app.command()
623
+ def routes(
624
+ celery_app: Annotated[
625
+ str,
626
+ typer.Argument(help="Path to Celery app (e.g., 'myapp.celery:app')"),
627
+ ],
628
+ prefix: Annotated[
629
+ str,
630
+ typer.Option("--prefix", help="URL prefix for all endpoints"),
631
+ ] = "",
632
+ ) -> None:
633
+ """
634
+ List all routes that would be generated for the Celery app.
635
+
636
+ Example:
637
+ celery-fastapi routes myapp.celery:app
638
+ """
639
+ from celery_fastapi.app import create_app, load_celery_app
640
+
641
+ try:
642
+ celery_instance = load_celery_app(celery_app)
643
+ except Exception as e:
644
+ console.print(f"[red]Error loading Celery app:[/] {e}")
645
+ raise typer.Exit(1)
646
+
647
+ fastapi_app = create_app(celery_instance, prefix=prefix)
648
+ bridge = fastapi_app.state.celery_bridge
649
+ routes_list = bridge.get_registered_routes()
650
+
651
+ if not routes_list:
652
+ console.print("[yellow]No routes found.[/]")
653
+ console.print(
654
+ "[dim]Make sure your Celery tasks are registered before importing.[/]"
655
+ )
656
+ raise typer.Exit()
657
+
658
+ table = Table(title=f"Routes for {celery_instance.main}")
659
+ table.add_column("Method", style="cyan", width=8)
660
+ table.add_column("Path", style="green")
661
+
662
+ for route in routes_list:
663
+ table.add_row(route["method"], route["path"])
664
+
665
+ console.print(table)
666
+ console.print(f"\n[dim]Total routes: {len(routes_list)}[/]")
667
+
668
+
669
+ @app.command()
670
+ def tasks(
671
+ celery_app: Annotated[
672
+ str,
673
+ typer.Argument(help="Path to Celery app (e.g., 'myapp.celery:app')"),
674
+ ],
675
+ ) -> None:
676
+ """
677
+ List all registered Celery tasks.
678
+
679
+ Example:
680
+ celery-fastapi tasks myapp.celery:app
681
+ """
682
+ from celery_fastapi.app import load_celery_app
683
+
684
+ try:
685
+ celery_instance = load_celery_app(celery_app)
686
+ except Exception as e:
687
+ console.print(f"[red]Error loading Celery app:[/] {e}")
688
+ raise typer.Exit(1)
689
+
690
+ table = Table(title=f"Tasks in {celery_instance.main}")
691
+ table.add_column("Task Name", style="green")
692
+ table.add_column("Queue", style="cyan")
693
+
694
+ task_count = 0
695
+ for name, task in celery_instance.tasks.items():
696
+ if name.startswith("celery."):
697
+ continue
698
+ queue = getattr(task, "queue", None) or "default"
699
+ table.add_row(name, queue)
700
+ task_count += 1
701
+
702
+ if task_count == 0:
703
+ console.print("[yellow]No custom tasks found.[/]")
704
+ console.print("[dim]Make sure your tasks are registered before importing.[/]")
705
+ else:
706
+ console.print(table)
707
+ console.print(f"\n[dim]Total tasks: {task_count}[/]")
708
+
709
+
710
+ @app.command()
711
+ def workers(
712
+ celery_app: Annotated[
713
+ str,
714
+ typer.Argument(help="Path to Celery app (e.g., 'myapp.celery:app')"),
715
+ ],
716
+ ) -> None:
717
+ """
718
+ Show information about active Celery workers.
719
+
720
+ Example:
721
+ celery-fastapi workers myapp.celery:app
722
+ """
723
+ from celery_fastapi.app import load_celery_app
724
+
725
+ try:
726
+ celery_instance = load_celery_app(celery_app)
727
+ except Exception as e:
728
+ console.print(f"[red]Error loading Celery app:[/] {e}")
729
+ raise typer.Exit(1)
730
+
731
+ inspector = celery_instance.control.inspect()
732
+
733
+ # Ping workers
734
+ ping_result = inspector.ping()
735
+ if not ping_result:
736
+ console.print("[yellow]No active workers found.[/]")
737
+ raise typer.Exit()
738
+
739
+ # Show workers
740
+ table = Table(title="Active Workers")
741
+ table.add_column("Worker", style="green")
742
+ table.add_column("Status", style="cyan")
743
+
744
+ for worker, status in ping_result.items():
745
+ table.add_row(worker, str(status))
746
+
747
+ console.print(table)
748
+
749
+ # Show stats
750
+ stats = inspector.stats()
751
+ if stats:
752
+ console.print("\n[bold]Worker Statistics:[/]")
753
+ for worker, stat in stats.items():
754
+ console.print(f"\n[cyan]{worker}[/]:")
755
+ console.print(
756
+ f" Pool: {stat.get('pool', {}).get('implementation', 'N/A')}"
757
+ )
758
+ console.print(
759
+ f" Processes: {len(stat.get('pool', {}).get('processes', []))}"
760
+ )
761
+ console.print(f" Total tasks: {stat.get('total', {})}")
762
+
763
+
764
+ @app.callback()
765
+ def main(
766
+ version: Annotated[
767
+ bool | None,
768
+ typer.Option(
769
+ "--version",
770
+ "-V",
771
+ callback=version_callback,
772
+ is_eager=True,
773
+ help="Show version and exit.",
774
+ ),
775
+ ] = None,
776
+ ) -> None:
777
+ """
778
+ Celery FastAPI - Automatic REST API for Celery tasks.
779
+
780
+ Generate FastAPI endpoints for your Celery tasks automatically.
781
+ Supports uvicorn and gunicorn for production deployment.
782
+ """
783
+ pass
784
+
785
+
786
+ if __name__ == "__main__":
787
+ app()