forked from platypush/platypush
HTTP backend dependencies moved from optional to required
If Platypush is supposed to work also without a manually created `config.yaml`, and the HTTP backend is enabled by default in that configuration, then Flask and companions should be among the required dependencies.
This commit is contained in:
parent
371fd7e46b
commit
fee5fc4ae0
4 changed files with 132 additions and 61 deletions
|
@ -91,14 +91,16 @@ class HttpBackend(Backend):
|
|||
other music plugin enabled. -->
|
||||
<Music class="col-3" />
|
||||
|
||||
<!-- Show current date, time and weather. It requires a `weather` plugin or backend enabled -->
|
||||
<!-- Show current date, time and weather.
|
||||
It requires a `weather` plugin or backend enabled -->
|
||||
<DateTimeWeather class="col-3" />
|
||||
</Row>
|
||||
|
||||
<!-- Display the following widgets on a second row -->
|
||||
<Row>
|
||||
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
|
||||
explicitly exposed as an HTTP resource through the backend `resource_dirs` attribute. -->
|
||||
explicitly exposed as an HTTP resource through the backend
|
||||
`resource_dirs` attribute. -->
|
||||
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
|
||||
|
||||
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
|
||||
|
@ -151,11 +153,7 @@ class HttpBackend(Backend):
|
|||
|
||||
Requires:
|
||||
|
||||
* **flask** (``pip install flask``)
|
||||
* **bcrypt** (``pip install bcrypt``)
|
||||
* **magic** (``pip install python-magic``), optional, for MIME type
|
||||
support if you want to enable media streaming
|
||||
* **gunicorn** (``pip install gunicorn``) - optional but recommended.
|
||||
* **gunicorn** (``pip install gunicorn``) - optional, to run the Platypush webapp over uWSGI.
|
||||
|
||||
By default the Platypush web server will run in a
|
||||
process spawned on the fly by the HTTP backend. However, being a
|
||||
|
@ -174,12 +172,22 @@ class HttpBackend(Backend):
|
|||
_DEFAULT_HTTP_PORT = 8008
|
||||
_DEFAULT_WEBSOCKET_PORT = 8009
|
||||
|
||||
def __init__(self, port=_DEFAULT_HTTP_PORT,
|
||||
websocket_port=_DEFAULT_WEBSOCKET_PORT,
|
||||
bind_address='0.0.0.0',
|
||||
disable_websocket=False, resource_dirs=None,
|
||||
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None,
|
||||
maps=None, run_externally=False, uwsgi_args=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
port=_DEFAULT_HTTP_PORT,
|
||||
websocket_port=_DEFAULT_WEBSOCKET_PORT,
|
||||
bind_address='0.0.0.0',
|
||||
disable_websocket=False,
|
||||
resource_dirs=None,
|
||||
ssl_cert=None,
|
||||
ssl_key=None,
|
||||
ssl_cafile=None,
|
||||
ssl_capath=None,
|
||||
maps=None,
|
||||
run_externally=False,
|
||||
uwsgi_args=None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
:param port: Listen port for the web server (default: 8008)
|
||||
:type port: int
|
||||
|
@ -246,26 +254,37 @@ class HttpBackend(Backend):
|
|||
self.bind_address = bind_address
|
||||
|
||||
if resource_dirs:
|
||||
self.resource_dirs = {name: os.path.abspath(
|
||||
os.path.expanduser(d)) for name, d in resource_dirs.items()}
|
||||
self.resource_dirs = {
|
||||
name: os.path.abspath(os.path.expanduser(d))
|
||||
for name, d in resource_dirs.items()
|
||||
}
|
||||
else:
|
||||
self.resource_dirs = {}
|
||||
|
||||
self.active_websockets = set()
|
||||
self.run_externally = run_externally
|
||||
self.uwsgi_args = uwsgi_args or []
|
||||
self.ssl_context = get_ssl_server_context(ssl_cert=ssl_cert,
|
||||
ssl_key=ssl_key,
|
||||
ssl_cafile=ssl_cafile,
|
||||
ssl_capath=ssl_capath) \
|
||||
if ssl_cert else None
|
||||
self.ssl_context = (
|
||||
get_ssl_server_context(
|
||||
ssl_cert=ssl_cert,
|
||||
ssl_key=ssl_key,
|
||||
ssl_cafile=ssl_cafile,
|
||||
ssl_capath=ssl_capath,
|
||||
)
|
||||
if ssl_cert
|
||||
else None
|
||||
)
|
||||
|
||||
if self.uwsgi_args:
|
||||
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + \
|
||||
['--module', 'platypush.backend.http.uwsgi', '--enable-threads']
|
||||
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + [
|
||||
'--module',
|
||||
'platypush.backend.http.uwsgi',
|
||||
'--enable-threads',
|
||||
]
|
||||
|
||||
self.local_base_url = '{proto}://localhost:{port}'.\
|
||||
format(proto=('https' if ssl_cert else 'http'), port=self.port)
|
||||
self.local_base_url = '{proto}://localhost:{port}'.format(
|
||||
proto=('https' if ssl_cert else 'http'), port=self.port
|
||||
)
|
||||
|
||||
self._websocket_lock_timeout = 10
|
||||
self._websocket_lock = threading.RLock()
|
||||
|
@ -275,7 +294,7 @@ class HttpBackend(Backend):
|
|||
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
|
||||
|
||||
def on_stop(self):
|
||||
""" On backend stop """
|
||||
"""On backend stop"""
|
||||
super().on_stop()
|
||||
self.logger.info('Received STOP event on HttpBackend')
|
||||
|
||||
|
@ -284,7 +303,9 @@ class HttpBackend(Backend):
|
|||
self.server_proc.kill()
|
||||
self.server_proc.wait(timeout=10)
|
||||
if self.server_proc.poll() is not None:
|
||||
self.logger.info('HTTP server process may be still alive at termination')
|
||||
self.logger.info(
|
||||
'HTTP server process may be still alive at termination'
|
||||
)
|
||||
else:
|
||||
self.logger.info('HTTP server process terminated')
|
||||
else:
|
||||
|
@ -293,17 +314,25 @@ class HttpBackend(Backend):
|
|||
if self.server_proc.is_alive():
|
||||
self.server_proc.kill()
|
||||
if self.server_proc.is_alive():
|
||||
self.logger.info('HTTP server process may be still alive at termination')
|
||||
self.logger.info(
|
||||
'HTTP server process may be still alive at termination'
|
||||
)
|
||||
else:
|
||||
self.logger.info('HTTP server process terminated')
|
||||
|
||||
if self.websocket_thread and self.websocket_thread.is_alive() and self._websocket_loop:
|
||||
if (
|
||||
self.websocket_thread
|
||||
and self.websocket_thread.is_alive()
|
||||
and self._websocket_loop
|
||||
):
|
||||
self._websocket_loop.stop()
|
||||
self.logger.info('HTTP websocket service terminated')
|
||||
|
||||
def _acquire_websocket_lock(self, ws):
|
||||
try:
|
||||
acquire_ok = self._websocket_lock.acquire(timeout=self._websocket_lock_timeout)
|
||||
acquire_ok = self._websocket_lock.acquire(
|
||||
timeout=self._websocket_lock_timeout
|
||||
)
|
||||
if not acquire_ok:
|
||||
raise TimeoutError('Websocket lock acquire timeout')
|
||||
|
||||
|
@ -313,13 +342,19 @@ class HttpBackend(Backend):
|
|||
finally:
|
||||
self._websocket_lock.release()
|
||||
|
||||
acquire_ok = self._websocket_locks[addr].acquire(timeout=self._websocket_lock_timeout)
|
||||
acquire_ok = self._websocket_locks[addr].acquire(
|
||||
timeout=self._websocket_lock_timeout
|
||||
)
|
||||
if not acquire_ok:
|
||||
raise TimeoutError('Websocket on address {} not ready to receive data'.format(addr))
|
||||
raise TimeoutError(
|
||||
'Websocket on address {} not ready to receive data'.format(addr)
|
||||
)
|
||||
|
||||
def _release_websocket_lock(self, ws):
|
||||
try:
|
||||
acquire_ok = self._websocket_lock.acquire(timeout=self._websocket_lock_timeout)
|
||||
acquire_ok = self._websocket_lock.acquire(
|
||||
timeout=self._websocket_lock_timeout
|
||||
)
|
||||
if not acquire_ok:
|
||||
raise TimeoutError('Websocket lock acquire timeout')
|
||||
|
||||
|
@ -327,12 +362,15 @@ class HttpBackend(Backend):
|
|||
if addr in self._websocket_locks:
|
||||
self._websocket_locks[addr].release()
|
||||
except Exception as e:
|
||||
self.logger.warning('Unhandled exception while releasing websocket lock: {}'.format(str(e)))
|
||||
self.logger.warning(
|
||||
'Unhandled exception while releasing websocket lock: {}'.format(str(e))
|
||||
)
|
||||
finally:
|
||||
self._websocket_lock.release()
|
||||
|
||||
def notify_web_clients(self, event):
|
||||
""" Notify all the connected web clients (over websocket) of a new event """
|
||||
"""Notify all the connected web clients (over websocket) of a new event"""
|
||||
|
||||
async def send_event(ws):
|
||||
try:
|
||||
self._acquire_websocket_lock(ws)
|
||||
|
@ -349,26 +387,35 @@ class HttpBackend(Backend):
|
|||
try:
|
||||
loop.run_until_complete(send_event(_ws))
|
||||
except ConnectionClosed:
|
||||
self.logger.warning('Websocket client {} connection lost'.format(_ws.remote_address))
|
||||
self.logger.warning(
|
||||
'Websocket client {} connection lost'.format(_ws.remote_address)
|
||||
)
|
||||
self.active_websockets.remove(_ws)
|
||||
if _ws.remote_address in self._websocket_locks:
|
||||
del self._websocket_locks[_ws.remote_address]
|
||||
|
||||
def websocket(self):
|
||||
""" Websocket main server """
|
||||
"""Websocket main server"""
|
||||
set_thread_name('WebsocketServer')
|
||||
|
||||
async def register_websocket(websocket, path):
|
||||
address = websocket.remote_address if websocket.remote_address \
|
||||
address = (
|
||||
websocket.remote_address
|
||||
if websocket.remote_address
|
||||
else '<unknown client>'
|
||||
)
|
||||
|
||||
self.logger.info('New websocket connection from {} on path {}'.format(address, path))
|
||||
self.logger.info(
|
||||
'New websocket connection from {} on path {}'.format(address, path)
|
||||
)
|
||||
self.active_websockets.add(websocket)
|
||||
|
||||
try:
|
||||
await websocket.recv()
|
||||
except ConnectionClosed:
|
||||
self.logger.info('Websocket client {} closed connection'.format(address))
|
||||
self.logger.info(
|
||||
'Websocket client {} closed connection'.format(address)
|
||||
)
|
||||
self.active_websockets.remove(websocket)
|
||||
if address in self._websocket_locks:
|
||||
del self._websocket_locks[address]
|
||||
|
@ -379,8 +426,13 @@ class HttpBackend(Backend):
|
|||
|
||||
self._websocket_loop = get_or_create_event_loop()
|
||||
self._websocket_loop.run_until_complete(
|
||||
websocket_serve(register_websocket, self.bind_address, self.websocket_port,
|
||||
**websocket_args))
|
||||
websocket_serve(
|
||||
register_websocket,
|
||||
self.bind_address,
|
||||
self.websocket_port,
|
||||
**websocket_args
|
||||
)
|
||||
)
|
||||
self._websocket_loop.run_forever()
|
||||
|
||||
def _start_web_server(self):
|
||||
|
@ -415,8 +467,9 @@ class HttpBackend(Backend):
|
|||
self.websocket_thread.start()
|
||||
|
||||
if not self.run_externally:
|
||||
self.server_proc = Process(target=self._start_web_server(),
|
||||
name='WebServer')
|
||||
self.server_proc = Process(
|
||||
target=self._start_web_server(), name='WebServer'
|
||||
)
|
||||
self.server_proc.start()
|
||||
self.server_proc.join()
|
||||
elif self.uwsgi_args:
|
||||
|
@ -424,9 +477,11 @@ class HttpBackend(Backend):
|
|||
self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd))
|
||||
self.server_proc = subprocess.Popen(uwsgi_cmd)
|
||||
else:
|
||||
self.logger.info('The web server is configured to be launched externally but ' +
|
||||
'no uwsgi_args were provided. Make sure that you run another external service' +
|
||||
'for the webserver (e.g. nginx)')
|
||||
self.logger.info(
|
||||
'The web server is configured to be launched externally but '
|
||||
+ 'no uwsgi_args were provided. Make sure that you run another external service'
|
||||
+ 'for the webserver (e.g. nginx)'
|
||||
)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -2,9 +2,6 @@ manifest:
|
|||
events: {}
|
||||
install:
|
||||
pip:
|
||||
- flask
|
||||
- bcrypt
|
||||
- python-magic
|
||||
- gunicorn
|
||||
package: platypush.backend.http
|
||||
type: backend
|
||||
|
|
|
@ -20,3 +20,4 @@ zeroconf
|
|||
paho-mqtt
|
||||
websocket-client
|
||||
croniter
|
||||
python-magic
|
||||
|
|
42
setup.py
42
setup.py
|
@ -17,7 +17,7 @@ def readfile(fname):
|
|||
def pkg_files(dir):
|
||||
paths = []
|
||||
# noinspection PyShadowingNames
|
||||
for (path, dirs, files) in os.walk(dir):
|
||||
for (path, _, files) in os.walk(dir):
|
||||
for file in files:
|
||||
paths.append(os.path.join('..', path, file))
|
||||
return paths
|
||||
|
@ -68,17 +68,21 @@ setup(
|
|||
'pyjwt',
|
||||
'marshmallow',
|
||||
'frozendict',
|
||||
'flask',
|
||||
'bcrypt',
|
||||
'python-magic',
|
||||
],
|
||||
|
||||
extras_require={
|
||||
# Support for thread custom name
|
||||
'threadname': ['python-prctl'],
|
||||
# Support for Kafka backend and plugin
|
||||
'kafka': ['kafka-python'],
|
||||
# Support for Pushbullet backend and plugin
|
||||
'pushbullet': ['pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master'],
|
||||
# Support for HTTP backend
|
||||
'http': ['flask', 'bcrypt', 'python-magic', 'gunicorn'],
|
||||
'pushbullet': [
|
||||
'pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master'
|
||||
],
|
||||
# Support for HTTP backend over uWSGI
|
||||
'http': ['gunicorn'],
|
||||
# Support for MQTT backends
|
||||
'mqtt': ['paho-mqtt'],
|
||||
# Support for RSS feeds parser
|
||||
|
@ -90,7 +94,11 @@ setup(
|
|||
# Support for MPD/Mopidy music server plugin and backend
|
||||
'mpd': ['python-mpd2'],
|
||||
# Support for Google text2speech plugin
|
||||
'google-tts': ['oauth2client', 'google-api-python-client', 'google-cloud-texttospeech'],
|
||||
'google-tts': [
|
||||
'oauth2client',
|
||||
'google-api-python-client',
|
||||
'google-cloud-texttospeech',
|
||||
],
|
||||
# Support for OMXPlayer plugin
|
||||
'omxplayer': ['omxplayer-wrapper'],
|
||||
# Support for YouTube
|
||||
|
@ -138,7 +146,8 @@ setup(
|
|||
# Support for web media subtitles
|
||||
'subtitles': [
|
||||
'webvtt-py',
|
||||
'python-opensubtitles @ https://github.com/agonzalezro/python-opensubtitles/tarball/master'],
|
||||
'python-opensubtitles @ https://github.com/agonzalezro/python-opensubtitles/tarball/master',
|
||||
],
|
||||
# Support for mpv player plugin
|
||||
'mpv': ['python-mpv'],
|
||||
# Support for NFC tags
|
||||
|
@ -156,14 +165,21 @@ setup(
|
|||
# Support for Dropbox integration
|
||||
'dropbox': ['dropbox'],
|
||||
# Support for Leap Motion backend
|
||||
'leap': ['leap-sdk @ https://github.com/BlackLight/leap-sdk-python3/tarball/master'],
|
||||
'leap': [
|
||||
'leap-sdk @ https://github.com/BlackLight/leap-sdk-python3/tarball/master'
|
||||
],
|
||||
# Support for Flic buttons
|
||||
'flic': ['flic @ https://github.com/50ButtonsEach/fliclib-linux-hci/tarball/master'],
|
||||
'flic': [
|
||||
'flic @ https://github.com/50ButtonsEach/fliclib-linux-hci/tarball/master'
|
||||
],
|
||||
# Support for Alexa/Echo plugin
|
||||
'alexa': ['avs @ https://github.com/BlackLight/avs/tarball/master'],
|
||||
# Support for bluetooth devices
|
||||
'bluetooth': ['pybluez', 'gattlib',
|
||||
'pyobex @ https://github.com/BlackLight/PyOBEX/tarball/master'],
|
||||
'bluetooth': [
|
||||
'pybluez',
|
||||
'gattlib',
|
||||
'pyobex @ https://github.com/BlackLight/PyOBEX/tarball/master',
|
||||
],
|
||||
# Support for TP-Link devices
|
||||
'tplink': ['pyHS100'],
|
||||
# Support for PMW3901 2-Dimensional Optical Flow Sensor
|
||||
|
@ -231,7 +247,9 @@ setup(
|
|||
# Support for Twilio integration
|
||||
'twilio': ['twilio'],
|
||||
# Support for DHT11/DHT22/AM2302 temperature/humidity sensors
|
||||
'dht': ['Adafruit_Python_DHT @ git+https://github.com/adafruit/Adafruit_Python_DHT'],
|
||||
'dht': [
|
||||
'Adafruit_Python_DHT @ git+https://github.com/adafruit/Adafruit_Python_DHT'
|
||||
],
|
||||
# Support for LCD display integration
|
||||
'lcd': ['RPi.GPIO', 'RPLCD'],
|
||||
# Support for IMAP mail integration
|
||||
|
|
Loading…
Reference in a new issue