python-bugzilla-2.1.0/0000775000175100017510000000000013067312016016323 5ustar crobinsocrobinso00000000000000python-bugzilla-2.1.0/examples/0000775000175100017510000000000013067312016020141 5ustar crobinsocrobinso00000000000000python-bugzilla-2.1.0/examples/create.py0000664000175100017510000000332513062015413021755 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. # create.py: Create a new bug report from __future__ import print_function import time import bugzilla # public test instance of bugzilla.redhat.com. # # Don't worry, changing things here is fine, and won't send any email to # users or anything. It's what partner-bugzilla.redhat.com is for! URL = "partner-bugzilla.redhat.com" bzapi = bugzilla.Bugzilla(URL) if not bzapi.logged_in: print("This example requires cached login credentials for %s" % URL) bzapi.interactive_login() # Similar to build_query, build_createbug is a helper function that handles # some bugzilla version incompatibility issues. All it does is return a # properly formatted dict(), and provide friendly parameter names. # The argument names map to those accepted by XMLRPC Bug.create: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug # # The arguments specified here are mandatory, but there are many other # optional ones like op_sys, platform, etc. See the docs createinfo = bzapi.build_createbug( product="Fedora", version="rawhide", component="python-bugzilla", summary="new example python-bugzilla bug %s" % time.time(), description="This is comment #0 of an example bug created by " "the python-bugzilla.git examples/create.py script.") newbug = bzapi.createbug(createinfo) print("Created new bug id=%s url=%s" % (newbug.id, newbug.weburl)) python-bugzilla-2.1.0/examples/getbug.py0000664000175100017510000000312613062015413021766 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. # getbug.py: Simple demonstration of connecting to bugzilla, fetching # a bug, and printing some details. from __future__ import print_function import pprint import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes URL = "partner-bugzilla.redhat.com" bzapi = bugzilla.Bugzilla(URL) # getbug() is just a simple wrapper around getbugs(), which takes a list # IDs, if you need to fetch multiple # # Example bug: https://partner-bugzilla.redhat.com/show_bug.cgi?id=427301 bug = bzapi.getbug(427301) print("Fetched bug #%s:" % bug.id) print(" Product = %s" % bug.product) print(" Component = %s" % bug.component) print(" Status = %s" % bug.status) print(" Resolution= %s" % bug.resolution) print(" Summary = %s" % bug.summary) # Just check dir(bug) for other attributes, or check upstream bugzilla # Bug.get docs for field names: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#get-bug # comments must be fetched separately on stock bugzilla. this just returns # a raw dict with all the info. comments = bug.getcomments() print("\nLast comment data:\n%s" % pprint.pformat(comments[-1])) # getcomments is just a wrapper around bzapi.get_comments(), which can be # used for bulk comments fetching python-bugzilla-2.1.0/examples/apikey.py0000664000175100017510000000223013062015413021766 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. # create.py: Create a new bug report from __future__ import print_function import bugzilla # Don't worry, changing things here is fine, and won't send any email to # users or anything. It's what landfill.bugzilla.org is for! URL = "https://landfill.bugzilla.org/bugzilla-5.0-branch/xmlrpc.cgi" print("You can get an API key at " "https://landfill.bugzilla.org/bugzilla-5.0-branch/userprefs.cgi") print("after creating an account, if necessary. " "This is a test site, so no harm will come!") api_key = raw_input("Enter Bugzilla API Key: ") # API key usage assumes the API caller is storing the API key; if you would # like to use one of the login options that stores credentials on-disk for # command-line usage, use tokens or cookies. bzapi = bugzilla.Bugzilla(URL, api_key=api_key) assert bzapi.logged_in python-bugzilla-2.1.0/examples/query.py0000664000175100017510000000656113062015413021664 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. # query.py: Perform a few varieties of queries from __future__ import print_function import time import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes URL = "partner-bugzilla.redhat.com" bzapi = bugzilla.Bugzilla(URL) # build_query is a helper function that handles some bugzilla version # incompatibility issues. All it does is return a properly formatted # dict(), and provide friendly parameter names. The param names map # to those accepted by XMLRPC Bug.search: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs query = bzapi.build_query( product="Fedora", component="python-bugzilla") # Since 'query' is just a dict, you could set your own parameters too, like # if your bugzilla had a custom field. This will set 'status' for example, # but for common opts it's better to use build_query query["status"] = "CLOSED" # query() is what actually performs the query. it's a wrapper around Bug.search t1 = time.time() bugs = bzapi.query(query) t2 = time.time() print("Found %d bugs with our query" % len(bugs)) print("Query processing time: %s" % (t2 - t1)) # Depending on the size of your query, you can massively speed things up # by telling bugzilla to only return the fields you care about, since a # large chunk of the return time is transmitting the extra bug data. You # tweak this with include_fields: # https://wiki.mozilla.org/Bugzilla:BzAPI#Field_Control # Bugzilla will only return those fields listed in include_fields. query = bzapi.build_query( product="Fedora", component="python-bugzilla", include_fields=["id", "summary"]) t1 = time.time() bugs = bzapi.query(query) t2 = time.time() print("Quicker query processing time: %s" % (t2 - t1)) # bugzilla.redhat.com, and bugzilla >= 5.0 support queries using the same # format as is used for 'advanced' search URLs via the Web UI. For example, # I go to partner-bugzilla.redhat.com -> Search -> Advanced Search, select # Classification=Fedora # Product=Fedora # Component=python-bugzilla # Unselect all bug statuses (so, all status values) # Under Custom Search # Creation date -- is less than or equal to -- 2010-01-01 # # Run that, copy the URL and bring it here, pass it to url_to_query to # convert it to a dict(), and query as usual query = bzapi.url_to_query("https://partner-bugzilla.redhat.com/" "buglist.cgi?classification=Fedora&component=python-bugzilla&" "f1=creation_ts&o1=lessthaneq&order=Importance&product=Fedora&" "query_format=advanced&v1=2010-01-01") query["include_fields"] = ["id", "summary"] bugs = bzapi.query(query) print("The URL query returned 22 bugs... " "I know that without even checking because it shouldn't change!... " "(count is %d)" % len(bugs)) # One note about querying... you can get subtley different results if # you are not logged in. Depending on your bugzilla setup it may not matter, # but if you are dealing with private bugs, check bzapi.logged_in setting # to ensure your cached credentials are up to date. See update.py for # an example usage python-bugzilla-2.1.0/examples/update.py0000664000175100017510000000470413062015413021776 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. # update.py: Make changes to an existing bug from __future__ import print_function import time import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes URL = "partner-bugzilla.redhat.com" bzapi = bugzilla.Bugzilla(URL) if not bzapi.logged_in: print("This example requires cached login credentials for %s" % URL) bzapi.interactive_login() # Similar to build_query, build_update is a helper function that handles # some bugzilla version incompatibility issues. All it does is return a # properly formatted dict(), and provide friendly parameter names. # The param names map to those accepted by XMLRPC Bug.update: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug # # Example bug: https://partner-bugzilla.redhat.com/show_bug.cgi?id=427301 # Don't worry, changing things here is fine, and won't send any email to # users or anything. It's what partner-bugzilla.redhat.com is for! bug = bzapi.getbug(427301) print("Bug id=%s original summary=%s" % (bug.id, bug.summary)) update = bzapi.build_update(summary="new example summary %s" % time.time()) bzapi.update_bugs([bug.id], update) # Call bug.refresh() to update its cached state bug.refresh() print("Bug id=%s new summary=%s" % (bug.id, bug.summary)) # Now let's add a comment comments = bug.getcomments() print("Bug originally has %d comments" % len(comments)) update = bzapi.build_update(comment="new example comment %s" % time.time()) bzapi.update_bugs([bug.id], update) # refresh() actually isn't required here because comments are fetched # on demand comments = bug.getcomments() print("Bug now has %d comments. Last comment=%s" % (len(comments), comments[-1]["text"])) # The 'bug' object actually has some old convenience APIs for specific # actions like commenting, and closing. However these aren't recommended: # they encourage splitting up bug edits when really batching should be done # as much as possible, not only to make your code quicker and save strain # on the bugzilla instance, but also to avoid spamming bugzilla users with # redundant email from two modifications that could have been batched. python-bugzilla-2.1.0/examples/bug_autorefresh.py0000664000175100017510000000523113062015413023674 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. # bug_autorefresh.py: Show what bug_autorefresh is all about, and explain # how to handle the default change via python-bugzilla in 2016 from __future__ import print_function import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes URL = "partner-bugzilla.redhat.com" bzapi = bugzilla.Bugzilla(URL) # The Bugzilla.bug_autorefresh setting controls whether bugs will # automatically go out and try to update their cached contents when code # tries to access a bug attribute that isn't already cached. # # Note this is likely only relevant if some part of your code is using # include_fields, or exclude_fields, or you are depending on access # to bugzilla.redhat.com 'extra_fields' type data like 'attachments' # without explicitly asking the API for them. If you aren't using any # of those bits, you can ignore this. # # Though if you aren't using include_fields and you are running regular # queries in a script, check examples/query.py for a simple usecase that # shows how much include_fields usage can speed up your scripts. # The default as of mid 2016 is bug_autorefresh=off, so set it True here # to demonstrate bzapi.bug_autorefresh = True bug = bzapi.getbug(427301, include_fields=["id", "summary"]) # The limited include_fields here means that only "id" and "summary" fields # of the bug are cached in the bug object. What happens when we try to # get component for example? print("Bug component=%s" % bug.component) # Because bug_autorefresh is True, the bug object basically did a # a bug.refresh() for us, grabbed all its data, and now the component field # is there. Let's try it again, but this time without bug_autorefresh bzapi.bug_autorefresh = False bug = bzapi.getbug(427301, include_fields=["id", "summary"]) try: print("Shouldn't see this! bug component=%s" % bug.component) except AttributeError: print("With bug_autorefresh=False, we received AttributeError as expected") # Why does this matter? Some scripts are implicitly depending on this # auto-refresh behavior, because their include_fields specification doesn't # cover all attributes they actually use. Your script will work, sure, but # it's likely doing many more XML-RPC calls than needed, possibly 1 per bug. # So if after upgrading python-bugzilla you start hitting issues, the # recommendation is to fix your include_fields. python-bugzilla-2.1.0/CONTRIBUTING.md0000664000175100017510000000356113067310407020563 0ustar crobinsocrobinso00000000000000# Setting up the environment If you already have system installed versions of python-bugzilla dependencies, running the command line from git is as simple as doing: cd python-bugzilla.git ./bugzilla-cli [arguments] # Running tests Once you have already activated an environment, you can use the following. ## Basic unit test suite python setup.py test ## Read-Only Functional tests There are more comprehensive tests that are disabled by default. Readonly functional tests that run against several public bugzilla instances. No login account is required: python setup.py test --ro-functional ## Read/Write Functional Tests. Before running rw-functional tests, make sure you have logged into bugzilla using. These currently run against the test bugzilla instance at partner-bugzilla.redhat.com, and requires a valid login there: bugzilla-cli --bugzilla=partner-bugzilla.redhat.com --username=$USER login python setup.py test --rw-functional ## Testing across python versions To test all supported python versions, run tox using any of the following. tox tox -- --ro-functional tox -- --rw-functional # pylint and pep8 To test for pylint or pep8 violations, you can run: python setup.py pylint Note: This expects that you already have pylint and pep8 installed. # Patch Submission If you are submitting a patch, ensure the following: [REQ] verify that no new pylint or pep8 violations [REQ] run basic unit test suite across all python versions as described above. Running any of the functional tests is not a requirement for patch submission, but please give them a go if you are interested. Patches can be submitted via github pull-request, or via the mailing list at python-bugzilla@lists.fedorahosted.org using 'git send-email'. # Bug reports Bug reports should be submitted as github issues, or sent to the mailing list python-bugzilla-2.1.0/PKG-INFO0000664000175100017510000000044113067312016017417 0ustar crobinsocrobinso00000000000000Metadata-Version: 1.0 Name: python-bugzilla Version: 2.1.0 Summary: Bugzilla XMLRPC access module Home-page: https://github.com/python-bugzilla/python-bugzilla Author: Cole Robinson Author-email: python-bugzilla@lists.fedorahosted.org License: GPLv2 Description: UNKNOWN Platform: UNKNOWN python-bugzilla-2.1.0/tests/0000775000175100017510000000000013067312016017465 5ustar crobinsocrobinso00000000000000python-bugzilla-2.1.0/tests/rw_functional.py0000664000175100017510000010267113067305775022736 0ustar crobinsocrobinso00000000000000# # Copyright Red Hat, Inc. 2012 # # This work is licensed under the terms of the GNU GPL, version 2 or later. # See the COPYING file in the top-level directory. # ''' Unit tests that do permanent functional against a real bugzilla instances. ''' from __future__ import print_function import datetime import os import random import sys import unittest if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: # pylint: disable=F0401,E0611 from urllib.parse import urlparse else: from urlparse import urlparse import bugzilla from bugzilla import Bugzilla from bugzilla.transport import _BugzillaTokenCache import tests cf = os.path.expanduser("~/.bugzillacookies") tf = os.path.expanduser("~/.bugzillatoken") def _split_int(s): return [int(i) for i in s.split(",")] class BaseTest(unittest.TestCase): url = None bzclass = None def _testBZClass(self): bz = Bugzilla(url=self.url, use_creds=False) self.assertTrue(bz.__class__ is self.bzclass) def _testCookieOrToken(self): domain = urlparse(self.url)[1] if os.path.exists(cf): out = open(cf).read(1024) if domain in out: return if os.path.exists(tf): token = _BugzillaTokenCache(self.url, tokenfilename=tf) if token.value is not None: return raise RuntimeError("%s or %s must exist and contain domain '%s'" % (cf, tf, domain)) class RHPartnerTest(BaseTest): # Despite its name, this instance is simply for bugzilla testing, # doesn't send out emails and is blown away occasionally. The front # page has some info. url = tests.REDHAT_URL or "partner-bugzilla.redhat.com" bzclass = bugzilla.RHBugzilla def _check_have_admin(self, bz, funcname): # groupnames is empty for any user if our logged in user does not # have admin privs. # Check a known account that likely won't ever go away ret = bool(bz.getuser("anaconda-maint-list@redhat.com").groupnames) if not ret: print("\nNo admin privs, reduced testing of %s" % funcname) return ret test2 = BaseTest._testBZClass def test00LoginState(self): bz = self.bzclass(url=self.url) self.assertTrue(bz.logged_in, "R/W tests require cached login credentials for url=%s" % self.url) bz = self.bzclass(url=self.url, use_creds=False) self.assertFalse(bz.logged_in, "Login state check failed for logged out user.") def test03NewBugBasic(self): """ Create a bug with minimal amount of fields, then close it """ bz = self.bzclass(url=self.url) component = "python-bugzilla" version = "rawhide" summary = ("python-bugzilla test basic bug %s" % datetime.datetime.today()) newout = tests.clicomm("bugzilla new " "--product Fedora --component %s --version %s " "--summary \"%s\" " "--comment \"Test bug from the python-bugzilla test suite\" " "--outputformat \"%%{bug_id}\"" % (component, version, summary), bz) self.assertTrue(len(newout.splitlines()) == 3) bugid = int(newout.splitlines()[2]) bug = bz.getbug(bugid) print("\nCreated bugid: %s" % bugid) # Verify hasattr works self.assertTrue(hasattr(bug, "id")) self.assertTrue(hasattr(bug, "bug_id")) self.assertEquals(bug.component, component) self.assertEquals(bug.version, version) self.assertEquals(bug.summary, summary) # Close the bug tests.clicomm("bugzilla modify --close NOTABUG %s" % bugid, bz) bug.refresh() self.assertEquals(bug.status, "CLOSED") self.assertEquals(bug.resolution, "NOTABUG") def test04NewBugAllFields(self): """ Create a bug using all 'new' fields, check some values, close it """ bz = self.bzclass(url=self.url) summary = ("python-bugzilla test manyfields bug %s" % datetime.datetime.today()) url = "http://example.com" osval = "Windows" cc = "triage@lists.fedoraproject.org" blocked = "461686,461687" dependson = "427301" comment = "Test bug from python-bugzilla test suite" sub_component = "Command-line tools (RHEL6)" alias = "pybz-%s" % datetime.datetime.today().strftime("%s") newout = tests.clicomm("bugzilla new " "--product 'Red Hat Enterprise Linux 6' --version 6.0 " "--component lvm2 --sub-component '%s' " "--summary \"%s\" " "--comment \"%s\" " "--url %s --severity Urgent --priority Low --os %s " "--arch ppc --cc %s --blocked %s --dependson %s " "--alias %s " "--outputformat \"%%{bug_id}\"" % (sub_component, summary, comment, url, osval, cc, blocked, dependson, alias), bz) self.assertTrue(len(newout.splitlines()) == 3) bugid = int(newout.splitlines()[2]) bug = bz.getbug(bugid, extra_fields=["sub_components"]) print("\nCreated bugid: %s" % bugid) self.assertEquals(bug.summary, summary) self.assertEquals(bug.bug_file_loc, url) self.assertEquals(bug.op_sys, osval) self.assertEquals(bug.blocks, _split_int(blocked)) self.assertEquals(bug.depends_on, _split_int(dependson)) self.assertTrue(all([e in bug.cc for e in cc.split(",")])) self.assertEquals(bug.longdescs[0]["text"], comment) self.assertEquals(bug.sub_components, {"lvm2": [sub_component]}) self.assertEquals(bug.alias, [alias]) # Close the bug # RHBZ makes it difficult to provide consistent semantics for # 'alias' update: # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 # alias += "-closed" tests.clicomm("bugzilla modify " "--close WONTFIX %s " % bugid, bz) bug.refresh() self.assertEquals(bug.status, "CLOSED") self.assertEquals(bug.resolution, "WONTFIX") self.assertEquals(bug.alias, [alias]) # Check bug's minimal history ret = bug.get_history_raw() self.assertTrue(len(ret["bugs"]) == 1) self.assertTrue(len(ret["bugs"][0]["history"]) == 1) def test05ModifyStatus(self): """ Modify status and comment fields for an existing bug """ bz = self.bzclass(url=self.url) bugid = "663674" cmd = "bugzilla modify %s " % bugid bug = bz.getbug(bugid) # We want to start with an open bug, so fix things if bug.status == "CLOSED": tests.clicomm(cmd + "--status ASSIGNED", bz) bug.refresh() self.assertEquals(bug.status, "ASSIGNED") origstatus = bug.status # Set to ON_QA with a private comment status = "ON_QA" comment = ("changing status to %s at %s" % (status, datetime.datetime.today())) tests.clicomm(cmd + "--status %s --comment \"%s\" --private" % (status, comment), bz) bug.refresh() self.assertEquals(bug.status, status) self.assertEquals(bug.longdescs[-1]["is_private"], 1) self.assertEquals(bug.longdescs[-1]["text"], comment) # Close bug as DEFERRED with a private comment resolution = "DEFERRED" comment = ("changing status to CLOSED=%s at %s" % (resolution, datetime.datetime.today())) tests.clicomm(cmd + "--close %s --comment \"%s\" --private" % (resolution, comment), bz) bug.refresh() self.assertEquals(bug.status, "CLOSED") self.assertEquals(bug.resolution, resolution) self.assertEquals(bug.comments[-1]["is_private"], 1) self.assertEquals(bug.comments[-1]["text"], comment) # Close bug as dup with no comment dupeid = "461686" desclen = len(bug.longdescs) tests.clicomm(cmd + "--close DUPLICATE --dupeid %s" % dupeid, bz) bug.refresh() self.assertEquals(bug.dupe_of, int(dupeid)) self.assertEquals(len(bug.longdescs), desclen + 1) self.assertTrue("marked as a duplicate" in bug.longdescs[-1]["text"]) # bz.setstatus test comment = ("adding lone comment at %s" % datetime.datetime.today()) bug.setstatus("POST", comment=comment, private=True) bug.refresh() self.assertEquals(bug.longdescs[-1]["is_private"], 1) self.assertEquals(bug.longdescs[-1]["text"], comment) self.assertEquals(bug.status, "POST") # bz.close test fixed_in = str(datetime.datetime.today()) bug.close("ERRATA", fixedin=fixed_in) bug.refresh() self.assertEquals(bug.status, "CLOSED") self.assertEquals(bug.resolution, "ERRATA") self.assertEquals(bug.fixed_in, fixed_in) # bz.addcomment test comment = ("yet another test comment %s" % datetime.datetime.today()) bug.addcomment(comment, private=False) bug.refresh() self.assertEquals(bug.longdescs[-1]["text"], comment) self.assertEquals(bug.longdescs[-1]["is_private"], 0) # Confirm comments is same as getcomments self.assertEquals(bug.comments, bug.getcomments()) # Reset state tests.clicomm(cmd + "--status %s" % origstatus, bz) bug.refresh() self.assertEquals(bug.status, origstatus) def test06ModifyEmails(self): """ Modify cc, assignee, qa_contact for existing bug """ bz = self.bzclass(url=self.url) bugid = "663674" cmd = "bugzilla modify %s " % bugid bug = bz.getbug(bugid) origcc = bug.cc # Test CC list and reset it email1 = "triage@lists.fedoraproject.org" email2 = "crobinso@redhat.com" bug.deletecc(origcc) tests.clicomm(cmd + "--cc %s --cc %s" % (email1, email2), bz) bug.addcc(email1) bug.refresh() self.assertTrue(email1 in bug.cc) self.assertTrue(email2 in bug.cc) self.assertEquals(len(bug.cc), 2) tests.clicomm(cmd + "--cc=-%s" % email1, bz) bug.refresh() self.assertTrue(email1 not in bug.cc) # Test assigned target tests.clicomm(cmd + "--assignee %s" % email1, bz) bug.refresh() self.assertEquals(bug.assigned_to, email1) # Test QA target tests.clicomm(cmd + "--qa_contact %s" % email1, bz) bug.refresh() self.assertEquals(bug.qa_contact, email1) # Reset values bug.deletecc(bug.cc) tests.clicomm(cmd + "--reset-qa-contact --reset-assignee", bz) bug.refresh() self.assertEquals(bug.cc, []) self.assertEquals(bug.assigned_to, "crobinso@redhat.com") self.assertEquals(bug.qa_contact, "extras-qa@fedoraproject.org") def test07ModifyMultiFlags(self): """ Modify flags and fixed_in for 2 bugs """ bz = self.bzclass(url=self.url) bugid1 = "461686" bugid2 = "461687" cmd = "bugzilla modify %s %s " % (bugid1, bugid2) def flagstr(b): ret = [] for flag in b.flags: ret.append(flag["name"] + flag["status"]) return " ".join(sorted(ret)) def cleardict_old(b): """ Clear flag dictionary, for format meant for bug.updateflags """ clearflags = {} for flag in b.flags: clearflags[flag["name"]] = "X" return clearflags def cleardict_new(b): """ Clear flag dictionary, for format meant for update_bugs """ clearflags = [] for flag in b.flags: clearflags.append({"name": flag["name"], "status": "X"}) return clearflags bug1 = bz.getbug(bugid1) if cleardict_old(bug1): bug1.updateflags(cleardict_old(bug1)) bug2 = bz.getbug(bugid2) if cleardict_old(bug2): bug2.updateflags(cleardict_old(bug2)) # Set flags and confirm setflags = "needinfo? requires_doc_text-" tests.clicomm(cmd + " ".join([(" --flag " + f) for f in setflags.split()]), bz) bug1.refresh() bug2.refresh() self.assertEquals(flagstr(bug1), setflags) self.assertEquals(flagstr(bug2), setflags) self.assertEquals(bug1.get_flags("needinfo")[0]["status"], "?") self.assertEquals(bug1.get_flag_status("requires_doc_text"), "-") # Clear flags if cleardict_new(bug1): bz.update_flags(bug1.id, cleardict_new(bug1)) bug1.refresh() if cleardict_new(bug2): bz.update_flags(bug2.id, cleardict_new(bug2)) bug2.refresh() self.assertEquals(cleardict_old(bug1), {}) self.assertEquals(cleardict_old(bug2), {}) # Set "Fixed In" field origfix1 = bug1.fixed_in origfix2 = bug2.fixed_in newfix = origfix1 and (origfix1 + "-new1") or "blippy1" if newfix == origfix2: newfix = origfix2 + "-2" tests.clicomm(cmd + "--fixed_in=%s" % newfix, bz) bug1.refresh() bug2.refresh() self.assertEquals(bug1.fixed_in, newfix) self.assertEquals(bug2.fixed_in, newfix) # Reset fixed_in tests.clicomm(cmd + "--fixed_in=\"-\"", bz) bug1.refresh() bug2.refresh() self.assertEquals(bug1.fixed_in, "-") self.assertEquals(bug2.fixed_in, "-") def test07ModifyMisc(self): bugid = "461686" cmd = "bugzilla modify %s " % bugid bz = self.bzclass(url=self.url) bug = bz.getbug(bugid) # modify --dependson tests.clicomm(cmd + "--dependson 123456", bz) bug.refresh() self.assertTrue(123456 in bug.depends_on) tests.clicomm(cmd + "--dependson =111222", bz) bug.refresh() self.assertEquals([111222], bug.depends_on) tests.clicomm(cmd + "--dependson=-111222", bz) bug.refresh() self.assertEquals([], bug.depends_on) # modify --blocked tests.clicomm(cmd + "--blocked 123,456", bz) bug.refresh() self.assertEquals([123, 456], bug.blocks) tests.clicomm(cmd + "--blocked =", bz) bug.refresh() self.assertEquals([], bug.blocks) # modify --keywords tests.clicomm(cmd + "--keywords +Documentation --keywords EasyFix", bz) bug.refresh() self.assertEquals(["Documentation", "EasyFix"], bug.keywords) tests.clicomm(cmd + "--keywords=-EasyFix --keywords=-Documentation", bz) bug.refresh() self.assertEquals([], bug.keywords) # modify --target_release # modify --target_milestone targetbugid = 492463 targetbug = bz.getbug(targetbugid) targetcmd = "bugzilla modify %s " % targetbugid tests.clicomm(targetcmd + "--target_milestone beta --target_release 6.2", bz) targetbug.refresh() self.assertEquals(targetbug.target_milestone, "beta") self.assertEquals(targetbug.target_release, ["6.2"]) tests.clicomm(targetcmd + "--target_milestone rc --target_release 6.0", bz) targetbug.refresh() self.assertEquals(targetbug.target_milestone, "rc") self.assertEquals(targetbug.target_release, ["6.0"]) # modify --priority # modify --severity tests.clicomm(cmd + "--priority low --severity high", bz) bug.refresh() self.assertEquals(bug.priority, "low") self.assertEquals(bug.severity, "high") tests.clicomm(cmd + "--priority medium --severity medium", bz) bug.refresh() self.assertEquals(bug.priority, "medium") self.assertEquals(bug.severity, "medium") # modify --os # modify --platform # modify --version tests.clicomm(cmd + "--version rawhide --os Windows --arch ppc " "--url http://example.com", bz) bug.refresh() self.assertEquals(bug.version, "rawhide") self.assertEquals(bug.op_sys, "Windows") self.assertEquals(bug.platform, "ppc") self.assertEquals(bug.url, "http://example.com") tests.clicomm(cmd + "--version rawhide --os Linux --arch s390 " "--url http://example.com/fribby", bz) bug.refresh() self.assertEquals(bug.version, "rawhide") self.assertEquals(bug.op_sys, "Linux") self.assertEquals(bug.platform, "s390") self.assertEquals(bug.url, "http://example.com/fribby") # modify --field tests.clicomm(cmd + "--field cf_fixed_in=foo-bar-1.2.3 \ --field=cf_release_notes=baz", bz) bug.refresh() self.assertEquals(bug.fixed_in, "foo-bar-1.2.3") self.assertEquals(bug.cf_release_notes, "baz") def test08Attachments(self): tmpdir = "__test_attach_output" if tmpdir in os.listdir("."): os.system("rm -r %s" % tmpdir) os.mkdir(tmpdir) os.chdir(tmpdir) try: self._test8Attachments() finally: os.chdir("..") os.system("rm -r %s" % tmpdir) def _test8Attachments(self): """ Get and set attachments for a bug """ bz = self.bzclass(url=self.url) getallbugid = "663674" setbugid = "461686" cmd = "bugzilla attach " testfile = "../tests/data/bz-attach-get1.txt" # Add attachment as CLI option setbug = bz.getbug(setbugid, extra_fields=["attachments"]) orignumattach = len(setbug.attachments) # Add attachment from CLI with mime guessing desc1 = "python-bugzilla cli upload %s" % datetime.datetime.today() out1 = tests.clicomm(cmd + "%s --description \"%s\" --file %s" % (setbugid, desc1, testfile), bz) desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() out2 = tests.clicomm(cmd + "%s --file test --summary \"%s\"" % (setbugid, desc2), bz, stdin=open(testfile)) # Expected output format: # Created attachment on bug setbug.refresh() self.assertEquals(len(setbug.attachments), orignumattach + 2) self.assertEquals(setbug.attachments[-2]["summary"], desc1) self.assertEquals(setbug.attachments[-2]["id"], int(out1.splitlines()[2].split()[2])) self.assertEquals(setbug.attachments[-1]["summary"], desc2) self.assertEquals(setbug.attachments[-1]["id"], int(out2.splitlines()[2].split()[2])) attachid = setbug.attachments[-2]["id"] # Set attachment flags self.assertEquals(setbug.attachments[-1]["flags"], []) bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], "review", status="+") setbug.refresh() self.assertEquals(len(setbug.attachments[-1]["flags"]), 1) self.assertEquals(setbug.attachments[-1]["flags"][0]["name"], "review") self.assertEquals(setbug.attachments[-1]["flags"][0]["status"], "+") bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], "review", status="X") setbug.refresh() self.assertEquals(setbug.attachments[-1]["flags"], []) # Get attachment, verify content out = tests.clicomm(cmd + "--get %s" % attachid, bz).splitlines() # Expect format: # Wrote fname = out[2].split()[1].strip() self.assertEquals(len(out), 3) self.assertEquals(fname, "bz-attach-get1.txt") self.assertEquals(open(fname).read(), open(testfile).read()) os.unlink(fname) # Get all attachments getbug = bz.getbug(getallbugid) getbug.autorefresh = True numattach = len(getbug.attachments) out = tests.clicomm(cmd + "--getall %s" % getallbugid, bz).splitlines() self.assertEquals(len(out), numattach + 2) fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] self.assertEquals(len(fnames), numattach) for f in fnames: if not os.path.exists(f): raise AssertionError("filename '%s' not found" % f) os.unlink(f) def test09Whiteboards(self): bz = self.bzclass(url=self.url) bug_id = "663674" cmd = "bugzilla modify %s " % bug_id bug = bz.getbug(bug_id) # Set all whiteboards initval = str(random.randint(1, 1024)) tests.clicomm(cmd + "--whiteboard =%sstatus " "--devel_whiteboard =%sdevel " "--internal_whiteboard '=%sinternal, security, foo security1' " "--qa_whiteboard =%sqa " % (initval, initval, initval, initval), bz) bug.refresh() self.assertEquals(bug.whiteboard, initval + "status") self.assertEquals(bug.qa_whiteboard, initval + "qa") self.assertEquals(bug.devel_whiteboard, initval + "devel") self.assertEquals(bug.internal_whiteboard, initval + "internal, security, foo security1") # Modify whiteboards tests.clicomm(cmd + "--whiteboard =foobar " "--qa_whiteboard _app " "--devel_whiteboard =pre-%s" % bug.devel_whiteboard, bz) bug.refresh() self.assertEquals(bug.qa_whiteboard, initval + "qa" + " _app") self.assertEquals(bug.devel_whiteboard, "pre-" + initval + "devel") self.assertEquals(bug.status_whiteboard, "foobar") # Verify that tag manipulation is smart about separator tests.clicomm(cmd + "--qa_whiteboard=-_app " "--internal_whiteboard=-security,", bz) bug.refresh() self.assertEquals(bug.qa_whiteboard, initval + "qa") self.assertEquals(bug.internal_whiteboard, initval + "internal, foo security1") # Clear whiteboards update = bz.build_update( whiteboard="", devel_whiteboard="", internal_whiteboard="", qa_whiteboard="") bz.update_bugs(bug.id, update) bug.refresh() self.assertEquals(bug.whiteboard, "") self.assertEquals(bug.qa_whiteboard, "") self.assertEquals(bug.devel_whiteboard, "") self.assertEquals(bug.internal_whiteboard, "") def test10Login(self): """ Failed login test, gives us a bit more coverage """ # We overwrite getpass for testing import getpass def fakegetpass(prompt): sys.stdout.write(prompt) sys.stdout.flush() return sys.stdin.readline() oldgetpass = getpass.getpass getpass.getpass = fakegetpass try: # Implied login with --username and --password ret = tests.clicomm("bugzilla --bugzilla %s " "--user foobar@example.com " "--password foobar query -b 123456" % self.url, None, expectfail=True) self.assertTrue("Login failed: " in ret) # 'login' with explicit options ret = tests.clicomm("bugzilla --bugzilla %s " "--user foobar@example.com " "--password foobar login" % self.url, None, expectfail=True) self.assertTrue("Login failed: " in ret) # 'login' with positional options ret = tests.clicomm("bugzilla --bugzilla %s " "login foobar@example.com foobar" % self.url, None, expectfail=True) self.assertTrue("Login failed: " in ret) # bare 'login' stdinstr = "foobar@example.com\n\rfoobar\n\r" ret = tests.clicomm("bugzilla --bugzilla %s login" % self.url, None, expectfail=True, stdinstr=stdinstr) self.assertTrue("Bugzilla Username:" in ret) self.assertTrue("Bugzilla Password:" in ret) self.assertTrue("Login failed: " in ret) finally: getpass.getpass = oldgetpass def test11UserUpdate(self): # This won't work if run by the same user we are using bz = self.bzclass(url=self.url) email = "anaconda-maint-list@redhat.com" group = "fedora_contrib" fn = sys._getframe().f_code.co_name # pylint: disable=protected-access have_admin = self._check_have_admin(bz, fn) user = bz.getuser(email) if have_admin: self.assertTrue(group in user.groupnames) origgroups = user.groupnames # Remove the group try: bz.updateperms(email, "remove", [group]) user.refresh() self.assertTrue(group not in user.groupnames) except: e = sys.exc_info()[1] if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) # Re add it try: bz.updateperms(email, "add", group) user.refresh() self.assertTrue(group in user.groupnames) except: e = sys.exc_info()[1] if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) # Set groups try: newgroups = user.groupnames[:] if have_admin: newgroups.remove(group) bz.updateperms(email, "set", newgroups) user.refresh() self.assertTrue(group not in user.groupnames) except: e = sys.exc_info()[1] if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) # Reset everything try: bz.updateperms(email, "set", origgroups) except: e = sys.exc_info()[1] if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) user.refresh() self.assertEqual(user.groupnames, origgroups) def test11ComponentEditing(self): bz = self.bzclass(url=self.url) component = ("python-bugzilla-testcomponent-%s" % str(random.randint(1, 1024 * 1024 * 1024))) basedata = { "product": "Fedora Documentation", "component": component, } fn = sys._getframe().f_code.co_name # pylint: disable=protected-access have_admin = self._check_have_admin(bz, fn) def compare(data, newid): proxy = bz._proxy # pylint: disable=protected-access products = proxy.Product.get({"names": [basedata["product"]]}) compdata = None for c in products["products"][0]["components"]: if int(c["id"]) == int(newid): compdata = c break self.assertTrue(bool(compdata)) self.assertEqual(data["component"], compdata["name"]) self.assertEqual(data["description"], compdata["description"]) self.assertEqual(data["initialowner"], compdata["default_assigned_to"]) self.assertEqual(data["initialqacontact"], compdata["default_qa_contact"]) self.assertEqual(data["is_active"], compdata["is_active"]) # Create component data = basedata.copy() data.update({ "description": "foo test bar", "initialowner": "crobinso@redhat.com", "initialqacontact": "extras-qa@fedoraproject.org", "initialcclist": ["wwoods@redhat.com", "toshio@fedoraproject.org"], "is_active": True, }) try: newid = bz.addcomponent(data)['id'] print("Created product=%s component=%s" % ( basedata["product"], basedata["component"])) compare(data, newid) except: e = sys.exc_info()[1] if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) # Edit component data = basedata.copy() data.update({ "description": "hey new desc!", "initialowner": "extras-qa@fedoraproject.org", "initialqacontact": "virt-mgr-maint@redhat.com", "initialcclist": ["libvirt-maint@redhat.com", "virt-maint@lists.fedoraproject.org"], "is_active": False, }) try: bz.editcomponent(data) compare(data, newid) except: e = sys.exc_info()[1] if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) def test12SetCookie(self): bz = self.bzclass(self.url, cookiefile=-1, tokenfile=None) try: bz.cookiefile = None raise AssertionError("Setting cookiefile for active connection " "should fail.") except RuntimeError: e = sys.exc_info()[1] self.assertTrue("disconnect()" in str(e)) bz.disconnect() bz.cookiefile = None bz.connect() self.assertFalse(bz.logged_in) def test13SubComponents(self): bz = self.bzclass(url=self.url) # Long closed RHEL5 lvm2 bug. This component has sub_components bug = bz.getbug("185526") bug.autorefresh = True self.assertEquals(bug.component, "lvm2") bz.update_bugs(bug.id, bz.build_update( component="lvm2", sub_component="Command-line tools (RHEL5)")) bug.refresh() self.assertEqual(bug.sub_components, {"lvm2": ["Command-line tools (RHEL5)"]}) bz.update_bugs(bug.id, bz.build_update(sub_component={})) bug.refresh() self.assertEqual(bug.sub_components, {}) def test13ExternalTrackerQuery(self): bz = self.bzclass(url=self.url) self.assertRaises(RuntimeError, bz.build_external_tracker_boolean_query) def _deleteAllExistingExternalTrackers(self, bugid): bz = self.bzclass(url=self.url) ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] if ids != []: bz.remove_external_tracker(ids=ids) def test14ExternalTrackersAddUpdateRemoveQuery(self): bz = self.bzclass(url=self.url) bugid = 461686 ext_bug_id = 380489 # Delete any existing external trackers to get to a known state self._deleteAllExistingExternalTrackers(bugid) url = "https://bugzilla.mozilla.org" if bz.bz_ver_major < 5: url = "http://bugzilla.mozilla.org" # test adding tracker kwargs = { 'ext_type_id': 6, 'ext_type_url': url, 'ext_type_description': 'Mozilla Foundation', 'ext_status': 'Original Status', 'ext_description': 'the description', 'ext_priority': 'the priority' } bz.add_external_tracker(bugid, ext_bug_id, **kwargs) added_bug = bz.getbug(bugid).external_bugs[0] assert added_bug['type']['id'] == kwargs['ext_type_id'] assert added_bug['type']['url'] == kwargs['ext_type_url'] assert (added_bug['type']['description'] == kwargs['ext_type_description']) assert added_bug['ext_status'] == kwargs['ext_status'] assert added_bug['ext_description'] == kwargs['ext_description'] assert added_bug['ext_priority'] == kwargs['ext_priority'] # test updating status, description, and priority by id kwargs = { 'ids': bz.getbug(bugid).external_bugs[0]['id'], 'ext_status': 'New Status', 'ext_description': 'New Description', 'ext_priority': 'New Priority' } bz.update_external_tracker(**kwargs) updated_bug = bz.getbug(bugid).external_bugs[0] assert updated_bug['ext_bz_bug_id'] == str(ext_bug_id) assert updated_bug['ext_status'] == kwargs['ext_status'] assert updated_bug['ext_description'] == kwargs['ext_description'] assert updated_bug['ext_priority'] == kwargs['ext_priority'] # test removing tracker ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] assert len(ids) == 1 bz.remove_external_tracker(ids=ids) ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] assert len(ids) == 0 def test15EnsureLoggedIn(self): bz = self.bzclass(url=self.url) comm = "bugzilla --ensure-logged-in query --bug_id 979546" tests.clicomm(comm, bz) def test16ModifyTags(self): bugid = "461686" cmd = "bugzilla modify %s " % bugid bz = self.bzclass(url=self.url) bug = bz.getbug(bugid) if bug.tags: bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() self.assertEquals(bug.tags, []) tests.clicomm(cmd + "--tags foo --tags +bar --tags baz", bz) bug.refresh() self.assertEquals(bug.tags, ["foo", "bar", "baz"]) tests.clicomm(cmd + "--tags=-bar", bz) bug.refresh() self.assertEquals(bug.tags, ["foo", "baz"]) bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() self.assertEquals(bug.tags, []) python-bugzilla-2.1.0/tests/data/0000775000175100017510000000000013067312016020376 5ustar crobinsocrobinso00000000000000python-bugzilla-2.1.0/tests/data/cookies-bad.txt0000664000175100017510000000003413046663414023324 0ustar crobinsocrobinso00000000000000foo this is invalid cookies python-bugzilla-2.1.0/tests/data/components_file.txt0000664000175100017510000000001413046663414024326 0ustar crobinsocrobinso00000000000000foo bar baz python-bugzilla-2.1.0/tests/data/cookies-lwp.txt0000664000175100017510000000046513046663414023410 0ustar crobinsocrobinso00000000000000#LWP-Cookies-2.0 Set-Cookie3: Bugzilla_login=notacookie; path="/"; domain=".partner-bugzilla.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 Set-Cookie3: Bugzilla_logincookie=notacookie; path="/"; domain=".partner-bugzilla.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 python-bugzilla-2.1.0/tests/data/bz-attach-get1.txt0000664000175100017510000000255613046663414023672 0ustar crobinsocrobinso00000000000000--- base.py.old 2010-12-16 12:15:09.932010659 +0100 +++ base.py 2010-12-16 16:04:18.995185933 +0100 @@ -19,6 +19,8 @@ import tempfile import logging import locale +import email.header +import re log = logging.getLogger('bugzilla') @@ -677,10 +679,17 @@ # RFC 2183 defines the content-disposition header, if you're curious disp = att.headers['content-disposition'].split(';') [filename_parm] = [i for i in disp if i.strip().startswith('filename=')] - (dummy,filename) = filename_parm.split('=') - # RFC 2045/822 defines the grammar for the filename value, but - # I think we just need to remove the quoting. I hope. - att.name = filename.strip('"') + (dummy,filename) = filename_parm.split('=',1) + # RFC 2045/822 defines the grammar for the filename value + filename = filename.strip('"') + # email.header.decode_header cannot handle strings not ending with '?=', + # so let's transform one =?...?= part at a time + while True: + match = re.search("=\?.*?\?=", filename) + if match is None: + break + filename = filename[:match.start()] + email.header.decode_header(match.group(0))[0][0] + filename[match.end():] + att.name = filename # Hooray, now we have a file-like object with .read() and .name return att python-bugzilla-2.1.0/tests/data/cookies-moz.txt0000664000175100017510000000044213046663414023406 0ustar crobinsocrobinso00000000000000# Netscape HTTP Cookie File # http://www.netscape.com/newsref/std/cookie_spec.html # This is a generated file! Do not edit. .partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie .partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie python-bugzilla-2.1.0/tests/misc.py0000664000175100017510000001127513062015413020774 0ustar crobinsocrobinso00000000000000# # Copyright Red Hat, Inc. 2012 # # This work is licensed under the terms of the GNU GPL, version 2 or later. # See the COPYING file in the top-level directory. # ''' Unit tests for building query strings with bin/bugzilla ''' from __future__ import print_function import os import tempfile import unittest import bugzilla import tests class MiscCLI(unittest.TestCase): """ Test miscellaneous CLI bits to get build out our code coverage """ maxDiff = None def testHelp(self): out = tests.clicomm("bugzilla --help", None) self.assertTrue(len(out.splitlines()) > 18) def testCmdHelp(self): out = tests.clicomm("bugzilla query --help", None) self.assertTrue(len(out.splitlines()) > 40) def testVersion(self): out = tests.clicomm("bugzilla --version", None) self.assertTrue(len(out.splitlines()) >= 2) class MiscAPI(unittest.TestCase): """ Test miscellaneous API bits """ def testUserAgent(self): b3 = tests.make_bz("3.0.0") self.assertTrue("python-bugzilla" in b3.user_agent) def testCookies(self): cookiesbad = os.path.join(os.getcwd(), "tests/data/cookies-bad.txt") cookieslwp = os.path.join(os.getcwd(), "tests/data/cookies-lwp.txt") cookiesmoz = os.path.join(os.getcwd(), "tests/data/cookies-moz.txt") # We used to convert LWP cookies, but it shouldn't matter anymore, # so verify they fail at least try: tests.make_bz("3.0.0", cookiefile=cookieslwp) raise AssertionError("Expected BugzillaError from parsing %s" % os.path.basename(cookieslwp)) except bugzilla.BugzillaError: # Expected result pass # Make sure bad cookies raise an error try: tests.make_bz("3.0.0", cookiefile=cookiesbad) raise AssertionError("Expected BugzillaError from parsing %s" % os.path.basename(cookiesbad)) except bugzilla.BugzillaError: # Expected result pass # Mozilla should 'just work' tests.make_bz("3.0.0", cookiefile=cookiesmoz) def test_readconfig(self): # Testing for bugzillarc handling bzapi = tests.make_bz("4.4.0", rhbz=True) bzapi.url = "foo.example.com" temp = tempfile.NamedTemporaryFile(mode="w") content = """ [example.com] foo=1 user=test1 password=test2""" temp.write(content) temp.flush() bzapi.readconfig(temp.name) self.assertEquals(bzapi.user, "test1") self.assertEquals(bzapi.password, "test2") self.assertEquals(bzapi.api_key, None) content = """ [foo.example.com] user=test3 password=test4 api_key=123abc """ temp.write(content) temp.flush() bzapi.readconfig(temp.name) self.assertEquals(bzapi.user, "test3") self.assertEquals(bzapi.password, "test4") self.assertEquals(bzapi.api_key, "123abc") bzapi.url = "bugzilla.redhat.com" bzapi.user = None bzapi.password = None bzapi.api_key = None bzapi.readconfig(temp.name) self.assertEquals(bzapi.user, None) self.assertEquals(bzapi.password, None) self.assertEquals(bzapi.api_key, None) def testPostTranslation(self): def _testPostCompare(bz, indict, outexpect): outdict = indict.copy() bz.post_translation({}, outdict) self.assertTrue(outdict == outexpect) # Make sure multiple calls don't change anything bz.post_translation({}, outdict) self.assertTrue(outdict == outexpect) bug3 = tests.make_bz("3.4.0") rhbz = tests.make_bz("4.4.0", rhbz=True) test1 = { "component": ["comp1"], "version": ["ver1", "ver2"], 'flags': [{ 'is_active': 1, 'name': 'qe_test_coverage', 'setter': 'pm-rhel@redhat.com', 'status': '?', }, { 'is_active': 1, 'name': 'rhel-6.4.0', 'setter': 'pm-rhel@redhat.com', 'status': '+', }], 'alias': ["FOO", "BAR"], 'blocks': [782183, 840699, 923128], 'keywords': ['Security'], 'groups': ['redhat'], } out_simple = test1.copy() out_simple["components"] = out_simple["component"] out_simple["component"] = out_simple["components"][0] out_simple["versions"] = out_simple["version"] out_simple["version"] = out_simple["versions"][0] _testPostCompare(bug3, test1, test1) _testPostCompare(rhbz, test1, out_simple) python-bugzilla-2.1.0/tests/pep8.cfg0000664000175100017510000000055513062015413021023 0ustar crobinsocrobinso00000000000000[pep8] format = pylint # [E125] Continuation indent isn't different from next block # [E128] Not indented for visual style # [E129] visually indented line with same indent as next logical line # [E303] Too many blank lines # [E402] module level import not at top of file # [E731] do not assign a lambda expression, use a def ignore=E125,E128,E129,E303,E402,E731 python-bugzilla-2.1.0/tests/ro_functional.py0000664000175100017510000003135013062015413022677 0ustar crobinsocrobinso00000000000000# -*- encoding: utf-8 -*- # # Copyright Red Hat, Inc. 2012 # # This work is licensed under the terms of the GNU GPL, version 2 or later. # See the COPYING file in the top-level directory. # ''' Unit tests that do readonly functional tests against real bugzilla instances. ''' import sys import unittest from bugzilla import Bugzilla, BugzillaError, RHBugzilla import tests class BaseTest(unittest.TestCase): url = None bzclass = Bugzilla bzversion = (0, 0) closestatus = "CLOSED" def clicomm(self, argstr, expectexc=False, bz=None): comm = "bugzilla " + argstr if not bz: bz = Bugzilla(url=self.url, use_creds=False) if expectexc: self.assertRaises(Exception, tests.clicomm, comm, bz) else: return tests.clicomm(comm, bz) def _testBZVersion(self): bz = Bugzilla(self.url, use_creds=False) self.assertEquals(bz.__class__, self.bzclass) if tests.REDHAT_URL: print("BZ version=%s.%s" % (bz.bz_ver_major, bz.bz_ver_minor)) else: self.assertEquals(bz.bz_ver_major, self.bzversion[0]) self.assertEquals(bz.bz_ver_minor, self.bzversion[1]) # Since we are running these tests against bugzilla instances in # the wild, we can't depend on certain data like product lists # remaining static. Use lax sanity checks in this case def _testInfoProducts(self, mincount, expectstr): out = self.clicomm("info --products").splitlines() self.assertTrue(len(out) >= mincount) self.assertTrue(expectstr in out) def _testInfoComps(self, comp, mincount, expectstr): out = self.clicomm("info --components \"%s\"" % comp).splitlines() self.assertTrue(len(out) >= mincount) self.assertTrue(expectstr in out) def _testInfoVers(self, comp, mincount, expectstr): out = self.clicomm("info --versions \"%s\"" % comp).splitlines() self.assertTrue(len(out) >= mincount) if expectstr: self.assertTrue(expectstr in out) def _testInfoCompOwners(self, comp, expectstr): expectexc = (expectstr == "FAIL") out = self.clicomm("info --component_owners \"%s\"" % comp, expectexc=expectexc) if expectexc: return self.assertTrue(expectstr in out.splitlines()) def _testQuery(self, args, mincount, expectbug): expectexc = (expectbug == "FAIL") cli = "query %s --bug_status %s" % (args, self.closestatus) out = self.clicomm(cli, expectexc=expectexc) if expectexc: return self.assertTrue(len(out.splitlines()) >= mincount) self.assertTrue(bool([l for l in out.splitlines() if l.startswith("#" + expectbug)])) # Check --ids output option out2 = self.clicomm(cli + " --ids") self.assertTrue(len(out.splitlines()) == len(out2.splitlines())) self.assertTrue(bool([l for l in out2.splitlines() if l == expectbug])) def _testQueryFull(self, bugid, mincount, expectstr): out = self.clicomm("query --full --bug_id %s" % bugid) self.assertTrue(len(out.splitlines()) >= mincount) self.assertTrue(expectstr in out) def _testQueryRaw(self, bugid, mincount, expectstr): out = self.clicomm("query --raw --bug_id %s" % bugid) self.assertTrue(len(out.splitlines()) >= mincount) self.assertTrue(expectstr in out) def _testQueryOneline(self, bugid, expectstr): out = self.clicomm("query --oneline --bug_id %s" % bugid) self.assertTrue(len(out.splitlines()) == 3) self.assertTrue(out.splitlines()[2].startswith("#%s" % bugid)) self.assertTrue(expectstr in out) def _testQueryExtra(self, bugid, expectstr): out = self.clicomm("query --extra --bug_id %s" % bugid) self.assertTrue(("#%s" % bugid) in out) self.assertTrue(expectstr in out) def _testQueryFormat(self, args, expectstr): out = self.clicomm("query %s" % args) self.assertTrue(expectstr in out) def _testQueryURL(self, querystr, count, expectstr): url = self.url if "/xmlrpc.cgi" in self.url: url = url.replace("/xmlrpc.cgi", querystr) else: url += querystr out = self.clicomm("query --from-url \"%s\"" % url) self.assertEqual(len(out.splitlines()), count) self.assertTrue(expectstr in out) class BZMozilla(BaseTest): def testVersion(self): # bugzilla.mozilla.org returns version values in YYYY-MM-DD # format, so just try to confirm that bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) self.assertEquals(bz.__class__, Bugzilla) self.assertTrue(bz.bz_ver_major >= 2016) self.assertTrue(bz.bz_ver_minor in range(1, 13)) class BZGentoo(BaseTest): url = "bugs.gentoo.org" bzversion = (5, 0) test0 = BaseTest._testBZVersion def testURLQuery(self): # This is a bugzilla 5.0 instance, which supports URL queries now query_url = ("https://bugs.gentoo.org/buglist.cgi?" "component=[CS]&product=Doc%20Translations" "&query_format=advanced&resolution=FIXED") bz = Bugzilla(url=self.url, use_creds=False) ret = bz.query(bz.url_to_query(query_url)) self.assertTrue(len(ret) > 0) class BZGnome(BaseTest): url = "https://bugzilla.gnome.org/xmlrpc.cgi" bzversion = (4, 4) closestatus = "RESOLVED" test0 = BaseTest._testBZVersion test1 = lambda s: BaseTest._testQuery(s, "--product dogtail --component sniff", 9, "321654") # BZ < 4 doesn't report values for --full test2 = lambda s: BaseTest._testQueryRaw(s, "321654", 30, "ATTRIBUTE[version]: CVS HEAD") test3 = lambda s: BaseTest._testQueryOneline(s, "321654", "Sniff") def testURLQuery(self): # This instance is old and doesn't support URL queries, we are # just verifying our extra error message report query_url = ("https://bugzilla.gnome.org/buglist.cgi?" "bug_status=RESOLVED&order=Importance&product=accerciser" "&query_format=advanced&resolution=NOTABUG") bz = Bugzilla(url=self.url, use_creds=False) try: bz.query(bz.url_to_query(query_url)) except BugzillaError: e = sys.exc_info()[1] self.assertTrue("derived from bugzilla" in str(e)) class BZFDO(BaseTest): url = "https://bugs.freedesktop.org/xmlrpc.cgi" bzversion = (5, 0) closestatus = "CLOSED,RESOLVED" test0 = BaseTest._testBZVersion test1 = lambda s: BaseTest._testQuery(s, "--product avahi", 10, "3450") test2 = lambda s: BaseTest._testQueryFull(s, "3450", 10, "Blocked: \n") test2 = lambda s: BaseTest._testQueryRaw(s, "3450", 30, "ATTRIBUTE[creator]: daniel@fooishbar.org") test3 = lambda s: BaseTest._testQueryOneline(s, "3450", "daniel@fooishbar.org libavahi") test4 = lambda s: BaseTest._testQueryExtra(s, "3450", "Error") test5 = lambda s: BaseTest._testQueryFormat(s, "--bug_id 3450 --outputformat " "\"%{bug_id} %{assigned_to} %{summary}\"", "3450 daniel@fooishbar.org Error") class RHTest(BaseTest): url = tests.REDHAT_URL or "https://bugzilla.redhat.com/xmlrpc.cgi" bzclass = RHBugzilla bzversion = (4, 4) test0 = BaseTest._testBZVersion test01 = lambda s: BaseTest._testInfoProducts(s, 125, "Virtualization Tools") test02 = lambda s: BaseTest._testInfoComps(s, "Virtualization Tools", 10, "virt-manager") test03 = lambda s: BaseTest._testInfoVers(s, "Fedora", 19, "rawhide") test04 = lambda s: BaseTest._testInfoCompOwners(s, "Virtualization Tools", "libvirt: Libvirt Maintainers") test05 = lambda s: BaseTest._testQuery(s, "--product Fedora --component python-bugzilla --version 14", 6, "621030") test06 = lambda s: BaseTest._testQueryFull(s, "621601", 60, "end-of-life (EOL)") test07 = lambda s: BaseTest._testQueryRaw(s, "307471", 70, "ATTRIBUTE[whiteboard]: bzcl34nup") test08 = lambda s: BaseTest._testQueryOneline(s, "785016", "[---] fedora-review+,fedora-cvs+") test09 = lambda s: BaseTest._testQueryExtra(s, "307471", " +Status Whiteboard: bzcl34nup") test10 = lambda s: BaseTest._testQueryFormat(s, "--bug_id 307471 --outputformat=\"id=%{bug_id} " "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " "sum=%{summary}\"", "id=307471 sw= bzcl34nup needinfo= ") test11 = lambda s: BaseTest._testQueryURL(s, "/buglist.cgi?f1=creation_ts" "&list_id=973582&o1=greaterthaneq&classification=Fedora&" "o2=lessthaneq&query_format=advanced&f2=creation_ts" "&v1=2010-01-01&component=python-bugzilla&v2=2011-01-01" "&product=Fedora", 26, "#553878 CLOSED") test12 = lambda s: BaseTest._testQueryFormat(s, "--bug_id 785016 --outputformat=\"id=%{bug_id} " "sw=%{whiteboard:status} flag=%{flag:fedora-review} " "sum=%{summary}\"", "id=785016 sw= flag=+") # Unicode in this bug's summary test13 = lambda s: BaseTest._testQueryFormat(s, "--bug_id 522796 --outputformat \"%{summary}\"", "V34 — system") # CVE bug output test14 = lambda s: BaseTest._testQueryOneline(s, "720784", " CVE-2011-2527") def testDoubleConnect(self): bz = self.bzclass(url=self.url) bz.connect(self.url) def testQueryFlags(self): bz = self.bzclass(url=self.url) if not bz.logged_in: print("not logged in, skipping testQueryFlags") return out = self.clicomm("query --product 'Red Hat Enterprise Linux 5' " "--component virt-manager --bug_status CLOSED " "--flag rhel-5.4.0+", bz=bz) self.assertTrue(len(out.splitlines()) > 15) self.assertTrue(len(out.splitlines()) < 28) self.assertTrue("223805" in out) def testQueryFixedIn(self): out = self.clicomm("query --fixed_in anaconda-15.29-1") self.assertEquals(len(out.splitlines()), 6) self.assertTrue("#629311 CLOSED" in out) def testComponentsDetails(self): """ Fresh call to getcomponentsdetails should properly refresh """ bz = self.bzclass(url=self.url, use_creds=False) self.assertTrue( bool(bz.getcomponentsdetails("Red Hat Developer Toolset"))) def testGetBugAlias(self): """ getbug() works if passed an alias """ bz = self.bzclass(url=self.url, use_creds=False) bug = bz.getbug("CVE-2011-2527") self.assertTrue(bug.bug_id == 720773) def testQuerySubComponent(self): out = self.clicomm("query --product 'Red Hat Enterprise Linux 7' " "--component lvm2 --sub-component 'Thin Provisioning'") self.assertTrue(len(out.splitlines()) >= 5) self.assertTrue("#1060931 " in out) def testBugFields(self): bz = self.bzclass(url=self.url, use_creds=False) fields1 = bz.getbugfields()[:] fields2 = bz.getbugfields(force_refresh=True)[:] self.assertTrue(bool([f for f in fields1 if f.startswith("attachments")])) self.assertEqual(fields1, fields2) def testBugAutoRefresh(self): bz = self.bzclass(self.url, use_creds=False) bz.bug_autorefresh = True bug = bz.query(bz.build_query(bug_id=720773, include_fields=["summary"]))[0] self.assertTrue(hasattr(bug, "component")) self.assertTrue(bool(bug.component)) bz.bug_autorefresh = False bug = bz.query(bz.build_query(bug_id=720773, include_fields=["summary"]))[0] self.assertFalse(hasattr(bug, "component")) try: self.assertFalse(bool(bug.component)) except: e = sys.exc_info()[1] self.assertTrue("adjust your include_fields" in str(e)) def testExtraFields(self): bz = self.bzclass(self.url, cookiefile=None, tokenfile=None) # Check default extra_fields will pull in comments bug = bz.getbug(720773, exclude_fields=["product"]) self.assertTrue("comments" in dir(bug)) self.assertTrue("product" not in dir(bug)) # Ensure that include_fields overrides default extra_fields bug = bz.getbug(720773, include_fields=["summary"]) self.assertTrue("summary" in dir(bug)) self.assertTrue("comments" not in dir(bug)) python-bugzilla-2.1.0/tests/__init__.py0000664000175100017510000000637013062015413021600 0ustar crobinsocrobinso00000000000000 from __future__ import print_function import atexit import difflib import imp import os import shlex import sys if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: from io import StringIO else: from StringIO import StringIO from bugzilla import Bugzilla, RHBugzilla _cleanup = [] def _import(name, path): _cleanup.append(path + "c") return imp.load_source(name, path) def _cleanup_cb(): for f in _cleanup: if os.path.exists(f): os.unlink(f) atexit.register(_cleanup_cb) bugzillascript = _import("bugzillascript", "bin/bugzilla") # This is overwritten by python setup.py test --redhat-url, and then # used in ro/rw tests REDHAT_URL = None def make_bz(version, *args, **kwargs): cls = Bugzilla if kwargs.pop("rhbz", False): cls = RHBugzilla if "cookiefile" not in kwargs and "tokenfile" not in kwargs: kwargs["use_creds"] = False if "url" not in kwargs: kwargs["url"] = None bz = cls(*args, **kwargs) bz._set_bz_version(version) # pylint: disable=protected-access return bz def diff(orig, new): """ Return a unified diff string between the passed strings """ return "".join(difflib.unified_diff(orig.splitlines(1), new.splitlines(1), fromfile="Orig", tofile="New")) def difffile(expect, filename): expect += '\n' if not os.path.exists(filename) or os.getenv("__BUGZILLA_UNITTEST_REGEN"): open(filename, "w").write(expect) ret = diff(open(filename).read(), expect) if ret: raise AssertionError("Output was different:\n%s" % ret) def clicomm(argv, bzinstance, returnmain=False, printcliout=False, stdin=None, stdinstr=None, expectfail=False): """ Run bin/bugzilla.main() directly with passed argv """ argv = shlex.split(argv) oldstdout = sys.stdout oldstderr = sys.stderr oldstdin = sys.stdin oldargv = sys.argv try: if not printcliout: out = StringIO() sys.stdout = out sys.stderr = out if stdin: sys.stdin = stdin elif stdinstr: sys.stdin = StringIO(stdinstr) sys.argv = argv ret = 0 mainout = None try: print(" ".join(argv)) print() mainout = bugzillascript.main(unittest_bz_instance=bzinstance) except SystemExit: sys_e = sys.exc_info()[1] ret = sys_e.code outt = "" if not printcliout: outt = out.getvalue() if outt.endswith("\n"): outt = outt[:-1] if ret != 0 and not expectfail: raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % (ret, argv, outt)) elif ret == 0 and expectfail: raise RuntimeError("Command succeeded but we expected success\n" "ret=%d\ncmd=%s\nout=%s" % (ret, argv, outt)) if returnmain: return mainout return outt finally: sys.stdout = oldstdout sys.stderr = oldstderr sys.stdin = oldstdin sys.argv = oldargv python-bugzilla-2.1.0/tests/query.py0000664000175100017510000002737113062015413021212 0ustar crobinsocrobinso00000000000000# # Copyright Red Hat, Inc. 2012 # # This work is licensed under the terms of the GNU GPL, version 2 or later. # See the COPYING file in the top-level directory. # ''' Unit tests for building query strings with bin/bugzilla ''' import copy import os import unittest import tests bz34 = tests.make_bz("3.4.0") bz4 = tests.make_bz("4.0.0") rhbz4 = tests.make_bz("4.4.0", rhbz=True) class BZ34Test(unittest.TestCase): """ This is the base query class, but it's also functional on its own. """ maxDiff = None def assertDictEqual(self, *args, **kwargs): # EPEL5 back compat if hasattr(unittest.TestCase, "assertDictEqual"): return unittest.TestCase.assertDictEqual(self, *args, **kwargs) return self.assertEqual(*args, **kwargs) def clicomm(self, argstr, out): comm = "bugzilla query --test-return-result " + argstr if out is None: self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) else: q = tests.clicomm(comm, self.bz, returnmain=True) self.assertDictEqual(out, q) def testBasicQuery(self): self.clicomm("--product foo --component foo,bar --bug_id 1234,2480", self._basic_query_out) def testOneline(self): self.clicomm("--product foo --oneline", self._oneline_out) def testOutputFormat(self): self.clicomm("--product foo --outputformat " "%{bug_id}:%{blockedby}:%{bug_status}:%{short_desc}:" "%{status_whiteboard}:%{product}:%{rep_platform}", self._output_format_out) def testBugStatusALL(self): self.clicomm("--product foo --bug_status ALL", self._status_all_out) def testBugStatusDEV(self): self.clicomm("--bug_status DEV", self._status_dev_out) def testBugStatusQE(self): self.clicomm("--bug_status QE", self._status_qe_out) def testBugStatusEOL(self): self.clicomm("--bug_status EOL", self._status_eol_out) def testBugStatusOPEN(self): self.clicomm("--bug_status OPEN", self._status_open_out) def testBugStatusRegular(self): self.clicomm("--bug_status POST", self._status_post_out) def testEmailOptions(self): cmd = ("--cc foo1@example.com " "--assigned_to foo2@example.com " "--reporter foo3@example.com " "--qa_contact foo7@example.com") self.clicomm(cmd, self._email_out) self.clicomm(cmd + " --emailtype notsubstring", self._email_type_out) def testComponentsFile(self): self.clicomm("--components_file " + os.getcwd() + "/tests/data/components_file.txt", self._components_file_out) def testKeywords(self): self.clicomm("--keywords Triaged " "--url http://example.com --url_type foo", self._keywords_out) def testBooleanChart(self): self.clicomm("--boolean_query 'keywords-substring-Partner & " "keywords-notsubstring-OtherQA' " "--boolean_query 'foo-bar-baz | foo-bar-wee' " "--boolean_query '! foo-bar-yargh'", None) def testLongDesc(self): self.clicomm("--long_desc 'foobar'", self._longdesc_out) def testQuicksearch(self): self.clicomm("--quicksearch 'foo bar baz'", self._quicksearch_out) def testSavedsearch(self): self.clicomm("--savedsearch 'my saved search' " "--savedsearch-sharer-id 123456", self._savedsearch_out) def testSubComponent(self): self.clicomm("--component lvm2,kernel " "--sub-component 'Command-line tools (RHEL5)'", self._sub_component_out) # Test data. This is what subclasses need to fill in bz = bz34 _basic_query_out = {'product': ['foo'], 'component': ['foo', 'bar'], 'id': ["1234", "2480"]} _oneline_out = {'product': ['foo']} _output_format_out = {'product': ['foo']} output_format_out = _output_format_out _status_all_out = {'product': ['foo']} _status_dev_out = {'bug_status': ['NEW', 'ASSIGNED', 'NEEDINFO', 'ON_DEV', 'MODIFIED', 'POST', 'REOPENED']} _status_qe_out = {'bug_status': ['ASSIGNED', 'ON_QA', 'FAILS_QA', 'PASSES_QA']} _status_eol_out = {'bug_status': ['VERIFIED', 'RELEASE_PENDING', 'CLOSED']} _status_open_out = {'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', 'ON_QA', 'VERIFIED', 'RELEASE_PENDING', 'POST']} _status_post_out = {'bug_status': ['POST']} _email_out = {'assigned_to': 'foo2@example.com', 'cc': ["foo1@example.com"], 'reporter': "foo3@example.com", "qa_contact": "foo7@example.com"} _email_type_out = { 'email1': ['foo1@example.com'], 'email2': "foo2@example.com", 'email3': 'foo3@example.com', 'email4': 'foo7@example.com', 'emailtype1': 'notsubstring', 'emailtype2': 'notsubstring', 'emailtype3': 'notsubstring', 'emailtype4': 'notsubstring', 'emailcc1': True, 'emailassigned_to2': True, 'emailreporter3': True, 'emailqa_contact4': True, 'query_format': 'advanced'} _components_file_out = {'component': ["foo", "bar", "baz"]} _keywords_out = {'query_format': 'advanced', 'field0-0-0': 'keywords', 'value0-0-0': 'Triaged', 'field1-0-0': 'bug_file_loc', 'value1-0-0': 'http://example.com', 'type0-0-0': 'substring', 'type1-0-0': 'foo'} _longdesc_out = {'longdesc': 'foobar', 'longdesc_type': 'allwordssubstr', 'query_format': 'advanced'} _quicksearch_out = {'quicksearch': 'foo bar baz'} _savedsearch_out = {'savedsearch': "my saved search", 'sharer_id': "123456"} _sub_component_out = {'component': ["lvm2", "kernel"], 'sub_components': ["Command-line tools (RHEL5)"]} class BZ4Test(BZ34Test): bz = bz4 _default_includes = ['assigned_to', 'id', 'status', 'summary'] _basic_query_out = BZ34Test._basic_query_out.copy() _basic_query_out["include_fields"] = _default_includes _oneline_out = BZ34Test._oneline_out.copy() _oneline_out["include_fields"] = ['assigned_to', 'blocks', 'component', 'flags', 'keywords', 'status', 'target_milestone', 'id'] _output_format_out = BZ34Test._output_format_out.copy() _output_format_out["include_fields"] = ['product', 'summary', 'platform', 'status', 'id', 'blocks', 'whiteboard'] _status_all_out = BZ34Test._status_all_out.copy() _status_all_out["include_fields"] = _default_includes _status_dev_out = BZ34Test._status_dev_out.copy() _status_dev_out["include_fields"] = _default_includes _status_qe_out = BZ34Test._status_qe_out.copy() _status_qe_out["include_fields"] = _default_includes _status_eol_out = BZ34Test._status_eol_out.copy() _status_eol_out["include_fields"] = _default_includes _status_open_out = BZ34Test._status_open_out.copy() _status_open_out["include_fields"] = _default_includes _status_post_out = BZ34Test._status_post_out.copy() _status_post_out["include_fields"] = _default_includes _email_out = BZ34Test._email_out.copy() _email_out["include_fields"] = _default_includes _email_type_out = BZ34Test._email_type_out.copy() _email_type_out["include_fields"] = _default_includes _components_file_out = BZ34Test._components_file_out.copy() _components_file_out["include_fields"] = _default_includes _keywords_out = BZ34Test._keywords_out.copy() _keywords_out["include_fields"] = _default_includes _longdesc_out = BZ34Test._longdesc_out.copy() _longdesc_out["include_fields"] = _default_includes _quicksearch_out = BZ34Test._quicksearch_out.copy() _quicksearch_out["include_fields"] = _default_includes _savedsearch_out = BZ34Test._savedsearch_out.copy() _savedsearch_out["include_fields"] = _default_includes _sub_component_out = BZ34Test._sub_component_out.copy() _sub_component_out["include_fields"] = _default_includes class RHBZTest(BZ4Test): bz = rhbz4 _output_format_out = BZ34Test.output_format_out.copy() _output_format_out["include_fields"] = ['product', 'summary', 'platform', 'status', 'id', 'blocks', 'whiteboard'] _booleans_out = {} def testTranslation(self): def translate(_in): _out = copy.deepcopy(_in) self.bz.pre_translation(_out) return _out in_query = { "fixed_in": "foo.bar", "product": "some-product", "cf_devel_whiteboard": "some_devel_whiteboard", "include_fields": ["fixed_in", "components", "cf_devel_whiteboard"], } out_query = translate(in_query) in_query["include_fields"] = [ "cf_devel_whiteboard", "cf_fixed_in", "component", "id"] self.assertDictEqual(in_query, out_query) in_query = {"bug_id": "123,456", "component": "foo,bar"} out_query = translate(in_query) self.assertEqual(out_query["id"], ["123", "456"]) self.assertEqual(out_query["component"], ["foo", "bar"]) in_query = {"bug_id": [123, 124], "column_list": ["id"]} out_query = translate(in_query) self.assertEqual(out_query["id"], [123, 124]) self.assertEqual(out_query["include_fields"], in_query["column_list"]) def testInvalidBoolean(self): self.assertRaises(RuntimeError, self.bz.build_query, boolean_query="foobar") def testBooleans(self): out = { 'query_format': 'advanced', 'type0-0-0': 'substring', 'type1-0-0': 'substring', 'type2-0-0': 'substring', 'type3-0-0': 'substring', 'value0-0-0': '123456', 'value1-0-0': 'needinfo & devel_ack', 'value2-0-0': '! baz foo', 'value3-0-0': 'foobar | baz', 'field0-0-0': 'blocked', 'field1-0-0': 'flagtypes.name', 'field2-0-0': 'cf_qa_whiteboard', 'field3-0-0': 'cf_devel_whiteboard', 'include_fields': ['assigned_to', 'id', 'status', 'summary'], } import bugzilla import logging log = logging.getLogger(bugzilla.__name__) handlers = log.handlers try: log.handlers = [] self.clicomm("--blocked 123456 " "--devel_whiteboard 'foobar | baz' " "--qa_whiteboard '! baz foo' " "--flag 'needinfo & devel_ack'", out) finally: log.handlers = handlers class TestURLToQuery(BZ34Test): def _check(self, url, query): self.assertDictEqual(bz4.url_to_query(url), query) def testSavedSearch(self): url = ("https://bugzilla.redhat.com/buglist.cgi?" "cmdtype=dorem&list_id=2342312&namedcmd=" "RHEL7%20new%20assigned%20virt-maint&remaction=run&" "sharer_id=321167") query = { 'sharer_id': '321167', 'savedsearch': 'RHEL7 new assigned virt-maint' } self._check(url, query) def testStandardQuery(self): url = ("https://bugzilla.redhat.com/buglist.cgi?" "component=virt-manager&query_format=advanced&classification=" "Fedora&product=Fedora&bug_status=NEW&bug_status=ASSIGNED&" "bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&" "bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=" "RELEASE_PENDING&bug_status=POST&order=bug_status%2Cbug_id") query = { 'product': 'Fedora', 'query_format': 'advanced', 'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', 'ON_QA', 'VERIFIED', 'FAILS_QA', 'RELEASE_PENDING', 'POST'], 'classification': 'Fedora', 'component': 'virt-manager', 'order': 'bug_status,bug_id' } self._check(url, query) python-bugzilla-2.1.0/tests/createbug.py0000664000175100017510000000542313062015413022000 0ustar crobinsocrobinso00000000000000# # Copyright Red Hat, Inc. 2013 # # This work is licensed under the terms of the GNU GPL, version 2 or later. # See the COPYING file in the top-level directory. # ''' Unit tests for building createbug dictionaries with bin/bugzilla ''' import unittest import tests bz4 = tests.make_bz("4.0.0") class CreatebugTest(unittest.TestCase): maxDiff = None bz = bz4 def assertDictEqual(self, *args, **kwargs): # EPEL5 back compat if hasattr(unittest.TestCase, "assertDictEqual"): return unittest.TestCase.assertDictEqual(self, *args, **kwargs) return self.assertEqual(*args, **kwargs) def clicomm(self, argstr, out): comm = "bugzilla new --test-return-result " + argstr if out is None: self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) else: q = tests.clicomm(comm, self.bz, returnmain=True) self.assertDictEqual(out, q) def testBasic(self): self.clicomm( "--product foo --component bar --summary baz --version 12", {'component': 'bar', 'product': 'foo', 'summary': 'baz', 'version': '12'} ) def testOpSys(self): self.clicomm( "--os windowsNT --arch ia64 --comment 'youze a foo' --cc me", {'description': 'youze a foo', 'op_sys': 'windowsNT', 'platform': 'ia64', 'cc': ["me"]} ) def testSeverity(self): self.clicomm( "--severity HIGH --priority Low --url http://example.com", {'url': 'http://example.com', 'priority': 'Low', 'severity': 'HIGH'} ) def testMisc(self): self.clicomm( "--alias some-alias", {"alias": "some-alias"} ) def testMultiOpts(self): # Test all opts that can take lists out = {'blocks': ['3', '4'], 'cc': ['1', '2'], 'depends_on': ['5', 'foo', 'wib'], 'groups': ['bar', '8'], 'keywords': ['TestOnly', 'ZStream']} self.clicomm( "--cc 1,2 --blocked 3,4 --dependson 5,foo,wib --groups bar,8 " "--keywords TestOnly,ZStream", out ) self.clicomm( "--cc 1 --cc 2 --blocked 3 --blocked 4 " "--dependson 5,foo --dependson wib --groups bar --groups 8 " "--keywords TestOnly --keywords ZStream", out ) def testFieldConversion(self): vc = self.bz._validate_createbug # pylint: disable=protected-access out = vc(product="foo", component="bar", version="12", description="foo", short_desc="bar", check_args=False) self.assertDictEqual(out, {'component': 'bar', 'description': 'foo', 'product': 'foo', 'summary': 'bar', 'version': '12'}) python-bugzilla-2.1.0/tests/bug.py0000664000175100017510000000455513062015413020621 0ustar crobinsocrobinso00000000000000# # Copyright Red Hat, Inc. 2014 # # This work is licensed under the terms of the GNU GPL, version 2 or later. # See the COPYING file in the top-level directory. # ''' Unit tests for testing some bug.py magic ''' import pickle import sys import unittest import tests from tests import StringIO from bugzilla.bug import Bug rhbz = tests.make_bz("4.4.0", rhbz=True) class BugTest(unittest.TestCase): maxDiff = None bz = rhbz def testBasic(self): data = { "bug_id": 123456, "status": "NEW", "assigned_to": "foo@bar.com", "component": "foo", "product": "bar", "short_desc": "some short desc", "cf_fixed_in": "nope", "fixed_in": "1.2.3.4", "devel_whiteboard": "some status value", } bug = Bug(bugzilla=self.bz, dict=data) def _assert_bug(): self.assertEqual(hasattr(bug, "component"), True) self.assertEqual(getattr(bug, "components"), ["foo"]) self.assertEqual(getattr(bug, "product"), "bar") self.assertEqual(hasattr(bug, "short_desc"), True) self.assertEqual(getattr(bug, "summary"), "some short desc") self.assertEqual(bool(getattr(bug, "cf_fixed_in")), True) self.assertEqual(getattr(bug, "fixed_in"), "1.2.3.4") self.assertEqual(bool(getattr(bug, "cf_devel_whiteboard")), True) self.assertEqual(getattr(bug, "devel_whiteboard"), "some status value") _assert_bug() self.assertEqual(str(bug), "#123456 NEW - foo@bar.com - some short desc") self.assertTrue(repr(bug).startswith("= 3: from io import BytesIO fd = BytesIO() else: fd = StringIO() pickle.dump(bug, fd) fd.seek(0) bug = pickle.load(fd) self.assertEqual(getattr(bug, "bugzilla"), None) bug.bugzilla = self.bz _assert_bug() def testBugNoID(self): try: Bug(bugzilla=self.bz, dict={"component": "foo"}) raise AssertionError("Expected lack of ID failure.") except TypeError: pass python-bugzilla-2.1.0/tests/modify.py0000664000175100017510000001536113062015413021330 0ustar crobinsocrobinso00000000000000# # Copyright Red Hat, Inc. 2013 # # This work is licensed under the terms of the GNU GPL, version 2 or later. # See the COPYING file in the top-level directory. # ''' Unit tests for building update dictionaries with 'bugzilla modify' ''' import unittest import tests rhbz = tests.make_bz("4.4.0", rhbz=True) class ModifyTest(unittest.TestCase): maxDiff = None bz = rhbz def assertDictEqual(self, *args, **kwargs): # EPEL5 back compat if hasattr(unittest.TestCase, "assertDictEqual"): return unittest.TestCase.assertDictEqual(self, *args, **kwargs) return self.assertEqual(*args, **kwargs) def clicomm(self, argstr, out, wbout=None, tags_add=None, tags_rm=None): comm = "bugzilla modify --test-return-result 123456 224466 " + argstr # pylint: disable=unpacking-non-sequence if out is None: self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) else: (mdict, wdict, tagsa, tagsr) = tests.clicomm( comm, self.bz, returnmain=True) if wbout: self.assertDictEqual(wbout, wdict) if out: self.assertDictEqual(out, mdict) if tags_add: self.assertEqual(tags_add, tagsa) if tags_rm: self.assertEqual(tags_rm, tagsr) def testBasic(self): self.clicomm( "--component foocomp --product barprod --status ASSIGNED " "--assignee foo@example.com --qa_contact bar@example.com " "--comment 'hey some comment'", {'assigned_to': 'foo@example.com', 'comment': {'comment': 'hey some comment'}, 'component': 'foocomp', 'product': 'barprod', 'qa_contact': 'bar@example.com', 'status': 'ASSIGNED'} ) def testPrivateComment(self): self.clicomm( "--comment 'hey private comment' --private", {'comment': {'comment': 'hey private comment', 'is_private': True}} ) def testClose(self): self.clicomm( "--close CANTFIX", {'resolution': 'CANTFIX', 'status': 'CLOSED'} ) self.clicomm( "--dupeid 111333", {'dupe_of': 111333, 'resolution': 'DUPLICATE', 'status': 'CLOSED'} ) def testFlags(self): self.clicomm( "--flag needinfoX --flag dev_ack+ --flag qa_ack-", {"flags": [ {'status': 'X', 'name': 'needinfo'}, {'status': '+', 'name': 'dev_ack'}, {'status': '-', 'name': 'qa_ack'} ]} ) def testWhiteboard(self): self.clicomm( "--whiteboard tagfoo --whiteboard=-tagbar", {}, wbout={"whiteboard": (["tagfoo"], ["tagbar"])} ) self.clicomm( "--whiteboard =foo --whiteboard =thisone", {'whiteboard': 'thisone'} ) self.clicomm( "--qa_whiteboard =yo-qa --qa_whiteboard=-foo " "--internal_whiteboard =internal-hey --internal_whiteboard +bar " "--devel_whiteboard =devel-duh --devel_whiteboard=-yay " "--tags foo1 --tags=-remove2", {'cf_devel_whiteboard': 'devel-duh', 'cf_internal_whiteboard': 'internal-hey', 'cf_qa_whiteboard': 'yo-qa'}, wbout={ "qa_whiteboard": ([], ["foo"]), "internal_whiteboard": (["bar"], []), "devel_whiteboard": ([], ["yay"]) }, tags_add=["foo1"], tags_rm=["remove2"], ) def testMisc(self): self.clicomm( "--fixed_in foo-bar-1.2.3 --reset-qa-contact --reset-assignee", {"cf_fixed_in": "foo-bar-1.2.3", 'reset_assigned_to': True, 'reset_qa_contact': True} ) self.clicomm( "--groups +foo --groups=-bar,baz --groups fribby", {'groups': {'add': ['foo', 'fribby'], 'remove': ['bar', 'baz']}} ) self.clicomm( "--target_milestone foomile --target_release relfoo", {"target_milestone": "foomile", "target_release": "relfoo"}, ) self.clicomm( "--priority medium --severity high", {"priority": "medium", "severity": "high"}, ) self.clicomm( "--os Windows --arch ia64 --version 1000 --url http://example.com " "--summary 'foo summary'", {"op_sys": "Windows", "platform": "ia64", "version": "1000", "url": "http://example.com", "summary": 'foo summary'}, ) self.clicomm( "--alias some-alias", {"alias": "some-alias"} ) def testField(self): self.clicomm( "--field cf_fixed_in=foo-bar-1.2.4", {"cf_fixed_in": "foo-bar-1.2.4"} ) self.clicomm( "--field cf_fixed_in=foo-bar-1.2.5 --field=cf_release_notes=blah", {"cf_fixed_in": "foo-bar-1.2.5", "cf_release_notes": "blah"} ) def testDepends(self): self.clicomm( "--dependson 100,200", {'depends_on': {'add': [100, 200]}} ) self.clicomm( "--dependson +100,200", {'depends_on': {'add': [100, 200]}} ) self.clicomm( "--dependson=-100,200", {'depends_on': {'remove': [100, 200]}} ) self.clicomm( "--dependson =100,200", {'depends_on': {'set': [100, 200]}} ) self.clicomm( "--dependson 1 --dependson=-2 --dependson +3 --dependson =4", {'depends_on': {'add': [1, 3], 'remove': [2], 'set': [4]}} ) self.clicomm( "--blocked 5 --blocked -6 --blocked +7 --blocked =8,9", {'blocks': {'add': [5, 7], 'remove': [6], 'set': [8, 9]}} ) self.clicomm( "--keywords foo --keywords=-bar --keywords +baz --keywords =yay", {'keywords': {'add': ["foo", "baz"], 'remove': ["bar"], 'set': ["yay"]}} ) self.clicomm("--keywords =", {'keywords': {'set': []}}) def testCC(self): self.clicomm( "--cc foo@example.com --cc=-minus@example.com " "--cc =foo@example.com --cc +foo@example.com", {'cc': {'add': ['foo@example.com', "=foo@example.com", "+foo@example.com"], 'remove': ["minus@example.com"]}}, ) def testSubComponents(self): self.clicomm("--component foo --sub-component 'bar baz'", {"component": "foo", "sub_components": {"foo": ["bar baz"]}}) def testSubComponentFail(self): self.assertRaises(ValueError, self.bz.build_update, sub_component="some sub component") python-bugzilla-2.1.0/tests/pylint.cfg0000664000175100017510000001543213062015413021466 0ustar crobinsocrobinso00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Add files or directories to the blacklist. They should be base names, not # paths. #ignore= # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [MESSAGES CONTROL] # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. #output-format=text # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. generated-members=REQUEST,acl_users,aq_parent [FORMAT] # Maximum number of characters on a single line. max-line-length=80 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the beginning of the name of dummy variables # (i.e. not used). dummy-variables-rgx=ignore.*|.*_ignore # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins=_ [BASIC] # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=__.*__ [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branchs=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=Exception python-bugzilla-2.1.0/bugzilla.10000664000175100017510000002342713067311773020237 0ustar crobinsocrobinso00000000000000.TH bugzilla 1 "Mar 30, 2017" "version 2.1.0" "User Commands" .SH NAME bugzilla \- command-line interface to Bugzilla over XML-RPC .SH SYNOPSIS .B bugzilla [\fIoptions\fR] [\fIcommand\fR] [\fIcommand-options\fR] .SH DESCRIPTION .PP .BR bugzilla is a command-line utility that allows access to the XML-RPC interface provided by Bugzilla. .PP \fIcommand\fP is one of: .br .I \fR * login - log into the given bugzilla instance .br .I \fR * new - create a new bug .br .I \fR * query - search for bugs matching given criteria .br .I \fR * modify - modify existing bugs .br .I \fR * attach - attach files to existing bugs, or get attachments .br .I \fR * info - get info about the given bugzilla instance .SH GLOBAL OPTIONS .IP "--version" show program's version number and exit .IP "--help, -h" show this help message and exit .IP "--bugzilla=BUGZILLA" bugzilla XMLRPC URI. default: https://bugzilla.redhat.com/xmlrpc.cgi .IP "--nosslverify" Don't error on invalid bugzilla SSL certificate .IP "--login" Run interactive "login" before performing the specified command. .IP "--username=USERNAME" Log in with this username .IP "--password=PASSWORD" Log in with this password .IP "--ensure-logged-in" Raise an error if we aren't logged in to bugzilla. Consider using this if you are depending on cached credentials, to ensure that when they expire the tool errors, rather than subtly change output. .IP "--no-cache-credentials" Don't save any bugzilla cookies or tokens to disk, and don't use any pre-existing credentials. .IP "--cookiefile=COOKIEFILE" cookie file to use for bugzilla authentication .IP "--tokenfile=TOKENFILE" token file to use for bugzilla authentication .IP "--verbose" give more info about what's going on .IP "--debug" output bunches of debugging info .IP "--version" show program's version number and exit .SH Standard bugzilla options .PP These options are shared by some combination of the 'new', 'query', and 'modify' sub commands. Not every option works for each command though. .IP "--product=PRODUCT, -p PRODUCT" Product name .IP "--version=VERSION, -v VERSION" Product version .IP "--component=COMPONENT, -c COMPONENT" Component name .IP "--summary=SUMMARY, -s SUMMARY, --short_desc=SUMMARY" Bug summary .IP "--comment=DESCRIPTION, -l DESCRIPTION" Set initial bug comment/description .IP "--sub-component=SUB_COMPONENT" RHBZ sub component name .IP "--os=OS, -o OS" Operating system .IP "--arch=ARCH" Arch this bug occurs on .IP "--severity=SEVERITY, -x SEVERITY" Bug severity .IP "--priority=PRIORITY, -z PRIORITY" Bug priority .IP "--alias=ALIAS" Bug alias (name) .IP "--status=STATUS, -s STATUS, --bug_status=STATUS" Bug status (NEW, ASSIGNED, etc.) .IP "--url=URL, -u URL" URL for further bug info .IP "--target_milestone=TARGET_MILESTONE, -m TARGET_MILESTONE" Target milestone .IP "--target_release=TARGET_RELEASE" RHBZ Target release .IP "--blocked=BUGID[, BUGID, ...]" Bug IDs that this bug blocks .IP "--dependson=BUGID[, BUGID, ...]" Bug IDs that this bug depends on .IP "--keywords=KEYWORD[, KEYWORD, ...]" Bug keywords .IP "--groups=GROUP[, GROUP, ...]" Which user groups can view this bug .IP "--cc=CC[, CC, ...]" CC list .IP "--assigned_to=ASSIGNED_TO, -a ASSIGNED_TO, --assignee ASSIGNED_TO" Bug assignee .IP "--qa_contact=QA_CONTACT, -q QA_CONTACT" QA contact .IP "--whiteboard WHITEBOARD, -w WHITEBOARD, --status_whiteboard WHITEBOARD" Whiteboard field .IP "--devel_whiteboard DEVEL_WHITEBOARD" RHBZ devel whiteboard field .IP "--internal_whiteboard INTERNAL_WHITEBOARD" RHBZ internal whiteboard field .IP "--qa_whiteboard QA_WHITEBOARD" RHBZ QA whiteboard field .IP "--fixed_in FIXED_IN, -F FIXED_IN RHBZ 'Fixed in version' field .IP "--field=FIELD=VALUE" Manually specify a bugzilla XMLRPC field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE .SH Output options .PP These options are shared by several commands, for tweaking the text output of the command results. .IP "--full, -f" output detailed bug info .IP "--ids, -i" output only bug IDs .IP "--extra, -e" output additional bug information (keywords, Whiteboards, etc.) .IP "--oneline" one line summary of the bug (useful for scripts) .IP "--raw" raw output of the bugzilla contents .IP "--outputformat=OUTPUTFORMAT" Print output in the form given. You can use RPM-style tags that match bug fields, e.g.: '%{id}: %{summary}'. The output of the bugzilla tool should NEVER BE PARSED unless you are using a custom --outputformat. For everything else, just don't parse it, the formats are not stable and are subject to change. --outputformat allows printing arbitrary bug data in a user preferred format. For example, to print a returned bug ID, component, and product, separated with ::, do: --outputformat "%{id}::%{component}::%{product}" The fields (like 'id', 'component', etc.) are the names of the values returned by bugzilla's XMLRPC interface. To see a list of all fields, check the API documentation in the 'SEE ALSO' section. Alternatively, run a 'bugzilla --debug query ...' and look at the key names returned in the query results. Also, in most cases, using the name of the associated command line switch should work, like --bug_status becomes %{bug_status}, etc. .SH \[oq]query\[cq] specific options Certain options can accept a comma separated list to query multiple values, including --status, --component, --product, --version, --id. Note: querying via explicit command line options will only get you so far. See the --from-url option for a way to use powerful Web UI queries from the command line. .IP "--id ID, -b ID, --bug_id ID" specify individual bugs by IDs, separated with commas .IP "--reporter REPORTER, -r REPORTER" Email: search reporter email for given address .IP "--quicksearch QUICKSEARCH" Search using bugzilla's quicksearch functionality. .IP "--savedsearch SAVEDSEARCH" Name of a bugzilla saved search. If you don't own this saved search, you must passed --savedsearch_sharer_id. .IP "--savedsearch-sharer-id SAVEDSEARCH_SHARER_ID" Owner ID of the --savedsearch. You can get this ID from the URL bugzilla generates when running the saved search from the web UI. .IP "--from-url WEB_QUERY_URL" Make a working query via bugzilla's 'Advanced search' web UI, grab the url from your browser (the string with query.cgi or buglist.cgi in it), and --from-url will run it via the bugzilla API. Don't forget to quote the string! This only works for Bugzilla 5 and Red Hat bugzilla .SH \[oq]modify\[cq] specific options Fields that take multiple values have a special input format. Append: --cc=foo@example.com Overwrite: --cc==foo@example.com Remove: --cc=-foo@example.com Options that accept this format: --cc, --blocked, --dependson, --groups, --tags, whiteboard fields. .IP "--close RESOLUTION, -k RESOLUTION" Close with the given resolution (WONTFIX, NOTABUG, etc.) .IP "--dupeid ORIGINAL, -d ORIGINAL" ID of original bug. Implies --close DUPLICATE .IP "--private" Mark new comment as private .IP "--reset-assignee" Reset assignee to component default .IP "--reset-qa-contact" Reset QA contact to component default .SH \[oq]attach\[cq] options .IP "--file=FILENAME, -f FILENAME" File to attach, or filename for data provided on stdin .IP "--description=DESCRIPTION, -d DESCRIPTION" A short description of the file being attached .IP "--type=MIMETYPE, -t MIMETYPE" Mime-type for the file being attached .IP "--get=ATTACHID, -g ATTACHID" Download the attachment with the given ID .IP "--getall=BUGID, --get-all=BUGID" Download all attachments on the given bug .SH \[oq]info\[cq] options .IP "--products, -p" Get a list of products .IP "--components=PRODUCT, -c PRODUCT" List the components in the given product .IP "--component_owners=PRODUCT, -o PRODUCT" List components (and their owners) .IP "--versions=VERSION, -v VERSION" List the versions for the given product .SH AUTHENTICATION COOKIES AND TOKENS Older bugzilla instances use cookie-based authentication, and newer bugzilla instances (around 5.0) use a non-cookie token system. When you log into bugzilla with the "login" subcommand or the "--login" argument, we cache the login credentials in ~/.cache/python-bugzilla/ Previously we cached credentials in ~/.. If you want to see which file the tool is using, check --debug output. To perform an authenticated bugzilla command on a new machine, run a one time "bugzilla login" to cache credentials before running the desired command. You can also run "bugzilla --login" and the login process will be initiated before invoking the command. Additionally, the --no-cache-credentials option will tell the bugzilla tool to _not_ save any credentials in $HOME, or use any previously cached credentials. .SH EXAMPLES .PP .RS 0 bugzilla query --bug_id 62037 bugzilla query --version 15 --component python-bugzilla # All boolean options can be formatted like this .br bugzilla query --blocked "123456 | 224466" bugzilla login bugzilla new -p Fedora -v rawhide -c python-bugzilla \\ --summary "python-bugzilla causes headaches" \\ --comment "python-bugzilla made my brain hurt when I used it." bugzilla attach --file ~/Pictures/cam1.jpg --desc "me, in pain" $BUGID bugzilla attach --getall $BUGID bugzilla modify --close NOTABUG --comment "Actually, you're hungover." $BUGID .SH EXIT STATUS .BR bugzilla normally returns 0 if the requested command was successful. Otherwise, exit status is 1 if .BR bugzilla is interrupted by the user (or a login attempt fails), 2 if a socket error occurs (e.g. TCP connection timeout), and 3 if the server returns an XML-RPC fault. .SH BUGS Please report any bugs as github issues at .br https://github.com/python-bugzilla/python-bugzilla .br to the mailing list at .br https://fedorahosted.org/mailman/listinfo/python-bugzilla .SH SEE ALSO .nf https://bugzilla.readthedocs.io/en/latest/api/index.html https://bugzilla.redhat.com/docs/en/html/api/Bugzilla/WebService/Bug.html python-bugzilla-2.1.0/xmlrpc-api-notes.txt0000664000175100017510000000670513062015413022272 0ustar crobinsocrobinso00000000000000 Fedora infrastructure depends on python-bugzilla in various ways: http://lists.fedorahosted.org/pipermail/python-bugzilla/2012-June/000001.html Red Hat bugzilla originally had a totally custom API. Much of that is being dropped in 2013, API conversions outlined here: https://bugzilla.redhat.com/show_bug.cgi?id=822007 Externally facing RH bugzilla instance that doesn't send email and is refreshed periodically. This is what is used in the functional test suite: http://partner-bugzilla.redhat.com Some trackers in the wild to use for API testing: bugzilla.redhat.com bugzilla.mozilla.org bugzilla.kernel.org bugzilla.gnome.org bugs.freedesktop.org bugzilla.novell.com bugzilla.zimbra.com bugzilla.samba.org bugs.gentoo.org Upstream timeline ================= Here's a timeline of the evolution of the upstream bugzilla XMLRPC API: Bugzilla 2.*: No XMLRPC API that I can tell Bugzilla 3.0: http://www.bugzilla.org/docs/3.0/html/api/index.html Bug.legal_values Bug.get_bugs: returns: id, alias, summary, creation_time, last_change_time Bug.create Bugzilla.version Bugzilla.timezone Product.get_selectable_products Product.get_enterable_products Product.get_accessible_products Product.get_products User.login User.logout User.offer_account_by_email User.create Bugzilla 3.2: http://www.bugzilla.org/docs/3.2/en/html/api/ Bug: RENAME: get_bugs->get, get_bugs should still work Bug.add_comment Bugzilla.extensions Product: RENAME: get_products->get, get_products should still work Bugzilla 3.4: http://www.bugzilla.org/docs/3.4/en/html/api/ Bug.comments Bug.history Bug.search Bug.update_see_also Bugzilla.time Bugzilla: DEPRECATED: timezone, use time instead User.get Util.filter_fields Util.validate Bugzilla 3.6: http://www.bugzilla.org/docs/3.6/en/html/api/ Bug.attachments Bug.fields Bug: DEPRECATED: legal_values Bugzilla: timezone now always returns UTC+0000 Bugzilla 4.0: http://www.bugzilla.org/docs/4.0/en/html/api/ Bug.add_attachment Bug.update Util.filter_wants Bugzilla 4.2: http://www.bugzilla.org/docs/4.2/en/html/api/ Group.create Product.create Bugzilla 4.4: http://www.bugzilla.org/docs/4.4/en/html/api/ Bug.update_tags Bugzilla.parameters Bugzilla.last_audit_time Classification.get Group.update Product.update User.update Util.translate Util.params_to_objects Bugzilla 5.0: (July 2015) https://www.bugzilla.org/docs/5.0/en/html/integrating/api/index.html Bug.update_attachment Bug.search/update_comment_tags Bug.search: search() now supports --from-url style, like rhbz before it search() now supports quicksearch Bug.update: update() alias is now a hash of add/remove/set, but has back compat update() can take 'flags' config now Component (new, or newly documented?) Component.create User.valid_login Bugzilla latest/tip: https://bugzilla.readthedocs.io/en/latest/api/index.html Redhat Bugzilla: 4.4 based with extensions. Bits on top of 4.4 https://bugzilla.redhat.com/docs/en/html/api/ Bug.search has --from-url extension Bug.update has more hashing support extra_fields for fetching comments, attachments, etc at Bug.get time ExternalBugs extension: https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html python-bugzilla-2.1.0/bin/0000775000175100017510000000000013067312016017073 5ustar crobinsocrobinso00000000000000python-bugzilla-2.1.0/bin/bugzilla0000775000175100017510000012015013067305775020647 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python # # bugzilla - a commandline frontend for the python bugzilla module # # Copyright (C) 2007-2017 Red Hat Inc. # Author: Will Woods # Author: Cole Robinson # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. from __future__ import print_function import locale from logging import getLogger, DEBUG, INFO, WARN, StreamHandler, Formatter import argparse import os import re import socket import sys import tempfile if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: # pylint: disable=F0401,W0622,E0611 from xmlrpc.client import Fault, ProtocolError from urllib.parse import urlparse basestring = (str, bytes) else: from xmlrpclib import Fault, ProtocolError from urlparse import urlparse import requests.exceptions import bugzilla DEFAULT_BZ = 'https://bugzilla.redhat.com/xmlrpc.cgi' _is_unittest = bool(os.getenv("__BUGZILLA_UNITTEST")) _is_unittest_debug = bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}") log = getLogger(bugzilla.__name__) handler = StreamHandler(sys.stderr) handler.setFormatter(Formatter( "[%(asctime)s] %(levelname)s (%(module)s:%(lineno)d) %(message)s", "%H:%M:%S")) log.addHandler(handler) ################ # Util helpers # ################ def to_encoding(ustring): string = '' if isinstance(ustring, basestring): string = ustring elif ustring is not None: string = str(ustring) if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: return string preferred = locale.getpreferredencoding() if _is_unittest: preferred = "UTF-8" return string.encode(preferred, 'replace') def open_without_clobber(name, *args): '''Try to open the given file with the given mode; if that filename exists, try "name.1", "name.2", etc. until we find an unused filename.''' fd = None count = 1 orig_name = name while fd is None: try: fd = os.open(name, os.O_CREAT | os.O_EXCL, 0o666) except OSError: err = sys.exc_info()[1] if err.errno == os.errno.EEXIST: name = "%s.%i" % (orig_name, count) count += 1 else: raise IOError(err.errno, err.strerror, err.filename) fobj = open(name, *args) if fd != fobj.fileno(): os.close(fd) return fobj def get_default_url(): """ Grab a default URL from bugzillarc [DEFAULT] url=X """ from bugzilla.base import _open_bugzillarc cfg = _open_bugzillarc() if cfg: cfgurl = cfg.defaults().get("url", None) if cfgurl is not None: log.debug("bugzillarc: found cli url=%s", cfgurl) return cfgurl return DEFAULT_BZ ################## # Option parsing # ################## def _setup_root_parser(): epilog = 'Try "bugzilla COMMAND --help" for command-specific help.' p = argparse.ArgumentParser(epilog=epilog) default_url = get_default_url() # General bugzilla connection options p.add_argument('--bugzilla', default=default_url, help="bugzilla XMLRPC URI. default: %s" % default_url) p.add_argument("--nosslverify", dest="sslverify", action="store_false", default=True, help="Don't error on invalid bugzilla SSL certificate") p.add_argument('--login', action="store_true", help='Run interactive "login" before performing the ' 'specified command.') p.add_argument('--username', help="Log in with this username") p.add_argument('--password', help="Log in with this password") p.add_argument('--ensure-logged-in', action="store_true", help="Raise an error if we aren't logged in to bugzilla. " "Consider using this if you are depending on " "cached credentials, to ensure that when they expire the " "tool errors, rather than subtly change output.") p.add_argument('--no-cache-credentials', action='store_false', default=True, dest='cache_credentials', help="Don't save any bugzilla cookies or tokens to disk, and " "don't use any pre-existing credentials.") p.add_argument('--cookiefile', default=None, help="cookie file to use for bugzilla authentication") p.add_argument('--tokenfile', default=None, help="token file to use for bugzilla authentication") p.add_argument('--verbose', action='store_true', help="give more info about what's going on") p.add_argument('--debug', action='store_true', help="output bunches of debugging info") p.add_argument('--version', action='version', version=bugzilla.__version__) # Allow user to specify BZClass to initialize. Kinda weird for the # CLI, I'd rather people file bugs about this so we can fix our detection. # So hide it from the help output but keep it for back compat p.add_argument('--bztype', default='auto', help=argparse.SUPPRESS) return p def _parser_add_output_options(p): outg = p.add_argument_group("Output format options") outg.add_argument('--full', action='store_const', dest='output', const='full', default='normal', help="output detailed bug info") outg.add_argument('-i', '--ids', action='store_const', dest='output', const='ids', help="output only bug IDs") outg.add_argument('-e', '--extra', action='store_const', dest='output', const='extra', help="output additional bug information " "(keywords, Whiteboards, etc.)") outg.add_argument('--oneline', action='store_const', dest='output', const='oneline', help="one line summary of the bug (useful for scripts)") outg.add_argument('--raw', action='store_const', dest='output', const='raw', help="raw output of the bugzilla contents") outg.add_argument('--outputformat', help="Print output in the form given. " "You can use RPM-style tags that match bug " "fields, e.g.: '%%{id}: %%{summary}'. See the man page " "section 'Output options' for more details.") def _parser_add_bz_fields(rootp, command): cmd_new = (command == "new") cmd_query = (command == "query") cmd_modify = (command == "modify") if cmd_new: comment_help = "Set initial bug comment/description" elif cmd_query: comment_help = "Search all bug comments" else: comment_help = "Add new bug comment" p = rootp.add_argument_group("Standard bugzilla options") p.add_argument('-p', '--product', help="Product name") p.add_argument('-v', '--version', help="Product version") p.add_argument('-c', '--component', help="Component name") p.add_argument('-t', '--summary', '--short_desc', help="Bug summary") p.add_argument('-l', '--comment', '--long_desc', help=comment_help) p.add_argument("--sub-component", action="append", help="RHBZ sub component field") p.add_argument('-o', '--os', help="Operating system") p.add_argument('--arch', help="Arch this bug occurs on") p.add_argument('-x', '--severity', help="Bug severity") p.add_argument('-z', '--priority', help="Bug priority") p.add_argument('--alias', help='Bug alias (name)') p.add_argument('-s', '--status', '--bug_status', help='Bug status (NEW, ASSIGNED, etc.)') p.add_argument('-u', '--url', help="URL field") p.add_argument('-m', '--target_milestone', help="Target milestone") p.add_argument('--target_release', help="RHBZ Target release") p.add_argument('--blocked', action="append", help="Bug IDs that this bug blocks") p.add_argument('--dependson', action="append", help="Bug IDs that this bug depends on") p.add_argument('--keywords', action="append", help="Bug keywords") p.add_argument('--groups', action="append", help="Which user groups can view this bug") p.add_argument('--cc', action="append", help="CC list") p.add_argument('-a', '--assigned_to', '--assignee', help="Bug assignee") p.add_argument('-q', '--qa_contact', help='QA contact') if not cmd_new: p.add_argument('-f', '--flag', action='append', help="Bug flags state. Ex:\n" " --flag needinfo?\n" " --flag dev_ack+") p.add_argument("--tags", action="append", help="Tags field.") p.add_argument('-w', "--whiteboard", '--status_whiteboard', action="append", help='Whiteboard field') p.add_argument("--devel_whiteboard", action="append", help='RHBZ devel whiteboard field') p.add_argument("--internal_whiteboard", action="append", help='RHBZ internal whiteboard field') p.add_argument("--qa_whiteboard", action="append", help='RHBZ QA whiteboard field') p.add_argument('-F', '--fixed_in', help="RHBZ 'Fixed in version' field") # Put this at the end, so it sticks out more p.add_argument('--field', metavar="FIELD=VALUE", action="append", dest="fields", help="Manually specify a bugzilla XMLRPC field. FIELD is " "the raw name used by the bugzilla instance. For example if your " "bugzilla instance has a custom field cf_my_field, do:\n" " --field cf_my_field=VALUE") # Used by unit tests, not for end user consumption p.add_argument('--test-return-result', action="store_true", help=argparse.SUPPRESS) if not cmd_modify: _parser_add_output_options(rootp) def _setup_action_new_parser(subparsers): description = ("Create a new bug report. " "--product, --component, --version, --summary, and --comment " "must be specified. " "Options that take multiple values accept comma separated lists, " "including --cc, --blocks, --dependson, --groups, and --keywords.") p = subparsers.add_parser("new", description=description) _parser_add_bz_fields(p, "new") def _setup_action_query_parser(subparsers): description = ("List bug reports that match the given criteria. " "Certain options can accept a comma separated list to query multiple " "values, including --status, --component, --product, --version, --id.") epilog = ("Note: querying via explicit command line options will only " "get you so far. See the --from-url option for a way to use powerful " "Web UI queries from the command line.") p = subparsers.add_parser("query", description=description, epilog=epilog) _parser_add_bz_fields(p, "query") g = p.add_argument_group("'query' specific options") g.add_argument('-b', '--id', '--bug_id', help="specify individual bugs by IDs, separated with commas") g.add_argument('-r', '--reporter', help="Email: search reporter email for given address") g.add_argument('--quicksearch', help="Search using bugzilla's quicksearch functionality.") g.add_argument('--savedsearch', help="Name of a bugzilla saved search. If you don't own this " "saved search, you must passed --savedsearch_sharer_id.") g.add_argument('--savedsearch-sharer-id', help="Owner ID of the --savedsearch. You can get this ID from " "the URL bugzilla generates when running the saved search " "from the web UI.") # Keep this at the end so it sticks out more g.add_argument('--from-url', metavar="WEB_QUERY_URL", help="Make a working query via bugzilla's 'Advanced search' web UI, " "grab the url from your browser (the string with query.cgi or " "buglist.cgi in it), and --from-url will run it via the " "bugzilla API. Don't forget to quote the string! " "This only works for Bugzilla 5 and Red Hat bugzilla") # Deprecated options p.add_argument('-E', '--emailtype', help=argparse.SUPPRESS) p.add_argument('--components_file', help=argparse.SUPPRESS) p.add_argument('-U', '--url_type', help=argparse.SUPPRESS) p.add_argument('-K', '--keywords_type', help=argparse.SUPPRESS) p.add_argument('-W', '--status_whiteboard_type', help=argparse.SUPPRESS) p.add_argument('-B', '--booleantype', help=argparse.SUPPRESS) p.add_argument('--boolean_query', action="append", help=argparse.SUPPRESS) p.add_argument('--fixed_in_type', help=argparse.SUPPRESS) def _setup_action_info_parser(subparsers): description = ("List products or component information about the " "bugzilla server.") p = subparsers.add_parser("info", description=description) p.add_argument('-p', '--products', action='store_true', help='Get a list of products') p.add_argument('-c', '--components', metavar="PRODUCT", help='List the components in the given product') p.add_argument('-o', '--component_owners', metavar="PRODUCT", help='List components (and their owners)') p.add_argument('-v', '--versions', metavar="VERSION", help='List the versions for the given product') def _setup_action_modify_parser(subparsers): usage = ("bugzilla modify [options] BUGID [BUGID...]\n" "Fields that take multiple values have a special input format.\n" "Append: --cc=foo@example.com\n" "Overwrite: --cc==foo@example.com\n" "Remove: --cc=-foo@example.com\n" "Options that accept this format: --cc, --blocked, --dependson,\n" " --groups, --tags, whiteboard fields.") p = subparsers.add_parser("modify", usage=usage) _parser_add_bz_fields(p, "modify") g = p.add_argument_group("'modify' specific options") g.add_argument('-k', '--close', metavar="RESOLUTION", help='Close with the given resolution (WONTFIX, NOTABUG, etc.)') g.add_argument('-d', '--dupeid', metavar="ORIGINAL", help='ID of original bug. Implies --close DUPLICATE') g.add_argument('--private', action='store_true', default=False, help='Mark new comment as private') g.add_argument('--reset-assignee', action="store_true", help='Reset assignee to component default') g.add_argument('--reset-qa-contact', action="store_true", help='Reset QA contact to component default') def _setup_action_attach_parser(subparsers): usage = """ bugzilla attach --file=FILE --desc=DESC [--type=TYPE] BUGID [BUGID...] bugzilla attach --get=ATTACHID --getall=BUGID [...] bugzilla attach --type=TYPE BUGID [BUGID...]""" description = "Attach files or download attachments." p = subparsers.add_parser("attach", description=description, usage=usage) p.add_argument('-f', '--file', metavar="FILENAME", help='File to attach, or filename for data provided on stdin') p.add_argument('-d', '--description', '--summary', metavar="SUMMARY", dest='desc', help="A short summary of the file being attached") p.add_argument('-t', '--type', metavar="MIMETYPE", help="Mime-type for the file being attached") p.add_argument('-g', '--get', metavar="ATTACHID", action="append", default=[], help="Download the attachment with the given ID") p.add_argument("--getall", "--get-all", metavar="BUGID", action="append", default=[], help="Download all attachments on the given bug") def _setup_action_login_parser(subparsers): usage = 'bugzilla login [username [password]]' description = "Log into bugzilla and save a login cookie or token." subparsers.add_parser("login", description=description, usage=usage) def setup_parser(): rootparser = _setup_root_parser() subparsers = rootparser.add_subparsers(dest="command_name") _setup_action_new_parser(subparsers) _setup_action_query_parser(subparsers) _setup_action_info_parser(subparsers) _setup_action_modify_parser(subparsers) _setup_action_attach_parser(subparsers) _setup_action_login_parser(subparsers) return rootparser #################### # Command routines # #################### def _merge_field_opts(query, opt, parser): # Add any custom fields if specified if opt.fields is None: return for f in opt.fields: try: f, v = f.split('=', 1) query[f] = v except: parser.error("Invalid field argument provided: %s" % (f)) def _do_query(bz, opt, parser): q = {} # Parse preconstructed queries. u = opt.from_url if u: q = bz.url_to_query(u) if opt.components_file: # Components slurped in from file (one component per line) # This can be made more robust clist = [] f = open(opt.components_file, 'r') for line in f.readlines(): line = line.rstrip("\n") clist.append(line) opt.component = clist if opt.status: val = opt.status stat = val if val == 'ALL': # leaving this out should return bugs of any status stat = None elif val == 'DEV': # Alias for all development bug statuses stat = ['NEW', 'ASSIGNED', 'NEEDINFO', 'ON_DEV', 'MODIFIED', 'POST', 'REOPENED'] elif val == 'QE': # Alias for all QE relevant bug statuses stat = ['ASSIGNED', 'ON_QA', 'FAILS_QA', 'PASSES_QA'] elif val == 'EOL': # Alias for EndOfLife bug statuses stat = ['VERIFIED', 'RELEASE_PENDING', 'CLOSED'] elif val == 'OPEN': # non-Closed statuses stat = ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', 'ON_QA', 'VERIFIED', 'RELEASE_PENDING', 'POST'] opt.status = stat # Convert all comma separated list parameters to actual lists, # which is what bugzilla wants # According to bugzilla docs, any parameter can be a list, but # let's only do this for options we explicitly mention can be # comma separated. for optname in ["severity", "id", "status", "component", "priority", "product", "version"]: val = getattr(opt, optname, None) if not isinstance(val, str): continue setattr(opt, optname, val.split(",")) include_fields = None if opt.output == 'raw': # 'raw' always does a getbug() call anyways, so just ask for ID back include_fields = ['id'] elif opt.outputformat: include_fields = [] for fieldname, rest in format_field_re.findall(opt.outputformat): # pylint: disable=redefined-variable-type if fieldname == "whiteboard" and rest: fieldname = rest + "_" + fieldname elif fieldname == "flag": fieldname = "flags" elif fieldname == "cve": fieldname = ["keywords", "blocks"] elif fieldname == "__unicode__": # Needs to be in sync with bug.__unicode__ fieldname = ["id", "status", "assigned_to", "summary"] flist = isinstance(fieldname, list) and fieldname or [fieldname] for f in flist: if f not in include_fields: include_fields.append(f) if include_fields is not None: include_fields.sort() built_query = bz.build_query( product=opt.product or None, component=opt.component or None, sub_component=opt.sub_component or None, version=opt.version or None, reporter=opt.reporter or None, bug_id=opt.id or None, short_desc=opt.summary or None, long_desc=opt.comment or None, cc=opt.cc or None, assigned_to=opt.assigned_to or None, qa_contact=opt.qa_contact or None, status=opt.status or None, blocked=opt.blocked or None, dependson=opt.dependson or None, keywords=opt.keywords or None, keywords_type=opt.keywords_type or None, url=opt.url or None, url_type=opt.url_type or None, status_whiteboard=opt.whiteboard or None, status_whiteboard_type=opt.status_whiteboard_type or None, fixed_in=opt.fixed_in or None, fixed_in_type=opt.fixed_in_type or None, flag=opt.flag or None, alias=opt.alias or None, qa_whiteboard=opt.qa_whiteboard or None, devel_whiteboard=opt.devel_whiteboard or None, boolean_query=opt.boolean_query or None, bug_severity=opt.severity or None, priority=opt.priority or None, target_release=opt.target_release or None, target_milestone=opt.target_milestone or None, emailtype=opt.emailtype or None, booleantype=opt.booleantype or None, include_fields=include_fields, quicksearch=opt.quicksearch or None, savedsearch=opt.savedsearch or None, savedsearch_sharer_id=opt.savedsearch_sharer_id or None, tags=opt.tags or None) _merge_field_opts(built_query, opt, parser) built_query.update(q) q = built_query if not q: parser.error("'query' command requires additional arguments") if opt.test_return_result: return q return bz.query(q) def _do_info(bz, opt): """ Handle the 'info' subcommand """ # All these commands call getproducts internally, so do it up front # with minimal include_fields for speed include_fields = ["name", "id"] if opt.versions: include_fields.append("versions") products = bz.getproducts(include_fields=include_fields) if opt.products: for name in sorted([p["name"] for p in products]): print(name) if opt.components: for name in sorted(bz.getcomponents(opt.components)): print(name) if opt.component_owners: # Looking up this info for rhbz 'Fedora' product is sloooow # since there are so many components. So delay getting this # info until as late as possible bz.refresh_products(names=[opt.component_owners], include_fields=include_fields + [ "components.default_assigned_to", "components.default_qa_contact", "components.name", "components.description"]) component_details = bz.getcomponentsdetails(opt.component_owners) for c in sorted(component_details): print(to_encoding(u"%s: %s" % (c, component_details[c]['initialowner']))) if opt.versions: for p in products: if p['name'] != opt.versions: continue if "versions" in p: for v in p['versions']: print(to_encoding(v["name"])) break def _convert_to_outputformat(output): fmt = "" if output == "normal": fmt = "%{__unicode__}" elif output == "ids": fmt = "%{id}" elif output == 'full': fmt += "%{__unicode__}\n" fmt += "Component: %{component}\n" fmt += "CC: %{cc}\n" fmt += "Blocked: %{blocks}\n" fmt += "Depends: %{depends_on}\n" fmt += "%{comments}\n" elif output == 'extra': fmt += "%{__unicode__}\n" fmt += " +Keywords: %{keywords}\n" fmt += " +QA Whiteboard: %{qa_whiteboard}\n" fmt += " +Status Whiteboard: %{status_whiteboard}\n" fmt += " +Devel Whiteboard: %{devel_whiteboard}\n" elif output == 'oneline': fmt += "#%{bug_id} %{status} %{assigned_to} %{component}\t" fmt += "[%{target_milestone}] %{flags} %{cve}" else: raise RuntimeError("Unknown output type '%s'" % output) return fmt def _format_output(bz, opt, buglist): if opt.output == 'raw': buglist = bz.getbugs([b.bug_id for b in buglist]) for b in buglist: print("Bugzilla %s: " % b.bug_id) for attrname in sorted(b.__dict__): print(to_encoding(u"ATTRIBUTE[%s]: %s" % (attrname, b.__dict__[attrname]))) print("\n\n") return def bug_field(matchobj): # whiteboard and flag allow doing # %{whiteboard:devel} and %{flag:needinfo} # That's what 'rest' matches (fieldname, rest) = matchobj.groups() if fieldname == "whiteboard" and rest: fieldname = rest + "_" + fieldname if fieldname == "flag" and rest: val = b.get_flag_status(rest) elif fieldname == "flags" or fieldname == "flags_requestee": tmpstr = [] for f in getattr(b, "flags", []): requestee = f.get('requestee', "") if fieldname == "flags": requestee = "" if fieldname == "flags_requestee": if requestee == "": continue tmpstr.append("%s" % requestee) else: tmpstr.append("%s%s%s" % (f['name'], f['status'], requestee)) val = ",".join(tmpstr) elif fieldname == "cve": cves = [] for key in getattr(b, "keywords", []): # grab CVE from keywords and blockers if key.find("Security") == -1: continue for bl in b.blocks: cvebug = bz.getbug(bl) for cb in cvebug.alias: if cb.find("CVE") == -1: continue if cb.strip() not in cves: cves.append(cb) val = ",".join(cves) elif fieldname == "comments": val = "" for c in getattr(b, "comments", []): val += ("\n* %s - %s:\n%s\n" % (c['time'], c.get("creator", ""), c['text'])) elif fieldname == "__unicode__": val = b.__unicode__() else: val = getattr(b, fieldname, "") vallist = isinstance(val, list) and val or [val] val = ','.join([to_encoding(v) for v in vallist]) return val for b in buglist: print(format_field_re.sub(bug_field, opt.outputformat)) def _parse_triset(vallist, checkplus=True, checkminus=True, checkequal=True, splitcomma=False): add_val = [] rm_val = [] set_val = None def make_list(v): if not v: return [] if splitcomma: return v.split(",") return [v] for val in isinstance(vallist, list) and vallist or [vallist]: val = val or "" if val.startswith("+") and checkplus: add_val += make_list(val[1:]) elif val.startswith("-") and checkminus: rm_val += make_list(val[1:]) elif val.startswith("=") and checkequal: # Intentionally overwrite this set_val = make_list(val[1:]) else: add_val += make_list(val) return add_val, rm_val, set_val def _do_new(bz, opt, parser): # Parse options that accept comma separated list def parse_multi(val): return _parse_triset(val, checkplus=False, checkminus=False, checkequal=False, splitcomma=True)[0] ret = bz.build_createbug( blocks=parse_multi(opt.blocked) or None, cc=parse_multi(opt.cc) or None, component=opt.component or None, depends_on=parse_multi(opt.dependson) or None, description=opt.comment or None, groups=parse_multi(opt.groups) or None, keywords=parse_multi(opt.keywords) or None, op_sys=opt.os or None, platform=opt.arch or None, priority=opt.priority or None, product=opt.product or None, severity=opt.severity or None, summary=opt.summary or None, url=opt.url or None, version=opt.version or None, assigned_to=opt.assigned_to or None, qa_contact=opt.qa_contact or None, sub_component=opt.sub_component or None, alias=opt.alias or None, ) _merge_field_opts(ret, opt, parser) if opt.test_return_result: return ret b = bz.createbug(ret) b.refresh() return [b] def _do_modify(bz, parser, opt, args): bugid_list = [bugid for a in args for bugid in a.split(',')] add_wb, rm_wb, set_wb = _parse_triset(opt.whiteboard) add_devwb, rm_devwb, set_devwb = _parse_triset(opt.devel_whiteboard) add_intwb, rm_intwb, set_intwb = _parse_triset(opt.internal_whiteboard) add_qawb, rm_qawb, set_qawb = _parse_triset(opt.qa_whiteboard) add_blk, rm_blk, set_blk = _parse_triset(opt.blocked, splitcomma=True) add_deps, rm_deps, set_deps = _parse_triset(opt.dependson, splitcomma=True) add_key, rm_key, set_key = _parse_triset(opt.keywords) add_cc, rm_cc, ignore = _parse_triset(opt.cc, checkplus=False, checkequal=False) add_groups, rm_groups, ignore = _parse_triset(opt.groups, checkequal=False, splitcomma=True) add_tags, rm_tags, ignore = _parse_triset(opt.tags, checkequal=False) status = opt.status or None if opt.dupeid is not None: opt.close = "DUPLICATE" if opt.close: status = "CLOSED" flags = [] if opt.flag: # Convert "foo+" to tuple ("foo", "+") for f in opt.flag: flags.append({"name": f[:-1], "status": f[-1]}) update = bz.build_update( assigned_to=opt.assigned_to or None, comment=opt.comment or None, comment_private=opt.private or None, component=opt.component or None, product=opt.product or None, blocks_add=add_blk or None, blocks_remove=rm_blk or None, blocks_set=set_blk, url=opt.url or None, cc_add=add_cc or None, cc_remove=rm_cc or None, depends_on_add=add_deps or None, depends_on_remove=rm_deps or None, depends_on_set=set_deps, groups_add=add_groups or None, groups_remove=rm_groups or None, keywords_add=add_key or None, keywords_remove=rm_key or None, keywords_set=set_key, op_sys=opt.os or None, platform=opt.arch or None, priority=opt.priority or None, qa_contact=opt.qa_contact or None, severity=opt.severity or None, status=status, summary=opt.summary or None, version=opt.version or None, reset_assigned_to=opt.reset_assignee or None, reset_qa_contact=opt.reset_qa_contact or None, resolution=opt.close or None, target_release=opt.target_release or None, target_milestone=opt.target_milestone or None, dupe_of=opt.dupeid or None, fixed_in=opt.fixed_in or None, whiteboard=set_wb and set_wb[0] or None, devel_whiteboard=set_devwb and set_devwb[0] or None, internal_whiteboard=set_intwb and set_intwb[0] or None, qa_whiteboard=set_qawb and set_qawb[0] or None, sub_component=opt.sub_component or None, alias=opt.alias or None, flags=flags or None, ) # We make this a little convoluted to facilitate unit testing wbmap = { "whiteboard": (add_wb, rm_wb), "internal_whiteboard": (add_intwb, rm_intwb), "qa_whiteboard": (add_qawb, rm_qawb), "devel_whiteboard": (add_devwb, rm_devwb), } for k, v in wbmap.copy().items(): if not v[0] and not v[1]: del(wbmap[k]) _merge_field_opts(update, opt, parser) log.debug("update bug dict=%s", update) log.debug("update whiteboard dict=%s", wbmap) if not any([update, wbmap, add_tags, rm_tags]): parser.error("'modify' command requires additional arguments") if opt.test_return_result: return (update, wbmap, add_tags, rm_tags) if add_tags or rm_tags: ret = bz.update_tags(bugid_list, tags_add=add_tags, tags_remove=rm_tags) log.debug("bz.update_tags returned=%s", ret) if update: ret = bz.update_bugs(bugid_list, update) log.debug("bz.update_bugs returned=%s", ret) if not wbmap: return # Now for the things we can't blindly batch. # Being able to prepend/append to whiteboards, which are just # plain string values, is an old rhbz semantic that we try to maintain # here. This is a bit weird for traditional bugzilla XMLRPC log.debug("Adjusting whiteboard fields one by one") for bug in bz.getbugs(bugid_list): for wb, (add_list, rm_list) in wbmap.items(): for tag in add_list: newval = getattr(bug, wb) or "" if newval: newval += " " newval += tag bz.update_bugs([bug.id], bz.build_update(**{wb: newval})) for tag in rm_list: newval = (getattr(bug, wb) or "").split() for t in newval[:]: if t == tag: newval.remove(t) bz.update_bugs([bug.id], bz.build_update(**{wb: " ".join(newval)})) def _do_get_attach(bz, opt, parser, args): if args: parser.error("Extra args '%s' not used for getting attachments" % args) for bug in bz.getbugs(opt.getall): opt.get += bug.get_attachment_ids() for attid in set(opt.get): att = bz.openattachment(attid) outfile = open_without_clobber(att.name, "wb") data = att.read(4096) while data: outfile.write(data) data = att.read(4096) print("Wrote %s" % outfile.name) return def _do_set_attach(bz, opt, parser, args): if not args: parser.error("Bug ID must be specified for setting attachments") if sys.stdin.isatty(): if not opt.file: parser.error("--file must be specified") fileobj = open(opt.file) else: # piped input on stdin if not opt.desc: parser.error("--description must be specified if passing " "file on stdin") fileobj = tempfile.NamedTemporaryFile(prefix="bugzilla-attach.") data = sys.stdin.read(4096) while data: fileobj.write(data.encode(locale.getpreferredencoding())) data = sys.stdin.read(4096) fileobj.seek(0) kwargs = {} if opt.file: kwargs["filename"] = os.path.basename(opt.file) if opt.type: kwargs["contenttype"] = opt.type if opt.type in ["text/x-patch"]: kwargs["ispatch"] = True desc = opt.desc or os.path.basename(fileobj.name) # Upload attachments for bugid in args: attid = bz.attachfile(bugid, fileobj, desc, **kwargs) print("Created attachment %i on bug %s" % (attid, bugid)) ################# # Main handling # ################# def _make_bz_instance(opt): """ Build the Bugzilla instance we will use """ if opt.bztype != 'auto': log.info("Explicit --bztype is no longer supported, ignoring") cookiefile = None tokenfile = None if opt.cache_credentials: cookiefile = opt.cookiefile or -1 tokenfile = opt.tokenfile or -1 bz = bugzilla.Bugzilla( url=opt.bugzilla, cookiefile=cookiefile, tokenfile=tokenfile, sslverify=opt.sslverify) return bz def _handle_login(opt, parser, args, action, bz): """ Handle all login related bits """ is_login_command = (action == 'login') do_interactive_login = (is_login_command or opt.login or opt.username or opt.password) if is_login_command: if len(args) == 2: (opt.username, opt.password) = args elif len(args) == 1: (opt.username, ) = args elif len(args) > 2: parser.error("Too many arguments for login") try: if do_interactive_login: if bz.url: print("Logging into %s" % urlparse(bz.url)[1]) bz.interactive_login( opt.username, opt.password) except bugzilla.BugzillaError: print(str(sys.exc_info()[1])) sys.exit(1) if opt.ensure_logged_in and not bz.logged_in: print("--ensure-logged-in passed but you aren't logged in to %s" % bz.url) sys.exit(1) if is_login_command: msg = "Login successful." if bz.cookiefile or bz.tokenfile: msg = "Login successful, token cache updated." print(msg) sys.exit(0) def main(unittest_bz_instance=None): parser = setup_parser() opt, args = parser.parse_known_args() action = opt.command_name if opt.debug: log.setLevel(DEBUG) elif opt.verbose: log.setLevel(INFO) else: log.setLevel(WARN) if _is_unittest_debug: log.setLevel(DEBUG) log.debug("Launched with command line: %s", " ".join(sys.argv)) # Connect to bugzilla log.info('Connecting to %s', opt.bugzilla) if unittest_bz_instance: bz = unittest_bz_instance else: bz = _make_bz_instance(opt) # Handle login options _handle_login(opt, parser, args, action, bz) ########################### # Run the actual commands # ########################### if hasattr(opt, "outputformat"): if not opt.outputformat and opt.output not in ['raw', None]: opt.outputformat = _convert_to_outputformat(opt.output) buglist = [] if action == 'info': if args: parser.error("Extra arguments '%s'" % args) if not (opt.products or opt.components or opt.component_owners or opt.versions): parser.error("'info' command requires additional arguments") _do_info(bz, opt) elif action == 'query': if args: parser.error("Extra arguments '%s'" % args) buglist = _do_query(bz, opt, parser) if opt.test_return_result: return buglist elif action == 'new': if args: parser.error("Extra arguments '%s'" % args) buglist = _do_new(bz, opt, parser) if opt.test_return_result: return buglist elif action == 'attach': if opt.get or opt.getall: _do_get_attach(bz, opt, parser, args) else: _do_set_attach(bz, opt, parser, args) elif action == 'modify': if not args: parser.error('No bug IDs given ' '(maybe you forgot an argument somewhere?)') modout = _do_modify(bz, parser, opt, args) if opt.test_return_result: return modout else: raise RuntimeError("Unexpected action '%s'" % action) # If we're doing new/query/modify, output our results if action in ['new', 'query']: _format_output(bz, opt, buglist) if __name__ == '__main__': try: main() except KeyboardInterrupt: log.debug("", exc_info=True) print("\nExited at user request.") sys.exit(1) except (Fault, bugzilla.BugzillaError): e = sys.exc_info()[1] log.debug("", exc_info=True) print("\nServer error: %s" % str(e)) sys.exit(3) except ProtocolError: e = sys.exc_info()[1] log.debug("", exc_info=True) print("\nInvalid server response: %d %s" % (e.errcode, e.errmsg)) # Detect redirect redir = (e.headers and 'location' in e.headers) if redir: print("\nServer was attempting a redirect. Try: " " bugzilla --bugzilla %s ..." % redir) sys.exit(4) except requests.exceptions.SSLError: e = sys.exc_info()[1] log.debug("", exc_info=True) # Give SSL recommendations print("SSL error: %s" % e) print("\nIf you trust the remote server, you can work " "around this error with:\n" " bugzilla --nosslverify ...") sys.exit(4) except (socket.error, requests.exceptions.HTTPError, requests.exceptions.ConnectionError): e = sys.exc_info()[1] log.debug("", exc_info=True) print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) python-bugzilla-2.1.0/COPYING0000664000175100017510000004325413046663414017376 0ustar crobinsocrobinso00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. python-bugzilla-2.1.0/python_bugzilla.egg-info/0000775000175100017510000000000013067312016023227 5ustar crobinsocrobinso00000000000000python-bugzilla-2.1.0/python_bugzilla.egg-info/PKG-INFO0000664000175100017510000000044113067312016024323 0ustar crobinsocrobinso00000000000000Metadata-Version: 1.0 Name: python-bugzilla Version: 2.1.0 Summary: Bugzilla XMLRPC access module Home-page: https://github.com/python-bugzilla/python-bugzilla Author: Cole Robinson Author-email: python-bugzilla@lists.fedorahosted.org License: GPLv2 Description: UNKNOWN Platform: UNKNOWN python-bugzilla-2.1.0/python_bugzilla.egg-info/requires.txt0000664000175100017510000000001113067312016025617 0ustar crobinsocrobinso00000000000000requests python-bugzilla-2.1.0/python_bugzilla.egg-info/SOURCES.txt0000664000175100017510000000165713067312016025124 0ustar crobinsocrobinso00000000000000CONTRIBUTING.md COPYING MANIFEST.in NEWS.md README.md bugzilla.1 python-bugzilla.spec requirements.txt setup.py test-requirements.txt xmlrpc-api-notes.txt bin/bugzilla bugzilla/__init__.py bugzilla/apiversion.py bugzilla/base.py bugzilla/bug.py bugzilla/oldclasses.py bugzilla/rhbugzilla.py bugzilla/transport.py examples/apikey.py examples/bug_autorefresh.py examples/create.py examples/getbug.py examples/query.py examples/update.py python_bugzilla.egg-info/PKG-INFO python_bugzilla.egg-info/SOURCES.txt python_bugzilla.egg-info/dependency_links.txt python_bugzilla.egg-info/requires.txt python_bugzilla.egg-info/top_level.txt tests/__init__.py tests/bug.py tests/createbug.py tests/misc.py tests/modify.py tests/pep8.cfg tests/pylint.cfg tests/query.py tests/ro_functional.py tests/rw_functional.py tests/data/bz-attach-get1.txt tests/data/components_file.txt tests/data/cookies-bad.txt tests/data/cookies-lwp.txt tests/data/cookies-moz.txtpython-bugzilla-2.1.0/python_bugzilla.egg-info/top_level.txt0000664000175100017510000000001113067312016025751 0ustar crobinsocrobinso00000000000000bugzilla python-bugzilla-2.1.0/python_bugzilla.egg-info/dependency_links.txt0000664000175100017510000000000113067312016027275 0ustar crobinsocrobinso00000000000000 python-bugzilla-2.1.0/test-requirements.txt0000664000175100017510000000006713046663414022577 0ustar crobinsocrobinso00000000000000# additional packages needed for testing coverage pep8 python-bugzilla-2.1.0/python-bugzilla.spec0000664000175100017510000000605013067311773022341 0ustar crobinsocrobinso00000000000000%if 0%{?fedora} || 0%{?rhel} >= 8 %global with_python3 1 %else %{!?__python2: %global __python2 /usr/bin/python2} %{!?python2_sitelib2: %global python2_sitelib %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} %endif Name: python-bugzilla Version: 2.1.0 Release: 1%{?dist} Summary: python2 library for interacting with Bugzilla License: GPLv2+ URL: https://github.com/python-bugzilla/python-bugzilla Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz BuildArch: noarch BuildRequires: python2-devel BuildRequires: python-requests BuildRequires: python-setuptools %if 0%{?el6} BuildRequires: python-argparse %endif %if 0%{?with_python3} BuildRequires: python3-devel BuildRequires: python3-requests BuildRequires: python3-setuptools %endif # if with_python3 Requires: python-requests Requires: python-magic %if 0%{?el6} Requires: python-argparse %endif # This dep is for back compat, so that installing python-bugzilla continues # to give the cli tool Requires: python-bugzilla-cli %description python-bugzilla is a python 2 library for interacting with bugzilla instances over XML-RPC. %if 0%{?with_python3} %package -n python3-bugzilla Summary: python 3 library for interacting with Bugzilla Requires: python3-requests Requires: python3-magic %description -n python3-bugzilla python3-bugzilla is a python 3 library for interacting with bugzilla instances over XML-RPC. %endif # if with_python3 %package cli Summary: Command line tool for interacting with Bugzilla %if 0%{?with_python3} Requires: python3-bugzilla = %{version}-%{release} %else Requires: python-bugzilla = %{version}-%{release} %endif %description cli This package includes the 'bugzilla' command-line tool for interacting with bugzilla. Uses the python-bugzilla API %prep %setup -q %if 0%{?with_python3} rm -rf %{py3dir} cp -a . %{py3dir} %endif # with_python3 %build %if 0%{?with_python3} pushd %{py3dir} %{__python3} setup.py build popd %endif # with_python3 %{__python2} setup.py build %install %if 0%{?with_python3} pushd %{py3dir} %{__python3} setup.py install -O1 --skip-build --root %{buildroot} rm %{buildroot}/usr/bin/bugzilla popd %endif # with_python3 %{__python2} setup.py install -O1 --skip-build --root %{buildroot} # Replace '#!/usr/bin/env python' with '#!/usr/bin/python2' # The format is ideal for upstream, but not a distro. See: # https://fedoraproject.org/wiki/Features/SystemPythonExecutablesUseSystemPython %if 0%{?with_python3} %global python_env_path %{__python3} %else %global python_env_path %{__python2} %endif for f in $(find %{buildroot} -type f -executable -print); do sed -i "1 s|^#!/usr/bin/.*|#!%{python_env_path}|" $f || : done %check %{__python2} setup.py test %files %doc COPYING README.md NEWS.md %{python2_sitelib}/* %if 0%{?with_python3} %files -n python3-bugzilla %doc COPYING README.md NEWS.md %{python3_sitelib}/* %endif # with_python3 %files cli %{_bindir}/bugzilla %{_mandir}/man1/bugzilla.1.gz python-bugzilla-2.1.0/setup.py0000775000175100017510000001365213062015413020043 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python from __future__ import print_function import glob import os import sys import unittest from distutils.core import Command from setuptools import setup def get_version(): f = open("bugzilla/apiversion.py") for line in f: if line.startswith('version = '): return eval(line.split('=')[-1]) class TestCommand(Command): user_options = [ ("ro-functional", None, "Run readonly functional tests against actual bugzilla instances. " "This will be very slow."), ("rw-functional", None, "Run read/write functional tests against actual bugzilla instances. " "As of now this only runs against partner-bugzilla.redhat.com, " "which requires an RH bugzilla account with cached cookies. " "This will also be very slow."), ("only=", None, "Run only tests whose name contains the passed string"), ("redhat-url=", None, "Redhat bugzilla URL to use for ro/rw_functional tests"), ("debug", None, "Enable python-bugzilla debug output. This may break output " "comparison tests."), ] def initialize_options(self): self.ro_functional = False self.rw_functional = False self.only = None self.redhat_url = None self.debug = False def finalize_options(self): pass def run(self): os.environ["__BUGZILLA_UNITTEST"] = "1" try: import coverage usecov = int(coverage.__version__.split(".")[0]) >= 3 except: usecov = False if usecov: cov = coverage.coverage(omit=[ "/*/tests/*", "/usr/*", "*dev-env*", "*.tox/*"]) cov.erase() cov.start() testfiles = [] for t in glob.glob(os.path.join(os.getcwd(), 'tests', '*.py')): if t.endswith("__init__.py"): continue base = os.path.basename(t) if (base == "ro_functional.py" and not self.ro_functional): continue if (base == "rw_functional.py" and not self.rw_functional): continue testfiles.append('.'.join(['tests', os.path.splitext(base)[0]])) if hasattr(unittest, "installHandler"): try: unittest.installHandler() except: print("installHandler hack failed") import tests as testsmodule testsmodule.REDHAT_URL = self.redhat_url if self.debug: import logging import bugzilla logging.getLogger(bugzilla.__name__).setLevel(logging.DEBUG) os.environ["__BUGZILLA_UNITTEST_DEBUG"] = "1" tests = unittest.TestLoader().loadTestsFromNames(testfiles) if self.only: newtests = [] for suite1 in tests: for suite2 in suite1: for testcase in suite2: if self.only in str(testcase): newtests.append(testcase) if not newtests: print("--only didn't find any tests") sys.exit(1) tests = unittest.TestSuite(newtests) print("Running only:") for test in newtests: print("%s" % test) print() t = unittest.TextTestRunner(verbosity=1) result = t.run(tests) if usecov: cov.stop() cov.save() err = int(bool(len(result.failures) > 0 or len(result.errors) > 0)) if not err and usecov: cov.report(show_missing=False) sys.exit(err) class PylintCommand(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass def _run(self): files = ["bugzilla/", "bin-bugzilla", "examples/*.py", "tests/*.py"] output_format = sys.stdout.isatty() and "colorized" or "text" cmd = "pylint " cmd += "--output-format=%s " % output_format cmd += " ".join(files) os.system(cmd + " --rcfile tests/pylint.cfg") print("running pep8") cmd = "pep8 " cmd += " ".join(files) os.system(cmd + " --config tests/pep8.cfg --exclude oldclasses.py") def run(self): os.link("bin/bugzilla", "bin-bugzilla") try: self._run() finally: try: os.unlink("bin-bugzilla") except: pass class RPMCommand(Command): description = "Build src and binary rpms." user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): """ Run sdist, then 'rpmbuild' the tar.gz """ os.system("cp python-bugzilla.spec /tmp") try: os.system("rm -rf python-bugzilla-%s" % get_version()) self.run_command('sdist') os.system('rpmbuild -ta --clean dist/python-bugzilla-%s.tar.gz' % get_version()) finally: os.system("mv /tmp/python-bugzilla.spec .") def _parse_requirements(fname): ret = [] for line in open(fname).readlines(): if not line or line.startswith("#"): continue ret.append(line) return ret setup(name='python-bugzilla', version=get_version(), description='Bugzilla XMLRPC access module', author='Cole Robinson', author_email='python-bugzilla@lists.fedorahosted.org', license="GPLv2", url='https://github.com/python-bugzilla/python-bugzilla', packages = ['bugzilla'], scripts=['bin/bugzilla'], data_files=[('share/man/man1', ['bugzilla.1'])], install_requires=_parse_requirements("requirements.txt"), tests_require=_parse_requirements("test-requirements.txt"), cmdclass={ "pylint" : PylintCommand, "rpm" : RPMCommand, "test" : TestCommand, }, ) python-bugzilla-2.1.0/MANIFEST.in0000664000175100017510000000035613067305775020103 0ustar crobinsocrobinso00000000000000include COPYING CONTRIBUTING.md MANIFEST.in README.md NEWS.md include bugzilla.1 include xmlrpc-api-notes.txt include python-bugzilla.spec include *requirements.txt recursive-include examples *.py recursive-include tests *.py *.txt *.cfg python-bugzilla-2.1.0/requirements.txt0000664000175100017510000000001113046663414021607 0ustar crobinsocrobinso00000000000000requests python-bugzilla-2.1.0/bugzilla/0000775000175100017510000000000013067312016020134 5ustar crobinsocrobinso00000000000000python-bugzilla-2.1.0/bugzilla/apiversion.py0000664000175100017510000000062513067311773022701 0ustar crobinsocrobinso00000000000000# # Copyright (C) 2014 Red Hat Inc. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. version = "2.1.0" __version__ = version python-bugzilla-2.1.0/bugzilla/__init__.py0000664000175100017510000000256213062015413022246 0ustar crobinsocrobinso00000000000000# python-bugzilla - a Python interface to bugzilla using xmlrpclib. # # Copyright (C) 2007, 2008 Red Hat Inc. # Author: Will Woods # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. from .apiversion import version, __version__ from .base import Bugzilla from .transport import BugzillaError from .rhbugzilla import RHBugzilla from .oldclasses import (Bugzilla3, Bugzilla32, Bugzilla34, Bugzilla36, Bugzilla4, Bugzilla42, Bugzilla44, NovellBugzilla, RHBugzilla3, RHBugzilla4) # This is the public API. If you are explicitly instantiating any other # class, using some function, or poking into internal files, don't complain # if things break on you. __all__ = [ "Bugzilla3", "Bugzilla32", "Bugzilla34", "Bugzilla36", "Bugzilla4", "Bugzilla42", "Bugzilla44", "NovellBugzilla", "RHBugzilla3", "RHBugzilla4", "RHBugzilla", 'BugzillaError', 'Bugzilla', "version", ] # Clear all other locals() from the public API for __sym in locals().copy(): if __sym.startswith("__") or __sym in __all__: continue locals().pop(__sym) locals().pop("__sym") python-bugzilla-2.1.0/bugzilla/bug.py0000664000175100017510000003647513067305775021320 0ustar crobinsocrobinso00000000000000# base.py - the base classes etc. for a Python interface to bugzilla # # Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. # Author: Will Woods # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. import locale from logging import getLogger import sys log = getLogger(__name__) class Bug(object): '''A container object for a bug report. Requires a Bugzilla instance - every Bug is on a Bugzilla, obviously. Optional keyword args: dict=DICT - populate attributes with the result of a getBug() call bug_id=ID - if dict does not contain bug_id, this is required before you can read any attributes or make modifications to this bug. ''' def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): # pylint: disable=redefined-builtin # API had pre-existing issue that we can't change ('dict' usage) self.bugzilla = bugzilla self._bug_fields = [] self.autorefresh = autorefresh if not dict: dict = {} if bug_id: dict["id"] = bug_id log.debug("Bug(%s)", sorted(dict.keys())) self._update_dict(dict) self.weburl = bugzilla.url.replace('xmlrpc.cgi', 'show_bug.cgi?id=%i' % self.bug_id) def __str__(self): '''Return a simple string representation of this bug This is available only for compatibility. Using 'str(bug)' and 'print(bug)' is not recommended because of potential encoding issues. Please use unicode(bug) where possible. ''' if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: return self.__unicode__() else: return self.__unicode__().encode( locale.getpreferredencoding(), 'replace') def __unicode__(self): '''Return a simple unicode string representation of this bug''' return u"#%-6s %-10s - %s - %s" % (self.bug_id, self.bug_status, self.assigned_to, self.summary) def __repr__(self): return '' % (self.bug_id, self.bugzilla.url, id(self)) def __getattr__(self, name): refreshed = False while True: if refreshed and name in self.__dict__: # If name was in __dict__ to begin with, __getattr__ would # have never been called. return self.__dict__[name] # pylint: disable=protected-access aliases = self.bugzilla._get_bug_aliases() # pylint: enable=protected-access for newname, oldname in aliases: if name == oldname and newname in self.__dict__: return self.__dict__[newname] # Doing dir(bugobj) does getattr __members__/__methods__, # don't refresh for those if name.startswith("__") and name.endswith("__"): break if refreshed or not self.autorefresh: break log.info("Bug %i missing attribute '%s' - doing implicit " "refresh(). This will be slow, if you want to avoid " "this, properly use query/getbug include_fields, and " "set bugzilla.bug_autorefresh = False to force failure.", self.bug_id, name) # We pass the attribute name to getbug, since for something like # 'attachments' which downloads lots of data we really want the # user to opt in. self.refresh(extra_fields=[name]) refreshed = True msg = ("Bug object has no attribute '%s'." % name) if not self.autorefresh: msg += ("\nIf '%s' is a bugzilla attribute, it may not have " "been cached when the bug was fetched. You may want " "to adjust your include_fields for getbug/query." % name) raise AttributeError(msg) def refresh(self, include_fields=None, exclude_fields=None, extra_fields=None): ''' Refresh the bug with the latest data from bugzilla ''' # pylint: disable=protected-access r = self.bugzilla._getbug(self.bug_id, include_fields=include_fields, exclude_fields=exclude_fields, extra_fields=self._bug_fields + (extra_fields or [])) # pylint: enable=protected-access self._update_dict(r) reload = refresh def _update_dict(self, newdict): ''' Update internal dictionary, in a way that ensures no duplicate entries are stored WRT field aliases ''' if self.bugzilla: self.bugzilla.post_translation({}, newdict) # pylint: disable=protected-access aliases = self.bugzilla._get_bug_aliases() # pylint: enable=protected-access for newname, oldname in aliases: if oldname not in newdict: continue if newname not in newdict: newdict[newname] = newdict[oldname] elif newdict[newname] != newdict[oldname]: log.debug("Update dict contained differing alias values " "d[%s]=%s and d[%s]=%s , dropping the value " "d[%s]", newname, newdict[newname], oldname, newdict[oldname], oldname) del(newdict[oldname]) for key in newdict.keys(): if key not in self._bug_fields: self._bug_fields.append(key) self.__dict__.update(newdict) if 'id' not in self.__dict__ and 'bug_id' not in self.__dict__: raise TypeError("Bug object needs a bug_id") ################## # pickle helpers # ################## def __getstate__(self): ret = {} for key in self._bug_fields: ret[key] = self.__dict__[key] return ret def __setstate__(self, vals): self._bug_fields = [] self.bugzilla = None self._update_dict(vals) ##################### # Modify bug status # ##################### def setstatus(self, status, comment=None, private=False): ''' Update the status for this bug report. Commonly-used values are ASSIGNED, MODIFIED, and NEEDINFO. To change bugs to CLOSED, use .close() instead. ''' # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(status=status, comment=comment, comment_private=private) log.debug("setstatus: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) def close(self, resolution, dupeid=None, fixedin=None, comment=None, isprivate=False): '''Close this bug. Valid values for resolution are in bz.querydefaults['resolution_list'] For bugzilla.redhat.com that's: ['NOTABUG', 'WONTFIX', 'DEFERRED', 'WORKSFORME', 'CURRENTRELEASE', 'RAWHIDE', 'ERRATA', 'DUPLICATE', 'UPSTREAM', 'NEXTRELEASE', 'CANTFIX', 'INSUFFICIENT_DATA'] If using DUPLICATE, you need to set dupeid to the ID of the other bug. If using WORKSFORME/CURRENTRELEASE/RAWHIDE/ERRATA/UPSTREAM/NEXTRELEASE you can (and should) set 'new_fixed_in' to a string representing the version that fixes the bug. You can optionally add a comment while closing the bug. Set 'isprivate' to True if you want that comment to be private. ''' # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(comment=comment, comment_private=isprivate, resolution=resolution, dupe_of=dupeid, fixed_in=fixedin, status="CLOSED") log.debug("close: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) ##################### # Modify bug emails # ##################### def setassignee(self, assigned_to=None, qa_contact=None, comment=None): ''' Set any of the assigned_to or qa_contact fields to a new bugzilla account, with an optional comment, e.g. setassignee(assigned_to='wwoods@redhat.com') setassignee(qa_contact='wwoods@redhat.com', comment='wwoods QA ftw') You must set at least one of the two assignee fields, or this method will throw a ValueError. Returns [bug_id, mailresults]. ''' if not (assigned_to or qa_contact): raise ValueError("You must set one of assigned_to " " or qa_contact") vals = self.bugzilla.build_update(assigned_to=assigned_to, qa_contact=qa_contact, comment=comment) log.debug("setassignee: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) def addcc(self, cclist, comment=None): ''' Adds the given email addresses to the CC list for this bug. cclist: list of email addresses (strings) comment: optional comment to add to the bug ''' vals = self.bugzilla.build_update(comment=comment, cc_add=cclist) log.debug("addcc: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) def deletecc(self, cclist, comment=None): ''' Removes the given email addresses from the CC list for this bug. ''' vals = self.bugzilla.build_update(comment=comment, cc_remove=cclist) log.debug("deletecc: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) #################### # comment handling # #################### def addcomment(self, comment, private=False): ''' Add the given comment to this bug. Set private to True to mark this comment as private. ''' # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(comment=comment, comment_private=private) log.debug("addcomment: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) def getcomments(self): ''' Returns an array of comment dictionaries for this bug ''' comment_list = self.bugzilla.get_comments([self.bug_id]) return comment_list['bugs'][str(self.bug_id)]['comments'] ##################### # Get/Set bug flags # ##################### def get_flag_type(self, name): """ Return flag_type information for a specific flag Older RHBugzilla returned a lot more info here, but it was non-upstream and is now gone. """ for t in self.flags: if t['name'] == name: return t return None def get_flags(self, name): """ Return flag value information for a specific flag """ ft = self.get_flag_type(name) if not ft: return None return [ft] def get_flag_status(self, name): """ Return a flag 'status' field This method works only for simple flags that have only a 'status' field with no "requestee" info, and no multiple values. For more complex flags, use get_flags() to get extended flag value information. """ f = self.get_flags(name) if not f: return None # This method works only for simple flags that have only one # value set. assert len(f) <= 1 return f[0]['status'] def updateflags(self, flags): """ Thin wrapper around build_update(flags=X). This only handles simple status changes, anything like needinfo requestee needs to call build_update + update_bugs directly :param flags: Dictionary of the form {"flagname": "status"}, example {"needinfo": "?", "devel_ack": "+"} """ flaglist = [] for key, value in flags.items(): flaglist.append({"name": key, "status": value}) return self.bugzilla.update_bugs([self.bug_id], self.bugzilla.build_update(flags=flaglist)) ######################## # Experimental methods # ######################## def get_attachment_ids(self): # pylint: disable=protected-access proxy = self.bugzilla._proxy # pylint: enable=protected-access if "attachments" in self.__dict__: attachments = self.attachments else: rawret = proxy.Bug.attachments( {"ids": [self.bug_id], "exclude_fields": ["data"]}) attachments = rawret["bugs"][str(self.bug_id)] return [a["id"] for a in attachments] def get_history_raw(self): ''' Experimental. Get the history of changes for this bug. ''' return self.bugzilla.bugs_history_raw([self.bug_id]) class User(object): '''Container object for a bugzilla User. :arg bugzilla: Bugzilla instance that this User belongs to. Rest of the params come straight from User.get() ''' def __init__(self, bugzilla, **kwargs): self.bugzilla = bugzilla self.__userid = kwargs.get('id') self.__name = kwargs.get('name') self.__email = kwargs.get('email', self.__name) self.__can_login = kwargs.get('can_login', False) self.real_name = kwargs.get('real_name', None) self.password = None self.groups = kwargs.get('groups', {}) self.groupnames = [] for g in self.groups: if "name" in g: self.groupnames.append(g["name"]) self.groupnames.sort() ######################## # Read-only attributes # ######################## # We make these properties so that the user cannot set them. They are # unaffected by the update() method so it would be misleading to let them # be changed. @property def userid(self): return self.__userid @property def email(self): return self.__email @property def can_login(self): return self.__can_login # name is a key in some methods. Mark it dirty when we change it # @property def name(self): return self.__name def refresh(self): """ Update User object with latest info from bugzilla """ newuser = self.bugzilla.getuser(self.email) self.__dict__.update(newuser.__dict__) def updateperms(self, action, groups): ''' A method to update the permissions (group membership) of a bugzilla user. :arg action: add, remove, or set :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) ''' self.bugzilla.updateperms(self.name, action, groups) python-bugzilla-2.1.0/bugzilla/transport.py0000664000175100017510000001464013062015413022543 0ustar crobinsocrobinso00000000000000# This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. from logging import getLogger import sys if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: # pylint: disable=import-error,no-name-in-module from configparser import SafeConfigParser from urllib.parse import urlparse from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport else: from ConfigParser import SafeConfigParser from urlparse import urlparse # pylint: disable=ungrouped-imports from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport import requests log = getLogger(__name__) class BugzillaError(Exception): '''Error raised in the Bugzilla client code.''' pass class _BugzillaTokenCache(object): """ Cache for tokens, including, with apologies for the duplicative terminology, both Bugzilla Tokens and API Keys. """ def __init__(self, uri, tokenfilename): self.tokenfilename = tokenfilename self.tokenfile = SafeConfigParser() self.domain = urlparse(uri)[1] if self.tokenfilename: self.tokenfile.read(self.tokenfilename) if self.domain not in self.tokenfile.sections(): self.tokenfile.add_section(self.domain) @property def value(self): if self.tokenfile.has_option(self.domain, 'token'): return self.tokenfile.get(self.domain, 'token') else: return None @value.setter def value(self, value): if self.value == value: return if value is None: self.tokenfile.remove_option(self.domain, 'token') else: self.tokenfile.set(self.domain, 'token', value) if self.tokenfilename: with open(self.tokenfilename, 'w') as tokenfile: log.debug("Saving to tokenfile") self.tokenfile.write(tokenfile) def __repr__(self): return '' % self.value class _BugzillaServerProxy(ServerProxy): def __init__(self, uri, tokenfile, *args, **kwargs): # pylint: disable=super-init-not-called # No idea why pylint complains here, must be a bug ServerProxy.__init__(self, uri, *args, **kwargs) self.token_cache = _BugzillaTokenCache(uri, tokenfile) self.api_key = None def use_api_key(self, api_key): self.api_key = api_key def clear_token(self): self.token_cache.value = None def _ServerProxy__request(self, methodname, params): if len(params) == 0: params = ({}, ) if self.api_key is not None: if 'Bugzilla_api_key' not in params[0]: params[0]['Bugzilla_api_key'] = self.api_key elif self.token_cache.value is not None: if 'Bugzilla_token' not in params[0]: params[0]['Bugzilla_token'] = self.token_cache.value # pylint: disable=maybe-no-member ret = ServerProxy._ServerProxy__request(self, methodname, params) # pylint: enable=maybe-no-member if isinstance(ret, dict) and 'token' in ret.keys(): self.token_cache.value = ret.get('token') return ret class _RequestsTransport(Transport): user_agent = 'Python/Bugzilla' def __init__(self, url, cookiejar=None, sslverify=True, sslcafile=None, debug=0): # pylint: disable=W0231 # pylint does not handle multiple import of Transport well if hasattr(Transport, "__init__"): Transport.__init__(self, use_datetime=False) self.verbose = debug self._cookiejar = cookiejar # transport constructor needs full url too, as xmlrpc does not pass # scheme to request self.scheme = urlparse(url)[0] if self.scheme not in ["http", "https"]: raise Exception("Invalid URL scheme: %s (%s)" % (self.scheme, url)) self.use_https = self.scheme == 'https' self.request_defaults = { 'cert': sslcafile if self.use_https else None, 'cookies': cookiejar, 'verify': sslverify, 'headers': { 'Content-Type': 'text/xml', 'User-Agent': self.user_agent, } } # Using an explicit Session, rather than requests.get, will use # HTTP KeepAlive if the server supports it. self.session = requests.Session() def parse_response(self, response): """ Parse XMLRPC response """ parser, unmarshaller = self.getparser() parser.feed(response.text.encode('utf-8')) parser.close() return unmarshaller.close() def _request_helper(self, url, request_body): """ A helper method to assist in making a request and provide a parsed response. """ response = None try: response = self.session.post( url, data=request_body, **self.request_defaults) # We expect utf-8 from the server response.encoding = 'UTF-8' # update/set any cookies if self._cookiejar is not None: for cookie in response.cookies: self._cookiejar.set_cookie(cookie) if self._cookiejar.filename is not None: # Save is required only if we have a filename self._cookiejar.save() response.raise_for_status() return self.parse_response(response) except requests.RequestException: e = sys.exc_info()[1] if not response: raise raise ProtocolError( url, response.status_code, str(e), response.headers) except Fault: raise sys.exc_info()[1] except Exception: # pylint: disable=W0201 e = BugzillaError(str(sys.exc_info()[1])) e.__traceback__ = sys.exc_info()[2] raise e def request(self, host, handler, request_body, verbose=0): self.verbose = verbose url = "%s://%s%s" % (self.scheme, host, handler) # xmlrpclib fails to escape \r request_body = request_body.replace(b'\r', b' ') return self._request_helper(url, request_body) python-bugzilla-2.1.0/bugzilla/oldclasses.py0000664000175100017510000000147613062015413022646 0ustar crobinsocrobinso00000000000000# This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. from .base import Bugzilla from .rhbugzilla import RHBugzilla # These are old compat classes. Nothing new should be added here, # and these should not be altered class Bugzilla3(Bugzilla): pass class Bugzilla32(Bugzilla): pass class Bugzilla34(Bugzilla): pass class Bugzilla36(Bugzilla): pass class Bugzilla4(Bugzilla): pass class Bugzilla42(Bugzilla): pass class Bugzilla44(Bugzilla): pass class NovellBugzilla(Bugzilla): pass class RHBugzilla3(RHBugzilla): pass class RHBugzilla4(RHBugzilla): pass python-bugzilla-2.1.0/bugzilla/rhbugzilla.py0000664000175100017510000003370713062015413022657 0ustar crobinsocrobinso00000000000000# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib. # # Copyright (C) 2008-2012 Red Hat Inc. # Author: Will Woods # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. from logging import getLogger from .base import Bugzilla log = getLogger(__name__) class RHBugzilla(Bugzilla): ''' Bugzilla class for connecting Red Hat's forked bugzilla instance, bugzilla.redhat.com Historically this class used many more non-upstream methods, but in 2012 RH started dropping most of its custom bits. By that time, upstream BZ had most of the important functionality. Much of the remaining code here is just trying to keep things operating in python-bugzilla back compatible manner. This class was written using bugzilla.redhat.com's API docs: https://bugzilla.redhat.com/docs/en/html/api/ ''' def _init_class_state(self): def _add_both_alias(newname, origname): self._add_field_alias(newname, origname, is_api=False) self._add_field_alias(origname, newname, is_bug=False) _add_both_alias('fixed_in', 'cf_fixed_in') _add_both_alias('qa_whiteboard', 'cf_qa_whiteboard') _add_both_alias('devel_whiteboard', 'cf_devel_whiteboard') _add_both_alias('internal_whiteboard', 'cf_internal_whiteboard') self._add_field_alias('component', 'components', is_bug=False) self._add_field_alias('version', 'versions', is_bug=False) self._add_field_alias('sub_component', 'sub_components', is_bug=False) # flags format isn't exactly the same but it's the closest approx self._add_field_alias('flags', 'flag_types') self._getbug_extra_fields = self._getbug_extra_fields + [ "comments", "description", "external_bugs", "flags", "sub_components", "tags", ] self._supports_getbug_extra_fields = True ###################### # Bug update methods # ###################### def build_update(self, **kwargs): adddict = {} def pop(key, destkey): val = kwargs.pop(key, None) if val is None: return adddict[destkey] = val def get_sub_component(): val = kwargs.pop("sub_component", None) if val is None: return if not isinstance(val, dict): component = self._listify(kwargs.get("component")) if not component: raise ValueError("component must be specified if " "specifying sub_component") val = {component[0]: val} adddict["sub_components"] = val def get_alias(): # RHBZ has a custom extension to allow a bug to have multiple # aliases, so the format of aliases is # {"add": [...], "remove": [...]} # But that means in order to approximate upstream, behavior # which just overwrites the existing alias, we need to read # the bug's state first to know what string to remove. Which # we can't do, since we don't know the bug numbers at this point. # So fail for now. # # The API should provide {"set": [...]} # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 # # Implementation will go here when it's available pass pop("fixed_in", "cf_fixed_in") pop("qa_whiteboard", "cf_qa_whiteboard") pop("devel_whiteboard", "cf_devel_whiteboard") pop("internal_whiteboard", "cf_internal_whiteboard") get_sub_component() get_alias() vals = Bugzilla.build_update(self, **kwargs) vals.update(adddict) return vals def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, ext_type_description=None, ext_type_url=None, ext_status=None, ext_description=None, ext_priority=None): """ Wrapper method to allow adding of external tracking bugs using the ExternalBugs::WebService::add_external_bug method. This is documented at https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#add_external_bug bug_ids: A single bug id or list of bug ids to have external trackers added. ext_bz_bug_id: The external bug id (ie: the bug number in the external tracker). ext_type_id: The external tracker id as used by Bugzilla. ext_type_description: The external tracker description as used by Bugzilla. ext_type_url: The external tracker url as used by Bugzilla. ext_status: The status of the external bug. ext_description: The description of the external bug. ext_priority: The priority of the external bug. """ param_dict = {'ext_bz_bug_id': ext_bz_bug_id} if ext_type_id is not None: param_dict['ext_type_id'] = ext_type_id if ext_type_description is not None: param_dict['ext_type_description'] = ext_type_description if ext_type_url is not None: param_dict['ext_type_url'] = ext_type_url if ext_status is not None: param_dict['ext_status'] = ext_status if ext_description is not None: param_dict['ext_description'] = ext_description if ext_priority is not None: param_dict['ext_priority'] = ext_priority params = { 'bug_ids': self._listify(bug_ids), 'external_bugs': [param_dict], } log.debug("Calling ExternalBugs.add_external_bug(%s)", params) return self._proxy.ExternalBugs.add_external_bug(params) def update_external_tracker(self, ids=None, ext_type_id=None, ext_type_description=None, ext_type_url=None, ext_bz_bug_id=None, bug_ids=None, ext_status=None, ext_description=None, ext_priority=None): """ Wrapper method to allow adding of external tracking bugs using the ExternalBugs::WebService::update_external_bug method. This is documented at https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#update_external_bug ids: A single external tracker bug id or list of external tracker bug ids. ext_type_id: The external tracker id as used by Bugzilla. ext_type_description: The external tracker description as used by Bugzilla. ext_type_url: The external tracker url as used by Bugzilla. ext_bz_bug_id: A single external bug id or list of external bug ids (ie: the bug number in the external tracker). bug_ids: A single bug id or list of bug ids to have external tracker info updated. ext_status: The status of the external bug. ext_description: The description of the external bug. ext_priority: The priority of the external bug. """ params = {} if ids is not None: params['ids'] = self._listify(ids) if ext_type_id is not None: params['ext_type_id'] = ext_type_id if ext_type_description is not None: params['ext_type_description'] = ext_type_description if ext_type_url is not None: params['ext_type_url'] = ext_type_url if ext_bz_bug_id is not None: params['ext_bz_bug_id'] = self._listify(ext_bz_bug_id) if bug_ids is not None: params['bug_ids'] = self._listify(bug_ids) if ext_status is not None: params['ext_status'] = ext_status if ext_description is not None: params['ext_description'] = ext_description if ext_priority is not None: params['ext_priority'] = ext_priority log.debug("Calling ExternalBugs.update_external_bug(%s)", params) return self._proxy.ExternalBugs.update_external_bug(params) def remove_external_tracker(self, ids=None, ext_type_id=None, ext_type_description=None, ext_type_url=None, ext_bz_bug_id=None, bug_ids=None): """ Wrapper method to allow removal of external tracking bugs using the ExternalBugs::WebService::remove_external_bug method. This is documented at https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#remove_external_bug ids: A single external tracker bug id or list of external tracker bug ids. ext_type_id: The external tracker id as used by Bugzilla. ext_type_description: The external tracker description as used by Bugzilla. ext_type_url: The external tracker url as used by Bugzilla. ext_bz_bug_id: A single external bug id or list of external bug ids (ie: the bug number in the external tracker). bug_ids: A single bug id or list of bug ids to have external tracker info updated. """ params = {} if ids is not None: params['ids'] = self._listify(ids) if ext_type_id is not None: params['ext_type_id'] = ext_type_id if ext_type_description is not None: params['ext_type_description'] = ext_type_description if ext_type_url is not None: params['ext_type_url'] = ext_type_url if ext_bz_bug_id is not None: params['ext_bz_bug_id'] = self._listify(ext_bz_bug_id) if bug_ids is not None: params['bug_ids'] = self._listify(bug_ids) log.debug("Calling ExternalBugs.remove_external_bug(%s)", params) return self._proxy.ExternalBugs.remove_external_bug(params) ################# # Query methods # ################# def pre_translation(self, query): '''Translates the query for possible aliases''' old = query.copy() if 'bug_id' in query: if not isinstance(query['bug_id'], list): query['id'] = query['bug_id'].split(',') else: query['id'] = query['bug_id'] del query['bug_id'] if 'component' in query: if not isinstance(query['component'], list): query['component'] = query['component'].split(',') if 'include_fields' not in query and 'column_list' not in query: return if 'include_fields' not in query: query['include_fields'] = [] if 'column_list' in query: query['include_fields'] = query['column_list'] del query['column_list'] # We need to do this for users here for users that # don't call build_query query.update(self._process_include_fields(query["include_fields"], None, None)) if old != query: log.debug("RHBugzilla pretranslated query to: %s", query) def post_translation(self, query, bug): ''' Convert the results of getbug back to the ancient RHBZ value formats ''' ignore = query # RHBZ _still_ returns component and version as lists, which # deviates from upstream. Copy the list values to components # and versions respectively. if 'component' in bug and "components" not in bug: val = bug['component'] bug['components'] = isinstance(val, list) and val or [val] bug['component'] = bug['components'][0] if 'version' in bug and "versions" not in bug: val = bug['version'] bug['versions'] = isinstance(val, list) and val or [val] bug['version'] = bug['versions'][0] # sub_components isn't too friendly of a format, add a simpler # sub_component value if 'sub_components' in bug and 'sub_component' not in bug: val = bug['sub_components'] bug['sub_component'] = "" if isinstance(val, dict): values = [] for vallist in val.values(): values += vallist bug['sub_component'] = " ".join(values) def build_external_tracker_boolean_query(self, *args, **kwargs): ignore1 = args ignore2 = kwargs raise RuntimeError("Building external boolean queries is " "no longer supported. Please build a URL query " "via the bugzilla web UI and pass it to 'query --from-url' " "or url_to_query()") def build_query(self, **kwargs): # We previously accepted a text format to approximate boolean # queries, and only for RHBugzilla. Upstream bz has --from-url # support now, so point people to that instead so we don't have # to document and maintain this logic anymore def _warn_bool(kwkey): vallist = self._listify(kwargs.get(kwkey, None)) for value in vallist or []: for s in value.split(" "): if s not in ["|", "&", "!"]: continue log.warn("%s value '%s' appears to use the now " "unsupported boolean formatting, your query may " "be incorrect. If you need complicated URL queries, " "look into bugzilla --from-url/url_to_query().", kwkey, value) return _warn_bool("fixed_in") _warn_bool("blocked") _warn_bool("dependson") _warn_bool("flag") _warn_bool("qa_whiteboard") _warn_bool("devel_whiteboard") _warn_bool("alias") return Bugzilla.build_query(self, **kwargs) python-bugzilla-2.1.0/bugzilla/base.py0000664000175100017510000017724513067307344021450 0ustar crobinsocrobinso00000000000000# base.py - the base classes etc. for a Python interface to bugzilla # # Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. # Author: Will Woods # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. import getpass import locale from logging import getLogger import os import sys from io import BytesIO # pylint: disable=ungrouped-imports if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: # pylint: disable=F0401,E0611 from configparser import SafeConfigParser from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse, parse_qsl from xmlrpc.client import Binary, Fault else: from ConfigParser import SafeConfigParser from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse, parse_qsl from xmlrpclib import Binary, Fault from .apiversion import __version__ from .bug import Bug, User from .transport import BugzillaError, _BugzillaServerProxy, _RequestsTransport log = getLogger(__name__) mimemagic = None def _detect_filetype(fname): # pylint: disable=E1103 # E1103: Instance of 'bool' has no '%s' member # pylint confuses mimemagic to be of type 'bool' global mimemagic if mimemagic is None: try: # pylint: disable=F0401 # F0401: Unable to import 'magic' (import-error) import magic mimemagic = magic.open(getattr(magic, "MAGIC_MIME_TYPE", 16)) mimemagic.load() except ImportError: e = sys.exc_info()[1] log.debug("Could not load python-magic: %s", e) mimemagic = None if not mimemagic: return None if not os.path.isabs(fname): return None try: return mimemagic.file(fname) except Exception: e = sys.exc_info()[1] log.debug("Could not detect content_type: %s", e) return None def _default_auth_location(filename): """ Determine auth location for filename, like 'bugzillacookies'. If old style ~/.bugzillacookies exists, we use that, otherwise we use ~/.cache/python-bugzilla/bugzillacookies. Same for bugzillatoken """ homepath = os.path.expanduser("~/.%s" % filename) xdgpath = os.path.expanduser("~/.cache/python-bugzilla/%s" % filename) if os.path.exists(xdgpath): return xdgpath if os.path.exists(homepath): return homepath if not os.path.exists(os.path.dirname(xdgpath)): os.makedirs(os.path.dirname(xdgpath), 0o700) return xdgpath def _build_cookiejar(cookiefile): cj = MozillaCookieJar(cookiefile) if cookiefile is None: return cj if not os.path.exists(cookiefile): # Make sure a new file has correct permissions open(cookiefile, 'a').close() os.chmod(cookiefile, 0o600) cj.save() return cj try: cj.load() return cj except LoadError: raise BugzillaError("cookiefile=%s not in Mozilla format" % cookiefile) _default_configpaths = [ '/etc/bugzillarc', '~/.bugzillarc', '~/.config/python-bugzilla/bugzillarc', ] def _open_bugzillarc(configpaths=-1): if configpaths == -1: configpaths = _default_configpaths[:] # pylint: disable=protected-access configpaths = [os.path.expanduser(p) for p in Bugzilla._listify(configpaths)] # pylint: enable=protected-access cfg = SafeConfigParser() read_files = cfg.read(configpaths) if not read_files: return log.info("Found bugzillarc files: %s", read_files) return cfg class _FieldAlias(object): """ Track API attribute names that differ from what we expose in users. For example, originally 'short_desc' was the name of the property that maps to 'summary' on modern bugzilla. We want pre-existing API users to be able to continue to use Bug.short_desc, and query({"short_desc": "foo"}). This class tracks that mapping. @oldname: The old attribute name @newname: The modern attribute name @is_api: If True, use this mapping for values sent to the xmlrpc API (like the query example) @is_bug: If True, use this mapping for Bug attribute names. """ def __init__(self, newname, oldname, is_api=True, is_bug=True): self.newname = newname self.oldname = oldname self.is_api = is_api self.is_bug = is_bug class _BugzillaAPICache(object): """ Helper class that holds cached API results for things like products, components, etc. """ def __init__(self): self.products = [] self.bugfields = [] self.components = {} self.components_details = {} class Bugzilla(object): """ The main API object. Connects to a bugzilla instance over XMLRPC, and provides wrapper functions to simplify dealing with API calls. The most common invocation here will just be with just a URL: bzapi = Bugzilla("http://bugzilla.example.com") If you have previously logged into that URL, and have cached login cookies/tokens, you will automatically be logged in. Otherwise to log in, you can either pass auth options to __init__, or call a login helper like interactive_login(). If you are not logged in, you won be able to access restricted data like user email, or perform write actions like bug create/update. But simple querys will work correctly. If you are unsure if you are logged in, you can check the .logged_in property. Another way to specify auth credentials is via a 'bugzillarc' file. See readconfig() documentation for details. """ # bugzilla version that the class is targeting. filled in by # subclasses bz_ver_major = 0 bz_ver_minor = 0 @staticmethod def url_to_query(url): ''' Given a big huge bugzilla query URL, returns a query dict that can be passed along to the Bugzilla.query() method. ''' q = {} # pylint: disable=unpacking-non-sequence (ignore, ignore, path, ignore, query, ignore) = urlparse(url) base = os.path.basename(path) if base not in ('buglist.cgi', 'query.cgi'): return {} for (k, v) in parse_qsl(query): if k not in q: q[k] = v elif isinstance(q[k], list): q[k].append(v) else: oldv = q[k] q[k] = [oldv, v] # Handle saved searches if base == "buglist.cgi" and "namedcmd" in q and "sharer_id" in q: q = { "sharer_id": q["sharer_id"], "savedsearch": q["namedcmd"], } return q @staticmethod def fix_url(url): """ Turn passed url into a bugzilla XMLRPC web url """ if '://' not in url: log.debug('No scheme given for url, assuming https') url = 'https://' + url if url.count('/') < 3: log.debug('No path given for url, assuming /xmlrpc.cgi') url = url + '/xmlrpc.cgi' return url @staticmethod def _listify(val): if val is None: return val if isinstance(val, list): return val return [val] def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at __init__ time, but you can defer connecting by passing url=None and calling connect(URL) manually :param user: optional username to connect with :param password: optional password for the connecting user :param cookiefile: Location to cache the login session cookies so you don't have to keep specifying username/password. Bugzilla 5+ will use tokens instead of cookies. If -1, use the default path. If None, don't use or save any cookiefile. :param sslverify: Set this to False to skip SSL hostname and CA validation checks, like out of date certificate :param tokenfile: Location to cache the API login token so youi don't have to keep specifying username/password. If -1, use the default path. If None, don't use or save any tokenfile. :param use_creds: If False, this disables cookiefile, tokenfile, and any bugzillarc reading. This overwrites any tokenfile or cookiefile settings :param sslverify: Maps to 'requests' sslverify parameter. Set to False to disable SSL verification, but it can also be a path to file or directory for custom certs. :param api_key: A bugzilla """ if url is -1: raise TypeError("Specify a valid bugzilla url, or pass url=None") # Settings the user might want to tweak self.user = user or '' self.password = password or '' self.api_key = api_key self.url = '' self._proxy = None self._transport = None self._cookiejar = None self._sslverify = sslverify self._cache = _BugzillaAPICache() self._bug_autorefresh = False self._field_aliases = [] self._init_field_aliases() self.configpath = _default_configpaths[:] if not use_creds: cookiefile = None tokenfile = None self.configpath = [] if cookiefile == -1: cookiefile = _default_auth_location("bugzillacookies") if tokenfile == -1: tokenfile = _default_auth_location("bugzillatoken") log.debug("Using tokenfile=%s", tokenfile) self.cookiefile = cookiefile self.tokenfile = tokenfile if url: self.connect(url) self._init_class_from_url() self._init_class_state() def _init_class_from_url(self): """ Detect if we should use RHBugzilla class, and if so, set it """ from bugzilla import RHBugzilla c = None if "bugzilla.redhat.com" in self.url: log.info("Using RHBugzilla for URL containing bugzilla.redhat.com") c = RHBugzilla else: try: extensions = self._proxy.Bugzilla.extensions() if "RedHat" in extensions.get('extensions', {}): log.info("Found RedHat bugzilla extension, " "using RHBugzilla") c = RHBugzilla except Fault: log.debug("Failed to fetch bugzilla extensions", exc_info=True) if not c: return self.__class__ = c def _init_class_state(self): """ Hook for subclasses to do any __init__ time setup """ pass def _init_field_aliases(self): # List of field aliases. Maps old style RHBZ parameter # names to actual upstream values. Used for createbug() and # query include_fields at least. self._add_field_alias('summary', 'short_desc') self._add_field_alias('description', 'comment') self._add_field_alias('platform', 'rep_platform') self._add_field_alias('severity', 'bug_severity') self._add_field_alias('status', 'bug_status') self._add_field_alias('id', 'bug_id') self._add_field_alias('blocks', 'blockedby') self._add_field_alias('blocks', 'blocked') self._add_field_alias('depends_on', 'dependson') self._add_field_alias('creator', 'reporter') self._add_field_alias('url', 'bug_file_loc') self._add_field_alias('dupe_of', 'dupe_id') self._add_field_alias('dupe_of', 'dup_id') self._add_field_alias('comments', 'longdescs') self._add_field_alias('creation_time', 'opendate') self._add_field_alias('creation_time', 'creation_ts') self._add_field_alias('whiteboard', 'status_whiteboard') self._add_field_alias('last_change_time', 'delta_ts') def _get_user_agent(self): return 'python-bugzilla/%s' % __version__ user_agent = property(_get_user_agent) ################### # Private helpers # ################### def _check_version(self, major, minor): """ Check if the detected bugzilla version is >= passed major/minor pair. """ if major < self.bz_ver_major: return True if (major == self.bz_ver_major and minor <= self.bz_ver_minor): return True return False def _product_id_to_name(self, productid): '''Convert a product ID (int) to a product name (str).''' for p in self.products: if p['id'] == productid: return p['name'] raise ValueError('No product with id #%i' % productid) def _product_name_to_id(self, product): '''Convert a product name (str) to a product ID (int).''' for p in self.products: if p['name'] == product: return p['id'] raise ValueError('No product named "%s"' % product) def _add_field_alias(self, *args, **kwargs): self._field_aliases.append(_FieldAlias(*args, **kwargs)) def _get_bug_aliases(self): return [(f.newname, f.oldname) for f in self._field_aliases if f.is_bug] def _get_api_aliases(self): return [(f.newname, f.oldname) for f in self._field_aliases if f.is_api] ################### # Cookie handling # ################### def _getcookiefile(self): '''cookiefile is the file that bugzilla session cookies are loaded and saved from. ''' return self._cookiejar.filename def _delcookiefile(self): self._cookiejar = None def _setcookiefile(self, cookiefile): if (self._cookiejar and cookiefile == self._cookiejar.filename): return if self._proxy is not None: raise RuntimeError("Can't set cookies with an open connection, " "disconnect() first.") log.debug("Using cookiefile=%s", cookiefile) self._cookiejar = _build_cookiejar(cookiefile) cookiefile = property(_getcookiefile, _setcookiefile, _delcookiefile) ############################# # Login/connection handling # ############################# def readconfig(self, configpath=None): """ :param configpath: Optional bugzillarc path to read, instead of the default list. This function is called automatically from Bugzilla connect(), which is called at __init__ if a URL is passed. Calling it manually is just for passing in a non-standard configpath. The locations for the bugzillarc file are preferred in this order: ~/.config/python-bugzilla/bugzillarc ~/.bugzillarc /etc/bugzillarc It has content like: [bugzilla.yoursite.com] user = username password = password Or [bugzilla.yoursite.com] api_key = key The file can have multiple sections for different bugzilla instances. A 'url' field in the [DEFAULT] section can be used to set a default URL for the bugzilla command line tool. Be sure to set appropriate permissions on bugzillarc if you choose to store your password in it! """ cfg = _open_bugzillarc(configpath or self.configpath) if not cfg: return section = "" log.debug("bugzillarc: Searching for config section matching %s", self.url) for s in sorted(cfg.sections()): # Substring match - prefer the longest match found if s in self.url: log.debug("bugzillarc: Found matching section: %s", s) section = s if not section: log.debug("bugzillarc: No section found") return for key, val in cfg.items(section): if key == "api_key": log.debug("bugzillarc: setting api_key") self.api_key = val elif key == "user": log.debug("bugzillarc: setting user=%s", val) self.user = val elif key == "password": log.debug("bugzillarc: setting password") self.password = val else: log.debug("bugzillarc: unknown key=%s", key) def _set_bz_version(self, version): try: self.bz_ver_major, self.bz_ver_minor = [ int(i) for i in version.split(".")[0:2]] except: log.debug("version doesn't match expected format X.Y.Z, " "assuming 5.0", exc_info=True) self.bz_ver_major = 5 self.bz_ver_minor = 0 def connect(self, url=None): ''' Connect to the bugzilla instance with the given url. This is called by __init__ if a URL is passed. Or it can be called manually at any time with a passed URL. This will also read any available config files (see readconfig()), which may set 'user' and 'password', and others. If 'user' and 'password' are both set, we'll run login(). Otherwise you'll have to login() yourself before some methods will work. ''' if self._transport: self.disconnect() if url is None and self.url: url = self.url url = self.fix_url(url) self._transport = _RequestsTransport( url, self._cookiejar, sslverify=self._sslverify) self._transport.user_agent = self.user_agent self._proxy = _BugzillaServerProxy(url, self.tokenfile, self._transport) self.url = url # we've changed URLs - reload config self.readconfig() if (self.user and self.password): log.info("user and password present - doing login()") self.login() if self.api_key: log.debug("using API key") self._proxy.use_api_key(self.api_key) version = self._proxy.Bugzilla.version()["version"] log.debug("Bugzilla version string: %s", version) self._set_bz_version(version) def disconnect(self): ''' Disconnect from the given bugzilla instance. ''' self._proxy = None self._transport = None self._cache = _BugzillaAPICache() def _login(self, user, password): '''Backend login method for Bugzilla3''' return self._proxy.User.login({'login': user, 'password': password}) def _logout(self): '''Backend login method for Bugzilla3''' return self._proxy.User.logout() def login(self, user=None, password=None): '''Attempt to log in using the given username and password. Subsequent method calls will use this username and password. Returns False if login fails, otherwise returns some kind of login info - typically either a numeric userid, or a dict of user info. If user is not set, the value of Bugzilla.user will be used. If *that* is not set, ValueError will be raised. If login fails, BugzillaError will be raised. This method will be called implicitly at the end of connect() if user and password are both set. So under most circumstances you won't need to call this yourself. ''' if self.api_key: raise ValueError("cannot login when using an API key") if user: self.user = user if password: self.password = password if not self.user: raise ValueError("missing username") if not self.password: raise ValueError("missing password") try: ret = self._login(self.user, self.password) self.password = '' log.info("login successful for user=%s", self.user) return ret except Fault: e = sys.exc_info()[1] raise BugzillaError("Login failed: %s" % str(e.faultString)) def interactive_login(self, user=None, password=None, force=False): """ Helper method to handle login for this bugzilla instance. :param user: bugzilla username. If not specified, prompt for it. :param password: bugzilla password. If not specified, prompt for it. :param force: Unused """ ignore = force log.debug('Calling interactive_login') if not user: sys.stdout.write('Bugzilla Username: ') sys.stdout.flush() user = sys.stdin.readline().strip() if not password: password = getpass.getpass('Bugzilla Password: ') log.info('Logging in... ') self.login(user, password) log.info('Authorization cookie received.') def logout(self): '''Log out of bugzilla. Drops server connection and user info, and destroys authentication cookies.''' self._logout() self.disconnect() self.user = '' self.password = '' @property def logged_in(self): """ This is True if this instance is logged in else False. We test if this session is authenticated by calling the User.get() XMLRPC method with ids set. Logged-out users cannot pass the 'ids' parameter and will result in a 505 error. For Bugzilla 5 and later, a new method, User.valid_login is available to test the validity of the token. However, this will require that the username be cached along with the token in order to work effectively in all scenarios and is not currently used. For more information, refer to the following url. http://bugzilla.readthedocs.org/en/latest/api/core/v1/user.html#valid-login """ try: self._proxy.User.get({'ids': []}) return True except Fault: e = sys.exc_info()[1] if e.faultCode == 505: return False raise e ############################################# # Fetching info about the bugzilla instance # ############################################# def _getbugfields(self): ''' Get the list of valid fields for Bug objects ''' r = self._proxy.Bug.fields({'include_fields': ['name']}) return [f['name'] for f in r['fields']] def getbugfields(self, force_refresh=False): ''' Calls getBugFields, which returns a list of fields in each bug for this bugzilla instance. This can be used to set the list of attrs on the Bug object. ''' if force_refresh or not self._cache.bugfields: log.debug("Refreshing bugfields") self._cache.bugfields = self._getbugfields() self._cache.bugfields.sort() log.debug("bugfields = %s", self._cache.bugfields) return self._cache.bugfields bugfields = property(fget=lambda self: self.getbugfields(), fdel=lambda self: setattr(self, '_bugfields', None)) def refresh_products(self, **kwargs): """ Refresh a product's cached info Takes same arguments as _getproductinfo """ for product in self._getproductinfo(**kwargs): added = False for current in self._cache.products[:]: if (current.get("id", -1) != product.get("id", -2) and current.get("name", -1) != product.get("name", -2)): continue self._cache.products.remove(current) self._cache.products.append(product) added = True break if not added: self._cache.products.append(product) def getproducts(self, force_refresh=False, **kwargs): '''Get product data: names, descriptions, etc. The data varies between Bugzilla versions but the basic format is a list of dicts, where the dicts will have at least the following keys: {'id':1, 'name':"Some Product", 'description':"This is a product"} Any method that requires a 'product' can be given either the id or the name.''' if force_refresh or not self._cache.products: self._cache.products = self._getproducts(**kwargs) return self._cache.products products = property(fget=lambda self: self.getproducts(), fdel=lambda self: setattr(self, '_products', None)) def getcomponentsdetails(self, product, force_refresh=False): '''Returns a dict of dicts, containing detailed component information for the given product. The keys of the dict are component names. For each component, the value is a dict with the following keys: description, initialowner, initialqacontact''' if force_refresh or product not in self._cache.components_details: clist = self._getcomponentsdetails(product) cdict = {} for item in clist: name = item['component'] del item['component'] cdict[name] = item self._cache.components_details[product] = cdict return self._cache.components_details[product] def getcomponentdetails(self, product, component, force_refresh=False): '''Get details for a single component. See bugzilla documentation for a list of returned keys.''' d = self.getcomponentsdetails(product, force_refresh) return d[component] def getcomponents(self, product, force_refresh=False): '''Return a dict of components:descriptions for the given product.''' if force_refresh or product not in self._cache.components: self._cache.components[product] = self._getcomponents(product) return self._cache.components[product] def _component_data_convert(self, data, update=False): if isinstance(data['product'], int): data['product'] = self._product_id_to_name(data['product']) # Back compat for the old RH interface convert_fields = [ ("initialowner", "default_assignee"), ("initialqacontact", "default_qa_contact"), ("initialcclist", "default_cc"), ] for old, new in convert_fields: if old in data: data[new] = data.pop(old) if update: names = {"product": data.pop("product"), "component": data.pop("component")} updates = {} for k in data.keys(): updates[k] = data.pop(k) data["names"] = [names] data["updates"] = updates def addcomponent(self, data): ''' A method to create a component in Bugzilla. Takes a dict, with the following elements: product: The product to create the component in component: The name of the component to create desription: A one sentence summary of the component default_assignee: The bugzilla login (email address) of the initial owner of the component default_qa_contact (optional): The bugzilla login of the initial QA contact default_cc: (optional) The initial list of users to be CC'ed on new bugs for the component. is_active: (optional) If False, the component is hidden from the component list when filing new bugs. ''' data = data.copy() self._component_data_convert(data) log.debug("Calling Component.create with: %s", data) return self._proxy.Component.create(data) def editcomponent(self, data): ''' A method to edit a component in Bugzilla. Takes a dict, with mandatory elements of product. component, and initialowner. All other elements are optional and use the same names as the addcomponent() method. ''' data = data.copy() self._component_data_convert(data, update=True) log.debug("Calling Component.update with: %s", data) return self._proxy.Component.update(data) def _getproductinfo(self, ids=None, names=None, include_fields=None, exclude_fields=None): ''' Get all info for the requested products. @ids: List of product IDs to lookup @names: List of product names to lookup (since bz 4.2, though we emulate it for older versions) @include_fields: Only include these fields in the output (since bz 4.2) @exclude_fields: Do not include these fields in the output (since bz 4.2) ''' if ids is None and names is None: raise RuntimeError("Products must be specified") kwargs = {} if not self._check_version(4, 2): if names: ids = [self._product_name_to_id(name) for name in names] names = None include_fields = None exclude_fields = None if ids: kwargs["ids"] = self._listify(ids) if names: kwargs["names"] = self._listify(names) if include_fields: kwargs["include_fields"] = include_fields if exclude_fields: kwargs["exclude_fields"] = exclude_fields log.debug("Calling Product.get with: %s", kwargs) ret = self._proxy.Product.get(kwargs) return ret['products'] def _getproducts(self, **kwargs): product_ids = self._proxy.Product.get_accessible_products() r = self._getproductinfo(product_ids['ids'], **kwargs) return r def _getcomponents(self, product): if isinstance(product, str): product = self._product_name_to_id(product) r = self._proxy.Bug.legal_values({'product_id': product, 'field': 'component'}) return r['values'] def _getcomponentsdetails(self, product): def _find_comps(): for p in self._cache.products: if p["name"] != product: continue return p.get("components", None) comps = _find_comps() if comps is None: self.refresh_products(names=[product], include_fields=["name", "id", "components"]) comps = _find_comps() if comps is None: raise ValueError("Unknown product '%s'" % product) # Convert to old style dictionary to maintain back compat # with original RH bugzilla call ret = [] for comp in comps: row = {} row["component"] = comp["name"] row["initialqacontact"] = comp["default_qa_contact"] row["initialowner"] = comp["default_assigned_to"] row["description"] = comp["description"] ret.append(row) return ret ################### # getbug* methods # ################### def _process_include_fields(self, include_fields, exclude_fields, extra_fields): """ Internal helper to process include_fields lists """ def _convert_fields(_in): if not _in: return _in for newname, oldname in self._get_api_aliases(): if oldname in _in: _in.remove(oldname) if newname not in _in: _in.append(newname) return _in ret = {} if self._check_version(4, 0): if include_fields: include_fields = _convert_fields(include_fields) if "id" not in include_fields: include_fields.append("id") ret["include_fields"] = include_fields if exclude_fields: exclude_fields = _convert_fields(exclude_fields) ret["exclude_fields"] = exclude_fields if self._supports_getbug_extra_fields: if extra_fields: ret["extra_fields"] = _convert_fields(extra_fields) return ret def _get_bug_autorefresh(self): """ This value is passed to Bug.autorefresh for all fetched bugs. If True, and an uncached attribute is requested from a Bug, the Bug will update its contents and try again. """ return self._bug_autorefresh def _set_bug_autorefresh(self, val): self._bug_autorefresh = bool(val) bug_autorefresh = property(_get_bug_autorefresh, _set_bug_autorefresh) # getbug_extra_fields: Extra fields that need to be explicitly # requested from Bug.get in order for the data to be returned. # # As of Dec 2012 it seems like only RH bugzilla actually has behavior # like this, for upstream bz it returns all info for every Bug.get() _getbug_extra_fields = [] _supports_getbug_extra_fields = False def _getbugs(self, idlist, permissive, include_fields=None, exclude_fields=None, extra_fields=None): ''' Return a list of dicts of full bug info for each given bug id. bug ids that couldn't be found will return None instead of a dict. ''' oldidlist = idlist idlist = [] for i in oldidlist: try: idlist.append(int(i)) except ValueError: # String aliases can be passed as well idlist.append(i) extra_fields = self._listify(extra_fields or []) extra_fields += self._getbug_extra_fields getbugdata = {"ids": idlist} if permissive: getbugdata["permissive"] = 1 getbugdata.update(self._process_include_fields( include_fields, exclude_fields, extra_fields)) log.debug("Calling Bug.get with: %s", getbugdata) r = self._proxy.Bug.get(getbugdata) if self._check_version(4, 0): bugdict = dict([(b['id'], b) for b in r['bugs']]) else: bugdict = dict([(b['id'], b['internals']) for b in r['bugs']]) ret = [] for i in idlist: found = None if i in bugdict: found = bugdict[i] else: # Need to map an alias for valdict in bugdict.values(): if i in valdict.get("alias", []): found = valdict break ret.append(found) return ret def _getbug(self, objid, **kwargs): """ Thin wrapper around _getbugs to handle the slight argument tweaks for fetching a single bug. The main bit is permissive=False, which will tell bugzilla to raise an explicit error if we can't fetch that bug. This logic is called from Bug() too """ return self._getbugs([objid], permissive=False, **kwargs)[0] def getbug(self, objid, include_fields=None, exclude_fields=None, extra_fields=None): '''Return a Bug object with the full complement of bug data already loaded.''' data = self._getbug(objid, include_fields=include_fields, exclude_fields=exclude_fields, extra_fields=extra_fields) return Bug(self, dict=data, autorefresh=self.bug_autorefresh) def getbugs(self, idlist, include_fields=None, exclude_fields=None, extra_fields=None, permissive=True): '''Return a list of Bug objects with the full complement of bug data already loaded. If there's a problem getting the data for a given id, the corresponding item in the returned list will be None.''' data = self._getbugs(idlist, include_fields=include_fields, exclude_fields=exclude_fields, extra_fields=extra_fields, permissive=permissive) return [(b and Bug(self, dict=b, autorefresh=self.bug_autorefresh)) or None for b in data] def get_comments(self, idlist): '''Returns a dictionary of bugs and comments. The comments key will be empty. See bugzilla docs for details''' return self._proxy.Bug.comments({'ids': idlist}) ################# # query methods # ################# def build_query(self, product=None, component=None, version=None, long_desc=None, bug_id=None, short_desc=None, cc=None, assigned_to=None, reporter=None, qa_contact=None, status=None, blocked=None, dependson=None, keywords=None, keywords_type=None, url=None, url_type=None, status_whiteboard=None, status_whiteboard_type=None, fixed_in=None, fixed_in_type=None, flag=None, alias=None, qa_whiteboard=None, devel_whiteboard=None, boolean_query=None, bug_severity=None, priority=None, target_release=None, target_milestone=None, emailtype=None, booleantype=None, include_fields=None, quicksearch=None, savedsearch=None, savedsearch_sharer_id=None, sub_component=None, tags=None, exclude_fields=None, extra_fields=None): """ Build a query string from passed arguments. Will handle query parameter differences between various bugzilla versions. Most of the parameters should be self explanatory. However if you want to perform a complex query, and easy way is to create it with the bugzilla web UI, copy the entire URL it generates, and pass it to the static method Bugzilla.url_to_query Then pass the output to Bugzilla.query() For details about the specific argument formats, see the bugzilla docs: https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs """ if boolean_query or booleantype: raise RuntimeError("boolean_query format is no longer supported. " "If you need complicated URL queries, look into " "query --from-url/url_to_query().") query = { "alias": alias, "product": self._listify(product), "component": self._listify(component), "version": version, "id": bug_id, "short_desc": short_desc, "bug_status": status, "bug_severity": bug_severity, "priority": priority, "target_release": target_release, "target_milestone": target_milestone, "tag": self._listify(tags), "quicksearch": quicksearch, "savedsearch": savedsearch, "sharer_id": savedsearch_sharer_id, # RH extensions... don't add any more. See comment below "sub_components": self._listify(sub_component), } def add_bool(bzkey, value, bool_id, booltype=None): value = self._listify(value) if value is None: return bool_id query["query_format"] = "advanced" for boolval in value: def make_bool_str(prefix): # pylint: disable=cell-var-from-loop return "%s%i-0-0" % (prefix, bool_id) query[make_bool_str("field")] = bzkey query[make_bool_str("value")] = boolval query[make_bool_str("type")] = booltype or "substring" bool_id += 1 return bool_id # RH extensions that we have to maintain here for back compat, # but all future custom fields should be specified via # cli --field option, or via extending the query dict() manually. # No more supporting custom fields in this API bool_id = 0 bool_id = add_bool("keywords", keywords, bool_id, keywords_type) bool_id = add_bool("blocked", blocked, bool_id) bool_id = add_bool("dependson", dependson, bool_id) bool_id = add_bool("bug_file_loc", url, bool_id, url_type) bool_id = add_bool("cf_fixed_in", fixed_in, bool_id, fixed_in_type) bool_id = add_bool("flagtypes.name", flag, bool_id) bool_id = add_bool("status_whiteboard", status_whiteboard, bool_id, status_whiteboard_type) bool_id = add_bool("cf_qa_whiteboard", qa_whiteboard, bool_id) bool_id = add_bool("cf_devel_whiteboard", devel_whiteboard, bool_id) def add_email(key, value, count): if value is None: return count if not emailtype: query[key] = value return count query["query_format"] = "advanced" query['email%i' % count] = value query['email%s%i' % (key, count)] = True query['emailtype%i' % count] = emailtype return count + 1 email_count = 1 email_count = add_email("cc", cc, email_count) email_count = add_email("assigned_to", assigned_to, email_count) email_count = add_email("reporter", reporter, email_count) email_count = add_email("qa_contact", qa_contact, email_count) if long_desc is not None: query["query_format"] = "advanced" query["longdesc"] = long_desc query["longdesc_type"] = "allwordssubstr" # 'include_fields' only available for Bugzilla4+ # 'extra_fields' is an RHBZ extension query.update(self._process_include_fields( include_fields, exclude_fields, extra_fields)) # Strip out None elements in the dict for k, v in query.copy().items(): if v is None: del(query[k]) self.pre_translation(query) return query def query(self, query): '''Query bugzilla and return a list of matching bugs. query must be a dict with fields like those in in querydata['fields']. Returns a list of Bug objects. Also see the _query() method for details about the underlying implementation. ''' log.debug("Calling Bug.search with: %s", query) try: r = self._proxy.Bug.search(query) except Fault: e = sys.exc_info()[1] # Try to give a hint in the error message if url_to_query # isn't supported by this bugzilla instance if ("query_format" not in str(e) or "RHBugzilla" in str(e.__class__) or self._check_version(5, 0)): raise raise BugzillaError("%s\nYour bugzilla instance does not " "appear to support API queries derived from bugzilla " "web URL queries." % e) log.debug("Query returned %s bugs", len(r['bugs'])) return [Bug(self, dict=b, autorefresh=self.bug_autorefresh) for b in r['bugs']] def pre_translation(self, query): '''In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function ''' pass def post_translation(self, query, bug): '''In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function ''' pass def bugs_history_raw(self, bug_ids): ''' Experimental. Gets the history of changes for particular bugs in the database. ''' return self._proxy.Bug.history({'ids': bug_ids}) ####################################### # Methods for modifying existing bugs # ####################################### # Bug() also has individual methods for many ops, like setassignee() def update_bugs(self, ids, updates): """ A thin wrapper around bugzilla Bug.update(). Used to update all values of an existing bug report, as well as add comments. The dictionary passed to this function should be generated with build_update(), otherwise we cannot guarantee back compatibility. """ tmp = updates.copy() tmp["ids"] = self._listify(ids) log.debug("Calling Bug.update with: %s", tmp) return self._proxy.Bug.update(tmp) def update_tags(self, idlist, tags_add=None, tags_remove=None): ''' Updates the 'tags' field for a bug. ''' tags = {} if tags_add: tags["add"] = self._listify(tags_add) if tags_remove: tags["remove"] = self._listify(tags_remove) d = { "ids": self._listify(idlist), "tags": tags, } log.debug("Calling Bug.update_tags with: %s", d) return self._proxy.Bug.update_tags(d) def update_flags(self, idlist, flags): """ A thin back compat wrapper around build_update(flags=X) """ return self.update_bugs(idlist, self.build_update(flags=flags)) def build_update(self, alias=None, assigned_to=None, blocks_add=None, blocks_remove=None, blocks_set=None, depends_on_add=None, depends_on_remove=None, depends_on_set=None, cc_add=None, cc_remove=None, is_cc_accessible=None, comment=None, comment_private=None, component=None, deadline=None, dupe_of=None, estimated_time=None, groups_add=None, groups_remove=None, keywords_add=None, keywords_remove=None, keywords_set=None, op_sys=None, platform=None, priority=None, product=None, qa_contact=None, is_creator_accessible=None, remaining_time=None, reset_assigned_to=None, reset_qa_contact=None, resolution=None, see_also_add=None, see_also_remove=None, severity=None, status=None, summary=None, target_milestone=None, target_release=None, url=None, version=None, whiteboard=None, work_time=None, fixed_in=None, qa_whiteboard=None, devel_whiteboard=None, internal_whiteboard=None, sub_component=None, flags=None): """ Returns a python dict() with properly formatted parameters to pass to update_bugs(). See bugzilla documentation for the format of the individual fields: https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug """ # pylint: disable=W0221 # Argument number differs from overridden method # Base defines it with *args, **kwargs, so we don't have to maintain # the master argument list in 2 places ret = {} # These are only supported for rhbugzilla for key, val in [ ("fixed_in", fixed_in), ("devel_whiteboard", devel_whiteboard), ("qa_whiteboard", qa_whiteboard), ("internal_whiteboard", internal_whiteboard), ("sub_component", sub_component), ]: if val is not None: raise ValueError("bugzilla instance does not support " "updating '%s'" % key) def s(key, val, convert=None): if val is None: return if convert: val = convert(val) ret[key] = val def add_dict(key, add, remove, _set=None, convert=None): if add is remove is _set is None: return def c(val): val = self._listify(val) if convert: val = [convert(v) for v in val] return val newdict = {} if add is not None: newdict["add"] = c(add) if remove is not None: newdict["remove"] = c(remove) if _set is not None: newdict["set"] = c(_set) ret[key] = newdict s("alias", alias) s("assigned_to", assigned_to) s("is_cc_accessible", is_cc_accessible, bool) s("component", component) s("deadline", deadline) s("dupe_of", dupe_of, int) s("estimated_time", estimated_time, int) s("op_sys", op_sys) s("platform", platform) s("priority", priority) s("product", product) s("qa_contact", qa_contact) s("is_creator_accessible", is_creator_accessible, bool) s("remaining_time", remaining_time, float) s("reset_assigned_to", reset_assigned_to, bool) s("reset_qa_contact", reset_qa_contact, bool) s("resolution", resolution) s("severity", severity) s("status", status) s("summary", summary) s("target_milestone", target_milestone) s("target_release", target_release) s("url", url) s("version", version) s("whiteboard", whiteboard) s("work_time", work_time, float) s("flags", flags) add_dict("blocks", blocks_add, blocks_remove, blocks_set, convert=int) add_dict("depends_on", depends_on_add, depends_on_remove, depends_on_set, convert=int) add_dict("cc", cc_add, cc_remove) add_dict("groups", groups_add, groups_remove) add_dict("keywords", keywords_add, keywords_remove, keywords_set) add_dict("see_also", see_also_add, see_also_remove) if comment is not None: ret["comment"] = {"comment": comment} if comment_private: ret["comment"]["is_private"] = comment_private return ret ######################################## # Methods for working with attachments # ######################################## def _attachment_uri(self, attachid): '''Returns the URI for the given attachment ID.''' att_uri = self.url.replace('xmlrpc.cgi', 'attachment.cgi') att_uri = att_uri + '?id=%s' % attachid return att_uri def attachfile(self, idlist, attachfile, description, **kwargs): ''' Attach a file to the given bug IDs. Returns the ID of the attachment or raises XMLRPC Fault if something goes wrong. attachfile may be a filename (which will be opened) or a file-like object, which must provide a 'read' method. If it's not one of these, this method will raise a TypeError. description is the short description of this attachment. Optional keyword args are as follows: file_name: this will be used as the filename for the attachment. REQUIRED if attachfile is a file-like object with no 'name' attribute, otherwise the filename or .name attribute will be used. comment: An optional comment about this attachment. is_private: Set to True if the attachment should be marked private. is_patch: Set to True if the attachment is a patch. content_type: The mime-type of the attached file. Defaults to application/octet-stream if not set. NOTE that text files will *not* be viewable in bugzilla unless you remember to set this to text/plain. So remember that! Returns the list of attachment ids that were added. If only one attachment was added, we return the single int ID for back compat ''' if isinstance(attachfile, str): f = open(attachfile) elif hasattr(attachfile, 'read'): f = attachfile else: raise TypeError("attachfile must be filename or file-like object") # Back compat if "contenttype" in kwargs: kwargs["content_type"] = kwargs.pop("contenttype") if "ispatch" in kwargs: kwargs["is_patch"] = kwargs.pop("ispatch") if "isprivate" in kwargs: kwargs["is_private"] = kwargs.pop("isprivate") if "filename" in kwargs: kwargs["file_name"] = kwargs.pop("filename") kwargs['summary'] = description data = f.read() if not isinstance(data, bytes): data = data.encode(locale.getpreferredencoding()) kwargs['data'] = Binary(data) kwargs['ids'] = self._listify(idlist) if 'file_name' not in kwargs and hasattr(f, "name"): kwargs['file_name'] = os.path.basename(f.name) if 'content_type' not in kwargs: ctype = _detect_filetype(getattr(f, "name", None)) if not ctype: ctype = 'application/octet-stream' kwargs['content_type'] = ctype ret = self._proxy.Bug.add_attachment(kwargs) if "attachments" in ret: # Up to BZ 4.2 ret = [int(k) for k in ret["attachments"].keys()] elif "ids" in ret: # BZ 4.4+ ret = ret["ids"] if isinstance(ret, list) and len(ret) == 1: ret = ret[0] return ret def openattachment(self, attachid): '''Get the contents of the attachment with the given attachment ID. Returns a file-like object.''' def get_filename(headers): import re match = re.search( r'^.*filename="?(.*)"$', headers.get('content-disposition', '') ) # default to attchid if no match was found return match.group(1) if match else attachid att_uri = self._attachment_uri(attachid) defaults = self._transport.request_defaults.copy() defaults["headers"] = defaults["headers"].copy() del(defaults["headers"]["Content-Type"]) response = self._transport.session.get( att_uri, stream=True, **defaults) ret = BytesIO() for chunk in response.iter_content(chunk_size=1024): if chunk: ret.write(chunk) ret.name = get_filename(response.headers) # Hooray, now we have a file-like object with .read() and .name ret.seek(0) return ret def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): ''' Updates a flag for the given attachment ID. Optional keyword args are: status: new status for the flag ('-', '+', '?', 'X') requestee: new requestee for the flag ''' # Bug ID was used for the original custom redhat API, no longer # needed though ignore = bugid flags = {"name": flagname} flags.update(kwargs) update = {'ids': [int(attachid)], 'flags': [flags]} log.debug("Calling Bug.update_attachment(%s)", update) return self._proxy.Bug.update_attachment(update) ##################### # createbug methods # ##################### createbug_required = ('product', 'component', 'summary', 'version', 'description') def build_createbug(self, product=None, component=None, version=None, summary=None, description=None, comment_private=None, blocks=None, cc=None, assigned_to=None, keywords=None, depends_on=None, groups=None, op_sys=None, platform=None, priority=None, qa_contact=None, resolution=None, severity=None, status=None, target_milestone=None, target_release=None, url=None, sub_component=None, alias=None): """" Returns a python dict() with properly formatted parameters to pass to createbug(). See bugzilla documentation for the format of the individual fields: https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug """ localdict = {} if blocks: localdict["blocks"] = self._listify(blocks) if cc: localdict["cc"] = self._listify(cc) if depends_on: localdict["depends_on"] = self._listify(depends_on) if groups: localdict["groups"] = self._listify(groups) if keywords: localdict["keywords"] = self._listify(keywords) if description: localdict["description"] = description if comment_private: localdict["comment_is_private"] = True # Most of the machinery and formatting here is the same as # build_update, so reuse that as much as possible ret = self.build_update(product=product, component=component, version=version, summary=summary, op_sys=op_sys, platform=platform, priority=priority, qa_contact=qa_contact, resolution=resolution, severity=severity, status=status, target_milestone=target_milestone, target_release=target_release, url=url, assigned_to=assigned_to, sub_component=sub_component, alias=alias) ret.update(localdict) return ret def _validate_createbug(self, *args, **kwargs): # Previous API required users specifying keyword args that mapped # to the XMLRPC arg names. Maintain that bad compat, but also allow # receiving a single dictionary like query() does if kwargs and args: raise BugzillaError("createbug: cannot specify positional " "args=%s with kwargs=%s, must be one or the " "other." % (args, kwargs)) if args: if len(args) > 1 or not isinstance(args[0], dict): raise BugzillaError("createbug: positional arguments only " "accept a single dictionary.") data = args[0] else: data = kwargs # If we're getting a call that uses an old fieldname, convert it to the # new fieldname instead. for newname, oldname in self._get_api_aliases(): if (newname in self.createbug_required and newname not in data and oldname in data): data[newname] = data.pop(oldname) # Back compat handling for check_args if "check_args" in data: del(data["check_args"]) return data def createbug(self, *args, **kwargs): ''' Create a bug with the given info. Returns a new Bug object. Check bugzilla API documentation for valid values, at least product, component, summary, version, and description need to be passed. ''' data = self._validate_createbug(*args, **kwargs) log.debug("Calling Bug.create with: %s", data) rawbug = self._proxy.Bug.create(data) return Bug(self, bug_id=rawbug["id"], autorefresh=self.bug_autorefresh) ############################## # Methods for handling Users # ############################## def _getusers(self, ids=None, names=None, match=None): '''Return a list of users that match criteria. :kwarg ids: list of user ids to return data on :kwarg names: list of user names to return data on :kwarg match: list of patterns. Returns users whose real name or login name match the pattern. :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the names array. Code 304: if the user was not authorized to see user they requested. Code 505: user is logged out and can't use the match or ids parameter. Available in Bugzilla-3.4+ ''' params = {} if ids: params['ids'] = self._listify(ids) if names: params['names'] = self._listify(names) if match: params['match'] = self._listify(match) if not params: raise BugzillaError('_get() needs one of ids, ' ' names, or match kwarg.') log.debug("Calling User.get with: %s", params) return self._proxy.User.get(params) def getuser(self, username): '''Return a bugzilla User for the given username :arg username: The username used in bugzilla. :raises XMLRPC Fault: Code 51 if the username does not exist :returns: User record for the username ''' ret = self.getusers(username) return ret and ret[0] def getusers(self, userlist): '''Return a list of Users from . :userlist: List of usernames to lookup :returns: List of User records ''' userobjs = [User(self, **rawuser) for rawuser in self._getusers(names=userlist).get('users', [])] # Return users in same order they were passed in ret = [] for u in userlist: for uobj in userobjs[:]: if uobj.email == u: userobjs.remove(uobj) ret.append(uobj) break ret += userobjs return ret def searchusers(self, pattern): '''Return a bugzilla User for the given list of patterns :arg pattern: List of patterns to match against. :returns: List of User records ''' return [User(self, **rawuser) for rawuser in self._getusers(match=pattern).get('users', [])] def createuser(self, email, name='', password=''): '''Return a bugzilla User for the given username :arg email: The email address to use in bugzilla :kwarg name: Real name to associate with the account :kwarg password: Password to set for the bugzilla account :raises XMLRPC Fault: Code 501 if the username already exists Code 500 if the email address isn't valid Code 502 if the password is too short Code 503 if the password is too long :return: User record for the username ''' self._proxy.User.create(email, name, password) return self.getuser(email) def updateperms(self, user, action, groups): ''' A method to update the permissions (group membership) of a bugzilla user. :arg user: The e-mail address of the user to be acted upon. Can also be a list of emails. :arg action: add, remove, or set :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) ''' groups = self._listify(groups) if action == "rem": action = "remove" if action not in ["add", "remove", "set"]: raise BugzillaError("Unknown user permission action '%s'" % action) update = { "names": self._listify(user), "groups": { action: groups, } } log.debug("Call User.update with: %s", update) return self._proxy.User.update(update) python-bugzilla-2.1.0/README.md0000664000175100017510000000134213062015413017576 0ustar crobinsocrobinso00000000000000# python-bugzilla This package provides two bits: * 'bugzilla' python module for talking to a [Bugzilla](https://www.bugzilla.org/) instance over XMLRPC * /usr/bin/bugzilla command line tool for performing actions from the command line: create or edit bugs, various queries, etc. This was originally written specifically for Red Hat's Bugzilla instance and is used heavily at Red Hat and in Fedora, but it should still be generically useful. You can find some code examples in the [examples](examples) directory For questions about submitting bug reports or patches, see [CONTRIBUTING.md](CONTRIBUTING.md) Questions, comments, and discussions should go to our mailing: http://lists.fedorahosted.org/mailman/listinfo/python-bugzilla python-bugzilla-2.1.0/setup.cfg0000664000175100017510000000007313067312016020144 0ustar crobinsocrobinso00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 python-bugzilla-2.1.0/NEWS.md0000664000175100017510000000565313067311773017443 0ustar crobinsocrobinso00000000000000# python-bugzilla release news ## Release 2.1.0 (March 30, 2017) - Support for bugzilla 5 API Keys (Dustin J. Mitchell) - bugzillarc can be used to set default URL for the cli tool - Revive update_flags wrapper - Bug fixes and minor improvements ## Release 2.0.0 (Feb 08 2017) This release contains several small to medium API breaks. I expect most users won't notice any difference. I previously outlined the changes here: https://lists.fedorahosted.org/archives/list/python-bugzilla@lists.fedorahosted.org/thread/WCYPOKJZFYOW7RRT44FCM5GQU26O56K4/ The major changes are: - Several fixes for use with bugzilla 5 - Bugzilla.bug_autorefresh now defaults to False - Credentials are now cached in ~/.cache/python-bugzilla/ - bin/bugzilla was converted to argparse - bugzilla query --boolean_chart option is removed - Unify command line flags across sub commands ## Release 1.2.2 (Sep 23 2015) - Switch hosting to http://github.com/python-bugzilla/python-bugzilla - Fix requests usage when ndg-httpsclient is installed (Arun Babu Neelicattu) - Add non-rhbz support for getting bug comments (AJ Lewis) - Misc bugfixes and improvements ## Release 1.2.1 (May 22 2015) - bin/bugzilla: Add --ensure-logged-in option - Fix get_products with bugzilla.redhat.com - A few other minor improvements ## Release 1.2.0 (Apr 08 2015) - Add bugzilla new/query/modify --field flag (Arun Babu Neelicattu) - API support for ExternalBugs (Arun Babu Neelicattu, Brian Bouterse) - Add new/modify --alias support (Adam Williamson) - Bugzilla.logged_in now returns live state (Arun Babu Neelicattu) - Fix getbugs API with latest Bugzilla releases ## Release 1.1.0 (Jun 01 2014) - Support for bugzilla tokens (Arun Babu Nelicattu) - bugzilla: Add query/modify --tags - bugzilla --login: Allow to login and run a command in one shot - bugzilla --no-cache-credentials: Don't use or save cached credentials when using the CLI - Show bugzilla errors when login fails - Don't pull down attachments in bug.refresh(), need to get bug.attachments manually - Add Bugzilla bug_autorefresh parameter. ## Release 1.0.0 (Mar 25 2014) - Python 3 support (Arun Babu Neelicattu) - Port to python-requests (Arun Babu Neelicattu) - bugzilla: new: Add --keywords, --assigned_to, --qa_contact (Lon Hohberger) - bugzilla: query: Add --quicksearch, --savedsearch - bugzilla: query: Support saved searches with --from-url - bugzilla: --sub-component support for all relevant commands ## Release 0.9.0 (Jun 19 2013) - CVE-2013-2191: Switch to pycurl to get SSL host and cert validation - bugzilla: modify: add --dependson (Don Zickus) - bugzilla: new: add --groups option (Paul Frields) - bugzilla: modify: Allow setting nearly every bug parameter - NovellBugzilla implementation removed, can't get it to work ## Release 0.8.0 (Feb 16 2013) - Replace usage of non-upstream Red Hat bugzilla APIs with upstream replacements - Test suite improvements, nearly complete code coverage - Fix all open bug reports and RFEs