diff --git a/README.md b/README.md index 0f5dd11..2b0d294 100644 --- a/README.md +++ b/README.md @@ -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 myemail@example.com --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? diff --git a/full_offline_backup_for_todoist/backup_attachments_downloader.py b/full_offline_backup_for_todoist/backup_attachments_downloader.py index 0159333..13dd8ba 100644 --- a/full_offline_backup_for_todoist/backup_attachments_downloader.py +++ b/full_offline_backup_for_todoist/backup_attachments_downloader.py @@ -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 diff --git a/full_offline_backup_for_todoist/controller.py b/full_offline_backup_for_todoist/controller.py index 5f614a6..8c530cd 100644 --- a/full_offline_backup_for_todoist/controller.py +++ b/full_offline_backup_for_todoist/controller.py @@ -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 diff --git a/full_offline_backup_for_todoist/frontend.py b/full_offline_backup_for_todoist/frontend.py index 22495d1..095877e 100644 --- a/full_offline_backup_for_todoist/frontend.py +++ b/full_offline_backup_for_todoist/frontend.py @@ -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 myemail@example.com --password P4ssW0rD".format(prog) + example1_str = ("Example: {} download --token 0123456789abcdef " + "--email myemail@example.com --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") diff --git a/full_offline_backup_for_todoist/runtime.py b/full_offline_backup_for_todoist/runtime.py index 8ea53d1..b82fac4 100644 --- a/full_offline_backup_for_todoist/runtime.py +++ b/full_offline_backup_for_todoist/runtime.py @@ -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) diff --git a/full_offline_backup_for_todoist/tests/test_integration.py b/full_offline_backup_for_todoist/tests/test_integration.py index 12fe454..9c622b6 100644 --- a/full_offline_backup_for_todoist/tests/test_integration.py +++ b/full_offline_backup_for_todoist/tests/test_integration.py @@ -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(), diff --git a/full_offline_backup_for_todoist/tests/test_util_static_http_request_handler.py b/full_offline_backup_for_todoist/tests/test_util_static_http_request_handler.py index d4912bc..67d99af 100644 --- a/full_offline_backup_for_todoist/tests/test_util_static_http_request_handler.py +++ b/full_offline_backup_for_todoist/tests/test_util_static_http_request_handler.py @@ -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 """ @@ -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 diff --git a/full_offline_backup_for_todoist/todoist_api.py b/full_offline_backup_for_todoist/todoist_api.py index 57a3e3d..39ed26a 100644 --- a/full_offline_backup_for_todoist/todoist_api.py +++ b/full_offline_backup_for_todoist/todoist_api.py @@ -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 diff --git a/full_offline_backup_for_todoist/tracer.py b/full_offline_backup_for_todoist/tracer.py index 14fc813..5c92ccc 100644 --- a/full_offline_backup_for_todoist/tracer.py +++ b/full_offline_backup_for_todoist/tracer.py @@ -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 diff --git a/full_offline_backup_for_todoist/url_downloader.py b/full_offline_backup_for_todoist/url_downloader.py index 986e796..e77ace1 100644 --- a/full_offline_backup_for_todoist/url_downloader.py +++ b/full_offline_backup_for_todoist/url_downloader.py @@ -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 @@ -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 @@ -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, @@ -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") diff --git a/full_offline_backup_for_todoist/virtual_fs.py b/full_offline_backup_for_todoist/virtual_fs.py index 993e540..91dd054 100644 --- a/full_offline_backup_for_todoist/virtual_fs.py +++ b/full_offline_backup_for_todoist/virtual_fs.py @@ -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.) """