# HG changeset patch # User Thierry Florac # Date 1425505207 -3600 # Node ID 11f0f97d508febb66d0327272b6058519967b72e First commit diff -r 000000000000 -r 11f0f97d508f .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,19 @@ + +syntax: regexp +^develop-eggs$ +syntax: regexp +^parts$ +syntax: regexp +^bin$ +syntax: regexp +^\.installed\.cfg$ +syntax: regexp +^\.settings$ +syntax: regexp +^build$ +syntax: regexp +^dist$ +syntax: regexp +^\.idea$ +syntax: regexp +.*\.pyc$ diff -r 000000000000 -r 11f0f97d508f LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,42 @@ +Zope Public License (ZPL) Version 2.1 +===================================== + +A copyright notice accompanies this license document that identifies +the copyright holders. + +This license has been certified as open source. It has also been designated +as GPL compatible by the Free Software Foundation (FSF). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions in source code must retain the accompanying copyright + notice, this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the accompanying copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Names of the copyright holders must not be used to endorse or promote + products derived from this software without prior written permission + from the copyright holders. + 4. The right to distribute this software or to use it for any purpose does + not give you the right to use Servicemarks (sm) or Trademarks (tm) of the + copyright holders. Use of them is covered by separate agreement with the + copyright holders. + 5. If any files are modified, you must cause the modified files to carry + prominent notices stating that you changed the files and the date of any + change. + + +Disclaimer +========== + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED +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 HOLDERS 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. diff -r 000000000000 -r 11f0f97d508f MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,5 @@ +include *.txt +recursive-include docs * +recursive-include src * +global-exclude *.pyc +global-exclude *.*~ diff -r 000000000000 -r 11f0f97d508f bootstrap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bootstrap.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,178 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os +import shutil +import sys +import tempfile + +from optparse import OptionParser + +tmpeggs = tempfile.mkdtemp() + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --find-links to point to local resources, you can keep +this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", help="use a specific zc.buildout version") + +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", "--config-file", + help=("Specify the path to the buildout configuration " + "file to be used.")) +parser.add_option("-f", "--find-links", + help=("Specify a URL to search for buildout releases")) +parser.add_option("--allow-site-packages", + action="store_true", default=False, + help=("Let bootstrap.py use existing site packages")) + + +options, args = parser.parse_args() + +###################################################################### +# load/install setuptools + +try: + if options.allow_site_packages: + import setuptools + import pkg_resources + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +ez = {} +exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) + +if not options.allow_site_packages: + # ez_setup imports site, which adds site packages + # this will remove them from the path to ensure that incompatible versions + # of setuptools are not in the path + import site + # inside a virtualenv, there is no 'getsitepackages'. + # We can't remove these reliably + if hasattr(site, 'getsitepackages'): + for sitepackage_path in site.getsitepackages(): + sys.path[:] = [x for x in sys.path if sitepackage_path not in x] + +setup_args = dict(to_dir=tmpeggs, download_delay=0) +ez['use_setuptools'](**setup_args) +import setuptools +import pkg_resources + +# This does not (always?) update the default working set. We will +# do it. +for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +###################################################################### +# Install buildout + +ws = pkg_resources.working_set + +cmd = [sys.executable, '-c', + 'from setuptools.command.easy_install import main; main()', + '-mZqNxd', tmpeggs] + +find_links = os.environ.get( + 'bootstrap-testing-find-links', + options.find_links or + ('http://downloads.buildout.org/' + if options.accept_buildout_test_releases else None) + ) +if find_links: + cmd.extend(['-f', find_links]) + +setuptools_path = ws.find( + pkg_resources.Requirement.parse('setuptools')).location + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + + def _final_version(parsed_version): + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( + search_path=[setuptools_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +import subprocess +if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: + raise Exception( + "Failed to execute command:\n%s" % repr(cmd)[1:-1]) + +###################################################################### +# Import and run buildout + +ws.add_entry(tmpeggs) +ws.require(requirement) +import zc.buildout.buildout + +if not [a for a in args if '=' not in a]: + args.append('bootstrap') + +# if -c was provided, we push it back into args for buildout' main function +if options.config_file is not None: + args[0:0] = ['-c', options.config_file] + +zc.buildout.buildout.main(args) +shutil.rmtree(tmpeggs) diff -r 000000000000 -r 11f0f97d508f buildout.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/buildout.cfg Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,59 @@ +[buildout] +eggs-directory = /var/local/env/pyams/eggs + +socket-timeout = 3 +show-picked-versions = true +newest = false + +allow-hosts = + bitbucket.org + *.python.org + *.sourceforge.net + github.com + +#extends = http://download.ztfy.org/webapp/ztfy.webapp.dev.cfg +versions = versions +newest = false +#allow-picked-versions = false + +src = src +develop = . + +parts = + package + i18n + pyflakes + test + +[package] +recipe = zc.recipe.egg +eggs = + pyams_zmq + pyramid + zope.component + zope.interface + +[i18n] +recipe = zc.recipe.egg +eggs = + babel + lingua + +[pyflakes] +recipe = zc.recipe.egg +eggs = pyflakes +scripts = pyflakes +entry-points = pyflakes=pyflakes.scripts.pyflakes:main +initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"] + +[pyflakesrun] +recipe = collective.recipe.cmd +on_install = true +cmds = ${buildout:develop}/bin/${pyflakes:scripts} + +[test] +recipe = zc.recipe.testrunner +eggs = pyams_zmq [test] + +[versions] +pyams_base = 0.1.0 diff -r 000000000000 -r 11f0f97d508f docs/HISTORY.txt diff -r 000000000000 -r 11f0f97d508f docs/README.txt diff -r 000000000000 -r 11f0f97d508f setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,66 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +This module contains pyams_zmq package +""" +import os +from setuptools import setup, find_packages + +DOCS = os.path.join(os.path.dirname(__file__), + 'docs') + +README = os.path.join(DOCS, 'README.txt') +HISTORY = os.path.join(DOCS, 'HISTORY.txt') + +version = '0.1.0' +long_description = open(README).read() + '\n\n' + open(HISTORY).read() + +tests_require = [] + +setup(name='pyams_zmq', + version=version, + description="PyAMS package for 0MQ", + long_description=long_description, + classifiers=[ + "License :: OSI Approved :: Zope Public License", + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='Pyramid PyAMS 0MQ', + author='Thierry Florac', + author_email='tflorac@ulthar.net', + url='http://hg.ztfy.org/pyams/pyams_zmq', + license='ZPL', + packages=find_packages('src'), + package_dir={'': 'src'}, + namespace_packages=[], + include_package_data=True, + package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo', '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']}, + zip_safe=False, + # uncomment this to be able to run tests with setup.py + test_suite="pyams_zmq.tests.test_utilsdocs.test_suite", + tests_require=tests_require, + extras_require=dict(test=tests_require), + install_requires=[ + 'setuptools', + # -*- Extra requirements: -*- + 'pyramid', + 'pyzmq', + 'zope.component', + 'zope.interface', + ], + entry_points=""" + # -*- Entry points: -*- + """) diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/PKG-INFO --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/PKG-INFO Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,18 @@ +Metadata-Version: 1.1 +Name: pyams-zmq +Version: 0.1.0 +Summary: PyAMS package for 0MQ +Home-page: http://hg.ztfy.org/pyams/pyams_zmq +Author: Thierry Florac +Author-email: tflorac@ulthar.net +License: ZPL +Description: + + +Keywords: Pyramid PyAMS 0MQ +Platform: UNKNOWN +Classifier: License :: OSI Approved :: Zope Public License +Classifier: Development Status :: 4 - Beta +Classifier: Programming Language :: Python +Classifier: Framework :: Pyramid +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/SOURCES.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/SOURCES.txt Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,21 @@ +MANIFEST.in +setup.py +docs/HISTORY.txt +docs/README.txt +src/pyams_zmq/__init__.py +src/pyams_zmq/configure.zcml +src/pyams_zmq/handler.py +src/pyams_zmq/process.py +src/pyams_zmq.egg-info/PKG-INFO +src/pyams_zmq.egg-info/SOURCES.txt +src/pyams_zmq.egg-info/dependency_links.txt +src/pyams_zmq.egg-info/entry_points.txt +src/pyams_zmq.egg-info/namespace_packages.txt +src/pyams_zmq.egg-info/not-zip-safe +src/pyams_zmq.egg-info/requires.txt +src/pyams_zmq.egg-info/top_level.txt +src/pyams_zmq/doctests/README.txt +src/pyams_zmq/interfaces/__init__.py +src/pyams_zmq/tests/__init__.py +src/pyams_zmq/tests/test_utilsdocs.py +src/pyams_zmq/tests/test_utilsdocstrings.py \ No newline at end of file diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/dependency_links.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/dependency_links.txt Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/entry_points.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/entry_points.txt Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,3 @@ + + # -*- Entry points: -*- + \ No newline at end of file diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/namespace_packages.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/namespace_packages.txt Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/not-zip-safe --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/not-zip-safe Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/requires.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/requires.txt Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,7 @@ +setuptools +pyramid +pyzmq +zope.component +zope.interface + +[test] diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq.egg-info/top_level.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq.egg-info/top_level.txt Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,1 @@ +pyams_zmq diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/__init__.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,30 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +from pyramid.i18n import TranslationStringFactory +_ = TranslationStringFactory('pyams_zmq') + + +def includeme(config): + """Pyramid include""" + + # add translations + config.add_translation_dirs('pyams_zmq:locales') + + # load registry components + config.scan() + + if hasattr(config, 'load_zcml'): + config.load_zcml('configure.zcml') diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/configure.zcml Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/doctests/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/doctests/README.txt Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,100 @@ +================= +pyams_zmq package +================= + +PyAMS 'ZMQ' package can be used to build wrapper around ØMQ (or ZeroMQ) library to exchange +messages following all ØMQ possible usages. + +At least two components are required to build a ØMQ based application: + +- a ØMQ server + +- a ØMQ client + +The way client and server communicate depends on used ØMQ protocol. + +We will take example on the medias conversion utility provided by 'pyams_media' package, which allows you +to automatically convert medias files (videos...) asynchronously as soon as they are uploaded. The conversion +process is a background process so doesn't return any result. + +The conversion process is a simple ØMQ process: + + >>> converter_address = '127.0.0.1:5555' + + >>> from pyams_zmq.process import ZMQProcess, process_exit_func + >>> + >>> class MediasConversionProcess(ZMQProcess): + ... """Medias conversion process""" + + >>> from multiprocessing import Process + >>> + >>> class ConversionProcess(Process): + ... """Conversion manager process""" + ... + ... def __init__(self, settings, group=None, target=None, name=None, *args, **kwargs): + ... Process.__init__(self, group=group, target=target, name=name, args=args, kwargs=kwargs) + ... self.settings = settings + ... + ... def run(self): + ... settings = self.settings + ... path = settings['path'] + ... format = settings['format'] + ... # just image you're doing anything you want with these settings! + +To be sure to run asynchronously, this process is managed by a thread: + + >>> import time + >>> from threading import Thread + >>> + >>> class ConversionThread(Thread): + ... """Conversion thread""" + ... + ... def __init__(self, process): + ... Thread.__init__(self) + ... self.process = process + ... + ... def run(self): + ... self.process.start() + ... self.process.join() + +The conversion handler is the simple class to which conversion is delegated: + + >>> class ConversionHandler(object): + ... """Conversion handler""" + ... + ... def convert(self, data): + ... ConversionThread(ConversionProcess(data)).start() + ... return [200, 'OK'] + +The message handler receives the message and handle it: + + >>> from pyams_zmq.handler import ZMQMessageHandler + >>> + >>> class ConversionMessageHandler(ZMQMessageHandler): + ... + ... handler = ConversionHandler + +The ØMQ process is generally started on application startup: + + >>> import atexit + >>> + >>> process = MediasConversionProcess(converter_address, ConversionMessageHandler) + >>> process.start() + >>> time.sleep(2) + >>> if process.is_alive(): + ... atexit.register(process_exit_func, process=process) + + +Once all these elements are in place, you just have to create a ØMQ client context, open a connection and send a +message: + + >>> import zmq + >>> settings = {'path': '/this/is/my/path', + ... 'format': 'JPEG'} + >>> message = ['convert', settings] + >>> context = zmq.Context() + >>> socket = context.socket(zmq.REQ) + >>> socket.connect('tcp://' + converter_address) + >>> socket.send_json(message) + >>> socket.recv_json() + [200, 'OK'] diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/handler.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/handler.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,63 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from zmq.utils import jsonapi + +# import interfaces +from pyams_zmq.interfaces import IZMQMessageHandler + +# import packages +from zope.interface import implementer + + +@implementer(IZMQMessageHandler) +class ZMQMessageHandler(object): + """Base class for message handlers for a :class:`pyams_zmq.process.Process`. + + Inheriting classes only need to implement a handler function for each + message type. + """ + + handler = None + + def __init__(self, process, stream, stop, handler=None, json_load=-1): + # ZMQ parent process + self.process = process + self._json_load = json_load + # Response stream + self.rep_stream = stream + self._stop = stop + # Response handler + self.rep_handler = handler or self.handler() + self.rep_handler.process = process + + def __call__(self, msg): + """ + Gets called when a messages is received by the stream this handlers is + registered at. *msg* is a list as returned by + :meth:`zmq.core.socket.Socket.recv_multipart`. + """ + # Try to JSON-decode the index "self._json_load" of the message + i = self._json_load + msg_type, data = jsonapi.loads(msg[i]) + msg[i] = data + + # Get the actual message handler and call it + if msg_type.startswith('_'): + raise AttributeError('%s starts with an "_"' % msg_type) + + rep = getattr(self.rep_handler, msg_type)(*msg) + self.rep_stream.send_json(rep) diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/interfaces/__init__.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,47 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + +# import standard library + +# import interfaces +from zope.interface import Interface, Attribute + +# import packages + + +class IZMQProcess(Interface): + """ZeroMQ process interface""" + + socket_type = Attribute("Socket type") + + def setup(self): + """Initialize process context and events loop and initialize stream""" + + def stream(self, sock_type, addr, bind, callback=None, subscribe=b''): + """Create ZMQStream""" + + def init_stream(self): + """initialize response stream""" + + def start(self): + """Start the process""" + + def stop(self): + """Stop the process""" + + +class IZMQMessageHandler(Interface): + """ZeroMQ message handler""" + + handler = Attribute("Concrete message handler") diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/process.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/process.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,141 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import multiprocessing +import signal +import sys +import zmq +from zmq.eventloop import ioloop, zmqstream + +# import interfaces +from pyams_zmq.interfaces import IZMQProcess + +# import packages +from zope.interface import implementer + + +@implementer(IZMQProcess) +class ZMQProcess(multiprocessing.Process): + """ + This is the base for all processes and offers utility methods + for setup and creating new streams. + """ + + socket_type = zmq.REP + + def __init__(self, bind_addr, handler): + super(ZMQProcess, self).__init__() + + self.context = None + """The ØMQ :class:`~zmq.Context` instance.""" + + self.loop = None + """PyZMQ's event loop (:class:`~zmq.eventloop.ioloop.IOLoop`).""" + + self.bind_addr = bind_addr + self.rep_stream = None + self.handler = handler + + def setup(self): + """Creates a :attr:`context` and an event :attr:`loop` for the process.""" + self.context = zmq.Context() + self.loop = ioloop.IOLoop.instance() + self.rep_stream, _ = self.stream(self.socket_type, self.bind_addr, bind=True) + self.initStream() + + def initStream(self): + """Initialize response stream""" + self.rep_stream.on_recv(self.handler(self, self.rep_stream, self.stop)) + + def run(self): + """Sets up everything and starts the event loop.""" + signal.signal(signal.SIGTERM, self.exit) + self.setup() + self.loop.start() + + def stop(self): + """Stops the event loop.""" + if self.loop is not None: + self.loop.stop() + self.loop = None + + def exit(self, num, frame): + self.stop() + sys.exit() + + def stream(self, sock_type, addr, bind, callback=None, subscribe=b''): + """ + Creates a :class:`~zmq.eventloop.zmqstream.ZMQStream`. + + :param sock_type: The ØMQ socket type (e.g. ``zmq.REQ``) + :param addr: Address to bind or connect to formatted as *host:port*, + *(host, port)* or *host* (bind to random port). + If *bind* is ``True``, *host* may be: + + - the wild-card ``*``, meaning all available interfaces, + - the primary IPv4 address assigned to the interface, in its + numeric representation or + - the interface name as defined by the operating system. + + If *bind* is ``False``, *host* may be: + + - the DNS name of the peer or + - the IPv4 address of the peer, in its numeric representation. + + If *addr* is just a host name without a port and *bind* is + ``True``, the socket will be bound to a random port. + :param bind: Binds to *addr* if ``True`` or tries to connect to it + otherwise. + :param callback: A callback for + :meth:`~zmq.eventloop.zmqstream.ZMQStream.on_recv`, optional + :param subscribe: Subscription pattern for *SUB* sockets, optional, + defaults to ``b''``. + :returns: A tuple containg the stream and the port number. + + """ + sock = self.context.socket(sock_type) + + # addr may be 'host:port' or ('host', port) + if isinstance(addr, str): + addr = addr.split(':') + host, port = addr if len(addr) == 2 else (addr[0], None) + + # Bind/connect the socket + if bind: + if port: + sock.bind('tcp://%s:%s' % (host, port)) + else: + port = sock.bind_to_random_port('tcp://%s' % host) + else: + sock.connect('tcp://%s:%s' % (host, port)) + + # Add a default subscription for SUB sockets + if sock_type == zmq.SUB: + sock.setsockopt(zmq.SUBSCRIBE, subscribe) + + # Create the stream and add the callback + stream = zmqstream.ZMQStream(sock, self.loop) + if callback: + stream.on_recv(callback) + + return stream, int(port) + + +def process_exit_func(process=None): + if process is not None: + if process.is_alive(): + process.terminate() + process.join() diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/tests/__init__.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/tests/test_utilsdocs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/tests/test_utilsdocs.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,59 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +Generic Test case for pyams_zmq doctest +""" +__docformat__ = 'restructuredtext' + +import unittest +import doctest +import sys +import os + + +current_dir = os.path.dirname(__file__) + +def doc_suite(test_dir, setUp=None, tearDown=None, globs=None): + """Returns a test suite, based on doctests found in /doctest.""" + suite = [] + if globs is None: + globs = globals() + + flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | + doctest.REPORT_ONLY_FIRST_FAILURE) + + package_dir = os.path.split(test_dir)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + + doctest_dir = os.path.join(package_dir, 'doctests') + + # filtering files on extension + docs = [os.path.join(doctest_dir, doc) for doc in + os.listdir(doctest_dir) if doc.endswith('.txt')] + + for test in docs: + suite.append(doctest.DocFileSuite(test, optionflags=flags, + globs=globs, setUp=setUp, + tearDown=tearDown, + module_relative=False)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff -r 000000000000 -r 11f0f97d508f src/pyams_zmq/tests/test_utilsdocstrings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_zmq/tests/test_utilsdocstrings.py Wed Mar 04 22:40:07 2015 +0100 @@ -0,0 +1,62 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +Generic Test case for pyams_zmq doc strings +""" +__docformat__ = 'restructuredtext' + +import unittest +import doctest +import sys +import os + + +current_dir = os.path.abspath(os.path.dirname(__file__)) + +def doc_suite(test_dir, globs=None): + """Returns a test suite, based on doc tests strings found in /*.py""" + suite = [] + if globs is None: + globs = globals() + + flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | + doctest.REPORT_ONLY_FIRST_FAILURE) + + package_dir = os.path.split(test_dir)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + + # filtering files on extension + docs = [doc for doc in + os.listdir(package_dir) if doc.endswith('.py')] + docs = [doc for doc in docs if not doc.startswith('__')] + + for test in docs: + fd = open(os.path.join(package_dir, test)) + content = fd.read() + fd.close() + if '>>> ' not in content: + continue + test = test.replace('.py', '') + location = 'pyams_zmq.%s' % test + suite.append(doctest.DocTestSuite(location, optionflags=flags, + globs=globs)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite')