827 lines
28 KiB
Python
827 lines
28 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
|
|
The :program:`celery` umbrella command.
|
|
|
|
.. program:: celery
|
|
|
|
"""
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
import anyjson
|
|
import numbers
|
|
import os
|
|
import sys
|
|
|
|
from functools import partial
|
|
from importlib import import_module
|
|
|
|
from celery.five import string_t, values
|
|
from celery.platforms import EX_OK, EX_FAILURE, EX_UNAVAILABLE, EX_USAGE
|
|
from celery.utils import term
|
|
from celery.utils import text
|
|
from celery.utils.timeutils import maybe_iso8601
|
|
|
|
# Cannot use relative imports here due to a Windows issue (#1111).
|
|
from celery.bin.base import Command, Option, Extensions
|
|
|
|
# Import commands from other modules
|
|
from celery.bin.amqp import amqp
|
|
from celery.bin.beat import beat
|
|
from celery.bin.events import events
|
|
from celery.bin.graph import graph
|
|
from celery.bin.worker import worker
|
|
|
|
__all__ = ['CeleryCommand', 'main']
|
|
|
|
HELP = """
|
|
---- -- - - ---- Commands- -------------- --- ------------
|
|
|
|
{commands}
|
|
---- -- - - --------- -- - -------------- --- ------------
|
|
|
|
Type '{prog_name} <command> --help' for help using a specific command.
|
|
"""
|
|
|
|
MIGRATE_PROGRESS_FMT = """\
|
|
Migrating task {state.count}/{state.strtotal}: \
|
|
{body[task]}[{body[id]}]\
|
|
"""
|
|
|
|
DEBUG = os.environ.get('C_DEBUG', False)
|
|
|
|
command_classes = [
|
|
('Main', ['worker', 'events', 'beat', 'shell', 'multi', 'amqp'], 'green'),
|
|
('Remote Control', ['status', 'inspect', 'control'], 'blue'),
|
|
('Utils', ['purge', 'list', 'migrate', 'call', 'result', 'report'], None),
|
|
]
|
|
if DEBUG: # pragma: no cover
|
|
command_classes.append(
|
|
('Debug', ['graph'], 'red'),
|
|
)
|
|
|
|
|
|
def determine_exit_status(ret):
|
|
if isinstance(ret, numbers.Integral):
|
|
return ret
|
|
return EX_OK if ret else EX_FAILURE
|
|
|
|
|
|
def main(argv=None):
|
|
# Fix for setuptools generated scripts, so that it will
|
|
# work with multiprocessing fork emulation.
|
|
# (see multiprocessing.forking.get_preparation_data())
|
|
try:
|
|
if __name__ != '__main__': # pragma: no cover
|
|
sys.modules['__main__'] = sys.modules[__name__]
|
|
cmd = CeleryCommand()
|
|
cmd.maybe_patch_concurrency()
|
|
from billiard import freeze_support
|
|
freeze_support()
|
|
cmd.execute_from_commandline(argv)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
|
|
class multi(Command):
|
|
"""Start multiple worker instances."""
|
|
respects_app_option = False
|
|
|
|
def get_options(self):
|
|
return ()
|
|
|
|
def run_from_argv(self, prog_name, argv, command=None):
|
|
from celery.bin.multi import MultiTool
|
|
multi = MultiTool(quiet=self.quiet, no_color=self.no_color)
|
|
return multi.execute_from_commandline(
|
|
[command] + argv, prog_name,
|
|
)
|
|
|
|
|
|
class list_(Command):
|
|
"""Get info from broker.
|
|
|
|
Examples::
|
|
|
|
celery list bindings
|
|
|
|
NOTE: For RabbitMQ the management plugin is required.
|
|
"""
|
|
args = '[bindings]'
|
|
|
|
def list_bindings(self, management):
|
|
try:
|
|
bindings = management.get_bindings()
|
|
except NotImplementedError:
|
|
raise self.Error('Your transport cannot list bindings.')
|
|
|
|
fmt = lambda q, e, r: self.out('{0:<28} {1:<28} {2}'.format(q, e, r))
|
|
fmt('Queue', 'Exchange', 'Routing Key')
|
|
fmt('-' * 16, '-' * 16, '-' * 16)
|
|
for b in bindings:
|
|
fmt(b['destination'], b['source'], b['routing_key'])
|
|
|
|
def run(self, what=None, *_, **kw):
|
|
topics = {'bindings': self.list_bindings}
|
|
available = ', '.join(topics)
|
|
if not what:
|
|
raise self.UsageError(
|
|
'You must specify one of {0}'.format(available))
|
|
if what not in topics:
|
|
raise self.UsageError(
|
|
'unknown topic {0!r} (choose one of: {1})'.format(
|
|
what, available))
|
|
with self.app.connection() as conn:
|
|
self.app.amqp.TaskConsumer(conn).declare()
|
|
topics[what](conn.manager)
|
|
|
|
|
|
class call(Command):
|
|
"""Call a task by name.
|
|
|
|
Examples::
|
|
|
|
celery call tasks.add --args='[2, 2]'
|
|
celery call tasks.add --args='[2, 2]' --countdown=10
|
|
"""
|
|
args = '<task_name>'
|
|
option_list = Command.option_list + (
|
|
Option('--args', '-a', help='positional arguments (json).'),
|
|
Option('--kwargs', '-k', help='keyword arguments (json).'),
|
|
Option('--eta', help='scheduled time (ISO-8601).'),
|
|
Option('--countdown', type='float',
|
|
help='eta in seconds from now (float/int).'),
|
|
Option('--expires', help='expiry time (ISO-8601/float/int).'),
|
|
Option('--serializer', default='json', help='defaults to json.'),
|
|
Option('--queue', help='custom queue name.'),
|
|
Option('--exchange', help='custom exchange name.'),
|
|
Option('--routing-key', help='custom routing key.'),
|
|
)
|
|
|
|
def run(self, name, *_, **kw):
|
|
# Positional args.
|
|
args = kw.get('args') or ()
|
|
if isinstance(args, string_t):
|
|
args = anyjson.loads(args)
|
|
|
|
# Keyword args.
|
|
kwargs = kw.get('kwargs') or {}
|
|
if isinstance(kwargs, string_t):
|
|
kwargs = anyjson.loads(kwargs)
|
|
|
|
# Expires can be int/float.
|
|
expires = kw.get('expires') or None
|
|
try:
|
|
expires = float(expires)
|
|
except (TypeError, ValueError):
|
|
# or a string describing an ISO 8601 datetime.
|
|
try:
|
|
expires = maybe_iso8601(expires)
|
|
except (TypeError, ValueError):
|
|
raise
|
|
|
|
res = self.app.send_task(name, args=args, kwargs=kwargs,
|
|
countdown=kw.get('countdown'),
|
|
serializer=kw.get('serializer'),
|
|
queue=kw.get('queue'),
|
|
exchange=kw.get('exchange'),
|
|
routing_key=kw.get('routing_key'),
|
|
eta=maybe_iso8601(kw.get('eta')),
|
|
expires=expires)
|
|
self.out(res.id)
|
|
|
|
|
|
class purge(Command):
|
|
"""Erase all messages from all known task queues.
|
|
|
|
WARNING: There is no undo operation for this command.
|
|
|
|
"""
|
|
warn_prelude = (
|
|
'{warning}: This will remove all tasks from {queues}: {names}.\n'
|
|
' There is no undo for this operation!\n\n'
|
|
'(to skip this prompt use the -f option)\n'
|
|
)
|
|
warn_prompt = 'Are you sure you want to delete all tasks'
|
|
fmt_purged = 'Purged {mnum} {messages} from {qnum} known task {queues}.'
|
|
fmt_empty = 'No messages purged from {qnum} {queues}'
|
|
option_list = Command.option_list + (
|
|
Option('--force', '-f', action='store_true',
|
|
help='Do not prompt for verification'),
|
|
)
|
|
|
|
def run(self, force=False, **kwargs):
|
|
names = list(sorted(self.app.amqp.queues.keys()))
|
|
qnum = len(names)
|
|
if not force:
|
|
self.out(self.warn_prelude.format(
|
|
warning=self.colored.red('WARNING'),
|
|
queues=text.pluralize(qnum, 'queue'), names=', '.join(names),
|
|
))
|
|
if self.ask(self.warn_prompt, ('yes', 'no'), 'no') != 'yes':
|
|
return
|
|
messages = self.app.control.purge()
|
|
fmt = self.fmt_purged if messages else self.fmt_empty
|
|
self.out(fmt.format(
|
|
mnum=messages, qnum=qnum,
|
|
messages=text.pluralize(messages, 'message'),
|
|
queues=text.pluralize(qnum, 'queue')))
|
|
|
|
|
|
class result(Command):
|
|
"""Gives the return value for a given task id.
|
|
|
|
Examples::
|
|
|
|
celery result 8f511516-e2f5-4da4-9d2f-0fb83a86e500
|
|
celery result 8f511516-e2f5-4da4-9d2f-0fb83a86e500 -t tasks.add
|
|
celery result 8f511516-e2f5-4da4-9d2f-0fb83a86e500 --traceback
|
|
|
|
"""
|
|
args = '<task_id>'
|
|
option_list = Command.option_list + (
|
|
Option('--task', '-t', help='name of task (if custom backend)'),
|
|
Option('--traceback', action='store_true',
|
|
help='show traceback instead'),
|
|
)
|
|
|
|
def run(self, task_id, *args, **kwargs):
|
|
result_cls = self.app.AsyncResult
|
|
task = kwargs.get('task')
|
|
traceback = kwargs.get('traceback', False)
|
|
|
|
if task:
|
|
result_cls = self.app.tasks[task].AsyncResult
|
|
result = result_cls(task_id)
|
|
if traceback:
|
|
value = result.traceback
|
|
else:
|
|
value = result.get()
|
|
self.out(self.pretty(value)[1])
|
|
|
|
|
|
class _RemoteControl(Command):
|
|
name = None
|
|
choices = None
|
|
leaf = False
|
|
option_list = Command.option_list + (
|
|
Option('--timeout', '-t', type='float',
|
|
help='Timeout in seconds (float) waiting for reply'),
|
|
Option('--destination', '-d',
|
|
help='Comma separated list of destination node names.'))
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.show_body = kwargs.pop('show_body', True)
|
|
self.show_reply = kwargs.pop('show_reply', True)
|
|
super(_RemoteControl, self).__init__(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def get_command_info(self, command,
|
|
indent=0, prefix='', color=None, help=False):
|
|
if help:
|
|
help = '|' + text.indent(self.choices[command][1], indent + 4)
|
|
else:
|
|
help = None
|
|
try:
|
|
# see if it uses args.
|
|
meth = getattr(self, command)
|
|
return text.join([
|
|
'|' + text.indent('{0}{1} {2}'.format(
|
|
prefix, color(command), meth.__doc__), indent),
|
|
help,
|
|
])
|
|
|
|
except AttributeError:
|
|
return text.join([
|
|
'|' + text.indent(prefix + str(color(command)), indent), help,
|
|
])
|
|
|
|
@classmethod
|
|
def list_commands(self, indent=0, prefix='', color=None, help=False):
|
|
color = color if color else lambda x: x
|
|
prefix = prefix + ' ' if prefix else ''
|
|
return '\n'.join(self.get_command_info(c, indent, prefix, color, help)
|
|
for c in sorted(self.choices))
|
|
|
|
@property
|
|
def epilog(self):
|
|
return '\n'.join([
|
|
'[Commands]',
|
|
self.list_commands(indent=4, help=True)
|
|
])
|
|
|
|
def usage(self, command):
|
|
return '%prog {0} [options] {1} <command> [arg1 .. argN]'.format(
|
|
command, self.args)
|
|
|
|
def call(self, *args, **kwargs):
|
|
raise NotImplementedError('call')
|
|
|
|
def run(self, *args, **kwargs):
|
|
if not args:
|
|
raise self.UsageError(
|
|
'Missing {0.name} method. See --help'.format(self))
|
|
return self.do_call_method(args, **kwargs)
|
|
|
|
def do_call_method(self, args, **kwargs):
|
|
method = args[0]
|
|
if method == 'help':
|
|
raise self.Error("Did you mean '{0.name} --help'?".format(self))
|
|
if method not in self.choices:
|
|
raise self.UsageError(
|
|
'Unknown {0.name} method {1}'.format(self, method))
|
|
|
|
if self.app.connection().transport.driver_type == 'sql':
|
|
raise self.Error('Broadcast not supported by SQL broker transport')
|
|
|
|
destination = kwargs.get('destination')
|
|
timeout = kwargs.get('timeout') or self.choices[method][0]
|
|
if destination and isinstance(destination, string_t):
|
|
destination = [dest.strip() for dest in destination.split(',')]
|
|
|
|
handler = getattr(self, method, self.call)
|
|
|
|
replies = handler(method, *args[1:], timeout=timeout,
|
|
destination=destination,
|
|
callback=self.say_remote_command_reply)
|
|
if not replies:
|
|
raise self.Error('No nodes replied within time constraint.',
|
|
status=EX_UNAVAILABLE)
|
|
return replies
|
|
|
|
|
|
class inspect(_RemoteControl):
|
|
"""Inspect the worker at runtime.
|
|
|
|
Availability: RabbitMQ (amqp), Redis, and MongoDB transports.
|
|
|
|
Examples::
|
|
|
|
celery inspect active --timeout=5
|
|
celery inspect scheduled -d worker1@example.com
|
|
celery inspect revoked -d w1@e.com,w2@e.com
|
|
|
|
"""
|
|
name = 'inspect'
|
|
choices = {
|
|
'active': (1.0, 'dump active tasks (being processed)'),
|
|
'active_queues': (1.0, 'dump queues being consumed from'),
|
|
'scheduled': (1.0, 'dump scheduled tasks (eta/countdown/retry)'),
|
|
'reserved': (1.0, 'dump reserved tasks (waiting to be processed)'),
|
|
'stats': (1.0, 'dump worker statistics'),
|
|
'revoked': (1.0, 'dump of revoked task ids'),
|
|
'registered': (1.0, 'dump of registered tasks'),
|
|
'ping': (0.2, 'ping worker(s)'),
|
|
'clock': (1.0, 'get value of logical clock'),
|
|
'conf': (1.0, 'dump worker configuration'),
|
|
'report': (1.0, 'get bugreport info'),
|
|
'memsample': (1.0, 'sample memory (requires psutil)'),
|
|
'memdump': (1.0, 'dump memory samples (requires psutil)'),
|
|
'objgraph': (60.0, 'create object graph (requires objgraph)'),
|
|
}
|
|
|
|
def call(self, method, *args, **options):
|
|
i = self.app.control.inspect(**options)
|
|
return getattr(i, method)(*args)
|
|
|
|
def objgraph(self, type_='Request', *args, **kwargs):
|
|
return self.call('objgraph', type_, **kwargs)
|
|
|
|
def conf(self, with_defaults=False, *args, **kwargs):
|
|
return self.call('conf', with_defaults, **kwargs)
|
|
|
|
|
|
class control(_RemoteControl):
|
|
"""Workers remote control.
|
|
|
|
Availability: RabbitMQ (amqp), Redis, and MongoDB transports.
|
|
|
|
Examples::
|
|
|
|
celery control enable_events --timeout=5
|
|
celery control -d worker1@example.com enable_events
|
|
celery control -d w1.e.com,w2.e.com enable_events
|
|
|
|
celery control -d w1.e.com add_consumer queue_name
|
|
celery control -d w1.e.com cancel_consumer queue_name
|
|
|
|
celery control -d w1.e.com add_consumer queue exchange direct rkey
|
|
|
|
"""
|
|
name = 'control'
|
|
choices = {
|
|
'enable_events': (1.0, 'tell worker(s) to enable events'),
|
|
'disable_events': (1.0, 'tell worker(s) to disable events'),
|
|
'add_consumer': (1.0, 'tell worker(s) to start consuming a queue'),
|
|
'cancel_consumer': (1.0, 'tell worker(s) to stop consuming a queue'),
|
|
'rate_limit': (
|
|
1.0, 'tell worker(s) to modify the rate limit for a task type'),
|
|
'time_limit': (
|
|
1.0, 'tell worker(s) to modify the time limit for a task type.'),
|
|
'autoscale': (1.0, 'change autoscale settings'),
|
|
'pool_grow': (1.0, 'start more pool processes'),
|
|
'pool_shrink': (1.0, 'use less pool processes'),
|
|
}
|
|
|
|
def call(self, method, *args, **options):
|
|
return getattr(self.app.control, method)(*args, reply=True, **options)
|
|
|
|
def pool_grow(self, method, n=1, **kwargs):
|
|
"""[N=1]"""
|
|
return self.call(method, int(n), **kwargs)
|
|
|
|
def pool_shrink(self, method, n=1, **kwargs):
|
|
"""[N=1]"""
|
|
return self.call(method, int(n), **kwargs)
|
|
|
|
def autoscale(self, method, max=None, min=None, **kwargs):
|
|
"""[max] [min]"""
|
|
return self.call(method, int(max), int(min), **kwargs)
|
|
|
|
def rate_limit(self, method, task_name, rate_limit, **kwargs):
|
|
"""<task_name> <rate_limit> (e.g. 5/s | 5/m | 5/h)>"""
|
|
return self.call(method, task_name, rate_limit, **kwargs)
|
|
|
|
def time_limit(self, method, task_name, soft, hard=None, **kwargs):
|
|
"""<task_name> <soft_secs> [hard_secs]"""
|
|
return self.call(method, task_name,
|
|
float(soft), float(hard), **kwargs)
|
|
|
|
def add_consumer(self, method, queue, exchange=None,
|
|
exchange_type='direct', routing_key=None, **kwargs):
|
|
"""<queue> [exchange [type [routing_key]]]"""
|
|
return self.call(method, queue, exchange,
|
|
exchange_type, routing_key, **kwargs)
|
|
|
|
def cancel_consumer(self, method, queue, **kwargs):
|
|
"""<queue>"""
|
|
return self.call(method, queue, **kwargs)
|
|
|
|
|
|
class status(Command):
|
|
"""Show list of workers that are online."""
|
|
option_list = inspect.option_list
|
|
|
|
def run(self, *args, **kwargs):
|
|
I = inspect(
|
|
app=self.app,
|
|
no_color=kwargs.get('no_color', False),
|
|
stdout=self.stdout, stderr=self.stderr,
|
|
show_reply=False, show_body=False, quiet=True,
|
|
)
|
|
replies = I.run('ping', **kwargs)
|
|
if not replies:
|
|
raise self.Error('No nodes replied within time constraint',
|
|
status=EX_UNAVAILABLE)
|
|
nodecount = len(replies)
|
|
if not kwargs.get('quiet', False):
|
|
self.out('\n{0} {1} online.'.format(
|
|
nodecount, text.pluralize(nodecount, 'node')))
|
|
|
|
|
|
class migrate(Command):
|
|
"""Migrate tasks from one broker to another.
|
|
|
|
Examples::
|
|
|
|
celery migrate redis://localhost amqp://guest@localhost//
|
|
celery migrate django:// redis://localhost
|
|
|
|
NOTE: This command is experimental, make sure you have
|
|
a backup of the tasks before you continue.
|
|
"""
|
|
args = '<source_url> <dest_url>'
|
|
option_list = Command.option_list + (
|
|
Option('--limit', '-n', type='int',
|
|
help='Number of tasks to consume (int)'),
|
|
Option('--timeout', '-t', type='float', default=1.0,
|
|
help='Timeout in seconds (float) waiting for tasks'),
|
|
Option('--ack-messages', '-a', action='store_true',
|
|
help='Ack messages from source broker.'),
|
|
Option('--tasks', '-T',
|
|
help='List of task names to filter on.'),
|
|
Option('--queues', '-Q',
|
|
help='List of queues to migrate.'),
|
|
Option('--forever', '-F', action='store_true',
|
|
help='Continually migrate tasks until killed.'),
|
|
)
|
|
progress_fmt = MIGRATE_PROGRESS_FMT
|
|
|
|
def on_migrate_task(self, state, body, message):
|
|
self.out(self.progress_fmt.format(state=state, body=body))
|
|
|
|
def run(self, source, destination, **kwargs):
|
|
from kombu import Connection
|
|
from celery.contrib.migrate import migrate_tasks
|
|
|
|
migrate_tasks(Connection(source),
|
|
Connection(destination),
|
|
callback=self.on_migrate_task,
|
|
**kwargs)
|
|
|
|
|
|
class shell(Command): # pragma: no cover
|
|
"""Start shell session with convenient access to celery symbols.
|
|
|
|
The following symbols will be added to the main globals:
|
|
|
|
- celery: the current application.
|
|
- chord, group, chain, chunks,
|
|
xmap, xstarmap subtask, Task
|
|
- all registered tasks.
|
|
|
|
"""
|
|
option_list = Command.option_list + (
|
|
Option('--ipython', '-I',
|
|
action='store_true', dest='force_ipython',
|
|
help='force iPython.'),
|
|
Option('--bpython', '-B',
|
|
action='store_true', dest='force_bpython',
|
|
help='force bpython.'),
|
|
Option('--python', '-P',
|
|
action='store_true', dest='force_python',
|
|
help='force default Python shell.'),
|
|
Option('--without-tasks', '-T', action='store_true',
|
|
help="don't add tasks to locals."),
|
|
Option('--eventlet', action='store_true',
|
|
help='use eventlet.'),
|
|
Option('--gevent', action='store_true', help='use gevent.'),
|
|
)
|
|
|
|
def run(self, force_ipython=False, force_bpython=False,
|
|
force_python=False, without_tasks=False, eventlet=False,
|
|
gevent=False, **kwargs):
|
|
sys.path.insert(0, os.getcwd())
|
|
if eventlet:
|
|
import_module('celery.concurrency.eventlet')
|
|
if gevent:
|
|
import_module('celery.concurrency.gevent')
|
|
import celery
|
|
import celery.task.base
|
|
self.app.loader.import_default_modules()
|
|
self.locals = {'app': self.app,
|
|
'celery': self.app,
|
|
'Task': celery.Task,
|
|
'chord': celery.chord,
|
|
'group': celery.group,
|
|
'chain': celery.chain,
|
|
'chunks': celery.chunks,
|
|
'xmap': celery.xmap,
|
|
'xstarmap': celery.xstarmap,
|
|
'subtask': celery.subtask,
|
|
'signature': celery.signature}
|
|
|
|
if not without_tasks:
|
|
self.locals.update(dict(
|
|
(task.__name__, task) for task in values(self.app.tasks)
|
|
if not task.name.startswith('celery.')),
|
|
)
|
|
|
|
if force_python:
|
|
return self.invoke_fallback_shell()
|
|
elif force_bpython:
|
|
return self.invoke_bpython_shell()
|
|
elif force_ipython:
|
|
return self.invoke_ipython_shell()
|
|
return self.invoke_default_shell()
|
|
|
|
def invoke_default_shell(self):
|
|
try:
|
|
import IPython # noqa
|
|
except ImportError:
|
|
try:
|
|
import bpython # noqa
|
|
except ImportError:
|
|
return self.invoke_fallback_shell()
|
|
else:
|
|
return self.invoke_bpython_shell()
|
|
else:
|
|
return self.invoke_ipython_shell()
|
|
|
|
def invoke_fallback_shell(self):
|
|
import code
|
|
try:
|
|
import readline
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
import rlcompleter
|
|
readline.set_completer(
|
|
rlcompleter.Completer(self.locals).complete)
|
|
readline.parse_and_bind('tab:complete')
|
|
code.interact(local=self.locals)
|
|
|
|
def invoke_ipython_shell(self):
|
|
try:
|
|
from IPython.terminal import embed
|
|
embed.TerminalInteractiveShell(user_ns=self.locals).mainloop()
|
|
except ImportError: # ipython < 0.11
|
|
from IPython.Shell import IPShell
|
|
IPShell(argv=[], user_ns=self.locals).mainloop()
|
|
|
|
def invoke_bpython_shell(self):
|
|
import bpython
|
|
bpython.embed(self.locals)
|
|
|
|
|
|
class help(Command):
|
|
"""Show help screen and exit."""
|
|
|
|
def usage(self, command):
|
|
return '%prog <command> [options] {0.args}'.format(self)
|
|
|
|
def run(self, *args, **kwargs):
|
|
self.parser.print_help()
|
|
self.out(HELP.format(
|
|
prog_name=self.prog_name,
|
|
commands=CeleryCommand.list_commands(colored=self.colored),
|
|
))
|
|
|
|
return EX_USAGE
|
|
|
|
|
|
class report(Command):
|
|
"""Shows information useful to include in bugreports."""
|
|
|
|
def run(self, *args, **kwargs):
|
|
self.out(self.app.bugreport())
|
|
return EX_OK
|
|
|
|
|
|
class CeleryCommand(Command):
|
|
namespace = 'celery'
|
|
ext_fmt = '{self.namespace}.commands'
|
|
commands = {
|
|
'amqp': amqp,
|
|
'beat': beat,
|
|
'call': call,
|
|
'control': control,
|
|
'events': events,
|
|
'graph': graph,
|
|
'help': help,
|
|
'inspect': inspect,
|
|
'list': list_,
|
|
'migrate': migrate,
|
|
'multi': multi,
|
|
'purge': purge,
|
|
'report': report,
|
|
'result': result,
|
|
'shell': shell,
|
|
'status': status,
|
|
'worker': worker,
|
|
|
|
}
|
|
enable_config_from_cmdline = True
|
|
prog_name = 'celery'
|
|
|
|
@classmethod
|
|
def register_command(cls, fun, name=None):
|
|
cls.commands[name or fun.__name__] = fun
|
|
return fun
|
|
|
|
def execute(self, command, argv=None):
|
|
try:
|
|
cls = self.commands[command]
|
|
except KeyError:
|
|
cls, argv = self.commands['help'], ['help']
|
|
cls = self.commands.get(command) or self.commands['help']
|
|
try:
|
|
return cls(
|
|
app=self.app, on_error=self.on_error,
|
|
no_color=self.no_color, quiet=self.quiet,
|
|
on_usage_error=partial(self.on_usage_error, command=command),
|
|
).run_from_argv(self.prog_name, argv[1:], command=argv[0])
|
|
except self.UsageError as exc:
|
|
self.on_usage_error(exc)
|
|
return exc.status
|
|
except self.Error as exc:
|
|
self.on_error(exc)
|
|
return exc.status
|
|
|
|
def on_usage_error(self, exc, command=None):
|
|
if command:
|
|
helps = '{self.prog_name} {command} --help'
|
|
else:
|
|
helps = '{self.prog_name} --help'
|
|
self.error(self.colored.magenta('Error: {0}'.format(exc)))
|
|
self.error("""Please try '{0}'""".format(helps.format(
|
|
self=self, command=command,
|
|
)))
|
|
|
|
def _relocate_args_from_start(self, argv, index=0):
|
|
if argv:
|
|
rest = []
|
|
while index < len(argv):
|
|
value = argv[index]
|
|
if value.startswith('--'):
|
|
rest.append(value)
|
|
elif value.startswith('-'):
|
|
# we eat the next argument even though we don't know
|
|
# if this option takes an argument or not.
|
|
# instead we will assume what is the command name in the
|
|
# return statements below.
|
|
try:
|
|
nxt = argv[index + 1]
|
|
if nxt.startswith('-'):
|
|
# is another option
|
|
rest.append(value)
|
|
else:
|
|
# is (maybe) a value for this option
|
|
rest.extend([value, nxt])
|
|
index += 1
|
|
except IndexError:
|
|
rest.append(value)
|
|
break
|
|
else:
|
|
break
|
|
index += 1
|
|
if argv[index:]:
|
|
# if there are more arguments left then divide and swap
|
|
# we assume the first argument in argv[i:] is the command
|
|
# name.
|
|
return argv[index:] + rest
|
|
# if there are no more arguments then the last arg in rest'
|
|
# must be the command.
|
|
[rest.pop()] + rest
|
|
return []
|
|
|
|
def prepare_prog_name(self, name):
|
|
if name == '__main__.py':
|
|
return sys.modules['__main__'].__file__
|
|
return name
|
|
|
|
def handle_argv(self, prog_name, argv):
|
|
self.prog_name = self.prepare_prog_name(prog_name)
|
|
argv = self._relocate_args_from_start(argv)
|
|
_, argv = self.prepare_args(None, argv)
|
|
try:
|
|
command = argv[0]
|
|
except IndexError:
|
|
command, argv = 'help', ['help']
|
|
return self.execute(command, argv)
|
|
|
|
def execute_from_commandline(self, argv=None):
|
|
argv = sys.argv if argv is None else argv
|
|
if 'multi' in argv[1:3]: # Issue 1008
|
|
self.respects_app_option = False
|
|
try:
|
|
sys.exit(determine_exit_status(
|
|
super(CeleryCommand, self).execute_from_commandline(argv)))
|
|
except KeyboardInterrupt:
|
|
sys.exit(EX_FAILURE)
|
|
|
|
@classmethod
|
|
def get_command_info(self, command, indent=0, color=None, colored=None):
|
|
colored = term.colored() if colored is None else colored
|
|
colored = colored.names[color] if color else lambda x: x
|
|
obj = self.commands[command]
|
|
cmd = 'celery {0}'.format(colored(command))
|
|
if obj.leaf:
|
|
return '|' + text.indent(cmd, indent)
|
|
return text.join([
|
|
' ',
|
|
'|' + text.indent('{0} --help'.format(cmd), indent),
|
|
obj.list_commands(indent, 'celery {0}'.format(command), colored),
|
|
])
|
|
|
|
@classmethod
|
|
def list_commands(self, indent=0, colored=None):
|
|
colored = term.colored() if colored is None else colored
|
|
white = colored.white
|
|
ret = []
|
|
for cls, commands, color in command_classes:
|
|
ret.extend([
|
|
text.indent('+ {0}: '.format(white(cls)), indent),
|
|
'\n'.join(
|
|
self.get_command_info(command, indent + 4, color, colored)
|
|
for command in commands),
|
|
''
|
|
])
|
|
return '\n'.join(ret).strip()
|
|
|
|
def with_pool_option(self, argv):
|
|
if len(argv) > 1 and 'worker' in argv[0:3]:
|
|
# this command supports custom pools
|
|
# that may have to be loaded as early as possible.
|
|
return (['-P'], ['--pool'])
|
|
|
|
def on_concurrency_setup(self):
|
|
self.load_extension_commands()
|
|
|
|
def load_extension_commands(self):
|
|
names = Extensions(self.ext_fmt.format(self=self),
|
|
self.register_command).load()
|
|
if names:
|
|
command_classes.append(('Extensions', names, 'magenta'))
|
|
|
|
|
|
def command(*args, **kwargs):
|
|
"""Deprecated: Use classmethod :meth:`CeleryCommand.register_command`
|
|
instead."""
|
|
_register = CeleryCommand.register_command
|
|
return _register(args[0]) if args else _register
|
|
|
|
|
|
if __name__ == '__main__': # pragma: no cover
|
|
main()
|