Skip to content
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

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bugwarrior/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ Contributors
- BinaryBabel (contributed support for YouTrack)
- Matthew Cengia (contributed extra support for Trello)
- Andrew Demas (contributed support for PivotalTracker)
- Stephan Meijer (contributed support for JIRA, Zepel, GMail and HTTP)
Copy link
Collaborator

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.

62 changes: 62 additions & 0 deletions bugwarrior/docs/services/http.rst
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]
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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. |
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what other http method than GET could possibly be appropriate for fetching an array of tasks?

+--------------------------------+---------------------------------------------------------------------------------+
| ``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) |
+---------------------+-----------------------------------+---------------+
75 changes: 75 additions & 0 deletions bugwarrior/services/http.py
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')
)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the APPLICATION_NAME attribute for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,6 @@
gmail=bugwarrior.services.gmail:GmailService
teamworks_projects=bugwarrior.services.teamworks_projects:TeamworksService
pivotaltracker=bugwarrior.services.pivotaltracker:PivotalTrackerService
http=bugwarrior.services.http:HttpService
""",
)
113 changes: 113 additions & 0 deletions tests/test_http.py
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)
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment block appears to be accidentally committed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)