def get_overdue_items(token=None, query="overdue", incl_time=False): """ Get overdue tasks. Currently, this just invokes `api.sync()` and then filtering the response manually. It may be more appropriate to either: (a) Use a filter, either a saved filter (using `api.filters), or (b) Use the (now deprecated) `query/` endpoint, or (c) Use the REST API at https://beta.todoist.com/API/v8/ """ api = get_todoist_api(token) api.sync() now = datetime.datetime.now(timezone.utc) # today = datetime.fromordinal(now.toordinal()) # ordinal only includes date not time. today = datetime.datetime(*now.timetuple()[:3]) cut_date = now if incl_time else today # items = (item for item in api.items.all() if item['due_date_utc']) # overdue = [item for item in items if arrow.get(parse_date(item['due_date_utc'])) < cut_date] # overdue = [item for item in items if parse_date(item['due_date_utc']) < cut_date] # overdue = [item for item in items if arrow.get(parse_date(item['due_date_utc'])) < today] # Using arrow # use timestamp (seconds from epoch): # overdue = [item for item in items if parse_date(item['due_date_utc']).timestamp() < cut_date.timestamp()] overdue = [item for item in api.items.all() if item['due_date_utc'] is not None and parse_date(item['due_date_utc']).timestamp() < today.timestamp()] # Timestamp is epoch float return overdue
def reschedule_cmd(query, new_date='today'): """ Args: query: new_date: Returns: Discussion: Example POST request form data using the web interface to "reschedule" a single item: resource_types:["all"] commands: [{ "type": "item_update", "args": {"date_string":"Aug 7", "date_lang":"en", "id":2235317410, "due_date_utc": "2017-8-8T03:59:59"}, "uuid":"9b5ba425-7dcf-db4d-5a2a-d870bf731xxx" }] day_orders_timestamp:1502140199.43 collaborators_timestamp: sync_token:xxxx with_web_static_version:true limit_notes:1 max_notes:5 with_dateist_version:1 _client_id:_td_1501792xxxxx OBS: To see how the web interface uses the sync api, just open the "Network" tab in Chrome's developer tools and look for form data for requests to the /API/v7/sync endpoint. """ # Q: Do we need an actual ISO date-string, or can we use the Todoist API to parse `new_date`? # Q: If we need an ISO date-string, can we use e.g. maya/arrow/pendulum to parse `new_date`? # Q: Which is better, to use the full "sync" API, or to reschedule individual tasks? # Q: # tasks = todoist_query(query) api = get_todoist_api() query_res = api.query(queries=[query]) # sequence of queries tasks = query_res[0]["data"] reschedule_items(tasks, new_date=new_date) api.sync() # Workflow: # 1. Sync: # api.sync() -> api._update_state() -> populates api.state['items'] # 2. Update: # api.items.update(item_id, **kwargs) -> api.queue.append(cmd) # 3. Commit: # api.commit() -> api.sync(commands=api.queue) -> api._post() date_string, due_date_utc, datetime_obj = dateutil.parser.parse(new_date) # api.items.all() is same as api.state['items'] for item in api.items.all(): # Yes, these are actual `model.Item` objects. Made during `api._update_state()` with:: # newobj = model(remoteobj, self); self.state[datatype].append(newobj) # self = api item.update(date_string=date_string, due_date_utc=due_date_utc) api.commit()
def todoist_query(query, token=None): """ Get tasks matching the given query. Note: This currently uses the `query` endpoint at https://todoist.com/API/v7/query, which has been deprecated. Apparently the `query` API endpoint had quality issues, c.f. https://github.com/Doist/todoist-api/issues/22 and https://github.com/Doist/todoist-python/issues/37. They currently recommend "client side filtering" after obtaining all todoist data via the Sync API. It seems they are building a new REST API ("v8") which allows for simple task queries using a filter string. Args: query: Filter/query string to search for tasks. token: Login token. If not provided, will try to load token from file or config. Returns: tasks: list of task dicts """ api = get_todoist_api(token) # If you are going to use any of the object models, you should always start by invoking `api.sync()`, # which fetches and caches the whole Todoist state (and also returns it). # If you are just going to use api.query(queries), then you don't need to sync. # Note that the query endpoint was deprecated due to quality issues, sometimes returning unexpected results. # TODO: The `qyery` endpoint on the v7 "Sync" API has been deprecated. # TODO: Consider using saved filters via todoist.managers.filters.FilterManager, # TODO: Or alternatively use the new v8 "REST" API. # Edit: FiltersManager is only to create/read/update/delete filters, not applying filters. Same for models.Filter. query_res = api.query(queries=[query]) # `api.query` expects a sequence of queries tasks = query_res[0]["data"] return tasks
def completed_get_all( token=None, verbose=1, project_id=None, since=None, until=None, limit=None, offset=None, annotate_notes=None, ): """ Get completed items, using the 'completed/get_all' endpoint (via CompletedManager). This endpoint is more specific than the Activity Log endpoint. It still returns a dict with "items" and "projects", but the data keys are more descriptive and specific (e.g. "completed_date" instead of "event_date"). Note: This API endpoint is only available for Todoist Premium users. Refs/docs: * https://developer.todoist.com/?python#get-all-completed-items Available filter parameters/keys/kwargs for the 'completed/get_all' endpoint includes: * project_id: Only return completed items under a given project. * since, until: return event after/before these time (formatted as 2016-06-28T12:00). * limit, offset: Maximum number of events to return, and paging offset. * annotate_notes: Return notes together with the completed items (a true or false value). Note that while the regular Sync API uses ridiculous time format, the 'completed' API takes sane ISO-8601 formatted datetime strings for `since` and `until` params. Returns: A two-tuple of (items, projects). * Although maybe it is better to just return the response dict which contains two keys, "items" and "projects"? Note: * I'm having issues with the project_id for tasks not matching any projects. * The projects returned are good; it is indeed an issue with the project_id value. Maybe it is an old value? * This issue doesn't seem to be present for the `activity/get` endpoint. * This issue is NOT described in the docs, https://developer.todoist.com/sync/v7/#get-all-completed-items * The documented example makes it appear that item['project_id'] should match projects[id]['id'] . """ api = get_todoist_api(token) kwargs = dict( project_id=project_id, since=since, until=until, limit=limit, offset=offset, annotate_notes=annotate_notes, ) kwargs = {k: v for k, v in kwargs.items() if v is not None} if verbose: print( "\nRetrieving completed tasks from the Todoist `completed/get_all` endpoint..." ) res = api.completed.get_all(**kwargs) # from pprint import pprint; pprint(res) return res['items'], res['projects']
def activity( token=None, object_type=None, object_id=None, event_type=None, object_event_types=None, parent_project_id=None, parent_item_id=None, initiator_id=None, since=None, until=None, limit=None, offset=None, ): """ Search using the activity log ('activity/get') endpoint. This endpoint can be used to retrieve all recent activity, all activity for a given project, etc. Refs/docs: * https://developer.todoist.com/?python#get-activity-logs Note: api.activity.get() without parameters will get all activity (limited to 30 entries by default). Endpoint parameters/keys are: * object_type (str): e.g. 'item', 'note', 'project', etc. * object_id (int): Only show for a particular object, but only if `object_type` is given. * event_type (str): e.g. 'added', 'updated', 'completed', 'deleted', * object_event_types: Combination of object_type and event_type. * parent_project_id: Only show events for items or notes under a given project. * parent_item_id: Only show event for notes under a given item. * initiator_id: ? * since, until: return event after/before these time (formatted as 2016-06-28T12:00). * limit, offset: Maximum number of events to return, and paging offset. Returns: events: A list of event dicts. """ api = get_todoist_api(token) kwargs = dict( object_type=object_type, object_id=object_id, event_type=event_type, object_event_types=object_event_types, parent_project_id=parent_project_id, parent_item_id=parent_item_id, initiator_id=initiator_id, since=since, until=until, limit=limit, offset=offset, ) kwargs = {k: v for k, v in kwargs.items() if v is not None} events = api.activity.get(**kwargs) return events
def print_projects(print_fmt="pprint-data", sort_key=None, sync=True, sep="\n"): api = get_todoist_api() if sync: print("\nSyncing data with server...") api.sync() projects = api.state['projects'] # Not api.projects, which is a non-iterable ProjectsManager. Sigh. if print_fmt == "pprint": pprint(projects) elif print_fmt == "pprint-data": pprint([project.data for project in projects]) # Convert to list of dicts which prints better. else: for project in projects: fmt_kwargs = project.data if isinstance(project, Project) else project print(print_fmt.format(**fmt_kwargs), end=sep)
def get_todays_completed_events(token=None, sync=True, verbose=1): """ Get all today's completed events using ActivityManager (against the 'activity/get' endpoint). See `activity()` for info. Since only tasks can be completed, this is basically just another way to get completed tasks. Better alternative: Use `get_todays_completed_items()`, which uses the CompletedManager. """ api = get_todoist_api(token) if sync: if verbose: print("Syncing with the server...") api.sync() kwargs = dict( since="{:%Y-%m-%dT06:00}".format(datetime.date.today()), object_type='item', event_type='completed', limit=40, ) events = api.activity.get(**kwargs) return events
def add_task( content, due=None, project=None, labels=None, priority=None, note=None, auto_reminder=None, auto_parse_labels=None, *, sync=True, commit=True, show_queue=False, verbose=0, api=None, ): """ Add a single task to Todoist. Args: content: The task content (task name). due: Due date (str) project: Project name (str) labels: List of task labels (names) priority: Task priority, either number [1-4] or str ["p4", "p3", 'p2", "p1"]. note: Add a single note to the task. auto_reminder: Automatically add a default reminder to the task, if task is due at a specific time of day. auto_parse_labels: Automatically extract "@label" strings from the task content. sync: Start by synching the Sync API cache (recommended, unless testing). commit: End by committing the added task to the Todoist server (recommended, unless testing). show_queue: Show API queue before submitting the changes to the server. verbose: Be extra verbose when printing information during function run. api: Use this TodoistAPI object for the operation. Returns: The newly added task (todoist.models.Item object). """ if api is None: api = get_todoist_api() if sync: api.sync() if verbose >= 0: print(f"\nAdding new task:", file=sys.stderr) print(f" - content:", content, file=sys.stderr) print(f" - project:", project, file=sys.stderr) print(f" - labels:", list(labels) if labels else None, file=sys.stderr) print(f" - priority:", priority, file=sys.stderr) print(f" - due:", due, file=sys.stderr) print(f" - note:", note, file=sys.stderr) params = {} if due: params['due'] = {"string": due} if project: if isinstance(project, str): # We need to find project_id from project_name: project_name = project try: project_id = next( iter(p['id'] for p in api.projects.all() if p['name'].lower() == project_name.lower())) except StopIteration: msg = f'Project name "{project_name}" was not recognized. Please create project first. ' print(f"\n\nERROR: {msg}\n") print( "(You can use `todoist-cli print-projects` to see a list of available projects.)\n" ) raise ValueError( msg ) from None # raise from None to not show the StopIteration exception. else: # Project should be a project_id: assert isinstance(project, int) project_id = project params['project_id'] = project_id if labels: if isinstance(labels, str): labels = [label.strip() for label in labels.split(",")] if any(isinstance(label, str) for label in labels): # Assume label-name: We need to find label_id from label_name (lower-cased): labels_by_name = { label['name'].lower(): label for label in api.labels.all() } labels = [ label if isinstance(label, int) else labels_by_name[label.lower()]['id'] for label in labels ] params['labels'] = labels if priority is not None: params['priority'] = get_proper_priority_int(priority) if auto_reminder is not None: assert isinstance(auto_reminder, bool) params['auto_reminder'] = auto_reminder if auto_parse_labels is not None: assert isinstance(auto_parse_labels, bool) params['auto_parse_labels'] = auto_parse_labels # Add/create new task: new_task = api.items.add(content, **params) if verbose >= 1: print(f"\nNew task added:", file=sys.stderr) print(pformat(new_task.data), file=sys.stderr) if note: # Using a temporary ID is OK. new_note = api.notes.add(new_task["id"], note) if verbose >= 1: print(f"\nNew note added:", file=sys.stderr) print(pformat(new_note.data), file=sys.stderr) if show_queue: def get_obj_data(obj): return obj.data print("\n\nAPI QUEUE:\n----------\n") import json # This is actually the best, since it can use `default=get_obj_data` to represent model objects. print(json.dumps(api.queue, indent=2, default=get_obj_data)) if commit: if verbose >= 0: print(f"\nSubmitting new task to server...", file=sys.stderr) api.commit() if verbose >= 0: print(f" - OK!", file=sys.stderr) else: if verbose >= 0: print( f"\nNew task created - but not committed to server! (because `commit=False`)", file=sys.stderr) return new_task