From 3c17cd3643faccc1c988a31bdbe191db5098dffc Mon Sep 17 00:00:00 2001
From: WilsonCodeSpace <123996129+WilsonCodeSpace@users.noreply.github.com>
Date: Sun, 26 Mar 2023 22:49:14 -0500
Subject: [PATCH 01/10] Azure DevOps Work Item Integration
Azure DevOps Work Item integration. It adds the ability to index Azure DevOps tasks that are returned by a Query Result.
---
.gitignore | 3 +-
app/data_sources/azuredevops.py | 117 +++++++++++++++++++
app/requirements.txt | 1 +
app/static/data_source_icons/azuredevops.png | Bin 0 -> 9962 bytes
ui/src/components/data-source-panel.tsx | 15 +++
5 files changed, 135 insertions(+), 1 deletion(-)
create mode 100644 app/data_sources/azuredevops.py
create mode 100644 app/static/data_source_icons/azuredevops.png
diff --git a/.gitignore b/.gitignore
index 8eac626..cf7b8aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,5 @@ venv
.*.sw?
-.env
\ No newline at end of file
+.env
+.DS_Store
diff --git a/app/data_sources/azuredevops.py b/app/data_sources/azuredevops.py
new file mode 100644
index 0000000..da1c42f
--- /dev/null
+++ b/app/data_sources/azuredevops.py
@@ -0,0 +1,117 @@
+import datetime
+import logging
+from typing import Dict, List
+import requests
+import base64
+import urllib.parse
+
+from azure.devops.connection import Connection
+from msrest.authentication import BasicAuthentication
+
+from data_source_api.base_data_source import BaseDataSource, ConfigField, HTMLInputType
+from data_source_api.basic_document import DocumentType, BasicDocument
+from data_source_api.exception import InvalidDataSourceConfig
+from index_queue import IndexQueue
+from pydantic import BaseModel
+from parsers.html import html_to_text
+from data_source_api.utils import parse_with_workers
+
+logger = logging.getLogger(__name__)
+
+class DevOpsConfig(BaseModel):
+ organization_url: str
+ access_token: str
+ project_name: str
+ query_id: str
+
+class AzuredevopsDataSource(BaseDataSource):
+ @staticmethod
+ def get_config_fields() -> List[ConfigField]:
+ return [
+ ConfigField(label="AzureDevOps organization URL", placeholder="https://dev.azure.com/org", name="organization_url"),
+ ConfigField(label="Personal Access Token", name="access_token", type=HTMLInputType.PASSWORD),
+ ConfigField(label="Project Name", name="project_name"),
+ ConfigField(label="Query ID", name="query_id"),
+ ]
+
+ @staticmethod
+ def validate_config(config: Dict) -> None:
+ try:
+ devops_config = DevOpsConfig(**config)
+ credentials = BasicAuthentication('', devops_config.access_token)
+ connection = Connection(base_url=devops_config.organization_url, creds=credentials)
+ core_client = connection.clients.get_core_client()
+ core_client.get_projects()
+ except Exception as e:
+ raise InvalidDataSourceConfig from e
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ devops_config = DevOpsConfig(**self._config)
+ self._query_id = devops_config.query_id.strip()
+ self._access_token = devops_config.access_token.strip()
+ self._project_name = devops_config.project_name.strip()
+ self._organization_url = devops_config.organization_url.strip()
+ credentials = BasicAuthentication('', self._access_token)
+ connection = Connection(base_url=self._organization_url, creds=credentials)
+ self._work_item_tracking_client = connection.clients.get_work_item_tracking_client()
+
+ def _parse_documents_worker(self, raw_docs: List[Dict]):
+ logging.info(f'Worker parsing {len(raw_docs)} documents')
+
+ parsed_docs = []
+ total_fed = 0
+ for item in raw_docs:
+ for raw_page in item['comments']:
+ create_date = datetime.datetime.strptime(raw_page['createdDate'], "%Y-%m-%dT%H:%M:%S.%fZ")
+ if create_date < self._last_index_time:
+ continue
+ author = raw_page['createdBy']['displayName']
+ workitem_id = raw_page['workItemId']
+ title = str(raw_page['workItemId']) + ' - ' + raw_page['createdBy']['displayName']
+ html_content = raw_page['text']
+ plain_text = html_to_text(html_content)
+ author_image_url = raw_page['createdBy']['_links']['avatar']['href']
+ url = f"{self._organization_url}/{urllib.parse.quote(self._project_name)}/_workitems/edit/{raw_page['workItemId']}".strip()
+
+ parsed_docs.append(BasicDocument(
+ id=workitem_id,
+ data_source_id=self._data_source_id,
+ author=author,
+ author_image_url=author_image_url,
+ content=plain_text,
+ type=DocumentType.COMMENT,
+ title=title,
+ timestamp=create_date,
+ location=self._project_name,
+ url=url
+ ))
+
+ if len(parsed_docs) >= 50:
+ total_fed += len(parsed_docs)
+ IndexQueue.get_instance().put(docs=parsed_docs)
+ parsed_docs = []
+
+ IndexQueue.get_instance().put(docs=parsed_docs)
+ total_fed += len(parsed_docs)
+ if total_fed > 0:
+ logging.info(f'Worker fed {total_fed} documents')
+
+
+ def _list_work_item_comments(self, work_item_url) -> List[Dict]:
+ authorization = str(base64.b64encode(bytes(':'+self._access_token, 'ascii')), 'ascii')
+ headers = {
+ 'Accept': 'application/json',
+ 'Authorization': 'Basic '+authorization
+ }
+ return requests.get(url=work_item_url + '/comments', headers=headers).json()
+
+ def _feed_new_documents(self) -> None:
+ logger.info('Feeding new Azure DevOps Work Items')
+ raw_docs = []
+ work_item_results = self._work_item_tracking_client.query_by_id(self._query_id)
+ for work_item in work_item_results.work_items:
+ result = self._list_work_item_comments(work_item.url)
+ if result['totalCount'] > 0:
+ raw_docs.append(result)
+ parse_with_workers(self._parse_documents_worker, raw_docs)
\ No newline at end of file
diff --git a/app/requirements.txt b/app/requirements.txt
index 08c5b93..8ff72b2 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -22,3 +22,4 @@ python-pptx
alembic
rocketchat-API
mattermostdriver
+azure-devops
diff --git a/app/static/data_source_icons/azuredevops.png b/app/static/data_source_icons/azuredevops.png
new file mode 100644
index 0000000000000000000000000000000000000000..504f338dbe5e472ddd5444ec58d3fbb346412693
GIT binary patch
literal 9962
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 that the url must begin with either http:// or https://
)}
+
+ {
+ this.state.selectedDataSource.value === 'azuredevops' && (
+
+ 1. {'Go to your Azure DevOps -> top-right Person with the Gear -> Personal Access Token'}
+ 2. {'Fill out the details, and set your expiration date'}
+ 3. {"Make sure Read under Work Items is checked and save your token"}
+ 4. {"Copy the token and paste it here"}
+ 5. {"Add your Project Name here"}
+ 6. {"Navigate to Your Project then Boards > Queries"}
+ 7. {"Select the query that you want from the list and get the id from the end of the URL"}
+ ex. {"https://dev.azure.com/{org}/{project}/_queries/query/{query_id}/"}
+
+ )
+ }