Compare commits

...

3 commits

Author SHA1 Message Date
1c5956c38b
Fixed some docstring warnings.
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-01-18 00:26:22 +01:00
85db77bb7b
[#298] Merged nextcloud backend and plugin.
Closes: #298
2024-01-18 00:26:22 +01:00
dd5bc7639b
Added disable_monitor flag to RunnablePlugin.
This is useful when users want to use a runnable plugin in a stateless
way. In some cases (for example systems with high latency or limited
quotas for API calls) the user may want to leverage the actions of a
plugin, but without running monitoring/polling logic nor generating
events.
2024-01-18 00:26:22 +01:00
10 changed files with 669 additions and 390 deletions

View file

@ -20,7 +20,6 @@ Backends
platypush/backend/music.mopidy.rst
platypush/backend/music.mpd.rst
platypush/backend/music.spotify.rst
platypush/backend/nextcloud.rst
platypush/backend/nfc.rst
platypush/backend/nodered.rst
platypush/backend/redis.rst

View file

@ -1,5 +0,0 @@
``nextcloud``
===============================
.. automodule:: platypush.backend.nextcloud
:members:

View file

@ -1,170 +0,0 @@
from typing import Optional
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.nextcloud import NextCloudActivityEvent
from platypush.plugins.nextcloud import NextcloudPlugin
from platypush.plugins.variable import VariablePlugin
class NextcloudBackend(Backend):
"""
This backend triggers events when new activities occur on a NextCloud instance.
The field ``activity_type`` in the triggered :class:`platypush.message.event.nextcloud.NextCloudActivityEvent`
events identifies the activity type (e.g. ``file_created``, ``file_deleted``,
``file_changed``). Example in the case of the creation of new files:
.. code-block:: json
{
"activity_id": 387,
"app": "files",
"activity_type": "file_created",
"user": "your-user",
"subject": "You created InstantUpload/Camera/IMG_0100.jpg",
"subject_rich": [
"You created {file3}, {file2} and {file1}",
{
"file1": {
"type": "file",
"id": "41994",
"name": "IMG_0100.jpg",
"path": "InstantUpload/Camera/IMG_0100.jpg",
"link": "https://your-domain/nextcloud/index.php/f/41994"
},
"file2": {
"type": "file",
"id": "42005",
"name": "IMG_0101.jpg",
"path": "InstantUpload/Camera/IMG_0102.jpg",
"link": "https://your-domain/nextcloud/index.php/f/42005"
},
"file3": {
"type": "file",
"id": "42014",
"name": "IMG_0102.jpg",
"path": "InstantUpload/Camera/IMG_0102.jpg",
"link": "https://your-domain/nextcloud/index.php/f/42014"
}
}
],
"message": "",
"message_rich": [
"",
[]
],
"object_type": "files",
"object_id": 41994,
"object_name": "/InstantUpload/Camera/IMG_0102.jpg",
"objects": {
"42014": "/InstantUpload/Camera/IMG_0100.jpg",
"42005": "/InstantUpload/Camera/IMG_0101.jpg",
"41994": "/InstantUpload/Camera/IMG_0102.jpg"
},
"link": "https://your-domain/nextcloud/index.php/apps/files/?dir=/InstantUpload/Camera",
"icon": "https://your-domain/nextcloud/apps/files/img/add-color.svg",
"datetime": "2020-09-07T17:04:29+00:00"
}
"""
_LAST_ACTIVITY_VARNAME = '_NEXTCLOUD_LAST_ACTIVITY_ID'
def __init__(
self,
url: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
object_type: Optional[str] = None,
object_id: Optional[int] = None,
poll_seconds: Optional[float] = 60.0,
**kwargs
):
"""
:param url: NextCloud instance URL (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`).
:param username: NextCloud username (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`).
:param password: NextCloud password (default: same as the :class:`platypush.plugins.nextcloud.NextCloudPlugin`).
:param object_type: If set, only filter events on this type of object.
:param object_id: If set, only filter events on this object ID.
:param poll_seconds: How often the backend should poll the instance (default: one minute).
"""
super().__init__(**kwargs)
self.url: Optional[str] = None
self.username: Optional[str] = None
self.password: Optional[str] = None
self.object_type = object_type
self.object_id = object_id
self.poll_seconds = poll_seconds
self._last_seen_id = None
try:
plugin: Optional[NextcloudPlugin] = get_plugin('nextcloud')
if plugin:
self.url = plugin.conf.url
self.username = plugin.conf.username
self.password = plugin.conf.password
except Exception as e:
self.logger.info('NextCloud plugin not configured: {}'.format(str(e)))
self.url = url if url else self.url
self.username = username if username else self.username
self.password = password if password else self.password
assert (
self.url and self.username and self.password
), 'No configuration provided neither for the NextCloud plugin nor the backend'
@property
def last_seen_id(self) -> Optional[int]:
if self._last_seen_id is None:
variables: VariablePlugin = get_plugin('variable')
last_seen_id = variables.get(self._LAST_ACTIVITY_VARNAME).output.get(
self._LAST_ACTIVITY_VARNAME
)
self._last_seen_id = last_seen_id
return self._last_seen_id
@last_seen_id.setter
def last_seen_id(self, value: Optional[int]):
variables: VariablePlugin = get_plugin('variable')
variables.set(**{self._LAST_ACTIVITY_VARNAME: value})
self._last_seen_id = value
@staticmethod
def _activity_to_event(activity: dict) -> NextCloudActivityEvent:
return NextCloudActivityEvent(activity_type=activity.pop('type'), **activity)
def loop(self):
last_seen_id = int(self.last_seen_id)
new_last_seen_id = int(last_seen_id)
plugin: NextcloudPlugin = get_plugin('nextcloud')
# noinspection PyUnresolvedReferences
activities = plugin.get_activities(
sort='desc',
url=self.url,
username=self.username,
password=self.password,
object_type=self.object_type,
object_id=self.object_id,
).output
events = []
for activity in activities:
if last_seen_id and activity['activity_id'] <= last_seen_id:
break
events.append(self._activity_to_event(activity))
if not new_last_seen_id or activity['activity_id'] > new_last_seen_id:
new_last_seen_id = int(activity['activity_id'])
for evt in events[::-1]:
self.bus.post(evt)
if new_last_seen_id and last_seen_id != new_last_seen_id:
self.last_seen_id = new_last_seen_id
# vim:sw=4:ts=4:et:

View file

@ -1,10 +0,0 @@
manifest:
events:
platypush.message.event.nextcloud.NextCloudActivityEvent: 'when new activity occurs
on the instance.The field ``activity_type`` identifies the activity type (e.g.
``file_created``, ``file_deleted``,``file_changed``). Example in the case of
the creation of new files:'
install:
pip: []
package: platypush.backend.nextcloud
type: backend

View file

@ -1,9 +1,103 @@
from datetime import datetime as dt
from typing import Optional
from platypush.message.event import Event
class NextCloudActivityEvent(Event):
def __init__(self, activity_id: int, activity_type: str, *args, **kwargs):
super().__init__(*args, activity_id=activity_id, activity_type=activity_type, **kwargs)
"""
Event triggered when a new activity is detected on a NextCloud instance.
"""
def __init__(
self,
*args,
activity_id: int,
activity_type: str,
object_id: int,
object_type: str,
object_name: str,
app: str,
user: str,
subject: str,
message: str,
subject_rich: Optional[list] = None,
message_rich: Optional[list] = None,
objects: Optional[dict] = None,
link: Optional[str] = None,
icon: Optional[str] = None,
datetime: Optional[dt] = None,
**kwargs,
):
"""
:param activity_id: Activity ID.
:param activity_type: Activity type - can be ``file_created``,
``file_deleted``, ``file_changed``, ``file_restored``,
``file_shared``, ``file_unshared``, ``file_downloaded``, etc.
:param object_id: Object ID.
:param object_type: Object type - can be files, comment, tag, share,
etc.
:param object_name: Object name. In the case of files, it's the file
path relative to the user's root directory.
:param app: Application that generated the activity.
:param user: User that generated the activity.
:param subject: Activity subject, in plain text. For example, *You
created hd/test1.txt and hd/test2.txt*.
:param message: Activity message, in plain text.
:param subject_rich: Activity subject, in rich/structured format.
Example:
.. code-block:: json
[
"You created {file2} and {file1}",
{
"file1": {
"type": "file",
"id": "1234",
"name": "test1.txt",
"path": "hd/text1.txt",
"link": "https://cloud.example.com/index.php/f/1234"
},
"file2": {
"type": "file",
"id": "1235",
"name": "test2.txt",
"path": "hd/text2.txt",
"link": "https://cloud.example.com/index.php/f/1235"
}
}
]
:param message_rich: Activity message, in rich/structured format.
:param objects: Additional objects associated to the activity, in the
format ``{object_id: object}``. For example, if the activity
involves files, the ``objects`` dictionary will contain the mapping
of the involved files in the format ``{file_id: path}``.
:param link: Link to the main object of this activity. Example:
``https://cloud.example.com/index.php/files/apps/files/?dir=/hd&fileid=1234``
:param icon: URL of the icon associated to the activity.
:param datetime: Activity timestamp.
"""
super().__init__(
*args,
activity_id=activity_id,
activity_type=activity_type,
object_id=object_id,
object_type=object_type,
object_name=object_name,
app=app,
user=user,
subject=subject,
subject_rich=subject_rich,
message=message,
message_rich=message_rich,
objects=objects or {},
link=link,
icon=icon,
datetime=datetime,
**kwargs,
)
# vim:sw=4:ts=4:et:

View file

@ -138,6 +138,7 @@ class RunnablePlugin(Plugin):
self,
poll_interval: Optional[float] = 15,
stop_timeout: Optional[float] = PLUGIN_STOP_TIMEOUT,
disable_monitor: bool = False,
**kwargs,
):
"""
@ -147,10 +148,15 @@ class RunnablePlugin(Plugin):
deprecated.
:param stop_timeout: How long we should wait for any running
threads/processes to stop before exiting (default: 5 seconds).
:param disable_monitor: If set to True then the plugin will not monitor
for new events. This is useful if you want to run a plugin in
stateless mode and only leverage its actions, without triggering any
events. Defaults to False.
"""
super().__init__(**kwargs)
self.poll_interval = poll_interval
self.bus: Optional[Bus] = None
self.disable_monitor = disable_monitor
self._should_stop = threading.Event()
self._stop_timeout = stop_timeout
self._thread: Optional[threading.Thread] = None
@ -178,6 +184,10 @@ class RunnablePlugin(Plugin):
"""
Wait until a stop event is received.
"""
if self.disable_monitor:
# Wait indefinitely if the monitor is disabled
return self._should_stop.wait(timeout=None)
return self._should_stop.wait(timeout=timeout)
def start(self):
@ -217,6 +227,9 @@ class RunnablePlugin(Plugin):
"""
Implementation of the runner thread.
"""
if self.disable_monitor:
return
self.logger.info('Starting %s', self.__class__.__name__)
while not self.should_stop():

View file

@ -396,10 +396,10 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
:return: The status of a Chromecast (if ``chromecast`` is specified) or
all the discovered/available Chromecasts. Format:
.. code-block:: python
.. code-block:: javascript
{
"type": "cast", # Can be "cast" or "audio"
"type": "cast", // Can be "cast" or "audio"
"name": "Living Room TV",
"manufacturer": "Google Inc.",
"model_name": "Chromecast",
@ -481,7 +481,7 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
"title": "Some media",
"track": null
}
}
}
"""
return self._status(chromecast=chromecast)

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,8 @@
manifest:
events: {}
events:
platypush.message.event.nextcloud.NextCloudActivityEvent: |
When new activity occurs on the instance - e.g. a file or bookmark is
created, a comment is added, a message is received etc.
install:
pip:
- nextcloud-api-wrapper

View file

@ -30,8 +30,8 @@ class TodoistPlugin(Plugin):
def __init__(self, api_token: str, **kwargs):
"""
:param api_token: Todoist API token. You can get it `here
<https://todoist.com/prefs/integrations>`_.
:param api_token: Todoist API token.
You can get it `here <https://todoist.com/prefs/integrations>`_.
"""
super().__init__(**kwargs)