Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

a very slow bzattach using the rest api

  • Loading branch information...
commit 8c488a26f1eca8ba2c17cf47e7f452a288c3519a 0 parents
@jbalogh jbalogh authored
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009, Jeff Balogh.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of bztools nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2  MANIFEST.in
@@ -0,0 +1,2 @@
+include LICENSE
+include README.rst
39 README.rst
@@ -0,0 +1,39 @@
+This package defines `remoteobjects`_ models and some scripts for all the
+resources provided in `Gervase Markham's`_ Bugzilla `REST API`_. Right now it's
+pretty damn slow. I hope that will change.
+
+.. _remoteobjects: http://sixapart.github.com/remoteobjects/
+.. _Gervase Markham's: http://weblogs.mozillazine.org/gerv/
+.. _REST API: https://wiki.mozilla.org/Bugzilla:REST_API
+
+
+Installation
+------------
+
+Currently, this package depends on a pre-release version of remoteobjects, so
+we'll have to do this the long way.
+
+#. Check out the code::
+
+ git clone git://github.com/jbalogh/bztools.git
+
+#. Create your virtualenv using virtualenvwrapper::
+
+ mkvirtualenv --no-site-packages bztools
+
+#. Install pip::
+
+ easy_install pip
+
+#. Install the dependencies for bztools::
+
+ pip install -r requirements.txt
+
+#. Run setup.py so the scripts are installed to your bin directory::
+
+ python setup.py install
+
+
+Now you'll have ``bzattach`` installed in the ``/bin`` directory of your
+virtual environment. To use the script, you'll have to activate this
+environment with ``workon bztools``.
11 bugzilla/__init__.py
@@ -0,0 +1,11 @@
+import httplib
+
+from remoteobjects import http
+
+
+# Monkey patch remoteobjects to accept 202 status codes.
+http.HttpObject.response_has_content[httplib.ACCEPTED] = False
+
+
+VERSION = (0, 0, 1)
+__version__ = '.'.join(map(str, VERSION))
23 bugzilla/fields.py
@@ -0,0 +1,23 @@
+from datetime import datetime
+
+from remoteobjects import fields
+import dateutil.parser
+
+
+class StringBoolean(fields.Field):
+ """Decodes a boolean hidden in a string."""
+
+ def decode(self, value):
+ return bool(int(value))
+
+
+class Datetime(fields.Datetime):
+ """Uses python-dateutil for working with datetimes."""
+
+ def decode(self, value):
+ return dateutil.parser.parse(value)
+
+ def encode(self, value):
+ if not isinstance(value, datetime):
+ raise TypeError('Value to encode %r is not a datetime' % (value,))
+ return value.replace(microsecond=0).strftime(self.dateformat)
174 bugzilla/models.py
@@ -0,0 +1,174 @@
+from remoteobjects import RemoteObject as RemoteObject_, fields
+
+from .fields import StringBoolean, Datetime
+
+
+# The datetime format is inconsistent.
+DATETIME_FORMAT_WITH_SECONDS = '%Y-%m-%d %H:%M:%S %z'
+DATETIME_FORMAT = '%Y-%m-%d %H:%M %Z'
+
+
+class RemoteObject(RemoteObject_):
+
+ def post_to(self, url):
+ self._location = url
+ self.post(self)
+ return self.api_data['ref']
+
+ def _get_location(self):
+ if self.__location is not None:
+ return self.__location
+ else:
+ return self.api_data.get('ref', None)
+
+ def _set_location(self, url):
+ self.__location = url
+
+ _location = property(_get_location, _set_location)
+
+
+class Bug(RemoteObject):
+
+ id = fields.Field()
+ summary = fields.Field()
+ assigned_to = fields.Object('User')
+ reporter = fields.Object('User')
+ target_milestone = fields.Field()
+ attachments = fields.List(fields.Object('Attachment'))
+ comments = fields.List(fields.Object('Comment'))
+ history = fields.List(fields.Object('Changeset'))
+ status = fields.Field()
+ resolution = fields.Field()
+
+ creation_time = Datetime(DATETIME_FORMAT_WITH_SECONDS)
+ flags = fields.List(fields.Object('Flag'))
+ blocks = fields.List(fields.Field())
+ depends_on = fields.List(fields.Field())
+ url = fields.Field()
+ cc = fields.List(fields.Object('User'))
+ keywords = fields.List(fields.Field())
+ whiteboard = fields.Field()
+
+ op_sys = fields.Field()
+ platform = fields.Field()
+ priority = fields.Field()
+ product = fields.Field()
+ qa_contact = fields.Object('User')
+ severity = fields.Field()
+ see_also = fields.List(fields.Field())
+ version = fields.Field()
+
+ alias = fields.Field()
+ classification = fields.Field()
+ component = fields.Field()
+ is_cc_accessible = StringBoolean()
+ is_everconfirmed = StringBoolean()
+ is_reporter_accessible = StringBoolean()
+ last_change_time = Datetime(DATETIME_FORMAT_WITH_SECONDS)
+ ref = fields.Field()
+
+ # Needed for submitting changes.
+ token = fields.Field()
+
+ # Time tracking.
+ actual_time = fields.Field()
+ deadline = Datetime(DATETIME_FORMAT_WITH_SECONDS)
+ estimated_time = fields.Field()
+ # groups = fields.Field() # unimplemented
+ percentage_complete = fields.Field()
+ remaining_time = fields.Field()
+ work_time = fields.Field()
+
+ def __repr__(self):
+ return '<Bug %s: "%s">' % (self.id, self.summary)
+
+
+class User(RemoteObject):
+
+ name = fields.Field()
+ real_name = fields.Field()
+ ref = fields.Field()
+
+ def __repr__(self):
+ return '<User "%s">' % self.real_name
+
+
+class Attachment(RemoteObject):
+
+ # Attachment data.
+ id = fields.Field()
+ attacher = fields.Object('User')
+ creation_time = Datetime(DATETIME_FORMAT_WITH_SECONDS)
+ description = fields.Field()
+ bug_id = fields.Field()
+ bug_ref = fields.Field()
+
+ # File data.
+ file_name = fields.Field()
+ size = fields.Field()
+ content_type = fields.Field()
+
+ # Attachment metadata.
+ flags = fields.List(fields.Object('Flag'))
+ is_obsolete = StringBoolean()
+ is_private = StringBoolean()
+ is_patch = StringBoolean()
+
+ # Used for submitting changes.
+ token = fields.Field()
+ ref = fields.Field()
+
+ # Only with attachmentdata=1
+ data = fields.Field()
+ encoding = fields.Field()
+
+ def __repr__(self):
+ return '<Attachment %s: "%s">' % (self.id, self.description)
+
+
+class Comment(RemoteObject):
+
+ id = fields.Field()
+ author = fields.Object('User')
+ creation_time = Datetime(DATETIME_FORMAT_WITH_SECONDS)
+ text = fields.Field()
+ is_private = StringBoolean()
+
+ def __repr__(self):
+ return '<Comment by %s on %s>' % (
+ self.author, self.creation_time.strftime(DATETIME_FORMAT))
+
+
+class Change(RemoteObject):
+
+ field_name = fields.Field()
+ added = fields.Field()
+ removed = fields.Field()
+
+ def __repr__(self):
+ return '<Change "%s": "%s" -> "%s">' % (self.field_name, self.removed,
+ self.added)
+
+
+class Changeset(RemoteObject):
+
+ changer = fields.Object('User')
+ changes = fields.List(fields.Object('Change'))
+ change_time = Datetime(DATETIME_FORMAT_WITH_SECONDS)
+
+ def __repr__(self):
+ return '<Changeset by %s on %s>' % (
+ self.changer, self.change_time.strptime(DATETIME_FORMAT))
+
+
+class Flag(RemoteObject):
+
+ id = fields.Field()
+ name = fields.Field()
+ setter = fields.Object('User')
+ status = fields.Field()
+ requestee = fields.Object('User')
+ type_id = fields.Field()
+
+ def __repr__(self):
+ return '<Flag "%s">' % self.name
58 bugzilla/utils.py
@@ -0,0 +1,58 @@
+import base64
+from ConfigParser import ConfigParser
+import getpass
+import os
+import posixpath
+import urllib
+
+
+def urljoin(base, *args):
+ """Remove any leading slashes so no subpaths look absolute."""
+ return posixpath.join(base, *[str(s).lstrip('/') for s in args])
+
+
+def qs(**kwargs):
+ """Build a URL query string."""
+ return '&'.join('%s=%s' % tuple(map(urllib.quote, map(str, pair)))
+ for pair in kwargs.items())
+
+
+def get_credentials():
+ username, password = None, None
+ rcfile = os.path.expanduser('~/.bztoolsrc')
+ config = ConfigParser()
+ config.add_section('bugzilla')
+
+ if os.path.exists(rcfile):
+ try:
+ config.read(rcfile)
+ username = config.get('bugzilla', 'username')
+ _password = config.get('bugzilla', 'password')
+ if _password:
+ password = base64.b64decode(_password)
+ except Exception:
+ pass
+
+ if not (username and password):
+ username = raw_input('Bugzilla username: ')
+ password = getpass.getpass('Bugzilla password: ')
+ config.set('bugzilla', 'username', username)
+ config.set('bugzilla', 'password', base64.b64encode(password))
+
+ with open(rcfile, 'wb') as configfile:
+ config.write(configfile)
+
+ return username, password
+
+
+FILE_TYPES = {
+ 'text': 'text/plain',
+ 'html': 'text/html',
+ 'xml': 'application/xml',
+ 'gif': 'image/gif',
+ 'jpg': 'image/jpeg',
+ 'png': 'image/png',
+ 'svg': 'image/svg+xml',
+ 'binary': 'application/octet-stream',
+ 'xul': 'application/vnd.mozilla.xul+xml',
+}
5 requirements.txt
@@ -0,0 +1,5 @@
+-e git://github.com/sixapart/remoteobjects.git#egg=remoteobjects
+argparse
+python-dateutil
+
+-e git://github.com/jbalogh/check.git#egg=check
0  scripts/__init__.py
No changes.
137 scripts/attach.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+
+import base64
+import itertools
+
+import argparse
+
+from bugzilla.models import Bug, Attachment, Flag, User, Comment
+from bugzilla.utils import urljoin, qs, get_credentials, FILE_TYPES
+
+
+API_ROOT = 'https://api-dev.bugzilla.mozilla.org/0.2/'
+REVIEW = 4
+
+
+class Agent(object):
+ """Stores credentials, navigates the site."""
+
+ def __init__(self, username, password):
+ self.username, self.password = username, password
+
+ def get_bug(self, bug, attachments=True, comments=True, history=True):
+ """Fetch Bug ``bug``."""
+ tmp = {'attachmentdata': attachments, 'comments': comments,
+ 'history': history}
+ params = dict((k, int(v)) for k, v in tmp.items())
+ url = urljoin(API_ROOT, 'bug/%s?%s' % (bug, self.qs(**params)))
+ return Bug.get(url)
+
+ def attach(self, bug_id, filename, description, patch=False,
+ reviewer=None, comment='', content_type='text/plain'):
+ """Create an attachment, add a comment, obsolete other attachments."""
+
+ print 'Adding "%s" to %s' % (filename, bug_id)
+ self._attach(bug_id, filename, description, patch,
+ reviewer, content_type)
+
+ bug = self.get_bug(bug_id)
+
+ if comment:
+ print 'Adding the comment'
+ self._comment(bug_id, comment)
+
+ print 'Finding attachments to make obsolete...'
+ self.obsolete(bug)
+
+ def _attach(self, bug_id, filename, description, is_patch=False,
+ reviewer=None, content_type='text/plain'):
+ """Create a new attachment."""
+ fields = {'data': base64.b64encode(open(filename).read()),
+ 'encoding': 'base64',
+ 'file_name': filename,
+ 'content_type': content_type,
+ 'description': description,
+ 'is_patch': is_patch,
+ }
+
+ if reviewer is not None:
+ fields['flags'] = [Flag(type_id=REVIEW, status='?',
+ requestee=User(name=reviewer))]
+
+ url = urljoin(API_ROOT, 'bug/%s/attachment?%s' % (bug_id, self.qs()))
+ return Attachment(**fields).post_to(url)
+
+ def _comment(self, bug_id, comment):
+ """Create a new comment."""
+ url = urljoin(API_ROOT, 'bug/%s/comment?%s' % (bug_id, self.qs()))
+ return Comment(text=comment).post_to(url)
+
+ def obsolete(self, bug):
+ """Ask what attachments should be obsoleted."""
+ attachments = [a for a in bug.attachments
+ if not bool(int(a.is_obsolete))]
+
+ if not attachments:
+ return
+
+ print "What attachments do you want to obsolete?"
+ msg = '[{index}] {a.id}: "{a.description}" ({a.file_name})'
+ for index, a in enumerate(attachments):
+ print msg.format(index=index, a=a)
+
+ numbers = raw_input('Enter the numbers (space-separated) of '
+ 'attachments to make obsolete:\n').split()
+
+ if not numbers:
+ return
+
+ map_ = dict((str(index), a) for index, a in enumerate(attachments))
+ for num, _ in itertools.groupby(sorted(numbers)):
+ try:
+ self._obsolete(map_[num])
+ except KeyError:
+ pass
+
+ def _obsolete(self, attachment):
+ """Mark an attachment obsolete."""
+ print "Obsoleting", attachment
+ attachment.is_obsolete = True
+ attachment._location += '?%s' % self.qs()
+ attachment.put()
+
+ def qs(self, **params):
+ if self.username and self.password:
+ params['username'] = self.username
+ params['password'] = self.password
+ return qs(**params)
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description='Submit Bugzilla attachments')
+ parser.add_argument('bug_id', type=int, metavar='BUG', help='Bug number')
+ parser.add_argument('filename', metavar='FILE', help='File to upload')
+
+ parser.add_argument('--description', help='Attachment description',
+ required=True)
+ parser.add_argument('--patch', action='store_true',
+ help='Is this a patch?')
+ parser.add_argument('--reviewer', help='Bugzilla name of someone to r?')
+ parser.add_argument('--comment', help='Comment for the attachment')
+
+ parser.add_argument('--content_type', choices=FILE_TYPES,
+ help="File's content_type")
+
+ args = parser.parse_args()
+
+ if args.content_type:
+ args.content_type = FILE_TYPES[args.content_type]
+
+ username, password = get_credentials()
+
+ Agent(username, password).attach(**dict(args._get_kwargs()))
+
+
+if __name__ == '__main__':
+ main()
36 setup.py
@@ -0,0 +1,36 @@
+import os
+
+from setuptools import setup, find_packages
+
+
+root = os.path.abspath(os.path.dirname(__file__))
+path = lambda *p: os.path.join(root, *p)
+
+
+setup(
+ name='bztools',
+ version=__import__('bugzilla').__version__,
+ description='Models and scripts to access the Bugzilla REST API.',
+ long_description=open(path('README.rst')).read(),
+ author='Jeff Balogh',
+ author_email='me@jeffbalogh.org',
+ url='http://github.com/jbalogh/bztools',
+ license='BSD',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ # install_requires=['remoteobjects>=1.1'],
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+ entry_points={
+ 'console_scripts': [
+ 'bzattach = scripts.attach:main',
+ ],
+ },
+)
Please sign in to comment.
Something went wrong with that request. Please try again.