Skip to content

Commit

Permalink
- refactoring, add filter prefix, always display a current filter ent…
Browse files Browse the repository at this point in the history
…ry at the top

- remove lib from gitignore
  • Loading branch information
b3rnhard authored and mx-bernhard committed Sep 2, 2019
1 parent b708a2d commit 56bef6a
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 305 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
Expand Down
64 changes: 50 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,70 @@
```ini
[server/my-server1]

# enable = true
# youtrack base url
base_url = https://youtrack.jetbrains.com
#base_url = https://youtrack.myserver1.com

# the token for auth - is required and starts with perm:
api_token = perm:...
#api_token =

filter_label = YouTrack Jetbrains (filter)
issues_label = YouTrack Jetbrains (issues)
# displayed entry text
#issues_label = My bugtracker (issues)

issues_icon = youtrack
filter_icon = youtrack
# defaults to "youtrack"
#issues_icon = youtrack

# displayed entry text
#filter_label = My bugtracker (filter)

# defaults to "youtrack"
#filter_icon = youtrack

# is prefixed to the entered filter
# Note: you can add the same server twice but with a different filter
#filter =

# disables the automatic whitespace added after the prefix filter, defaults to False
#filter_dont_append_whitespace=False

[server/my-server2]

# enable = true
# youtrack base url
base_url = https://youtrack-server.foo.com
#base_url = https://youtrack.myserver2.com

# the token for auth - is required and starts with perm:
api_token = perm:...
#api_token =

# displayed entry text
#issues_label = My bugtracker (issues)

# defaults to "youtrack"
#issues_icon = youtrack

filter_label = YouTrack Foo (filter)
issues_label = YouTrack Foo (issues)
# displayed entry text
#filter_label = My bugtracker (filter)

issues_icon = youtrack
filter_icon = youtrack
# defaults to "youtrack"
#filter_icon = youtrack

# is prefixed to the entered filter
# Note: you can add the same server twice but with a different filter
#filter =

# disables the automatic whitespace added after the prefix filter, defaults to False
#filter_dont_append_whitespace=False
```

* You can add the same server more than once but use different `filter` values that are prefixed to all queries.
* A space is added to the end of the prefix before the user input so that suggestions do not target the prefix
* Put your png icons in a subfolder youtrack and prefix them with `icon_` - in the example below ´test´ and ´xyz´ are valid identifiers in the ´youtrack.ini´:
```
+
|– youtrack.ini
|– youtrack/
|– icon_test.png
|– icon_xyz.png
```

## Features

### Filter mode
Expand Down
Empty file added __init__.py
Empty file.
132 changes: 132 additions & 0 deletions lib/api.py
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

18 changes: 18 additions & 0 deletions lib/util.py
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
55 changes: 55 additions & 0 deletions tests/intellisense_result.xml
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>&amp;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>&amp;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>
7 changes: 7 additions & 0 deletions tests/keypirinha_api.py
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
30 changes: 30 additions & 0 deletions tests/youtrack_tests.py
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
Loading

0 comments on commit 56bef6a

Please sign in to comment.