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/__init__.py +27 -0
- celery_fastapi/app.py +154 -0
- celery_fastapi/cli.py +787 -0
- celery_fastapi/core.py +759 -0
- celery_fastapi/py.typed +1 -0
- celery_fastapi/server.py +98 -0
- celery_fastapi-0.1.0.dist-info/METADATA +410 -0
- celery_fastapi-0.1.0.dist-info/RECORD +11 -0
- celery_fastapi-0.1.0.dist-info/WHEEL +4 -0
- celery_fastapi-0.1.0.dist-info/entry_points.txt +3 -0
- celery_fastapi-0.1.0.dist-info/licenses/LICENSE +21 -0
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()
|