forked from platypush/platypush
Removed Todoist backend and migrated responses to schemas.
The Todoist Websocket interface is no longer working properly, and I'm sick of reverse engineering it. Closes: #305
This commit is contained in:
parent
c2f53b7771
commit
1a777c6276
12 changed files with 368 additions and 603 deletions
|
@ -38,7 +38,6 @@ Backends
|
||||||
platypush/backend/stt.picovoice.hotword.rst
|
platypush/backend/stt.picovoice.hotword.rst
|
||||||
platypush/backend/stt.picovoice.speech.rst
|
platypush/backend/stt.picovoice.speech.rst
|
||||||
platypush/backend/tcp.rst
|
platypush/backend/tcp.rst
|
||||||
platypush/backend/todoist.rst
|
|
||||||
platypush/backend/weather.buienradar.rst
|
platypush/backend/weather.buienradar.rst
|
||||||
platypush/backend/weather.darksky.rst
|
platypush/backend/weather.darksky.rst
|
||||||
platypush/backend/weather.openweathermap.rst
|
platypush/backend/weather.openweathermap.rst
|
||||||
|
|
|
@ -67,7 +67,6 @@ Events
|
||||||
platypush/events/stt.rst
|
platypush/events/stt.rst
|
||||||
platypush/events/sun.rst
|
platypush/events/sun.rst
|
||||||
platypush/events/tensorflow.rst
|
platypush/events/tensorflow.rst
|
||||||
platypush/events/todoist.rst
|
|
||||||
platypush/events/torrent.rst
|
platypush/events/torrent.rst
|
||||||
platypush/events/trello.rst
|
platypush/events/trello.rst
|
||||||
platypush/events/video.rst
|
platypush/events/video.rst
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``todoist``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.todoist
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``todoist``
|
|
||||||
===================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.todoist
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``todoist``
|
|
||||||
======================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.todoist
|
|
||||||
:members:
|
|
|
@ -17,6 +17,5 @@ Responses
|
||||||
platypush/responses/ssh.rst
|
platypush/responses/ssh.rst
|
||||||
platypush/responses/stt.rst
|
platypush/responses/stt.rst
|
||||||
platypush/responses/tensorflow.rst
|
platypush/responses/tensorflow.rst
|
||||||
platypush/responses/todoist.rst
|
|
||||||
platypush/responses/translate.rst
|
platypush/responses/translate.rst
|
||||||
platypush/responses/weather.buienradar.rst
|
platypush/responses/weather.buienradar.rst
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.context import get_plugin
|
|
||||||
from platypush.message.event.todoist import (
|
|
||||||
NewItemEvent,
|
|
||||||
RemovedItemEvent,
|
|
||||||
ModifiedItemEvent,
|
|
||||||
CheckedItemEvent,
|
|
||||||
ItemContentChangeEvent,
|
|
||||||
TodoistSyncRequiredEvent,
|
|
||||||
)
|
|
||||||
|
|
||||||
from platypush.plugins.todoist import TodoistPlugin
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend listens for events on a Todoist account.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, api_token: str = None, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._plugin: TodoistPlugin = get_plugin('todoist')
|
|
||||||
|
|
||||||
if not api_token:
|
|
||||||
assert (
|
|
||||||
self._plugin and self._plugin.api_token
|
|
||||||
), 'No api_token specified either on Todoist backend or plugin'
|
|
||||||
self.api_token = self._plugin.api_token
|
|
||||||
else:
|
|
||||||
self.api_token = api_token
|
|
||||||
|
|
||||||
self.url = self._plugin.get_user().output['websocket_url']
|
|
||||||
self._ws = None
|
|
||||||
self._connected = False
|
|
||||||
self._todoist_initialized = False
|
|
||||||
|
|
||||||
self._items = {}
|
|
||||||
self._event_handled = False
|
|
||||||
|
|
||||||
def _on_msg(self):
|
|
||||||
def hndl(*args):
|
|
||||||
msg = args[1] if len(args) > 1 else args[0]
|
|
||||||
msg = json.loads(msg)
|
|
||||||
if msg.get('type') == 'sync_needed':
|
|
||||||
self._refresh_all()
|
|
||||||
|
|
||||||
return hndl
|
|
||||||
|
|
||||||
def _retry_hndl(self):
|
|
||||||
self._ws = None
|
|
||||||
self.logger.warning('Todoist websocket connection closed')
|
|
||||||
self._connected = False
|
|
||||||
|
|
||||||
while not self._connected:
|
|
||||||
self._connect()
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
def _on_error(self):
|
|
||||||
def hndl(*args):
|
|
||||||
error = args[1] if len(args) > 1 else args[0]
|
|
||||||
self.logger.warning('Todoist websocket error: {}'.format(error))
|
|
||||||
self._retry_hndl()
|
|
||||||
|
|
||||||
return hndl
|
|
||||||
|
|
||||||
def _on_close(self):
|
|
||||||
def hndl(*_):
|
|
||||||
self.logger.info('Todoist websocket closed')
|
|
||||||
self._retry_hndl()
|
|
||||||
|
|
||||||
return hndl
|
|
||||||
|
|
||||||
def _on_open(self):
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
def hndl(ws):
|
|
||||||
self._connected = True
|
|
||||||
self.logger.info('Todoist websocket connected')
|
|
||||||
|
|
||||||
if not self._todoist_initialized:
|
|
||||||
self._refresh_all()
|
|
||||||
self._todoist_initialized = True
|
|
||||||
|
|
||||||
return hndl
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
import websocket
|
|
||||||
|
|
||||||
if not self._ws:
|
|
||||||
self._ws = websocket.WebSocketApp(
|
|
||||||
self.url,
|
|
||||||
on_message=self._on_msg(),
|
|
||||||
on_error=self._on_error(),
|
|
||||||
on_close=self._on_close(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _refresh_items(self):
|
|
||||||
new_items = {i['id']: i for i in self._plugin.get_items().output}
|
|
||||||
|
|
||||||
if self._todoist_initialized:
|
|
||||||
for id, item in new_items.items():
|
|
||||||
if id not in self._items.keys():
|
|
||||||
self._event_handled = True
|
|
||||||
self.bus.post(NewItemEvent(item))
|
|
||||||
|
|
||||||
for id, item in self._items.items():
|
|
||||||
if id not in new_items.keys():
|
|
||||||
self._event_handled = True
|
|
||||||
self.bus.post(RemovedItemEvent(item))
|
|
||||||
elif new_items[id] != item:
|
|
||||||
if new_items[id]['checked'] != item['checked']:
|
|
||||||
self._event_handled = True
|
|
||||||
self.bus.post(CheckedItemEvent(new_items[id]))
|
|
||||||
elif new_items[id]['is_deleted'] != item['is_deleted']:
|
|
||||||
self._event_handled = True
|
|
||||||
self.bus.post(RemovedItemEvent(new_items[id]))
|
|
||||||
elif new_items[id]['content'] != item['content']:
|
|
||||||
self._event_handled = True
|
|
||||||
self.bus.post(ItemContentChangeEvent(new_items[id]))
|
|
||||||
else:
|
|
||||||
self._event_handled = True
|
|
||||||
self.bus.post(ModifiedItemEvent(new_items[id]))
|
|
||||||
|
|
||||||
self._items = new_items
|
|
||||||
|
|
||||||
def _refresh_all(self):
|
|
||||||
self._event_handled = False
|
|
||||||
self._plugin.sync()
|
|
||||||
self._refresh_items()
|
|
||||||
|
|
||||||
if not self._event_handled:
|
|
||||||
self.bus.post(TodoistSyncRequiredEvent())
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
self.logger.info('Started Todoist backend')
|
|
||||||
|
|
||||||
self._connect()
|
|
||||||
self._ws.on_open = self._on_open()
|
|
||||||
self._ws.run_forever()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,16 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.todoist.CheckedItemEvent: when an item is checked.
|
|
||||||
platypush.message.event.todoist.ItemContentChangeEvent: when the content of an
|
|
||||||
item is changed.
|
|
||||||
platypush.message.event.todoist.ModifiedItemEvent: when an item is changed and
|
|
||||||
the changedoesn't fall into the categories above.
|
|
||||||
platypush.message.event.todoist.NewItemEvent: when a new item is created.
|
|
||||||
platypush.message.event.todoist.RemovedItemEvent: when an item is removed.
|
|
||||||
platypush.message.event.todoist.TodoistSyncRequiredEvent: when an update has occurred
|
|
||||||
that doesn'tfall into the categories above and a sync is required to get up-to-date.
|
|
||||||
install:
|
|
||||||
pip:
|
|
||||||
- todoist-python
|
|
||||||
package: platypush.backend.todoist
|
|
||||||
type: backend
|
|
|
@ -1,59 +0,0 @@
|
||||||
from platypush.message.event import Event
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistEvent(Event):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NewItemEvent(TodoistEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a new item is created.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, item, *args, **kwargs):
|
|
||||||
super().__init__(*args, item=item, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class RemovedItemEvent(TodoistEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a new item is removed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, item, *args, **kwargs):
|
|
||||||
super().__init__(*args, item=item, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ModifiedItemEvent(TodoistEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when an item is changed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, item, *args, **kwargs):
|
|
||||||
super().__init__(*args, item=item, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CheckedItemEvent(ModifiedItemEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when an item is checked.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, item, *args, **kwargs):
|
|
||||||
super().__init__(*args, item=item, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ItemContentChangeEvent(ModifiedItemEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when the content of an item changes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, item, *args, **kwargs):
|
|
||||||
super().__init__(*args, item=item, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistSyncRequiredEvent(TodoistEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when an event occurs that doesn't fall into the categories above.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,326 +0,0 @@
|
||||||
import todoist.models
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
|
|
||||||
from platypush.message import Mapping
|
|
||||||
from platypush.message.response import Response
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistResponse(Response):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistUserResponse(TodoistResponse):
|
|
||||||
def __init__(self,
|
|
||||||
auto_reminder: Optional[int] = None,
|
|
||||||
avatar_big: Optional[str] = None,
|
|
||||||
avatar_medium: Optional[str] = None,
|
|
||||||
avatar_s640: Optional[str] = None,
|
|
||||||
avatar_small: Optional[str] = None,
|
|
||||||
business_account_id: Optional[int] = None,
|
|
||||||
daily_goal: Optional[int] = None,
|
|
||||||
date_format: Optional[str] = None,
|
|
||||||
dateist_inline_disabled: Optional[bool] = None,
|
|
||||||
dateist_lang: Optional[str] = None,
|
|
||||||
days_off: Optional[List[int]] = None,
|
|
||||||
default_reminder: Optional[str] = None,
|
|
||||||
email: Optional[str] = None,
|
|
||||||
features: Optional[Dict[str, Any]] = None,
|
|
||||||
full_name: Optional[str] = None,
|
|
||||||
id: Optional[int] = None,
|
|
||||||
image_id: Optional[str] = None,
|
|
||||||
inbox_project: Optional[int] = None,
|
|
||||||
is_biz_admin: Optional[bool] = None,
|
|
||||||
is_premium: Optional[bool] = None,
|
|
||||||
join_date: Optional[datetime.datetime] = None,
|
|
||||||
karma: Optional[float] = None,
|
|
||||||
karma_trend: Optional[str] = None,
|
|
||||||
lang: Optional[str] = None,
|
|
||||||
legacy_inbox_project: Optional[int] = None,
|
|
||||||
mobile_host: Optional[str] = None,
|
|
||||||
mobile_number: Optional[str] = None,
|
|
||||||
next_week: Optional[int] = None,
|
|
||||||
premium_until: Optional[datetime.datetime] = None,
|
|
||||||
share_limit: Optional[int] = None,
|
|
||||||
sort_order: Optional[int] = None,
|
|
||||||
start_day: Optional[int] = None,
|
|
||||||
start_page: Optional[str] = None,
|
|
||||||
theme: Optional[int] = None,
|
|
||||||
time_format: Optional[int] = None,
|
|
||||||
token: Optional[str] = None,
|
|
||||||
tz_info: Optional[Dict[str, Any]] = None,
|
|
||||||
unique_prefix: Optional[int] = None,
|
|
||||||
websocket_url: Optional[str] = None,
|
|
||||||
weekly_goal: Optional[int] = None,
|
|
||||||
**kwargs):
|
|
||||||
response = {
|
|
||||||
'auto_reminder': auto_reminder,
|
|
||||||
'avatar_big': avatar_big,
|
|
||||||
'avatar_medium': avatar_medium,
|
|
||||||
'avatar_s640': avatar_s640,
|
|
||||||
'avatar_small': avatar_small,
|
|
||||||
'business_account_id': business_account_id,
|
|
||||||
'daily_goal': daily_goal,
|
|
||||||
'date_format': date_format,
|
|
||||||
'dateist_inline_disabled': dateist_inline_disabled,
|
|
||||||
'dateist_lang': dateist_lang,
|
|
||||||
'days_off': days_off,
|
|
||||||
'default_reminder': default_reminder,
|
|
||||||
'email': email,
|
|
||||||
'features': features,
|
|
||||||
'full_name': full_name,
|
|
||||||
'id': id,
|
|
||||||
'image_id': image_id,
|
|
||||||
'inbox_project': inbox_project,
|
|
||||||
'is_biz_admin': is_biz_admin,
|
|
||||||
'is_premium': is_premium,
|
|
||||||
'join_date': join_date,
|
|
||||||
'karma': karma,
|
|
||||||
'karma_trend': karma_trend,
|
|
||||||
'lang': lang,
|
|
||||||
'legacy_inbox_project': legacy_inbox_project,
|
|
||||||
'mobile_host': mobile_host,
|
|
||||||
'mobile_number': mobile_number,
|
|
||||||
'next_week': next_week,
|
|
||||||
'premium_until': premium_until,
|
|
||||||
'share_limit': share_limit,
|
|
||||||
'sort_order': sort_order,
|
|
||||||
'start_day': start_day,
|
|
||||||
'start_page': start_page,
|
|
||||||
'theme': theme,
|
|
||||||
'time_format': time_format,
|
|
||||||
'token': token,
|
|
||||||
'tz_info': tz_info,
|
|
||||||
'unique_prefix': unique_prefix,
|
|
||||||
'websocket_url': websocket_url,
|
|
||||||
'weekly_goal': weekly_goal,
|
|
||||||
}
|
|
||||||
|
|
||||||
super().__init__(output=response, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistProject(Mapping):
|
|
||||||
def __init__(self,
|
|
||||||
child_order: int,
|
|
||||||
collapsed: int,
|
|
||||||
color: int,
|
|
||||||
has_more_notes: bool,
|
|
||||||
id: int,
|
|
||||||
is_archived: bool,
|
|
||||||
is_deleted: bool,
|
|
||||||
is_favorite: bool,
|
|
||||||
name: str,
|
|
||||||
shared: bool,
|
|
||||||
inbox_project: Optional[bool] = None,
|
|
||||||
legacy_id: Optional[int] = None,
|
|
||||||
parent_id: Optional[int] = None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.child_order = child_order
|
|
||||||
self.collapsed = collapsed
|
|
||||||
self.color = color
|
|
||||||
self.has_more_notes = has_more_notes
|
|
||||||
self.id = id
|
|
||||||
self.inbox_project = inbox_project
|
|
||||||
self.is_archived = bool(is_archived)
|
|
||||||
self.is_deleted = bool(is_deleted)
|
|
||||||
self.is_favorite = bool(is_favorite)
|
|
||||||
self.name = name
|
|
||||||
self.shared = shared
|
|
||||||
self.legacy_id = legacy_id
|
|
||||||
self.parent_id = parent_id
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistProjectsResponse(TodoistResponse):
|
|
||||||
def __init__(self, projects: List[TodoistProject], **kwargs):
|
|
||||||
self.projects = [TodoistProject(**(p.data if isinstance(p, todoist.models.Project) else p)) for p in projects]
|
|
||||||
super().__init__(output=[p.__dict__ for p in self.projects], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistItem(Mapping):
|
|
||||||
def __init__(self,
|
|
||||||
content: str,
|
|
||||||
id: int,
|
|
||||||
checked: bool,
|
|
||||||
priority: int,
|
|
||||||
child_order: int,
|
|
||||||
collapsed: bool,
|
|
||||||
day_order: int,
|
|
||||||
date_added: datetime.datetime,
|
|
||||||
in_history: bool,
|
|
||||||
is_deleted: bool,
|
|
||||||
user_id: int,
|
|
||||||
has_more_notes: bool = False,
|
|
||||||
project_id: Optional[int] = None,
|
|
||||||
parent_id: Optional[int] = None,
|
|
||||||
responsible_uid: Optional[int] = None,
|
|
||||||
date_completed: Optional[datetime.datetime] = None,
|
|
||||||
assigned_by_uid: Optional[int] = None,
|
|
||||||
due: Optional[Dict[str, Any]] = None,
|
|
||||||
labels: Optional[List[str]] = None,
|
|
||||||
legacy_project_id: Optional[int] = None,
|
|
||||||
section_id: Optional[int] = None,
|
|
||||||
sync_id: Optional[int] = None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.content = content
|
|
||||||
self.id = id
|
|
||||||
self.checked = bool(checked)
|
|
||||||
self.priority = priority
|
|
||||||
self.child_order = child_order
|
|
||||||
self.collapsed = bool(collapsed)
|
|
||||||
self.day_order = day_order
|
|
||||||
self.date_added = date_added
|
|
||||||
self.has_more_notes = bool(has_more_notes)
|
|
||||||
self.in_history = bool(in_history)
|
|
||||||
self.is_deleted = bool(is_deleted)
|
|
||||||
self.user_id = user_id
|
|
||||||
self.project_id = project_id
|
|
||||||
self.parent_id = parent_id
|
|
||||||
self.responsible_uid = responsible_uid
|
|
||||||
self.date_completed = date_completed
|
|
||||||
self.assigned_by_uid = assigned_by_uid
|
|
||||||
self.due = due
|
|
||||||
self.labels = labels
|
|
||||||
self.legacy_project_id = legacy_project_id
|
|
||||||
self.section_id = section_id
|
|
||||||
self.sync_id = sync_id
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistItemsResponse(TodoistResponse):
|
|
||||||
def __init__(self, items: List[TodoistItem], **kwargs):
|
|
||||||
self.items = [TodoistItem(**(i.data if isinstance(i, todoist.models.Item) else i.__dict__)) for i in items]
|
|
||||||
super().__init__(output=[i.__dict__ for i in self.items], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistFilter(Mapping):
|
|
||||||
def __init__(self,
|
|
||||||
color: int,
|
|
||||||
id: int,
|
|
||||||
is_deleted: bool,
|
|
||||||
is_favorite: bool,
|
|
||||||
item_order: int,
|
|
||||||
name: str,
|
|
||||||
query: str,
|
|
||||||
legacy_id: Optional[int] = None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.color = color
|
|
||||||
self.id = id
|
|
||||||
self.is_deleted = is_deleted
|
|
||||||
self.is_favorite = is_favorite
|
|
||||||
self.item_order = item_order
|
|
||||||
self.name = name
|
|
||||||
self.query = query
|
|
||||||
self.legacy_id = legacy_id
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistFiltersResponse(TodoistResponse):
|
|
||||||
def __init__(self, filters: List[TodoistFilter], **kwargs):
|
|
||||||
self.filters = [TodoistFilter(**(f.data if isinstance(f, todoist.models.Filter) else f.__dict__))
|
|
||||||
for f in filters]
|
|
||||||
|
|
||||||
super().__init__(output=[f.__dict__ for f in self.filters], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistLiveNotification(Mapping):
|
|
||||||
def __init__(self,
|
|
||||||
id: int,
|
|
||||||
is_deleted: bool,
|
|
||||||
created: str,
|
|
||||||
is_unread: bool,
|
|
||||||
notification_key: str,
|
|
||||||
notification_type: str,
|
|
||||||
completed_last_month: Optional[int] = None,
|
|
||||||
karma_level: Optional[int] = None,
|
|
||||||
promo_img: Optional[str] = None,
|
|
||||||
completed_tasks: Optional[int] = None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.id = id
|
|
||||||
self.is_deleted = bool(is_deleted)
|
|
||||||
self.completed_last_month = completed_last_month
|
|
||||||
self.completed_tasks = completed_tasks
|
|
||||||
self.created = created
|
|
||||||
self.is_unread = bool(is_unread)
|
|
||||||
self.karma_level = karma_level
|
|
||||||
self.notification_key = notification_key
|
|
||||||
self.notification_type = notification_type
|
|
||||||
self.promo_img = promo_img
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistLiveNotificationsResponse(TodoistResponse):
|
|
||||||
def __init__(self, notifications: List[TodoistLiveNotification], **kwargs):
|
|
||||||
self.notifications = [TodoistLiveNotification(**(n.data if isinstance(n, todoist.models.LiveNotification)
|
|
||||||
else n.__dict__)) for n in notifications]
|
|
||||||
|
|
||||||
super().__init__(output=[n.__dict__ for n in self.notifications], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistCollaborator(Mapping):
|
|
||||||
def __init__(self, data: Dict[str, Any], *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for k, v in data.items():
|
|
||||||
self.__setattr__(k, v)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistCollaboratorsResponse(TodoistResponse):
|
|
||||||
def __init__(self, collaborators: List[TodoistCollaborator], **kwargs):
|
|
||||||
self.collaborators = [TodoistCollaborator(c.data if isinstance(c, todoist.models.Collaborator) else c.__dict__)
|
|
||||||
for c in collaborators]
|
|
||||||
|
|
||||||
super().__init__(output=[c.__dict__ for c in self.collaborators], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistNote(Mapping):
|
|
||||||
def __init__(self, data: Dict[str, Any], *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for k, v in data.items():
|
|
||||||
self.__setattr__(k, v)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistNotesResponse(TodoistResponse):
|
|
||||||
def __init__(self, notes: List[TodoistCollaborator], **kwargs):
|
|
||||||
self.notes = [TodoistCollaborator(n.data if isinstance(n, todoist.models.Note) else n.__dict__)
|
|
||||||
for n in notes]
|
|
||||||
|
|
||||||
super().__init__(output=[n.__dict__ for n in self.notes], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistProjectNote(Mapping):
|
|
||||||
def __init__(self, data: Dict[str, Any], *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for k, v in data.items():
|
|
||||||
self.__setattr__(k, v)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistProjectNotesResponse(TodoistResponse):
|
|
||||||
def __init__(self, notes: List[TodoistCollaborator], **kwargs):
|
|
||||||
self.notes = [TodoistCollaborator(n.data if isinstance(n, todoist.models.ProjectNote) else n.__dict__)
|
|
||||||
for n in notes]
|
|
||||||
|
|
||||||
super().__init__(output=[n.__dict__ for n in self.notes], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistReminder(Mapping):
|
|
||||||
def __init__(self, data: Dict[str, Any], *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for k, v in data.items():
|
|
||||||
self.__setattr__(k, v)
|
|
||||||
|
|
||||||
|
|
||||||
class TodoistRemindersResponse(TodoistResponse):
|
|
||||||
def __init__(self, reminders: List[TodoistReminder], **kwargs):
|
|
||||||
self.reminders = [TodoistReminder(n.data if isinstance(n, todoist.models.Reminder) else n.__dict__)
|
|
||||||
for n in reminders]
|
|
||||||
|
|
||||||
super().__init__(output=[r.__dict__ for r in self.reminders], **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,20 +1,20 @@
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import todoist
|
import todoist
|
||||||
import todoist.managers.items
|
import todoist.managers.items # type: ignore
|
||||||
|
import websocket
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
from platypush.message.response.todoist import (
|
from platypush.schemas.todoist import (
|
||||||
TodoistUserResponse,
|
TodoistCollaboratorSchema,
|
||||||
TodoistProjectsResponse,
|
TodoistFilterSchema,
|
||||||
TodoistItemsResponse,
|
TodoistItemSchema,
|
||||||
TodoistFiltersResponse,
|
TodoistLiveNotificationSchema,
|
||||||
TodoistLiveNotificationsResponse,
|
TodoistNoteSchema,
|
||||||
TodoistCollaboratorsResponse,
|
TodoistProjectNoteSchema,
|
||||||
TodoistNotesResponse,
|
TodoistProjectSchema,
|
||||||
TodoistProjectNotesResponse,
|
TodoistUserSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,14 +22,16 @@ class TodoistPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Todoist integration.
|
Todoist integration.
|
||||||
|
|
||||||
You'll also need a Todoist token. You can get it `here <https://todoist.com/prefs/integrations>`.
|
You'll also need a Todoist token. You can get it `here
|
||||||
|
<https://todoist.com/prefs/integrations>`_.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_sync_timeout = 60.0
|
_sync_timeout = 60.0
|
||||||
|
|
||||||
def __init__(self, api_token: str, **kwargs):
|
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)
|
super().__init__(**kwargs)
|
||||||
|
@ -37,9 +39,17 @@ class TodoistPlugin(Plugin):
|
||||||
self._api = None
|
self._api = None
|
||||||
self._last_sync_time = None
|
self._last_sync_time = None
|
||||||
|
|
||||||
def _get_api(self) -> todoist.TodoistAPI:
|
self._ws_url: Optional[str] = None
|
||||||
|
self._ws: Optional[websocket.WebSocketApp] = None
|
||||||
|
self._connected = False
|
||||||
|
self._todoist_initialized = False
|
||||||
|
|
||||||
|
self._items = {}
|
||||||
|
self._event_handled = False
|
||||||
|
|
||||||
|
def _get_api(self) -> todoist.TodoistAPI: # type: ignore
|
||||||
if not self._api:
|
if not self._api:
|
||||||
self._api = todoist.TodoistAPI(self.api_token)
|
self._api = todoist.TodoistAPI(self.api_token) # type: ignore
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not self._last_sync_time
|
not self._last_sync_time
|
||||||
|
@ -50,68 +60,82 @@ class TodoistPlugin(Plugin):
|
||||||
return self._api
|
return self._api
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_user(self) -> TodoistUserResponse:
|
def get_user(self):
|
||||||
"""
|
"""
|
||||||
Get logged user info.
|
Get logged user info.
|
||||||
|
|
||||||
|
:return: .. schema:: todoist.TodoistUserSchema
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistUserSchema().dump(self._get_api().state['user'])
|
||||||
return TodoistUserResponse(**api.state['user'])
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_projects(self) -> TodoistProjectsResponse:
|
def get_projects(self):
|
||||||
"""
|
"""
|
||||||
Get list of Todoist projects.
|
Get list of Todoist projects.
|
||||||
|
|
||||||
|
:return: .. schema:: todoist.TodoistProjectSchema(many=True)
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistProjectSchema().dump(self._get_api().state['projects'], many=True)
|
||||||
return TodoistProjectsResponse(api.state['projects'])
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_items(self) -> TodoistItemsResponse:
|
def get_items(self):
|
||||||
"""
|
"""
|
||||||
Get list of Todoist projects.
|
Get list of Todoist projects.
|
||||||
|
|
||||||
|
:return .. schema:: todoist.TodoistItemSchema(many=True)
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistItemSchema().dump(self._get_api().state['items'], many=True)
|
||||||
return TodoistItemsResponse(api.state['items'])
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_filters(self) -> TodoistFiltersResponse:
|
def get_filters(self):
|
||||||
"""
|
"""
|
||||||
Get list of Todoist filters.
|
Get list of Todoist filters.
|
||||||
|
|
||||||
|
:return: .. schema:: todoist.TodoistFilterSchema(many=True)
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistFilterSchema().dump(self._get_api().state['filters'], many=True)
|
||||||
return TodoistFiltersResponse(api.state['filters'])
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_live_notifications(self) -> TodoistLiveNotificationsResponse:
|
def get_live_notifications(self):
|
||||||
"""
|
"""
|
||||||
Get list of Todoist live notifications.
|
Get list of Todoist live notifications.
|
||||||
|
|
||||||
|
:return: .. schema:: todoist.TodoistLiveNotificationSchema(many=True)
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistLiveNotificationSchema().dump(
|
||||||
return TodoistLiveNotificationsResponse(api.state['live_notifications'])
|
self._get_api().state['live_notifications'], many=True
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_collaborators(self) -> TodoistCollaboratorsResponse:
|
def get_collaborators(self):
|
||||||
"""
|
"""
|
||||||
Get list of collaborators.
|
Get list of collaborators.
|
||||||
|
|
||||||
|
:return: .. schema:: todoist.TodoistCollaboratorSchema(many=True)
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistCollaboratorSchema().dump(
|
||||||
return TodoistCollaboratorsResponse(api.state['collaborators'])
|
self._get_api().state['collaborators'], many=True
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_notes(self) -> TodoistNotesResponse:
|
def get_notes(self):
|
||||||
"""
|
"""
|
||||||
Get list of Todoist notes.
|
Get list of Todoist notes.
|
||||||
|
|
||||||
|
:return: .. schema:: todoist.TodoistNoteSchema(many=True)
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistNoteSchema().dump(self._get_api().state['notes'], many=True)
|
||||||
return TodoistNotesResponse(api.state['notes'])
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_project_notes(self) -> TodoistProjectNotesResponse:
|
def get_project_notes(self):
|
||||||
"""
|
"""
|
||||||
Get list of Todoist project notes.
|
Get list of Todoist project notes.
|
||||||
|
|
||||||
|
:return: .. schema:: todoist.TodoistProjectNoteSchema(many=True)
|
||||||
"""
|
"""
|
||||||
api = self._get_api()
|
return TodoistProjectNoteSchema().dump(
|
||||||
return TodoistProjectNotesResponse(api.state['project_notes'])
|
self._get_api().state['project_notes'], many=True
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def add_item(self, content: str, project_id: Optional[int] = None, **kwargs):
|
def add_item(self, content: str, project_id: Optional[int] = None, **kwargs):
|
||||||
|
|
305
platypush/schemas/todoist.py
Normal file
305
platypush/schemas/todoist.py
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
from marshmallow import EXCLUDE, fields
|
||||||
|
from marshmallow.schema import Schema
|
||||||
|
|
||||||
|
from platypush.schemas import DateTime
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistUserSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist user schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
auto_reminder = fields.Int()
|
||||||
|
avatar_big = fields.Url(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's avatar URL.",
|
||||||
|
"example": "https://example.com/user/100x100.png",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
avatar_medium = fields.Url(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's avatar URL.",
|
||||||
|
"example": "https://example.com/user/50x50.png",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
avatar_s640 = fields.Url(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's avatar URL.",
|
||||||
|
"example": "https://example.com/user/640x640.png",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
avatar_small = fields.Url(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's avatar URL.",
|
||||||
|
"example": "https://example.com/user/25x25.png",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
business_account_id = fields.Int(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's business account ID.",
|
||||||
|
"example": 123456,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
daily_goal = fields.Int(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's daily goal.",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
date_format = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's date format.",
|
||||||
|
"example": "dd-mm-yyyy",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dateist_inline_disabled = fields.Bool()
|
||||||
|
dateist_lang = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's dateist language.",
|
||||||
|
"example": "en",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
days_off = fields.List(
|
||||||
|
fields.Int(),
|
||||||
|
metadata={
|
||||||
|
"description": "The user's days off.",
|
||||||
|
"example": [0, 6],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
default_reminder = fields.String()
|
||||||
|
email = fields.Email(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's email.",
|
||||||
|
"example": "user@example.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
features = fields.Dict()
|
||||||
|
full_name = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's full name.",
|
||||||
|
"example": "John Doe",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
id = fields.Int(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
"description": "The user's unique identifier.",
|
||||||
|
"example": 123456,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
image_id = fields.String()
|
||||||
|
inbox_project = fields.Int(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's inbox project ID.",
|
||||||
|
"example": 123456,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
is_biz_admin = fields.Bool()
|
||||||
|
is_premium = fields.Bool()
|
||||||
|
join_date = DateTime()
|
||||||
|
karma = fields.Float()
|
||||||
|
karma_trend = fields.String()
|
||||||
|
lang = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's language.",
|
||||||
|
"example": "en",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
legacy_inbox_project = fields.Int()
|
||||||
|
mobile_host = fields.String()
|
||||||
|
mobile_number = fields.String()
|
||||||
|
next_week = fields.Int()
|
||||||
|
premium_until = DateTime()
|
||||||
|
share_limit = fields.Int()
|
||||||
|
sort_order = fields.Int()
|
||||||
|
start_day = fields.Int()
|
||||||
|
start_page = fields.String()
|
||||||
|
theme = fields.Int()
|
||||||
|
time_format = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "The user's time format.",
|
||||||
|
"example": "24h",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
token = fields.String()
|
||||||
|
tz_info = fields.Dict()
|
||||||
|
unique_prefix = fields.Int()
|
||||||
|
websocket_url = fields.Url()
|
||||||
|
weekly_goal = fields.Int()
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistProjectSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist project schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
child_order = fields.Int()
|
||||||
|
collapsed = fields.Bool()
|
||||||
|
color = fields.Int()
|
||||||
|
has_more_notes = fields.Bool()
|
||||||
|
id = fields.Int(required=True)
|
||||||
|
is_archived = fields.Bool()
|
||||||
|
is_deleted = fields.Bool()
|
||||||
|
is_favorite = fields.Bool()
|
||||||
|
name = fields.String(required=True)
|
||||||
|
shared = fields.Bool()
|
||||||
|
inbox_project = fields.Bool()
|
||||||
|
legacy_id = fields.Int()
|
||||||
|
parent_id = fields.Int()
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistItemSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist item schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
assigned_by_uid = fields.Int()
|
||||||
|
checked = fields.Bool()
|
||||||
|
child_order = fields.Int()
|
||||||
|
collapsed = fields.Bool()
|
||||||
|
content = fields.String()
|
||||||
|
date_added = DateTime()
|
||||||
|
date_completed = DateTime()
|
||||||
|
day_order = fields.Int()
|
||||||
|
due = fields.Dict()
|
||||||
|
has_more_notes = fields.Bool()
|
||||||
|
id = fields.Int(required=True)
|
||||||
|
in_history = fields.Bool()
|
||||||
|
is_deleted = fields.Bool()
|
||||||
|
labels = fields.List(fields.String())
|
||||||
|
legacy_project_id = fields.Int()
|
||||||
|
parent_id = fields.Int()
|
||||||
|
priority = fields.Int()
|
||||||
|
project_id = fields.Int()
|
||||||
|
responsible_uid = fields.Int()
|
||||||
|
section_id = fields.Int()
|
||||||
|
sync_id = fields.Int()
|
||||||
|
user_id = fields.Int()
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistFilterSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist filter schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
color = fields.Int()
|
||||||
|
id = fields.Int(required=True)
|
||||||
|
is_deleted = fields.Bool()
|
||||||
|
item_order = fields.Int()
|
||||||
|
name = fields.String()
|
||||||
|
query = fields.String()
|
||||||
|
user_id = fields.Int()
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistLiveNotificationSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist live notifications schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
id = fields.Int(required=True)
|
||||||
|
notification_key = fields.String()
|
||||||
|
notification_type = fields.String()
|
||||||
|
project_id = fields.Int()
|
||||||
|
user_id = fields.Int()
|
||||||
|
is_deleted = fields.Bool()
|
||||||
|
is_unread = fields.Bool()
|
||||||
|
completed_last_month = fields.Bool()
|
||||||
|
karma_level = fields.Int()
|
||||||
|
promo_img = fields.Url()
|
||||||
|
completed_tasks = fields.Int()
|
||||||
|
created = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistCollaboratorSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist collaborator schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
data = fields.Dict()
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistNoteSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist note schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
data = fields.Dict()
|
||||||
|
|
||||||
|
|
||||||
|
class TodoistProjectNoteSchema(Schema):
|
||||||
|
"""
|
||||||
|
Todoist project note schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Meta class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
data = fields.Dict()
|
Loading…
Reference in a new issue