cl-0.0.3/0000755000076500001200000000000011665173071012520 5ustar asksoladmin00000000000000cl-0.0.3/AUTHORS0000644000076500001200000000021311622005343013551 0ustar asksoladmin00000000000000================================== AUTHORS (in chronological order) ================================== Ask Solem cl-0.0.3/Changelog0000644000076500001200000000017211622005363014321 0ustar asksoladmin00000000000000================ Change history ================ .. _version-0.0.1: 0.0.1 ===== :release-date: TBA * Initial version. cl-0.0.3/cl/0000755000076500001200000000000011665173071013116 5ustar asksoladmin00000000000000cl-0.0.3/cl/__init__.py0000600000076500000240000000355311665173053015243 0ustar asksolstaff00000000000000"""Kombu actor framework""" from __future__ import absolute_import VERSION = (0, 0, 3) __version__ = ".".join(map(str, VERSION[0:3])) + "".join(VERSION[3:]) __author__ = "Ask Solem" __contact__ = "ask@rabbitmq.com" __homepage__ = "http://github.com/ask/cl/" __docformat__ = "restructuredtext en" # -eof meta- import sys # Lazy loading. # - See werkzeug/__init__.py for the rationale behind this. from types import ModuleType all_by_module = { "cl.actors": ["Actor"], "cl.agents": ["Agent"], } object_origins = {} for module, items in all_by_module.iteritems(): for item in items: object_origins[item] = module class module(ModuleType): def __getattr__(self, name): if name in object_origins: module = __import__(object_origins[name], None, None, [name]) for extra_name in all_by_module[module.__name__]: setattr(self, extra_name, getattr(module, extra_name)) return getattr(module, name) return ModuleType.__getattribute__(self, name) def __dir__(self): result = list(new_module.__all__) result.extend(("__file__", "__path__", "__doc__", "__all__", "__docformat__", "__name__", "__path__", "VERSION", "__package__", "__version__", "__author__", "__contact__", "__homepage__", "__docformat__")) return result # keep a reference to this module so that it's not garbage collected old_module = sys.modules[__name__] new_module = sys.modules[__name__] = module(__name__) new_module.__dict__.update({ "__file__": __file__, "__path__": __path__, "__doc__": __doc__, "__all__": tuple(object_origins), "__version__": __version__, "__author__": __author__, "__contact__": __contact__, "__homepage__": __homepage__, "__docformat__": __docformat__, "VERSION": VERSION}) cl-0.0.3/cl/actors.py0000644000076500001200000004036511647550131014767 0ustar asksoladmin00000000000000"""cl.actors""" from __future__ import absolute_import, with_statement import sys import traceback from itertools import count from operator import itemgetter from kombu import Consumer, Exchange, Queue from kombu.common import (collect_replies, ipublish, isend_reply, maybe_declare, uuid) from kombu.log import Log from kombu.pools import producers from kombu.utils import kwdict, reprcall, reprkwargs from kombu.utils.encoding import safe_repr from . import __version__ from . import exceptions from .results import AsyncResult from .utils import cached_property, shortuuid __all__ = ["Actor"] builtin_fields = {"ver": __version__} class ActorType(type): def __repr__(self): name = self.name if not name: try: name = self.__name__ except AttributeError: name = self.__class__.__name__ return "<@actor: %s>" % (name, ) class Actor(object): __metaclass__ = ActorType AsyncResult = AsyncResult Error = exceptions.clError Next = exceptions.Next NoReplyError = exceptions.NoReplyError NoRouteError = exceptions.NoRouteError NotBoundError = exceptions.NotBoundError #: Actor name. #: Defaults to the defined class name. name = None #: Default exchange used for messages to this actor. exchange = None #: Default routing key used if no ``to`` argument passed. default_routing_key = None #: Delivery mode: persistent or transient. Default is persistent. delivery_mode = "persistent" #: Set to True to disable acks. no_ack = False #: List of calling types this actor should handle. #: Valid types are: #: #: * direct #: Send the message directly to an agent by exact routing key. #: * round-robin #: Send the message to an agent by round-robin. #: * scatter #: Send the message to all of the agents (broadcast). types = ("direct", ) #: Default serializer used to send messages and reply messages. serializer = "json" #: Default timeout in seconds as a float which after #: we give up waiting for replies. default_timeout = 10.0 #: Time in seconds as a float which after replies expires. reply_expires = 100.0 #: Exchanged used for replies. reply_exchange = Exchange("cl.reply", "direct") #: Should we retry publishing messages by default? #: Default: NO retry = None #: Default policy used when retrying publishing messages. #: see :meth:`kombu.BrokerConnection.ensure` for a list #: of supported keys. retry_policy = {"max_retries": 100, "interval_start": 0, "interval_max": 1, "interval_step": 0.2} #: returns the next anonymous ticket number #: used for identifying related logs. next_anon_ticket = count(1).next #: Additional fields added to reply messages by default. default_fields = {} #: Map of calling types and their special routing keys. type_to_rkey = {"rr": "__rr__", "round-robin": "__rr__", "scatter": "__scatter__"} meta = {} class state: pass def __init__(self, connection=None, id=None, name=None, exchange=None, logger=None, agent=None, **kwargs): self.connection = connection self.id = id or uuid() self.name = name or self.name or self.__class__.__name__ self.exchange = exchange or self.exchange self.agent = agent self.type_to_queue = {"direct": self.get_direct_queue, "round-robin": self.get_rr_queue, "scatter": self.get_scatter_queue} if self.default_fields is None: self.default_fields = {} if not self.exchange: self.exchange = Exchange("cl.%s" % (self.name, ), "direct", auto_delete=True) logger_name = self.name if self.agent: logger_name = "%s#%s" % (self.name, shortuuid(self.agent.id, )) self.log = Log("!<%s>" % (logger_name, ), logger=logger) self.state = self.contribute_to_state(self.construct_state()) self.setup() def setup(self): pass def construct_state(self): """Instantiates the state class of this actor.""" return self.state() def maybe_setattr(self, obj, attr, value): if not hasattr(obj, attr): setattr(obj, attr, value) def on_agent_ready(self): pass def contribute_to_object(self, obj, map): for attr, value in map.iteritems(): self.maybe_setattr(obj, attr, value) return obj def contribute_to_state(self, state): try: contribute = state.contribute_to_state except AttributeError: return self.contribute_to_object(state, { "actor": self, "agent": self.agent, "log": self.log, "Next": self.Next, "NoRouteError": self.NoRouteError, "NoReplyError": self.NoReplyError}) else: return contribute(self) def send(self, method, args={}, to=None, nowait=False, **kwargs): """Call method on agent listening to ``routing_key``. See :meth:`call_or_cast` for a full list of supported arguments. If the keyword argument `nowait` is false (default) it will block and return the reply. """ if to is None: to = self.routing_key r = self.call_or_cast(method, args, routing_key=to, nowait=nowait, **kwargs) if not nowait: return r.get() def throw(self, method, args={}, nowait=False, **kwargs): """Call method on one of the agents in round robin. See :meth:`call_or_cast` for a full list of supported arguments. If the keyword argument `nowait` is false (default) it will block and return the reply. """ r = self.call_or_cast(method, args, type="round-robin", nowait=nowait, **kwargs) if not nowait: return r.get() def scatter(self, method, args={}, nowait=False, **kwargs): """Broadcast method to all agents. In this context the reply limit is disabled, and the timeout is set to 1 by default, which means we collect all the replies that managed to be sent within the requested timeout. See :meth:`call_or_cast` for a full list of supported arguments. If the keyword argument `nowait` is false (default) it will block and return the replies. """ kwargs.setdefault("timeout", 2) r = self.call_or_cast(method, args, type="scatter", nowait=nowait, **kwargs) if not nowait: return r.gather(**kwargs) def get_default_scatter_limit(self): if self.agent: return self.agent.get_default_scatter_limit(self.name) return None def call_or_cast(self, method, args={}, nowait=False, **kwargs): """Apply remote `method` asynchronously or synchronously depending on the value of `nowait`. :param method: The name of the remote method to perform. :keyword args: Dictionary of arguments for the method. :keyword nowait: If false the call will be block until the result is available and return it (default), if true the call will be non-blocking. :keyword retry: If set to true then message sending will be retried in the event of connection failures. Default is decided by the :attr:`retry` attributed. :keyword retry_policy: Override retry policies. See :attr:`retry_policy`. This must be a dictionary, and keys will be merged with the default retry policy. :keyword timeout: Timeout to wait for replies in seconds as a float (**only relevant in blocking mode**). :keyword limit: Limit number of replies to wait for (**only relevant in blocking mode**). :keyword callback: If provided, this callback will be called for every reply received (**only relevant in blocking mode**). :keyword \*\*props: Additional message properties. See :meth:`kombu.Producer.publish`. """ return (nowait and self.cast or self.call)(method, args, **kwargs) def get_queues(self): return [self.type_to_queue[type]() for type in self.types] def get_direct_queue(self): """Returns a unique queue that can be used to listen for messages to this class.""" return Queue(self.id, self.exchange, routing_key=self.routing_key, auto_delete=True) def get_scatter_queue(self): return Queue("%s.%s.scatter" % (self.name, self.id), self.exchange, routing_key=self.type_to_rkey["scatter"], auto_delete=True) def get_rr_queue(self): return Queue(self.exchange.name + ".rr", self.exchange, routing_key=self.type_to_rkey["round-robin"], auto_delete=True) def get_reply_queue(self, ticket): return Queue(ticket, self.reply_exchange, ticket, auto_delete=True, queue_arguments={ "x-expires": int(self.reply_expires * 1000)}) def Consumer(self, channel, **kwargs): """Returns a :class:`kombu.Consumer` instance for this Actor.""" kwargs.setdefault("no_ack", self.no_ack) return Consumer(channel, self.get_queues(), callbacks=[self.on_message], **kwargs) def _publish(self, body, producer, before=None, **props): if before is not None: before(producer.connection, producer.channel) maybe_declare(props["exchange"], producer.channel) return producer.publish(body, **props) def cast(self, method, args={}, before=None, retry=None, retry_policy=None, type=None, **props): """Send message to actor. Discarding replies.""" retry = self.retry if retry is None else retry body = {"class": self.name, "method": method, "args": args} exchange = self.exchange _retry_policy = self.retry_policy if retry_policy: # merge default and custom policies. _retry_policy = dict(_retry_policy, **retry_policy) if type: props.setdefault("routing_key", self.type_to_rkey[type]) props.setdefault("routing_key", self.default_routing_key) props.setdefault("serializer", self.serializer) props = dict(props, exchange=exchange, before=before) ipublish(producers[self._connection], self._publish, (body, ), dict(props, exchange=exchange, before=before), **(retry_policy or {})) def call(self, method, args={}, retry=False, retry_policy=None, **props): """Send message to actor and return :class:`AsyncResult`.""" ticket = uuid() reply_q = self.get_reply_queue(ticket) def before(connection, channel): reply_q(channel).declare() self.cast(method, args, before, **dict(props, reply_to=ticket)) return self.AsyncResult(ticket, self) def handle_cast(self, body, message): """Handle cast message.""" self._DISPATCH(body) def handle_call(self, body, message): """Handle call message.""" try: r = self._DISPATCH(body, ticket=message.properties["reply_to"]) except self.Next: # don't reply, delegate to other agent. pass else: self.reply(message, r) def reply(self, req, body, **props): return isend_reply(producers[self._connection], self.reply_exchange, req, body, props) def on_message(self, body, message): """What to do when a message is received. This is a kombu consumer callback taking the standard ``body`` and ``message`` arguments. Note that if the properties of the message contains a value for ``reply_to`` then a proper implementation is expected to send a reply. """ if message.properties.get("reply_to"): handler = self.handle_call else: handler = self.handle_cast def handle(): # Do not ack the message if an exceptional error occurs, # but do ack the message if SystemExit or KeyboardInterrupt # is raised, as this is probably intended. try: handler(body, message) except Exception: raise except BaseException: message.ack() raise else: message.ack() handle() def _collect_replies(self, conn, channel, ticket, *args, **kwargs): kwargs.setdefault("timeout", self.default_timeout) if "limit" not in kwargs: kwargs["limit"] = self.get_default_scatter_limit() return collect_replies(conn, channel, self.get_reply_queue(ticket), *args, **kwargs) def lookup_action(self, name): try: method = getattr(self.state, name) except AttributeError: raise KeyError(name) if not callable(method) or name.startswith("_"): raise KeyError(method) return method def _DISPATCH(self, body, ticket=None): """Dispatch message to the appropriate method in :attr:`state`, handle possible exceptions, and return a response suitable to be used in a reply. To protect from calling special methods it does not dispatch method names starting with underscore (``_``). This returns the return value or exception error with defaults fields in a suitable format to be used as a reply. The exceptions :exc:`SystemExit` and :exc:`KeyboardInterrupt` will not be handled, and will propagate. In the case of a successful call the return value will be:: {"ok": return_value, **default_fields} If the method raised an exception the return value will be:: {"nok": [repr exc, str traceback], **default_fields} :raises KeyError: if the method specified is unknown or is a special method (name starting with underscore). """ if ticket: sticket = "%s" % (shortuuid(ticket), ) else: ticket = sticket = str(self.next_anon_ticket()) try: method, args = itemgetter("method", "args")(body) self.log.info("#%s --> %s", sticket, self._reprcall(method, args)) act = self.lookup_action(method) r = {"ok": act(**kwdict(args or {}))} self.log.info("#%s <-- %s", sticket, reprkwargs(r)) except self.Next: raise except Exception, exc: einfo = sys.exc_info() r = {"nok": [safe_repr(exc), self._get_traceback(einfo)]} self.log.error("#%s <-- nok=%r", sticket, exc) return dict(self._default_fields, **r) def _get_traceback(self, exc_info): return "".join(traceback.format_exception(*exc_info)) def _reprcall(self, method, args): return "%s.%s" % (self.name, reprcall(method, (), args)) def bind(self, connection, agent=None): return self.__class__(connection, self.id, self.name, self.exchange, agent=agent) def is_bound(self): return self.connection is not None def __copy__(self): cls, args = self.__reduce__() return cls(*args) def __reduce__(self): return (self.__class__, (self.connection, self.id, self.name, self.exchange)) @property def _connection(self): if not self.is_bound(): raise self.NotBoundError("Actor is not bound to a connection.") return self.connection @cached_property def _default_fields(self): return dict(builtin_fields, **self.default_fields) @property def routing_key(self): if self.default_routing_key: return self.default_routing_key return self.agent.id cl-0.0.3/cl/agents.py0000644000076500001200000000253111647550131014746 0ustar asksoladmin00000000000000"""cl.agents""" from __future__ import absolute_import from kombu.common import uuid from kombu.log import setup_logging from kombu.mixins import ConsumerMixin __all__ = ["Agent"] class Agent(ConsumerMixin): actors = [] def __init__(self, connection, id=None, actors=None): self.connection = connection self.id = id or uuid() if actors is not None: self.actors = actors self.actors = self.prepare_actors() def on_run(self): pass def run(self): self.info("Agent on behalf of [%s] starting...", ", ".join(actor.name for actor in self.actors)) self.on_run() super(Agent, self).run() def stop(self): pass def on_consume_ready(self, *args, **kwargs): for actor in self.actors: actor.on_agent_ready() def run_from_commandline(self, loglevel=None, logfile=None): setup_logging(loglevel, logfile) try: self.run() except KeyboardInterrupt: self.info("[Quit requested by user]") def prepare_actors(self): return [actor.bind(self.connection, self) for actor in self.actors] def get_consumers(self, Consumer, channel): return [actor.Consumer(channel) for actor in self.actors] def get_default_scatter_limit(self, actor): return None cl-0.0.3/cl/bin/0000755000076500001200000000000011665173071013666 5ustar asksoladmin00000000000000cl-0.0.3/cl/bin/__init__.py0000644000076500001200000000000011622173151015755 0ustar asksoladmin00000000000000cl-0.0.3/cl/bin/base.py0000644000076500001200000000423011622176522015146 0ustar asksoladmin00000000000000"""cl.bin.base""" from __future__ import absolute_import import optparse import os import sys from .. import __version__ __all__ = ["Option", "Command"] Option = optparse.make_option class Command(object): Parser = optparse.OptionParser args = '' version = __version__ option_list = () def run(self, *args, **options): raise NotImplementedError("subclass responsibility") def execute_from_commandline(self, argv=None): """Execute application from command line. :keyword argv: The list of command line arguments. Defaults to ``sys.argv``. """ if argv is None: argv = list(sys.argv) prog_name = os.path.basename(argv[0]) return self.handle_argv(prog_name, argv[1:]) def usage(self): """Returns the command-line usage string for this app.""" return "%%prog [options] %s" % (self.args, ) def get_options(self): """Get supported command line options.""" return self.option_list def handle_argv(self, prog_name, argv): """Parses command line arguments from ``argv`` and dispatches to :meth:`run`. :param prog_name: The program name (``argv[0]``). :param argv: Command arguments. Exits with an error message if :attr:`supports_args` is disabled and ``argv`` contains positional arguments. """ options, args = self.parse_options(prog_name, argv) return self.run(*args, **vars(options)) def parse_options(self, prog_name, arguments): """Parse the available options.""" # Don't want to load configuration to just print the version, # so we handle --version manually here. if "--version" in arguments: print(self.version) sys.exit(0) parser = self.create_parser(prog_name) options, args = parser.parse_args(arguments) return options, args def create_parser(self, prog_name): return self.Parser(prog=prog_name, usage=self.usage(), version=self.version, option_list=self.get_options()) cl-0.0.3/cl/bin/cl.py0000644000076500001200000000467111622176556014652 0ustar asksoladmin00000000000000"""cl.bin.cl""" from __future__ import absolute_import from kombu import Connection from .base import Command, Option from .. import Agent from ..utils import instantiate __all__ = ["cl", "main"] class cl(Command): option_list = ( Option("-i", "--id", default=None, action="store", dest="id", help="Id of the agent (or automatically generated)."), Option("-l", "--loglevel", default=None, action="store", dest="loglevel", help="Loglevel (CRITICAL/ERROR/WARNING/INFO/DEBUG)."), Option("-f", "--logfile", default=None, action="store", dest="logfile", help="Logfile. Default is stderr."), Option("-H", "--hostname", default=None, action="store", dest="hostname", help="Broker hostname."), Option("-P", "--port", default=None, action="store", type="int", dest="port", help="Broker port."), Option("-u", "--userid", default=None, action="store", dest="userid", help="Broker user id."), Option("-p", "--password", default=None, action="store", dest="password", help="Broker password."), Option("-v", "--virtual-host", default=None, action="store", dest="virtual_host", help="Broker virtual host"), Option("-T", "--transport", default=None, action="store", dest="transport", help="Broker transport"), ) def run(self, *actors, **kwargs): id = kwargs.get("id") loglevel = kwargs.get("loglevel") actors = [instantiate(actor) for actor in list(actors)] connection = Connection(hostname=kwargs.get("hostname"), port=kwargs.get("port"), userid=kwargs.get("userid"), password=kwargs.get("password"), virtual_host=kwargs.get("virtual_host"), transport=kwargs.get("transport")) agent = Agent(connection, actors=actors, id=kwargs.get("id")) agent.run_from_commandline(loglevel=kwargs.get("loglevel"), logfile=kwargs.get("logfile")) def main(argv=None): return cl().execute_from_commandline(argv) if __name__ == "__main__": main() cl-0.0.3/cl/exceptions.py0000644000076500001200000000175411647550131015654 0ustar asksoladmin00000000000000"""cl.exceptions""" from __future__ import absolute_import __all__ = ["clError", "Next", "NoReplyError", "NotBoundError"] FRIENDLY_ERROR_FMT = """ Remote method raised exception: ------------------------------------ %s """ class clError(Exception): """Remote method raised exception.""" exc = None traceback = None def __init__(self, exc=None, traceback=None): self.exc = exc self.traceback = traceback Exception.__init__(self, exc, traceback) def __str__(self): return FRIENDLY_ERROR_FMT % (self.traceback, ) class Next(Exception): """Used in a gather scenario to signify that no reply should be sent, to give another agent the chance to reply.""" pass class NoReplyError(Exception): """No reply received within time constraint""" pass class NotBoundError(Exception): """Object is not bound to a connection.""" pass class NoRouteError(Exception): """Presence: No known route for wanted item.""" pass cl-0.0.3/cl/g/0000755000076500001200000000000011665173071013344 5ustar asksoladmin00000000000000cl-0.0.3/cl/g/__init__.py0000644000076500001200000000233311647550131015452 0ustar asksoladmin00000000000000from __future__ import absolute_import from kombu.syn import detect_environment from ..utils import cached_property G_NOT_FOUND = """\ cl does not currently support %r, please use one of %s\ """ class G(object): map = {"eventlet": "_eventlet"} def spawn(self, fun, *args, **kwargs): return self.current.spawn(fun, *args, **kwargs) def timer(self, interval, fun, *args, **kwargs): return self.current.timer(interval, fun, *args, **kwargs) def blocking(self, fun, *args, **kwargs): return self.current.blocking(fun, *args, **kwargs) def Queue(self, *args, **kwargs): return self.current.Queue(*args, **kwargs) def Event(self, *args, **kwargs): return self.current.Event(*args, **kwargs) @cached_property def _eventlet(self): from . import eventlet return eventlet @cached_property def current(self): type = detect_environment() try: return getattr(self, self.map[type]) except KeyError: raise KeyError(G_NOT_FOUND % (type, ", ".join(self.map.keys()))) g = G() blocking = g.blocking spawn = g.spawn timer = g.timer Queue = g.Queue Event = g.Event cl-0.0.3/cl/g/eventlet.py0000644000076500001200000000306311647550131015542 0ustar asksoladmin00000000000000from __future__ import absolute_import import warnings from eventlet import Timeout # noqa from eventlet import event from eventlet import greenthread from eventlet import queue from greenlet import GreenletExit from kombu import syn blocking = syn.blocking spawn = greenthread.spawn Queue = queue.LightQueue Event = event.Event class Entry(object): g = None def __init__(self, interval, fun, *args, **kwargs): self.interval = interval self.fun = fun self.args = args self.kwargs = kwargs self.cancelled = False self._spawn() def _spawn(self): self.g = greenthread.spawn_after_local(self.interval, self) self.g.link(self._exit) def __call__(self): try: return blocking(self.fun, *self.args, **self.kwargs) except Exception, exc: warnings.warn("Periodic timer %r raised: %r" % (self.fun, exc)) finally: self._spawn() def _exit(self, g): try: self.g.wait() except GreenletExit: self.cancel() def cancel(self): if self.g and not self.cancelled: self.g.cancel() self.cancelled = True def kill(self): if self.g: try: self.g.kill() except GreenletExit: pass def __repr__(self): return "" % ( self.fun, "cancelled" if self.cancelled else "alive") def timer(interval, fun, *args, **kwargs): return Entry(interval, fun, *args, **kwargs) cl-0.0.3/cl/models.py0000644000076500001200000000570711622202257014754 0ustar asksoladmin00000000000000"""cl.models""" from __future__ import absolute_import from kombu import Consumer, Queue from kombu.utils import gen_unique_id from . import Actor _all__ = ["ModelActor", "ModelConsumer"] class ModelConsumer(Consumer): model = None field = "name" auto_delete = True def __init__(self, channel, exchange, *args, **kwargs): model = kwargs.pop("model", None) self.model = model if model is not None else self.model self.exchange = exchange self.prepare_signals(kwargs.pop("sigmap", None)) queues = self.sync_queues(kwargs.pop("queues", [])) super(ModelConsumer, self).__init__(channel, queues, *args, **kwargs) def prepare_signals(self, sigmap=None): for callback, connect in (sigmap or {}).iteritems(): if isinstance(callback, basestring): callback = getattr(self, callback) connect(callback) def create_queue(self, field_value): return Queue(gen_unique_id(), self.exchange, field_value, auto_delete=self.auto_delete) def sync_queues(self, keep_queues=[]): expected = [getattr(obj, self.field) for obj in self.model._default_manager.enabled()] queues = set() create = self.create_queue for v in expected: queues.add(create(v)) for queue in queues: if queue.routing_key not in expected: queues.discard(v) return list(keep_queues) + list(queues) def on_create(self, instance=None, **kwargs): fv = getattr(instance, self.field) if not self.find_queue_by_rkey(fv): self.add_queue(self.create_queue(fv)) self.consume() def on_delete(self, instance=None, **kwargs): fv = getattr(instance, self.field) queue = self.find_queue_by_rkey(fv) if queue: self.cancel_by_queue(queue.name) def find_queue_by_rkey(self, rkey): for queue in self.queues: if queue.routing_key == rkey: return queue class ModelActor(Actor): #: The model this actor is a controller for (*required*). model = None #: Map of signals to connect and corresponding actions. sigmap = {} def __init__(self, connection=None, id=None, name=None, *args, **kwargs): if self.model is None: raise NotImplementedError( "ModelActors must define the 'model' attribute!") if not name or self.name: name = self.model.__name__ super(ModelActor, self).__init__(connection, id, name, *args, **kwargs) def Consumer(self, channel, **kwargs): return ModelConsumer(channel, self.exchange, callbacks=[self.on_message], sigmap=self.sigmap, model=self.model, queues=[self.get_scatter_queue(), self.get_rr_queue()], **kwargs) cl-0.0.3/cl/presence.py0000644000076500001200000001732211647550131015275 0ustar asksoladmin00000000000000"""scs.presence""" from __future__ import absolute_import, with_statement import warnings from collections import defaultdict from contextlib import contextmanager from functools import wraps from random import shuffle from time import time, sleep from kombu import Exchange, Queue from kombu.common import ipublish from kombu.log import LogMixin from kombu.mixins import ConsumerMixin from kombu.pools import producers from kombu.utils.functional import promise from .agents import Agent from .g import spawn, timer from .utils import cached_property, first_or_raise, shortuuid class State(LogMixin): logger_name = "cl.presence.state" def __init__(self, presence): self.presence = presence self._agents = defaultdict(lambda: {}) self.heartbeat_expire = self.presence.interval * 2.5 self.handlers = {"online": self.when_online, "offline": self.when_offline, "heartbeat": self.when_heartbeat, "wakeup": self.when_wakeup} def can(self, actor): able = set() for id, state in self.agents.iteritems(): if actor in state["actors"]: # remove the . from the agent, which means that the # agent is a clone of another agent. able.add(id.partition(".")[0]) return able def meta_for(self, actor): return self._agents["meta"][actor] def update_meta_for(self, agent, meta): self._agents[agent].update(meta=meta) def agents_by_meta(self, predicate, *sections): agents = self._agents agent_ids = agents.keys() # shuffle the agents so we don't get the same agent every time. shuffle(agent_ids) for agent in agent_ids: d = agents[agent]["meta"] for i, section in enumerate(sections): d = d[section] if predicate(d): yield agent def first_agent_by_meta(self, predicate, *sections): for agent in self.agents_by_meta(predicate, *sections): return agent raise KeyError() def on_message(self, body, message): event = body["event"] self.handlers[event](**body) self.debug("agents after event recv: %s", promise(lambda: self.agents)) def when_online(self, agent=None, **kw): self._update_agent(agent, kw) def when_wakeup(self, **kw): self.presence.send_heartbeat() def when_heartbeat(self, agent=None, **kw): self._update_agent(agent, kw) def when_offline(self, agent=None, **kw): self._remove_agent(agent) def expire_agents(self): expired = set() for id, state in self._agents.iteritems(): if state and state.get("ts"): if time() > state["ts"] + self.heartbeat_expire: expired.add(id) for id in expired: self._remove_agent(id) return self._agents def update_agent(self, agent=None, **kw): return self._update_agent(agent, kw) def _update_agent(self, agent, kw): kw = dict(kw) meta = kw.pop("meta", None) if meta: self.update_meta_for(agent, meta) self._agents[agent].update(kw) def _remove_agent(self, agent): self._agents[agent].clear() def neighbors(self): return {"agents": self.agents.keys()} @property def agents(self): return self.expire_agents() class Event(dict): pass class Presence(ConsumerMixin): Event = Event State = State exchange = Exchange("cl.agents", type="topic", auto_delete=True) interval = 10 _channel = None g = None def __init__(self, agent, interval=None, on_awake=None): self.agent = agent self.state = self.State(self) self.interval = interval or self.interval self.connection = agent.connection self.on_awake = on_awake def get_queue(self): return Queue("cl.agents.%s" % (self.agent.id, ), self.exchange, routing_key="#", auto_delete=True) def get_consumers(self, Consumer, channel): return [Consumer(self.get_queue(), callbacks=[self.state.on_message], no_ack=True)] def create_event(self, type): return self.Event(agent=self.agent.id, event=type, actors=[actor.name for actor in self.agent.actors], meta=self.meta(), ts=time(), neighbors=self.state.neighbors()) def meta(self): return dict((actor.name, actor.meta) for actor in self.agent.actors) @contextmanager def extra_context(self, connection, channel): self.send_online() self.wakeup() sleep(1.0) if self.on_awake: self.on_awake() timer(self.interval, self.send_heartbeat) self.agent.on_presence_ready() yield self.send_offline() def _announce(self, event, producer=None): producer.publish(event, exchange=self.exchange.name, routing_key=self.agent.id) def announce(self, event, **retry_policy): return ipublish(producers[self.agent.connection], self._announce, (event, ), **retry_policy) def start(self): self.g = spawn(self.run) def send_online(self): return self.announce(self.create_event("online")) def send_heartbeat(self): return self.announce(self.create_event("heartbeat")) def send_offline(self): return self.announce(self.create_event("offline")) def wakeup(self): event = self.create_event("wakeup") self.state.update_agent(**event) return self.announce(event) def can(self, actor): return self.state.can(actor) @property def logger_name(self): return "Presence#%s" % (shortuuid(self.agent.id), ) @property def should_stop(self): return self.agent.should_stop class AwareAgent(Agent): def on_run(self): self.presence.start() def get_default_scatter_limit(self, actor): able = self.presence.can(actor) if not able: warnings.warn("Presence running, but no agents available?!?") return len(able) if able else None def on_awake(self): pass def on_presence_ready(self): pass def lookup_agent(self, pred, *sections): return self.presence.state.first_agent_by_meta(pred, *sections) def lookup_agents(self, pred, *sections): return self.presence.state.agents_by_meta(pred, *sections) @cached_property def presence(self): return Presence(self, on_awake=self.on_awake) class AwareActorMixin(object): meta_lookup_section = None def lookup(self, value): if self.agent: return self.agent.lookup_agent(lambda values: value in values, self.name, self.meta_lookup_section) def send_to_able(self, method, args={}, to=None, **kwargs): actor = None try: actor = self.lookup(to) except KeyError: raise self.NoRouteError(to) if actor: return self.send(method, args, to=actor, **kwargs) r = self.scatter(method, args, propagate=True, **kwargs) if r: return first_or_raise(r, self.NoRouteError(to)) def wakeup_all_agents(self): if self.agent: self.log.info("presence wakeup others") self.agent.presence.wakeup() def announce_after(fun): @wraps(fun) def _inner(self, *args, **kwargs): try: return fun(self, *args, **kwargs) finally: self.actor.wakeup_all_agents() return _inner cl-0.0.3/cl/results.py0000644000076500001200000000301111647550131015160 0ustar asksoladmin00000000000000"""cl.result""" from __future__ import absolute_import from __future__ import with_statement from kombu.pools import producers from .exceptions import clError, NoReplyError __all__ = ["AsyncResult"] class AsyncResult(object): Error = clError NoReplyError = NoReplyError def __init__(self, ticket, actor): self.ticket = ticket self.actor = actor def _first(self, replies): if replies is not None: replies = list(replies) if replies: return replies[0] raise self.NoReplyError("No reply received within time constraint") def get(self, **kwargs): return self._first(self.gather(**dict(kwargs, limit=1))) def gather(self, propagate=True, **kwargs): connection = self.actor.connection gather = self._gather with producers[connection].acquire(block=True) as producer: for r in gather(producer.connection, producer.channel, self.ticket, propagate=propagate, **kwargs): yield r def _gather(self, *args, **kwargs): propagate = kwargs.pop("propagate", True) return (self.to_python(reply, propagate=propagate) for reply in self.actor._collect_replies(*args, **kwargs)) def to_python(self, reply, propagate=True): try: return reply["ok"] except KeyError: error = self.Error(*reply.get("nok") or ()) if propagate: raise error return error cl-0.0.3/cl/utils/0000755000076500001200000000000011665173071014256 5ustar asksoladmin00000000000000cl-0.0.3/cl/utils/__init__.py0000644000076500001200000000503311647550131016364 0ustar asksoladmin00000000000000"""cl.utils""" from __future__ import absolute_import import operator from importlib import import_module from itertools import imap, ifilter from kombu.utils import cached_property # noqa __all__ = ["force_list", "flatten", "get_cls_by_name", "instantiate", "cached_property"] def force_list(obj): if not hasattr(obj, "__iter__"): return [obj] return obj def flatten(it): if it: try: return reduce(operator.add, imap(force_list, ifilter(None, it))) except TypeError: return [] return it def first(it, default=None): try: it.next() except StopIteration: return default def first_or_raise(it, exc): for reply in it: if not isinstance(reply, Exception): return reply raise exc def get_cls_by_name(name, aliases={}, imp=None): """Get class by name. The name should be the full dot-separated path to the class:: modulename.ClassName Example:: celery.concurrency.processes.TaskPool ^- class name If `aliases` is provided, a dict containing short name/long name mappings, the name is looked up in the aliases first. Examples: >>> get_cls_by_name("celery.concurrency.processes.TaskPool") >>> get_cls_by_name("default", { ... "default": "celery.concurrency.processes.TaskPool"}) # Does not try to look up non-string names. >>> from celery.concurrency.processes import TaskPool >>> get_cls_by_name(TaskPool) is TaskPool True """ if imp is None: imp = import_module if not isinstance(name, basestring): return name # already a class name = aliases.get(name) or name module_name, _, cls_name = name.rpartition(".") try: module = imp(module_name) except ValueError, exc: raise ValueError("Couldn't import %r: %s" % (name, exc)) return getattr(module, cls_name) def instantiate(name, *args, **kwargs): """Instantiate class by name. See :func:`get_cls_by_name`. """ return get_cls_by_name(name)(*args, **kwargs) def abbr(S, max, ellipsis="..."): if S and len(S) > max: return ellipsis and (S[:max - len(ellipsis)] + ellipsis) or S[:max] return S def shortuuid(u): if '-' in u: return u[:u.index('-')] return abbr(u, 16) cl-0.0.3/cl.egg-info/0000755000076500001200000000000011665173071014610 5ustar asksoladmin00000000000000cl-0.0.3/cl.egg-info/dependency_links.txt0000644000076500001200000000000111665173065020661 0ustar asksoladmin00000000000000 cl-0.0.3/cl.egg-info/entry_points.txt0000644000076500001200000000004711665173065020112 0ustar asksoladmin00000000000000[console_scripts] cl = cl.bin.cl:main cl-0.0.3/cl.egg-info/not-zip-safe0000644000076500001200000000000111621746622017036 0ustar asksoladmin00000000000000 cl-0.0.3/cl.egg-info/PKG-INFO0000644000076500001200000000535711665173065015722 0ustar asksoladmin00000000000000Metadata-Version: 1.0 Name: cl Version: 0.0.3 Summary: Kombu actor framework Home-page: http://github.com/ask/cl/ Author: Ask Solem Author-email: ask@rabbitmq.com License: UNKNOWN Description: ############################################# cl - Actor framework for Kombu ############################################# :Version: 0.0.3 Synopsis ======== `cl` (pronounced *cell*) is an actor framework for `Kombu`_. .. _`Kombu`: http://pypi.python.org/pypi/kombu Installation ============ You can install `cl` either via the Python Package Index (PyPI) or from source. To install using `pip`,:: $ pip install cl To install using `easy_install`,:: $ easy_install cl If you have downloaded a source tarball you can install it by doing the following,:: $ python setup.py build # python setup.py install # as root Getting Help ============ Mailing list ------------ Join the `carrot-users`_ mailing list. .. _`carrot-users`: http://groups.google.com/group/carrot-users/ Bug tracker =========== If you have any suggestions, bug reports or annoyances please report them to our issue tracker at http://github.com/ask/cl/issues/ Contributing ============ Development of `cl` happens at Github: http://github.com/ask/cl You are highly encouraged to participate in the development. If you don't like Github (for some reason) you're welcome to send regular patches. License ======= This software is licensed under the `New BSD License`. See the `LICENSE` file in the top distribution directory for the full license text. Copyright ========= Copyright (C) 2011 VMware, Inc. Platform: any Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.5 Classifier: Intended Audience :: Developers Classifier: Topic :: Communications Classifier: Topic :: System :: Distributed Computing Classifier: Topic :: System :: Networking Classifier: Topic :: Software Development :: Libraries :: Python Modules cl-0.0.3/cl.egg-info/requires.txt0000644000076500001200000000001411665173065017206 0ustar asksoladmin00000000000000kombu>=1.3.0cl-0.0.3/cl.egg-info/SOURCES.txt0000644000076500001200000000113111665173065016473 0ustar asksoladmin00000000000000AUTHORS Changelog LICENSE MANIFEST.in README README.rst setup.cfg setup.py cl/__init__.py cl/actors.py cl/agents.py cl/exceptions.py cl/models.py cl/presence.py cl/results.py cl.egg-info/PKG-INFO cl.egg-info/SOURCES.txt cl.egg-info/dependency_links.txt cl.egg-info/entry_points.txt cl.egg-info/not-zip-safe cl.egg-info/requires.txt cl.egg-info/top_level.txt cl/bin/__init__.py cl/bin/base.py cl/bin/cl.py cl/g/__init__.py cl/g/eventlet.py cl/utils/__init__.py examples/__init__.py examples/distributed_cache.py examples/hello.py requirements/default.txt requirements/pkgutils.txt requirements/test.txtcl-0.0.3/cl.egg-info/top_level.txt0000644000076500001200000000000311665173065017336 0ustar asksoladmin00000000000000cl cl-0.0.3/examples/0000755000076500001200000000000011665173071014336 5ustar asksoladmin00000000000000cl-0.0.3/examples/__init__.py0000644000076500001200000000000011622174745016437 0ustar asksoladmin00000000000000cl-0.0.3/examples/distributed_cache.py0000644000076500001200000000256511622171072020355 0ustar asksoladmin00000000000000from UserDict import DictMixin from cl import Actor, Agent from cl.utils import flatten def first_reply(replies, key): try: return replies.next() except StopIteration: raise KeyError(key) class Cache(Actor, DictMixin): types = ("scatter", "round-robin") default_timeout = 1 class state(object): def __init__(self, data=None): self.data = {} def get(self, key): if key not in self.data: # delegate to next agent. raise Actor.Next() return self.data[key] def delete(self, key): if key not in self.data: raise Actor.Next() return self.data.pop(key, None) def set(self, key, value): self.data[key] = value def keys(self): return self.data.keys() def __getitem__(self, key): return first_reply(self.scatter("get", {"key": key}), key) def __delitem__(self, key): return first_reply(self.scatter("delete", {"key": key}), key) def __setitem__(self, key, value): return self.throw("set", {"key": key, "value": value}) def keys(self): return flatten(self.scatter("keys")) class CacheAgent(Agent): actors = [Cache()] if __name__ == "__main__": from kombu import Connection CacheAgent(Connection()).run_from_commandline() cl-0.0.3/examples/hello.py0000644000076500001200000000111211647550131016002 0ustar asksoladmin00000000000000from cl import Actor, Agent from kombu import Connection connection = Connection() class GreetingActor(Actor): default_routing_key = "GreetingActor" class state: def greet(self, who="world"): return "Hello %s" % who greeting = GreetingActor(connection) class GreetingAgent(Agent): actors = [greeting] if __name__ == "__main__": GreetingAgent(connection).run_from_commandline() # Run this script from the command line and try this # in another console: # # >>> from hello import greeting # >>> greeting.call("greet") # "Hello world" cl-0.0.3/LICENSE0000644000076500001200000000272511622166430013525 0ustar asksoladmin00000000000000Copyright (c) 2011 VMware, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of VMware, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cl-0.0.3/MANIFEST.in0000644000076500001200000000033111622004475014245 0ustar asksoladmin00000000000000include AUTHORS include Changelog include LICENSE include MANIFEST.in include README.rst include README include setup.cfg recursive-include cl *.py recursive-include requirements *.txt recursive-include examples *.py cl-0.0.3/PKG-INFO0000644000076500001200000000535711665173071013627 0ustar asksoladmin00000000000000Metadata-Version: 1.0 Name: cl Version: 0.0.3 Summary: Kombu actor framework Home-page: http://github.com/ask/cl/ Author: Ask Solem Author-email: ask@rabbitmq.com License: UNKNOWN Description: ############################################# cl - Actor framework for Kombu ############################################# :Version: 0.0.3 Synopsis ======== `cl` (pronounced *cell*) is an actor framework for `Kombu`_. .. _`Kombu`: http://pypi.python.org/pypi/kombu Installation ============ You can install `cl` either via the Python Package Index (PyPI) or from source. To install using `pip`,:: $ pip install cl To install using `easy_install`,:: $ easy_install cl If you have downloaded a source tarball you can install it by doing the following,:: $ python setup.py build # python setup.py install # as root Getting Help ============ Mailing list ------------ Join the `carrot-users`_ mailing list. .. _`carrot-users`: http://groups.google.com/group/carrot-users/ Bug tracker =========== If you have any suggestions, bug reports or annoyances please report them to our issue tracker at http://github.com/ask/cl/issues/ Contributing ============ Development of `cl` happens at Github: http://github.com/ask/cl You are highly encouraged to participate in the development. If you don't like Github (for some reason) you're welcome to send regular patches. License ======= This software is licensed under the `New BSD License`. See the `LICENSE` file in the top distribution directory for the full license text. Copyright ========= Copyright (C) 2011 VMware, Inc. Platform: any Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.5 Classifier: Intended Audience :: Developers Classifier: Topic :: Communications Classifier: Topic :: System :: Distributed Computing Classifier: Topic :: System :: Networking Classifier: Topic :: Software Development :: Libraries :: Python Modules cl-0.0.3/README0000600000076500000240000000260011665173053013404 0ustar asksolstaff00000000000000############################################# cl - Actor framework for Kombu ############################################# :Version: 0.0.3 Synopsis ======== `cl` (pronounced *cell*) is an actor framework for `Kombu`_. .. _`Kombu`: http://pypi.python.org/pypi/kombu Installation ============ You can install `cl` either via the Python Package Index (PyPI) or from source. To install using `pip`,:: $ pip install cl To install using `easy_install`,:: $ easy_install cl If you have downloaded a source tarball you can install it by doing the following,:: $ python setup.py build # python setup.py install # as root Getting Help ============ Mailing list ------------ Join the `carrot-users`_ mailing list. .. _`carrot-users`: http://groups.google.com/group/carrot-users/ Bug tracker =========== If you have any suggestions, bug reports or annoyances please report them to our issue tracker at http://github.com/ask/cl/issues/ Contributing ============ Development of `cl` happens at Github: http://github.com/ask/cl You are highly encouraged to participate in the development. If you don't like Github (for some reason) you're welcome to send regular patches. License ======= This software is licensed under the `New BSD License`. See the `LICENSE` file in the top distribution directory for the full license text. Copyright ========= Copyright (C) 2011 VMware, Inc. cl-0.0.3/README.rst0000600000076500000240000000000011665173053016114 1cl-0.0.3/READMEustar asksolstaff00000000000000cl-0.0.3/requirements/0000755000076500001200000000000011665173071015243 5ustar asksoladmin00000000000000cl-0.0.3/requirements/default.txt0000644000076500001200000000001511647550131017420 0ustar asksoladmin00000000000000kombu>=1.3.0 cl-0.0.3/requirements/pkgutils.txt0000644000076500001200000000002411621746547017650 0ustar asksoladmin00000000000000paver flake8 Sphinx cl-0.0.3/requirements/test.txt0000644000076500001200000000007311621746606016765 0ustar asksoladmin00000000000000nose nose-cover3 unittest2>=0.5.0 coverage>=3.0 simplejson cl-0.0.3/setup.cfg0000644000076500001200000000054411665173071014344 0ustar asksoladmin00000000000000[nosetests] verbosity = 1 detailed-errors = 1 where = cl/tests cover3-branch = 1 cover3-html = 1 cover3-package = cl cover3-exclude = cl [build_sphinx] source-dir = docs/ build-dir = docs/.build all_files = 1 [upload_sphinx] upload-dir = docs/.build/html [bdist_rpm] requires = kombu >= 1.3.0 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 cl-0.0.3/setup.py0000644000076500001200000000730611647550131014234 0ustar asksoladmin00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import codecs extra = {} tests_require = ["nose", "nose-cover3"] if sys.version_info >= (3, 0): extra.update(use_2to3=True) elif sys.version_info <= (2, 6): tests_require.append("unittest2") elif sys.version_info <= (2, 5): tests_require.append("simplejson") if sys.version_info < (2, 5): raise Exception("cl requires Python 2.5 or higher.") try: from setuptools import setup except ImportError: from distutils.core import setup # noqa from distutils.command.install import INSTALL_SCHEMES # -- Parse meta import re re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') re_vers = re.compile(r'VERSION\s*=\s*\((.*?)\)') re_doc = re.compile(r'^"""(.+?)"""') rq = lambda s: s.strip("\"'") def add_default(m): attr_name, attr_value = m.groups() return ((attr_name, rq(attr_value)), ) def add_version(m): v = list(map(rq, m.groups()[0].split(", "))) return (("VERSION", ".".join(v[0:3]) + "".join(v[3:])), ) def add_doc(m): return (("doc", m.groups()[0]), ) pats = {re_meta: add_default, re_vers: add_version, re_doc: add_doc} here = os.path.abspath(os.path.dirname(__file__)) meta_fh = open(os.path.join(here, "cl/__init__.py")) try: meta = {} for line in meta_fh: if line.strip() == '# -eof meta-': break for pattern, handler in pats.items(): m = pattern.match(line.strip()) if m: meta.update(handler(m)) finally: meta_fh.close() # -- packages, data_files = [], [] root_dir = os.path.dirname(__file__) if root_dir != '': os.chdir(root_dir) src_dir = "cl" def fullsplit(path, result=None): if result is None: result = [] head, tail = os.path.split(path) if head == '': return [tail] + result if head == path: return result return fullsplit(head, [tail] + result) for scheme in list(INSTALL_SCHEMES.values()): scheme['data'] = scheme['purelib'] for dirpath, dirnames, filenames in os.walk(src_dir): # Ignore dirnames that start with '.' for i, dirname in enumerate(dirnames): if dirname.startswith("."): del dirnames[i] for filename in filenames: if filename.endswith(".py"): packages.append('.'.join(fullsplit(dirpath))) else: data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) if os.path.exists("README.rst"): long_description = codecs.open('README.rst', "r", "utf-8").read() else: long_description = "See http://pypi.python.org/pypi/cl" setup( name='cl', version=meta["VERSION"], description=meta["doc"], author=meta["author"], author_email=meta["contact"], url=meta["homepage"], platforms=["any"], packages=packages, data_files=data_files, zip_safe=False, test_suite="nose.collector", install_requires=[ 'kombu>=1.3.0', ], tests_require=tests_require, classifiers=[ "Development Status :: 3 - Alpha", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.5", "Intended Audience :: Developers", "Topic :: Communications", "Topic :: System :: Distributed Computing", "Topic :: System :: Networking", "Topic :: Software Development :: Libraries :: Python Modules", ], entry_points={ "console_scripts": ["cl = cl.bin.cl:main"], }, long_description=long_description, **extra)