-
Notifications
You must be signed in to change notification settings - Fork 209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#770 HTTP support #777
base: develop
Are you sure you want to change the base?
#770 HTTP support #777
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
HTTP | ||
===== | ||
|
||
Receive tasks from HTTP services such as APIs. | ||
|
||
Additional Dependencies | ||
----------------------- | ||
|
||
Install packages needed for HTTP support with: | ||
|
||
.. code:: bash | ||
|
||
pip install bugwarrior[http] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There don't appear to be any extra dependencies required for this service. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true |
||
|
||
Example Service | ||
--------------- | ||
|
||
Here's an example of a HTTP target: | ||
|
||
:: | ||
|
||
[my_api] | ||
service = http | ||
http.url = "https://example.com/tasks" | ||
|
||
Example response from APIs | ||
-------------------------- | ||
|
||
In order to create a task correctly, the HTTP endpoint needs to return data in the following format: | ||
|
||
:: | ||
|
||
[ | ||
{ | ||
"tags": ["home", "garden"], | ||
"entry": "20200709T141933Z", | ||
"description": "Attempting to scare those annoying cats away", | ||
"uuid": "8ce52fe2-ec48-489d-ba00-c30f463fc422", | ||
"modified": "20200709T141933Z", | ||
"project": "AnnoyingCats" | ||
} | ||
] | ||
|
||
Possibly other attributes such as annotations or priority will be implemented later on. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think adding an explicit list of required keys to this section would be helpful. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true |
||
|
||
Other settings | ||
++++++++++++++ | ||
|
||
+--------------------------------+---------------------------------------------------------------------------------+ | ||
| ``http.method`` | HTTP method to use, such as GET or POST. | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why support methods other than GET? We're very rigid about the body format we accept so why be flexible about the http methd? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Body format is harder to make flexible, I would have done so if easily possible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But what other http method than |
||
+--------------------------------+---------------------------------------------------------------------------------+ | ||
| ``http.authorization_header`` | Header for authorization, useable for Bearer-tokens, Basic-Authentication, etc. | | ||
+--------------------------------+---------------------------------------------------------------------------------+ | ||
|
||
Provided UDA Fields | ||
------------------- | ||
|
||
+---------------------+-----------------------------------+---------------+ | ||
| ``http_uuid`` | UUID for task in service | Text (string) | | ||
+---------------------+-----------------------------------+---------------+ | ||
| ``http_url`` | URL used to retrieve task | Text (string) | | ||
+---------------------+-----------------------------------+---------------+ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import requests | ||
import logging | ||
from dateutil.parser import isoparse | ||
|
||
from bugwarrior.services import IssueService, Issue | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class HttpIssue(Issue): | ||
UUID = 'http_uuid' | ||
URL = 'http_url' | ||
|
||
UNIQUE_KEY = (UUID,) | ||
UDAS = { | ||
URL: { | ||
'type': 'string', | ||
'label': "API URL" | ||
}, | ||
UUID: { | ||
'type': 'string', | ||
'label': 'Virtual API UUID for task' | ||
} | ||
} | ||
|
||
def get_default_description(self): | ||
return self.record.get( | ||
'description', | ||
self.record.get( | ||
'uuid', | ||
self.extra.get('url') | ||
) | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like the description field is a required return value by the http service so why do we need these fallbacks? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true |
||
|
||
def to_taskwarrior(self): | ||
return { | ||
'entry': isoparse(self.record.get('entry')) if self.record.get('entry') else None, | ||
'tags': self.record.get('tags', []), | ||
'project': self.get_project(), | ||
'description': self.record.get('description'), | ||
'priority': self.get_priority(), | ||
|
||
self.UUID: self.record.get('uuid'), | ||
self.URL: self.extra.get('url') | ||
} | ||
|
||
|
||
class HttpService(IssueService): | ||
APPLICATION_NAME = 'Bugwarrior HTTP Service' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the APPLICATION_NAME attribute for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know, will remove |
||
|
||
ISSUE_CLASS = HttpIssue | ||
CONFIG_PREFIX = 'http' | ||
|
||
def __init__(self, *args, **kw): | ||
super(HttpService, self).__init__(*args, **kw) | ||
|
||
self.url = self.config.get('url') | ||
self.method = self.config.get('method', 'GET') | ||
self.authorization_header = self.config.get('authorization_header') | ||
|
||
def issues(self): | ||
return ( self.convert_to_issue(task) for task in self.request() ) | ||
|
||
def request(self): | ||
return requests.request( | ||
self.method, self.url, | ||
headers={ 'Authorization': self.authorization_header } | ||
).json() | ||
|
||
def convert_to_issue(self, task): | ||
issue = self.get_issue_for_record(task) | ||
|
||
issue.update_extra({ 'url': self.url }) | ||
|
||
return issue |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import os.path | ||
import pickle | ||
from copy import copy | ||
from datetime import datetime, timedelta | ||
from unittest import mock | ||
from unittest.mock import patch | ||
|
||
import responses | ||
|
||
from dateutil.tz import tzutc | ||
from six.moves import configparser | ||
|
||
import bugwarrior.services.http as http | ||
from bugwarrior.config import ServiceConfig | ||
from bugwarrior.services.http import HttpService | ||
|
||
from .base import AbstractServiceTest, ConfigTest, ServiceTest | ||
|
||
|
||
TEST_RESPONSE = [ | ||
{ | ||
"tags": ["home", "garden"], | ||
"entry": "20200709T141933Z", | ||
"description": "Attempting to scare those annoying cats away", | ||
"uuid": "8ce52fe2-ec48-489d-ba00-c30f463fc422", | ||
"modified": "20200709T141933Z", | ||
"project": "AnnoyingCats", | ||
"priority": "H" | ||
}, | ||
{ | ||
"description": "Minimum task", | ||
"uuid": "9ce52fe2-ec48-000d-ba00-c30f463fc422" | ||
} | ||
] | ||
|
||
MIN_TASK = { | ||
'priority': u'L', | ||
'project': 'IDK', | ||
'tags': ['add', 'tags'], | ||
"entry": None, | ||
'description': 'Minimum task', | ||
'http_uuid': '9ce52fe2-ec48-000d-ba00-c30f463fc422', | ||
'http_url': 'https://example.com/tasks' | ||
}; | ||
|
||
MAX_TASK = { | ||
'description': 'Attempting to scare those annoying cats away', | ||
'entry': datetime(2020, 7, 9, 14, 19, 33, tzinfo=tzutc()), | ||
'tags': ['home', 'garden', 'add', 'tags'], | ||
'project': 'AnnoyingCats', | ||
'priority': u'H', | ||
'http_uuid': '8ce52fe2-ec48-489d-ba00-c30f463fc422', | ||
'http_url': 'https://example.com/tasks' | ||
} | ||
|
||
class TestHttpIssue(AbstractServiceTest, ServiceTest): | ||
maxDiff = None | ||
SERVICE_CONFIG = { | ||
'http.url': 'https://example.com/tasks', | ||
'http.add_tags': "add,tags", | ||
'http.project_name': 'IDK', | ||
'http.default_priority': "L" | ||
} | ||
|
||
def setUp(self): | ||
super(TestHttpIssue, self).setUp() | ||
|
||
self.service = self.get_mock_service(HttpService, section='test_section') | ||
|
||
@responses.activate | ||
def test_issues(self): | ||
""" | ||
Test: conversion from HTTP to taskwarrior tasks. | ||
""" | ||
responses.add( | ||
responses.GET, | ||
'https://example.com/tasks', | ||
json=TEST_RESPONSE, | ||
status=200 | ||
) | ||
|
||
self.assertEqual(self.service.request(), TEST_RESPONSE) | ||
|
||
issues = [ issue.get_taskwarrior_record() for issue in self.service.issues() ] | ||
|
||
self.assertEqual(issues[0], MAX_TASK) | ||
self.assertEqual(issues[1], MIN_TASK) | ||
|
||
""" | ||
|
||
responses.add( | ||
responses.GET, | ||
'https://example.com/tasks', | ||
json=TEST_RESPONSE, | ||
status=200 | ||
) | ||
|
||
issues = list(self.service.issues()) | ||
|
||
print(issues) | ||
|
||
self.assertEqual(len(issues), 2) | ||
|
||
self.assertTrue(MAX_TASK in issues) | ||
self.assertEqual(issues[1], MIN_TASK) | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment block appears to be accidentally committed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops |
||
|
||
def test_to_taskwarrior(self): | ||
issue = self.service.convert_to_issue(TEST_RESPONSE[0]) | ||
|
||
taskwarrior = issue.get_taskwarrior_record() | ||
|
||
self.assertEqual(taskwarrior, MAX_TASK) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The context of the way we've been using this list suggests that you were the originall/primary contributor of the gmail and jira services.