-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- refactoring, add filter prefix, always display a current filter ent…
…ry at the top - remove lib from gitignore
- Loading branch information
1 parent
b708a2d
commit 56bef6a
Showing
11 changed files
with
530 additions
and
305 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,6 @@ dist/ | |
downloads/ | ||
eggs/ | ||
.eggs/ | ||
lib/ | ||
lib64/ | ||
parts/ | ||
sdist/ | ||
|
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
Empty file.
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,132 @@ | ||
from typing import Sequence, Callable, Union | ||
from urllib import parse, request | ||
from xml.dom import minidom | ||
from xml.dom.minidom import Element | ||
|
||
from .util import get_as_xml, get_value, get_child_att_value | ||
|
||
class IntellisenseResult(object): | ||
def __init__(self, full_option: str, prefix: Union[str, None], suffix: Union[str,None], option: str, start: int, end: int, description: str): | ||
self.description = description | ||
self.end = end | ||
self.start = start | ||
self.option = option | ||
self.suffix = suffix | ||
self.prefix = prefix | ||
self.full_option = full_option | ||
|
||
class Issue(object): | ||
def __init__(self, id: str, summary: str, description: str, url: str): | ||
self.url = url | ||
self.id = id | ||
self.summary = summary | ||
self.description = description | ||
|
||
class Api(): | ||
AUTH_HEADER: str = 'Authorization' | ||
TOKEN_PREFIX: str = 'Bearer ' | ||
YOUTRACK_INTELLISENSE_ISSUE_API: str = '{base_url}/rest/issue/intellisense/?' | ||
YOUTRACK_LIST_OF_ISSUES_API: str = '{base_url}/rest/issue?' | ||
YOUTRACK_ISSUE: str = '{base_url}/issue/{id}' | ||
YOUTRACK_ISSUES: str = '{base_url}/issues/?' | ||
|
||
def __init__(self, api_token: str, youtrack_url: str, dbg): | ||
super().__init__() | ||
self.dbg = dbg | ||
self.api_token = api_token | ||
self.youtrack_url = youtrack_url | ||
|
||
def open_url(self, http_url) -> str: | ||
req = request.Request(http_url) | ||
req.add_header(self.AUTH_HEADER, self.TOKEN_PREFIX + self.api_token) | ||
|
||
with request.urlopen(req) as resp: | ||
content = resp.read() | ||
return content | ||
|
||
def print(self, **kwargs): | ||
toPrint = str.join(",", [key + " = \"" + str(value) + "\"" for key, value in kwargs.items()]) | ||
self.dbg("[" + toPrint + "]") | ||
|
||
def create_issues_url(self, filter): | ||
return self.YOUTRACK_ISSUES.format(base_url=self.youtrack_url) + parse.urlencode({'q': filter}) | ||
|
||
def create_issue_url(self, id): | ||
return self.YOUTRACK_ISSUE.format(base_url=self.youtrack_url, id=id) | ||
|
||
def get_intellisense_suggestions(self, actual_user_input: str) -> Sequence[IntellisenseResult]: | ||
""" | ||
There is no non-legacy yet (YouTrack 2019.2) but already announced that it will be discontinued | ||
once everything has been published under the new api. | ||
""" | ||
requestUrl = self.YOUTRACK_INTELLISENSE_ISSUE_API.format(base_url=self.youtrack_url) | ||
filter = parse.urlencode({'filter': actual_user_input}) | ||
requestUrl = requestUrl + filter | ||
self.print(requesturl=requestUrl) | ||
content: bytes = self.open_url(requestUrl) | ||
api_result_suggestions = self.parse_intellisense_suggestions(content) | ||
return api_result_suggestions | ||
|
||
def parse_intellisense_suggestions(self, response: bytes) -> Sequence[IntellisenseResult]: | ||
dom = get_as_xml(response) | ||
if (dom.documentElement.nodeName != 'IntelliSense'): return [] | ||
items = [itemOrRecentItem | ||
for suggestOrRecent in dom.documentElement.childNodes | ||
for itemOrRecentItem in suggestOrRecent.childNodes | ||
if isinstance(itemOrRecentItem, Element) and itemOrRecentItem.nodeName in ['item', 'recentItem']] | ||
result = [] | ||
|
||
for item in items: | ||
prefix: str = get_value(item, 'prefix') | ||
suffix: str = get_value(item, 'suffix') | ||
option: str = get_value(item, 'option') | ||
description: str = get_value(item, 'description') | ||
start: int = int(get_child_att_value(item, 'completion', 'start')) | ||
end: int = int(get_child_att_value(item, 'completion', 'end')) | ||
if option is None: continue | ||
res = str.join('', (item for item in [prefix, option, suffix] if item is not None)) | ||
intelliRes = IntellisenseResult( | ||
full_option=res, | ||
prefix=prefix, | ||
suffix=suffix, | ||
option=option, | ||
start=start, | ||
end=end, | ||
description=description) | ||
result.append(intelliRes) | ||
return result | ||
|
||
def parse_list_of_issues_result(self, response: str) -> Sequence[Issue]: | ||
dom = get_as_xml(response) | ||
if (dom.documentElement.nodeName != 'issueCompacts'): return [] | ||
items = [issue for issue in dom.documentElement.childNodes | ||
if isinstance(issue, minidom.Element) and issue.nodeName == 'issue'] | ||
issues: Sequence[Issue] = [] | ||
for item in items: | ||
self.print(item=str(item)) | ||
id = item.getAttribute('id') | ||
description = self.extract_field_value('description', "", item) | ||
summary: str = self.extract_field_value('summary', "--no summary--", item) | ||
issue = Issue(id=id, summary=summary, description=description, url=self.create_issue_url(id)) | ||
issues.append(issue) | ||
self.print(id=id, summary=summary,url=issue.url) | ||
return issues | ||
|
||
|
||
def extract_field_value(self, field_name: str, fallback: str, item) -> str: | ||
return next((get_value(fieldNode, "value") | ||
for fieldNode in item.childNodes | ||
if isinstance(fieldNode, minidom.Element) and fieldNode.nodeName == 'field' and fieldNode.getAttribute( | ||
'name') == field_name), | ||
fallback) | ||
|
||
def get_issues_matching_filter(self, actual_user_input: str) -> Sequence[Issue]: | ||
requestUrl: str = self.YOUTRACK_LIST_OF_ISSUES_API.format(base_url=self.youtrack_url) | ||
filter: str = parse.urlencode({'filter': actual_user_input}) | ||
requestUrl = requestUrl + filter | ||
self.print(requesturl=requestUrl) | ||
content: str = self.open_url(requestUrl) | ||
self.dbg("parsing issues result") | ||
issues = self.parse_list_of_issues_result(content) | ||
return issues | ||
|
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,18 @@ | ||
from xml.dom import minidom | ||
|
||
|
||
def get_as_xml(response: bytes): | ||
response = response.decode(encoding="utf-8", errors="strict") | ||
dom = minidom.parseString(response) | ||
return dom | ||
|
||
def get_value(node, node_name): | ||
return next((child.childNodes[0].nodeValue for child in node.childNodes if child.nodeName == node_name), None) | ||
|
||
def get_child_att_value(node, node_name, att_name): | ||
return next((child.getAttribute(att_name) for child in node.childNodes if child.nodeName == node_name), None) | ||
|
||
class AttrDict(dict): | ||
def __init__(self, *args, **kwargs): | ||
super(AttrDict, self).__init__(*args, **kwargs) | ||
self.__dict__ = self |
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,55 @@ | ||
<IntelliSense> | ||
<suggest> | ||
<item> | ||
<caret>9</caret> | ||
<completion start="0" end="2"/> | ||
<description>by updated</description> | ||
<match start="0" end="2"/> | ||
<option>updated</option> | ||
<styleClass>keyword</styleClass> | ||
<suffix>:</suffix> | ||
</item> | ||
<item> | ||
<caret>12</caret> | ||
<completion start="0" end="2"/> | ||
<description>by updater</description> | ||
<match start="0" end="2"/> | ||
<option>updated by</option> | ||
<styleClass>keyword</styleClass> | ||
<suffix>:</suffix> | ||
</item> | ||
</suggest> | ||
<recent> | ||
<recentItem> | ||
<caret>0</caret> | ||
<completion start="0" end="0"/> | ||
<description>Recent Searches</description> | ||
<match start="0" end="0"/> | ||
</recentItem> | ||
<recentItem> | ||
<caret>43</caret> | ||
<completion start="0" end="2"/> | ||
<description>&nbsp;</description> | ||
<htmlDescription>true</htmlDescription> | ||
<match start="22" end="24"/> | ||
<option>updated: Today</option> | ||
<styleClass>field</styleClass> | ||
</recentItem> | ||
<recentItem> | ||
<caret>18</caret> | ||
<completion start="0" end="2"/> | ||
<description>&nbsp;</description> | ||
<htmlDescription>true</htmlDescription> | ||
<match start="0" end="2"/> | ||
<option>updated: Yesterday</option> | ||
<styleClass>field</styleClass> | ||
</recentItem> | ||
</recent> | ||
<highlight> | ||
<range> | ||
<styleClass>string</styleClass> | ||
<start>0</start> | ||
<end>2</end> | ||
</range> | ||
</highlight> | ||
</IntelliSense> |
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,7 @@ | ||
def name(): '' | ||
def label(): '' | ||
def version_tuple(): '' | ||
|
||
class Plugin(object): | ||
def __new__(cls): | ||
pass |
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,30 @@ | ||
import os | ||
import unittest | ||
from typing import Sequence | ||
|
||
from lib.api import Api, IntellisenseResult | ||
|
||
TESTDATA_FILENAME = os.path.join(os.path.dirname(__file__), 'intellisense_result.xml') | ||
|
||
class TestApi: | ||
|
||
def setup_class(self): | ||
self.testdata = open(TESTDATA_FILENAME).read() | ||
|
||
def test_read_issues(self): | ||
dummyDbg = lambda x: None | ||
fixture = Api("no token", "https://foo.com", dummyDbg) | ||
#fixture.get_intellisense_suggestions("test") | ||
|
||
res: Sequence[IntellisenseResult] = fixture.parse_intellisense_suggestions(self.testdata.encode("UTF-8")) | ||
assert len(res) == 4 | ||
assert self.equals(res[0],IntellisenseResult(start=0,end=2,description="by updated",option="updated",full_option="updated:",prefix=None,suffix=":")) | ||
|
||
def equals(self, one:IntellisenseResult, two:IntellisenseResult): | ||
return one.description == two.description \ | ||
and one.suffix == two.suffix \ | ||
and one.end ==two.end \ | ||
and one.full_option == two.full_option \ | ||
and one.option==two.option \ | ||
and one.prefix==two.prefix \ | ||
and one.start==two.start |
Oops, something went wrong.