Example #1
0
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
Example #2
0
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()
Example #3
0
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
Example #4
0
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']
Example #5
0
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
Example #6
0
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)
Example #7
0
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
Example #8
0
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