Updated email addresses and black'd some old source files.

This commit is contained in:
Fabio Manganiello 2023-07-22 23:02:44 +02:00
parent cf8ecf349b
commit 66981bd00b
Signed by: blacklight
GPG key ID: D90FBA7F76362774
18 changed files with 358 additions and 256 deletions

View file

@ -25,7 +25,7 @@ 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 set_thread_name, get_enabled_plugins
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>' __author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
__version__ = '0.50.2' __version__ = '0.50.2'
log = logging.getLogger('platypush') log = logging.getLogger('platypush')

View file

@ -1,8 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
.. license: MIT
"""
import logging import logging
import re import re
import socket import socket
@ -15,9 +10,17 @@ from platypush.bus import Bus
from platypush.common import ExtensionWithManifest from platypush.common import ExtensionWithManifest
from platypush.config import Config from platypush.config import Config
from platypush.context import get_backend from platypush.context import get_backend
from platypush.message.event.zeroconf import ZeroconfServiceAddedEvent, ZeroconfServiceRemovedEvent from platypush.message.event.zeroconf import (
from platypush.utils import set_timeout, clear_timeout, \ ZeroconfServiceAddedEvent,
get_redis_queue_name_by_message, set_thread_name, get_backend_name_by_class ZeroconfServiceRemovedEvent,
)
from platypush.utils import (
set_timeout,
clear_timeout,
get_redis_queue_name_by_message,
set_thread_name,
get_backend_name_by_class,
)
from platypush import __version__ from platypush import __version__
from platypush.event import EventGenerator from platypush.event import EventGenerator
@ -44,7 +47,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
# Loop function, can be implemented by derived classes # Loop function, can be implemented by derived classes
loop = None loop = None
def __init__(self, bus: Optional[Bus] = None, poll_seconds: Optional[float] = None, **kwargs): def __init__(
self, bus: Optional[Bus] = None, poll_seconds: Optional[float] = None, **kwargs
):
""" """
:param bus: Reference to the bus object to be used in the backend :param bus: Reference to the bus object to be used in the backend
:param poll_seconds: If the backend implements a ``loop`` method, this parameter expresses how often the :param poll_seconds: If the backend implements a ``loop`` method, this parameter expresses how often the
@ -65,14 +70,15 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
self.thread_id = None self.thread_id = None
self._stop_event = ThreadEvent() self._stop_event = ThreadEvent()
self._kwargs = kwargs self._kwargs = kwargs
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__)) self.logger = logging.getLogger(
'platypush:backend:' + get_backend_name_by_class(self.__class__)
)
self.zeroconf = None self.zeroconf = None
self.zeroconf_info = None self.zeroconf_info = None
# Internal-only, we set the request context on a backend if that # Internal-only, we set the request context on a backend if that
# backend is intended to react for a response to a specific request # backend is intended to react for a response to a specific request
self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs \ self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs else None
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.get('logging').upper()))
@ -90,11 +96,14 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
msg = Message.build(msg) msg = Message.build(msg)
if not getattr(msg, 'target') 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('Message received on the {} backend: {}'.format( self.logger.debug(
self.__class__.__name__, msg)) 'Message received on the {} backend: {}'.format(
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
@ -112,18 +121,23 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
and msg is that response""" and msg is that response"""
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
return self._request_context \ return (
and isinstance(msg, Response) \ self._request_context
and isinstance(msg, Response)
and msg.id == self._request_context['request'].id and msg.id == self._request_context['request'].id
)
def _get_backend_config(self): def _get_backend_config(self):
config_name = 'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower() config_name = (
'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower()
)
return Config.get(config_name) return Config.get(config_name)
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('Timed out while waiting for a response from {}'. raise RuntimeError(
format(request.target)) 'Timed out while waiting for a response from {}'.format(request.target)
)
req_ctx = { req_ctx = {
'request': request, 'request': request,
@ -131,8 +145,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
'response_timeout': response_timeout, 'response_timeout': response_timeout,
} }
resp_backend = self.__class__(bus=self.bus, _req_ctx=req_ctx, resp_backend = self.__class__(
**self._get_backend_config(), **self._kwargs) bus=self.bus, _req_ctx=req_ctx, **self._get_backend_config(), **self._kwargs
)
# Set the response timeout # Set the response timeout
if response_timeout: if response_timeout:
@ -157,8 +172,13 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
self.send_message(event, **kwargs) self.send_message(event, **kwargs)
def send_request(self, request, on_response=None, def send_request(
response_timeout=_default_response_timeout, **kwargs): self,
request,
on_response=None,
response_timeout=_default_response_timeout,
**kwargs
):
""" """
Send a request message on the backend. Send a request message on the backend.
@ -215,10 +235,12 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
if not redis: if not redis:
raise KeyError() raise KeyError()
except KeyError: except KeyError:
self.logger.warning(( self.logger.warning(
(
"Backend {} does not implement send_message " "Backend {} 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__)) ).format(self.__class__.__name__)
)
return return
redis.send_message(msg, queue_name=queue_name) redis.send_message(msg, queue_name=queue_name)
@ -249,7 +271,11 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
elif has_error: elif has_error:
time.sleep(5) time.sleep(5)
except Exception as e: except Exception as e:
self.logger.error('{} initialization error: {}'.format(self.__class__.__name__, str(e))) self.logger.error(
'{} initialization error: {}'.format(
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)
@ -267,6 +293,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
def stop(self): def stop(self):
"""Stops the backend thread by sending a STOP event on its bus""" """Stops the backend thread by sending a STOP event on its bus"""
def _async_stop(): def _async_stop():
self._stop_event.set() self._stop_event.set()
self.unregister_service() self.unregister_service()
@ -285,8 +312,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
redis_backend = get_backend('redis') redis_backend = get_backend('redis')
if not redis_backend: if not redis_backend:
self.logger.warning('Redis backend not configured - some ' self.logger.warning(
'web server features may not be working properly') 'Redis backend not configured - some '
'web server features may not be working properly'
)
redis_args = {} redis_args = {}
else: else:
redis_args = redis_backend.redis_args redis_args = redis_backend.redis_args
@ -305,7 +334,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
return response return response
except Exception as e: except Exception as e:
self.logger.error('Error while processing response to {}: {}'.format(msg, str(e))) self.logger.error(
'Error while processing response to {}: {}'.format(msg, str(e))
)
@staticmethod @staticmethod
def _get_ip() -> str: def _get_ip() -> str:
@ -318,13 +349,15 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
s.close() s.close()
return addr return addr
def register_service(self, def register_service(
self,
port: Optional[int] = None, port: Optional[int] = None,
name: Optional[str] = None, name: Optional[str] = None,
srv_type: Optional[str] = None, srv_type: Optional[str] = None,
srv_name: Optional[str] = None, srv_name: Optional[str] = None,
udp: bool = False, udp: bool = False,
properties: Optional[Dict] = None): properties: Optional[Dict] = None,
):
""" """
Initialize the Zeroconf service configuration for this backend. Initialize the Zeroconf service configuration for this backend.
@ -348,7 +381,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
from zeroconf import ServiceInfo, Zeroconf from zeroconf import ServiceInfo, Zeroconf
from platypush.plugins.zeroconf import ZeroconfListener from platypush.plugins.zeroconf import ZeroconfListener
except ImportError: except ImportError:
self.logger.warning('zeroconf package not available, service discovery will be disabled.') self.logger.warning(
'zeroconf package not available, service discovery will be disabled.'
)
return return
self.zeroconf = Zeroconf() self.zeroconf = Zeroconf()
@ -360,28 +395,40 @@ 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(name=name, proto='udp' if udp else 'tcp') srv_type = srv_type or '_platypush-{name}._{proto}.local.'.format(
srv_name = srv_name or '{host}.{type}'.format(host=self.device_id, type=srv_type) name=name, proto='udp' if udp else 'tcp'
)
srv_name = srv_name or '{host}.{type}'.format(
host=self.device_id, type=srv_type
)
if port: if port:
srv_port = port srv_port = port
else: else:
srv_port = self.port if hasattr(self, 'port') else None srv_port = self.port if hasattr(self, 'port') else None
self.zeroconf_info = ServiceInfo(srv_type, srv_name, self.zeroconf_info = ServiceInfo(
srv_type,
srv_name,
addresses=[socket.inet_aton(self._get_ip())], addresses=[socket.inet_aton(self._get_ip())],
port=srv_port, port=srv_port,
weight=0, weight=0,
priority=0, priority=0,
properties=srv_desc) properties=srv_desc,
)
if not self.zeroconf_info: if not self.zeroconf_info:
self.logger.warning('Could not register Zeroconf service') self.logger.warning('Could not register Zeroconf service')
return return
self.zeroconf.register_service(self.zeroconf_info) self.zeroconf.register_service(self.zeroconf_info)
self.bus.post(ZeroconfServiceAddedEvent(service_type=srv_type, service_name=srv_name, self.bus.post(
service_info=ZeroconfListener.parse_service_info(self.zeroconf_info))) ZeroconfServiceAddedEvent(
service_type=srv_type,
service_name=srv_name,
service_info=ZeroconfListener.parse_service_info(self.zeroconf_info),
)
)
def unregister_service(self): def unregister_service(self):
""" """
@ -391,17 +438,26 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
try: try:
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('Could not register Zeroconf service {}: {}: {}'.format( self.logger.warning(
self.zeroconf_info.name, type(e).__name__, str(e))) 'Could not register Zeroconf service {}: {}: {}'.format(
self.zeroconf_info.name, type(e).__name__, str(e)
)
)
if self.zeroconf: if self.zeroconf:
self.zeroconf.close() self.zeroconf.close()
if self.zeroconf_info: if self.zeroconf_info:
self.bus.post(ZeroconfServiceRemovedEvent(service_type=self.zeroconf_info.type, self.bus.post(
service_name=self.zeroconf_info.name)) ZeroconfServiceRemovedEvent(
service_type=self.zeroconf_info.type,
service_name=self.zeroconf_info.name,
)
)
else: else:
self.bus.post(ZeroconfServiceRemovedEvent(service_type=None, service_name=None)) self.bus.post(
ZeroconfServiceRemovedEvent(service_type=None, service_name=None)
)
self.zeroconf_info = None self.zeroconf_info = None
self.zeroconf = None self.zeroconf = None

View file

@ -1,8 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
.. license: MIT
"""
import json import json
import os import os
import time import time

View file

@ -1,8 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
.. license: MIT
"""
import os import os
import threading import threading
@ -89,10 +84,14 @@ class AssistantSnowboyBackend(AssistantBackend):
self.detector = snowboydecoder.HotwordDetector( self.detector = snowboydecoder.HotwordDetector(
[model['voice_model_file'] for model in self.models.values()], [model['voice_model_file'] for model in self.models.values()],
sensitivity=[model['sensitivity'] for model in self.models.values()], sensitivity=[model['sensitivity'] for model in self.models.values()],
audio_gain=self.audio_gain) audio_gain=self.audio_gain,
)
self.logger.info('Initialized Snowboy hotword detection with {} voice model configurations'. self.logger.info(
format(len(self.models))) 'Initialized Snowboy hotword detection with {} voice model configurations'.format(
len(self.models)
)
)
def _init_models(self, models): def _init_models(self, models):
if not models: if not models:
@ -107,7 +106,9 @@ class AssistantSnowboyBackend(AssistantBackend):
detect_sound = conf.get('detect_sound') detect_sound = conf.get('detect_sound')
if not model_file: if not model_file:
raise AttributeError('No voice_model_file specified for model {}'.format(name)) raise AttributeError(
'No voice_model_file specified for model {}'.format(name)
)
model_file = os.path.abspath(os.path.expanduser(model_file)) model_file = os.path.abspath(os.path.expanduser(model_file))
assistant_plugin_name = conf.get('assistant_plugin') assistant_plugin_name = conf.get('assistant_plugin')
@ -116,14 +117,19 @@ class AssistantSnowboyBackend(AssistantBackend):
detect_sound = os.path.abspath(os.path.expanduser(detect_sound)) detect_sound = os.path.abspath(os.path.expanduser(detect_sound))
if not os.path.isfile(model_file): if not os.path.isfile(model_file):
raise FileNotFoundError('Voice model file {} does not exist or it not a regular file'. raise FileNotFoundError(
format(model_file)) 'Voice model file {} does not exist or it not a regular file'.format(
model_file
)
)
self.models[name] = { self.models[name] = {
'voice_model_file': model_file, 'voice_model_file': model_file,
'sensitivity': conf.get('sensitivity', 0.5), 'sensitivity': conf.get('sensitivity', 0.5),
'detect_sound': detect_sound, 'detect_sound': detect_sound,
'assistant_plugin': get_plugin(assistant_plugin_name) if assistant_plugin_name else None, 'assistant_plugin': get_plugin(assistant_plugin_name)
if assistant_plugin_name
else None,
'assistant_language': conf.get('assistant_language'), 'assistant_language': conf.get('assistant_language'),
'tts_plugin': conf.get('tts_plugin'), 'tts_plugin': conf.get('tts_plugin'),
'tts_args': conf.get('tts_args', {}), 'tts_args': conf.get('tts_args', {}),
@ -143,7 +149,9 @@ class AssistantSnowboyBackend(AssistantBackend):
def callback(): def callback():
if not self.is_detecting(): if not self.is_detecting():
self.logger.info('Hotword detected but assistant response currently paused') self.logger.info(
'Hotword detected but assistant response currently paused'
)
return return
self.bus.post(HotwordDetectedEvent(hotword=hotword)) self.bus.post(HotwordDetectedEvent(hotword=hotword))
@ -159,8 +167,11 @@ class AssistantSnowboyBackend(AssistantBackend):
threading.Thread(target=sound_thread, args=(detect_sound,)).start() threading.Thread(target=sound_thread, args=(detect_sound,)).start()
if assistant_plugin: if assistant_plugin:
assistant_plugin.start_conversation(language=assistant_language, tts_plugin=tts_plugin, assistant_plugin.start_conversation(
tts_args=tts_args) language=assistant_language,
tts_plugin=tts_plugin,
tts_args=tts_args,
)
return callback return callback
@ -172,10 +183,11 @@ class AssistantSnowboyBackend(AssistantBackend):
def run(self): def run(self):
super().run() super().run()
self.detector.start(detected_callback=[ self.detector.start(
self.hotword_detected(hotword) detected_callback=[
for hotword in self.models.keys() self.hotword_detected(hotword) for hotword in self.models.keys()
]) ]
)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,13 +1,12 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import re import re
import time import time
from platypush.backend import Backend from platypush.backend import Backend
from platypush.message.event.wiimote import WiimoteEvent, \ from platypush.message.event.wiimote import (
WiimoteConnectionEvent, WiimoteDisconnectionEvent WiimoteEvent,
WiimoteConnectionEvent,
WiimoteDisconnectionEvent,
)
class WiimoteBackend(Backend): class WiimoteBackend(Backend):
@ -30,8 +29,9 @@ class WiimoteBackend(Backend):
_last_btn_event_time = 0 _last_btn_event_time = 0
_bdaddr = None _bdaddr = None
def __init__(self, bdaddr=_bdaddr, inactivity_timeout=_inactivity_timeout, def __init__(
*args, **kwargs): self, bdaddr=_bdaddr, inactivity_timeout=_inactivity_timeout, *args, **kwargs
):
""" """
:param bdaddr: If set, connect to this specific Wiimote physical address (example: 00:11:22:33:44:55) :param bdaddr: If set, connect to this specific Wiimote physical address (example: 00:11:22:33:44:55)
:type bdaddr: str :type bdaddr: str
@ -55,7 +55,9 @@ class WiimoteBackend(Backend):
self._wiimote = cwiid.Wiimote() self._wiimote = cwiid.Wiimote()
self._wiimote.enable(cwiid.FLAG_MOTIONPLUS) self._wiimote.enable(cwiid.FLAG_MOTIONPLUS)
self._wiimote.rpt_mode = cwiid.RPT_ACC | cwiid.RPT_BTN | cwiid.RPT_MOTIONPLUS self._wiimote.rpt_mode = (
cwiid.RPT_ACC | cwiid.RPT_BTN | cwiid.RPT_MOTIONPLUS
)
self.logger.info('WiiMote connected') self.logger.info('WiiMote connected')
self._last_btn_event_time = time.time() self._last_btn_event_time = time.time()
@ -65,24 +67,34 @@ class WiimoteBackend(Backend):
def get_state(self): def get_state(self):
import cwiid import cwiid
wm = self.get_wiimote() wm = self.get_wiimote()
state = wm.state state = wm.state
parsed_state = {} parsed_state = {}
# Get buttons # Get buttons
all_btns = [attr for attr in dir(cwiid) if attr.startswith('BTN_')] all_btns = [attr for attr in dir(cwiid) if attr.startswith('BTN_')]
parsed_state['buttons'] = {btn: True for btn in all_btns parsed_state['buttons'] = {
if state.get('buttons', 0) & getattr(cwiid, btn) != 0} btn: True
for btn in all_btns
if state.get('buttons', 0) & getattr(cwiid, btn) != 0
}
# Get LEDs # Get LEDs
all_leds = [attr for attr in dir(cwiid) if re.match('LED\d_ON', attr)] all_leds = [attr for attr in dir(cwiid) if re.match(r'LED\d_ON', attr)]
parsed_state['led'] = {led[:4]: True for led in all_leds parsed_state['led'] = {
if state.get('leds', 0) & getattr(cwiid, led) != 0} led[:4]: True
for led in all_leds
if state.get('leds', 0) & getattr(cwiid, led) != 0
}
# Get errors # Get errors
all_errs = [attr for attr in dir(cwiid) if attr.startswith('ERROR_')] all_errs = [attr for attr in dir(cwiid) if attr.startswith('ERROR_')]
parsed_state['error'] = {err: True for err in all_errs parsed_state['error'] = {
if state.get('errs', 0) & getattr(cwiid, err) != 0} err: True
for err in all_errs
if state.get('errs', 0) & getattr(cwiid, err) != 0
}
parsed_state['battery'] = round(state.get('battery', 0) / cwiid.BATTERY_MAX, 3) parsed_state['battery'] = round(state.get('battery', 0) / cwiid.BATTERY_MAX, 3)
parsed_state['rumble'] = bool(state.get('rumble', 0)) parsed_state['rumble'] = bool(state.get('rumble', 0))
@ -92,8 +104,9 @@ class WiimoteBackend(Backend):
if 'motionplus' in state: if 'motionplus' in state:
parsed_state['motionplus'] = { parsed_state['motionplus'] = {
'angle_rate': tuple(int(angle / 100) for angle 'angle_rate': tuple(
in state['motionplus']['angle_rate']), int(angle / 100) for angle in state['motionplus']['angle_rate']
),
'low_speed': state['motionplus']['low_speed'], 'low_speed': state['motionplus']['low_speed'],
} }
@ -121,23 +134,30 @@ class WiimoteBackend(Backend):
while not self.should_stop(): while not self.should_stop():
try: try:
state = self.get_state() state = self.get_state()
changed_state = {k: state[k] for k in state.keys() changed_state = {
if state[k] != last_state.get(k)} k: state[k] for k in state.keys() if state[k] != last_state.get(k)
}
if changed_state: if changed_state:
self.bus.post(WiimoteEvent(**changed_state)) self.bus.post(WiimoteEvent(**changed_state))
if 'buttons' in changed_state: if 'buttons' in changed_state:
self._last_btn_event_time = time.time() self._last_btn_event_time = time.time()
elif last_state and time.time() - \ elif (
self._last_btn_event_time >= self._inactivity_timeout: last_state
and time.time() - self._last_btn_event_time
>= self._inactivity_timeout
):
self.logger.info('Wiimote disconnected upon timeout') self.logger.info('Wiimote disconnected upon timeout')
self.close() self.close()
last_state = state last_state = state
time.sleep(0.1) time.sleep(0.1)
except RuntimeError as e: except RuntimeError as e:
if type(e) == RuntimeError and str(e) == 'Error opening wiimote connection': if (
type(e) == RuntimeError
and str(e) == 'Error opening wiimote connection'
):
if self._connection_attempts == 0: if self._connection_attempts == 0:
self.logger.info('Press 1+2 to pair your WiiMote controller') self.logger.info('Press 1+2 to pair your WiiMote controller')
else: else:
@ -146,4 +166,5 @@ class WiimoteBackend(Backend):
self.close() self.close()
self._connection_attempts += 1 self._connection_attempts += 1
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -3,9 +3,6 @@ Platydock
Platydock is a helper that allows you to easily manage (create, destroy, start, Platydock is a helper that allows you to easily manage (create, destroy, start,
stop and list) Platypush instances as Docker images. stop and list) Platypush instances as Docker images.
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
.. license: MIT
""" """
import argparse import argparse

View file

@ -1,15 +1,16 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import os import os
from typing import Optional
from platypush.context import get_bus from platypush.context import get_bus
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.assistant import AssistantPlugin from platypush.plugins.assistant import AssistantPlugin
from platypush.message.event.assistant import ConversationStartEvent, \ from platypush.message.event.assistant import (
ConversationEndEvent, SpeechRecognizedEvent, ResponseEvent ConversationStartEvent,
ConversationEndEvent,
SpeechRecognizedEvent,
ResponseEvent,
)
class AssistantEchoPlugin(AssistantPlugin): class AssistantEchoPlugin(AssistantPlugin):
@ -38,8 +39,13 @@ class AssistantEchoPlugin(AssistantPlugin):
* **avs** (``pip install avs``) * **avs** (``pip install avs``)
""" """
def __init__(self, avs_config_file: str = None, audio_device: str = 'default', def __init__(
audio_player: str = 'default', **kwargs): self,
avs_config_file: Optional[str] = None,
audio_device: str = 'default',
audio_player: str = 'default',
**kwargs
):
""" """
:param avs_config_file: AVS credentials file - default: ~/.avs.json. If the file doesn't exist then :param avs_config_file: AVS credentials file - default: ~/.avs.json. If the file doesn't exist then
an instance of the AVS authentication service will be spawned. You can login through an Amazon an instance of the AVS authentication service will be spawned. You can login through an Amazon
@ -61,9 +67,12 @@ class AssistantEchoPlugin(AssistantPlugin):
if not avs_config_file or not os.path.isfile(avs_config_file): if not avs_config_file or not os.path.isfile(avs_config_file):
from avs.auth import auth from avs.auth import auth
auth(None, avs_config_file) auth(None, avs_config_file)
self.logger.warning('Amazon Echo assistant credentials not configured. Open http://localhost:3000 ' + self.logger.warning(
'to authenticate this client') 'Amazon Echo assistant credentials not configured. Open http://localhost:3000 '
+ 'to authenticate this client'
)
self.audio_device = audio_device self.audio_device = audio_device
self.audio_player = audio_player self.audio_player = audio_player
@ -84,37 +93,43 @@ class AssistantEchoPlugin(AssistantPlugin):
def _on_ready(self): def _on_ready(self):
def _callback(): def _callback():
self._ready = True self._ready = True
return _callback return _callback
def _on_listening(self): def _on_listening(self):
def _callback(): def _callback():
get_bus().post(ConversationStartEvent(assistant=self)) get_bus().post(ConversationStartEvent(assistant=self))
return _callback return _callback
def _on_speaking(self): def _on_speaking(self):
def _callback(): def _callback():
# AVS doesn't provide a way to access the response text # AVS doesn't provide a way to access the response text
get_bus().post(ResponseEvent(assistant=self, response_text='')) get_bus().post(ResponseEvent(assistant=self, response_text=''))
return _callback return _callback
def _on_finished(self): def _on_finished(self):
def _callback(): def _callback():
get_bus().post(ConversationEndEvent(assistant=self)) get_bus().post(ConversationEndEvent(assistant=self))
return _callback return _callback
def _on_disconnected(self): def _on_disconnected(self):
def _callback(): def _callback():
self._ready = False self._ready = False
return _callback return _callback
def _on_thinking(self): def _on_thinking(self):
def _callback(): def _callback():
# AVS doesn't provide a way to access the detected text # AVS doesn't provide a way to access the detected text
get_bus().post(SpeechRecognizedEvent(assistant=self, phrase='')) get_bus().post(SpeechRecognizedEvent(assistant=self, phrase=''))
return _callback return _callback
@action @action
def start_conversation(self, **kwargs): def start_conversation(self, **_):
if not self._ready: if not self._ready:
raise RuntimeError('Echo assistant not ready') raise RuntimeError('Echo assistant not ready')

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
from platypush.backend.assistant.google import AssistantGoogleBackend from platypush.backend.assistant.google import AssistantGoogleBackend
from platypush.context import get_backend from platypush.context import get_backend
from platypush.plugins import action from platypush.plugins import action
@ -19,10 +15,12 @@ class AssistantGooglePlugin(AssistantPlugin):
super().__init__(**kwargs) super().__init__(**kwargs)
def _get_assistant(self) -> AssistantGoogleBackend: def _get_assistant(self) -> AssistantGoogleBackend:
return get_backend('assistant.google') backend = get_backend('assistant.google')
assert backend, 'The assistant.google backend is not configured.'
return backend
@action @action
def start_conversation(self, **kwargs): def start_conversation(self):
""" """
Programmatically start a conversation with the assistant Programmatically start a conversation with the assistant
""" """

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import json import json
import os import os
from typing import Optional, Dict, Any from typing import Optional, Dict, Any

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import dateutil.parser import dateutil.parser
import importlib import importlib
@ -53,7 +49,9 @@ class CalendarPlugin(Plugin, CalendarInterface):
for calendar in calendars: for calendar in calendars:
if 'type' not in calendar: if 'type' not in calendar:
self.logger.warning("Invalid calendar with no type specified: {}".format(calendar)) self.logger.warning(
"Invalid calendar with no type specified: {}".format(calendar)
)
continue continue
cal_type = calendar.pop('type') cal_type = calendar.pop('type')
@ -62,7 +60,6 @@ class CalendarPlugin(Plugin, CalendarInterface):
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
self.calendars.append(getattr(module, class_name)(**calendar)) self.calendars.append(getattr(module, class_name)(**calendar))
@action @action
def get_upcoming_events(self, max_results=10): def get_upcoming_events(self, max_results=10):
""" """
@ -71,7 +68,8 @@ class CalendarPlugin(Plugin, CalendarInterface):
:param max_results: Maximum number of results to be returned (default: 10) :param max_results: Maximum number of results to be returned (default: 10)
:type max_results: int :type max_results: int
:returns: platypush.message.Response -- Response object with the list of events in the Google calendar API format. :returns: platypush.message.Response -- Response object with the list of
events in the Google calendar API format.
Example:: Example::
@ -113,15 +111,16 @@ class CalendarPlugin(Plugin, CalendarInterface):
except Exception as e: except Exception as e:
self.logger.warning('Could not retrieve events: {}'.format(str(e))) self.logger.warning('Could not retrieve events: {}'.format(str(e)))
events = sorted(events, key=lambda event: events = sorted(
dateutil.parser.parse( events,
key=lambda event: dateutil.parser.parse(
event['start']['dateTime'] event['start']['dateTime']
if 'dateTime' in event['start'] if 'dateTime' in event['start']
else event['start']['date'] + 'T00:00:00+00:00' else event['start']['date'] + 'T00:00:00+00:00'
))[:max_results] ),
)[:max_results]
return events return events
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import datetime import datetime
import requests import requests
from typing import Optional from typing import Optional
@ -35,15 +31,14 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
return return
if type(t.dt) == datetime.date: if type(t.dt) == datetime.date:
return ( return datetime.datetime(
datetime.datetime(
t.dt.year, t.dt.month, t.dt.day, tzinfo=datetime.timezone.utc t.dt.year, t.dt.month, t.dt.day, tzinfo=datetime.timezone.utc
).isoformat() ).isoformat()
)
return ( return (
datetime.datetime.utcfromtimestamp(t.dt.timestamp()) datetime.datetime.utcfromtimestamp(t.dt.timestamp())
.replace(tzinfo=datetime.timezone.utc).isoformat() .replace(tzinfo=datetime.timezone.utc)
.isoformat()
) )
@classmethod @classmethod
@ -52,23 +47,27 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
'id': str(event.get('uid')) if event.get('uid') else None, 'id': str(event.get('uid')) if event.get('uid') else None,
'kind': 'calendar#event', 'kind': 'calendar#event',
'summary': str(event.get('summary')) if event.get('summary') else None, 'summary': str(event.get('summary')) if event.get('summary') else None,
'description': str(event.get('description')) if event.get('description') else None, 'description': str(event.get('description'))
if event.get('description')
else None,
'status': str(event.get('status')).lower() if event.get('status') else None, 'status': str(event.get('status')).lower() if event.get('status') else None,
'responseStatus': str(event.get('partstat')).lower() if event.get('partstat') else None, 'responseStatus': str(event.get('partstat')).lower()
if event.get('partstat')
else None,
'location': str(event.get('location')) if event.get('location') else None, 'location': str(event.get('location')) if event.get('location') else None,
'htmlLink': str(event.get('url')) if event.get('url') else None, 'htmlLink': str(event.get('url')) if event.get('url') else None,
'organizer': { 'organizer': {
'email': str(event.get('organizer')).replace('MAILTO:', ''), 'email': str(event.get('organizer')).replace('MAILTO:', ''),
'displayName': event.get('organizer').params.get('cn') 'displayName': event.get('organizer').params.get('cn'),
} if event.get('organizer') else None, }
if event.get('organizer')
else None,
'created': cls._convert_timestamp(event, 'created'), 'created': cls._convert_timestamp(event, 'created'),
'updated': cls._convert_timestamp(event, 'last-modified'), 'updated': cls._convert_timestamp(event, 'last-modified'),
'start': { 'start': {
'dateTime': cls._convert_timestamp(event, 'dtstart'), 'dateTime': cls._convert_timestamp(event, 'dtstart'),
'timeZone': 'UTC', 'timeZone': 'UTC',
}, },
'end': { 'end': {
'dateTime': cls._convert_timestamp(event, 'dtend'), 'dateTime': cls._convert_timestamp(event, 'dtend'),
'timeZone': 'UTC', 'timeZone': 'UTC',
@ -76,7 +75,7 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
} }
@action @action
def get_upcoming_events(self, max_results=10, only_participating=True): def get_upcoming_events(self, *_, only_participating=True, **__):
""" """
Get the upcoming events. See Get the upcoming events. See
:func:`~platypush.plugins.calendar.CalendarPlugin.get_upcoming_events`. :func:`~platypush.plugins.calendar.CalendarPlugin.get_upcoming_events`.
@ -86,8 +85,9 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
events = [] events = []
response = requests.get(self.url) response = requests.get(self.url)
assert response.ok, \ assert response.ok, "HTTP error while getting events from {}: {}".format(
"HTTP error while getting events from {}: {}".format(self.url, response.text) self.url, response.text
)
calendar = Calendar.from_ical(response.text) calendar = Calendar.from_ical(response.text)
for event in calendar.walk(): for event in calendar.walk():
@ -99,14 +99,22 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
if ( if (
event['status'] != 'cancelled' event['status'] != 'cancelled'
and event['end'].get('dateTime') and event['end'].get('dateTime')
and event['end']['dateTime'] >= datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() and event['end']['dateTime']
>= datetime.datetime.utcnow()
.replace(tzinfo=datetime.timezone.utc)
.isoformat()
and ( and (
(only_participating (
and event.get('responseStatus') in [None, 'accepted', 'tentative']) only_participating
or not only_participating) and event.get('responseStatus')
in [None, 'accepted', 'tentative']
)
or not only_participating
)
): ):
events.append(event) events.append(event)
return events return events
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
from platypush.plugins import Plugin from platypush.plugins import Plugin
@ -23,7 +19,8 @@ class GooglePlugin(Plugin):
5. Generate a credentials file for the needed scope:: 5. Generate a credentials file for the needed scope::
python -m platypush.plugins.google.credentials 'https://www.googleapis.com/auth/gmail.compose' ~/client_secret.json python -m platypush.plugins.google.credentials \
'https://www.googleapis.com/auth/gmail.compose' ~/client_secret.json
Requires: Requires:
@ -32,7 +29,7 @@ class GooglePlugin(Plugin):
""" """
def __init__(self, scopes=None, *args, **kwargs): def __init__(self, scopes=None, **kwargs):
""" """
Initialized the Google plugin with the required scopes. Initialized the Google plugin with the required scopes.
@ -41,14 +38,13 @@ class GooglePlugin(Plugin):
""" """
from platypush.plugins.google.credentials import get_credentials from platypush.plugins.google.credentials import get_credentials
super().__init__(**kwargs) super().__init__(**kwargs)
self._scopes = scopes or [] self._scopes = scopes or []
if self._scopes: if self._scopes:
scopes = ' '.join(sorted(self._scopes)) scopes = ' '.join(sorted(self._scopes))
self.credentials = { self.credentials = {scopes: get_credentials(scopes)}
scopes: get_credentials(scopes)
}
else: else:
self.credentials = {} self.credentials = {}
@ -57,7 +53,7 @@ class GooglePlugin(Plugin):
from apiclient import discovery from apiclient import discovery
if scopes is None: if scopes is None:
scopes = getattr(self, 'scopes') if hasattr(self, 'scopes') else [] scopes = getattr(self, 'scopes', [])
scopes = ' '.join(sorted(scopes)) scopes = ' '.join(sorted(scopes))
credentials = self.credentials[scopes] credentials = self.credentials[scopes]

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import datetime import datetime
from platypush.plugins import action from platypush.plugins import action
@ -34,9 +30,17 @@ class GoogleCalendarPlugin(GooglePlugin, CalendarInterface):
now = datetime.datetime.utcnow().isoformat() + 'Z' now = datetime.datetime.utcnow().isoformat() + 'Z'
service = self.get_service('calendar', 'v3') service = self.get_service('calendar', 'v3')
result = service.events().list(calendarId='primary', timeMin=now, result = (
maxResults=max_results, singleEvents=True, service.events()
orderBy='startTime').execute() .list(
calendarId='primary',
timeMin=now,
maxResults=max_results,
singleEvents=True,
orderBy='startTime',
)
.execute()
)
events = result.get('items', []) events = result.get('items', [])
return events return events

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import base64 import base64
import mimetypes import mimetypes
import os import os
@ -81,8 +77,9 @@ class GoogleMailPlugin(GooglePlugin):
elif main_type == 'audio': elif main_type == 'audio':
msg = MIMEAudio(content, _subtype=sub_type) msg = MIMEAudio(content, _subtype=sub_type)
elif main_type == 'application': elif main_type == 'application':
msg = MIMEApplication(content, _subtype=sub_type, msg = MIMEApplication(
_encoder=encode_base64) content, _subtype=sub_type, _encoder=encode_base64
)
else: else:
msg = MIMEBase(main_type, sub_type) msg = MIMEBase(main_type, sub_type)
msg.set_payload(content) msg.set_payload(content)
@ -93,8 +90,7 @@ class GoogleMailPlugin(GooglePlugin):
service = self.get_service('gmail', 'v1') service = self.get_service('gmail', 'v1')
body = {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()} body = {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}
message = (service.users().messages().send( message = service.users().messages().send(userId='me', body=body).execute()
userId='me', body=body).execute())
return message return message
@ -108,4 +104,5 @@ class GoogleMailPlugin(GooglePlugin):
labels = results.get('labels', []) labels = results.get('labels', [])
return labels return labels
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
from datetime import datetime from datetime import datetime
from typing import List, Union, Optional from typing import List, Union, Optional
@ -50,23 +46,29 @@ class GoogleMapsPlugin(GooglePlugin):
:type longitude: float :type longitude: float
""" """
response = requests.get('https://maps.googleapis.com/maps/api/geocode/json', response = requests.get(
'https://maps.googleapis.com/maps/api/geocode/json',
params={ params={
'latlng': '{},{}'.format(latitude, longitude), 'latlng': '{},{}'.format(latitude, longitude),
'key': self.api_key, 'key': self.api_key,
}).json() },
).json()
address = dict( address = {
(t, None) for t in ['street_number', 'street', 'locality', 'country', 'postal_code'] t: None
) for t in ['street_number', 'street', 'locality', 'country', 'postal_code']
}
address['latitude'] = latitude address['latitude'] = latitude
address['longitude'] = longitude address['longitude'] = longitude
if 'results' in response and response['results']: if 'results' in response and response['results']:
result = response['results'][0] result = response['results'][0]
self.logger.info('Google Maps geocode response for latlng ({},{}): {}'. self.logger.info(
format(latitude, longitude, result)) 'Google Maps geocode response for latlng ({},{}): {}'.format(
latitude, longitude, result
)
)
address['address'] = result['formatted_address'].split(',')[0] address['address'] = result['formatted_address'].split(',')[0]
for addr_component in result['address_components']: for addr_component in result['address_components']:
@ -92,11 +94,13 @@ class GoogleMapsPlugin(GooglePlugin):
:type longitude: float :type longitude: float
""" """
response = requests.get('https://maps.googleapis.com/maps/api/elevation/json', response = requests.get(
'https://maps.googleapis.com/maps/api/elevation/json',
params={ params={
'locations': '{},{}'.format(latitude, longitude), 'locations': '{},{}'.format(latitude, longitude),
'key': self.api_key, 'key': self.api_key,
}).json() },
).json()
elevation = None elevation = None
@ -106,7 +110,10 @@ class GoogleMapsPlugin(GooglePlugin):
return {'elevation': elevation} return {'elevation': elevation}
@action @action
def get_travel_time(self, origins: List[str], destinations: List[str], def get_travel_time(
self,
origins: List[str],
destinations: List[str],
departure_time: Optional[datetime_types] = None, departure_time: Optional[datetime_types] = None,
arrival_time: Optional[datetime_types] = None, arrival_time: Optional[datetime_types] = None,
units: str = 'metric', units: str = 'metric',
@ -115,7 +122,8 @@ class GoogleMapsPlugin(GooglePlugin):
mode: Optional[str] = None, mode: Optional[str] = None,
traffic_model: Optional[str] = None, traffic_model: Optional[str] = None,
transit_mode: Optional[List[str]] = None, transit_mode: Optional[List[str]] = None,
transit_route_preference: Optional[str] = None): transit_route_preference: Optional[str] = None,
):
""" """
Get the estimated travel time between a set of departure points and a set of destinations. Get the estimated travel time between a set of departure points and a set of destinations.
@ -194,17 +202,25 @@ class GoogleMapsPlugin(GooglePlugin):
'origins': '|'.join(origins), 'origins': '|'.join(origins),
'destinations': '|'.join(destinations), 'destinations': '|'.join(destinations),
'units': units, 'units': units,
**({'departure_time': to_datetime(departure_time)} if departure_time else {}), **(
{'departure_time': to_datetime(departure_time)}
if departure_time
else {}
),
**({'arrival_time': to_datetime(arrival_time)} if arrival_time else {}), **({'arrival_time': to_datetime(arrival_time)} if arrival_time else {}),
**({'avoid': '|'.join(avoid)} if avoid else {}), **({'avoid': '|'.join(avoid)} if avoid else {}),
**({'language': language} if language else {}), **({'language': language} if language else {}),
**({'mode': mode} if mode else {}), **({'mode': mode} if mode else {}),
**({'traffic_model': traffic_model} if traffic_model else {}), **({'traffic_model': traffic_model} if traffic_model else {}),
**({'transit_mode': transit_mode} if transit_mode else {}), **({'transit_mode': transit_mode} if transit_mode else {}),
**({'transit_route_preference': transit_route_preference} **(
if transit_route_preference else {}), {'transit_route_preference': transit_route_preference}
if transit_route_preference
else {}
),
'key': self.api_key, 'key': self.api_key,
}).json() },
).json()
assert not rs.get('error_message'), f'{rs["status"]}: {rs["error_message"]}' assert not rs.get('error_message'), f'{rs["status"]}: {rs["error_message"]}'
rows = rs.get('rows', []) rows = rs.get('rows', [])

View file

@ -1,7 +1,3 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.google import GooglePlugin from platypush.plugins.google import GooglePlugin
@ -48,10 +44,12 @@ class GoogleYoutubePlugin(GooglePlugin):
:type max_results: int :type max_results: int
:param kwargs: Any extra arguments that will be transparently passed to the YouTube API. :param kwargs: Any extra arguments that will be transparently passed to the YouTube API.
See the `Getting started - parameters <https://developers.google.com/youtube/v3/docs/search/list#parameters>`_. See the `Getting started - parameters
<https://developers.google.com/youtube/v3/docs/search/list#parameters>`_.
:return: A list of YouTube resources. :return: A list of YouTube resources.
See the `Getting started - Resource <https://developers.google.com/youtube/v3/docs/search#resource>`_. See the `Getting started - Resource
<https://developers.google.com/youtube/v3/docs/search#resource>`_.
""" """
parts = parts or self._default_parts[:] parts = parts or self._default_parts[:]
@ -63,9 +61,11 @@ class GoogleYoutubePlugin(GooglePlugin):
types = ','.join(types) types = ','.join(types)
service = self.get_service('youtube', 'v3') service = self.get_service('youtube', 'v3')
result = service.search().list(part=parts, q=query, type=types, result = (
maxResults=max_results, service.search()
**kwargs).execute() .list(part=parts, q=query, type=types, maxResults=max_results, **kwargs)
.execute()
)
events = result.get('items', []) events = result.get('items', [])
return events return events

View file

@ -1,12 +1,9 @@
"""
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import time import time
from platypush.context import get_backend from platypush.context import get_backend
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
class WiimotePlugin(Plugin): class WiimotePlugin(Plugin):
""" """
WiiMote plugin. WiiMote plugin.
@ -20,7 +17,6 @@ class WiimotePlugin(Plugin):
def _get_wiimote(cls): def _get_wiimote(cls):
return get_backend('wiimote').get_wiimote() return get_backend('wiimote').get_wiimote()
@action @action
def connect(self): def connect(self):
""" """
@ -28,7 +24,6 @@ class WiimotePlugin(Plugin):
""" """
self._get_wiimote() self._get_wiimote()
@action @action
def close(self): def close(self):
""" """
@ -36,7 +31,6 @@ class WiimotePlugin(Plugin):
""" """
get_backend('wiimote').close() get_backend('wiimote').close()
@action @action
def rumble(self, secs): def rumble(self, secs):
""" """
@ -47,7 +41,6 @@ class WiimotePlugin(Plugin):
time.sleep(secs) time.sleep(secs)
wm.rumble = False wm.rumble = False
@action @action
def state(self): def state(self):
""" """
@ -55,23 +48,22 @@ class WiimotePlugin(Plugin):
""" """
return get_backend('wiimote').get_state() return get_backend('wiimote').get_state()
@action @action
def set_leds(self, leds): def set_leds(self, leds):
""" """
Set the LEDs state on the controller Set the LEDs state on the controller
:param leds: Iterable with the new states to be applied to the LEDs. Example: [1, 0, 0, 0] or (False, True, False, False) :param leds: Iterable with the new states to be applied to the LEDs.
Example: [1, 0, 0, 0] or (False, True, False, False)
:type leds: list :type leds: list
""" """
new_led = 0 new_led = 0
for i, led in enumerate(leds): for i, led in enumerate(leds):
if led: if led:
new_led |= (1 << i) new_led |= 1 << i
self._get_wiimote().led = new_led self._get_wiimote().led = new_led
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -30,7 +30,7 @@ setup(
name="platypush", name="platypush",
version="0.50.2", version="0.50.2",
author="Fabio Manganiello", author="Fabio Manganiello",
author_email="info@fabiomanganiello.com", author_email="fabio@manganiello.tech",
description="Platypush service", description="Platypush service",
license="MIT", license="MIT",
python_requires='>= 3.6', python_requires='>= 3.6',