diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..dc7b715 --- /dev/null +++ b/auth.py @@ -0,0 +1,61 @@ +import json +import os + +import asana +from asana.session import AsanaOAuth2Session + +from secrets import CLIENT_ID, CLIENT_SECRET + +class Auth: + def __init__(self): + try: + f = open(".oauth", "r") + token = json.loads(f.readline()) + f.close() + self.client = asana.Client.oauth( + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + token=token, + token_updater=self.saveToken, + auto_refresh_url=AsanaOAuth2Session.token_url, + auto_refresh_kwargs={ + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET + }, + ) + except IOError: + self.getToken() + + def saveToken(self, token): + f = open('.oauth', 'w') + f.write(json.dumps(token)) + f.close() + os.chmod('.oauth', 0o600) + + def getToken(self): + self.client = asana.Client.oauth( + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + redirect_uri='urn:ietf:wg:oauth:2.0:oob', + token_updater=self.saveToken, + auto_refresh_url=AsanaOAuth2Session.token_url, + auto_refresh_kwargs={ + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET + }, + ) + (url, state) = self.client.session.authorization_url() + print("Go to the following link and enter the code:") + print(url) + try: + import webbrowser + webbrowser.open(url) + except Exception: + pass + + code = sys.stdin.readline().strip() + token = self.client.session.fetch_token(code=code) + self.saveToken(token) + + def getClient(self): + return self.client diff --git a/cmdasana.py b/cmdasana.py index e88c8cd..6d14060 100755 --- a/cmdasana.py +++ b/cmdasana.py @@ -1,74 +1,23 @@ #!/usr/bin/env python # -*- coding: latin-1 -*- +import json import os import sys -import json from threading import Thread import urwid import asana -from asana.session import AsanaOAuth2Session -from oauthlib.oauth2.rfc6749.errors import TokenExpiredError import ui -from secrets import CLIENT_ID, CLIENT_SECRET - -# id of the personal projects domain -PERSONAL = 498346170860 +from auth import Auth +from data import Data class CmdAsana: loop = None def __init__(self): - try: - f = open(".oauth", "r") - token = json.loads(f.readline()) - f.close() - self.client = asana.Client.oauth( - client_id=CLIENT_ID, - client_secret=CLIENT_SECRET, - token=token, - token_updater=self.saveToken, - auto_refresh_url=AsanaOAuth2Session.token_url, - auto_refresh_kwargs={ - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET - }, - ) - except IOError: - self.getToken() - - self.me = self.client.users.me() - - def saveToken(self, token): - f = open('.oauth', 'w') - f.write(json.dumps(token)) - f.close() - - def getToken(self): - self.client = asana.Client.oauth( - client_id=CLIENT_ID, - client_secret=CLIENT_SECRET, - redirect_uri='urn:ietf:wg:oauth:2.0:oob', - token_updater=self.saveToken, - auto_refresh_url=AsanaOAuth2Session.token_url, - auto_refresh_kwargs={ - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET - }, - ) - (url, state) = self.client.session.authorization_url() - print("Go to the following link and enter the code:") - print(url) - try: - import webbrowser - webbrowser.open(url) - except Exception: - pass - - code = sys.stdin.readline().strip() - token = self.client.session.fetch_token(code=code) - self.saveToken(token) + auth = Auth() + self.data = Data(auth.getClient()) def saveState(self): f = open('.state', 'w') @@ -88,44 +37,17 @@ class CmdAsana: 'workspace_id': workspace_id } - def myWorkspaces(self): - return self.me['workspaces'] - def allMyTasks(self, workspace_id): - return self.client.tasks.find_all(params={ - 'assignee': self.me['id'], - 'workspace': workspace_id, - 'completed_since': 'now' - }) - - def allMyProjects(self): - if self.workspace_id != PERSONAL: - return self.client.projects.find_by_workspace(self.workspace_id) - else: - return self.client.projects.find_by_workspace(self.workspace_id, - page_size=None) - - def projectTasks(self, project_id): - return self.client.tasks.find_by_project(project_id, params={ - 'completed_since': 'now' - }) - - def completeTask(self, task_id): - self.client.tasks.update(task_id, completed=True) - - def newTask(self, task_after_id): + def newTask(self, task_after_id): def runInThread(): if self.state['view'] == 'project': - task = self.client.tasks.create_in_workspace( + task = self.data.createTask( self.state['workspace_id'], - projects=[self.state['id']] + opt_projects=[self.state['id']], + opt_after=task_after_id ) - if task_after_id != None: - self.client.tasks.add_project(task['id'], - project=self.state['id'], - insert_after=task_after_id) else: - task = self.client.tasks.create_in_workspace( + task = self.data.createTask( self.state['workspace_id'], assignee=self.me['id'] ) @@ -140,49 +62,29 @@ class CmdAsana: thread = Thread(target=runInThread) thread.start() - def updateTask(self, task_id, name): + def updateTask(self, task_id, opt_name=None, opt_projects=None, + opt_assignee=None, opt_notes=None): def runInThread(): - self.client.tasks.update(task_id, name=name) - - thread = Thread(target=runInThread) - thread.start() - - def updateDescription(self, task_id, description): - def runInThread(): - self.client.tasks.update(task_id, notes=description) - - thread = Thread(target=runInThread) - thread.start() - - def assignTask(self, task_id, user_id): - def runInThread(): - self.client.tasks.update(task_id, assignee=user_id) + self.data.updateTask(task_id, + opt_name=opt_name, + opt_projects=opt_projects, + opt_assignee=opt_assignee, + opt_notes=opt_notes) thread = Thread(target=runInThread) thread.start() - + def addComment(self, task_id, comment): def runInThread(): - self.client.stories.create_on_task(task_id, {"text": comment}) + self.data.addComment(task_id, comment) self.showDetails(task_id, show_loading=False) - + thread = Thread(target=runInThread) thread.start() def userTypeAhead(self, text, callback): def runInThread(): - if self.state['workspace_id'] != PERSONAL: - users = self.client.workspaces \ - .typeahead(self.state['workspace_id'], - { - 'type': 'user', - 'query': text, - 'count': 5 - }) - else: - users = [self.me] - - callback(users) + callback(self.data.userTypeahead(self.state['workspace_id'], text)) self.loop.draw_screen() thread = Thread(target=runInThread) @@ -209,7 +111,7 @@ class CmdAsana: self.showMainLoading() def runInThread(): - tasks = self.allMyTasks(workspace_id) + tasks = self.data.myTasks(workspace_id) update(tasks) def update(tasks): @@ -229,7 +131,7 @@ class CmdAsana: self.showMainLoading() def runInThread(): - tasks = self.projectTasks(project_id) + tasks = self.data.tasksInProject(project_id) update(tasks) def update(tasks): @@ -249,14 +151,14 @@ class CmdAsana: self.showMainLoading() def runInThread(): - projects = self.allMyProjects() + projects = self.data.allMyProjects() update(projects) def update(projects): project_list = ui.ProjectList(projects) urwid.connect_signal(project_list, 'loadproject', self.showProject) self.replaceBody(project_list) - + thread = Thread(target=runInThread) thread.start() @@ -268,9 +170,7 @@ class CmdAsana: self.showMainLoading() def runInThread(): - task = self.client.tasks.find_by_id(task_id) - stories = self.client.stories.find_by_task(task_id) - subtasks = self.client.tasks.subtasks(task_id) + task, stories, subtasks = self.data.getTask(task_id) update(task, stories, subtasks) def update(task, stories, subtasks): @@ -355,7 +255,7 @@ class CmdAsana: urwid.set_encoding("UTF-8") self.registerSignals() - workspace_menu = ui.WorkspaceMenu(self.myWorkspaces()) + workspace_menu = ui.WorkspaceMenu(self.data.myWorkspaces()) urwid.connect_signal(workspace_menu, 'click', self.showProjectList) self.frame = urwid.Pile([ diff --git a/data.py b/data.py new file mode 100644 index 0000000..5eee75e --- /dev/null +++ b/data.py @@ -0,0 +1,89 @@ +import asana + +# id of the personal projects domain +PERSONAL = 498346170860 + +class Data: + def __init__(self, client): + self.client = client + self.me = self.client.users.me() + + def me(self): + return self.me + + def myWorkspaces(self): + return self.me['workspaces'] + + def myTasks(self, workspace_id): + return self.client.tasks.find_all(params={ + 'assignee': self.me['id'], + 'workspace': workspace_id, + 'completed_since': 'now' + }) + + def allMyProjects(self): + if self.workspace_id != PERSONAL: + return self.client.projects.find_by_workspace(self.workspace_id) + else: + return self.client.projects.find_by_workspace(self.workspace_id, + page_size=None) + + def tasksInProject(self, project_id): + return self.client.tasks.find_by_project(project_id, params={ + 'completed_since': 'now' + }) + + def getTask(self, task_id): + task = self.client.tasks.find_by_id(task_id, params={ + 'opt_fields': ['name', + 'assignee.name', + 'due_date', + 'notes', + 'parent', + 'projects.name', + 'subtasks.name' + ] + }) + + stories = self.client.stories.find_by_task(task_id) + return (task, stories, task['subtasks']) + + def completeTask(self, task_id): + self.client.tasks.update(task_id, completed=True) + + def createTask(self, workspace, opt_projects=[], opt_after=None, + opt_assignee=None, opt_name=None): + task = self.client.tasks.create_in_workspace(workspace, + projects=opt_projects, + assignee=opt_assignee, + name=opt_name) + if opt_after and len(opt_projects) > 0: + self.client.tasks.add_project(task['id'], + project=opt_projects[0], + insert_after=opt_after) + + return task + + def updateTask(self, task_id, opt_name=None, opt_projects=None, + opt_assignee=None, opt_notes=None): + return self.client.tasks.update(task_id, + name=opt_name, + projects=opt_projects, + assignee=opt_assignee, + notes=opt_notes) + + def addComment(self, task_id, comment): + return self.client.stories.create_on_task(task_id, {"text": comment}) + + def _typeahead(self, workspace, asana_type, query, count=5): + return self.client.workspaces.typeahead(workspace, { + 'type': asana_type, + 'query': query, + 'count': count + }) + + def userTypeahead(self, workspace, query, count): + return self._typeahead(workspace, 'user', query, count) + + def projectTypeahead(self, workspace, query, opt_count): + return self._typeahead(workspace, 'project', query, count=opt_count)