Skip to content

Commit

Permalink
Update to API v8 due to removal of v7 API at the end of august, fix s…
Browse files Browse the repository at this point in the history
…ome pylint warnings and explain the email/pw workaround in the README.
  • Loading branch information
joanbm committed Aug 3, 2019
1 parent ba5b244 commit 21279b2
Show file tree
Hide file tree
Showing 11 changed files with 38 additions and 36 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,21 @@ Early version - enough for basic use, but not tested under every possible scenar

## Usage examples

Create a backup from Todoist's servers, including attachments:
Create a backup from Todoist's servers, without including attachments:

``python3 __main__.py download --with-attachments --token 0123456789abcdef``
``python3 __main__.py download --token 0123456789abcdef``

Create a backup from Todoist's servers, without including attachments, and with tracing/progress info:
Create a backup from Todoist's servers, including attachments, and with tracing/progress info:

``python3 __main__.py --verbose download --token 0123456789abcdef``
``python3 __main__.py --verbose download --with-attachments --token 0123456789abcdef --email [email protected] --password P4ssW0rD``

**NOTE:** The email and password is **required** to download the attachments, as a workaround due to security restriction introduced by Todoist in 2018 (see [issue #1](https://github.com/joanbm/full-offline-backup-for-todoist/issues/1)). As of today, there is no official way provided by Todoist to automate attachment download, and the current workaround may break at any time.

Print full help:

``python3 __main__.py -h``

**IMPORTANT:** To use this tool you will need to replace the "0123456789abcdef" string above with your Todoist API token.
**IMPORTANT:** To use this tool you will need to replace the "0123456789abcdef" string above with your Todoist API token (and similarly for your email and password).

## How to get my Todoist API token?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __fetch_attachment_infos_from_csv(self, csv_string):
matches = self.__TODOIST_ATTACHMENT_REGEXP.findall(row["CONTENT"])
for matchstr in matches:
attachment_info = self.__fetch_attachment_info_from_json(matchstr)
if attachment_info != None:
if attachment_info is not None:
attachment_infos.append(attachment_info)

return attachment_infos
Expand Down
2 changes: 1 addition & 1 deletion full_offline_backup_for_todoist/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from abc import ABCMeta, abstractmethod

class ControllerDependencyInjector(object, metaclass=ABCMeta):
class ControllerDependencyInjector(metaclass=ABCMeta):
""" Rudimentary dependency injection container for the controller """

@abstractmethod
Expand Down
7 changes: 4 additions & 3 deletions full_offline_backup_for_todoist/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ def __add_authorization_group(parser):
token_group.add_argument("--token-file", type=str,
help="file containing the todoist API token")
parser.add_argument("--email", type=str,
help="todoist email address for authorization")
help="todoist email address for attachment download authorization")
parser.add_argument("--password", type=str,
help="todoist email address for authorization")
help="todoist password for attachment download authorization")

def __parse_command_line_args(self, prog, arguments):
example1_str = "Example: {} download --token 0123456789abcdef --email [email protected] --password P4ssW0rD".format(prog)
example1_str = ("Example: {} download --token 0123456789abcdef "
"--email [email protected] --password P4ssW0rD").format(prog)
parser = argparse.ArgumentParser(prog=prog, formatter_class=argparse.RawTextHelpFormatter,
epilog=example1_str)
parser.add_argument("--verbose", action="store_true", help="print details to console")
Expand Down
4 changes: 2 additions & 2 deletions full_offline_backup_for_todoist/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ def __init__(self, auth, verbose):
super(RuntimeControllerDependencyInjector, self).__init__(auth, verbose)
self.__tracer = ConsoleTracer() if verbose else NullTracer()
if ("email" in auth and auth["email"] is not None and
"password" in auth and auth["password"] is not None):
"password" in auth and auth["password"] is not None):
urldownloader = TodoistAuthURLDownloader(self.__tracer, auth["email"], auth["password"])
else:
self.__tracer.trace("NOTE: No email & password given, falling back to no-auth downloader")
self.__tracer.trace("NOTE: No email/password given, falling back to no-auth downloader")
urldownloader = URLLibURLDownloader()
todoist_api = TodoistApi(auth["token"], self.__tracer, urldownloader)
self.__backup_downloader = TodoistBackupDownloader(self.__tracer, todoist_api)
Expand Down
16 changes: 8 additions & 8 deletions full_offline_backup_for_todoist/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ def setUp(self):
# Set up the fake HTTP server with local responses
# pylint: disable=line-too-long
route_responses = {
"/https://todoist.com/api/v7/sync?token=mysecrettoken&sync_token=%2A&resource_types=%5B%22projects%22%5D":
"/https://api.todoist.com/sync/v8/sync?token=mysecrettoken&sync_token=%2A&resource_types=%5B%22projects%22%5D":
Path(self.__get_test_file("sources/project_list.json")).read_bytes(),
"/https://todoist.com/api/v7/templates/export_as_file?token=mysecrettoken&project_id=2181147955":
"/https://api.todoist.com/sync/v8/templates/export_as_file?token=mysecrettoken&project_id=2181147955":
Path(self.__get_test_file("sources/Project_2181147955.csv")).read_bytes(),
"/https://todoist.com/api/v7/templates/export_as_file?token=mysecrettoken&project_id=2181147714":
"/https://api.todoist.com/sync/v8/templates/export_as_file?token=mysecrettoken&project_id=2181147714":
Path(self.__get_test_file("sources/Project_2181147714.csv")).read_bytes(),
"/https://todoist.com/api/v7/templates/export_as_file?token=mysecrettoken&project_id=2181147709":
"/https://api.todoist.com/sync/v8/templates/export_as_file?token=mysecrettoken&project_id=2181147709":
Path(self.__get_test_file("sources/Project_2181147709.csv")).read_bytes(),
"/https://todoist.com/api/v7/templates/export_as_file?token=mysecrettoken&project_id=2181147715":
"/https://api.todoist.com/sync/v8/templates/export_as_file?token=mysecrettoken&project_id=2181147715":
Path(self.__get_test_file("sources/Project_2181147715.csv")).read_bytes(),
"/https://todoist.com/api/v7/templates/export_as_file?token=mysecrettoken&project_id=2181147711":
"/https://api.todoist.com/sync/v8/templates/export_as_file?token=mysecrettoken&project_id=2181147711":
Path(self.__get_test_file("sources/Project_2181147711.csv")).read_bytes(),
"/https://todoist.com/api/v7/templates/export_as_file?token=mysecrettoken&project_id=2181147712":
"/https://api.todoist.com/sync/v8/templates/export_as_file?token=mysecrettoken&project_id=2181147712":
Path(self.__get_test_file("sources/Project_2181147712.csv")).read_bytes(),
"/https://todoist.com/api/v7/templates/export_as_file?token=mysecrettoken&project_id=2181147713":
"/https://api.todoist.com/sync/v8/templates/export_as_file?token=mysecrettoken&project_id=2181147713":
Path(self.__get_test_file("sources/Project_2181147713.csv")).read_bytes(),
"/https://d1x0mwiac2rqwt.cloudfront.net/g75-kL8pwVYNObSczLnVXe4FIyJd8YQL6b8yCilGyix09bMdJmxbtrGMW9jIeIwJ/by/16542905/as/bug.txt":
Path(self.__get_test_file("sources/bug.txt")).read_bytes(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class TestHTTPRequestHandler(BaseHTTPRequestHandler):

def log_message(self, format, *args): # pylint: disable=redefined-builtin
""" Disables console output for the HTTP Request Handler """
pass

def do_GET(self): # pylint: disable=invalid-name
""" Handles a request using the defined static mapping """
Expand All @@ -38,6 +37,5 @@ def do_GET(self): # pylint: disable=invalid-name

if self.path in route_responses:
self.wfile.write(route_responses[self.path])
return

return TestHTTPRequestHandler
5 changes: 3 additions & 2 deletions full_offline_backup_for_todoist/todoist_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ def __init__(self, name, identifier):
class TodoistApi:
""" Provides access to a subset of the features of the Todoist API"""

__SYNC_ENDPOINT = "https://todoist.com/api/v7/sync"
__EXPORT_PROJECT_AS_CSV_FILE_ENDPOINT = "https://todoist.com/api/v7/templates/export_as_file"
__BASE_URL = "https://api.todoist.com/sync/v8"
__SYNC_ENDPOINT = __BASE_URL + "/sync"
__EXPORT_PROJECT_AS_CSV_FILE_ENDPOINT = __BASE_URL + "/templates/export_as_file"

def __init__(self, api_token, tracer, urldownloader):
self.__api_token = api_token
Expand Down
2 changes: 1 addition & 1 deletion full_offline_backup_for_todoist/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
""" Definitions and implementations of a simple logging / tracing method """
from abc import ABCMeta, abstractmethod

class Tracer(object, metaclass=ABCMeta):
class Tracer(metaclass=ABCMeta):
""" Base class for implementations of a simple logging / tracing method """

@abstractmethod
Expand Down
20 changes: 10 additions & 10 deletions full_offline_backup_for_todoist/url_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import urllib.parse
import http.cookiejar

class URLDownloader(object, metaclass=ABCMeta):
class URLDownloader(metaclass=ABCMeta):
""" Implementation of a class to download the contents of an URL """

@abstractmethod
Expand All @@ -28,13 +28,12 @@ class TodoistAuthURLDownloader(URLDownloader):
""" Implementation of a class to download the contents of an URL through URLLib,
authenticating before with Todoist's servers using a username/password """

URL_SHOWLOGIN='https://todoist.com/Users/showLogin'
URL_POSTLOGIN='https://todoist.com/Users/login'
URL_FILE_TO_DOWNLOAD="https://files.todoist.com/VxxTiO9w6TCd_gHiHxpovh3oLL71OllvLxXFgv70jhxJkMSuu6veoX7fVnasbZpc/by/15956439/as/IMG_3285.JPG"
URL_SHOWLOGIN = 'https://todoist.com/Users/showLogin'
URL_POSTLOGIN = 'https://todoist.com/Users/login'

LOGIN_PARAM_CSRF="csrf"
LOGIN_PARAM_EMAIL="email"
LOGIN_PARAM_PASSWORD="password"
LOGIN_PARAM_CSRF = "csrf"
LOGIN_PARAM_EMAIL = "email"
LOGIN_PARAM_PASSWORD = "password"

def __init__(self, tracer, email, password):
self.__tracer = tracer
Expand All @@ -52,13 +51,14 @@ def get(self, url, data=None):
self.__tracer.trace("Auth Step 1: Get CSRF token")

# Ping the login page, in order to get a CSRF token as a cookie
with self.__opener.open(TodoistAuthURLDownloader.URL_SHOWLOGIN) as get_csrf_response:
with self.__opener.open(TodoistAuthURLDownloader.URL_SHOWLOGIN) as _:
pass

self.__tracer.trace("Auth Step 2: Building login request params")

# Build the parameters (CSRF, email and password) for the login POST request
csrf_value = next(c.value for c in cookiejar if c.name == TodoistAuthURLDownloader.LOGIN_PARAM_CSRF)
csrf_value = next(c.value for c in cookiejar
if c.name == TodoistAuthURLDownloader.LOGIN_PARAM_CSRF)
params = {
TodoistAuthURLDownloader.LOGIN_PARAM_CSRF: csrf_value,
TodoistAuthURLDownloader.LOGIN_PARAM_EMAIL: self.__email,
Expand All @@ -68,7 +68,7 @@ def get(self, url, data=None):
self.__tracer.trace("Auth Step 3: Send login request")

# Send the login POST request, which will give us our identifier cookie
with self.__opener.open(TodoistAuthURLDownloader.URL_POSTLOGIN, params_str) as login_response:
with self.__opener.open(TodoistAuthURLDownloader.URL_POSTLOGIN, params_str) as _:
pass

self.__tracer.trace("Auth completed")
Expand Down
2 changes: 1 addition & 1 deletion full_offline_backup_for_todoist/virtual_fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import zipfile
from pathlib import Path

class VirtualFs(object, metaclass=ABCMeta):
class VirtualFs(metaclass=ABCMeta):
""" An abstract layer over the filesystem
(e.g. can represent a real folder, a ZIP file, etc.) """

Expand Down

0 comments on commit 21279b2

Please sign in to comment.