diff --git a/.travis.yml b/.travis.yml index bbb1f2d4d2c78c6ed42e85816e3ceeb466b0e53c..f700f4342f19a4d257db15c34064ff4ef5996060 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ python: script: - python compile_definitions.py - python setup.py nosetests + - python -m stress_tests install: - pip install -r requirements.txt - pip install --force-reinstall "coverage>=4.0,<4.4" codeclimate-test-reporter diff --git a/Vagrantfile b/Vagrantfile index 81775013ef6857180bb2d2a31248479967270264..efc48bec9f8a61059d0aa8d3e7db6140ba4096c3 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -3,7 +3,7 @@ Vagrant.configure("2") do |config| - config.vm.box = "debian/contrib-jessie64" + config.vm.box = "debian/contrib-stretch64" # Rabbit MQ management config.vm.network "forwarded_port", guest: 15672, host: 15672, auto_correct: true @@ -12,31 +12,33 @@ Vagrant.configure("2") do |config| config.vm.network "forwarded_port", guest: 80, host: 8765 config.vm.provision "shell", inline: <<-SHELL - apt-get update - - # Python - apt-get install -y htop curl python python-setuptools python-pip python-dev build-essential rabbitmq-server python3 python3-pip python3-setuptools - pip install --upgrade pip setuptools - pip3 install --upgrade pip setuptools - - /usr/lib/rabbitmq/bin/rabbitmq-plugins enable rabbitmq_management - sudo service rabbitmq-server restart - rabbitmqctl add_user user user - rabbitmqctl set_permissions -p / user ".*" ".*" ".*" - rabbitmqctl set_permissions -p / guest ".*" ".*" ".*" - rabbitmqctl set_user_tags user administrator - - # Install deps - pip install -r /vagrant/requirements.txt - pip install nose coverage - pip3 install -r /vagrant/requirements.txt - pip3 install nose coverage - - # HTTP server for viewing coverage reports + apt-get update + + # Python + apt-get install -y htop curl python python-setuptools python-pip python-dev build-essential rabbitmq-server python3 python3-pip python3-setuptools + sudo pip install --upgrade pip setuptools + sudo pip3 install --upgrade pip setuptools + + /usr/lib/rabbitmq/bin/rabbitmq-plugins enable rabbitmq_management + sudo service rabbitmq-server restart + rabbitmqctl add_user user user + rabbitmqctl set_permissions -p / user ".*" ".*" ".*" + rabbitmqctl set_permissions -p / guest ".*" ".*" ".*" + rabbitmqctl set_user_tags user administrator + + # Install deps + sudo pip install -r /vagrant/requirements.txt + sudo pip install nose coverage mock + sudo pip3 install -r /vagrant/requirements.txt + sudo pip3 install nose coverage mock + + sudo pip3 install -r /vagrant/stress_tests/requirements.txt + + # HTTP server for viewing coverage reports apt-get -y install nginx rm -rf /var/www/html ln -s /vagrant/htmlcov /var/www/html - + # .bashrc for default user echo """# .bashrc cd /vagrant""" > /home/vagrant/.bashrc diff --git a/coolamqp/objects.py b/coolamqp/objects.py index c6f0f3473db957867f19fe3927e5711e6c55ff87..458b4afdbd02102a996ec154937621baabcb5bb5 100644 --- a/coolamqp/objects.py +++ b/coolamqp/objects.py @@ -4,6 +4,7 @@ Core objects used in CoolAMQP """ import logging import uuid +import typing as tp import six @@ -104,15 +105,17 @@ class ReceivedMessage(Message): and .nack() are no-ops. """ - def __init__(self, body, exchange_name, routing_key, - properties=None, - delivery_tag=None, - ack=None, - nack=None): + def __init__(self, body, # type: tp.Union[six.binary_type, memoryview, tp.List[memoryview]] + exchange_name, # type: memoryview + routing_key, # type: memoryview + properties=None, # type: MessageProperties + delivery_tag=None, # type: int + ack=None, # type: tp.Callable[[], None] + nack=None): # type: tp.Callable[[], None] """ :param body: message body. A stream of octets. :type body: str (py2) or bytes (py3) or a list of memoryviews, if - particular disabled-by-default option is turned on. + particular disabled-by-default option is turned on, or a single memoryview :param exchange_name: name of exchange this message was submitted to :type exchange_name: memoryview :param routing_key: routing key with which this message was sent @@ -124,9 +127,11 @@ class ReceivedMessage(Message): this message :param ack: a callable to call when you want to ack (via basic.ack) this message. None if received by the no-ack mechanism + :type ack: Callable[[], None] :param nack: a callable to call when you want to nack (via basic.reject) this message. None if received by the no-ack mechanism + :type nack: Callable[[], None] """ Message.__init__(self, body, properties=properties) @@ -231,20 +236,20 @@ class NodeDefinition(object): """ Create a cluster node definition. - a = NodeDefinition(host='192.168.0.1', user='admin', password='password', - virtual_host='vhost') + >>> a = NodeDefinition(host='192.168.0.1', user='admin', password='password', + >>> virtual_host='vhost') or - a = NodeDefinition('192.168.0.1', 'admin', 'password') + >>> a = NodeDefinition('192.168.0.1', 'admin', 'password') or - a = NodeDefinition('amqp://user:password@host/virtual_host') + >>> a = NodeDefinition('amqp://user:password@host/virtual_host') or - a = NodeDefinition('amqp://user:password@host:port/virtual_host', hearbeat=20) + >>> a = NodeDefinition('amqp://user:password@host:port/virtual_host', hearbeat=20) AMQP connection string may be either bytes or str/unicode diff --git a/docs/caveats.md b/docs/caveats.rst similarity index 51% rename from docs/caveats.md rename to docs/caveats.rst index 514f38432264a156efa5c0b342ea261054d56ff7..658396210feaddbf19ac617b89b1bf2c860e10ca 100644 --- a/docs/caveats.md +++ b/docs/caveats.rst @@ -1,28 +1,30 @@ -# Caveats +Caveats +======= Things to look out for -## memoryviews +memoryviews +----------- Since CoolAMQP tries to be fast, it uses memoryviews everywhere. _ReceivedMessage_ properties, and message properties therefore, are memoryviews. So, it you wanted to read the routing key a message was sent with, or message's encoding, you should do: -```python -received_msg.routing_key.to_bytes() -received_msg.properties.content_encoding.to_bytes() -``` +:: -Only the _body_ property of the message will be a byte object (and not even that it you explicitly ask otherwise). + received_msg.routing_key.to_bytes() + received_msg.properties.content_encoding.to_bytes() + +Only the **body** property of the message will be a byte object (and not even that it you explicitly ask otherwise). Note that YOU, when sending messages, should not use memoryviews. Pass proper byte objects and text objects as required. -_AMQPError_'s returned to you via futures will also have memoryviews as _reply_text_! +**AMQPError**'s returned to you via futures will also have memoryviews as **reply_text**! -It was considered whether to unserialize short fields, such as _routing_key_ or _exchange_, but it was decided against. +It was considered whether to unserialize short fields, such as **routing_key** or **exchange**, but it was decided against. Creating a new memoryview carries at least much overhead as an empty string, but there's no need to copy. Plus, it's not known whether you will use these strings at all! -If you need to, you got memoryviews. Plus they support the \__eq__ protocol, which should cover most +If you need to, you got memoryviews. Plus they support the **__eq__** protocol, which should cover most use cases without even converting. diff --git a/docs/conf.py b/docs/conf.py index d9f72fe9ad9f88e6d135865dce4cdb8a7ab8d757..2edc8d78d442ca4478ebb1087e133ecb4feb4536 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,13 +16,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) -from recommonmark.parser import CommonMarkParser +import os +import sys +sys.path.insert(0, os.path.abspath('..')) source_parsers = { - '.md': CommonMarkParser } # -- General configuration ------------------------------------------------ @@ -42,14 +40,14 @@ templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = ['.rst', '.md'] +source_suffix = ['.rst'] # The master toctree document. master_doc = 'index' # General information about the project. project = u'CoolAMQP' -copyright = u'2016-2017, DMS Serwis s.c.' +copyright = u'2016-2019, SMOK Serwis s.c.' author = u'DMS Serwis s.c.' # The version info for the project you're documenting, acts as replacement for diff --git a/docs/tutorial.md b/docs/tutorial.rst similarity index 62% rename from docs/tutorial.md rename to docs/tutorial.rst index 57e3704b7d20d363be94cfdef766286ad9d941d3..e0fc3e6a6bd4585dcc54b6bc57f979d84ad0e96d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.rst @@ -1,4 +1,6 @@ -# Tutorial +======== +Tutorial +======== If you want to connect to an AMQP broker, you need: * its address (and port) @@ -10,36 +12,42 @@ in the future, you should define the nodes first. You can do it using _NodeDefin See NodeDefinition's documentation for alternative ways to do this, but here we will use the AMQP connection string. -```python -from coolamqp.objects import NodeDefinition +.. autoclass:: coolamqp.objects.NodeDefinition + :members: -node = NodeDefinition('amqp://user@password:host/vhost') -``` +:: -_Cluster_ instances are used to interface with the cluster (or a single broker). It + from coolamqp.objects import NodeDefinition + + node = NodeDefinition('amqp://user@password:host/vhost') + +Cluster instances are used to interface with the cluster (or a single broker). It accepts a list of nodes: -```python -from coolamqp.clustering import Cluster -cluster = Cluster([node]) -cluster.start(wait=True) -``` +:: + from coolamqp.clustering import Cluster + cluster = Cluster([node]) + cluster.start(wait=True) + +.. autoclass:: coolamqp.clustering.Cluster + :members: -_wait=True_ will block until connection is completed. After this, you can use other methods. +*wait=True* will block until connection is completed. After this, you can use other methods. -## Publishing and consuming +Publishing and consuming +------------------------ Connecting is boring. After we do, we want to do something! Let's try sending a message, and receiving it. To do that, you must first define a queue, and register a consumer. -```python -from coolamqp.objects import Queue +:: -queue = Queue(u'my_queue', auto_delete=True, exclusive=True) + from coolamqp.objects import Queue -consumer, consume_confirm = cluster.consume(queue, no_ack=False) -consume_confirm.result() # wait for consuming to start -``` + queue = Queue(u'my_queue', auto_delete=True, exclusive=True) + + consumer, consume_confirm = cluster.consume(queue, no_ack=False) + consume_confirm.result() # wait for consuming to start This will create an auto-delete and exclusive queue. After than, a consumer will be registered for this queue. _no_ack=False_ will mean that we have to manually confirm messages. @@ -53,12 +61,15 @@ when AMQP _basic.consume-ok_ is received. To send a message we need to construct it first, and later publish: -```python -from coolamqp.objects import Message +:: + + from coolamqp.objects import Message + + msg = Message(b'hello world', properties=Message.Properties()) + cluster.publish(msg, routing_key=u'my_queue') -msg = Message(b'hello world', properties=Message.Properties()) -cluster.publish(msg, routing_key=u'my_queue') -``` +.. autoclass:: coolamqp.objects.Message + :members: This creates a message with no properties, and sends it through default (direct) exchange to our queue. Note that CoolAMQP simply considers your messages to be bags of bytes + properties. It will not modify them, diff --git a/requirements.txt b/requirements.txt index 897b05d1921931d68d0b05ef436e47211ec1df65..837c43015ff26d92692f31060f1d748c22d831c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ six monotonic futures +typing diff --git a/setup.cfg b/setup.cfg index 079e26a411dce271c2faafa3f85156d04dd8d881..4553be3f616520757b2fb049167f0a1eb8b29cf9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] description-file = README.md name = CoolAMQP -version = 0.97a1 +version = 0.97a2 license = MIT License classifiers = Programming Language :: Python