From 29789461d7e311c8c8ca7c959108cb7b7c5d0e74 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 25 Dec 2019 20:32:54 +0100 Subject: [PATCH] Added Todoist integration --- docs/source/conf.py | 1 + platypush/backend/music/mopidy.py | 10 +- platypush/backend/todoist.py | 148 +++++++++++ platypush/message/__init__.py | 65 ++++- platypush/message/event/todoist.py | 59 +++++ platypush/message/response/__init__.py | 1 + platypush/message/response/todoist.py | 326 +++++++++++++++++++++++++ platypush/plugins/todoist.py | 190 ++++++++++++++ requirements.txt | 3 + setup.py | 2 + 10 files changed, 799 insertions(+), 6 deletions(-) create mode 100644 platypush/backend/todoist.py create mode 100644 platypush/message/event/todoist.py create mode 100644 platypush/message/response/todoist.py create mode 100644 platypush/plugins/todoist.py diff --git a/docs/source/conf.py b/docs/source/conf.py index bfd99869..63610d5b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -229,6 +229,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'pyaudio', 'avs', 'PyOBEX', + 'todoist', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/platypush/backend/music/mopidy.py b/platypush/backend/music/mopidy.py index 4289a4f9..c41b2da9 100644 --- a/platypush/backend/music/mopidy.py +++ b/platypush/backend/music/mopidy.py @@ -45,6 +45,7 @@ class MusicMopidyBackend(Backend): self._msg_id = 0 self._ws = None self._latest_status = {} + self._connected = False try: self._latest_status = self._get_tracklist_status() @@ -202,13 +203,18 @@ class MusicMopidyBackend(Backend): def _on_close(self): def hndl(): self._ws = None + self._connected = False self.logger.warning('Mopidy websocket connection closed') - time.sleep(5) - self._connect() + + while not self._connected: + self._connect() + time.sleep(10) + return hndl def _on_open(self): def hndl(ws): + self._connected = True self.logger.info('Mopidy websocket connected') return hndl diff --git a/platypush/backend/todoist.py b/platypush/backend/todoist.py new file mode 100644 index 00000000..15797345 --- /dev/null +++ b/platypush/backend/todoist.py @@ -0,0 +1,148 @@ +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 remote Todoist account. + + Requires: + + * **todoist-python** (``pip install todoist-python``) + * **websocket-client** (``pip install websocket-client``) + + Triggers: + + * :class:`platypush.message.event.todoist.NewItemEvent` when a new item is created. + * :class:`platypush.message.event.todoist.RemovedItemEvent` when an item is removed. + * :class:`platypush.message.event.todoist.CheckedItemEvent` when an item is checked. + * :class:`platypush.message.event.todoist.ItemContentChangeEvent` when the content of an item is changed. + * :class:`platypush.message.event.todoist.ModifiedItemEvent` when an item is changed and the change + doesn't fall into the categories above. + * :class:`platypush.message.event.todoist.TodoistSyncRequiredEvent` when an update has occurred that doesn't + fall into the categories above and a sync is required to get up-to-date. + + """ + + 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): + # noinspection PyUnusedLocal + def hndl(ws, msg): + msg = json.loads(msg) + if msg.get('type') == 'sync_needed': + self._refresh_all() + + return hndl + + def _on_error(self): + def hndl(error): + self.logger.warning('Todoist websocket error: {}'.format(error)) + return hndl + + def _on_close(self): + def hndl(): + self._ws = None + self.logger.warning('Todoist websocket connection closed') + self._connected = False + + while not self._connected: + self._connect() + time.sleep(10) + + 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: diff --git a/platypush/message/__init__.py b/platypush/message/__init__.py index a5561229..5103bac1 100644 --- a/platypush/message/__init__.py +++ b/platypush/message/__init__.py @@ -46,6 +46,7 @@ class Message(object): if isinstance(msg, bytes) or isinstance(msg, bytearray): msg = msg.decode('utf-8') if isinstance(msg, str): + # noinspection PyBroadException try: msg = json.loads(msg.strip()) except: @@ -53,7 +54,7 @@ class Message(object): assert isinstance(msg, dict) - if not '_timestamp' in msg: + if '_timestamp' not in msg: msg['_timestamp'] = time.time() return msg @@ -67,10 +68,66 @@ class Message(object): """ from platypush.utils import get_message_class_by_type - msg = cls.parse(msg) msgtype = get_message_class_by_type(msg['type']) - if msgtype != cls: return msgtype.build(msg) + if msgtype != cls: + return msgtype.build(msg) + + +class Mapping(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __setitem__(self, key, item): + self.__dict__[key] = item + + def __getitem__(self, key): + return self.__dict__[key] + + def __repr__(self): + return repr(self.__dict__) + + def __len__(self): + return len(self.__dict__) + + def __delitem__(self, key): + del self.__dict__[key] + + def clear(self): + return self.__dict__.clear() + + def copy(self): + return self.__dict__.copy() + + def has_key(self, k): + return k in self.__dict__ + + def update(self, *args, **kwargs): + return self.__dict__.update(*args, **kwargs) + + def keys(self): + return self.__dict__.keys() + + def values(self): + return self.__dict__.values() + + def items(self): + return self.__dict__.items() + + def pop(self, *args): + return self.__dict__.pop(*args) + + def __cmp__(self, dict_): + return self.__cmp__(dict_) + + def __contains__(self, item): + return item in self.__dict__ + + def __iter__(self): + return iter(self.__dict__) + + def __str__(self): + return str(self.__dict__) + # vim:sw=4:ts=4:et: - diff --git a/platypush/message/event/todoist.py b/platypush/message/event/todoist.py new file mode 100644 index 00000000..dc77fa3d --- /dev/null +++ b/platypush/message/event/todoist.py @@ -0,0 +1,59 @@ +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: diff --git a/platypush/message/response/__init__.py b/platypush/message/response/__init__.py index d6227fe9..f42e254e 100644 --- a/platypush/message/response/__init__.py +++ b/platypush/message/response/__init__.py @@ -84,4 +84,5 @@ class Response(Message): return json.dumps(response_dict) + # vim:sw=4:ts=4:et: diff --git a/platypush/message/response/todoist.py b/platypush/message/response/todoist.py new file mode 100644 index 00000000..bcb22db9 --- /dev/null +++ b/platypush/message/response/todoist.py @@ -0,0 +1,326 @@ +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: diff --git a/platypush/plugins/todoist.py b/platypush/plugins/todoist.py new file mode 100644 index 00000000..96b9a108 --- /dev/null +++ b/platypush/plugins/todoist.py @@ -0,0 +1,190 @@ +import time + +from typing import Optional + +import todoist +import todoist.managers.items + +from platypush.plugins import Plugin, action +from platypush.message.response.todoist import TodoistUserResponse, TodoistProjectsResponse, TodoistItemsResponse, \ + TodoistFiltersResponse, TodoistLiveNotificationsResponse, TodoistCollaboratorsResponse, TodoistNotesResponse, \ + TodoistProjectNotesResponse + + +class TodoistPlugin(Plugin): + """ + Todoist integration. + + Requires: + + * **todoist-python** (``pip install todoist-python``) + + You'll also need a Todoist token. You can get it `here `. + """ + + _sync_timeout = 60.0 + + def __init__(self, api_token: str, **kwargs): + """ + :param api_token: Todoist API token. You can get it `here `. + """ + + super().__init__(**kwargs) + self.api_token = api_token + self._api = None + self._last_sync_time = None + + def _get_api(self) -> todoist.TodoistAPI: + if not self._api: + self._api = todoist.TodoistAPI(self.api_token) + + if not self._last_sync_time or time.time() - self._last_sync_time > self._sync_timeout: + self._api.sync() + + return self._api + + @action + def get_user(self) -> TodoistUserResponse: + """ + Get logged user info. + """ + api = self._get_api() + return TodoistUserResponse(**api.state['user']) + + @action + def get_projects(self) -> TodoistProjectsResponse: + """ + Get list of Todoist projects. + """ + api = self._get_api() + return TodoistProjectsResponse(api.state['projects']) + + @action + def get_items(self) -> TodoistItemsResponse: + """ + Get list of Todoist projects. + """ + api = self._get_api() + return TodoistItemsResponse(api.state['items']) + + @action + def get_filters(self) -> TodoistFiltersResponse: + """ + Get list of Todoist filters. + """ + api = self._get_api() + return TodoistFiltersResponse(api.state['filters']) + + @action + def get_live_notifications(self) -> TodoistLiveNotificationsResponse: + """ + Get list of Todoist live notifications. + """ + api = self._get_api() + return TodoistLiveNotificationsResponse(api.state['live_notifications']) + + @action + def get_collaborators(self) -> TodoistCollaboratorsResponse: + """ + Get list of collaborators. + """ + api = self._get_api() + return TodoistCollaboratorsResponse(api.state['collaborators']) + + @action + def get_notes(self) -> TodoistNotesResponse: + """ + Get list of Todoist notes. + """ + api = self._get_api() + return TodoistNotesResponse(api.state['notes']) + + @action + def get_project_notes(self) -> TodoistProjectNotesResponse: + """ + Get list of Todoist project notes. + """ + api = self._get_api() + return TodoistProjectNotesResponse(api.state['project_notes']) + + @action + def add_item(self, content: str, project_id: Optional[int] = None, **kwargs): + """ + Add a new item. + """ + api = self._get_api() + manager = todoist.managers.items.ItemsManager(api=api) + item = manager.add(content, project_id=project_id, **kwargs) + api.commit() + return item.data + + @action + def delete_item(self, item_id: int): + """ + Delete an item by id. + """ + api = self._get_api() + manager = todoist.managers.items.ItemsManager(api=api) + manager.delete(item_id) + api.commit() + + @action + def update_item(self, item_id: int, **kwargs): + """ + Update an item by id. + """ + api = self._get_api() + manager = todoist.managers.items.ItemsManager(api=api) + manager.update(item_id, **kwargs) + api.commit() + + @action + def complete_item(self, item_id: int): + """ + Mark an item as done. + """ + api = self._get_api() + manager = todoist.managers.items.ItemsManager(api=api) + manager.complete(item_id) + api.commit() + + @action + def uncomplete_item(self, item_id: int): + """ + Mark an item as not done. + """ + api = self._get_api() + manager = todoist.managers.items.ItemsManager(api=api) + manager.uncomplete(item_id) + api.commit() + + @action + def archive(self, item_id: int): + """ + Archive an item by id. + """ + api = self._get_api() + manager = todoist.managers.items.ItemsManager(api=api) + manager.archive(item_id) + api.commit() + + @action + def unarchive(self, item_id: int): + """ + Un-archive an item by id. + """ + api = self._get_api() + manager = todoist.managers.items.ItemsManager(api=api) + manager.unarchive(item_id) + api.commit() + + @action + def sync(self): + """ + Sync/update info with the remote server. + """ + api = self._get_api() + api.sync() + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index c4c348f4..5f90130a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -188,3 +188,6 @@ croniter # Support for Node-RED integration # nodered + +# Support for Todoist integration +# todoist diff --git a/setup.py b/setup.py index fbac88bc..71f39201 100755 --- a/setup.py +++ b/setup.py @@ -252,6 +252,8 @@ setup( 'htmldoc': ['docutils'], # Support for Node-RED integration 'nodered': ['pynodered'], + # Support for Todoist integration + 'nodered': ['todoist-python'], }, )