-
Notifications
You must be signed in to change notification settings - Fork 147
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add support for tracking Data Issues via Asana
Signed-off-by: Nathan Lawrence <nathanlawrence@asana.com>
- Loading branch information
1 parent
6af0e13
commit 11bccd0
Showing
9 changed files
with
285 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
amundsen_application/proxy/issue_tracker_clients/asana_client.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
# Copyright Contributors to the Amundsen project. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
import asana | ||
import logging | ||
from typing import Dict, List | ||
|
||
from amundsen_application.base.base_issue_tracker_client import BaseIssueTrackerClient | ||
from amundsen_application.models.data_issue import DataIssue, Priority | ||
from amundsen_application.models.issue_results import IssueResults | ||
|
||
|
||
class AsanaClient(BaseIssueTrackerClient): | ||
|
||
def __init__(self, issue_labels: List[str], | ||
issue_tracker_url: str, | ||
issue_tracker_user: str, | ||
issue_tracker_password: str, | ||
issue_tracker_project_id: int, | ||
issue_tracker_max_results: int) -> None: | ||
self.issue_labels = issue_labels | ||
self.asana_url = issue_tracker_url | ||
self.asana_user = issue_tracker_user | ||
self.asana_password = issue_tracker_password | ||
self.asana_max_results = issue_tracker_max_results | ||
|
||
self.asana_project_gid = issue_tracker_project_id | ||
self.asana_client = asana.Client.access_token(issue_tracker_password) | ||
|
||
asana_project = self.asana_client.projects.get_project(self.asana_project_gid) | ||
self.asana_workspace_gid = asana_project['workspace']['gid'] | ||
|
||
self._setup_custom_fields() | ||
|
||
def get_issues(self, table_uri: str) -> IssueResults: | ||
""" | ||
:param table_uri: Table Uri ie databasetype://database/table | ||
:return: Metadata of matching issues | ||
""" | ||
|
||
table_parent_task_gid = self._get_parent_task_gid_for_table_uri(table_uri) | ||
|
||
tasks = list(self.asana_client.tasks.get_subtasks_for_task( | ||
table_parent_task_gid, | ||
{ | ||
'opt_fields': [ | ||
'name', 'completed', 'notes', 'custom_fields', | ||
] | ||
} | ||
)) | ||
|
||
return IssueResults( | ||
issues=[ | ||
self._asana_task_to_amundsen_data_issue(task) for task in tasks | ||
], | ||
total=len(tasks), | ||
all_issues_url=self._task_url(table_parent_task_gid), | ||
) | ||
|
||
def create_issue(self, table_uri: str, title: str, description: str) -> DataIssue: | ||
""" | ||
Creates an issue in Jira | ||
:param description: Description of the Jira issue | ||
:param table_uri: Table Uri ie databasetype://database/table | ||
:param title: Title of the Jira ticket | ||
:return: Metadata about the newly created issue | ||
""" | ||
|
||
table_parent_task_gid = self._get_parent_task_gid_for_table_uri(table_uri) | ||
|
||
return self._asana_task_to_amundsen_data_issue( | ||
self.asana_client.tasks.create_subtask_for_task( | ||
table_parent_task_gid, | ||
{ | ||
'name': title, | ||
'notes': description, | ||
} | ||
) | ||
) | ||
|
||
def _setup_custom_fields(self) -> None: | ||
TABLE_URI_FIELD_NAME = 'Table URI (Amundsen)' | ||
PRIORITY_FIELD_NAME = 'Priority (Amundsen)' | ||
|
||
custom_fields = \ | ||
self.asana_client.custom_field_settings.get_custom_field_settings_for_project( | ||
self.asana_project_gid | ||
) | ||
|
||
custom_fields = {f['custom_field']['name']: f['custom_field'] for f in custom_fields} | ||
|
||
if TABLE_URI_FIELD_NAME in custom_fields: | ||
table_uri_field = custom_fields[TABLE_URI_FIELD_NAME] | ||
else: | ||
table_uri_field = self.asana_client.custom_fields.create_custom_field({ | ||
'workspace': self.asana_workspace_gid, | ||
'name': TABLE_URI_FIELD_NAME, | ||
'format': 'custom', | ||
'resource_subtype': 'text', | ||
}) | ||
|
||
self.asana_client.projects.add_custom_field_setting_for_project( | ||
self.asana_project_gid, | ||
{ | ||
'custom_field': table_uri_field['gid'], | ||
'is_important': True, | ||
} | ||
) | ||
|
||
if PRIORITY_FIELD_NAME in custom_fields: | ||
priority_field = custom_fields[PRIORITY_FIELD_NAME] | ||
else: | ||
priority_field = self.asana_client.custom_fields.create_custom_field({ | ||
'workspace': self.asana_workspace_gid, | ||
'name': PRIORITY_FIELD_NAME, | ||
'format': 'custom', | ||
'resource_subtype': 'enum', | ||
'enum_options': [ | ||
{ | ||
'name': p.level | ||
} for p in Priority | ||
] | ||
}) | ||
|
||
self.asana_client.projects.add_custom_field_setting_for_project( | ||
self.asana_project_gid, | ||
{ | ||
'custom_field': priority_field['gid'], | ||
'is_important': True, | ||
} | ||
) | ||
|
||
self.table_uri_field_gid = table_uri_field['gid'] | ||
self.priority_field_gid = priority_field['gid'] | ||
|
||
def _get_parent_task_gid_for_table_uri(self, table_uri: str) -> str: | ||
table_parent_tasks = list(self.asana_client.tasks.search_tasks_for_workspace( | ||
self.asana_workspace_gid, | ||
{ | ||
'projects.any': [self.asana_project_gid], | ||
'custom_fields.{}.value'.format(self.table_uri_field_gid): table_uri, | ||
} | ||
)) | ||
|
||
# Create the parent task if it doesn't exist. | ||
if len(table_parent_tasks) == 0: | ||
table_parent_task = self.asana_client.tasks.create_task({ | ||
'name': table_uri, | ||
'custom_fields': { | ||
self.table_uri_field_gid: table_uri, | ||
}, | ||
'projects': [self.asana_project_gid], | ||
}) | ||
|
||
return table_parent_task['gid'] | ||
else: | ||
if len(table_parent_tasks) > 1: | ||
logging.warn('There are currently two tasks with the name "{}"'.format(table_uri)) | ||
|
||
return table_parent_tasks[0]['gid'] | ||
|
||
def _task_url(self, task_gid: str) -> str: | ||
return 'https://app.asana.com/0/{project_gid}/{task_gid}'.format( | ||
project_gid=self.asana_project_gid, task_gid=task_gid | ||
) | ||
|
||
def _asana_task_to_amundsen_data_issue(self, task: Dict) -> DataIssue: | ||
custom_fields = {f['gid']: f for f in task['custom_fields']} | ||
priority_field = custom_fields[self.priority_field_gid] | ||
|
||
priority = None | ||
if priority_field.get('enum_value'): | ||
priority = Priority.from_level(priority_field['enum_value']['name']) | ||
else: | ||
priority = Priority.P3 | ||
|
||
return DataIssue( | ||
issue_key=task['gid'], | ||
title=task['name'], | ||
url=self._task_url(task['gid']), | ||
status='closed' if task['completed'] else 'open', | ||
priority=priority, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# Copyright Contributors to the Amundsen project. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
from unittest.mock import Mock | ||
|
||
import flask | ||
import unittest | ||
from amundsen_application.proxy.issue_tracker_clients.issue_exceptions import IssueConfigurationException | ||
from amundsen_application.proxy.issue_tracker_clients.asana_client import AsanaClient | ||
from amundsen_application.models.data_issue import DataIssue, Priority | ||
|
||
app = flask.Flask(__name__) | ||
app.config.from_object('amundsen_application.config.TestConfig') | ||
|
||
|
||
class AsanaClientTest(unittest.TestCase): | ||
|
||
def setUp(self) -> None: | ||
self.mock_issue_instance = DataIssue(issue_key='key', | ||
title='some title', | ||
url='http://somewhere', | ||
status='open', | ||
priority=Priority.P2) | ||
|
||
@unittest.mock.patch('amundsen_application.proxy.issue_tracker_clients.asana_client.asana.Client') | ||
def test_create_AsanaClient_validates_config(self, mock_asana_client: Mock) -> None: | ||
with app.test_request_context(): | ||
try: | ||
AsanaClient( | ||
issue_labels=[], | ||
issue_tracker_url='', | ||
issue_tracker_user='', | ||
issue_tracker_password='', | ||
issue_tracker_project_id=-1, | ||
issue_tracker_max_results=-1) | ||
|
||
except IssueConfigurationException as e: | ||
self.assertTrue(type(e), type(IssueConfigurationException)) | ||
self.assertTrue(e, 'The following config settings must be set for Asana: ' | ||
'ISSUE_TRACKER_URL, ISSUE_TRACKER_USER, ISSUE_TRACKER_PASSWORD, ' | ||
'ISSUE_TRACKER_PROJECT_ID') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.