Skip to content
This repository has been archived by the owner on Feb 21, 2022. It is now read-only.

Commit

Permalink
Add archive manager makers and managers (#68)
Browse files Browse the repository at this point in the history
* Add archive manager makers and managers

- Manager makers available as "items_archive" and "sections_archive"
attributes of API object.

- Managers returned by methods "for_item", "for_section", "for_project"
of these makers.

- Items and sections available as elements, returned by methods items()
or sections() of those managers.

* Add mypy to type hints

* Fix type hints for archive manager

- Don't subclass ArchiveManager from Manager. Subclassing doesn't
provide any extra goodies, and is only confusing (mainly because we
don't follow the "implicit protocol" of Manager where state_name and
object_type must be defined)
- Rename internal property object_type to element_type to keep it
consistent with elements

* Add typing to setup.py (for python2.7 compatibility)

* Add tests for items and sections archive manager

* tox.ini: replace py.test with pytest

The name py.test is deprecated

* Add a CHANGELOG record
  • Loading branch information
imankulov committed Dec 5, 2019
1 parent 14dc945 commit 8a44d52
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ repos:
hooks:
- id: seed-isort-config

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.750
hooks:
- id: mypy

- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
hooks:
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## [Unreleased]

* Add support for items and sections archive manager.

## [8.1.1] - 2019-10-29
- Add `__contains__()` to `Model`.

## [8.1.0] - 2019-10-11
- Add support for sections.

## [8.0.2] - 2019-10-07
- Fix the parameters of `update_date_complete()`.

## [8.0.1] - 2019-10-07
- Fix the default API endpoint.

## [8.0] - 2019-04-18

* All arguments expecting a date/time must be formatted according to [RFC
Expand Down
11 changes: 11 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[mypy]
python_version = 2.7
follow_imports = silent
scripts_are_modules = true

# We had to ignore missing imports, because of third-party libraries installed
# inside the virtualenv, and apparently there's no easy way for mypy to respect
# packages inside the virtualenv. That's the option pre-commit-config runs with
# by default, but we add it here as well for the sake of uniformity of the
# output
ignore_missing_imports = true
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def read(fname):
license="BSD",
description="todoist-python - The official Todoist Python API library",
long_description=read("README.md"),
install_requires=["requests"],
install_requires=["requests", "typing"],
# see here for complete list of classifiers
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=(
Expand Down
46 changes: 46 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,52 @@ def test_share_delete(cleanup, cleanup2, api_endpoint, api_token, api_token2):
api.commit()


def test_items_archive(cleanup, api_endpoint, api_token):
api = todoist.api.TodoistAPI(api_token, api_endpoint)

# Create and complete five tasks
project = api.projects.add("Project")
items = [
api.items.add("task{}".format(i), project_id=project["id"]) for i in range(5)
]
for i, item in enumerate(items):
date_completed = "2019-01-01T00:00:0{}Z".format(i)
api.items.complete(item_id=item["id"], date_completed=date_completed)
api.commit()

# Create an archive manager to iterate over them
manager = api.items_archive.for_project(project["id"])
item_ids = [item["id"] for item in manager.items()]
assert item_ids == [item["id"] for item in items[::-1]]

# tear down
project.delete()
api.commit()


def test_sections_archive(cleanup, api_endpoint, api_token):
api = todoist.api.TodoistAPI(api_token, api_endpoint)

# Create and complete five sections
project = api.projects.add("Project")
sections = [
api.sections.add("s{}".format(i), project_id=project["id"]) for i in range(5)
]
for i, section in enumerate(sections):
date_archived = "2019-01-01T00:00:0{}Z".format(i)
api.sections.archive(section_id=section["id"], date_archived=date_archived)
api.commit()

# Create an archive manager to iterate over them
manager = api.sections_archive.for_project(project["id"])
section_ids = [section["id"] for section in manager.sections()]
assert section_ids == [section["id"] for section in sections[::-1]]

# tear down
project.delete()
api.commit()


def test_templates(cleanup, api_endpoint, api_token):
api = todoist.api.TodoistAPI(api_token, api_endpoint)

Expand Down
13 changes: 12 additions & 1 deletion todoist/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

from todoist import models
from todoist.managers.activity import ActivityManager
from todoist.managers.archive import (
ItemsArchiveManagerMaker,
SectionsArchiveManagerMaker,
)
from todoist.managers.backups import BackupsManager
from todoist.managers.biz_invitations import BizInvitationsManager
from todoist.managers.business_users import BusinessUsersManager
Expand All @@ -31,6 +35,8 @@
from todoist.managers.user import UserManager
from todoist.managers.user_settings import UserSettingsManager

DEFAULT_API_VERSION = "v8"


class SyncError(Exception):
pass
Expand All @@ -56,10 +62,12 @@ def __init__(
self,
token="",
api_endpoint="https://api.todoist.com",
api_version=DEFAULT_API_VERSION,
session=None,
cache="~/.todoist-sync/",
):
self.api_endpoint = api_endpoint
self.api_version = api_version
self.reset_state()
self.token = token # User's API token
self.temp_ids = {} # Mapping of temporary ids to real ids
Expand Down Expand Up @@ -93,6 +101,9 @@ def __init__(
self.templates = TemplatesManager(self)
self.uploads = UploadsManager(self)

self.items_archive = ItemsArchiveManagerMaker(self)
self.sections_archive = SectionsArchiveManagerMaker(self)

if cache: # Read and write user state on local disk cache
self.cache = os.path.expanduser(cache)
self._read_cache()
Expand Down Expand Up @@ -129,7 +140,7 @@ def serialize(self):
return {key: getattr(self, key) for key in self._serialize_fields}

def get_api_url(self):
return "%s/sync/v8/" % self.api_endpoint
return "{0}/sync/{1}/".format(self.api_endpoint, self.api_version)

def _update_state(self, syncdata):
"""
Expand Down
179 changes: 179 additions & 0 deletions todoist/managers/archive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""
Managers to get the list of archived items and sections.
Manager makers available as "items_archive" and "sections_archive" attributes of
API object.
Usage example (for items).
```python
# Create an API object
import todoist
api = todoist.TodoistAPI(...)
# Get project ID (take inbox)
project_id = api.user.get()['inbox_project']
# Initiate ItemsArchiveManager
archive = api.items_archive.for_project(project_id)
# Iterate over the list of completed items for the archive
for item in archive.items():
print(item["date_completed"], item["content"])
```
"""
from typing import TYPE_CHECKING, Dict, Iterator, Optional

from ..models import Item, Model, Section

if TYPE_CHECKING:
from ..api import TodoistAPI


class ArchiveManager(object):

object_model = Model

def __init__(self, api, element_type):
# type: (TodoistAPI, str) -> None
assert element_type in {"sections", "items"}
self.api = api
self.element_type = element_type

def next_page(self, cursor):
# type: (Optional[str]) -> Dict
"""Return response for the next page of the archive."""
resp = self.api.session.get(
self._next_url(),
params=self._next_query_params(cursor),
headers=self._request_headers(),
)
resp.raise_for_status()
return resp.json()

def _next_url(self):
return "{0}/sync/{1}/archive/{2}".format(
self.api.api_endpoint, self.api.api_version, self.element_type
)

def _next_query_params(self, cursor):
# type: (Optional[str]) -> Dict
ret = {}
if cursor:
ret["cursor"] = cursor
return ret

def _request_headers(self):
return {"Authorization": "Bearer {}".format(self.api.token)}

def _iterate(self):
has_more = True
cursor = None

while True:
if not has_more:
break

resp = self.next_page(cursor)

elements = [self._make_element(data) for data in resp[self.element_type]]
has_more = resp["has_more"]
cursor = resp.get("next_cursor")
for el in elements:
yield el

def _make_element(self, data):
return self.object_model(data, self.api)


class SectionsArchiveManagerMaker(object):
def __init__(self, api):
self.api = api

def __repr__(self):
return "{}()".format(self.__class__.__name__)

def for_project(self, project_id):
"""Get manager to iterate over all archived sections for project."""
return SectionsArchiveManager(api=self.api, project_id=project_id)


class SectionsArchiveManager(ArchiveManager):

object_model = Section

def __init__(self, api, project_id):
super(SectionsArchiveManager, self).__init__(api, "sections")
self.project_id = project_id

def __repr__(self):
return "SectionsArchiveManager(project_id={})".format(self.project_id)

def sections(self):
# type: () -> Iterator[Section]
"""Iterate over all archived sections."""
for obj in self._iterate():
yield obj

def _next_query_params(self, cursor):
ret = super(SectionsArchiveManager, self)._next_query_params(cursor)
ret["project_id"] = self.project_id
return ret


class ItemsArchiveManagerMaker(object):
def __init__(self, api):
self.api = api

def __repr__(self):
return "{}()".format(self.__class__.__name__)

def for_project(self, project_id):
"""Get manager to iterate over all top-level archived items for project."""
return ItemsArchiveManager(api=self.api, project_id=project_id)

def for_section(self, section_id):
"""Get manager to iterate over all top-level archived items for section."""
return ItemsArchiveManager(api=self.api, section_id=section_id)

def for_parent(self, parent_id):
"""Get manager to iterate over all archived sub-tasks for an item."""
return ItemsArchiveManager(api=self.api, parent_id=parent_id)


class ItemsArchiveManager(ArchiveManager):

object_model = Item

def __init__(self, api, project_id=None, section_id=None, parent_id=None):
super(ItemsArchiveManager, self).__init__(api, "items")
assert sum([bool(project_id), bool(section_id), bool(parent_id)]) == 1
self.project_id = project_id
self.section_id = section_id
self.parent_id = parent_id

def __repr__(self):
k, v = self._key_value()
return "ItemsArchiveManager({}={})".format(k, v)

def items(self):
# type: () -> Iterator[Item]
"""Iterate over all archived items."""
for obj in self._iterate():
yield obj

def _next_query_params(self, cursor):
ret = super(ItemsArchiveManager, self)._next_query_params(cursor)
k, v = self._key_value()
ret[k] = v
return ret

def _key_value(self):
if self.project_id:
return "project_id", self.project_id
elif self.section_id:
return "section_id", self.section_id
else: # if self.parent_id:
return "parent_id", self.parent_id
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
envlist = py27,py37
[testenv]
deps = pytest
commands = py.test {posargs}
commands = pytest {posargs}

0 comments on commit 8a44d52

Please sign in to comment.