Bugsy: import Bugsy from upstream
authorGregory Szorc <gps@mozilla.com>
Tue, 03 Mar 2015 15:46:59 -0800
changeset 360465 73bd35b317b0f972391bcf386058b06ef9ccefc2
parent 360464 f5ddde0ae86c8b042c9db9284a18ad2337fdaf97
child 360466 beae0925b33e8c53802d2940cbdbdf1474da4079
push id16998
push userrwood@mozilla.com
push dateMon, 02 May 2016 19:42:03 +0000
Bugsy: import Bugsy from upstream f7271ed194b19d139551b6c942f82cb4dd65ee77 was imported from the Bugsy repository. Main reason for import is to get support for cookie auth.
pylib/Bugsy/CONTRIBUTING.md
pylib/Bugsy/History.md
pylib/Bugsy/bugsy/bug.py
pylib/Bugsy/bugsy/bugsy.py
pylib/Bugsy/bugsy/search.py
pylib/Bugsy/setup.py
pylib/Bugsy/tests/test_bugsy.py
pylib/Bugsy/tests/test_search.py
--- a/pylib/Bugsy/CONTRIBUTING.md
+++ b/pylib/Bugsy/CONTRIBUTING.md
@@ -2,16 +2,21 @@
 
 Firstly, thanks for wanting to contribute back to this project. Below are
 guidelines that should hopefully minimise the amount of backwards and forwards
 in the review process.
 
 ## Getting Started
 
 * Fork the repository on GitHub - well... duh :P
+* Create a virtualenv: `virtualenv venv`
+* Activate the virtualenv: `. venv/bin/activate`
+* Install the package in develop mode: `python setup.py develop`
+* Install requirements: `pip install -r requirements.txt`
+* Run the tests to check that everything was successful: `py.test tests`
 
 ## Making Changes
 
 * Create a topic branch from where you want to base your work.
   * This is usually the master branch.
   * Only target release branches if you are certain your fix must be on that
     branch.
   * To quickly create a topic branch based on master; `git checkout -b
--- a/pylib/Bugsy/History.md
+++ b/pylib/Bugsy/History.md
@@ -1,8 +1,17 @@
+
+0.4.0 / 2014-12-05
+==================
+
+ * Add in the ability to use change history fields for searchings
+ * allow searching to handle time frames
+ * Change UserAgent for bugsy to that Bugzilla knows that it is us calling. Fixes #4
+ * Add version added to comments documentation
+ * Add documenation for comments
 
 0.3.0 / 2014-07-14
 ==================
 
  * Updated Documentation
  * Fix adding comments to bugs that already exist. Fixes #2
  * Give the ability to search for multiple bugs which allows changing the fields returned
  * Only request a small number of fields from Bugzilla. Fixes #3
--- a/pylib/Bugsy/bugsy/bug.py
+++ b/pylib/Bugsy/bugsy/bug.py
@@ -269,25 +269,47 @@ class Bug(object):
             Return the raw dict that is used inside this object
         """
         return self._bug
 
 
 class Comment(object):
     """
         Represents a single Bugzilla comment.
+
+        To get comments you need to do the following
+
+        >>> bugs = bugzilla.search_for.keywords("checkin-needed").search()
+        >>> comments = bugs[0].get_comments()
+        >>> comments[0].text # Returns the comment 0 of the first checkin-needed bug
     """
 
     def __init__(self, **kwargs):
 
         self.attachment_id = kwargs['attachment_id']
         self.author = kwargs['author']
         self.bug_id = kwargs['bug_id']
         self.creation_time = str2datetime(kwargs['creation_time'])
         self.creator = kwargs['creator']
-        self.id = kwargs['id']
+        self._id = kwargs['id']
         self.is_private = kwargs['is_private']
-        self.text = kwargs['text']
+        self._text = kwargs['text']
         self.time = str2datetime(kwargs['time'])
 
         if 'tags' in kwargs:
             self.tags = set(kwargs['tags'])
 
+    @property
+    def text(self):
+        r"""
+            Return the text that is in this comment
+
+            >>> comment.text # David really likes cheese apparently
+
+        """
+        return self._text
+
+    @property
+    def id(self):
+        r"""
+            Return the comment id that is associated with Bugzilla.
+        """
+        return self._id
--- a/pylib/Bugsy/bugsy/bugsy.py
+++ b/pylib/Bugsy/bugsy/bugsy.py
@@ -32,43 +32,68 @@ class LoginException(Exception):
 class Bugsy(object):
     """
         Bugsy allows easy getting and putting of Bugzilla bugs
     """
 
     DEFAULT_SEARCH = ['version', 'id', 'summary', 'status', 'op_sys',
                       'resolution', 'product', 'component', 'platform']
 
-    def __init__(self, username=None, password=None, bugzilla_url='https://bugzilla.mozilla.org/rest'):
+    def __init__(
+            self,
+            username=None,
+            password=None,
+            userid=None,
+            cookie=None,
+            bugzilla_url='https://bugzilla.mozilla.org/rest'
+    ):
         """
             Initialises a new instance of Bugsy
 
             :param username: Username to login with. Defaults to None
             :param password: Password to login with. Defaults to None
+            :param userid: User ID to login with. Defaults to None
+            :param cookie: Cookie to login with. Defaults to None
             :param bugzilla_url: URL endpoint to interact with. Defaults to https://bugzilla.mozilla.org/rest
 
             If a username AND password are passed in Bugsy will try get a login token
             from Bugzilla. If we can't login then a LoginException will
             be raised.
+
+            If a userid AND cookie are passed in Bugsy will create a login token from them.
+            If no username was passed in it will then try to get the username
+            from Bugzilla.
         """
         self.username = username
         self.password = password
+        self.userid = userid
+        self.cookie = cookie
         self.bugzilla_url = bugzilla_url
         self.token = None
         self.session = requests.Session()
 
         if self.username and self.password:
             result = self.request('login',
                 params={'login': username, 'password': password})
             result = result.json()
             if result.has_key('token'):
                 self.session.params['token'] = result['token']
                 self.token = result['token']
             else:
                 raise LoginException(result['message'])
+        elif self.userid and self.cookie:
+            # The token is crafted from the userid and cookie.
+            self.token = '%s-%s' % (self.userid, self.cookie)
+            self.session.params['token'] = self.token
+            if not self.username:
+                result = self.request('user/%s' % self.userid).json()
+                if result.get('users', []):
+                    self.username = result['users'][0]['name']
+                else:
+                    raise LoginException(result['message'])
 
     def get(self, bug_number):
         """
             Get a bug from Bugzilla. If there is a login token created during object initialisation
             it will be part of the query string passed to Bugzilla
 
             :param bug_number: Bug Number that will be searched. If found will return a Bug object.
 
--- a/pylib/Bugsy/bugsy/search.py
+++ b/pylib/Bugsy/bugsy/search.py
@@ -1,13 +1,23 @@
 import copy
 
-import requests
 from bug import Bug
-import bugsy as Bugsy
+
+
+class SearchException(Exception):
+    """
+        If while interacting with Bugzilla and we try do something that is not
+        supported this error will be raised.
+    """
+    def __init__(self, msg):
+        self.msg = msg
+
+    def __str__(self):
+        return "%s" % self.msg
 
 
 class Search(object):
     """
         This allows searching for bugs in Bugzilla
     """
     def __init__(self, bugsy):
         """
@@ -17,16 +27,18 @@ class Search(object):
         """
         self._bugsy = bugsy
         self._includefields = copy.copy(bugsy.DEFAULT_SEARCH)
         self._keywords = []
         self._assigned = []
         self._summaries = []
         self._whiteboard = []
         self._bug_numbers = []
+        self._time_frame = {}
+        self._change_history = {"fields": []}
 
     def include_fields(self, *args):
         r"""
             Include fields is the fields that you want to be returned when searching. These
             are in addition to the fields that are always included below.
 
             :param args: items passed in will be turned into a list
             :returns: :class:`Search`
@@ -98,28 +110,57 @@ class Search(object):
             :param bug_numbers: A string for the bug number or a list of strings
             :returns: :class:`Search`
 
             >>> bugzilla.search_for.bug_number(['123123', '123456'])
         """
         self._bug_numbers = list(bug_numbers)
         return self
 
+    def timeframe(self, start, end):
+        r"""
+            When you want to search bugs for a certain time frame.
+
+            :param start:
+            :param end:
+            :returns: :class:`Search`
+        """
+        if start:
+            self._time_frame['chfieldfrom'] = start
+        if end:
+            self._time_frame['chfieldto'] = end
+        return self
+
+    def change_history_fields(self, fields, value=None):
+        r"""
+
+        """
+        if not isinstance(fields, list):
+            raise Exception('fields should be a list')
+
+        self._change_history['fields'] = fields
+        if value:
+            self._change_history['value'] = value
+
+        return self
+
     def search(self):
         r"""
             Call the Bugzilla endpoint that will do the search. It will take the information
             used in other methods on the Search object and build up the query string. If no
             bugs are found then an empty list is returned.
 
             >>> bugs = bugzilla.search_for\
             ...                .keywords("checkin-needed")\
             ...                .include_fields("flags")\
             ...                .search()
         """
         params = {}
+        params = dict(params.items() + self._time_frame.items())
+
         if self._includefields:
             params['include_fields'] = list(self._includefields)
         if self._bug_numbers:
             bugs = []
             for bug in self._bug_numbers:
                 result = self._bugsy.request('bug/%s' % bug, params=params).json()
                 bugs.append(Bug(self._bugsy, **result['bugs'][0]))
 
@@ -130,11 +171,18 @@ class Search(object):
             if self._assigned:
                 params['assigned_to'] = list(self._assigned)
             if self._summaries:
                 params['short_desc_type'] = 'allwordssubstr'
                 params['short_desc'] = list(self._summaries)
             if self._whiteboard:
                 params['short_desc_type'] = 'allwordssubstr'
                 params['whiteboard'] = list(self._whiteboard)
+            if self._change_history['fields']:
+                params['chfield'] = self._change_history['fields']
+            if self._change_history.get('value', None):
+                params['chfieldvalue'] = self._change_history['value']
 
             results = self._bugsy.request('bug', params=params).json()
+            error = results.get("error", None)
+            if error:
+                raise SearchException(results['message'])
             return [Bug(self._bugsy, **bug) for bug in results['bugs']]
--- a/pylib/Bugsy/setup.py
+++ b/pylib/Bugsy/setup.py
@@ -1,12 +1,12 @@
 from setuptools import setup, find_packages
 
 setup(name='bugsy',
-      version='0.3.0',
+      version='0.4.0',
       description='A library for interacting Bugzilla Native REST API',
       author='David Burns',
       author_email='david.burns at theautomatedtester dot co dot uk',
       url='http://oss.theautomatedtester.co.uk/bugzilla',
       classifiers=['Development Status :: 3 - Alpha',
                   'Intended Audience :: Developers',
                   'License :: OSI Approved :: Apache Software License',
                   'Operating System :: POSIX',
--- a/pylib/Bugsy/tests/test_bugsy.py
+++ b/pylib/Bugsy/tests/test_bugsy.py
@@ -76,16 +76,25 @@ def test_we_can_get_a_bug_with_login_tok
                     content_type='application/json', match_querystring=True)
   bugzilla = Bugsy("foo", "bar")
   bug = bugzilla.get(1017315)
   assert bug.id == 1017315
   assert bug.status == 'RESOLVED'
   assert bug.summary == 'Schedule Mn tests on opt Linux builds on cedar'
 
 @responses.activate
+def test_we_can_get_username_with_userid_cookie():
+  responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/user/1234?token=1234-abcd',
+                        body='{"users": [{"name": "user@example.com"}]}', status=200,
+                        content_type='application/json', match_querystring=True)
+
+  bugzilla = Bugsy(userid='1234', cookie='abcd')
+  assert bugzilla.username == 'user@example.com'
+
+@responses.activate
 def test_we_can_create_a_new_remote_bug():
     bug = Bug()
     bug.summary = "I like foo"
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                       body='{"token": "foobar"}', status=200,
                       content_type='application/json', match_querystring=True)
     bug_dict = bug.to_dict().copy()
     bug_dict['id'] = 123123
--- a/pylib/Bugsy/tests/test_search.py
+++ b/pylib/Bugsy/tests/test_search.py
@@ -1,11 +1,12 @@
 import bugsy
 from bugsy import Bugsy, BugsyException, LoginException
 from bugsy import Bug
+from bugsy.search import SearchException
 
 import responses
 import json
 
 @responses.activate
 def test_we_only_ask_for_the_include_fields():
   include_return = {
          "bugs" : [
@@ -294,9 +295,111 @@ def test_we_can_search_for_a_list_of_bug
     bugzilla = Bugsy()
     bugs = bugzilla.search_for\
             .bug_number(['1017315', '1017316'])\
             .search()
 
     assert len(responses.calls) == 2
     assert len(bugs) == 2
     assert bugs[0].product == return_1['bugs'][0]['product']
-    assert bugs[0].summary == return_1['bugs'][0]['summary']
\ No newline at end of file
+    assert bugs[0].summary == return_1['bugs'][0]['summary']
+
+@responses.activate
+def test_we_can_search_for_a_list_of_bug_numbers_with_start_finish_dates():
+    return_1 = {
+     "bugs" : [
+        {
+           "component" : "CSS Parsing and Computation",
+           "product" : "Core",
+           "summary" : "Map \"rebeccapurple\" to #663399 in named color list."
+        }
+      ]
+    }
+
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug?chfieldfrom=2014-12-01&chfieldto=2014-12-05&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
+                      body=json.dumps(return_1), status=200,
+                      content_type='application/json', match_querystring=True)
+
+    bugzilla = Bugsy()
+    bugs = bugzilla.search_for\
+            .timeframe('2014-12-01', '2014-12-05')\
+            .search()
+
+    assert len(responses.calls) == 1
+    assert len(bugs) == 1
+    assert bugs[0].product == return_1['bugs'][0]['product']
+    assert bugs[0].summary == return_1['bugs'][0]['summary']
+
+@responses.activate
+def test_we_can_search_with_change_history_field_throws_when_not_given_a_list():
+
+    return_1 = {
+     "bugs" : [
+        {
+           "component" : "CSS Parsing and Computation",
+           "product" : "Core",
+           "summary" : "Map \"rebeccapurple\" to #663399 in named color list."
+        }
+      ]
+    }
+
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug?chfieldfrom=2014-12-01&chfieldto=2014-12-05&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform&chfield=[Bug Creation]&chfield=Alias&chfieldvalue=foo',
+                      body=json.dumps(return_1), status=200,
+                      content_type='application/json', match_querystring=False)
+    try:
+      bugzilla = Bugsy()
+      bugs = bugzilla.search_for\
+              .change_history_fields('[Bug Creation]', 'foo')\
+              .timeframe('2014-12-01', '2014-12-05')\
+              .search()
+    except Exception as e:
+      assert str(e) == "fields should be a list"
+
+
+@responses.activate
+def test_we_can_search_with_change_history_field_gets_bugs():
+
+    return_1 = {
+     "bugs" : [
+        {
+           "component" : "CSS Parsing and Computation",
+           "product" : "Core",
+           "summary" : "Map \"rebeccapurple\" to #663399 in named color list."
+        }
+      ]
+    }
+
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug?chfield=%5BBug+creation%5D&chfield=Alias&chfieldvalue=foo&chfieldfrom=2014-12-01&chfieldto=2014-12-05&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
+                      body=json.dumps(return_1), status=200,
+                      content_type='application/json', match_querystring=True)
+
+    bugzilla = Bugsy()
+    bugs = bugzilla.search_for\
+            .change_history_fields(['[Bug creation]', 'Alias'], 'foo')\
+            .timeframe('2014-12-01', '2014-12-05')\
+            .search()
+
+    assert len(responses.calls) == 1
+    assert len(bugs) == 1
+    assert bugs[0].product == return_1['bugs'][0]['product']
+    assert bugs[0].summary == return_1['bugs'][0]['summary']
+
+@responses.activate
+def test_we_can_handle_errors_coming_back_from_search():
+    error_return = {
+        "code" : 108,
+        "documentation" : "http://www.bugzilla.org/docs/tip/en/html/api/",
+        "error" : True,
+        "message" : "Can't use [Bug Creation] as a field name."
+    }
+
+    responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug?chfield=%5BBug+Creation%5D&chfield=Alias&chfieldvalue=foo&chfieldfrom=2014-12-01&chfieldto=2014-12-05&include_fields=version&include_fields=id&include_fields=summary&include_fields=status&include_fields=op_sys&include_fields=resolution&include_fields=product&include_fields=component&include_fields=platform',
+                      body=json.dumps(error_return), status=200,
+                      content_type='application/json', match_querystring=True)
+
+    bugzilla = Bugsy()
+    try:
+        bugzilla.search_for\
+                .change_history_fields(['[Bug Creation]', 'Alias'], 'foo')\
+                .timeframe('2014-12-01', '2014-12-05')\
+                .search()
+    except SearchException as e:
+        assert str(e) == "Can't use [Bug Creation] as a field name."
\ No newline at end of file