mirror of https://github.com/celery/kombu.git
Control pattern matching (#997)
* Added pattern/matcher to Mailbox * pattern/match for kombu 4 * Ensure kombu.matcher is covered by our documentation. * Adds test_matcher & pidbox unit tests. * Added tests to ensure exception is raised when matcher is not registered. * Adds to test for destination passed in to process.
This commit is contained in:
parent
eb6e4c8d51
commit
41dbbe3063
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
kombu
|
kombu
|
||||||
kombu.common
|
kombu.common
|
||||||
|
kombu.matcher
|
||||||
kombu.mixins
|
kombu.mixins
|
||||||
kombu.simple
|
kombu.simple
|
||||||
kombu.clocks
|
kombu.clocks
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
==============================================
|
||||||
|
Pattern matching registry - ``kombu.matcher``
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
.. currentmodule:: kombu.matcher
|
||||||
|
|
||||||
|
.. automodule:: kombu.matcher
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
|
@ -0,0 +1,140 @@
|
||||||
|
"""Pattern matching registry."""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from re import match as rematch
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
|
from .utils.compat import entrypoints
|
||||||
|
from .utils.encoding import bytes_to_str
|
||||||
|
|
||||||
|
|
||||||
|
class MatcherNotInstalled(Exception):
|
||||||
|
"""Matcher not installed/found."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MatcherRegistry(object):
|
||||||
|
"""Pattern matching function registry."""
|
||||||
|
|
||||||
|
MatcherNotInstalled = MatcherNotInstalled
|
||||||
|
matcher_pattern_first = ["pcre", ]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._matchers = {}
|
||||||
|
self._default_matcher = None
|
||||||
|
|
||||||
|
def register(self, name, matcher):
|
||||||
|
"""Add matcher by name to the registry."""
|
||||||
|
self._matchers[name] = matcher
|
||||||
|
|
||||||
|
def unregister(self, name):
|
||||||
|
"""Remove matcher by name from the registry."""
|
||||||
|
try:
|
||||||
|
self._matchers.pop(name)
|
||||||
|
except KeyError:
|
||||||
|
raise self.MatcherNotInstalled(
|
||||||
|
'No matcher installed for {}'.format(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_default_matcher(self, name):
|
||||||
|
"""Set the default matching method.
|
||||||
|
|
||||||
|
:param name: The name of the registered matching method.
|
||||||
|
For example, `glob` (default), `pcre`, or any custom
|
||||||
|
methods registered using :meth:`register`.
|
||||||
|
|
||||||
|
:raises MatcherNotInstalled: If the matching method requested
|
||||||
|
is not available.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._default_matcher = self._matchers[name]
|
||||||
|
except KeyError:
|
||||||
|
raise self.MatcherNotInstalled(
|
||||||
|
'No matcher installed for {}'.format(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
def match(self, data, pattern, matcher=None, matcher_kwargs=None):
|
||||||
|
"""Call the matcher."""
|
||||||
|
if matcher and not self._matchers.get(matcher):
|
||||||
|
raise self.MatcherNotInstalled(
|
||||||
|
'No matcher installed for {}'.format(matcher)
|
||||||
|
)
|
||||||
|
match_func = self._matchers[matcher or 'glob']
|
||||||
|
if matcher in self.matcher_pattern_first:
|
||||||
|
first_arg = bytes_to_str(pattern)
|
||||||
|
second_arg = bytes_to_str(data)
|
||||||
|
else:
|
||||||
|
first_arg = bytes_to_str(data)
|
||||||
|
second_arg = bytes_to_str(pattern)
|
||||||
|
return match_func(first_arg, second_arg, **matcher_kwargs or {})
|
||||||
|
|
||||||
|
|
||||||
|
#: Global registry of matchers.
|
||||||
|
registry = MatcherRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
.. function:: match(data, pattern, matcher=default_matcher,
|
||||||
|
matcher_kwargs=None):
|
||||||
|
|
||||||
|
Match `data` by `pattern` using `matcher`.
|
||||||
|
|
||||||
|
:param data: The data that should be matched. Must be string.
|
||||||
|
:param pattern: The pattern that should be applied. Must be string.
|
||||||
|
:keyword matcher: An optional string representing the mathcing
|
||||||
|
method (for example, `glob` or `pcre`).
|
||||||
|
|
||||||
|
If :const:`None` (default), then `glob` will be used.
|
||||||
|
|
||||||
|
:keyword matcher_kwargs: Additional keyword arguments that will be passed
|
||||||
|
to the specified `matcher`.
|
||||||
|
:returns: :const:`True` if `data` matches pattern,
|
||||||
|
:const:`False` otherwise.
|
||||||
|
|
||||||
|
:raises MatcherNotInstalled: If the matching method requested is not
|
||||||
|
available.
|
||||||
|
"""
|
||||||
|
match = registry.match
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
.. function:: register(name, matcher):
|
||||||
|
Register a new matching method.
|
||||||
|
|
||||||
|
:param name: A convience name for the mathing method.
|
||||||
|
:param matcher: A method that will be passed data and pattern.
|
||||||
|
"""
|
||||||
|
register = registry.register
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
.. function:: unregister(name):
|
||||||
|
Unregister registered matching method.
|
||||||
|
|
||||||
|
:param name: Registered matching method name.
|
||||||
|
"""
|
||||||
|
unregister = registry.unregister
|
||||||
|
|
||||||
|
|
||||||
|
def register_glob():
|
||||||
|
"""Register glob into default registry."""
|
||||||
|
registry.register('glob', fnmatch)
|
||||||
|
|
||||||
|
|
||||||
|
def register_pcre():
|
||||||
|
"""Register pcre into default registry."""
|
||||||
|
registry.register('pcre', rematch)
|
||||||
|
|
||||||
|
|
||||||
|
# Register the base matching methods.
|
||||||
|
register_glob()
|
||||||
|
register_pcre()
|
||||||
|
|
||||||
|
# Default matching method is 'glob'
|
||||||
|
registry._set_default_matcher('glob')
|
||||||
|
|
||||||
|
|
||||||
|
# Load entrypoints from installed extensions
|
||||||
|
for ep, args in entrypoints('kombu.matchers'):
|
||||||
|
register(ep.name, *args)
|
|
@ -15,11 +15,14 @@ from . import Exchange, Queue, Consumer, Producer
|
||||||
from .clocks import LamportClock
|
from .clocks import LamportClock
|
||||||
from .common import maybe_declare, oid_from
|
from .common import maybe_declare, oid_from
|
||||||
from .exceptions import InconsistencyError
|
from .exceptions import InconsistencyError
|
||||||
from .five import range
|
from .five import range, string_t
|
||||||
from .log import get_logger
|
from .log import get_logger
|
||||||
from .utils.functional import maybe_evaluate, reprcall
|
from .utils.functional import maybe_evaluate, reprcall
|
||||||
from .utils.objects import cached_property
|
from .utils.objects import cached_property
|
||||||
from .utils.uuid import uuid
|
from .utils.uuid import uuid
|
||||||
|
from .matcher import match
|
||||||
|
|
||||||
|
REPLY_QUEUE_EXPIRES = 10
|
||||||
|
|
||||||
W_PIDBOX_IN_USE = """\
|
W_PIDBOX_IN_USE = """\
|
||||||
A node named {node.hostname} is already using this process mailbox!
|
A node named {node.hostname} is already using this process mailbox!
|
||||||
|
@ -123,9 +126,21 @@ class Node(object):
|
||||||
|
|
||||||
def handle_message(self, body, message=None):
|
def handle_message(self, body, message=None):
|
||||||
destination = body.get('destination')
|
destination = body.get('destination')
|
||||||
|
pattern = body.get('pattern')
|
||||||
|
matcher = body.get('matcher')
|
||||||
if message:
|
if message:
|
||||||
self.adjust_clock(message.headers.get('clock') or 0)
|
self.adjust_clock(message.headers.get('clock') or 0)
|
||||||
if not destination or self.hostname in destination:
|
hostname = self.hostname
|
||||||
|
run_dispatch = False
|
||||||
|
if destination:
|
||||||
|
if hostname in destination:
|
||||||
|
run_dispatch = True
|
||||||
|
elif pattern and matcher:
|
||||||
|
if match(hostname, pattern, matcher):
|
||||||
|
run_dispatch = True
|
||||||
|
else:
|
||||||
|
run_dispatch = True
|
||||||
|
if run_dispatch:
|
||||||
return self.dispatch(**body)
|
return self.dispatch(**body)
|
||||||
dispatch_from_message = handle_message
|
dispatch_from_message = handle_message
|
||||||
|
|
||||||
|
@ -270,10 +285,12 @@ class Mailbox(object):
|
||||||
|
|
||||||
def _publish(self, type, arguments, destination=None,
|
def _publish(self, type, arguments, destination=None,
|
||||||
reply_ticket=None, channel=None, timeout=None,
|
reply_ticket=None, channel=None, timeout=None,
|
||||||
serializer=None, producer=None):
|
serializer=None, producer=None, pattern=None, matcher=None):
|
||||||
message = {'method': type,
|
message = {'method': type,
|
||||||
'arguments': arguments,
|
'arguments': arguments,
|
||||||
'destination': destination}
|
'destination': destination,
|
||||||
|
'pattern': pattern,
|
||||||
|
'matcher': matcher}
|
||||||
chan = channel or self.connection.default_channel
|
chan = channel or self.connection.default_channel
|
||||||
exchange = self.exchange
|
exchange = self.exchange
|
||||||
if reply_ticket:
|
if reply_ticket:
|
||||||
|
@ -292,12 +309,19 @@ class Mailbox(object):
|
||||||
|
|
||||||
def _broadcast(self, command, arguments=None, destination=None,
|
def _broadcast(self, command, arguments=None, destination=None,
|
||||||
reply=False, timeout=1, limit=None,
|
reply=False, timeout=1, limit=None,
|
||||||
callback=None, channel=None, serializer=None):
|
callback=None, channel=None, serializer=None,
|
||||||
|
pattern=None, matcher=None):
|
||||||
if destination is not None and \
|
if destination is not None and \
|
||||||
not isinstance(destination, (list, tuple)):
|
not isinstance(destination, (list, tuple)):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'destination must be a list/tuple not {0}'.format(
|
'destination must be a list/tuple not {0}'.format(
|
||||||
type(destination)))
|
type(destination)))
|
||||||
|
if (pattern is not None and not isinstance(pattern, string_t) and
|
||||||
|
matcher is not None and not isinstance(matcher, string_t)):
|
||||||
|
raise ValueError(
|
||||||
|
'pattern and matcher must be '
|
||||||
|
'strings not {}, {}'.format(type(pattern), type(matcher))
|
||||||
|
)
|
||||||
|
|
||||||
arguments = arguments or {}
|
arguments = arguments or {}
|
||||||
reply_ticket = reply and uuid() or None
|
reply_ticket = reply and uuid() or None
|
||||||
|
@ -312,7 +336,9 @@ class Mailbox(object):
|
||||||
reply_ticket=reply_ticket,
|
reply_ticket=reply_ticket,
|
||||||
channel=chan,
|
channel=chan,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
serializer=serializer)
|
serializer=serializer,
|
||||||
|
pattern=pattern,
|
||||||
|
matcher=matcher)
|
||||||
|
|
||||||
if reply_ticket:
|
if reply_ticket:
|
||||||
return self._collect(reply_ticket, limit=limit,
|
return self._collect(reply_ticket, limit=limit,
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from kombu.matcher import (
|
||||||
|
match, register, registry, unregister, fnmatch, rematch,
|
||||||
|
MatcherNotInstalled
|
||||||
|
)
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class test_Matcher(object):
|
||||||
|
|
||||||
|
def test_register_match_unregister_matcher(self):
|
||||||
|
register("test_matcher", rematch)
|
||||||
|
registry.matcher_pattern_first.append("test_matcher")
|
||||||
|
assert registry._matchers["test_matcher"] == rematch
|
||||||
|
assert match("data", r"d.*", "test_matcher") is not None
|
||||||
|
assert registry._default_matcher == fnmatch
|
||||||
|
registry._set_default_matcher("test_matcher")
|
||||||
|
assert registry._default_matcher == rematch
|
||||||
|
unregister("test_matcher")
|
||||||
|
assert "test_matcher" not in registry._matchers
|
||||||
|
registry._set_default_matcher("glob")
|
||||||
|
assert registry._default_matcher == fnmatch
|
||||||
|
|
||||||
|
def test_unregister_matcher_not_registered(self):
|
||||||
|
with pytest.raises(MatcherNotInstalled):
|
||||||
|
unregister('notinstalled')
|
||||||
|
|
||||||
|
def test_match_using_unregistered_matcher(self):
|
||||||
|
with pytest.raises(MatcherNotInstalled):
|
||||||
|
match("data", r"d.*", "notinstalled")
|
|
@ -43,6 +43,11 @@ class test_Mailbox:
|
||||||
def _handler(self, state):
|
def _handler(self, state):
|
||||||
return self.stats['var']
|
return self.stats['var']
|
||||||
|
|
||||||
|
def test_broadcast_matcher_pattern_string_type(self):
|
||||||
|
mailbox = pidbox.Mailbox("test_matcher_str")(self.connection)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
mailbox._broadcast("ping", pattern=1, matcher=2)
|
||||||
|
|
||||||
def test_publish_reply_ignores_InconsistencyError(self):
|
def test_publish_reply_ignores_InconsistencyError(self):
|
||||||
mailbox = pidbox.Mailbox('test_reply__collect')(self.connection)
|
mailbox = pidbox.Mailbox('test_reply__collect')(self.connection)
|
||||||
with patch('kombu.pidbox.Producer') as Producer:
|
with patch('kombu.pidbox.Producer') as Producer:
|
||||||
|
@ -233,6 +238,19 @@ class test_Mailbox:
|
||||||
body['destination'] = ['some_other_node']
|
body['destination'] = ['some_other_node']
|
||||||
assert node.handle_message(body, None) is None
|
assert node.handle_message(body, None) is None
|
||||||
|
|
||||||
|
# message for me should be processed.
|
||||||
|
body['destination'] = ['test_dispatch_from_message']
|
||||||
|
assert node.handle_message(body, None) is not None
|
||||||
|
|
||||||
|
# message not for me should not be processed.
|
||||||
|
body.pop("destination")
|
||||||
|
body['matcher'] = 'glob'
|
||||||
|
body["pattern"] = "something*"
|
||||||
|
assert node.handle_message(body, None) is None
|
||||||
|
|
||||||
|
body["pattern"] = "test*"
|
||||||
|
assert node.handle_message(body, None) is not None
|
||||||
|
|
||||||
def test_handle_message_adjusts_clock(self):
|
def test_handle_message_adjusts_clock(self):
|
||||||
node = self.bound.Node('test_adjusts_clock')
|
node = self.bound.Node('test_adjusts_clock')
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue