Compare commits

..

127 commits

Author SHA1 Message Date
486cd66885
More LINTs 2022-10-23 21:23:19 +02:00
72c7444a45
LINT 2022-10-23 18:23:20 +02:00
951950c864
Added dimmer entities 2022-10-23 00:30:32 +02:00
d7278857e5
Ensure that no records with duplicate key exist within an SQLAlchemy session before flushing 2022-10-23 00:28:42 +02:00
3e6ebdd23b
Don't store/show the state of write-only toggle switches 2022-10-23 00:28:01 +02:00
8cd5cb3338
The Slider should only react to @input events 2022-10-23 00:26:59 +02:00
1af7ece881
Added deprecation notice for zwave plugin and backend (use zwave.mqtt instead) 2022-10-22 19:17:58 +02:00
5c68365188
Better management for entity error icons 2022-10-14 23:37:36 +02:00
7f575bacaa
Implemented the new zwavejs2mqtt features for adding and removing nodes 2022-10-14 23:28:02 +02:00
5995d045e1
Merge branch 'master' into 29-generic-entities-support 2022-10-14 20:57:13 +02:00
c89ed24f4b
Updated webapp dist files 2022-10-12 03:07:17 +02:00
1b791156bd
Proper support for color zigbee lights 2022-10-12 03:00:42 +02:00
e617fc75d4
Fixed slider ranges and label 2022-10-12 02:59:50 +02:00
041f64c80f
Dirty workaround to prevent redefinition of SQLAlchemy ORM model classes 2022-10-10 01:38:15 +02:00
aa5b52db2f
FIX: Still redirect to /register by default if no users have been created 2022-10-10 01:36:28 +02:00
5f09d449f4
extend_existing=True for entity tables 2022-10-09 23:15:50 +02:00
6ec8a991df
Fixed tests 2022-10-08 15:18:26 +02:00
958ef6b987
Better entity modal padding 2022-10-07 11:12:30 +02:00
16c55b45f6
updated dist files 2022-10-07 11:12:13 +02:00
b9b7404230
Web panel improvements.
- Don't return a redirect to the login page if an authentication failed
  over a JSON endpoint - instead, return a JSON payload with the error.

- Added support for additional fonts.

- Re-designed the login/registration page.

- Updated caniuse database.
2022-10-07 02:24:29 +02:00
c0ffea681f
updated dist files 2022-10-07 02:23:12 +02:00
2aab1d090d
Increased maxkb limit 2022-10-07 02:23:04 +02:00
2cc80e7f16
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-10-07 00:05:54 +02:00
deb25196d2
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-09-28 02:17:10 +02:00
1880a99052
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-08-29 01:41:47 +02:00
3513ee3e1c
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-07-08 23:13:36 +02:00
0d0995d71d
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-06-02 20:58:34 +02:00
2898a33752
s/click_url/url/g in ntfy message definitions 2022-06-02 00:36:14 +02:00
0919a0055d
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-06-02 00:13:43 +02:00
5b3e1317f4
Only refresh entities that are visible on the interface 2022-05-30 09:23:25 +02:00
1df71cb54a
Proper support for light entities on smartthings 2022-05-30 09:23:05 +02:00
0689e05e96
Apply the light color to the icon fill instead of the bulb icon itself 2022-05-30 09:18:19 +02:00
89560e7c38
Only include entities associated to enabled plugins or with no plugins in entities.get 2022-05-29 23:59:46 +02:00
30dfdeecb0
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-05-25 10:11:57 +02:00
f57f940d57
Made _is_switch more resilient against rogue Z-Wave values 2022-05-01 22:18:46 +02:00
117f92e5b4
Deprecated the light.hue backend
The polling logic has been moved to the `light.hue` plugin itself
instead, so it's no longer required to have both a plugin and a backend
enabled in order to fully manage a Hue bridge.
2022-05-01 21:55:35 +02:00
a5541c33b0
Added support for light entities in zigbee.mqtt
TODO: Support for colors (I don't have a color Zigbee bulb to test it on yet)
2022-05-01 21:10:54 +02:00
b23f45f45e
Process a zigbee entity update event with all the properties, not only the ones that have been changed 2022-05-01 21:09:13 +02:00
088cf23958
Do not emit input event from the light component upon update
It may be an incomplete update that breaks the UI, and it will be
overwritten by the backend event anyway
2022-05-01 21:08:02 +02:00
e8f4b7c10e
CSS adjustments 2022-05-01 15:44:57 +02:00
dd12d57552
Added light UI entity component 2022-05-01 15:35:20 +02:00
5aa3750807
Re-sync the list of entities when the entities component is mounted 2022-05-01 15:34:45 +02:00
f760d44224
Refactored/simplified UI code for entities management 2022-05-01 15:34:15 +02:00
8d91fec771
Better implementation for light.hue.set_lights 2022-05-01 15:33:12 +02:00
c22c17a55d
More flexible implementation for LightPlugin abstract methods 2022-05-01 15:31:45 +02:00
46df3a6a98
FIX: reachable is an attribute of state 2022-05-01 01:58:05 +02:00
8e06b8c727
Fixed range scaling on Slider component 2022-04-30 23:40:14 +02:00
30a024befb
Manage hue/sat/bri/ct light ranges on the light entity object itself 2022-04-30 19:38:50 +02:00
b16af0a97f
Include entity data attributes in the entity info modal 2022-04-30 16:39:37 +02:00
c7970842d7
Disable logging by default for entity events (they can be quite spammy) 2022-04-30 02:13:20 +02:00
7df67aca82
updated_at should have utcnow() onupdate, not now() 2022-04-30 01:48:55 +02:00
d29b377cf1
Exclude deleted lights/groups/scenes from the returned lists 2022-04-30 01:39:39 +02:00
8d57cf06c2
Major refactor for the light.hue plugin.
- Added support for lights as native platform entities.
- Improved performance by using the JSON API objects whenever possible
  to interact with the bridge instead of the native Python objects,
  which perform a bunch of lazy API calls under the hood resulting in
  degraded performance.
- Fixed lights animation attributes by setting only the ones actually
  supported by a light.
- Several LINT fixes.
2022-04-30 01:07:00 +02:00
975d37c562
Added relevant attributes to light entities 2022-04-29 23:29:04 +02:00
90f067de61
Added reachable flag to device entities 2022-04-29 23:27:35 +02:00
f45df5d4d3
Explictly cast entity IDs to strings when coalescing entity updates
Some plugins may represent entity IDs as integers, while the database
maps external IDs to strings. This may result in entities being
incorrectly mapped during merging. Casting to string prevents these
type-related ambiguities.
2022-04-29 23:24:28 +02:00
975991ba69
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-04-29 16:53:41 +02:00
d22fbcd9db
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-04-28 01:58:24 +02:00
47f8520f3b
Added support for description/read_only/write_only on entity level 2022-04-24 22:18:29 +02:00
d261b9bb9b
Frontend support for entities deletion 2022-04-24 21:40:10 +02:00
9981cc4746
Backend support for entities deletion 2022-04-24 21:38:45 +02:00
3e4b13d20f
Added standard Vue component for confirm dialogs 2022-04-24 21:34:39 +02:00
321a61d06d
Align .section.right content to the right 2022-04-24 11:30:52 +02:00
b22df768eb
Fixed entity icon alignment on mobile 2022-04-24 01:42:14 +02:00
8e2154f2b5
Do not overwrite an entity's state from an event if the state was not sampled 2022-04-24 01:41:45 +02:00
a9751f21f1
entities should be the default view when the web panel is opened 2022-04-24 01:40:34 +02:00
135965176d
Support for entity icon color change 2022-04-23 17:52:21 +02:00
ef6b57df31
Added entity info modal and (partial) support for renaming entities 2022-04-23 01:01:14 +02:00
7d4bd20df0
Support for individual entity group refresh 2022-04-19 23:56:49 +02:00
e6bfa1c50f
Better dynamic entities discovery 2022-04-13 11:25:14 +02:00
332c91252c
zwave.mqtt.status renamed to controller_status, while status should return the current state of the values 2022-04-12 23:44:14 +02:00
b35c761a43
Fixed entities panel mobile layout 2022-04-12 22:24:19 +02:00
08c0779347
<style> on entity components should be scoped 2022-04-12 16:00:31 +02:00
595ebe49ca
Support for entity scan timeout errors and visual error handling 2022-04-12 15:58:19 +02:00
548d487e73
Publish a switch entity from zigbee.mqtt only if the update includes its state 2022-04-12 14:41:21 +02:00
20530c2b6d
Loading events are now synchronized both ways upon entity action/refresh 2022-04-12 01:10:09 +02:00
9ddcf5eaeb
Implemented entities refresh on the UI 2022-04-12 00:43:22 +02:00
2aa8778078
Do not process EntityUpdateEvents only in case of payload changes
The UI relies on these events upon refresh to detect if a device is
still reacheable. Therefore, we shouldn't mask them if we don't detect
any changes with the current entity configuration/state.
2022-04-12 00:41:20 +02:00
72617b4b75
Handle EntityUpdateEvents on the UI 2022-04-11 23:16:29 +02:00
be4d1e8e01
Proper support for native entities in zigbee.mqtt integration 2022-04-11 21:16:45 +02:00
db4ad5825e
Fire an EntityUpdateEvent when the zwave.mqtt backend gets a value changed message 2022-04-11 01:40:49 +02:00
4471001110
smartthings.toggle should properly publish the updated entity 2022-04-11 00:43:31 +02:00
f17245e8c7
Send an EntityUpdateEvent only if an entity has already been persisted
If an event comes from an entity that hasn't been persisted yet on the
internal storage then we wait for the entity record to be committed
before firing an event. It's better to wait a couple of seconds for the
database to synchronize rather than dealing with entity events with
incomplete objects.
2022-04-11 00:38:11 +02:00
67ff585f6c
Entities engine improvements
- Added cache support to prevent duplicate EntityUpdateEvents
- The cache is smartly pre-populated and kept up-to-date, so it's
  possible to trigger events as soon as the entities are published by
  the plugin (not only when the records are flushed to the internal db)
2022-04-11 00:01:21 +02:00
17615ff028
Support for multiple entity types/plugins filter on entities.get 2022-04-10 21:23:03 +02:00
532217be12
Support for filtering entities by search string 2022-04-10 17:57:51 +02:00
f301fd7e69
Added standard NoItems component to handle visualization of no-results divs 2022-04-10 14:27:32 +02:00
58861afb1c
Added entities panel 2022-04-10 13:07:36 +02:00
8ec9c8f203
Added standard component for icons 2022-04-10 13:07:01 +02:00
3435f591eb
Support for keep-open-on-item-click and icon URLs on dropdown elements 2022-04-10 01:57:39 +02:00
19223bbbe1
Added SmartThings icon 2022-04-10 01:56:47 +02:00
453652ef76
Updated plugin icons 2022-04-10 01:50:45 +02:00
b2ff66aa62
Added mixins to capitalize/prettify text 2022-04-10 01:50:13 +02:00
655d56f4da
Upgraded font-awesome to 6.x 2022-04-10 01:49:14 +02:00
f52b556219
- icon_class should not be part of the backend model
- Interaction with entities should occur through the `entities.action`
  method, not by implementing native methods on each of the model
  objects
2022-04-08 16:49:47 +02:00
947b50b937
Added meta as a JSON field on the Entity table
Metadata attributes can now be defined and overridden on the object
itself, as well as on the database. Note that db settings will always
take priority in case of value conflicts.
2022-04-07 22:11:31 +02:00
db7c2095ea
Implemented meta property for entities (for now it only include icon_class) 2022-04-07 18:09:25 +02:00
e40b668380
Added missing docs 2022-04-07 01:49:13 +02:00
d3dc86a5e2
Added documentation for plugin/entity type registry 2022-04-07 01:47:42 +02:00
28026b0428
Trigger an EntityUpdateEvent when an entity state changes 2022-04-07 01:46:37 +02:00
44707731a8
Normalize UTC timezone on all the entity timestamps 2022-04-07 01:13:29 +02:00
948f37afd4
Filter by configured/enabled plugins when returning the entity/plugin registry 2022-04-07 01:04:06 +02:00
3b4f7d3dad
Added entities plugin to query/action entities 2022-04-07 00:22:54 +02:00
2eeb1d4fea
Entity objects are now JSON-able 2022-04-07 00:21:54 +02:00
26ffc0b0e1
Use Redis instead of an in-process map to store the entity/plugin registry
This is particularly useful when we want to access the registry from
another process, like the web server or an external script.
2022-04-07 00:18:11 +02:00
7b1a63e287
Make sure that flake8 and black don't step on each other's toes 2022-04-07 00:17:39 +02:00
1c6ff2fa49
(actually, the other way around is better) 2022-04-06 23:56:10 +02:00
d311629403
black validation should run before flake8 2022-04-06 23:48:27 +02:00
d52ae2fb80
Implemented RunnablePlugin.wait_stop() utility method 2022-04-05 23:33:02 +02:00
061268cdaf
Support for direct actions on native entities [WIP] 2022-04-05 23:22:54 +02:00
91ff47167b
Don't terminate the entities engine thread if a batch of entity records fails 2022-04-05 23:04:57 +02:00
fe0f3202fe
columns should be a property of the Entity object 2022-04-05 23:04:19 +02:00
8a70f1d38e
Replaced deprecated sqlalchemy.ext.declarative with sqlalchemy.orm 2022-04-05 22:47:44 +02:00
4b7eeaa4ed
Smarter merging of entities with the same key before they are committed 2022-04-05 21:17:58 +02:00
b43ed169c7
Added support for switches as native entities to zwave.mqtt plugin 2022-04-05 20:22:47 +02:00
0dac2c0e92
Fixed handling of possible null device definition in zigbee.mqtt 2022-04-05 00:31:04 +02:00
28b3672432
Added native support for switch entities to the zigbee.mqtt plugin. 2022-04-05 00:07:55 +02:00
9f2793118b
black fix 2022-04-04 22:43:04 +02:00
9d9ec1dc59
Added native support for switch entities to the smartthings plugin 2022-04-04 22:41:04 +02:00
b9c78ad913
Added native support for switch entities to switchbot.bluetooth plugin 2022-04-04 21:12:59 +02:00
91ff8d811f
Added native entities support in switchbot plugin 2022-04-04 20:56:28 +02:00
783238642d
Skip string and underscore normalization in black 2022-04-04 20:56:28 +02:00
53da19b638
Added entities engine support to WeMo switch plugin 2022-04-04 17:22:55 +02:00
7459f0115b
Added more pre-commit hooks 2022-04-04 17:22:54 +02:00
2c4c27855d
Added .exception action to logger plugin 2022-04-04 17:22:54 +02:00
9c25a131fa
get_bus() should return a default RedisBus() instance if the main bus is not registered 2022-04-04 17:22:54 +02:00
4ee7e4db29
Basic support for entities on the local db and implemented support for switch entities on the tplink plugin 2022-04-04 16:50:17 +02:00
542 changed files with 8139 additions and 2279 deletions

View file

@ -11,6 +11,7 @@ repos:
- id: check-xml - id: check-xml
- id: check-symlinks - id: check-symlinks
- id: check-added-large-files - id: check-added-large-files
args: ['--maxkb=1500']
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs - repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.2 rev: v1.1.2

View file

@ -20,6 +20,7 @@ Events
platypush/events/custom.rst platypush/events/custom.rst
platypush/events/dbus.rst platypush/events/dbus.rst
platypush/events/distance.rst platypush/events/distance.rst
platypush/events/entities.rst
platypush/events/file.rst platypush/events/file.rst
platypush/events/foursquare.rst platypush/events/foursquare.rst
platypush/events/geo.rst platypush/events/geo.rst

View file

@ -0,0 +1,5 @@
``entities``
============
.. automodule:: platypush.message.event.entities
:members:

View file

@ -0,0 +1,5 @@
``entities``
============
.. automodule:: platypush.plugins.entities
:members:

View file

@ -32,6 +32,7 @@ Plugins
platypush/plugins/db.rst platypush/plugins/db.rst
platypush/plugins/dbus.rst platypush/plugins/dbus.rst
platypush/plugins/dropbox.rst platypush/plugins/dropbox.rst
platypush/plugins/entities.rst
platypush/plugins/esp.rst platypush/plugins/esp.rst
platypush/plugins/ffmpeg.rst platypush/plugins/ffmpeg.rst
platypush/plugins/file.rst platypush/plugins/file.rst

View file

@ -9,11 +9,13 @@ import argparse
import logging import logging
import os import os
import sys import sys
from typing import Optional
from .bus.redis import RedisBus from .bus.redis import RedisBus
from .config import Config from .config import Config
from .context import register_backends, register_plugins from .context import register_backends, register_plugins
from .cron.scheduler import CronScheduler from .cron.scheduler import CronScheduler
from .entities import init_entities_engine, EntitiesEngine
from .event.processor import EventProcessor from .event.processor import EventProcessor
from .logger import Logger from .logger import Logger
from .message.event import Event from .message.event import Event
@ -96,6 +98,7 @@ class Daemon:
self.no_capture_stdout = no_capture_stdout self.no_capture_stdout = no_capture_stdout
self.no_capture_stderr = no_capture_stderr self.no_capture_stderr = no_capture_stderr
self.event_processor = EventProcessor() self.event_processor = EventProcessor()
self.entities_engine: Optional[EntitiesEngine] = None
self.requests_to_process = requests_to_process self.requests_to_process = requests_to_process
self.processed_requests = 0 self.processed_requests = 0
self.cron_scheduler = None self.cron_scheduler = None
@ -199,6 +202,7 @@ class Daemon:
"""Stops the backends and the bus""" """Stops the backends and the bus"""
from .plugins import RunnablePlugin from .plugins import RunnablePlugin
if self.backends:
for backend in self.backends.values(): for backend in self.backends.values():
backend.stop() backend.stop()
@ -206,9 +210,17 @@ class Daemon:
if isinstance(plugin, RunnablePlugin): if isinstance(plugin, RunnablePlugin):
plugin.stop() plugin.stop()
if self.bus:
self.bus.stop() self.bus.stop()
self.bus = None
if self.cron_scheduler: if self.cron_scheduler:
self.cron_scheduler.stop() self.cron_scheduler.stop()
self.cron_scheduler = None
if self.entities_engine:
self.entities_engine.stop()
self.entities_engine = None
def run(self): def run(self):
"""Start the daemon""" """Start the daemon"""
@ -230,6 +242,9 @@ class Daemon:
# Initialize the plugins # Initialize the plugins
register_plugins(bus=self.bus) register_plugins(bus=self.bus)
# Initialize the entities engine
self.entities_engine = init_entities_engine()
# Start the cron scheduler # Start the cron scheduler
if Config.get_cronjobs(): if Config.get_cronjobs():
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs()) self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())

View file

@ -3,8 +3,7 @@ import os
from typing import Optional, Union, List, Dict, Any from typing import Optional, Union, List, Dict, Any
from sqlalchemy import create_engine, Column, Integer, String, DateTime from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from sqlalchemy.ext.declarative import declarative_base
from platypush.backend import Backend from platypush.backend import Backend
from platypush.config import Config from platypush.config import Config
@ -20,7 +19,7 @@ class Covid19Update(Base):
"""Models the Covid19Data table""" """Models the Covid19Data table"""
__tablename__ = 'covid19data' __tablename__ = 'covid19data'
__table_args__ = ({'sqlite_autoincrement': True}) __table_args__ = {'sqlite_autoincrement': True}
country = Column(String, primary_key=True) country = Column(String, primary_key=True)
confirmed = Column(Integer, nullable=False, default=0) confirmed = Column(Integer, nullable=False, default=0)
@ -40,7 +39,12 @@ class Covid19Backend(Backend):
""" """
# noinspection PyProtectedMember # noinspection PyProtectedMember
def __init__(self, country: Optional[Union[str, List[str]]], poll_seconds: Optional[float] = 3600.0, **kwargs): def __init__(
self,
country: Optional[Union[str, List[str]]],
poll_seconds: Optional[float] = 3600.0,
**kwargs
):
""" """
:param country: Default country (or list of countries) to retrieve the stats for. It can either be the full :param country: Default country (or list of countries) to retrieve the stats for. It can either be the full
country name or the country code. Special values: country name or the country code. Special values:
@ -56,7 +60,9 @@ class Covid19Backend(Backend):
super().__init__(poll_seconds=poll_seconds, **kwargs) super().__init__(poll_seconds=poll_seconds, **kwargs)
self._plugin: Covid19Plugin = get_plugin('covid19') self._plugin: Covid19Plugin = get_plugin('covid19')
self.country: List[str] = self._plugin._get_countries(country) self.country: List[str] = self._plugin._get_countries(country)
self.workdir = os.path.join(os.path.expanduser(Config.get('workdir')), 'covid19') self.workdir = os.path.join(
os.path.expanduser(Config.get('workdir')), 'covid19'
)
self.dbfile = os.path.join(self.workdir, 'data.db') self.dbfile = os.path.join(self.workdir, 'data.db')
os.makedirs(self.workdir, exist_ok=True) os.makedirs(self.workdir, exist_ok=True)
@ -67,22 +73,30 @@ class Covid19Backend(Backend):
self.logger.info('Stopped Covid19 backend') self.logger.info('Stopped Covid19 backend')
def _process_update(self, summary: Dict[str, Any], session: Session): def _process_update(self, summary: Dict[str, Any], session: Session):
update_time = datetime.datetime.fromisoformat(summary['Date'].replace('Z', '+00:00')) update_time = datetime.datetime.fromisoformat(
summary['Date'].replace('Z', '+00:00')
)
self.bus.post(Covid19UpdateEvent( self.bus.post(
Covid19UpdateEvent(
country=summary['Country'], country=summary['Country'],
country_code=summary['CountryCode'], country_code=summary['CountryCode'],
confirmed=summary['TotalConfirmed'], confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'], deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'], recovered=summary['TotalRecovered'],
update_time=update_time, update_time=update_time,
)) )
)
session.merge(Covid19Update(country=summary['CountryCode'], session.merge(
Covid19Update(
country=summary['CountryCode'],
confirmed=summary['TotalConfirmed'], confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'], deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'], recovered=summary['TotalRecovered'],
last_updated_at=update_time)) last_updated_at=update_time,
)
)
def loop(self): def loop(self):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@ -90,23 +104,30 @@ class Covid19Backend(Backend):
if not summaries: if not summaries:
return return
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False}) engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session.configure(bind=engine) Session.configure(bind=engine)
session = Session() session = Session()
last_records = { last_records = {
record.country: record record.country: record
for record in session.query(Covid19Update).filter(Covid19Update.country.in_(self.country)).all() for record in session.query(Covid19Update)
.filter(Covid19Update.country.in_(self.country))
.all()
} }
for summary in summaries: for summary in summaries:
country = summary['CountryCode'] country = summary['CountryCode']
last_record = last_records.get(country) last_record = last_records.get(country)
if not last_record or \ if (
summary['TotalConfirmed'] != last_record.confirmed or \ not last_record
summary['TotalDeaths'] != last_record.deaths or \ or summary['TotalConfirmed'] != last_record.confirmed
summary['TotalRecovered'] != last_record.recovered: or summary['TotalDeaths'] != last_record.deaths
or summary['TotalRecovered'] != last_record.recovered
):
self._process_update(summary=summary, session=session) self._process_update(summary=summary, session=session)
session.commit() session.commit()

View file

@ -6,15 +6,28 @@ from typing import Optional, List
import requests import requests
from sqlalchemy import create_engine, Column, String, DateTime from sqlalchemy import create_engine, Column, String, DateTime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from platypush.backend import Backend from platypush.backend import Backend
from platypush.config import Config from platypush.config import Config
from platypush.message.event.github import GithubPushEvent, GithubCommitCommentEvent, GithubCreateEvent, \ from platypush.message.event.github import (
GithubDeleteEvent, GithubEvent, GithubForkEvent, GithubWikiEvent, GithubIssueCommentEvent, GithubIssueEvent, \ GithubPushEvent,
GithubMemberEvent, GithubPublicEvent, GithubPullRequestEvent, GithubPullRequestReviewCommentEvent, \ GithubCommitCommentEvent,
GithubReleaseEvent, GithubSponsorshipEvent, GithubWatchEvent GithubCreateEvent,
GithubDeleteEvent,
GithubEvent,
GithubForkEvent,
GithubWikiEvent,
GithubIssueCommentEvent,
GithubIssueEvent,
GithubMemberEvent,
GithubPublicEvent,
GithubPullRequestEvent,
GithubPullRequestReviewCommentEvent,
GithubReleaseEvent,
GithubSponsorshipEvent,
GithubWatchEvent,
)
Base = declarative_base() Base = declarative_base()
Session = scoped_session(sessionmaker()) Session = scoped_session(sessionmaker())
@ -71,8 +84,17 @@ class GithubBackend(Backend):
_base_url = 'https://api.github.com' _base_url = 'https://api.github.com'
def __init__(self, user: str, user_token: str, repos: Optional[List[str]] = None, org: Optional[str] = None, def __init__(
poll_seconds: int = 60, max_events_per_scan: Optional[int] = 10, *args, **kwargs): self,
user: str,
user_token: str,
repos: Optional[List[str]] = None,
org: Optional[str] = None,
poll_seconds: int = 60,
max_events_per_scan: Optional[int] = 10,
*args,
**kwargs
):
""" """
If neither ``repos`` nor ``org`` is specified then the backend will monitor all new events on user level. If neither ``repos`` nor ``org`` is specified then the backend will monitor all new events on user level.
@ -102,11 +124,17 @@ class GithubBackend(Backend):
def _request(self, uri: str, method: str = 'get') -> dict: def _request(self, uri: str, method: str = 'get') -> dict:
method = getattr(requests, method.lower()) method = getattr(requests, method.lower())
return method(self._base_url + uri, auth=(self.user, self.user_token), return method(
headers={'Accept': 'application/vnd.github.v3+json'}).json() self._base_url + uri,
auth=(self.user, self.user_token),
headers={'Accept': 'application/vnd.github.v3+json'},
).json()
def _init_db(self): def _init_db(self):
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False}) engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session.configure(bind=engine) Session.configure(bind=engine)
@ -128,7 +156,11 @@ class GithubBackend(Backend):
def _get_last_event_time(self, uri: str): def _get_last_event_time(self, uri: str):
with self.db_lock: with self.db_lock:
record = self._get_or_create_resource(uri=uri, session=Session()) record = self._get_or_create_resource(uri=uri, session=Session())
return record.last_updated_at.replace(tzinfo=datetime.timezone.utc) if record.last_updated_at else None return (
record.last_updated_at.replace(tzinfo=datetime.timezone.utc)
if record.last_updated_at
else None
)
def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime): def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime):
with self.db_lock: with self.db_lock:
@ -158,9 +190,18 @@ class GithubBackend(Backend):
'WatchEvent': GithubWatchEvent, 'WatchEvent': GithubWatchEvent,
} }
event_type = event_mapping[event['type']] if event['type'] in event_mapping else GithubEvent event_type = (
return event_type(event_type=event['type'], actor=event['actor'], repo=event.get('repo', {}), event_mapping[event['type']]
payload=event['payload'], created_at=cls._to_datetime(event['created_at'])) if event['type'] in event_mapping
else GithubEvent
)
return event_type(
event_type=event['type'],
actor=event['actor'],
repo=event.get('repo', {}),
payload=event['payload'],
created_at=cls._to_datetime(event['created_at']),
)
def _events_monitor(self, uri: str, method: str = 'get'): def _events_monitor(self, uri: str, method: str = 'get'):
def thread(): def thread():
@ -175,7 +216,10 @@ class GithubBackend(Backend):
fired_events = [] fired_events = []
for event in events: for event in events:
if self.max_events_per_scan and len(fired_events) >= self.max_events_per_scan: if (
self.max_events_per_scan
and len(fired_events) >= self.max_events_per_scan
):
break break
event_time = self._to_datetime(event['created_at']) event_time = self._to_datetime(event['created_at'])
@ -189,12 +233,17 @@ class GithubBackend(Backend):
for event in fired_events: for event in fired_events:
self.bus.post(event) self.bus.post(event)
self._update_last_event_time(uri=uri, last_updated_at=new_last_event_time) self._update_last_event_time(
uri=uri, last_updated_at=new_last_event_time
)
except Exception as e: except Exception as e:
self.logger.warning('Encountered exception while fetching events from {}: {}'.format( self.logger.warning(
uri, str(e))) 'Encountered exception while fetching events from {}: {}'.format(
uri, str(e)
)
)
self.logger.exception(e) self.logger.exception(e)
finally:
if self.wait_stop(timeout=self.poll_seconds): if self.wait_stop(timeout=self.poll_seconds):
break break
@ -206,12 +255,30 @@ class GithubBackend(Backend):
if self.repos: if self.repos:
for repo in self.repos: for repo in self.repos:
monitors.append(threading.Thread(target=self._events_monitor('/networks/{repo}/events'.format(repo=repo)))) monitors.append(
threading.Thread(
target=self._events_monitor(
'/networks/{repo}/events'.format(repo=repo)
)
)
)
if self.org: if self.org:
monitors.append(threading.Thread(target=self._events_monitor('/orgs/{org}/events'.format(org=self.org)))) monitors.append(
threading.Thread(
target=self._events_monitor(
'/orgs/{org}/events'.format(org=self.org)
)
)
)
if not (self.repos or self.org): if not (self.repos or self.org):
monitors.append(threading.Thread(target=self._events_monitor('/users/{user}/events'.format(user=self.user)))) monitors.append(
threading.Thread(
target=self._events_monitor(
'/users/{user}/events'.format(user=self.user)
)
)
)
for monitor in monitors: for monitor in monitors:
monitor.start() monitor.start()
@ -222,4 +289,5 @@ class GithubBackend(Backend):
self.logger.info('Github backend terminated') self.logger.info('Github backend terminated')
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,6 +1,7 @@
import json import json
from flask import Blueprint, abort, request, Response from flask import Blueprint, abort, request
from flask.wrappers import Response
from platypush.backend.http.app import template_folder from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import authenticate, logger, send_message from platypush.backend.http.app.utils import authenticate, logger, send_message
@ -14,8 +15,8 @@ __routes__ = [
@execute.route('/execute', methods=['POST']) @execute.route('/execute', methods=['POST'])
@authenticate() @authenticate(json=True)
def execute(): def execute_route():
"""Endpoint to execute commands""" """Endpoint to execute commands"""
try: try:
msg = json.loads(request.data.decode('utf-8')) msg = json.loads(request.data.decode('utf-8'))

View file

@ -8,16 +8,21 @@ from platypush.backend.http.app import template_folder
img_folder = os.path.join(template_folder, 'img') img_folder = os.path.join(template_folder, 'img')
fonts_folder = os.path.join(template_folder, 'fonts')
icons_folder = os.path.join(template_folder, 'icons') icons_folder = os.path.join(template_folder, 'icons')
resources = Blueprint('resources', __name__, template_folder=template_folder) resources = Blueprint('resources', __name__, template_folder=template_folder)
favicon = Blueprint('favicon', __name__, template_folder=template_folder) favicon = Blueprint('favicon', __name__, template_folder=template_folder)
img = Blueprint('img', __name__, template_folder=template_folder) img = Blueprint('img', __name__, template_folder=template_folder)
icons = Blueprint('icons', __name__, template_folder=template_folder)
fonts = Blueprint('fonts', __name__, template_folder=template_folder)
# Declare routes list # Declare routes list
__routes__ = [ __routes__ = [
resources, resources,
favicon, favicon,
img, img,
icons,
fonts,
] ]
@ -42,9 +47,11 @@ def resources_path(path):
real_path = real_base_path real_path = real_base_path
file_path = [ file_path = [
s for s in re.sub( s
for s in re.sub(
r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection] r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection]
).split('/') if s ).split('/')
if s
] ]
for p in file_path[:-1]: for p in file_path[:-1]:
@ -71,10 +78,16 @@ def imgpath(path):
return send_from_directory(img_folder, path) return send_from_directory(img_folder, path)
@img.route('/icons/<path:path>', methods=['GET']) @icons.route('/icons/<path:path>', methods=['GET'])
def iconpath(path): def iconpath(path):
"""Default static icons""" """Default static icons"""
return send_from_directory(icons_folder, path) return send_from_directory(icons_folder, path)
@fonts.route('/fonts/<path:path>', methods=['GET'])
def fontpath(path):
"""Default fonts"""
return send_from_directory(fonts_folder, path)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -3,7 +3,8 @@ import logging
import os import os
from functools import wraps from functools import wraps
from flask import abort, request, redirect, Response, current_app from flask import abort, request, redirect, jsonify, current_app
from flask.wrappers import Response
from redis import Redis from redis import Redis
# NOTE: The HTTP service will *only* work on top of a Redis bus. The default # NOTE: The HTTP service will *only* work on top of a Redis bus. The default
@ -184,7 +185,37 @@ def _authenticate_csrf_token():
) )
def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=False): def authenticate(
redirect_page='',
skip_auth_methods=None,
check_csrf_token=False,
json=False,
):
def on_auth_fail(has_users=True):
if json:
if has_users:
return (
jsonify(
{
'message': 'Not logged in',
}
),
401,
)
return (
jsonify(
{
'message': 'Please register a user through '
'the web panel first',
}
),
412,
)
target_page = 'login' if has_users else 'register'
return redirect(f'/{target_page}?redirect={redirect_page or request.url}', 307)
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@ -213,9 +244,7 @@ def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=Fals
if session_auth_ok: if session_auth_ok:
return f(*args, **kwargs) return f(*args, **kwargs)
return redirect( return on_auth_fail()
'/login?redirect=' + (redirect_page or request.url), 307
)
# CSRF token check # CSRF token check
if check_csrf_token: if check_csrf_token:
@ -224,9 +253,7 @@ def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=Fals
return abort(403, 'Invalid or missing csrf_token') return abort(403, 'Invalid or missing csrf_token')
if n_users == 0 and 'session' not in skip_methods: if n_users == 0 and 'session' not in skip_methods:
return redirect( return on_auth_fail(has_users=False)
'/register?redirect=' + (redirect_page or request.url), 307
)
if ( if (
('http' not in skip_methods and http_auth_ok) ('http' not in skip_methods and http_auth_ok)

View file

@ -2,11 +2,17 @@ import datetime
import enum import enum
import os import os
from sqlalchemy import create_engine, Column, Integer, String, DateTime, \ from sqlalchemy import (
Enum, ForeignKey create_engine,
Column,
Integer,
String,
DateTime,
Enum,
ForeignKey,
)
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from platypush.backend.http.request import HttpRequest from platypush.backend.http.request import HttpRequest
@ -44,18 +50,31 @@ class RssUpdates(HttpRequest):
""" """
user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' + \ user_agent = (
'Chrome/62.0.3202.94 Safari/537.36' 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
+ 'Chrome/62.0.3202.94 Safari/537.36'
)
def __init__(self, url, title=None, headers=None, params=None, max_entries=None, def __init__(
extract_content=False, digest_format=None, user_agent: str = user_agent, self,
body_style: str = 'font-size: 22px; ' + url,
'font-family: "Merriweather", Georgia, "Times New Roman", Times, serif;', title=None,
headers=None,
params=None,
max_entries=None,
extract_content=False,
digest_format=None,
user_agent: str = user_agent,
body_style: str = 'font-size: 22px; '
+ 'font-family: "Merriweather", Georgia, "Times New Roman", Times, serif;',
title_style: str = 'margin-top: 30px', title_style: str = 'margin-top: 30px',
subtitle_style: str = 'margin-top: 10px; page-break-after: always', subtitle_style: str = 'margin-top: 10px; page-break-after: always',
article_title_style: str = 'page-break-before: always', article_title_style: str = 'page-break-before: always',
article_link_style: str = 'color: #555; text-decoration: none; border-bottom: 1px dotted', article_link_style: str = 'color: #555; text-decoration: none; border-bottom: 1px dotted',
article_content_style: str = '', *argv, **kwargs): article_content_style: str = '',
*argv,
**kwargs,
):
""" """
:param url: URL to the RSS feed to be monitored. :param url: URL to the RSS feed to be monitored.
:param title: Optional title for the feed. :param title: Optional title for the feed.
@ -91,7 +110,9 @@ class RssUpdates(HttpRequest):
# If true, then the http.webpage plugin will be used to parse the content # If true, then the http.webpage plugin will be used to parse the content
self.extract_content = extract_content self.extract_content = extract_content
self.digest_format = digest_format.lower() if digest_format else None # Supported formats: html, pdf self.digest_format = (
digest_format.lower() if digest_format else None
) # Supported formats: html, pdf
os.makedirs(os.path.expanduser(os.path.dirname(self.dbfile)), exist_ok=True) os.makedirs(os.path.expanduser(os.path.dirname(self.dbfile)), exist_ok=True)
@ -119,7 +140,11 @@ class RssUpdates(HttpRequest):
@staticmethod @staticmethod
def _get_latest_update(session, source_id): def _get_latest_update(session, source_id):
return session.query(func.max(FeedEntry.published)).filter_by(source_id=source_id).scalar() return (
session.query(func.max(FeedEntry.published))
.filter_by(source_id=source_id)
.scalar()
)
def _parse_entry_content(self, link): def _parse_entry_content(self, link):
self.logger.info('Extracting content from {}'.format(link)) self.logger.info('Extracting content from {}'.format(link))
@ -130,14 +155,20 @@ class RssUpdates(HttpRequest):
errors = response.errors errors = response.errors
if not output: if not output:
self.logger.warning('Mercury parser error: {}'.format(errors or '[unknown error]')) self.logger.warning(
'Mercury parser error: {}'.format(errors or '[unknown error]')
)
return return
return output.get('content') return output.get('content')
def get_new_items(self, response): def get_new_items(self, response):
import feedparser import feedparser
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session.configure(bind=engine) Session.configure(bind=engine)
@ -157,12 +188,16 @@ class RssUpdates(HttpRequest):
content = u''' content = u'''
<h1 style="{title_style}">{title}</h1> <h1 style="{title_style}">{title}</h1>
<h2 style="{subtitle_style}">Feeds digest generated on {creation_date}</h2>'''.\ <h2 style="{subtitle_style}">Feeds digest generated on {creation_date}</h2>'''.format(
format(title_style=self.title_style, title=self.title, subtitle_style=self.subtitle_style, title_style=self.title_style,
creation_date=datetime.datetime.now().strftime('%d %B %Y, %H:%M')) title=self.title,
subtitle_style=self.subtitle_style,
creation_date=datetime.datetime.now().strftime('%d %B %Y, %H:%M'),
)
self.logger.info('Parsed {:d} items from RSS feed <{}>' self.logger.info(
.format(len(feed.entries), self.url)) 'Parsed {:d} items from RSS feed <{}>'.format(len(feed.entries), self.url)
)
for entry in feed.entries: for entry in feed.entries:
if not entry.published_parsed: if not entry.published_parsed:
@ -171,9 +206,10 @@ class RssUpdates(HttpRequest):
try: try:
entry_timestamp = datetime.datetime(*entry.published_parsed[:6]) entry_timestamp = datetime.datetime(*entry.published_parsed[:6])
if latest_update is None \ if latest_update is None or entry_timestamp > latest_update:
or entry_timestamp > latest_update: self.logger.info(
self.logger.info('Processed new item from RSS feed <{}>'.format(self.url)) 'Processed new item from RSS feed <{}>'.format(self.url)
)
entry.summary = entry.summary if hasattr(entry, 'summary') else None entry.summary = entry.summary if hasattr(entry, 'summary') else None
if self.extract_content: if self.extract_content:
@ -188,9 +224,13 @@ class RssUpdates(HttpRequest):
<a href="{link}" target="_blank" style="{article_link_style}">{title}</a> <a href="{link}" target="_blank" style="{article_link_style}">{title}</a>
</h1> </h1>
<div class="_parsed-content" style="{article_content_style}">{content}</div>'''.format( <div class="_parsed-content" style="{article_content_style}">{content}</div>'''.format(
article_title_style=self.article_title_style, article_link_style=self.article_link_style, article_title_style=self.article_title_style,
article_content_style=self.article_content_style, link=entry.link, title=entry.title, article_link_style=self.article_link_style,
content=entry.content) article_content_style=self.article_content_style,
link=entry.link,
title=entry.title,
content=entry.content,
)
e = { e = {
'entry_id': entry.id, 'entry_id': entry.id,
@ -207,21 +247,32 @@ class RssUpdates(HttpRequest):
if self.max_entries and len(entries) > self.max_entries: if self.max_entries and len(entries) > self.max_entries:
break break
except Exception as e: except Exception as e:
self.logger.warning('Exception encountered while parsing RSS ' + self.logger.warning(
'RSS feed {}: {}'.format(entry.link, str(e))) 'Exception encountered while parsing RSS '
+ f'RSS feed {entry.link}: {e}'
)
self.logger.exception(e) self.logger.exception(e)
source_record.last_updated_at = parse_start_time source_record.last_updated_at = parse_start_time
digest_filename = None digest_filename = None
if entries: if entries:
self.logger.info('Parsed {} new entries from the RSS feed {}'.format( self.logger.info(
len(entries), self.title)) 'Parsed {} new entries from the RSS feed {}'.format(
len(entries), self.title
)
)
if self.digest_format: if self.digest_format:
digest_filename = os.path.join(self.workdir, 'cache', '{}_{}.{}'.format( digest_filename = os.path.join(
self.workdir,
'cache',
'{}_{}.{}'.format(
datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
self.title, self.digest_format)) self.title,
self.digest_format,
),
)
os.makedirs(os.path.dirname(digest_filename), exist_ok=True) os.makedirs(os.path.dirname(digest_filename), exist_ok=True)
@ -233,12 +284,15 @@ class RssUpdates(HttpRequest):
</head> </head>
<body style="{body_style}">{content}</body> <body style="{body_style}">{content}</body>
</html> </html>
'''.format(title=self.title, body_style=self.body_style, content=content) '''.format(
title=self.title, body_style=self.body_style, content=content
)
with open(digest_filename, 'w', encoding='utf-8') as f: with open(digest_filename, 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
elif self.digest_format == 'pdf': elif self.digest_format == 'pdf':
from weasyprint import HTML, CSS from weasyprint import HTML, CSS
try: try:
from weasyprint.fonts import FontConfiguration from weasyprint.fonts import FontConfiguration
except ImportError: except ImportError:
@ -246,37 +300,47 @@ class RssUpdates(HttpRequest):
body_style = 'body { ' + self.body_style + ' }' body_style = 'body { ' + self.body_style + ' }'
font_config = FontConfiguration() font_config = FontConfiguration()
css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'), css = [
CSS(string=body_style, font_config=font_config)] CSS('https://fonts.googleapis.com/css?family=Merriweather'),
CSS(string=body_style, font_config=font_config),
]
HTML(string=content).write_pdf(digest_filename, stylesheets=css) HTML(string=content).write_pdf(digest_filename, stylesheets=css)
else: else:
raise RuntimeError('Unsupported format: {}. Supported formats: ' + raise RuntimeError(
'html or pdf'.format(self.digest_format)) f'Unsupported format: {self.digest_format}. Supported formats: html, pdf'
)
digest_entry = FeedDigest(source_id=source_record.id, digest_entry = FeedDigest(
source_id=source_record.id,
format=self.digest_format, format=self.digest_format,
filename=digest_filename) filename=digest_filename,
)
session.add(digest_entry) session.add(digest_entry)
self.logger.info('{} digest ready: {}'.format(self.digest_format, digest_filename)) self.logger.info(
'{} digest ready: {}'.format(self.digest_format, digest_filename)
)
session.commit() session.commit()
self.logger.info('Parsing RSS feed {}: completed'.format(self.title)) self.logger.info('Parsing RSS feed {}: completed'.format(self.title))
return NewFeedEvent(request=dict(self), response=entries, return NewFeedEvent(
request=dict(self),
response=entries,
source_id=source_record.id, source_id=source_record.id,
source_title=source_record.title, source_title=source_record.title,
title=self.title, title=self.title,
digest_format=self.digest_format, digest_format=self.digest_format,
digest_filename=digest_filename) digest_filename=digest_filename,
)
class FeedSource(Base): class FeedSource(Base):
"""Models the FeedSource table, containing RSS sources to be parsed""" """Models the FeedSource table, containing RSS sources to be parsed"""
__tablename__ = 'FeedSource' __tablename__ = 'FeedSource'
__table_args__ = ({'sqlite_autoincrement': True}) __table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
title = Column(String) title = Column(String)
@ -288,7 +352,7 @@ class FeedEntry(Base):
"""Models the FeedEntry table, which contains RSS entries""" """Models the FeedEntry table, which contains RSS entries"""
__tablename__ = 'FeedEntry' __tablename__ = 'FeedEntry'
__table_args__ = ({'sqlite_autoincrement': True}) __table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
entry_id = Column(String) entry_id = Column(String)
@ -309,7 +373,7 @@ class FeedDigest(Base):
pdf = 2 pdf = 2
__tablename__ = 'FeedDigest' __tablename__ = 'FeedDigest'
__table_args__ = ({'sqlite_autoincrement': True}) __table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
source_id = Column(Integer, ForeignKey('FeedSource.id'), nullable=False) source_id = Column(Integer, ForeignKey('FeedSource.id'), nullable=False)
@ -317,4 +381,5 @@ class FeedDigest(Base):
filename = Column(String, nullable=False) filename = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

Binary file not shown.

View file

@ -0,0 +1,7 @@
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./Poppins.ttf) format('truetype');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.5adf1720.js"></script><script defer="defer" type="module" src="/static/js/app.12b17001.js"></script><link href="/static/css/chunk-vendors.5cf89a0c.css" rel="stylesheet"><link href="/static/css/app.5028a669.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.6835b8d0.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.ab28664f.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.95bedba1.js"></script><script defer="defer" type="module" src="/static/js/app.0ecd5641.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.3b5b9cec.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.c27e0a41.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.602f8c67.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[1595],{1595:function(e,t,n){n.r(t),n.d(t,{default:function(){return f}});var s=n(6252),o=n(3577),i={class:"date-time"},a=["textContent"],r=["textContent"];function u(e,t,n,u,h,w){return(0,s.wg)(),(0,s.iD)("div",i,[w._showDate?((0,s.wg)(),(0,s.iD)("div",{key:0,class:"date",textContent:(0,o.zw)(e.formatDate(e.now))},null,8,a)):(0,s.kq)("",!0),w._showTime?((0,s.wg)(),(0,s.iD)("div",{key:1,class:"time",textContent:(0,o.zw)(e.formatTime(e.now,w._showSeconds))},null,8,r)):(0,s.kq)("",!0)])}var h=n(2628),w={name:"DateTime",mixins:[h.Z],props:{showDate:{required:!1,default:!0},showTime:{required:!1,default:!0},showSeconds:{required:!1,default:!0}},computed:{_showTime:function(){return this.parseBoolean(this.showTime)},_showDate:function(){return this.parseBoolean(this.showDate)},_showSeconds:function(){return this.parseBoolean(this.showSeconds)}},data:function(){return{now:new Date}},methods:{refreshTime:function(){this.now=new Date}},mounted:function(){this.refreshTime(),setInterval(this.refreshTime,1e3)}},c=n(3744);const d=(0,c.Z)(w,[["render",u],["__scopeId","data-v-ca42eb9c"]]);var f=d}}]); "use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[1595],{1595:function(e,t,n){n.r(t),n.d(t,{default:function(){return f}});var s=n(6252),o=n(3577),i={class:"date-time"},a=["textContent"],r=["textContent"];function u(e,t,n,u,h,w){return(0,s.wg)(),(0,s.iD)("div",i,[w._showDate?((0,s.wg)(),(0,s.iD)("div",{key:0,class:"date",textContent:(0,o.zw)(e.formatDate(e.now))},null,8,a)):(0,s.kq)("",!0),w._showTime?((0,s.wg)(),(0,s.iD)("div",{key:1,class:"time",textContent:(0,o.zw)(e.formatTime(e.now,w._showSeconds))},null,8,r)):(0,s.kq)("",!0)])}var h=n(6813),w={name:"DateTime",mixins:[h.Z],props:{showDate:{required:!1,default:!0},showTime:{required:!1,default:!0},showSeconds:{required:!1,default:!0}},computed:{_showTime:function(){return this.parseBoolean(this.showTime)},_showDate:function(){return this.parseBoolean(this.showDate)},_showSeconds:function(){return this.parseBoolean(this.showSeconds)}},data:function(){return{now:new Date}},methods:{refreshTime:function(){this.now=new Date}},mounted:function(){this.refreshTime(),setInterval(this.refreshTime,1e3)}},c=n(3744);const d=(0,c.Z)(w,[["render",u],["__scopeId","data-v-ca42eb9c"]]);var f=d}}]);
//# sourceMappingURL=1595-legacy.ddcdc704.js.map //# sourceMappingURL=1595-legacy.69aea4ae.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"static/js/1595-legacy.69aea4ae.js","mappings":"0LACOA,MAAM,a,8EAAX,QAGM,MAHN,EAGM,CAF6C,EAAAC,YAAA,WAAjD,QAA8D,O,MAAzDD,MAAM,O,aAAO,QAAwB,EAAN,WAAC,EAAAE,OAArC,2BAC+D,EAAAC,YAAA,WAA/D,QAA4E,O,MAAvEH,MAAM,O,aAAO,QAAsC,EAApB,WAAC,EAAAE,IAAK,EAAAE,gBAA1C,4B,eAQJ,GACEC,KAAM,WACNC,OAAQ,CAACC,EAAA,GACTC,MAAO,CAELC,SAAU,CACRC,UAAU,EACVC,SAAS,GAIXC,SAAU,CACRF,UAAU,EACVC,SAAS,GAIXE,YAAa,CACXH,UAAU,EACVC,SAAS,IAIbG,SAAU,CACRX,UADQ,WAEN,OAAOY,KAAKC,aAAaD,KAAKH,SAC/B,EAEDX,UALQ,WAMN,OAAOc,KAAKC,aAAaD,KAAKN,SAC/B,EAEDL,aATQ,WAUN,OAAOW,KAAKC,aAAaD,KAAKF,YAC/B,GAGHI,KAAM,WACJ,MAAO,CACLf,IAAK,IAAIgB,KAEZ,EAEDC,QAAS,CACPC,YADO,WAELL,KAAKb,IAAM,IAAIgB,IAChB,GAGHG,QAAS,WACPN,KAAKK,cACLE,YAAYP,KAAKK,YAAa,IAC/B,G,UCxDH,MAAMG,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/widgets/DateTime/Index.vue","webpack://platypush/./src/components/widgets/DateTime/Index.vue?dfd6"],"sourcesContent":["<template>\n <div class=\"date-time\">\n <div class=\"date\" v-text=\"formatDate(now)\" v-if=\"_showDate\" />\n <div class=\"time\" v-text=\"formatTime(now, _showSeconds)\" v-if=\"_showTime\" />\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\n\n// Widget to show date and time\nexport default {\n name: 'DateTime',\n mixins: [Utils],\n props: {\n // If false then don't display the date.\n showDate: {\n required: false,\n default: true,\n },\n\n // If false then don't display the time.\n showTime: {\n required: false,\n default: true,\n },\n\n // If false then don't display the seconds.\n showSeconds: {\n required: false,\n default: true,\n },\n },\n\n computed: {\n _showTime() {\n return this.parseBoolean(this.showTime)\n },\n\n _showDate() {\n return this.parseBoolean(this.showDate)\n },\n\n _showSeconds() {\n return this.parseBoolean(this.showSeconds)\n },\n },\n\n data: function() {\n return {\n now: new Date(),\n };\n },\n\n methods: {\n refreshTime() {\n this.now = new Date()\n },\n },\n\n mounted: function() {\n this.refreshTime()\n setInterval(this.refreshTime, 1000)\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.date-time {\n .date {\n font-size: 1.3em;\n }\n\n .time {\n font-size: 2em;\n }\n}\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=ca42eb9c&scoped=true\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=ca42eb9c&lang=scss&scoped=true\"\n\nimport exportComponent from \"/home/blacklight/git_tree/platypush/platypush/backend/http/webapp/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-ca42eb9c\"]])\n\nexport default __exports__"],"names":["class","_showDate","now","_showTime","_showSeconds","name","mixins","Utils","props","showDate","required","default","showTime","showSeconds","computed","this","parseBoolean","data","Date","methods","refreshTime","mounted","setInterval","__exports__","render"],"sourceRoot":""}

View file

@ -1 +0,0 @@
{"version":3,"file":"static/js/1595-legacy.ddcdc704.js","mappings":"0LACOA,MAAM,a,8EAAX,QAGM,MAHN,EAGM,CAF6C,EAAAC,YAAA,WAAjD,QAA8D,O,MAAzDD,MAAM,O,aAAO,QAAwB,EAAN,WAAC,EAAAE,OAArC,2BAC+D,EAAAC,YAAA,WAA/D,QAA4E,O,MAAvEH,MAAM,O,aAAO,QAAsC,EAApB,WAAC,EAAAE,IAAK,EAAAE,gBAA1C,6B,cAQJ,GACEC,KAAM,WACNC,OAAQ,CAACC,EAAA,GACTC,MAAO,CAELC,SAAU,CACRC,UAAU,EACVC,SAAS,GAIXC,SAAU,CACRF,UAAU,EACVC,SAAS,GAIXE,YAAa,CACXH,UAAU,EACVC,SAAS,IAIbG,SAAU,CACRX,UADQ,WAEN,OAAOY,KAAKC,aAAaD,KAAKH,WAGhCX,UALQ,WAMN,OAAOc,KAAKC,aAAaD,KAAKN,WAGhCL,aATQ,WAUN,OAAOW,KAAKC,aAAaD,KAAKF,eAIlCI,KAAM,WACJ,MAAO,CACLf,IAAK,IAAIgB,OAIbC,QAAS,CACPC,YADO,WAELL,KAAKb,IAAM,IAAIgB,OAInBG,QAAS,WACPN,KAAKK,cACLE,YAAYP,KAAKK,YAAa,O,UCvDlC,MAAMG,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF","sources":["webpack://platypush/./src/components/widgets/DateTime/Index.vue","webpack://platypush/./src/components/widgets/DateTime/Index.vue?dfd6"],"sourcesContent":["<template>\n <div class=\"date-time\">\n <div class=\"date\" v-text=\"formatDate(now)\" v-if=\"_showDate\" />\n <div class=\"time\" v-text=\"formatTime(now, _showSeconds)\" v-if=\"_showTime\" />\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\n\n// Widget to show date and time\nexport default {\n name: 'DateTime',\n mixins: [Utils],\n props: {\n // If false then don't display the date.\n showDate: {\n required: false,\n default: true,\n },\n\n // If false then don't display the time.\n showTime: {\n required: false,\n default: true,\n },\n\n // If false then don't display the seconds.\n showSeconds: {\n required: false,\n default: true,\n },\n },\n\n computed: {\n _showTime() {\n return this.parseBoolean(this.showTime)\n },\n\n _showDate() {\n return this.parseBoolean(this.showDate)\n },\n\n _showSeconds() {\n return this.parseBoolean(this.showSeconds)\n },\n },\n\n data: function() {\n return {\n now: new Date(),\n };\n },\n\n methods: {\n refreshTime() {\n this.now = new Date()\n },\n },\n\n mounted: function() {\n this.refreshTime()\n setInterval(this.refreshTime, 1000)\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.date-time {\n .date {\n font-size: 1.3em;\n }\n\n .time {\n font-size: 2em;\n }\n}\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=ca42eb9c&scoped=true\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=ca42eb9c&lang=scss&scoped=true\"\n\nimport exportComponent from \"/home/blacklight/git_tree/platypush/platypush/backend/http/webapp/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-ca42eb9c\"]])\n\nexport default __exports__"],"names":["class","_showDate","now","_showTime","_showSeconds","name","mixins","Utils","props","showDate","required","default","showTime","showSeconds","computed","this","parseBoolean","data","Date","methods","refreshTime","mounted","setInterval","__exports__","render"],"sourceRoot":""}

Some files were not shown because too many files have changed in this diff Show more