python-bugzilla-2.3.0/0000775000175000017500000000000013531047715016332 5ustar crobinsocrobinso00000000000000python-bugzilla-2.3.0/CONTRIBUTING.md0000664000175000017500000000421613335550333020563 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 Our test suite uses pytest. If your system has dependencies already, the quick unit test suite is invoked simply with: pytest ## Read-Only Functional tests There are more comprehensive, readonly functional tests that run against several public bugzilla instances, but they are not run by default. No login account is required. Run them with: pytest --ro-functional ## Read/Write Functional Tests. Read/Write functional tests use partner-bugzilla.redhat.com, which is a bugzilla instance specifically for this type of testing. Data is occasionally hard synced with regular bugzilla.redhat.com, and all local edits are removed. Login accounts are also synced. If you want access to partner-bugzilla.redhat.com, sign up for a regular bugzilla.redhat.com login and wait for the next sync period. Before running these tests, you'll need to cache login credentials. Example: ./bugzilla-cli --bugzilla=partner-bugzilla.redhat.com --username=$USER login pytest --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 pycodestyle To test for pylint or pycodestyle violations, you can run: ./setup.py pylint Note: This expects that you already have pylint and pycodestyle installed. # Patch Submission If you are submitting a patch, ensure the following: [REQ] verify that no new pylint or pycodestyle 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.3.0/COPYING0000664000175000017500000004325413066537263017402 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.3.0/MANIFEST.in0000664000175000017500000000035613204543154020067 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.3.0/NEWS.md0000664000175000017500000000671413531047627017442 0ustar crobinsocrobinso00000000000000# python-bugzilla release news ## Release 2.3.0 (August 26, 2019) - restrict-login suppot (Viliam Krizan) - cli: Add support for private attachments (Brian 'Redbeard' Harrington) - Fix python3 deprecation warnings - Drop python 3.3 support, minimum python3 is python 3.4 now ## Release 2.2.0 (August 11, 2018) - Port tests to pytest - cli: --cert Client side certificate support (Tobias Wolter) - cli: add ability to post comment while sending attachment (Jeff Mahoney) - cli: Add --comment-tag option - cli: Add info --active-components - Add a raw Product.get wrapper API ## 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 python-bugzilla-2.3.0/PKG-INFO0000664000175000017500000000161113531047715017426 0ustar crobinsocrobinso00000000000000Metadata-Version: 1.1 Name: python-bugzilla Version: 2.3.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 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 python-bugzilla-2.3.0/README.md0000664000175000017500000000134213204543154017604 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.3.0/bugzilla/0000775000175000017500000000000013531047715020143 5ustar crobinsocrobinso00000000000000python-bugzilla-2.3.0/bugzilla/__init__.py0000664000175000017500000000256213333373136022260 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.3.0/bugzilla/_cli.py0000775000175000017500000012311013527025740021423 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python3 # # 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 errno import locale from logging import getLogger, DEBUG, INFO, WARN, StreamHandler, Formatter import argparse import os import re import socket import sys import tempfile # pylint: disable=import-error if sys.version_info[0] >= 3: # pylint: disable=no-name-in-module,redefined-builtin from xmlrpc.client import Fault, ProtocolError from urllib.parse import urlparse basestring = (str, bytes) else: from xmlrpclib import Fault, ProtocolError from urlparse import urlparse # pylint: enable=import-error import requests.exceptions import bugzilla DEFAULT_BZ = 'https://bugzilla.redhat.com/xmlrpc.cgi' format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}") log = getLogger(bugzilla.__name__) ################ # Util helpers # ################ def _is_unittest(): return bool(os.getenv("__BUGZILLA_UNITTEST")) def _is_unittest_debug(): return bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) def to_encoding(ustring): string = '' if isinstance(ustring, basestring): string = ustring elif ustring is not None: string = str(ustring) if sys.version_info[0] >= 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 as err: if err.errno == 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 def setup_logging(debug, verbose): handler = StreamHandler(sys.stderr) handler.setFormatter(Formatter( "[%(asctime)s] %(levelname)s (%(module)s:%(lineno)d) %(message)s", "%H:%M:%S")) log.addHandler(handler) if debug: log.setLevel(DEBUG) elif verbose: log.setLevel(INFO) else: log.setLevel(WARN) if _is_unittest_debug(): log.setLevel(DEBUG) ################## # 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('--cert', help="client side certificate file needed by the webserver") 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('--restrict-login', action="store_true", help="The session (login token) will be restricted to " "the current IP address.") 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) if not cmd_query: p.add_argument("--comment-tag", action="append", help="Comment tag for the new comment") 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+ \n" " clear with --flag needinfoX") p.add_argument("--tags", action="append", help="Tags/Personal 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", dest="test_return_result", 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) x = p.add_mutually_exclusive_group(required=True) x.add_argument('-p', '--products', action='store_true', help='Get a list of products') x.add_argument('-c', '--components', metavar="PRODUCT", help='List the components in the given product') x.add_argument('-o', '--component_owners', metavar="PRODUCT", help='List components (and their owners)') x.add_argument('-v', '--versions', metavar="PRODUCT", help='List the versions for the given product') p.add_argument('--active-components', action="store_true", help='Only show active components. Combine with --components*') 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("ids", nargs="+", help="Bug IDs to modify") 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("ids", nargs="*", help="BUGID references") 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") p.add_argument('-l', '--comment', '--long_desc', help="Add comment with attachment") p.add_argument('--private', action='store_true', default=False, help='Mark new comment as private') def _setup_action_login_parser(subparsers): usage = 'bugzilla login [username [password]]' description = """Log into bugzilla and save a login cookie or token. Note: These tokens are short-lived, and future Bugzilla versions will no longer support token authentication at all. Please use a ~/.config/python-bugzilla/bugzillarc file with an API key instead.""" p = subparsers.add_parser("login", description=description, usage=usage) p.add_argument("pos_username", nargs="?", help="Optional username", metavar="username") p.add_argument("pos_password", nargs="?", help="Optional password", metavar="password") def setup_parser(): rootparser = _setup_root_parser() subparsers = rootparser.add_subparsers(dest="command") subparsers.required = True _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 Exception: 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): 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 def _filter_components(compdetails): ret = {} for k, v in compdetails.items(): if v.get("is_active", True): ret[k] = v return ret productname = (opt.components or opt.component_owners or opt.versions) include_fields = ["name", "id"] fastcomponents = (opt.components and not opt.active_components) if opt.versions: include_fields += ["versions"] if opt.component_owners: include_fields += [ "components.default_assigned_to", "components.name", ] if (opt.active_components and any(["components" in i for i in include_fields])): include_fields += ["components.is_active"] bz.refresh_products(names=productname and [productname] or None, include_fields=include_fields) if opt.products: for name in sorted([p["name"] for p in bz.getproducts()]): print(name) elif fastcomponents: for name in sorted(bz.getcomponents(productname)): print(name) elif opt.components: details = bz.getcomponentsdetails(productname) for name in sorted(_filter_components(details)): print(name) elif opt.versions: proddict = bz.getproducts()[0] for v in proddict['versions']: print(to_encoding(v["name"])) elif opt.component_owners: details = bz.getcomponentsdetails(productname) for c in sorted(_filter_components(details)): print(to_encoding(u"%s: %s" % (c, details[c]['default_assigned_to']))) 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 in ["flags", "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.get("author", "")), c['text'])) elif fieldname == "external_bugs": val = "" for e in getattr(b, "external_bugs", []): url = e["type"]["full_url"].replace("%id%", e["ext_bz_bug_id"]) if not val: val += "\n" val += "External bug: %s\n" % url 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, comment_tags=opt.comment_tag 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): bugid_list = [bugid for a in opt.ids 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, comment_tags=opt.comment_tag 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): 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) def _do_set_attach(bz, opt, parser): if not opt.ids: 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, "rb") 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 if opt.comment: kwargs["comment"] = opt.comment if opt.private: kwargs["is_private"] = True desc = opt.desc or os.path.basename(fileobj.name) # Upload attachments for bugid in opt.ids: 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 use_creds = False if opt.cache_credentials: cookiefile = opt.cookiefile or -1 tokenfile = opt.tokenfile or -1 use_creds = True bz = bugzilla.Bugzilla( url=opt.bugzilla, cookiefile=cookiefile, tokenfile=tokenfile, sslverify=opt.sslverify, use_creds=use_creds, cert=opt.cert) return bz def _handle_login(opt, 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) username = getattr(opt, "pos_username", None) or opt.username password = getattr(opt, "pos_password", None) or opt.password try: if do_interactive_login: if bz.url: print("Logging into %s" % urlparse(bz.url)[1]) bz.interactive_login(username, password, restrict_login=opt.restrict_login) except bugzilla.BugzillaError as e: print(str(e)) 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): parser = setup_parser() opt = parser.parse_args() action = opt.command setup_logging(opt.debug, opt.verbose) log.debug("Launched with command line: %s", " ".join(sys.argv)) log.debug("Bugzilla module: %s", bugzilla) # 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, 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 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': buglist = _do_query(bz, opt, parser) if opt.test_return_result: return buglist elif action == 'new': buglist = _do_new(bz, opt, parser) if opt.test_return_result: return buglist elif action == 'attach': if opt.get or opt.getall: if opt.ids: parser.error("Bug IDs '%s' not used for " "getting attachments" % opt.ids) _do_get_attach(bz, opt) else: _do_set_attach(bz, opt, parser) elif action == 'modify': modout = _do_modify(bz, parser, opt) 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) def main(unittest_bz_instance=None): try: try: return _main(unittest_bz_instance) except (Exception, KeyboardInterrupt): log.debug("", exc_info=True) raise except (Fault, bugzilla.BugzillaError) as e: print("\nServer error: %s" % str(e)) sys.exit(3) except requests.exceptions.SSLError as e: # 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, ProtocolError) as e: print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) def cli(): try: main() except KeyboardInterrupt: log.debug("", exc_info=True) print("\nExited at user request.") sys.exit(1) python-bugzilla-2.3.0/bugzilla/apiversion.py0000664000175000017500000000062513531047627022701 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.3.0" __version__ = version python-bugzilla-2.3.0/bugzilla/base.py0000664000175000017500000020302213527620024021421 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 mimetypes import os import sys from io import BytesIO # pylint: disable=import-error if sys.version_info[0] >= 3: # pylint: disable=no-name-in-module from collections.abc import Mapping from configparser import ConfigParser from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse, parse_qsl from xmlrpc.client import Binary, Fault else: from collections import Mapping from ConfigParser import SafeConfigParser as ConfigParser from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse, parse_qsl from xmlrpclib import Binary, Fault # pylint: enable=import-error from .apiversion import __version__ from .bug import Bug, User from .transport import BugzillaError, _BugzillaServerProxy, _RequestsTransport log = getLogger(__name__) def _nested_update(d, u): # Helper for nested dict update() for k, v in list(u.items()): if isinstance(v, Mapping): d[k] = _nested_update(d.get(k, {}), v) else: d[k] = v return d 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 = ConfigParser() 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.component_names = {} self.bugfields = [] 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't 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, cert=None, configpaths=-1): """ :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 cert: optional certificate file for client side certificate authentication :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 bugzilla5+ API key :param configpaths: A list of possible bugzillarc locations. """ if url == -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.cert = cert or '' 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() if not use_creds: cookiefile = None tokenfile = None configpaths = [] if cookiefile == -1: cookiefile = _default_auth_location("bugzillacookies") if tokenfile == -1: tokenfile = _default_auth_location("bugzillatoken") if configpaths == -1: configpaths = _default_configpaths[:] log.debug("Using tokenfile=%s", tokenfile) self.cookiefile = cookiefile self.tokenfile = tokenfile self.configpath = configpaths 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 if isinstance(self, RHBugzilla): return 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 _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) def _parse_hostname(_u): # If http://example.com is passed, netloc=example.com path="" # If just example.com is passed, netloc="" path=example.com parsedbits = urlparse(self.url) return parsedbits.netloc or parsedbits.path urlhost = _parse_hostname(self.url) for sectionhost in sorted(cfg.sections()): # If the section is just a hostname, make it match # If the section has a / in it, do a substring match if "/" not in sectionhost: if sectionhost == urlhost: section = sectionhost elif sectionhost in self.url: section = sectionhost if section: log.debug("bugzillarc: Found matching section: %s", section) break 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 elif key == "cert": log.debug("bugzillarc: setting cert") self.cert = 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 Exception: 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, cert=self.cert) 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, restrict_login=None): """ Backend login method for Bugzilla3 """ payload = {'login': user, 'password': password} if restrict_login: payload['restrict_login'] = True return self._proxy.User.login(payload) def _logout(self): """ Backend login method for Bugzilla3 """ return self._proxy.User.logout() def login(self, user=None, password=None, restrict_login=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. The login session can be restricted to current user IP address with restrict_login argument. (Bugzilla 4.4+) 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") if restrict_login: log.info("logging in with restrict_login=True") try: ret = self._login(self.user, self.password, restrict_login) self.password = '' log.info("login successful for user=%s", self.user) return ret except Fault as e: raise BugzillaError("Login failed: %s" % str(e.faultString)) def interactive_login(self, user=None, password=None, force=False, restrict_login=None): """ 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 :param restrict_login: restricts session to IP address """ 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, restrict_login) 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. If we tried to login with a token, but the token was incorrect or expired, the server returns a 32000 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 as e: if e.faultCode == 505 or e.faultCode == 32000: return False raise e ###################### # Bugfields querying # ###################### 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)) #################### # Product querying # #################### def product_get(self, ids=None, names=None, include_fields=None, exclude_fields=None, ptype=None): """ Raw wrapper around Product.get https://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product This does not perform any caching like other product API calls. If ids, names, or ptype is not specified, we default to ptype=accessible for historical reasons @ids: List of product IDs to lookup @names: List of product names to lookup @ptype: Either 'accessible', 'selectable', or 'enterable'. If specified, we return data for all those @include_fields: Only include these fields in the output @exclude_fields: Do not include these fields in the output """ if ids is None and names is None and ptype is None: ptype = "accessible" if ptype: raw = None if ptype == "accessible": raw = self._proxy.Product.get_accessible_products() elif ptype == "selectable": raw = self._proxy.Product.get_selectable_products() elif ptype == "enterable": raw = self._proxy.Product.get_enterable_products() if raw is None: raise RuntimeError("Unknown ptype=%s" % ptype) ids = raw['ids'] log.debug("For ptype=%s found ids=%s", ptype, ids) kwargs = {} 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 ret = self._proxy.Product.get(kwargs) return ret['products'] def refresh_products(self, **kwargs): """ Refresh a product's cached info. Basically calls product_get with the passed arguments, and tries to intelligently update our product cache. For example, if we already have cached info for product=foo, and you pass in names=["bar", "baz"], the new cache will have info for products foo, bar, baz. Individual product fields are also updated. """ for product in self.product_get(**kwargs): updated = 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 _nested_update(current, product) updated = True break if not updated: self._cache.products.append(product) def getproducts(self, force_refresh=False, **kwargs): """ Query all products and return the raw dict info. Takes all the same arguments as product_get. On first invocation this will contact bugzilla and internally cache the results. Subsequent getproducts calls or accesses to self.products will return this cached data only. :param force_refresh: force refreshing via refresh_products() """ if force_refresh or not self._cache.products: self.refresh_products(**kwargs) return self._cache.products products = property( fget=lambda self: self.getproducts(), fdel=lambda self: setattr(self, '_products', None), doc="Helper for accessing the products cache. If nothing " "has been cached yet, this calls getproducts()") ####################### # components querying # ####################### def _lookup_product_in_cache(self, productname): prodstr = isinstance(productname, str) and productname or None prodint = isinstance(productname, int) and productname or None for proddict in self._cache.products: if prodstr == proddict.get("name", -1): return proddict if prodint == proddict.get("id", "nope"): return proddict return {} def getcomponentsdetails(self, product, force_refresh=False): """ Wrapper around Product.get(include_fields=["components"]), returning only the "components" data for the requested product, slightly reworked to a dict mapping of components.name: components, for historical reasons. This uses the product cache, but will update it if the product isn't found or "components" isn't cached for the product. In cases like bugzilla.redhat.com where there are tons of components for some products, this API will time out. You should use product_get instead. """ proddict = self._lookup_product_in_cache(product) if (force_refresh or not proddict or "components" not in proddict): self.refresh_products(names=[product], include_fields=["name", "id", "components"]) proddict = self._lookup_product_in_cache(product) ret = {} for compdict in proddict["components"]: ret[compdict["name"]] = compdict return ret def getcomponentdetails(self, product, component, force_refresh=False): """ Helper for accessing a single component's info. This is a wrapper around getcomponentsdetails, see that for explanation """ d = self.getcomponentsdetails(product, force_refresh) return d[component] def getcomponents(self, product, force_refresh=False): """ Return a list of component names for the passed product. This can be implemented with Product.get, but behind the scenes it uses Bug.legal_values. Reason being that on bugzilla instances with tons of components, like bugzilla.redhat.com Product=Fedora for example, there's a 10x speed difference even with properly limited Product.get calls. On first invocation the value is cached, and subsequent calls will return the cached data. :param force_refresh: Force refreshing the cache, and return the new data """ proddict = self._lookup_product_in_cache(product) product_id = proddict.get("id", None) if (force_refresh or product_id is None or product_id not in self._cache.component_names): self.refresh_products(names=[product], include_fields=["name", "id"]) proddict = self._lookup_product_in_cache(product) if "id" not in proddict: raise BugzillaError("Product '%s' not found" % product) product_id = proddict["id"] opts = {'product_id': product_id, 'field': 'component'} names = self._proxy.Bug.legal_values(opts)["values"] self._cache.component_names[product_id] = names return self._cache.component_names[product_id] ############################ # component adding/editing # ############################ def _component_data_convert(self, data, update=False): # 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 list(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 description: 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) 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) return self._proxy.Component.update(data) ################### # 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)) 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 self._listify(valdict.get("alias", None) or []): 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. """ try: r = self._proxy.Bug.search(query) except Fault as e: # 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) 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, } 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, comment_tags=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 """ 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) s("comment_tags", comment_tags, self._listify) 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, "rb") 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 = None if kwargs['file_name']: ctype = mimetypes.guess_type( kwargs['file_name'], strict=False)[0] kwargs['content_type'] = ctype or 'application/octet-stream' 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. """ attachments = self.get_attachments(None, attachid) data = attachments["attachments"][str(attachid)] xmlrpcbinary = data["data"] ret = BytesIO() ret.write(xmlrpcbinary.data) ret.name = data["file_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]} return self._proxy.Bug.update_attachment(update) def get_attachments(self, ids, attachment_ids, include_fields=None, exclude_fields=None): """ Wrapper for Bug.attachments. One of ids or attachment_ids is required :param ids: Get attachments for this bug ID :param attachment_ids: Specific attachment ID to get https://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment """ params = { "ids": self._listify(ids) or [], "attachment_ids": self._listify(attachment_ids) or [], } if include_fields: params["include_fields"] = self._listify(include_fields) if exclude_fields: params["exclude_fields"] = self._listify(exclude_fields) return self._proxy.Bug.attachments(params) ##################### # 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, comment_tags=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, comment_tags=comment_tags) 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) 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.') 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, } } return self._proxy.User.update(update) python-bugzilla-2.3.0/bugzilla/bug.py0000664000175000017500000003705513335550333021301 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. from __future__ import unicode_literals 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 sys.version_info[0] >= 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 "#%-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_attachments(self, include_fields=None, exclude_fields=None): """ Helper call to Bugzilla.get_attachments. If you want to fetch specific attachment IDs, use that function instead """ if "attachments" in self.__dict__: return self.attachments data = self.bugzilla.get_attachments([self.bug_id], None, include_fields, exclude_fields) return data["bugs"][str(self.bug_id)] def get_attachment_ids(self): """ Helper function to return only the attachment IDs for this bug """ return [a["id"] for a in self.get_attachments(exclude_fields=["data"])] 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.3.0/bugzilla/oldclasses.py0000664000175000017500000000157013423166260022651 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.3.0/bugzilla/rhbugzilla.py0000664000175000017500000003416313335550333022664 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) # Yes, sub_components is the field name the API expects self._add_field_alias('sub_components', 'sub_component', 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): # pylint: disable=arguments-differ 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): # pylint: disable=arguments-differ # 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.warning("%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.3.0/bugzilla/transport.py0000664000175000017500000001472413415453341022556 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 # pylint: disable=import-error if sys.version_info[0] >= 3: from configparser import ConfigParser from urllib.parse import urlparse # pylint: disable=no-name-in-module from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport else: from ConfigParser import SafeConfigParser as ConfigParser from urlparse import urlparse from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport # pylint: enable=import-error 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 = ConfigParser() 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, object): def __init__(self, uri, tokenfile, *args, **kwargs): super(_BugzillaServerProxy, self).__init__(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 = ({}, ) log.debug("XMLRPC call: %s(%s)", methodname, params[0]) 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=no-member ret = super(_BugzillaServerProxy, self)._ServerProxy__request(methodname, params) # pylint: enable=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=True, cert=None): 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() if cert: self.session.cert = cert 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 # pylint: disable=try-except-raise 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 as e: if not response: raise raise ProtocolError( url, response.status_code, str(e), response.headers) except Fault: raise except Exception: e = BugzillaError(str(sys.exc_info()[1])) # pylint: disable=attribute-defined-outside-init e.__traceback__ = sys.exc_info()[2] # pylint: enable=attribute-defined-outside-init 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.3.0/bugzilla.10000664000175000017500000002506213423166260020227 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 "--cert=CERTFILE" client side certificate file needed by the webserver. .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 "--restrict-login" The session (login token) will be restricted to the current IP address. .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 "--comment-tag=TAG" Comment tag for the new comment .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 "--flag=FLAG" Set or unset a flag. For example, to set a flag named devel_ack, do --flag devel_ack+ Unset a flag with the 'X' value, like --flag needinfoX .IP "--tags=TAG" Set (personal) tags field .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 .IP "--comment=COMMENT, -l COMMENT" Add comment with attachment .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=PRODUCT, -v PRODUCT" List the versions for the given product .IP "--active-components" Only show active components. Combine with --components* .SH AUTHENTICATION CACHE AND API KEYS Some command usage will require an active login to the bugzilla instance. For example, if the bugzilla instance has some private bugs, those bugs will be missing from 'query' output if you do not have an active login. If you are connecting to a bugzilla 5.0 or later instance, the best option is to use bugzilla API keys. From the bugzilla web UI, log in, navigate to Preferences->API Keys, and generate a key (it will be a long string of characters and numbers). Then create a ~/.config/python-bugzilla/bugzillarc like this: $ cat ~/.config/python-bugzilla/bugzillarc [bugzilla.example.com] api_key=YOUR_API_KEY Replace 'bugzilla.example.com' with your bugzilla host name, and YOUR_API_KEY with the generated API Key from the Web UI. For older bugzilla instances, you will need to cache a login cookie or token with the "login" subcommand or the "--login" argument. Additionally, the --no-cache-credentials option will tell the bugzilla tool to _not_ save or use any authentication cache, including the bugzillarc file. .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.3.0/examples/0000775000175000017500000000000013531047715020150 5ustar crobinsocrobinso00000000000000python-bugzilla-2.3.0/examples/apikey.py0000664000175000017500000000243213335550333022002 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 sys 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:\n " " https://landfill.bugzilla.org/bugzilla-5.0-branch/userprefs.cgi") print("This is a test site, so no harm will come!\n") # pylint: disable=undefined-variable if sys.version_info[0] >= 3: api_key = input("Enter Bugzilla API Key: ") else: api_key = raw_input("Enter Bugzilla API Key: ") # pylint: enable=undefined-variable # 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.3.0/examples/bug_autorefresh.py0000664000175000017500000000523113204543154023702 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.3.0/examples/create.py0000664000175000017500000000332513204543154021763 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.3.0/examples/getbug.py0000664000175000017500000000312613204543154021774 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.3.0/examples/query.py0000664000175000017500000000656113204543154021672 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.3.0/examples/update.py0000664000175000017500000000470413204543154022004 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.3.0/python-bugzilla.spec0000664000175000017500000000741613531047627022350 0ustar crobinsocrobinso00000000000000%if 0%{?fedora} || 0%{?rhel} > 7 # Enable python3 by default %bcond_without python3 %else %bcond_with python3 %endif %if 0%{?rhel} > 7 # Disable python2 build by default %bcond_with python2 %else %bcond_without python2 %{!?__python2: %global __python2 /usr/bin/python2} %{!?python2_sitelib: %global python2_sitelib %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} %endif Name: python-bugzilla Version: 2.3.0 Release: 1%{?dist} Summary: Python 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 %if %{with python2} BuildRequires: python2-devel BuildRequires: python2-requests BuildRequires: python2-setuptools BuildRequires: python2-pytest %endif # with python2 %if %{with python3} BuildRequires: python3-devel BuildRequires: python3-requests BuildRequires: python3-setuptools BuildRequires: python3-pytest %endif # if with_python3 %global _description\ python-bugzilla is a python library for interacting with bugzilla instances\ over XML-RPC.\ %description %_description %if %{with python2} %package -n python2-bugzilla Summary: %summary Requires: python2-requests # This dep is for back compat, so that installing python-bugzilla continues # to give the cli tool Requires: python-bugzilla-cli %{?python_provide:%python_provide python2-bugzilla} %description -n python2-bugzilla %_description %endif # with python2 %if %{with python3} %package -n python3-bugzilla Summary: %summary Requires: python3-requests %{?python_provide:%python_provide python3-bugzilla} %if %{without python2} Obsoletes: python-bugzilla < %{version}-%{release} Obsoletes: python2-bugzilla < %{version}-%{release} %endif # without python2 %description -n python3-bugzilla %_description %endif # if with_python3 %package cli Summary: Command line tool for interacting with Bugzilla %if %{with python3} Requires: python3-bugzilla = %{version}-%{release} %else Requires: python2-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 %{with python3} rm -rf %{py3dir} cp -a . %{py3dir} %endif # with_python3 %build %if %{with python3} pushd %{py3dir} %{__python3} setup.py build popd %endif # with_python3 %if %{with python2} %{__python2} setup.py build %endif # with python2 %install %if %{with python3} pushd %{py3dir} %{__python3} setup.py install -O1 --skip-build --root %{buildroot} %if %{with python2} rm %{buildroot}/usr/bin/bugzilla %endif popd %endif # with_python3 %if %{with python2} %{__python2} setup.py install -O1 --skip-build --root %{buildroot} %endif # with python2 # 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 %{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 %if %{with python2} # py.test naming is needed for RHEL7 compat, works fine with Fedora py.test %endif # with python2 %if %{with python3} pytest-3 %endif # with python3 %if %{with python2} %files -n python2-bugzilla %doc COPYING README.md NEWS.md %{python2_sitelib}/* %endif # with python2 %if %{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.3.0/python_bugzilla.egg-info/0000775000175000017500000000000013531047715023236 5ustar crobinsocrobinso00000000000000python-bugzilla-2.3.0/python_bugzilla.egg-info/PKG-INFO0000664000175000017500000000161113531047715024332 0ustar crobinsocrobinso00000000000000Metadata-Version: 1.1 Name: python-bugzilla Version: 2.3.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 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 python-bugzilla-2.3.0/python_bugzilla.egg-info/SOURCES.txt0000664000175000017500000000201013531047715025113 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 bugzilla/__init__.py bugzilla/_cli.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/entry_points.txt python_bugzilla.egg-info/requires.txt python_bugzilla.egg-info/top_level.txt tests/__init__.py tests/conftest.py tests/pycodestyle.cfg tests/test_bug.py tests/test_createbug.py tests/test_misc.py tests/test_modify.py tests/test_query.py tests/test_ro_functional.py tests/test_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.3.0/python_bugzilla.egg-info/dependency_links.txt0000664000175000017500000000000113531047715027304 0ustar crobinsocrobinso00000000000000 python-bugzilla-2.3.0/python_bugzilla.egg-info/entry_points.txt0000664000175000017500000000006013531047715026530 0ustar crobinsocrobinso00000000000000[console_scripts] bugzilla = bugzilla._cli:cli python-bugzilla-2.3.0/python_bugzilla.egg-info/requires.txt0000664000175000017500000000001113531047715025626 0ustar crobinsocrobinso00000000000000requests python-bugzilla-2.3.0/python_bugzilla.egg-info/top_level.txt0000664000175000017500000000001113531047715025760 0ustar crobinsocrobinso00000000000000bugzilla python-bugzilla-2.3.0/requirements.txt0000664000175000017500000000001113066537263021613 0ustar crobinsocrobinso00000000000000requests python-bugzilla-2.3.0/setup.cfg0000664000175000017500000000004613531047715020153 0ustar crobinsocrobinso00000000000000[egg_info] tag_build = tag_date = 0 python-bugzilla-2.3.0/setup.py0000775000175000017500000000761613423166260020056 0ustar crobinsocrobinso00000000000000#!/usr/bin/env python3 from __future__ import print_function import glob import os import sys from distutils.core import Command from setuptools import setup def unsupported_python_version(): return sys.version_info < (2, 7) \ or (sys.version_info > (3,) and sys.version_info < (3, 4)) if unsupported_python_version(): raise ImportError("python-bugzilla does not support this python version") def get_version(): f = open("bugzilla/apiversion.py") for line in f: if line.startswith('version = '): return eval(line.split('=')[-1]) # pylint: disable=eval-used class TestCommand(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): print("\n* Tests are now run with the 'pytest' tool.\n" "* See CONTRIBUTING.md for details.") class PylintCommand(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): import pylint.lint import pycodestyle files = (["bugzilla-cli", "bugzilla", "setup.py"] + glob.glob("examples/*.py") + glob.glob("tests/*.py")) output_format = sys.stdout.isatty() and "colorized" or "text" print("running pycodestyle") style_guide = pycodestyle.StyleGuide( config_file='tox.ini', format="pylint", paths=files, ) report = style_guide.check_files() if style_guide.options.count: sys.stderr.write(str(report.total_errors) + '\n') print("running pylint") pylint_opts = [ "--rcfile", "pylintrc", "--output-format=%s" % output_format, ] pylint.lint.Run(files + pylint_opts) 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', classifiers=[ 'Topic :: Software Development :: Libraries :: Python Modules', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], packages=['bugzilla'], entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, 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.3.0/test-requirements.txt0000664000175000017500000000006013335550333022564 0ustar crobinsocrobinso00000000000000# additional packages needed for testing pytest python-bugzilla-2.3.0/tests/0000775000175000017500000000000013531047715017474 5ustar crobinsocrobinso00000000000000python-bugzilla-2.3.0/tests/__init__.py0000664000175000017500000000427613335550333021613 0ustar crobinsocrobinso00000000000000 from __future__ import print_function import os import shlex import sys # pylint: disable=import-error if sys.version_info[0] >= 3: from io import StringIO else: from StringIO import StringIO # pylint: enable=import-error from bugzilla import Bugzilla, RHBugzilla, _cli class _CLICONFIG(object): def __init__(self): self.REDHAT_URL = None CLICONFIG = _CLICONFIG() os.environ["__BUGZILLA_UNITTEST"] = "1" 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 clicomm(argvstr, bzinstance, returnmain=False, stdin=None, expectfail=False): """ Run bin/bugzilla.main() directly with passed argv """ argv = shlex.split(argvstr) oldstdout = sys.stdout oldstderr = sys.stderr oldstdin = sys.stdin oldargv = sys.argv try: out_io = StringIO() sys.stdout = out_io sys.stderr = out_io if stdin: sys.stdin = stdin sys.argv = argv ret = 0 test_return = None try: print(" ".join(argv)) print() test_return = _cli.main(unittest_bz_instance=bzinstance) except SystemExit as sys_e: ret = sys_e.code outstr = out_io.getvalue() if outstr.endswith("\n"): outstr = outstr[:-1] if ret != 0 and not expectfail: raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % (ret, argvstr, outstr)) if ret == 0 and expectfail: raise RuntimeError("Command succeeded but we expected success\n" "ret=%d\ncmd=%s\nout=%s" % (ret, argvstr, outstr)) if returnmain: return test_return return outstr finally: sys.stdout = oldstdout sys.stderr = oldstderr sys.stdin = oldstdin sys.argv = oldargv python-bugzilla-2.3.0/tests/conftest.py0000664000175000017500000000333413335550333021673 0ustar crobinsocrobinso00000000000000import logging import os import tests import bugzilla # pytest plugin adding custom options. Hooks are documented here: # https://docs.pytest.org/en/latest/writing_plugins.html def pytest_addoption(parser): parser.addoption("--ro-functional", action="store_true", default=False, help=("Run readonly functional tests against actual " "bugzilla instances. This will be very slow.")) parser.addoption("--rw-functional", action="store_true", default=False, help=("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 login creds. This will " "also be very slow.")) parser.addoption("--redhat-url", help="Redhat bugzilla URL to use for ro/rw_functional tests") parser.addoption("--pybz-debug", action="store_true", default=False, help=("Enable python-bugzilla debug output. This may break " "output comparison tests.")) def pytest_ignore_collect(path, config): if ((os.path.basename(str(path)) == "test_ro_functional.py") and not config.getoption("--ro-functional")): return True if ((os.path.basename(str(path)) == "test_rw_functional.py") and not config.getoption("--rw-functional")): return True def pytest_configure(config): if config.getoption("--redhat-url"): tests.CLICONFIG.REDHAT_URL = config.getoption("--redhat-url") if config.getoption("--pybz-debug"): logging.getLogger(bugzilla.__name__).setLevel(logging.DEBUG) os.environ["__BUGZILLA_UNITTEST_DEBUG"] = "1" python-bugzilla-2.3.0/tests/data/0000775000175000017500000000000013531047715020405 5ustar crobinsocrobinso00000000000000python-bugzilla-2.3.0/tests/data/bz-attach-get1.txt0000664000175000017500000000255613066537263023676 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.3.0/tests/data/components_file.txt0000664000175000017500000000001413066537263024332 0ustar crobinsocrobinso00000000000000foo bar baz python-bugzilla-2.3.0/tests/data/cookies-bad.txt0000664000175000017500000000003413066537263023330 0ustar crobinsocrobinso00000000000000foo this is invalid cookies python-bugzilla-2.3.0/tests/data/cookies-lwp.txt0000664000175000017500000000046513066537263023414 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.3.0/tests/data/cookies-moz.txt0000664000175000017500000000044213066537263023412 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.3.0/tests/pycodestyle.cfg0000664000175000017500000000000013423166260022504 0ustar crobinsocrobinso00000000000000python-bugzilla-2.3.0/tests/test_bug.py0000664000175000017500000000424713335550333021666 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): 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(): assert hasattr(bug, "component") is True assert getattr(bug, "components") == ["foo"] assert getattr(bug, "product") == "bar" assert hasattr(bug, "short_desc") is True assert getattr(bug, "summary") == "some short desc" assert bool(getattr(bug, "cf_fixed_in")) is True assert getattr(bug, "fixed_in") == "1.2.3.4" assert bool(getattr(bug, "cf_devel_whiteboard")) is True assert getattr(bug, "devel_whiteboard") == "some status value" _assert_bug() assert str(bug) == "#123456 NEW - foo@bar.com - some short desc" assert repr(bug).startswith("= 3: from io import BytesIO fd = BytesIO() else: fd = StringIO() pickle.dump(bug, fd) fd.seek(0) bug = pickle.load(fd) assert getattr(bug, "bugzilla") is 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.3.0/tests/test_createbug.py0000664000175000017500000000502113335550333023041 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): bz = bz4 def clicomm(self, argstr, out): comm = "bugzilla new --__test-return-result " + argstr q = tests.clicomm(comm, self.bz, returnmain=True) assert 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"} ) self.clicomm("--comment 'foo bar' --comment-tag tag1 " "--comment-tag tag2", {'comment_tags': ['tag1', 'tag2'], 'description': 'foo bar'}) 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) assert out == { 'component': 'bar', 'description': 'foo', 'product': 'foo', 'summary': 'bar', 'version': '12'} python-bugzilla-2.3.0/tests/test_misc.py0000664000175000017500000001130613415637764022053 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 pytest 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) assert len(out.splitlines()) > 18 def testCmdHelp(self): out = tests.clicomm("bugzilla query --help", None) assert len(out.splitlines()) > 40 def testVersion(self): out = tests.clicomm("bugzilla --version", None) assert len(out.splitlines()) >= 2 def testPositionalArgs(self): # Make sure cli correctly rejects ambiguous positional args out = tests.clicomm("bugzilla login --xbadarg foo", None, expectfail=True) assert "unrecognized arguments: --xbadarg" in out out = tests.clicomm("bugzilla modify 123456 --foobar --status NEW", None, expectfail=True) assert "unrecognized arguments: --foobar" in out class MiscAPI(unittest.TestCase): """ Test miscellaneous API bits """ def testUserAgent(self): b3 = tests.make_bz("3.0.0") assert "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 with pytest.raises(bugzilla.BugzillaError): tests.make_bz("3.0.0", cookiefile=cookieslwp) with pytest.raises(bugzilla.BugzillaError): tests.make_bz("3.0.0", cookiefile=cookiesbad) # 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 = "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) assert bzapi.user == "test1" assert bzapi.password == "test2" assert bzapi.api_key is None bzapi.url = "foo.example.com" bzapi.user = None bzapi.readconfig(temp.name) assert bzapi.user is None content = """ [foo.example.com] user=test3 password=test4 api_key=123abc """ temp.write(content) temp.flush() bzapi.readconfig(temp.name) assert bzapi.user == "test3" assert bzapi.password == "test4" assert bzapi.api_key == "123abc" bzapi.url = "bugzilla.redhat.com" bzapi.user = None bzapi.password = None bzapi.api_key = None bzapi.readconfig(temp.name) assert bzapi.user is None assert bzapi.password is None assert bzapi.api_key is None def testPostTranslation(self): def _testPostCompare(bz, indict, outexpect): outdict = indict.copy() bz.post_translation({}, outdict) assert outdict == outexpect # Make sure multiple calls don't change anything bz.post_translation({}, outdict) assert 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.3.0/tests/test_modify.py0000664000175000017500000001460513335550333022377 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 pytest import tests rhbz = tests.make_bz("4.4.0", rhbz=True) class ModifyTest(unittest.TestCase): bz = rhbz def clicomm(self, argstr, out, wbout=None, tags_add=None, tags_rm=None): comm = "bugzilla modify --__test-return-result 123456 224466 " + argstr (mdict, wdict, tagsa, tagsr) = tests.clicomm( comm, self.bz, returnmain=True) if wbout: assert wbout == wdict if out: assert out == mdict if tags_add: assert tags_add == tagsa if tags_rm: assert 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"} ) self.clicomm("--comment 'foo bar' --comment-tag tag1 ", {'comment': {'comment': 'foo bar'}, 'comment_tags': ['tag1']}) 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): with pytest.raises(ValueError): self.bz.build_update(sub_component="some sub component") python-bugzilla-2.3.0/tests/test_query.py0000664000175000017500000002660113444204514022252 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 pytest 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. """ def clicomm(self, argstr, out): comm = "bugzilla query --__test-return-result " + argstr if not out: with pytest.raises(RuntimeError): tests.clicomm(comm, self.bz) else: q = tests.clicomm(comm, self.bz, returnmain=True) assert 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"] assert in_query == out_query in_query = {"bug_id": "123,456", "component": "foo,bar"} out_query = translate(in_query) assert out_query["id"] == ["123", "456"] assert out_query["component"] == ["foo", "bar"] in_query = {"bug_id": [123, 124], "column_list": ["id"]} out_query = translate(in_query) assert out_query["id"] == [123, 124] assert out_query["include_fields"] == in_query["column_list"] def testInvalidBoolean(self): with pytest.raises(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 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' } assert bz4.url_to_query(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' } assert bz4.url_to_query(url) == query python-bugzilla-2.3.0/tests/test_ro_functional.py0000664000175000017500000003431113403774613023753 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 os import unittest import pytest 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: with pytest.raises(Exception): tests.clicomm(comm, bz) else: return tests.clicomm(comm, bz) def _testBZVersion(self): bz = Bugzilla(self.url, use_creds=False) assert bz.__class__ == self.bzclass if tests.CLICONFIG.REDHAT_URL: return assert bz.bz_ver_major == self.bzversion[0] assert 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() assert len(out) >= mincount assert expectstr in out def _testInfoComps(self, comp, mincount, expectstr): out = self.clicomm("info --components \"%s\"" % comp).splitlines() assert len(out) >= mincount assert expectstr in out def _testInfoVers(self, comp, mincount, expectstr): out = self.clicomm("info --versions \"%s\"" % comp).splitlines() assert len(out) >= mincount if expectstr: assert expectstr in out def _testInfoCompOwners(self, comp, expectstr): expectexc = (expectstr == "FAIL") out = self.clicomm("info --component_owners \"%s\"" % comp, expectexc=expectexc) if expectexc: return assert 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 assert len(out.splitlines()) >= mincount assert bool([l1 for l1 in out.splitlines() if l1.startswith("#" + expectbug)]) # Check --ids output option out2 = self.clicomm(cli + " --ids") assert len(out.splitlines()) == len(out2.splitlines()) assert bool([l2 for l2 in out2.splitlines() if l2 == expectbug]) def _testQueryFull(self, bugid, mincount, expectstr): out = self.clicomm("query --full --bug_id %s" % bugid) assert len(out.splitlines()) >= mincount assert expectstr in out def _testQueryRaw(self, bugid, mincount, expectstr): out = self.clicomm("query --raw --bug_id %s" % bugid) assert len(out.splitlines()) >= mincount assert expectstr in out def _testQueryOneline(self, bugid, expectstr): out = self.clicomm("query --oneline --bug_id %s" % bugid) assert len(out.splitlines()) == 3 assert out.splitlines()[2].startswith("#%s" % bugid) assert expectstr in out def _testQueryExtra(self, bugid, expectstr): out = self.clicomm("query --extra --bug_id %s" % bugid) assert ("#%s" % bugid) in out assert expectstr in out def _testQueryFormat(self, args, expectstr): out = self.clicomm("query %s" % args) assert 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) assert len(out.splitlines()) == count assert 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 try: bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) assert bz.__class__ == Bugzilla assert bz.bz_ver_major >= 2016 assert bz.bz_ver_minor in range(1, 13) except Exception as e: # travis environment throws SSL errors here # https://travis-ci.org/python-bugzilla/python-bugzilla/builds/304713566 if "EOF occurred" not in str(e): raise self.skipTest("travis environment SSL error hit: %s" % str(e)) 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)) assert 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 as e: assert "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.CLICONFIG.REDHAT_URL or "https://bugzilla.redhat.com/xmlrpc.cgi") bzclass = RHBugzilla bzversion = (5, 0) test0 = BaseTest._testBZVersion test01 = lambda s: BaseTest._testInfoProducts(s, 125, "Virtualization Tools") test02 = lambda s: BaseTest._testInfoComps(s, "Virtualization Tools", 10, "virtinst") 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) assert len(out.splitlines()) > 15 assert len(out.splitlines()) < 28 assert "223805" in out def testQueryFixedIn(self): out = self.clicomm("query --fixed_in anaconda-15.29-1") assert len(out.splitlines()) == 6 assert "#629311 CLOSED" in out def testComponentsDetails(self): """ Fresh call to getcomponentsdetails should properly refresh """ bz = self.bzclass(url=self.url, use_creds=False) assert 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") assert bug.bug_id == 720773 def testQuerySubComponent(self): out = self.clicomm("query --product 'Red Hat Enterprise Linux 7' " "--component lvm2 --sub-component 'Thin Provisioning'") assert len(out.splitlines()) >= 5 assert "#1060931 " in out def testBugFields(self): bz = self.bzclass(url=self.url, use_creds=False) fields1 = bz.getbugfields()[:] fields2 = bz.getbugfields(force_refresh=True)[:] assert bool([f for f in fields1 if f.startswith("attachments")]) assert 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] assert hasattr(bug, "component") assert bool(bug.component) bz.bug_autorefresh = False bug = bz.query(bz.build_query(bug_id=720773, include_fields=["summary"]))[0] assert not hasattr(bug, "component") try: assert bool(bug.component) except Exception as e: assert "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"]) assert "comments" in dir(bug) assert "product" not in dir(bug) # Ensure that include_fields overrides default extra_fields bug = bz.getbug(720773, include_fields=["summary"]) assert "summary" in dir(bug) assert "comments" not in dir(bug) def testExternalBugsOutput(self): out = self.clicomm('query --bug_id 989253 ' '--outputformat="%{external_bugs}"') assert "bugzilla.gnome.org/show_bug.cgi?id=703421" in out assert "External bug: https://bugs.launchpad.net/bugs/1203576" in out def testActiveComps(self): out = self.clicomm("info --components 'Virtualization Tools' " "--active-components") assert "virtinst" not in out out = self.clicomm("info --component_owners 'Virtualization Tools' " "--active-components") assert "virtinst" not in out def testFaults(self): # Test special error wrappers in bugzilla/_cli.py bzinstance = Bugzilla(self.url, use_creds=False) out = tests.clicomm("bugzilla query --field=IDONTEXIST=FOO", bzinstance, expectfail=True) assert "Server error:" in out out = tests.clicomm("bugzilla " "--bugzilla https://example.com/xmlrpc.cgi " "query --field=IDONTEXIST=FOO", None, expectfail=True) assert "Connection lost/failed" in out out = tests.clicomm("bugzilla " "--bugzilla https://expired.badssl.com/ " "query --bug_id 1234", None, expectfail=True) assert "trust the remote server" in out assert "--nosslverify" in out def testCertFail(self): # No public setup that I know of to test cert succeeds, so # let's give it a bogus file and ensure it fails badcert = os.path.join(os.path.dirname(__file__), "..", "README.md") out = tests.clicomm( "bugzilla --cert %s query --bug_id 123456" % badcert, None, expectfail=True) assert "PEM" in out python-bugzilla-2.3.0/tests/test_rw_functional.py0000664000175000017500000007726613415450132023771 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 # pylint: disable=import-error if sys.version_info[0] >= 3: from io import StringIO else: from StringIO import StringIO # pylint: enable=import-error import pytest import bugzilla from bugzilla import Bugzilla import tests RHURL = tests.CLICONFIG.REDHAT_URL or "partner-bugzilla.redhat.com" def _split_int(s): return [int(i) for i in s.split(",")] if not bugzilla.RHBugzilla(url=RHURL).logged_in: print("R/W tests require cached login credentials for url=%s" % RHURL) sys.exit(1) class RHPartnerTest(unittest.TestCase): # 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 = RHURL 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 def test0LoggedInNoCreds(self): bz = self.bzclass(url=self.url, use_creds=False) assert not bz.logged_in def test2(self): bz = Bugzilla(url=self.url, use_creds=False) assert bz.__class__ is self.bzclass def _makebug(self, bz): 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) assert len(newout.splitlines()) == 3 bugid = int(newout.splitlines()[2]) bug = bz.getbug(bugid) print("\nCreated bugid: %s" % bug.id) assert bug.component == component assert bug.version == version assert bug.summary == summary return bug def test03NewBugBasic(self): """ Create a bug with minimal amount of fields, then close it """ bz = self.bzclass(url=self.url) bug = self._makebug(bz) # Verify hasattr works assert hasattr(bug, "id") assert hasattr(bug, "bug_id") # Close the bug tests.clicomm("bugzilla modify --close NOTABUG %s" % bug.id, bz) bug.refresh() assert bug.status == "CLOSED" assert 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) assert len(newout.splitlines()) == 3 bugid = int(newout.splitlines()[2]) bug = bz.getbug(bugid, extra_fields=["sub_components"]) print("\nCreated bugid: %s" % bugid) assert bug.summary == summary assert bug.bug_file_loc == url assert bug.op_sys == osval assert bug.blocks == _split_int(blocked) assert bug.depends_on == _split_int(dependson) assert all([e in bug.cc for e in cc.split(",")]) assert bug.longdescs[0]["text"] == comment assert bug.sub_components == {"lvm2": [sub_component]} assert 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() assert bug.status == "CLOSED" assert bug.resolution == "WONTFIX" assert bug.alias == [alias] # Check bug's minimal history ret = bug.get_history_raw() assert len(ret["bugs"]) == 1 assert 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() assert 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() assert bug.status == status assert bug.longdescs[-1]["is_private"] == 1 assert 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() assert bug.status == "CLOSED" assert bug.resolution == resolution assert bug.comments[-1]["is_private"] == 1 assert 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() assert bug.dupe_of == int(dupeid) assert len(bug.longdescs) == (desclen + 1) assert "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() assert bug.longdescs[-1]["is_private"] == 1 assert bug.longdescs[-1]["text"] == comment assert bug.status == "POST" # bz.close test fixed_in = str(datetime.datetime.today()) bug.close("ERRATA", fixedin=fixed_in) bug.refresh() assert bug.status == "CLOSED" assert bug.resolution == "ERRATA" assert bug.fixed_in == fixed_in # bz.addcomment test comment = ("yet another test comment %s" % datetime.datetime.today()) bug.addcomment(comment, private=False) bug.refresh() assert bug.longdescs[-1]["text"] == comment assert bug.longdescs[-1]["is_private"] == 0 # Confirm comments is same as getcomments assert bug.comments == bug.getcomments() # Reset state tests.clicomm(cmd + "--status %s" % origstatus, bz) bug.refresh() assert 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() assert email1 in bug.cc assert email2 in bug.cc assert len(bug.cc) == 2 tests.clicomm(cmd + "--cc=-%s" % email1, bz) bug.refresh() assert email1 not in bug.cc # Test assigned target tests.clicomm(cmd + "--assignee %s" % email1, bz) bug.refresh() assert bug.assigned_to == email1 # Test QA target tests.clicomm(cmd + "--qa_contact %s" % email1, bz) bug.refresh() assert bug.qa_contact == email1 # Reset values bug.deletecc(bug.cc) tests.clicomm(cmd + "--reset-qa-contact --reset-assignee", bz) bug.refresh() assert bug.cc == [] assert bug.assigned_to == "crobinso@redhat.com" assert 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() assert flagstr(bug1) == setflags assert flagstr(bug2) == setflags assert bug1.get_flags("needinfo")[0]["status"] == "?" assert 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() assert cleardict_old(bug1) == {} assert 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() assert bug1.fixed_in == newfix assert bug2.fixed_in == newfix # Reset fixed_in tests.clicomm(cmd + "--fixed_in=\"-\"", bz) bug1.refresh() bug2.refresh() assert bug1.fixed_in == "-" assert 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() assert 123456 in bug.depends_on tests.clicomm(cmd + "--dependson =111222", bz) bug.refresh() assert [111222] == bug.depends_on tests.clicomm(cmd + "--dependson=-111222", bz) bug.refresh() assert [] == bug.depends_on # modify --blocked tests.clicomm(cmd + "--blocked 123,456", bz) bug.refresh() assert [123, 456] == bug.blocks tests.clicomm(cmd + "--blocked =", bz) bug.refresh() assert [] == bug.blocks # modify --keywords tests.clicomm(cmd + "--keywords +Documentation --keywords EasyFix", bz) bug.refresh() assert ["Documentation", "EasyFix"] == bug.keywords tests.clicomm(cmd + "--keywords=-EasyFix --keywords=-Documentation", bz) bug.refresh() assert [] == 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() assert targetbug.target_milestone == "beta" assert targetbug.target_release == ["6.2"] tests.clicomm(targetcmd + "--target_milestone rc --target_release 6.10", bz) targetbug.refresh() assert targetbug.target_milestone == "rc" assert targetbug.target_release == ["6.10"] # modify --priority # modify --severity tests.clicomm(cmd + "--priority low --severity high", bz) bug.refresh() assert bug.priority == "low" assert bug.severity == "high" tests.clicomm(cmd + "--priority medium --severity medium", bz) bug.refresh() assert bug.priority == "medium" assert 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() assert bug.version == "rawhide" assert bug.op_sys == "Windows" assert bug.platform == "ppc" assert bug.url == "http://example.com" tests.clicomm(cmd + "--version rawhide --os Linux --arch s390 " "--url http://example.com/fribby", bz) bug.refresh() assert bug.version == "rawhide" assert bug.op_sys == "Linux" assert bug.platform == "s390" assert 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() assert bug.fixed_in == "foo-bar-1.2.3" assert 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) cmd = "bugzilla attach " testfile = "../tests/data/bz-attach-get1.txt" # Add attachment as CLI option setbug = self._makebug(bz) setbug = bz.getbug(setbug.id, 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" % (setbug.id, desc1, testfile), bz, stdin=open("/dev/tty", "rb")) desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() out2 = tests.clicomm(cmd + "%s --file test --summary \"%s\"" % (setbug.id, desc2), bz, stdin=open(testfile)) # Expected output format: # Created attachment on bug setbug.refresh() assert len(setbug.attachments) == (orignumattach + 2) att1 = setbug.attachments[-2] attachid = att1["id"] assert att1["summary"] == desc1 assert att1["id"] == int(out1.splitlines()[2].split()[2]) assert att1["content_type"] == "text/plain" att2 = setbug.attachments[-1] assert att2["summary"] == desc2 assert att2["id"] == int(out2.splitlines()[2].split()[2]) assert att2["content_type"] == "application/octet-stream" # Set attachment flags assert att1["flags"] == [] bz.updateattachmentflags(setbug.id, att2["id"], "review", status="+") setbug.refresh() assert len(setbug.attachments[-1]["flags"]) == 1 assert setbug.attachments[-1]["flags"][0]["name"] == "review" assert setbug.attachments[-1]["flags"][0]["status"] == "+" bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], "review", status="X") setbug.refresh() assert 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() assert len(out) == 3 assert fname == "bz-attach-get1.txt" assert open(fname).read() == open(testfile).read() os.unlink(fname) # Get all attachments getbug = bz.getbug(setbug.id) getbug.autorefresh = True numattach = len(getbug.attachments) out = tests.clicomm(cmd + "--getall %s" % getbug.id, bz).splitlines() assert len(out) == (numattach + 2) fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] assert 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() assert bug.whiteboard == (initval + "status") assert bug.qa_whiteboard == (initval + "qa") assert bug.devel_whiteboard == (initval + "devel") assert (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() assert bug.qa_whiteboard == (initval + "qa" + " _app") assert bug.devel_whiteboard == ("pre-" + initval + "devel") assert bug.status_whiteboard == "foobar" # Verify that tag manipulation is smart about separator tests.clicomm(cmd + "--qa_whiteboard=-_app " "--internal_whiteboard=-security,", bz) bug.refresh() assert bug.qa_whiteboard == (initval + "qa") assert 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() assert bug.whiteboard == "" assert bug.qa_whiteboard == "" assert bug.devel_whiteboard == "" assert 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: cmd = "bugzilla --no-cache-credentials --bugzilla %s" % self.url # Implied login with --username and --password ret = tests.clicomm("%s --user foobar@example.com " "--password foobar query -b 123456" % cmd, None, expectfail=True) assert "Login failed: " in ret # 'login' with explicit options ret = tests.clicomm("%s --user foobar@example.com " "--password foobar login" % cmd, None, expectfail=True) assert "Login failed: " in ret # 'login' with positional options ret = tests.clicomm("%s login foobar@example.com foobar" % cmd, None, expectfail=True) assert "Login failed: " in ret # bare 'login' stdinstr = StringIO("foobar@example.com\n\rfoobar\n\r") ret = tests.clicomm("%s login" % cmd, None, expectfail=True, stdin=stdinstr) assert "Bugzilla Username:" in ret assert "Bugzilla Password:" in ret assert "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: assert group in user.groupnames origgroups = user.groupnames # Remove the group try: bz.updateperms(email, "remove", [group]) user.refresh() assert group not in user.groupnames except Exception as e: if have_admin: raise assert "Sorry, you aren't a member" in str(e) # Re add it try: bz.updateperms(email, "add", group) user.refresh() assert group in user.groupnames except Exception as e: if have_admin: raise assert "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() assert group not in user.groupnames except Exception as e: if have_admin: raise assert "Sorry, you aren't a member" in str(e) # Reset everything try: bz.updateperms(email, "set", origgroups) except Exception as e: if have_admin: raise assert "Sorry, you aren't a member" in str(e) user.refresh() assert 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 assert bool(compdata) assert data["component"] == compdata["name"] assert data["description"] == compdata["description"] assert data["initialowner"] == compdata["default_assigned_to"] assert data["initialqacontact"] == compdata["default_qa_contact"] assert 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, }) newid = None try: newid = bz.addcomponent(data)['id'] print("Created product=%s component=%s" % ( basedata["product"], basedata["component"])) compare(data, newid) except Exception as e: if have_admin: raise assert (("Sorry, you aren't a member" in str(e)) or # bugzilla 5 error string ("You are not allowed" 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) if newid is not None: compare(data, newid) except Exception as e: if have_admin: raise assert (("Sorry, you aren't a member" in str(e)) or # bugzilla 5 error string ("You are not allowed" in str(e))) def test12SetCookie(self): bz = self.bzclass(self.url, cookiefile=-1, tokenfile=None, configpaths=[]) try: bz.cookiefile = None raise AssertionError("Setting cookiefile for active connection " "should fail.") except RuntimeError as e: assert "disconnect()" in str(e) bz.disconnect() bz.cookiefile = None bz.connect() assert not 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 assert bug.component == "lvm2" bz.update_bugs(bug.id, bz.build_update( component="lvm2", sub_component="Command-line tools (RHEL5)")) bug.refresh() assert bug.sub_components == {"lvm2": ["Command-line tools (RHEL5)"]} bz.update_bugs(bug.id, bz.build_update(sub_component={})) bug.refresh() assert bug.sub_components == {} def test13ExternalTrackerQuery(self): bz = self.bzclass(url=self.url) with pytest.raises(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() assert bug.tags == [] tests.clicomm(cmd + "--tags foo --tags +bar --tags baz", bz) bug.refresh() assert bug.tags, ["foo", "bar" == "baz"] tests.clicomm(cmd + "--tags=-bar", bz) bug.refresh() assert bug.tags, ["foo" == "baz"] bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() assert bug.tags == [] def test17LoginAPIKey(self): api_key = "somefakeapikey1234" bz = self.bzclass(url=self.url, use_creds=False, api_key=api_key) if bz.bz_ver_major < 5: self.skipTest("can only test apikey on bugzilla 5+") try: assert bz.logged_in is False # Use this to trigger a warning about api_key bz.createbug(bz.build_createbug()) except Exception as e: assert "The API key you specified is invalid" in str(e) python-bugzilla-2.3.0/xmlrpc-api-notes.txt0000664000175000017500000000735413415440373022304 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://bugzilla.readthedocs.io/en/5.0/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: 5.0 based with extensions 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 Fedora infrastructure python-bugzilla consumers: https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/distgit/pagure/templates/pagure-sync-bugzilla.py.j2 https://github.com/fedora-infra/bodhi/blob/develop/bodhi/server/bugs.py https://github.com/fedora-infra/fas/blob/develop/tools/export-bugzilla.py