PK!z}]toodledo/__init__.py"""Python wrapper for the Toodledo v3 API which is documented at http://api.toodledo.com/3/""" from .authorization import CommandLineAuthorization from .context import Context from .folder import Folder from .storage import TokenStorageFile from .task import Task from .transport import AuthorizationNeeded, Toodledo, ToodledoError from .types import DueDateModifier, Priority, Status PK!=]toodledo/account.py"""Account-related stuff""" from marshmallow import post_load, Schema from .custom_fields import _ToodledoDatetime class _Account: # pylint: disable=too-few-public-methods def __init__(self, lastEditTask, lastDeleteTask): self.lastEditTask = lastEditTask self.lastDeleteTask = lastDeleteTask def __repr__(self): return "<_Account lastEditTask={}, lastDeleteTask={}>".format(self.lastEditTask, self.lastDeleteTask) class _AccountSchema(Schema): lastEditTask = _ToodledoDatetime(dump_to="lastedit_task", load_from="lastedit_task") lastDeleteTask = _ToodledoDatetime(dump_to="lastdelete_task", load_from="lastdelete_task") @post_load def _MakeAccount(self, data): # pylint: disable=no-self-use return _Account(data["lastEditTask"], data["lastDeleteTask"]) PK!?9Wtoodledo/authorization.py"""Example authorization""" from requests_oauthlib import OAuth2Session from .transport import Toodledo def CommandLineAuthorization(clientId, clientSecret, scope, tokenStorage): """Authorize in a command line program""" authorizationBaseUrl = "https://api.toodledo.com/3/account/authorize.php" session = OAuth2Session(client_id=clientId, scope=scope) authorizationUrl, _ = session.authorization_url(authorizationBaseUrl) print("Go to the following URL and authorize the app:" + authorizationUrl) try: from pyperclip import copy copy(authorizationUrl) print("URL copied to clipboard") except ImportError: pass redirectResponse = input("Paste the full redirect URL here: ") token = session.fetch_token(Toodledo.tokenUrl, client_secret=clientSecret, authorization_response=redirectResponse, token_updater=tokenStorage.Save) tokenStorage.Save(token) return token PK!/<toodledo/context.py"""Account-related stuff""" from marshmallow import fields, post_load, Schema from .custom_fields import _ToodledoBoolean class Context: # pylint: disable=too-few-public-methods """Toodledo context""" def __init__(self, **data): for name, item in data.items(): setattr(self, name, item) def __repr__(self): attributes = sorted(["{}={}".format(name, item) for name, item in self.__dict__.items()]) return "".format(", ".join(attributes)) class _ContextSchema(Schema): id_ = fields.Integer(dump_to="id", load_from="id") name = fields.String() private = _ToodledoBoolean() @post_load def _MakeContext(self, data): # pylint: disable=no-self-use return Context(**data) PK!ҰzG toodledo/custom_fields.py"""Implementation""" from datetime import date, datetime from marshmallow import fields from .types import DueDateModifier, Priority, Status # all the fields have the option for not being set # dates have an extra option of being 0 or a real date # states for this field are: # a GMT timestamp with the time set to noon # unset, represented by API as 0 class _ToodledoDate(fields.Field): def _serialize(self, value, attr, obj): if value is None: return 0 return datetime(year=value.year, month=value.month, day=value.day).timestamp() def _deserialize(self, value, attr, data): if value == 0: return None return date.fromtimestamp(float(value)) # states for this field are: # a GMT timestamp # unset, represented by API as 0 class _ToodledoDatetime(fields.Field): def _serialize(self, value, attr, obj): if value is None: return 0 return value.timestamp() def _deserialize(self, value, attr, data): if value == 0: return None return datetime.fromtimestamp(float(value)) class _ToodledoTags(fields.Field): def _serialize(self, value, attr, obj): assert isinstance(value, list) return ", ".join(sorted(value)) def _deserialize(self, value, attr, data): assert isinstance(value, str) if value == "": return [] return [x.strip() for x in value.split(",")] # Can't use the standard marshmallow boolean because it serializes to True/False rather than 1/0 class _ToodledoBoolean(fields.Field): def _serialize(self, value, attr, obj): assert isinstance(value, bool) return 1 if value else 0 def _deserialize(self, value, attr, data): assert isinstance(value, int) assert 0 <= value <= 1 return value == 1 class _ToodledoListId(fields.Field): def _serialize(self, value, attr, obj): assert value is None or isinstance(value, int) return value if value is not None else 0 def _deserialize(self, value, attr, data): assert isinstance(value, int) if value == 0: return None return value class _ToodledoPriority(fields.Field): def _serialize(self, value, attr, obj): assert isinstance(value, Priority) return value.value def _deserialize(self, value, attr, data): assert isinstance(value, int) assert -1 <= value <= 3 for enumValue in Priority: if enumValue.value == value: return enumValue assert False, "Bad incoming integer for priority enum" return None class _ToodledoDueDateModifier(fields.Field): def _serialize(self, value, attr, obj): assert isinstance(value, DueDateModifier) return value.value def _deserialize(self, value, attr, data): assert isinstance(value, int) assert 0 <= value <= 3 for enumValue in DueDateModifier: if enumValue.value == value: return enumValue assert False, "Bad incoming integer for due date modifier enum" return None class _ToodledoStatus(fields.Field): def _serialize(self, value, attr, obj): assert isinstance(value, Status) return value.value def _deserialize(self, value, attr, data): assert isinstance(value, int) assert 0 <= value <= 10 for enumValue in Status: if enumValue.value == value: return enumValue assert False, "Bad incoming integer for status enum" return None PK!toodledo/errors.py"""Known Toodledo errors""" class ToodledoError(Exception): """Custom error for wrapping API error codes""" errorCodeToMessage = { 1: "No access token was given", 2: "The access token was invalid", 3: "Too many API requests", 4: "The API is offline for maintenance", 101: "SSL connection is required", 102: "There was an error requesting a token", 103: "Too many token requests", 201: "Your folder must have a name.", 202: "A folder with that name already exists.", 203: "Max folders reached (1000).", 204: "Empty id.", 205: "Invalid folder.", 206: "Nothing was edited.", 301: "Your context must have a name.", 302: "A context with that name already exists.", 303: "Max contexts reached (1000).", 304: "Empty id.", 305: "Invalid context.", 306: "Nothing was edited.", 601: "Your task must have a title.", 602: "Only 50 tasks can be added/edited/deleted at a time.", 603: "The maximum number of tasks allowed per account (20000) has been reached", 604: "Empty id", 605: "Invalid task", 606: "Nothing was added/edited. You'll get this error if you attempt to edit a task but don't pass any parameters to edit.", 607: "Invalid folder id", 608: "Invalid context id", 609: "Invalid goal id", 610: "Invalid location id", 611: "Malformed request", 612: "Invalid parent id", 613: "Incorrect field parameters", 614: "Parent was deleted", 615: "Invalid collaborator", 616: "Unable to reassign or share task" } def __init__(self, errorCode): errorMessage = ToodledoError.errorCodeToMessage.get(errorCode, "Unknown error") super().__init__(errorMessage, errorCode) PK!5O&&toodledo/folder.py"""Account-related stuff""" from marshmallow import fields, post_load, Schema from .custom_fields import _ToodledoBoolean class Folder: # pylint: disable=too-few-public-methods """Toodledo folder""" def __init__(self, **data): for name, item in data.items(): setattr(self, name, item) def __repr__(self): attributes = sorted(["{}={}".format(name, item) for name, item in self.__dict__.items()]) return "".format(", ".join(attributes)) class _FolderSchema(Schema): id_ = fields.Integer(dump_to="id", load_from="id") name = fields.String() private = _ToodledoBoolean() archived = _ToodledoBoolean() order = fields.Integer(dump_to="ord", load_from="ord") @post_load def _MakeFolder(self, data): # pylint: disable=no-self-use return Folder(**data) PK!?/toodledo/storage.py"""Token storage""" from json import dump, load class TokenStorageFile: """Stores the API tokens as a file""" def __init__(self, path): self.path = path def Save(self, token): """Save the given token. Called by Toodledo class""" with open(self.path, "w") as f: dump(token, f) def Load(self): """Load and return the token. Called by Toodledo class""" try: with open(self.path, "r") as f: return load(f) except FileNotFoundError: return None PK!X,,toodledo/task.py"""Task-related stuff""" from marshmallow import fields, post_load, Schema from marshmallow.validate import Length from .custom_fields import _ToodledoBoolean, _ToodledoDate, _ToodledoDatetime, _ToodledoDueDateModifier, _ToodledoListId, _ToodledoPriority, _ToodledoStatus, _ToodledoTags class Task: """Represents a single task""" def __init__(self, **data): for name, item in data.items(): setattr(self, name, item) def __repr__(self): attributes = sorted(["{}={}".format(name, item) for name, item in self.__dict__.items()]) return "".format(", ".join(attributes)) def IsComplete(self): """Indicate whether this task is complete""" return self.completedDate is not None # pylint: disable=no-member class _TaskSchema(Schema): id_ = fields.Integer(dump_to="id", load_from="id") title = fields.String(validate=Length(max=255)) tags = _ToodledoTags(dump_to="tag", load_from="tag") startDate = _ToodledoDate(dump_to="startdate", load_from="startdate") dueDate = _ToodledoDate(dump_to="duedate", load_from="duedate") modified = _ToodledoDatetime() completedDate = _ToodledoDate(dump_to="completed", load_from="completed") star = _ToodledoBoolean() priority = _ToodledoPriority() dueDateModifier = _ToodledoDueDateModifier(dump_to="duedatemod", load_from="duedatemod") status = _ToodledoStatus() length = fields.Integer() note = fields.String() folderId = _ToodledoListId(dump_to="folder", load_from="folder") contextId = _ToodledoListId(dump_to="context", load_from="context") @post_load def _MakeTask(self, data): # pylint: disable=no-self-use return Task(**data) def _DumpTaskList(taskList): # TODO - pass many=True to the schema instead of this custom stuff schema = _TaskSchema() return [schema.dump(task).data for task in taskList] PK!Btoodledo/transport.py"""Implementation""" from json import dumps from logging import debug, error from requests_oauthlib import OAuth2Session from .account import _AccountSchema from .context import _ContextSchema from .errors import ToodledoError from .folder import _FolderSchema from .task import _DumpTaskList, _TaskSchema class AuthorizationNeeded(Exception): """Thrown when the token storage doesn't contain a token""" class Toodledo: """Wrapper for the Toodledo v3 API""" baseUrl = "https://api.toodledo.com/3/" tokenUrl = baseUrl + "account/token.php" getAccountUrl = baseUrl + "account/get.php" getTasksUrl = baseUrl + "tasks/get.php" deleteTasksUrl = baseUrl + "tasks/delete.php" addTasksUrl = baseUrl + "tasks/add.php" editTasksUrl = baseUrl + "tasks/edit.php" getFoldersUrl = baseUrl + "folders/get.php" addFolderUrl = baseUrl + "folders/add.php" deleteFolderUrl = baseUrl + "folders/delete.php" editFolderUrl = baseUrl + "folders/edit.php" getContextsUrl = baseUrl + "contexts/get.php" addContextUrl = baseUrl + "contexts/add.php" editContextUrl = baseUrl + "contexts/edit.php" deleteContextUrl = baseUrl + "contexts/delete.php" def __init__(self, clientId, clientSecret, tokenStorage, scope): self.tokenStorage = tokenStorage self.clientId = clientId self.clientSecret = clientSecret self.scope = scope def _Session(self): token = self.tokenStorage.Load() if token is None: raise AuthorizationNeeded("No token in storage") return OAuth2Session( client_id=self.clientId, token=token, auto_refresh_kwargs={ "client_id": self.clientId, "client_secret": self.clientSecret }, auto_refresh_url=Toodledo.tokenUrl, token_updater=self.tokenStorage.Save) def GetFolders(self): """Get all the folders as folder objects""" folders = self._Session().get(Toodledo.getFoldersUrl) folders.raise_for_status() schema = _FolderSchema() return [schema.load(x).data for x in folders.json()] def AddFolder(self, folder): """Add folder, return the created folder""" response = self._Session().post(Toodledo.addFolderUrl, params={"name": folder.name, "private": 1 if folder.private else 0}) response.raise_for_status() if "errorCode" in response.json(): error("Toodledo error: {}".format(response.json())) raise ToodledoError(response.json()["errorCode"]) return _FolderSchema().load(response.json()[0]).data def DeleteFolder(self, folder): """Delete folder""" response = self._Session().post(Toodledo.deleteFolderUrl, params={"id": folder.id_}) response.raise_for_status() jsonResponse = response.json() if "errorCode" in jsonResponse: error("Toodledo error: {}".format(jsonResponse)) raise ToodledoError(jsonResponse["errorCode"]) assert jsonResponse == {"deleted": folder.id_}, dumps(jsonResponse) def EditFolder(self, folder): """Edits the given folder to have the given properties""" folderData = _FolderSchema().dump(folder).data response = self._Session().post(Toodledo.editFolderUrl, params=folderData) response.raise_for_status() responseAsDict = response.json() if "errorCode" in responseAsDict: error("Toodledo error: {}".format(responseAsDict)) raise ToodledoError(responseAsDict["errorCode"]) return _FolderSchema().load(responseAsDict[0]).data def GetContexts(self): """Get all the contexts as context objects""" contexts = self._Session().get(Toodledo.getContextsUrl) contexts.raise_for_status() schema = _ContextSchema() return [schema.load(x).data for x in contexts.json()] def AddContext(self, context): """Add context, return the created context""" response = self._Session().post(Toodledo.addContextUrl, params={"name": context.name, "private": 1 if context.private else 0}) response.raise_for_status() if "errorCode" in response.json(): error("Toodledo error: {}".format(response.json())) raise ToodledoError(response.json()["errorCode"]) return _ContextSchema().load(response.json()[0]).data def DeleteContext(self, context): """Delete context""" response = self._Session().post(Toodledo.deleteContextUrl, params={"id": context.id_}) response.raise_for_status() jsonResponse = response.json() if "errorCode" in jsonResponse: error("Toodledo error: {}".format(jsonResponse)) raise ToodledoError(jsonResponse["errorCode"]) assert jsonResponse == {"deleted": context.id_}, dumps(jsonResponse) def EditContext(self, context): """Edits the given folder to have the given properties""" contextData = _ContextSchema().dump(context).data response = self._Session().post(Toodledo.editContextUrl, params=contextData) response.raise_for_status() responseAsDict = response.json() if "errorCode" in responseAsDict: error("Toodledo error: {}".format(responseAsDict)) raise ToodledoError(responseAsDict["errorCode"]) return _ContextSchema().load(responseAsDict[0]).data def GetAccount(self): """Get the Toodledo account""" accountInfo = self._Session().get(Toodledo.getAccountUrl) accountInfo.raise_for_status() return _AccountSchema().load(accountInfo.json()).data def GetTasks(self, params): """Get the tasks filtered by the given params""" allTasks = [] limit = 1000 # single request limit start = 0 while True: debug("Start: {}".format(start)) params["start"] = start params["num"] = limit response = self._Session().get(Toodledo.getTasksUrl, params=params) response.raise_for_status() tasks = response.json() if "errorCode" in tasks: error("Toodledo error: {}".format(tasks)) raise ToodledoError(tasks["errorCode"]) # the first field contains the count or the error code allTasks.extend(tasks[1:]) debug("Retrieved {:,} tasks".format(len(tasks[1:]))) if len(tasks[1:]) < limit: break start += limit schema = _TaskSchema() return [schema.load(x).data for x in allTasks] def EditTasks(self, taskList): """Change the existing tasks to be the same as the ones in the given list""" if len(taskList) == 0: return debug("Total tasks to edit: {}".format(len(taskList))) limit = 50 # single request limit start = 0 while True: debug("Start: {}".format(start)) listDump = _DumpTaskList(taskList[start:start + limit]) response = self._Session().post(Toodledo.editTasksUrl, params={"tasks": dumps(listDump)}) response.raise_for_status() debug("Response: {},{}".format(response, response.text)) taskResponse = response.json() if "errorCode" in taskResponse: raise ToodledoError(taskResponse["errorCode"]) if len(taskList[start:start + limit]) < limit: break start += limit def AddTasks(self, taskList): """Add the given tasks""" if len(taskList) == 0: return limit = 50 # single request limit start = 0 while True: debug("Start: {}".format(start)) listDump = _DumpTaskList(taskList[start:start + limit]) response = self._Session().post(Toodledo.addTasksUrl, params={"tasks": dumps(listDump)}) response.raise_for_status() if "errorCode" in response.json(): raise ToodledoError(response.json()["errorCode"]) if len(taskList[start:start + limit]) < limit: break start += limit def DeleteTasks(self, taskList): """Delete the given tasks""" if len(taskList) == 0: return taskIdList = [task.id_ for task in taskList] limit = 50 # single request limit start = 0 while True: debug("Start: {}".format(start)) response = self._Session().post(Toodledo.deleteTasksUrl, params={"tasks": dumps(taskIdList[start:start + limit])}) response.raise_for_status() if "errorCode" in response.json(): raise ToodledoError(response.json()["errorCode"]) if len(taskIdList[start:start + limit]) < limit: break start += limit PK!,CCtoodledo/types.py"""Python-side Toodledo types""" from enum import Enum class Priority(Enum): """Priority as an enum with the correct Toodledo integer equivalents""" NEGATIVE = -1 LOW = 0 MEDIUM = 1 HIGH = 2 TOP = 3 class DueDateModifier(Enum): """Enum for all the due date modifiers""" DUE_BY = 0 DUE_ON = 1 DUE_AFTER = 2 OPTIONALLY = 3 class Status(Enum): """Enum for all the possible statuses""" NONE = 0 NEXT_ACTION = 1 ACTIVE = 2 PLANNING = 3 DELEGATED = 4 WAITING = 5 HOLD = 6 POSTPONED = 7 SOMEDAY = 8 CANCELED = 9 REFERENCE = 10 PK!HڽTUtoodledo-1.0.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HDaD!toodledo-1.0.0.dist-info/METADATATN@Wr T0mj "i7cT;k; *攝yov[_#T uT1G,12&-05b; 'D,1GM+- #,SakvՔX r:"Yp{SLM#є2&<1,Ӕ7`F~і]J+sK1k8fc\Vc KO닅Kw]+<'⢐{\[,Le$5N [kF*Ī=ޜeW)+5J x{AG,0kkZƝmz—gV2U<~XB0N_x4K(vKYfxzx6 '/QB@C` qӢm5nD2sCХwV@O]]YY<)#b6Cze-!8n۵L0j}mAB@>ׂ*4O7؋4:|\H}dYc cd^8Hי?)%tZIvPB)R~c.M8:KLO8i? 踇Ų+bZ]CPK!HQ-ˏtoodledo-1.0.0.dist-info/RECORDuIs@}~ $ x E ( 4C٤&u_s.\3JӪH~v/jf n:|T̢u_ѧ7mL9o0;SKyfzb5P3M<$-\P3Rv-rtZ 9)P[Fe?-Vid/Xys/ٙnrks&vQOJ57iY<.amخۺ錷PtZZiopaz3쯈K*X345*RQ$Pjx 7.w $*q}FÂrsѼ/d3 ucNA"VPrCvA J5j=$7ŝ3TQH6 A:qWmjl+W}%4da22iE[Y|pp\uji#Ȕ$p:\{qoeFBLݩͭCN tz&Z7p4