forked from platypush/platypush
Removed dependency from prctl.
Also, black'd and LINT-fixed some files that hadn't been touched in a while.
This commit is contained in:
parent
04b759e4d5
commit
0dc380fa94
14 changed files with 270 additions and 199 deletions
|
@ -23,7 +23,7 @@ from .message.event import Event
|
||||||
from .message.event.application import ApplicationStartedEvent
|
from .message.event.application import ApplicationStartedEvent
|
||||||
from .message.request import Request
|
from .message.request import Request
|
||||||
from .message.response import Response
|
from .message.response import Response
|
||||||
from .utils import set_thread_name, get_enabled_plugins
|
from .utils import get_enabled_plugins
|
||||||
|
|
||||||
__author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
|
__author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
|
||||||
__version__ = '0.50.3'
|
__version__ = '0.50.3'
|
||||||
|
@ -252,7 +252,6 @@ class Daemon:
|
||||||
if not self.no_capture_stderr:
|
if not self.no_capture_stderr:
|
||||||
sys.stderr = Logger(log.warning)
|
sys.stderr = Logger(log.warning)
|
||||||
|
|
||||||
set_thread_name('platypush')
|
|
||||||
log.info('---- Starting platypush v.%s', __version__)
|
log.info('---- Starting platypush v.%s', __version__)
|
||||||
|
|
||||||
# Initialize the backends and link them to the bus
|
# Initialize the backends and link them to the bus
|
||||||
|
|
|
@ -18,7 +18,6 @@ from platypush.utils import (
|
||||||
set_timeout,
|
set_timeout,
|
||||||
clear_timeout,
|
clear_timeout,
|
||||||
get_redis_queue_name_by_message,
|
get_redis_queue_name_by_message,
|
||||||
set_thread_name,
|
|
||||||
get_backend_name_by_class,
|
get_backend_name_by_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -81,7 +80,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs else None
|
self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs else None
|
||||||
|
|
||||||
if 'logging' in kwargs:
|
if 'logging' in kwargs:
|
||||||
self.logger.setLevel(getattr(logging, kwargs.get('logging').upper()))
|
self.logger.setLevel(getattr(logging, kwargs['logging'].upper()))
|
||||||
|
|
||||||
def on_message(self, msg):
|
def on_message(self, msg):
|
||||||
"""
|
"""
|
||||||
|
@ -95,21 +94,23 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
msg = Message.build(msg)
|
msg = Message.build(msg)
|
||||||
|
if msg is None:
|
||||||
|
return
|
||||||
|
|
||||||
if not getattr(msg, 'target', None) or msg.target != self.device_id:
|
if not getattr(msg, 'target', None) or msg.target != self.device_id:
|
||||||
return # Not for me
|
return # Not for me
|
||||||
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'Message received on the {} backend: {}'.format(
|
'Message received on the %s backend: %s', self.__class__.__name__, msg
|
||||||
self.__class__.__name__, msg
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._is_expected_response(msg):
|
if self._is_expected_response(msg):
|
||||||
# Expected response, trigger the response handler
|
# Expected response, trigger the response handler
|
||||||
clear_timeout()
|
clear_timeout()
|
||||||
# pylint: disable=unsubscriptable-object
|
if self._request_context:
|
||||||
self._request_context['on_response'](msg)
|
# pylint: disable=unsubscriptable-object
|
||||||
|
self._request_context['on_response'](msg)
|
||||||
|
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -117,8 +118,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
self.bus.post(msg)
|
self.bus.post(msg)
|
||||||
|
|
||||||
def _is_expected_response(self, msg):
|
def _is_expected_response(self, msg):
|
||||||
"""Internal only - returns true if we are expecting for a response
|
"""
|
||||||
and msg is that response"""
|
Internal only - returns true if we are expecting for a response and msg
|
||||||
|
is that response.
|
||||||
|
"""
|
||||||
|
|
||||||
# pylint: disable=unsubscriptable-object
|
# pylint: disable=unsubscriptable-object
|
||||||
return (
|
return (
|
||||||
|
@ -131,12 +134,12 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
config_name = (
|
config_name = (
|
||||||
'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower()
|
'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower()
|
||||||
)
|
)
|
||||||
return Config.get(config_name)
|
return Config.get(config_name) or {}
|
||||||
|
|
||||||
def _setup_response_handler(self, request, on_response, response_timeout):
|
def _setup_response_handler(self, request, on_response, response_timeout):
|
||||||
def _timeout_hndl():
|
def _timeout_hndl():
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'Timed out while waiting for a response from {}'.format(request.target)
|
f'Timed out while waiting for a response from {request.target}'
|
||||||
)
|
)
|
||||||
|
|
||||||
req_ctx = {
|
req_ctx = {
|
||||||
|
@ -177,7 +180,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
request,
|
request,
|
||||||
on_response=None,
|
on_response=None,
|
||||||
response_timeout=_default_response_timeout,
|
response_timeout=_default_response_timeout,
|
||||||
**kwargs
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Send a request message on the backend.
|
Send a request message on the backend.
|
||||||
|
@ -237,9 +240,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
(
|
(
|
||||||
"Backend {} does not implement send_message "
|
"Backend %s does not implement send_message "
|
||||||
"and the fallback Redis backend isn't configured"
|
"and the fallback Redis backend isn't configured"
|
||||||
).format(self.__class__.__name__)
|
),
|
||||||
|
self.__class__.__name__,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -248,7 +252,6 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined."""
|
"""Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined."""
|
||||||
self.thread_id = get_ident()
|
self.thread_id = get_ident()
|
||||||
set_thread_name(self._thread_name)
|
|
||||||
if not callable(self.loop):
|
if not callable(self.loop):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -272,21 +275,19 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
'{} initialization error: {}'.format(
|
'%s initialization error: %s', self.__class__.__name__, e
|
||||||
self.__class__.__name__, str(e)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
time.sleep(self.poll_seconds or 5)
|
time.sleep(self.poll_seconds or 5)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Invoked when the backend is initialized, if the main logic is within a ``loop()`` function"""
|
"""Invoked when the backend is initialized, if the main logic is within a ``loop()`` function"""
|
||||||
self.logger.info('Initialized backend {}'.format(self.__class__.__name__))
|
self.logger.info('Initialized backend %s', self.__class__.__name__)
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, *_, **__):
|
||||||
"""Invoked when the backend is terminated, if the main logic is within a ``loop()`` function"""
|
"""Invoked when the backend is terminated, if the main logic is within a ``loop()`` function"""
|
||||||
self.on_stop()
|
self.on_stop()
|
||||||
self.logger.info('Terminated backend {}'.format(self.__class__.__name__))
|
self.logger.info('Terminated backend %s', self.__class__.__name__)
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
"""Callback invoked when the process stops"""
|
"""Callback invoked when the process stops"""
|
||||||
|
@ -324,9 +325,14 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
return redis
|
return redis
|
||||||
|
|
||||||
def get_message_response(self, msg):
|
def get_message_response(self, msg):
|
||||||
|
queue = get_redis_queue_name_by_message(msg)
|
||||||
|
if not queue:
|
||||||
|
self.logger.warning('No response queue configured for the message')
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
redis = self._get_redis()
|
redis = self._get_redis()
|
||||||
response = redis.blpop(get_redis_queue_name_by_message(msg), timeout=60)
|
response = redis.blpop(queue, timeout=60)
|
||||||
if response and len(response) > 1:
|
if response and len(response) > 1:
|
||||||
response = Message.build(response[1])
|
response = Message.build(response[1])
|
||||||
else:
|
else:
|
||||||
|
@ -334,9 +340,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(
|
self.logger.error('Error while processing response to %s: %s', msg, e)
|
||||||
'Error while processing response to {}: {}'.format(msg, str(e))
|
|
||||||
)
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_ip() -> str:
|
def _get_ip() -> str:
|
||||||
|
@ -395,18 +401,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
}
|
}
|
||||||
|
|
||||||
name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower()
|
name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower()
|
||||||
srv_type = srv_type or '_platypush-{name}._{proto}.local.'.format(
|
srv_type = srv_type or f'_platypush-{name}._{"udp" if udp else "tcp"}.local.'
|
||||||
name=name, proto='udp' if udp else 'tcp'
|
srv_name = srv_name or f'{self.device_id}.{srv_type}'
|
||||||
)
|
srv_port = port if port else getattr(self, 'port', None)
|
||||||
srv_name = srv_name or '{host}.{type}'.format(
|
|
||||||
host=self.device_id, type=srv_type
|
|
||||||
)
|
|
||||||
|
|
||||||
if port:
|
|
||||||
srv_port = port
|
|
||||||
else:
|
|
||||||
srv_port = self.port if hasattr(self, 'port') else None
|
|
||||||
|
|
||||||
self.zeroconf_info = ServiceInfo(
|
self.zeroconf_info = ServiceInfo(
|
||||||
srv_type,
|
srv_type,
|
||||||
srv_name,
|
srv_name,
|
||||||
|
@ -439,9 +436,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
self.zeroconf.unregister_service(self.zeroconf_info)
|
self.zeroconf.unregister_service(self.zeroconf_info)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Could not register Zeroconf service {}: {}: {}'.format(
|
'Could not register Zeroconf service %s: %s: %s',
|
||||||
self.zeroconf_info.name, type(e).__name__, str(e)
|
self.zeroconf_info.name,
|
||||||
)
|
type(e).__name__,
|
||||||
|
e,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.zeroconf:
|
if self.zeroconf:
|
||||||
|
|
|
@ -5,7 +5,6 @@ from typing import Optional, List
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.message.event.google.pubsub import GooglePubsubMessageEvent
|
from platypush.message.event.google.pubsub import GooglePubsubMessageEvent
|
||||||
from platypush.utils import set_thread_name
|
|
||||||
|
|
||||||
|
|
||||||
class GooglePubsubBackend(Backend):
|
class GooglePubsubBackend(Backend):
|
||||||
|
@ -25,7 +24,9 @@ class GooglePubsubBackend(Backend):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, topics: List[str], credentials_file: Optional[str] = None, *args, **kwargs):
|
def __init__(
|
||||||
|
self, topics: List[str], credentials_file: Optional[str] = None, *args, **kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param topics: List of topics to subscribe. You can either specify the full topic name in the format
|
:param topics: List of topics to subscribe. You can either specify the full topic name in the format
|
||||||
``projects/<project_id>/topics/<topic_name>``, where ``<project_id>`` must be the ID of your
|
``projects/<project_id>/topics/<topic_name>``, where ``<project_id>`` must be the ID of your
|
||||||
|
@ -35,7 +36,7 @@ class GooglePubsubBackend(Backend):
|
||||||
``google.pubsub`` plugin or ``~/.credentials/platypush/google/pubsub.json``).
|
``google.pubsub`` plugin or ``~/.credentials/platypush/google/pubsub.json``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, name='GooglePubSub', **kwargs)
|
||||||
self.topics = topics
|
self.topics = topics
|
||||||
|
|
||||||
if credentials_file:
|
if credentials_file:
|
||||||
|
@ -46,7 +47,9 @@ class GooglePubsubBackend(Backend):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_plugin():
|
def _get_plugin():
|
||||||
return get_plugin('google.pubsub')
|
plugin = get_plugin('google.pubsub')
|
||||||
|
assert plugin, 'google.pubsub plugin not enabled'
|
||||||
|
return plugin
|
||||||
|
|
||||||
def _message_callback(self, topic):
|
def _message_callback(self, topic):
|
||||||
def callback(msg):
|
def callback(msg):
|
||||||
|
@ -54,7 +57,7 @@ class GooglePubsubBackend(Backend):
|
||||||
try:
|
try:
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug('Not a valid JSON: {}: {}'.format(data, str(e)))
|
self.logger.debug('Not a valid JSON: %s: %s', data, e)
|
||||||
|
|
||||||
msg.ack()
|
msg.ack()
|
||||||
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))
|
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))
|
||||||
|
@ -64,20 +67,23 @@ class GooglePubsubBackend(Backend):
|
||||||
def run(self):
|
def run(self):
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
from google.cloud import pubsub_v1
|
from google.cloud import pubsub_v1
|
||||||
|
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
from google.api_core.exceptions import AlreadyExists
|
from google.api_core.exceptions import AlreadyExists
|
||||||
|
|
||||||
super().run()
|
super().run()
|
||||||
set_thread_name('GooglePubSub')
|
|
||||||
plugin = self._get_plugin()
|
plugin = self._get_plugin()
|
||||||
project_id = plugin.get_project_id()
|
project_id = plugin.get_project_id()
|
||||||
credentials = plugin.get_credentials(plugin.subscriber_audience)
|
credentials = plugin.get_credentials(plugin.subscriber_audience)
|
||||||
subscriber = pubsub_v1.SubscriberClient(credentials=credentials)
|
subscriber = pubsub_v1.SubscriberClient(credentials=credentials)
|
||||||
|
|
||||||
for topic in self.topics:
|
for topic in self.topics:
|
||||||
if not topic.startswith('projects/{}/topics/'.format(project_id)):
|
prefix = f'projects/{project_id}/topics/'
|
||||||
topic = 'projects/{}/topics/{}'.format(project_id, topic)
|
if not topic.startswith(prefix):
|
||||||
subscription_name = '/'.join([*topic.split('/')[:2], 'subscriptions', topic.split('/')[-1]])
|
topic = f'{prefix}{topic}'
|
||||||
|
subscription_name = '/'.join(
|
||||||
|
[*topic.split('/')[:2], 'subscriptions', topic.split('/')[-1]]
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subscriber.create_subscription(name=subscription_name, topic=topic)
|
subscriber.create_subscription(name=subscription_name, topic=topic)
|
||||||
|
|
|
@ -1,28 +1,36 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import requests
|
from threading import Thread
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
from frozendict import frozendict
|
from frozendict import frozendict
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from platypush.message.event.http import HttpEvent
|
from platypush.message.event.http import HttpEvent
|
||||||
from platypush.utils import set_thread_name
|
|
||||||
|
|
||||||
|
|
||||||
class HttpRequest(object):
|
class HttpRequest:
|
||||||
|
"""
|
||||||
|
Backend used for polling HTTP resources.
|
||||||
|
"""
|
||||||
|
|
||||||
poll_seconds = 60
|
poll_seconds = 60
|
||||||
timeout = 5
|
timeout = 5
|
||||||
|
|
||||||
class HttpRequestArguments(object):
|
class HttpRequestArguments:
|
||||||
def __init__(self, url, method='get', *args, **kwargs):
|
"""
|
||||||
|
Models the properties of an HTTP request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url, *args, method='get', **kwargs):
|
||||||
self.method = method.lower()
|
self.method = method.lower()
|
||||||
self.url = url
|
self.url = url
|
||||||
self.args = args
|
self.args = args
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def __init__(self, args, bus=None, poll_seconds=None, timeout=None,
|
def __init__(
|
||||||
skip_first_call=True, **kwargs):
|
self, args, bus=None, poll_seconds=None, timeout=None, skip_first_call=True, **_
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.poll_seconds = poll_seconds or self.poll_seconds
|
self.poll_seconds = poll_seconds or self.poll_seconds
|
||||||
|
@ -43,12 +51,13 @@ class HttpRequest(object):
|
||||||
self.args.kwargs['timeout'] = self.timeout
|
self.args.kwargs['timeout'] = self.timeout
|
||||||
|
|
||||||
self.request_args = {
|
self.request_args = {
|
||||||
'method': self.args.method, 'url': self.args.url, **self.args.kwargs
|
'method': self.args.method,
|
||||||
|
'url': self.args.url,
|
||||||
|
**self.args.kwargs,
|
||||||
}
|
}
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
def _thread_func():
|
def _thread_func():
|
||||||
set_thread_name('HttpPoll')
|
|
||||||
is_first_call = self.last_request_timestamp == 0
|
is_first_call = self.last_request_timestamp == 0
|
||||||
self.last_request_timestamp = time.time()
|
self.last_request_timestamp = time.time()
|
||||||
|
|
||||||
|
@ -63,30 +72,45 @@ class HttpRequest(object):
|
||||||
else:
|
else:
|
||||||
event = HttpEvent(dict(self), new_items)
|
event = HttpEvent(dict(self), new_items)
|
||||||
|
|
||||||
if new_items and self.bus:
|
if (
|
||||||
if not self.skip_first_call or (
|
new_items
|
||||||
self.skip_first_call and not is_first_call):
|
and self.bus
|
||||||
self.bus.post(event)
|
and (
|
||||||
|
not self.skip_first_call
|
||||||
|
or (self.skip_first_call and not is_first_call)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.bus.post(event)
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
self.logger.warning('Encountered an error while retrieving {}: {}'.
|
self.logger.warning(
|
||||||
format(self.args.url, str(e)))
|
'Encountered an error while retrieving %s: %s', self.args.url, e
|
||||||
|
)
|
||||||
|
|
||||||
Thread(target=_thread_func, name='HttpPoll').start()
|
Thread(target=_thread_func, name='HttpPoll').start()
|
||||||
|
|
||||||
def get_new_items(self, response):
|
def get_new_items(self, response):
|
||||||
""" Gets new items out of a response """
|
"""Gets new items out of a response"""
|
||||||
raise NotImplementedError("get_new_items must be implemented in a derived class")
|
raise NotImplementedError(
|
||||||
|
"get_new_items must be implemented in a derived class"
|
||||||
|
)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for (key, value) in self.request_args.items():
|
"""
|
||||||
|
:return: The ``request_args`` as key-value pairs.
|
||||||
|
"""
|
||||||
|
for key, value in self.request_args.items():
|
||||||
yield key, value
|
yield key, value
|
||||||
|
|
||||||
|
|
||||||
class JsonHttpRequest(HttpRequest):
|
class JsonHttpRequest(HttpRequest):
|
||||||
def __init__(self, path=None, *args, **kwargs):
|
"""
|
||||||
|
Specialization of the HttpRequest class for JSON requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, path=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.seen_entries = set()
|
self.seen_entries = set()
|
||||||
|
@ -97,7 +121,8 @@ class JsonHttpRequest(HttpRequest):
|
||||||
|
|
||||||
if self.path:
|
if self.path:
|
||||||
m = re.match(r'\${\s*(.*)\s*}', self.path)
|
m = re.match(r'\${\s*(.*)\s*}', self.path)
|
||||||
response = eval(m.group(1))
|
if m:
|
||||||
|
response = eval(m.group(1)) # pylint: disable=eval-used
|
||||||
|
|
||||||
for entry in response:
|
for entry in response:
|
||||||
flattened_entry = deep_freeze(entry)
|
flattened_entry = deep_freeze(entry)
|
||||||
|
@ -109,6 +134,11 @@ class JsonHttpRequest(HttpRequest):
|
||||||
|
|
||||||
|
|
||||||
def deep_freeze(x):
|
def deep_freeze(x):
|
||||||
|
"""
|
||||||
|
Deep freezes a Python object - works for strings, dictionaries, sets and
|
||||||
|
iterables.
|
||||||
|
"""
|
||||||
|
|
||||||
if isinstance(x, str) or not hasattr(x, "__len__"):
|
if isinstance(x, str) or not hasattr(x, "__len__"):
|
||||||
return x
|
return x
|
||||||
if hasattr(x, "keys") and hasattr(x, "values"):
|
if hasattr(x, "keys") and hasattr(x, "values"):
|
||||||
|
@ -118,4 +148,5 @@ def deep_freeze(x):
|
||||||
|
|
||||||
return frozenset(map(deep_freeze, x))
|
return frozenset(map(deep_freeze, x))
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -5,11 +5,17 @@ import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.utils import set_thread_name
|
from platypush.message.event.music.snapcast import (
|
||||||
from platypush.message.event.music.snapcast import ClientVolumeChangeEvent, \
|
ClientVolumeChangeEvent,
|
||||||
GroupMuteChangeEvent, ClientConnectedEvent, ClientDisconnectedEvent, \
|
GroupMuteChangeEvent,
|
||||||
ClientLatencyChangeEvent, ClientNameChangeEvent, GroupStreamChangeEvent, \
|
ClientConnectedEvent,
|
||||||
StreamUpdateEvent, ServerUpdateEvent
|
ClientDisconnectedEvent,
|
||||||
|
ClientLatencyChangeEvent,
|
||||||
|
ClientNameChangeEvent,
|
||||||
|
GroupStreamChangeEvent,
|
||||||
|
StreamUpdateEvent,
|
||||||
|
ServerUpdateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MusicSnapcastBackend(Backend):
|
class MusicSnapcastBackend(Backend):
|
||||||
|
@ -31,11 +37,17 @@ class MusicSnapcastBackend(Backend):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_DEFAULT_SNAPCAST_PORT = 1705
|
_DEFAULT_SNAPCAST_PORT = 1705
|
||||||
_DEFAULT_POLL_SECONDS = 10 # Poll servers each 10 seconds
|
_DEFAULT_POLL_SECONDS = 10 # Poll servers each 10 seconds
|
||||||
_SOCKET_EOL = '\r\n'.encode()
|
_SOCKET_EOL = '\r\n'.encode()
|
||||||
|
|
||||||
def __init__(self, hosts=None, ports=None,
|
def __init__(
|
||||||
poll_seconds=_DEFAULT_POLL_SECONDS, *args, **kwargs):
|
self,
|
||||||
|
hosts=None,
|
||||||
|
ports=None,
|
||||||
|
poll_seconds=_DEFAULT_POLL_SECONDS,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param hosts: List of Snapcast server names or IPs to monitor (default:
|
:param hosts: List of Snapcast server names or IPs to monitor (default:
|
||||||
`['localhost']`
|
`['localhost']`
|
||||||
|
@ -72,24 +84,25 @@ class MusicSnapcastBackend(Backend):
|
||||||
if self._socks.get(host):
|
if self._socks.get(host):
|
||||||
return self._socks[host]
|
return self._socks[host]
|
||||||
|
|
||||||
self.logger.debug('Connecting to {host}:{port}'.format(host=host, port=port))
|
self.logger.debug('Connecting to %s:%d', host, port)
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.connect((host, port))
|
sock.connect((host, port))
|
||||||
self._socks[host] = sock
|
self._socks[host] = sock
|
||||||
self.logger.info('Connected to {host}:{port}'.format(host=host, port=port))
|
self.logger.info('Connected to %s:%d', host, port)
|
||||||
return sock
|
return sock
|
||||||
|
|
||||||
def _disconnect(self, host, port):
|
def _disconnect(self, host, port):
|
||||||
sock = self._socks.get(host)
|
sock = self._socks.get(host)
|
||||||
if not sock:
|
if not sock:
|
||||||
self.logger.debug('Not connected to {}:{}'.format(host, port))
|
self.logger.debug('Not connected to %s:%d', host, port)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock.close()
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(('Exception while disconnecting from {host}:{port}: {error}'.
|
self.logger.warning(
|
||||||
format(host=host, port=port, error=str(e))))
|
'Exception while disconnecting from %s:%d: %s', host, port, e
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
self._socks[host] = None
|
self._socks[host] = None
|
||||||
|
|
||||||
|
@ -103,7 +116,7 @@ class MusicSnapcastBackend(Backend):
|
||||||
if ready[0]:
|
if ready[0]:
|
||||||
buf += sock.recv(1)
|
buf += sock.recv(1)
|
||||||
else:
|
else:
|
||||||
return
|
return None
|
||||||
|
|
||||||
return json.loads(buf.decode().strip())
|
return json.loads(buf.decode().strip())
|
||||||
|
|
||||||
|
@ -115,8 +128,9 @@ class MusicSnapcastBackend(Backend):
|
||||||
client_id = msg.get('params', {}).get('id')
|
client_id = msg.get('params', {}).get('id')
|
||||||
volume = msg.get('params', {}).get('volume', {}).get('percent')
|
volume = msg.get('params', {}).get('volume', {}).get('percent')
|
||||||
muted = msg.get('params', {}).get('volume', {}).get('muted')
|
muted = msg.get('params', {}).get('volume', {}).get('muted')
|
||||||
evt = ClientVolumeChangeEvent(host=host, client=client_id,
|
evt = ClientVolumeChangeEvent(
|
||||||
volume=volume, muted=muted)
|
host=host, client=client_id, volume=volume, muted=muted
|
||||||
|
)
|
||||||
elif msg.get('method') == 'Group.OnMute':
|
elif msg.get('method') == 'Group.OnMute':
|
||||||
group_id = msg.get('params', {}).get('id')
|
group_id = msg.get('params', {}).get('id')
|
||||||
muted = msg.get('params', {}).get('mute')
|
muted = msg.get('params', {}).get('mute')
|
||||||
|
@ -149,10 +163,8 @@ class MusicSnapcastBackend(Backend):
|
||||||
|
|
||||||
return evt
|
return evt
|
||||||
|
|
||||||
def _client(self, host, port, thread_name):
|
def _client(self, host, port):
|
||||||
def _thread():
|
def _thread():
|
||||||
set_thread_name(thread_name)
|
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
try:
|
try:
|
||||||
sock = self._connect(host, port)
|
sock = self._connect(host, port)
|
||||||
|
@ -164,16 +176,20 @@ class MusicSnapcastBackend(Backend):
|
||||||
msgs = [msgs]
|
msgs = [msgs]
|
||||||
|
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
self.logger.debug('Received message on {host}:{port}: {msg}'.
|
self.logger.debug(
|
||||||
format(host=host, port=port, msg=msg))
|
'Received message on {host}:{port}: {msg}'.format(
|
||||||
|
host=host, port=port, msg=msg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
evt = self._parse_msg(host=host, msg=msg)
|
evt = self._parse_msg(host=host, msg=msg)
|
||||||
if evt:
|
if evt:
|
||||||
self.bus.post(evt)
|
self.bus.post(evt)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Exception while getting the status ' + 'of the Snapcast server {}:{}: {}'.
|
self.logger.warning(
|
||||||
format(host, port, str(e)))
|
'Exception while getting the status '
|
||||||
|
+ 'of the Snapcast server {}:{}: {}'.format(host, port, str(e))
|
||||||
|
)
|
||||||
|
|
||||||
self._disconnect(host, port)
|
self._disconnect(host, port)
|
||||||
finally:
|
finally:
|
||||||
|
@ -184,17 +200,19 @@ class MusicSnapcastBackend(Backend):
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
self.logger.info('Initialized Snapcast backend - hosts: {} ports: {}'.
|
self.logger.info(
|
||||||
format(self.hosts, self.ports))
|
'Initialized Snapcast backend - hosts: {} ports: {}'.format(
|
||||||
|
self.hosts, self.ports
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
for i, host in enumerate(self.hosts):
|
for i, host in enumerate(self.hosts):
|
||||||
port = self.ports[i]
|
port = self.ports[i]
|
||||||
thread_name = 'Snapcast-{host}-{port}'.format(host=host, port=port)
|
thread_name = f'Snapcast-{host}-{port}'
|
||||||
|
|
||||||
self._threads[host] = threading.Thread(
|
self._threads[host] = threading.Thread(
|
||||||
target=self._client(host, port, thread_name),
|
target=self._client(host, port), name=thread_name
|
||||||
name=thread_name
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._threads[host].start()
|
self._threads[host].start()
|
||||||
|
@ -211,8 +229,11 @@ class MusicSnapcastBackend(Backend):
|
||||||
try:
|
try:
|
||||||
sock.close()
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Could not close Snapcast connection to {}: {}: {}'.format(
|
self.logger.warning(
|
||||||
host, type(e), str(e)))
|
'Could not close Snapcast connection to {}: {}: {}'.format(
|
||||||
|
host, type(e), str(e)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -6,7 +6,6 @@ from typing import Optional
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.message import Message
|
from platypush.message import Message
|
||||||
from platypush.utils import set_thread_name
|
|
||||||
|
|
||||||
|
|
||||||
class TcpBackend(Backend):
|
class TcpBackend(Backend):
|
||||||
|
@ -17,19 +16,20 @@ class TcpBackend(Backend):
|
||||||
# Maximum length of a request to be processed
|
# Maximum length of a request to be processed
|
||||||
_MAX_REQ_SIZE = 2048
|
_MAX_REQ_SIZE = 2048
|
||||||
|
|
||||||
def __init__(self, port, bind_address=None, listen_queue=5, *args, **kwargs):
|
def __init__(self, port, bind_address=None, listen_queue=5, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param port: TCP port number
|
:param port: TCP port number
|
||||||
:type port: int
|
:type port: int
|
||||||
|
|
||||||
:param bind_address: Specify a bind address if you want to hook the service to a specific interface (default: listen for any connections)
|
:param bind_address: Specify a bind address if you want to hook the
|
||||||
|
service to a specific interface (default: listen for any connections).
|
||||||
:type bind_address: str
|
:type bind_address: str
|
||||||
|
|
||||||
:param listen_queue: Maximum number of queued connections (default: 5)
|
:param listen_queue: Maximum number of queued connections (default: 5)
|
||||||
:type listen_queue: int
|
:type listen_queue: int
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(name=self.__class__.__name__, **kwargs)
|
||||||
|
|
||||||
self.port = port
|
self.port = port
|
||||||
self.bind_address = bind_address or '0.0.0.0'
|
self.bind_address = bind_address or '0.0.0.0'
|
||||||
|
@ -46,8 +46,11 @@ class TcpBackend(Backend):
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
if processed_bytes > self._MAX_REQ_SIZE:
|
if processed_bytes > self._MAX_REQ_SIZE:
|
||||||
self.logger.warning('Ignoring message longer than {} bytes from {}'
|
self.logger.warning(
|
||||||
.format(self._MAX_REQ_SIZE, address[0]))
|
'Ignoring message longer than {} bytes from {}'.format(
|
||||||
|
self._MAX_REQ_SIZE, address[0]
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
ch = sock.recv(1)
|
ch = sock.recv(1)
|
||||||
|
@ -76,17 +79,16 @@ class TcpBackend(Backend):
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = Message.build(msg)
|
msg = Message.build(msg)
|
||||||
self.logger.info('Received request from {}: {}'.format(msg, address[0]))
|
self.logger.info('Received request from %s: %s', msg, address[0])
|
||||||
self.on_message(msg)
|
self.on_message(msg)
|
||||||
|
|
||||||
response = self.get_message_response(msg)
|
response = self.get_message_response(msg)
|
||||||
self.logger.info('Processing response on the TCP backend: {}'.format(response))
|
self.logger.info('Processing response on the TCP backend: %s', response)
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
sock.send(str(response).encode())
|
sock.send(str(response).encode())
|
||||||
|
|
||||||
def _f_wrapper():
|
def _f_wrapper():
|
||||||
set_thread_name('TCPListener')
|
|
||||||
try:
|
try:
|
||||||
_f()
|
_f()
|
||||||
finally:
|
finally:
|
||||||
|
@ -111,11 +113,16 @@ class TcpBackend(Backend):
|
||||||
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
serv_sock.settimeout(0.5)
|
serv_sock.settimeout(0.5)
|
||||||
|
|
||||||
self.logger.info('Initialized TCP backend on port {} with bind address {}'.
|
self.logger.info(
|
||||||
format(self.port, self.bind_address))
|
'Initialized TCP backend on port %s with bind address %s',
|
||||||
|
self.port,
|
||||||
|
self.bind_address,
|
||||||
|
)
|
||||||
|
|
||||||
serv_sock.listen(self.listen_queue)
|
serv_sock.listen(self.listen_queue)
|
||||||
self._srv = multiprocessing.Process(target=self._accept_process, args=(serv_sock,))
|
self._srv = multiprocessing.Process(
|
||||||
|
target=self._accept_process, args=(serv_sock,)
|
||||||
|
)
|
||||||
self._srv.start()
|
self._srv.start()
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
|
@ -124,7 +131,7 @@ class TcpBackend(Backend):
|
||||||
except (socket.timeout, queue.Empty):
|
except (socket.timeout, queue.Empty):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.logger.info('Accepted connection from client {}'.format(address[0]))
|
self.logger.info('Accepted connection from client %s', address[0])
|
||||||
self._process_client(sock, address)
|
self._process_client(sock, address)
|
||||||
|
|
||||||
if self._srv:
|
if self._srv:
|
||||||
|
|
|
@ -8,7 +8,7 @@ import croniter
|
||||||
from dateutil.tz import gettz
|
from dateutil.tz import gettz
|
||||||
|
|
||||||
from platypush.procedure import Procedure
|
from platypush.procedure import Procedure
|
||||||
from platypush.utils import is_functional_cron, set_thread_name
|
from platypush.utils import is_functional_cron
|
||||||
|
|
||||||
logger = logging.getLogger('platypush:cron')
|
logger = logging.getLogger('platypush:cron')
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class Cronjob(threading.Thread):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name, cron_expression, actions):
|
def __init__(self, name, cron_expression, actions):
|
||||||
super().__init__()
|
super().__init__(name=f'cron:{name}')
|
||||||
self.cron_expression = cron_expression
|
self.cron_expression = cron_expression
|
||||||
self.name = name
|
self.name = name
|
||||||
self.state = CronjobState.IDLE
|
self.state = CronjobState.IDLE
|
||||||
|
@ -79,7 +79,6 @@ class Cronjob(threading.Thread):
|
||||||
"""
|
"""
|
||||||
Inner logic of the cronjob thread.
|
Inner logic of the cronjob thread.
|
||||||
"""
|
"""
|
||||||
set_thread_name(f'cron:{self.name}')
|
|
||||||
|
|
||||||
# Wait until an event is received or the next execution slot is reached
|
# Wait until an event is received or the next execution slot is reached
|
||||||
self.wait()
|
self.wait()
|
||||||
|
@ -203,7 +202,7 @@ class CronScheduler(threading.Thread):
|
||||||
logger.info('Running cron scheduler')
|
logger.info('Running cron scheduler')
|
||||||
|
|
||||||
while not self.should_stop():
|
while not self.should_stop():
|
||||||
for (job_name, job_config) in self.jobs_config.items():
|
for job_name, job_config in self.jobs_config.items():
|
||||||
job = self._get_job(name=job_name, config=job_config)
|
job = self._get_job(name=job_name, config=job_config)
|
||||||
if job.state == CronjobState.IDLE:
|
if job.state == CronjobState.IDLE:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -5,7 +5,6 @@ from typing import Dict, Optional
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.entities import Entity
|
from platypush.entities import Entity
|
||||||
from platypush.message.event.entities import EntityUpdateEvent
|
from platypush.message.event.entities import EntityUpdateEvent
|
||||||
from platypush.utils import set_thread_name
|
|
||||||
|
|
||||||
from platypush.entities._base import EntityKey, EntitySavedCallback
|
from platypush.entities._base import EntityKey, EntitySavedCallback
|
||||||
from platypush.entities._engine.queue import EntitiesQueue
|
from platypush.entities._engine.queue import EntitiesQueue
|
||||||
|
@ -99,7 +98,6 @@ class EntitiesEngine(Thread):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
set_thread_name('entities')
|
|
||||||
self.logger.info('Started entities engine')
|
self.logger.info('Started entities engine')
|
||||||
self._running.set()
|
self._running.set()
|
||||||
|
|
||||||
|
|
|
@ -3,20 +3,23 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
from platypush.common import exec_wrapper
|
from platypush.common import exec_wrapper
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
from platypush.message.request import Request
|
from platypush.message.request import Request
|
||||||
from platypush.procedure import Procedure
|
from platypush.procedure import Procedure
|
||||||
from platypush.utils import get_event_class_by_type, set_thread_name, is_functional_hook
|
from platypush.utils import get_event_class_by_type, is_functional_hook
|
||||||
|
|
||||||
logger = logging.getLogger('platypush')
|
logger = logging.getLogger('platypush')
|
||||||
|
|
||||||
|
|
||||||
def parse(msg):
|
def parse(msg):
|
||||||
"""Builds a dict given another dictionary or
|
"""
|
||||||
a JSON UTF-8 encoded string/bytearray"""
|
Builds a dict given another dictionary or a JSON UTF-8 encoded
|
||||||
|
string/bytearray.
|
||||||
|
"""
|
||||||
|
|
||||||
if isinstance(msg, (bytes, bytearray)):
|
if isinstance(msg, (bytes, bytearray)):
|
||||||
msg = msg.decode('utf-8')
|
msg = msg.decode('utf-8')
|
||||||
|
@ -24,16 +27,18 @@ def parse(msg):
|
||||||
try:
|
try:
|
||||||
msg = json.loads(msg.strip())
|
msg = json.loads(msg.strip())
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.warning('Invalid JSON message: {}'.format(msg))
|
logger.warning('Invalid JSON message: %s', msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
class EventCondition:
|
class EventCondition:
|
||||||
"""Event hook condition class"""
|
"""Event hook condition class"""
|
||||||
|
|
||||||
def __init__(self, type=Event.__class__, priority=None, **kwargs):
|
# pylint: disable=redefined-builtin
|
||||||
|
def __init__(self, type: Optional[Type[Event]] = None, priority=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Rule constructor.
|
Rule constructor.
|
||||||
Params:
|
Params:
|
||||||
|
@ -42,40 +47,40 @@ class EventCondition:
|
||||||
or recognized_phrase='Your phrase')
|
or recognized_phrase='Your phrase')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.type = type
|
self.type = type or Event.__class__ # type: ignore
|
||||||
self.args = {}
|
self.args = {}
|
||||||
self.parsed_args = {}
|
self.parsed_args = {}
|
||||||
self.priority = priority
|
self.priority = priority
|
||||||
|
|
||||||
for (key, value) in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
# TODO So far we only allow simple value match. If value is a dict
|
|
||||||
# instead, we should allow more a sophisticated attribute matching,
|
|
||||||
# e.g. or conditions, in, and other operators.
|
|
||||||
self.args[key] = value
|
self.args[key] = value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, rule):
|
def build(cls, rule):
|
||||||
"""Builds a rule given either another EventRule, a dictionary or
|
"""
|
||||||
a JSON UTF-8 encoded string/bytearray"""
|
Builds a rule given either another EventRule, a dictionary or a JSON
|
||||||
|
UTF-8 encoded string/bytearray.
|
||||||
|
"""
|
||||||
|
|
||||||
if isinstance(rule, cls):
|
if isinstance(rule, cls):
|
||||||
return rule
|
return rule
|
||||||
else:
|
|
||||||
rule = parse(rule)
|
|
||||||
|
|
||||||
|
rule = parse(rule)
|
||||||
assert isinstance(rule, dict), f'Not a valid rule: {rule}'
|
assert isinstance(rule, dict), f'Not a valid rule: {rule}'
|
||||||
type = get_event_class_by_type(rule.pop('type') if 'type' in rule else 'Event')
|
type = get_event_class_by_type(rule.pop('type') if 'type' in rule else 'Event')
|
||||||
|
|
||||||
args = {}
|
args = {}
|
||||||
for (key, value) in rule.items():
|
for key, value in rule.items():
|
||||||
args[key] = value
|
args[key] = value
|
||||||
|
|
||||||
return cls(type=type, **args)
|
return cls(type=type, **args)
|
||||||
|
|
||||||
|
|
||||||
class EventAction(Request):
|
class EventAction(Request):
|
||||||
"""Event hook action class. It is a special type of runnable request
|
"""
|
||||||
whose fields can be configured later depending on the event context"""
|
Event hook action class. It is a special type of runnable request whose
|
||||||
|
fields can be configured later depending on the event context.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, target=None, action=None, **args):
|
def __init__(self, target=None, action=None, **args):
|
||||||
if target is None:
|
if target is None:
|
||||||
|
@ -84,30 +89,34 @@ class EventAction(Request):
|
||||||
super().__init__(target=target, action=action, **args_copy)
|
super().__init__(target=target, action=action, **args_copy)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, action):
|
def build(cls, msg):
|
||||||
action = super().parse(action)
|
msg = super().parse(msg)
|
||||||
action['origin'] = Config.get('device_id')
|
msg['origin'] = Config.get('device_id')
|
||||||
|
|
||||||
if 'target' not in action:
|
if 'target' not in msg:
|
||||||
action['target'] = action['origin']
|
msg['target'] = msg['origin']
|
||||||
|
|
||||||
token = Config.get('token')
|
token = Config.get('token')
|
||||||
if token:
|
if token:
|
||||||
action['token'] = token
|
msg['token'] = token
|
||||||
|
|
||||||
return super().build(action)
|
return super().build(msg)
|
||||||
|
|
||||||
|
|
||||||
class EventHook:
|
class EventHook:
|
||||||
"""Event hook class. It consists of one conditions and
|
"""
|
||||||
one or multiple actions to be executed"""
|
Event hook class. It consists of one conditions and one or multiple actions
|
||||||
|
to be executed.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, name, priority=None, condition=None, actions=None):
|
def __init__(self, name, priority=None, condition=None, actions=None):
|
||||||
"""Constructor. Takes a name, a EventCondition object and an event action
|
"""
|
||||||
procedure as input. It may also have a priority attached
|
Takes a name, a EventCondition object and an event action procedure as
|
||||||
as a positive number. If multiple hooks match against an event,
|
input. It may also have a priority attached as a positive number. If
|
||||||
only the ones that have either the maximum match score or the
|
multiple hooks match against an event, only the ones that have either
|
||||||
maximum pre-configured priority will be run."""
|
the maximum match score or the maximum pre-configured priority will be
|
||||||
|
run.
|
||||||
|
"""
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.condition = EventCondition.build(condition or {})
|
self.condition = EventCondition.build(condition or {})
|
||||||
|
@ -116,24 +125,28 @@ class EventHook:
|
||||||
self.condition.priority = self.priority
|
self.condition.priority = self.priority
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, name, hook):
|
def build(cls, name, hook): # pylint: disable=redefined-outer-name
|
||||||
"""Builds a rule given either another EventRule, a dictionary or
|
"""
|
||||||
a JSON UTF-8 encoded string/bytearray"""
|
Builds a rule given either another EventRule, a dictionary or a JSON
|
||||||
|
UTF-8 encoded string/bytearray.
|
||||||
|
"""
|
||||||
|
|
||||||
if isinstance(hook, cls):
|
if isinstance(hook, cls):
|
||||||
return hook
|
return hook
|
||||||
else:
|
|
||||||
hook = parse(hook)
|
|
||||||
|
|
||||||
|
hook = parse(hook)
|
||||||
if is_functional_hook(hook):
|
if is_functional_hook(hook):
|
||||||
actions = Procedure(name=name, requests=[hook], _async=False)
|
actions = Procedure(name=name, requests=[hook], _async=False)
|
||||||
return cls(name=name, condition=hook.condition, actions=actions)
|
return cls(
|
||||||
|
name=name, condition=getattr(hook, 'condition', None), actions=actions
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(hook, dict)
|
assert isinstance(hook, dict)
|
||||||
condition = EventCondition.build(hook['if']) if 'if' in hook else None
|
condition = EventCondition.build(hook['if']) if 'if' in hook else None
|
||||||
actions = []
|
actions = []
|
||||||
priority = hook['priority'] if 'priority' in hook else None
|
priority = hook['priority'] if 'priority' in hook else None
|
||||||
condition.priority = priority
|
if condition:
|
||||||
|
condition.priority = priority
|
||||||
|
|
||||||
if 'then' in hook:
|
if 'then' in hook:
|
||||||
if isinstance(hook['then'], list):
|
if isinstance(hook['then'], list):
|
||||||
|
@ -145,29 +158,38 @@ class EventHook:
|
||||||
return cls(name=name, condition=condition, actions=actions, priority=priority)
|
return cls(name=name, condition=condition, actions=actions, priority=priority)
|
||||||
|
|
||||||
def matches_event(self, event):
|
def matches_event(self, event):
|
||||||
"""Returns an EventMatchResult object containing the information
|
"""
|
||||||
about the match between the event and this hook"""
|
Returns an EventMatchResult object containing the information about the
|
||||||
|
match between the event and this hook.
|
||||||
|
"""
|
||||||
|
|
||||||
return event.matches_condition(self.condition)
|
return event.matches_condition(self.condition)
|
||||||
|
|
||||||
def run(self, event):
|
def run(self, event):
|
||||||
"""Checks the condition of the hook against a particular event and
|
"""
|
||||||
runs the hook actions if the condition is met"""
|
Checks the condition of the hook against a particular event and runs
|
||||||
|
the hook actions if the condition is met.
|
||||||
|
"""
|
||||||
|
|
||||||
def _thread_func(result):
|
def _thread_func(result):
|
||||||
set_thread_name('Event-' + self.name)
|
executor = getattr(self.actions, 'execute', None)
|
||||||
self.actions.execute(event=event, **result.parsed_args)
|
if executor and callable(executor):
|
||||||
|
executor(event=event, **result.parsed_args)
|
||||||
|
|
||||||
result = self.matches_event(event)
|
result = self.matches_event(event)
|
||||||
|
|
||||||
if result.is_match:
|
if result.is_match:
|
||||||
logger.info('Running hook {} triggered by an event'.format(self.name))
|
logger.info('Running hook %s triggered by an event', self.name)
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=_thread_func, name='Event-' + self.name, args=(result,)
|
target=_thread_func, name='Event-' + self.name, args=(result,)
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
|
|
||||||
def hook(event_type=Event, **condition):
|
def hook(event_type=Event, **condition):
|
||||||
|
"""
|
||||||
|
Decorator used for event hook functions.
|
||||||
|
"""
|
||||||
|
|
||||||
def wrapper(f):
|
def wrapper(f):
|
||||||
f.hook = True
|
f.hook = True
|
||||||
f.condition = EventCondition(type=event_type, **condition)
|
f.condition = EventCondition(type=event_type, **condition)
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Request(Message):
|
||||||
target,
|
target,
|
||||||
action,
|
action,
|
||||||
origin=None,
|
origin=None,
|
||||||
id=None,
|
id=None, # pylint: disable=redefined-builtin
|
||||||
backend=None,
|
backend=None,
|
||||||
args=None,
|
args=None,
|
||||||
token=None,
|
token=None,
|
||||||
|
@ -109,8 +109,6 @@ class Request(Message):
|
||||||
return proc.execute(*args, **kwargs)
|
return proc.execute(*args, **kwargs)
|
||||||
|
|
||||||
def _expand_context(self, event_args=None, **context):
|
def _expand_context(self, event_args=None, **context):
|
||||||
from platypush.config import Config
|
|
||||||
|
|
||||||
if event_args is None:
|
if event_args is None:
|
||||||
event_args = copy.deepcopy(self.args)
|
event_args = copy.deepcopy(self.args)
|
||||||
|
|
||||||
|
@ -138,16 +136,19 @@ class Request(Message):
|
||||||
return event_args
|
return event_args
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
def expand_value_from_context(cls, _value, **context):
|
def expand_value_from_context(cls, _value, **context):
|
||||||
for k, v in context.items():
|
for k, v in context.items():
|
||||||
if isinstance(v, Message):
|
if isinstance(v, Message):
|
||||||
v = json.loads(str(v))
|
v = json.loads(str(v))
|
||||||
try:
|
try:
|
||||||
exec('{}={}'.format(k, v))
|
exec('{}={}'.format(k, v)) # pylint: disable=exec-used
|
||||||
except Exception:
|
except Exception:
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
try:
|
try:
|
||||||
exec('{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v)))
|
exec( # pylint: disable=exec-used
|
||||||
|
'{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v))
|
||||||
|
)
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Could not set context variable %s=%s: %s', k, v, e2
|
'Could not set context variable %s=%s: %s', k, v, e2
|
||||||
|
@ -167,7 +168,7 @@ class Request(Message):
|
||||||
_value = m.group(4)
|
_value = m.group(4)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context_value = eval(inner_expr)
|
context_value = eval(inner_expr) # pylint: disable=eval-used
|
||||||
|
|
||||||
if callable(context_value):
|
if callable(context_value):
|
||||||
context_value = context_value()
|
context_value = context_value()
|
||||||
|
@ -209,6 +210,7 @@ class Request(Message):
|
||||||
redis.send_message(queue_name, response)
|
redis.send_message(queue_name, response)
|
||||||
redis.expire(queue_name, 60)
|
redis.expire(queue_name, 60)
|
||||||
|
|
||||||
|
# pylint: disable=too-many-statements
|
||||||
def execute(self, n_tries=1, _async=True, **context):
|
def execute(self, n_tries=1, _async=True, **context):
|
||||||
"""
|
"""
|
||||||
Execute this request and returns a Response object
|
Execute this request and returns a Response object
|
||||||
|
@ -224,6 +226,7 @@ class Request(Message):
|
||||||
- group: ${group_name} # will be expanded as "Kitchen lights")
|
- group: ${group_name} # will be expanded as "Kitchen lights")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
def _thread_func(_n_tries, errors=None):
|
def _thread_func(_n_tries, errors=None):
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.plugins import RunnablePlugin
|
from platypush.plugins import RunnablePlugin
|
||||||
|
|
|
@ -24,7 +24,6 @@ from platypush.message.event.light import (
|
||||||
LightStatusChangeEvent,
|
LightStatusChangeEvent,
|
||||||
)
|
)
|
||||||
from platypush.plugins import RunnablePlugin, action
|
from platypush.plugins import RunnablePlugin, action
|
||||||
from platypush.utils import set_thread_name
|
|
||||||
|
|
||||||
|
|
||||||
class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
||||||
|
@ -1054,7 +1053,6 @@ class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
||||||
return self._animation_stop.is_set()
|
return self._animation_stop.is_set()
|
||||||
|
|
||||||
def _animate_thread(lights):
|
def _animate_thread(lights):
|
||||||
set_thread_name('HueAnimate')
|
|
||||||
get_bus().post(
|
get_bus().post(
|
||||||
LightAnimationStartedEvent(
|
LightAnimationStartedEvent(
|
||||||
lights=lights,
|
lights=lights,
|
||||||
|
@ -1209,7 +1207,7 @@ class LightHuePlugin(RunnablePlugin, LightEntityManager):
|
||||||
def status(self, *_, **__) -> Iterable[LightEntity]:
|
def status(self, *_, **__) -> Iterable[LightEntity]:
|
||||||
lights = self.transform_entities(self._get_lights(publish_entities=True))
|
lights = self.transform_entities(self._get_lights(publish_entities=True))
|
||||||
for light in lights:
|
for light in lights:
|
||||||
light.id = light.external_id
|
light.id = light.external_id # type: ignore
|
||||||
for attr, value in (light.data or {}).items():
|
for attr, value in (light.data or {}).items():
|
||||||
setattr(light, attr, value)
|
setattr(light, attr, value)
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ def get_plugin_class_by_name(plugin_name):
|
||||||
|
|
||||||
module = get_plugin_module_by_name(plugin_name)
|
module = get_plugin_module_by_name(plugin_name)
|
||||||
if not module:
|
if not module:
|
||||||
return
|
return None
|
||||||
|
|
||||||
class_name = getattr(
|
class_name = getattr(
|
||||||
module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin'
|
module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin'
|
||||||
|
@ -126,7 +126,7 @@ def get_backend_class_by_name(backend_name: str):
|
||||||
|
|
||||||
module = get_backend_module_by_name(backend_name)
|
module = get_backend_module_by_name(backend_name)
|
||||||
if not module:
|
if not module:
|
||||||
return
|
return None
|
||||||
|
|
||||||
class_name = getattr(
|
class_name = getattr(
|
||||||
module,
|
module,
|
||||||
|
@ -149,7 +149,7 @@ def get_backend_class_by_name(backend_name: str):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_backend_name_by_class(backend) -> Optional[str]:
|
def get_backend_name_by_class(backend) -> str:
|
||||||
"""Gets the common name of a backend (e.g. "http" or "mqtt") given its class."""
|
"""Gets the common name of a backend (e.g. "http" or "mqtt") given its class."""
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
|
@ -206,12 +206,14 @@ def get_decorators(cls, climb_class_hierarchy=False):
|
||||||
|
|
||||||
decorators = {}
|
decorators = {}
|
||||||
|
|
||||||
# noinspection PyPep8Naming
|
|
||||||
def visit_FunctionDef(node):
|
def visit_FunctionDef(node):
|
||||||
for n in node.decorator_list:
|
for n in node.decorator_list:
|
||||||
if isinstance(n, ast.Call):
|
if isinstance(n, ast.Call):
|
||||||
# noinspection PyUnresolvedReferences
|
name = (
|
||||||
name = n.func.attr if isinstance(n.func, ast.Attribute) else n.func.id
|
n.func.attr
|
||||||
|
if isinstance(n.func, ast.Attribute)
|
||||||
|
else n.func.id # type: ignore
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
name = n.attr if isinstance(n, ast.Attribute) else n.id
|
name = n.attr if isinstance(n, ast.Attribute) else n.id
|
||||||
|
|
||||||
|
@ -257,6 +259,7 @@ def _get_ssl_context(
|
||||||
else:
|
else:
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
|
||||||
|
assert ssl_cert, 'No certificate specified'
|
||||||
if ssl_cafile or ssl_capath:
|
if ssl_cafile or ssl_capath:
|
||||||
ssl_context.load_verify_locations(cafile=ssl_cafile, capath=ssl_capath)
|
ssl_context.load_verify_locations(cafile=ssl_cafile, capath=ssl_capath)
|
||||||
|
|
||||||
|
@ -311,18 +314,6 @@ def get_ssl_client_context(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_thread_name(name: str):
|
|
||||||
"""
|
|
||||||
Set the name of the current thread.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import prctl
|
|
||||||
|
|
||||||
prctl.set_name(name) # pylint: disable=no-member
|
|
||||||
except ImportError:
|
|
||||||
logger.debug('Unable to set thread name: prctl module is missing')
|
|
||||||
|
|
||||||
|
|
||||||
def find_bins_in_path(bin_name):
|
def find_bins_in_path(bin_name):
|
||||||
"""
|
"""
|
||||||
Search for a binary in the PATH variable.
|
Search for a binary in the PATH variable.
|
||||||
|
@ -399,7 +390,6 @@ def get_mime_type(resource: str) -> Optional[str]:
|
||||||
offset = len('file://')
|
offset = len('file://')
|
||||||
resource = resource[offset:]
|
resource = resource[offset:]
|
||||||
|
|
||||||
# noinspection HttpUrlsUsage
|
|
||||||
if resource.startswith('http://') or resource.startswith('https://'):
|
if resource.startswith('http://') or resource.startswith('https://'):
|
||||||
with urllib.request.urlopen(resource) as response:
|
with urllib.request.urlopen(resource) as response:
|
||||||
return response.info().get_content_type()
|
return response.info().get_content_type()
|
||||||
|
@ -442,6 +432,8 @@ def grouper(n, iterable, fillvalue=None):
|
||||||
for chunk in zip_longest(*args):
|
for chunk in zip_longest(*args):
|
||||||
yield filter(None, chunk)
|
yield filter(None, chunk)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def is_functional_procedure(obj) -> bool:
|
def is_functional_procedure(obj) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -529,7 +521,7 @@ def get_or_generate_jwt_rsa_key_pair():
|
||||||
"""
|
"""
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
|
|
||||||
key_dir = os.path.join(Config.get('workdir'), 'jwt')
|
key_dir = os.path.join(Config.workdir, 'jwt')
|
||||||
priv_key_file = os.path.join(key_dir, 'id_rsa')
|
priv_key_file = os.path.join(key_dir, 'id_rsa')
|
||||||
pub_key_file = priv_key_file + '.pub'
|
pub_key_file = priv_key_file + '.pub'
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ marshmallow_dataclass
|
||||||
paho-mqtt
|
paho-mqtt
|
||||||
python-dateutil
|
python-dateutil
|
||||||
python-magic
|
python-magic
|
||||||
python-prctl
|
|
||||||
pyyaml
|
pyyaml
|
||||||
redis
|
redis
|
||||||
requests
|
requests
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -83,8 +83,6 @@ setup(
|
||||||
'zeroconf>=0.27.0',
|
'zeroconf>=0.27.0',
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
# Support for thread custom name
|
|
||||||
'threadname': ['python-prctl'],
|
|
||||||
# Support for Kafka backend and plugin
|
# Support for Kafka backend and plugin
|
||||||
'kafka': ['kafka-python'],
|
'kafka': ['kafka-python'],
|
||||||
# Support for Pushbullet backend and plugin
|
# Support for Pushbullet backend and plugin
|
||||||
|
|
Loading…
Reference in a new issue