├── .editorconfig ├── .gitignore ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── circle.yml ├── flask_nameko ├── __init__.py ├── connection_pool.py ├── errors.py └── proxies.py ├── pytest.ini ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── test_connection_pool.py └── test_flask_pooled_cluster_rpc_proxy.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | venv/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | .envrc 62 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | `1.4.0 `__ (2017-09-10) 6 | ------------------------------------------------------------------------------------- 7 | 8 | Features 9 | ~~~~~~~~~~~~~ 10 | 11 | - **Python**: Python 3 support + fix pep8 voilations 12 | (`69d116 `__) 13 | 14 | `1.3.0 `__ (2016-09-20) 15 | ------------------------------------------------------------------------------------- 16 | 17 | Documentation 18 | ~~~~~~~~~~~~~ 19 | 20 | - **README**: adds docs for NAMEKO\_POOL\_RECYCLE 21 | (`a27a1be `__) 22 | 23 | Features 24 | ~~~~~~~~ 25 | 26 | - **ConnectionPool**: add pool recycling to eliminate closed connection 27 | issue 28 | (`4d90f1d `__) 29 | 30 | `1.2.0 `__ (2016-09-16) 31 | ------------------------------------------------------------------------------------- 32 | 33 | Features 34 | ~~~~~~~~ 35 | 36 | - **PooledClusterRpcProxy**: add support for NAMEKO\_RPC\_TIMEOUT 37 | setting 38 | (`d4d6042 `__) 39 | 40 | `1.1.0 `__ (2016-08-09) 41 | ------------------------------------------------------------------------------------- 42 | 43 | Features 44 | ~~~~~~~~ 45 | 46 | - **FlaskPooledClusterRpcProxy**: add lazy\_load\_services config 47 | (`26184f6 `__) 48 | 49 | `0.1.0 `__ (2016-07-28) 50 | ----------------------------------------------------------------------------------------------------------------------- 51 | 52 | Features 53 | ~~~~~~~~ 54 | 55 | - **flask\_nameko**: adds FlaskPooledClusterRpcProxy 56 | (`285df05 `__) 57 | 58 | 0.1.0 59 | ------------------ 60 | 61 | * First release on PyPI. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Jesse Pollak 2 | All rights reserved. 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst 2 | include LICENSE 3 | include README.md 4 | 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | 9 | recursive-include docs *.rst conf.py Makefile make.bat 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build clean 2 | define BROWSER_PYSCRIPT 3 | import os, webbrowser, sys 4 | try: 5 | from urllib import pathname2url 6 | except: 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 13 | 14 | help: 15 | @echo "clean - remove all build, test, coverage and Python artifacts" 16 | @echo "clean-build - remove build artifacts" 17 | @echo "clean-pyc - remove Python file artifacts" 18 | @echo "clean-test - remove test and coverage artifacts" 19 | @echo "lint - check style with flake8" 20 | @echo "test - run tests quickly with the default Python" 21 | @echo "watch-test - run tests every time a Python file is saved" 22 | @echo "test-all - run tests on every Python version with tox" 23 | @echo "coverage - check code coverage quickly with the default Python" 24 | @echo "release - package and upload a release to the internal PyPI server" 25 | @echo "dist - package" 26 | @echo "install - install the package to the active Python's site-packages" 27 | @echo "develop - bootstrap a development environment" 28 | 29 | clean: clean-build clean-pyc clean-test 30 | 31 | clean-build: 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | 36 | clean-pyc: 37 | find . -name '*.pyc' -exec rm -f {} + 38 | find . -name '*.pyo' -exec rm -f {} + 39 | find . -name '*~' -exec rm -f {} + 40 | find . -name '__pycache__' -exec rm -fr {} + 41 | 42 | clean-test: 43 | rm -fr .tox/ 44 | rm -f .coverage 45 | rm -fr htmlcov/ 46 | 47 | lint: 48 | flake8 flask_nameko tests 49 | 50 | develop: clean 51 | virtualenv --python=python2.7 venv 52 | . venv/bin/activate && pip install -r requirements_dev.txt 53 | 54 | test: 55 | python setup.py test 56 | 57 | watch-test: test 58 | watchmedo shell-command -p '*.py' -c '$(MAKE) test' -R -D . 59 | 60 | test-all: 61 | tox 62 | 63 | coverage: 64 | coverage run --source flask_nameko setup.py test 65 | coverage report -m 66 | coverage html 67 | $(BROWSER) htmlcov/index.html 68 | 69 | release: clean dist 70 | twine upload dist/* 71 | git push origin master && git push --tags 72 | 73 | dist: clean 74 | python setup.py sdist 75 | ls -l dist 76 | 77 | install: clean 78 | python setup.py install 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask_nameko 2 | 3 | A wrapper for using nameko services with Flask 4 | 5 | ## Installation 6 | 7 | Install it via PyPI: 8 | 9 | pip install flask_nameko 10 | 11 | ## Usage 12 | 13 | To start using `flask_nameko`, you need to create and configure a new `FlaskPooledClusterRpcProxy` singleton, which you'll use to communicate with your Nameko cluster. 14 | 15 | # __init__.py 16 | from flask import Flask 17 | from flask_nameko import FlaskPooledClusterRpcProxy 18 | 19 | rpc = FlaskPooledClusterRpcProxy() 20 | 21 | def create_app(): 22 | app = Flask(__name__) 23 | app.config.update(dict( 24 | NAMEKO_AMQP_URI='amqp://localhost' 25 | )) 26 | 27 | rpc.init_app(app) 28 | 29 | app = create_app() 30 | 31 | Then, you can use the `FlaskPooledClusterRpcProxy` singleton just as you would normally use a `ClusterRpcProxy`, by accessing individual services by name and calling methods on them: 32 | 33 | # routes.py 34 | 35 | from . import ( 36 | app, 37 | rpc 38 | ) 39 | 40 | @app.route('/'): 41 | def index(): 42 | result = rpc.service.do_something('test') 43 | return result 44 | 45 | ## API 46 | 47 | ### Configuration 48 | 49 | `FlaskPooledClusterRpcProxy` accepts all nameko configuration values, prefixed with the `NAMEKO_` prefix. In addition, it exposes additional configuration options: 50 | 51 | * `NAMEKO_INITIAL_CONNECTIONS (int, default=2)` - the number of initial connections to the Nameko cluster to create 52 | * `NAMEKO_MAX_CONNECTIONS (int, default=8)` - the max number of connections to the Nameko cluster to create before raises an error 53 | * `NAMEKO_CONNECT_ON_METHOD_CALL (bool, default=True)` - whether connections to services should be loaded when the service is accessed (False) or when a method is called on a service (True) 54 | * `NAMEKO_RPC_TIMEOUT` (int, default=None) - the default timeout before raising an error when trying to call a service RPC method 55 | * `NAMEKO_POOL_RECYCLE` (int, default=None) - if specified, connections that are older than this interval, specified in seconds, will be automatically recycled on checkout. This setting is useful for environments where connections are happening through a proxy like HAProxy which automatically closes connections after a specified interval. 56 | 57 | ### Proxies 58 | 59 | *flask_nameko.**FlaskPooledClusterRpcProxy**(app=None, connect_on_method_call=True)* 60 | 61 | This class is used to create a pool of connections to a Nameko cluster. It provides the following options: 62 | 63 | * `connect_on_method_call` - if this is true, the connection to a service is created when a method is called on a service rather than when the service is accessed 64 | 65 | *init_app(app=None)* 66 | 67 | Configure the proxy for a given app. 68 | 69 | ## Development 70 | 71 | $ git clone git@github.com:jessepollak/flask_nameko.git flask_nameko 72 | $ cd flask_nameko 73 | $ make develop 74 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | cache_directories: 3 | - .tox 4 | override: 5 | - pyenv versions 6 | - pyenv local 2.7.10 3.3.6 3.4.3 3.5.3 3.6.1 7 | - pip install tox tox-pyenv flake8 pep8-naming && tox --notest 8 | test: 9 | override: 10 | - tox 11 | -------------------------------------------------------------------------------- /flask_nameko/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'Jesse Pollak' 4 | __email__ = 'jesse@pollak.io' 5 | __version__ = '1.4.0' 6 | 7 | from .errors import * # noqa 8 | from .proxies import FlaskPooledClusterRpcProxy # noqa 9 | -------------------------------------------------------------------------------- /flask_nameko/connection_pool.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from datetime import datetime, timedelta 4 | from threading import Lock 5 | 6 | from six.moves.queue import Queue, Empty 7 | 8 | from .errors import ClientUnavailableError 9 | 10 | 11 | class Connection(object): 12 | def __init__(self, connection): 13 | self.connection = connection 14 | self._created_at = datetime.now() 15 | 16 | def is_stale(self, recycle_interval): 17 | return (datetime.now() - self._created_at) > recycle_interval 18 | 19 | def __getattr__(self, attr): 20 | return getattr(self.connection, attr) 21 | 22 | 23 | class ConnectionPool(object): 24 | def __init__( 25 | self, get_connection, initial_connections=2, max_connections=8, 26 | recycle=None 27 | ): 28 | """ 29 | Create a new pool 30 | :param func get_connection: The function that returns a connection 31 | :param int initial_connections: The initial number of connection 32 | objects to create 33 | :param int max_connections: The maximum amount of connections 34 | to create. These 35 | connections will only be created on demand and will potentially be 36 | destroyed once they have been returned via a call to 37 | :meth:`release_connection` 38 | constructor 39 | """ 40 | self._get_connection = get_connection 41 | self._queue = Queue() 42 | self._current_connections = 0 43 | self._max_connections = max_connections 44 | self._recycle = timedelta(seconds=recycle) if recycle else False 45 | self._lock = Lock() 46 | 47 | for x in range(initial_connections): 48 | connection = self._make_connection() 49 | self._queue.put(connection) 50 | 51 | def _make_connection(self): 52 | ret = Connection(self._get_connection()) 53 | self._current_connections += 1 54 | return ret 55 | 56 | def _delete_connection(self, connection): 57 | del connection 58 | self._current_connections -= 1 59 | 60 | def _recycle_connection(self, connection): 61 | self._lock.acquire() 62 | self._delete_connection(connection) 63 | connection = self._make_connection() 64 | self._lock.release() 65 | return connection 66 | 67 | def _get_connection_from_queue(self, initial_timeout, next_timeout): 68 | try: 69 | return self._queue.get(True, initial_timeout) 70 | except Empty: 71 | try: 72 | self._lock.acquire() 73 | if self._current_connections == self._max_connections: 74 | raise ClientUnavailableError("Too many connections in use") 75 | cb = self._make_connection() 76 | return cb 77 | except ClientUnavailableError as ex: 78 | try: 79 | return self._queue.get(True, next_timeout) 80 | except Empty: 81 | raise ex 82 | finally: 83 | self._lock.release() 84 | 85 | def get_connection(self, initial_timeout=0.05, next_timeout=1): 86 | """ 87 | Wait until a connection instance is available 88 | :param float initial_timeout: 89 | how long to wait initially for an existing connection to complete 90 | :param float next_timeout: 91 | if the pool could not obtain a connection during the 92 | initial timeout, and we have allocated the maximum available 93 | number of connections, wait this long until we can retrieve 94 | another one 95 | :return: A connection object 96 | """ 97 | connection = self._get_connection_from_queue( 98 | initial_timeout, next_timeout) 99 | 100 | if self._recycle and connection.is_stale(self._recycle): 101 | connection = self._recycle_connection(connection) 102 | 103 | return connection 104 | 105 | def release_connection(self, cb): 106 | """ 107 | Return a Connection object to the pool 108 | :param Connection cb: the connection to release 109 | """ 110 | self._queue.put(cb, True) 111 | -------------------------------------------------------------------------------- /flask_nameko/errors.py: -------------------------------------------------------------------------------- 1 | class BadConfigurationError(Exception): 2 | pass 3 | 4 | 5 | class ClientUnavailableError(Exception): 6 | pass 7 | 8 | 9 | class ClusterNotConfiguredError(Exception): 10 | pass 11 | -------------------------------------------------------------------------------- /flask_nameko/proxies.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import re 4 | 5 | import six 6 | from flask import g 7 | from nameko.standalone.rpc import ClusterRpcProxy 8 | 9 | from .connection_pool import ConnectionPool 10 | from .errors import ( 11 | BadConfigurationError, 12 | ClusterNotConfiguredError 13 | ) 14 | 15 | 16 | class PooledClusterRpcProxy(object): 17 | 18 | _pool = None 19 | _config = None 20 | 21 | def __init__(self, config=None): 22 | if config: 23 | self.configure(config) 24 | 25 | def configure(self, config): 26 | if not config.get('AMQP_URI'): 27 | raise BadConfigurationError( 28 | "Please provide a valid configuration.") 29 | 30 | self._config = config 31 | self._pool = ConnectionPool( 32 | self._get_nameko_connection, 33 | initial_connections=config.get('INITIAL_CONNECTIONS', 2), 34 | max_connections=config.get('MAX_CONNECTIONS', 8), 35 | recycle=config.get('POOL_RECYCLE') 36 | ) 37 | 38 | def _get_nameko_connection(self): 39 | proxy = ClusterRpcProxy( 40 | self._config, 41 | timeout=self._config.get('RPC_TIMEOUT', None) 42 | ) 43 | return proxy.start() 44 | 45 | def get_connection(self): 46 | if not self._pool: 47 | raise ClusterNotConfiguredError( 48 | "Please configure your cluster beore requesting a connection.") 49 | return self._pool.get_connection() 50 | 51 | def release_connection(self, connection): 52 | return self._pool.release_connection(connection) 53 | 54 | 55 | class LazyServiceProxy(object): 56 | def __init__(self, get_connection, service): 57 | self.get_connection = get_connection 58 | self.service = service 59 | 60 | def __getattr__(self, name): 61 | return getattr(getattr(self.get_connection(), self.service), name) 62 | 63 | 64 | class FlaskPooledClusterRpcProxy(PooledClusterRpcProxy): 65 | def __init__(self, app=None, connect_on_method_call=True): 66 | self._connect_on_method_call = connect_on_method_call 67 | if app: 68 | self.init_app(app) 69 | 70 | def init_app(self, app): 71 | config = dict() 72 | for key, val in six.iteritems(app.config): 73 | match = re.match(r"NAMEKO\_(?P.*)", key) 74 | if match: 75 | config[match.group('name')] = val 76 | self.configure(config) 77 | 78 | self._connect_on_method_call = config.get( 79 | 'NAMEKO_CONNECT_ON_METHOD_CALL', 80 | self._connect_on_method_call 81 | ) 82 | 83 | app.teardown_appcontext(self._teardown_nameko_connection) 84 | 85 | def get_connection(self): 86 | connection = getattr(g, '_nameko_connection', None) 87 | if not connection: 88 | connection = super( 89 | FlaskPooledClusterRpcProxy, self).get_connection() 90 | g._nameko_connection = connection 91 | return connection 92 | 93 | def _teardown_nameko_connection(self, exception): 94 | connection = getattr(g, '_nameko_connection', None) 95 | if connection is not None: 96 | self.release_connection(connection) 97 | 98 | def _get_service(self, service): 99 | if self._connect_on_method_call: 100 | return LazyServiceProxy(lambda: self.get_connection(), service) 101 | else: 102 | return getattr(self.get_connection(), service) 103 | 104 | def __getattr__(self, name): 105 | return self._get_service(name) 106 | 107 | def __getitem__(self, name): 108 | return self._get_service(name) 109 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = venv .git *.egg *site-packages* 3 | addopts = -s 4 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | bumpversion==0.5.3 2 | wheel==0.23.0 3 | watchdog==0.8.3 4 | flake8==2.4.1 5 | tox==2.1.1 6 | pep8-naming==0.4.1 7 | tox-pyenv==1.0.3 8 | coverage==4.0 9 | Sphinx==1.3.1 10 | twine==1.6.3 11 | mock==2.0.0 12 | 13 | -e . 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.3.0 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-?(?P[a-z]+)\.(?P\d+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{release}.{release_num} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:part:release] 11 | optional_value = production 12 | values = 13 | dev 14 | production 15 | 16 | [bumpversion:file:setup.py] 17 | 18 | [bumpversion:file:flask_nameko/__init__.py] 19 | 20 | [wheel] 21 | universal = 1 22 | 23 | [flake8] 24 | exclude = docs 25 | 26 | [aliases] 27 | test = pytest 28 | 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | 11 | with open('README.md') as readme_file: 12 | readme = readme_file.read() 13 | 14 | with open('HISTORY.rst') as history_file: 15 | history = history_file.read() 16 | 17 | requirements = [ 18 | # TODO: put package requirements here 19 | # 20 | # These requirements should be pinned according to SemVer. 21 | # i.e. 22 | # "left_pad>=1.0, <2.0" 23 | "nameko", 24 | "flask", 25 | "six" 26 | ] 27 | 28 | test_requirements = [ 29 | # TODO: put package test requirements here 30 | "mock==2.0.0", 31 | "pytest" 32 | ] 33 | 34 | setup( 35 | name='flask_nameko', 36 | version='1.4.0', 37 | description="A wrapper for using nameko services with Flask", 38 | long_description=readme + '\n\n' + history, 39 | author="Jesse Pollak", 40 | author_email='jesse@getclef.com', 41 | url='https://github.com/clef/flask_nameko', 42 | packages=[ 43 | 'flask_nameko', 44 | ], 45 | package_dir={'flask_nameko': 46 | 'flask_nameko'}, 47 | include_package_data=True, 48 | install_requires=requirements, 49 | license="ISCL", 50 | zip_safe=False, 51 | keywords='flask_nameko', 52 | classifiers=[], 53 | test_suite='tests', 54 | tests_require=test_requirements, 55 | setup_requires=['pytest-runner'] 56 | ) 57 | -------------------------------------------------------------------------------- /tests/test_connection_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from datetime import timedelta 5 | 6 | import eventlet 7 | import pytest 8 | from mock import Mock, patch 9 | 10 | from flask_nameko.connection_pool import Connection, ConnectionPool 11 | from flask_nameko.errors import ClientUnavailableError 12 | 13 | 14 | @pytest.fixture 15 | def some_fixture(): 16 | pass 17 | 18 | 19 | @pytest.fixture 20 | def get_connection(): 21 | connection = Mock(side_effect=lambda: object()) 22 | return connection 23 | 24 | 25 | def test_connections_recycled(get_connection): 26 | pool = ConnectionPool(get_connection, initial_connections=0) 27 | 28 | o = pool.get_connection() 29 | pool.release_connection(o) 30 | o1 = pool.get_connection() 31 | o2 = pool.get_connection() 32 | 33 | assert o1 == o 34 | assert o1 != o2 35 | 36 | 37 | def test_new_connections_used(get_connection): 38 | pool = ConnectionPool(get_connection, initial_connections=0) 39 | 40 | o = pool.get_connection() 41 | o1 = pool.get_connection() 42 | 43 | assert o1 != o 44 | 45 | 46 | def test_max_connections_raises(get_connection): 47 | pool = ConnectionPool( 48 | get_connection, initial_connections=0, max_connections=2) 49 | 50 | pool.get_connection() 51 | pool.get_connection() 52 | 53 | with pytest.raises(ClientUnavailableError): 54 | pool.get_connection(next_timeout=0) 55 | 56 | 57 | def test_creates_initial_connections(get_connection): 58 | ConnectionPool(get_connection, initial_connections=2) 59 | assert get_connection.call_count == 2 60 | 61 | 62 | def test_connections_get_recycled(get_connection): 63 | pool = ConnectionPool( 64 | get_connection, 65 | initial_connections=1, 66 | max_connections=1, 67 | recycle=3600 68 | ) 69 | 70 | conn = pool.get_connection() 71 | pool.release_connection(conn) 72 | conn2 = pool.get_connection() 73 | pool.release_connection(conn2) 74 | 75 | assert conn == conn2 76 | 77 | with patch.object(conn2, 'is_stale', return_value=True): 78 | conn3 = pool.get_connection() 79 | 80 | assert conn3 != conn 81 | assert conn3 != conn2 82 | 83 | 84 | def test_connection_is_stale_for_stale_connection(): 85 | connection = Connection(None) 86 | eventlet.sleep(2) 87 | assert connection.is_stale(timedelta(seconds=1)) 88 | 89 | 90 | def test_connection_is_not_stale_for_good_connection(): 91 | connection = Connection(None) 92 | assert not connection.is_stale(timedelta(seconds=3600)) 93 | -------------------------------------------------------------------------------- /tests/test_flask_pooled_cluster_rpc_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import pytest 4 | from flask import Flask 5 | from mock import ANY, MagicMock, Mock, patch 6 | from nameko.standalone.rpc import ClusterRpcProxy 7 | 8 | from flask_nameko import FlaskPooledClusterRpcProxy 9 | from flask_nameko.connection_pool import ConnectionPool 10 | from flask_nameko.errors import ClusterNotConfiguredError 11 | from flask_nameko.proxies import LazyServiceProxy 12 | 13 | 14 | @pytest.fixture 15 | def some_fixture(): 16 | pass 17 | 18 | 19 | @pytest.fixture 20 | def flask_app(): 21 | app = Flask(__name__) 22 | app.config.update(dict(NAMEKO_AMQP_URI='test')) 23 | return app 24 | 25 | 26 | def test_configuration_pulls_nameko_names(flask_app): 27 | with patch.object(FlaskPooledClusterRpcProxy, 'configure') as configure: 28 | FlaskPooledClusterRpcProxy(flask_app) 29 | configure.assert_called_once_with(dict(AMQP_URI='test')) 30 | 31 | 32 | def test_not_configured_raises_exception(flask_app): 33 | rpc = FlaskPooledClusterRpcProxy() 34 | with flask_app.test_request_context(): 35 | with pytest.raises(ClusterNotConfiguredError): 36 | rpc.not_configured_service.test() 37 | 38 | 39 | def test_connection_is_reused_in_app_context(flask_app): 40 | with patch('flask_nameko.proxies.ClusterRpcProxy'): 41 | rpc = FlaskPooledClusterRpcProxy( 42 | flask_app, connect_on_method_call=False 43 | ) 44 | with flask_app.test_request_context(): 45 | connection = rpc.get_connection() 46 | connection1 = rpc.get_connection() 47 | assert connection == connection1 48 | 49 | 50 | def test_connection_is_returned_and_reused_when_app_context_ends(flask_app): 51 | flask_app.config.update(dict(NAMEKO_INITIAL_CONNECTIONS=1)) 52 | with patch('flask_nameko.proxies.ClusterRpcProxy'): 53 | rpc = FlaskPooledClusterRpcProxy( 54 | flask_app, connect_on_method_call=False 55 | ) 56 | with flask_app.test_request_context(): 57 | connection = rpc.get_connection() 58 | with flask_app.test_request_context(): 59 | connection1 = rpc.get_connection() 60 | assert connection1 == connection 61 | 62 | 63 | def test_new_connection_is_used_for_new_app_context(flask_app): 64 | class FakeClusterRpcProxy(object): 65 | n = 0 66 | 67 | def start(self): 68 | self.n = self.n + 1 69 | return self.n 70 | 71 | mock = FakeClusterRpcProxy() 72 | with patch('flask_nameko.proxies.ClusterRpcProxy', return_value=mock): 73 | 74 | rpc = FlaskPooledClusterRpcProxy( 75 | flask_app, connect_on_method_call=False 76 | ) 77 | 78 | with flask_app.test_request_context(): 79 | connection = rpc.get_connection() 80 | 81 | with flask_app.test_request_context(): 82 | connection1 = rpc.get_connection() 83 | 84 | assert connection1 != connection 85 | 86 | 87 | def test_connect_on_method_call_false_returns_connection(flask_app): 88 | with flask_app.test_request_context(): 89 | with patch( 90 | 'flask_nameko.proxies.ClusterRpcProxy', return_value=MagicMock() 91 | ): 92 | rpc = FlaskPooledClusterRpcProxy( 93 | flask_app, connect_on_method_call=False 94 | ) 95 | assert isinstance(rpc.service, Mock) 96 | 97 | 98 | def test_connect_on_method_call_returns_lazy_proxy(flask_app): 99 | with patch( 100 | 'flask_nameko.proxies.ClusterRpcProxy', return_value=MagicMock() 101 | ): 102 | rpc = FlaskPooledClusterRpcProxy( 103 | flask_app, connect_on_method_call=True 104 | ) 105 | assert isinstance(rpc.service, LazyServiceProxy) 106 | 107 | 108 | def test_timeout_is_passed_through_to_cluster(flask_app): 109 | flask_app.config.update(dict(NAMEKO_RPC_TIMEOUT=10)) 110 | with patch( 111 | 'flask_nameko.proxies.ClusterRpcProxy', spec_set=ClusterRpcProxy 112 | ) as mock: 113 | FlaskPooledClusterRpcProxy(flask_app, connect_on_method_call=True) 114 | mock.assert_called_with(ANY, timeout=10) 115 | 116 | 117 | def test_pool_recycle_is_passed_through_to_cluster(flask_app): 118 | flask_app.config.update(dict(NAMEKO_POOL_RECYCLE=3600)) 119 | with patch( 120 | 'flask_nameko.proxies.ConnectionPool', spec_set=ConnectionPool 121 | ) as mock: 122 | FlaskPooledClusterRpcProxy(flask_app) 123 | mock.assert_called_with( 124 | ANY, initial_connections=ANY, max_connections=ANY, recycle=3600 125 | ) 126 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35,py36 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/flask_nameko 7 | commands = 8 | flake8 9 | python setup.py test 10 | 11 | ; If you want to make tox run the tests with the same versions, create a 12 | ; requirements.txt with the pinned versions and uncomment the following lines: 13 | deps = 14 | -r{toxinidir}/requirements_dev.txt 15 | --------------------------------------------------------------------------------