# -*- coding: utf-8 -*-
# Copyright (C) Brian Moe, Branson Stephens (2015)
#
# This file is part of gracedb
#
# gracedb 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 3 of the License, or
# (at your option) any later version.
#
# It 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 gracedb.  If not, see <http://www.gnu.org/licenses/>.

import six.moves.http_client, socket, ssl
import mimetypes
import urllib
import os
import sys
if os.name == 'posix':
    import pwd
import json
from six.moves.urllib.parse import urlparse, urlencode
from base64 import b64encode
import six
from six.moves import map

from .exceptions import HTTPError
from .utils import event_or_superevent, handle_str_or_list_arg, safe_netrc, \
    cleanListInput, get_dt_from_openssl_output, is_expired


DEFAULT_SERVICE_URL = "https://gracedb.ligo.org/api/"
DEFAULT_BASIC_SERVICE_URL = "https://gracedb.ligo.org/apibasic/"
KNOWN_TEST_HOSTS = ['moe.phys.uwm.edu', 'embb-dev.ligo.caltech.edu', 'simdb.phys.uwm.edu',]

#---------------------------------------------------------------------
# This monkey patch forces TLSv1 if the python version is 2.6.6.
# It was introduced because clients connection from CIT *occasionally*
# try to use SSLv3.  See:
# http://stackoverflow.com/questions/18669457/python-httplib-ssl23-get-server-hellounknown-protocol
#---------------------------------------------------------------------
if sys.hexversion < 0x20709f0:
    wrap_socket_orig = ssl.wrap_socket
    def wrap_socket_patched(sock, keyfile=None, certfile=None,
                            server_side=False, cert_reqs=ssl.CERT_NONE,
                            ssl_version=ssl.PROTOCOL_TLSv1, ca_certs=None,
                            do_handshake_on_connect=True,
                            suppress_ragged_eofs=True):
        return wrap_socket_orig(sock, keyfile, certfile, server_side,
                                cert_reqs, ssl_version, ca_certs,
                                do_handshake_on_connect,
                                suppress_ragged_eofs)
    ssl.wrap_socket = wrap_socket_patched

#-----------------------------------------------------------------
# HTTP/S Proxy classes
# Taken from: http://code.activestate.com/recipes/456195/

class ProxyHTTPConnection(six.moves.http_client.HTTPConnection):

    _ports = {'http' : 80, 'https' : 443}

    def request(self, method, url, body=None, headers={}):
        #request is called before connect, so can interpret url and get
        #real host/port to be used to make CONNECT request to proxy
        o = urlparse(url)
        proto = o.scheme
        port = o.port
        host = o.hostname
        if proto is None:
            raise ValueError("unknown URL type: %s" % url)
        if port is None:
            try:
                port = self._ports[proto]
            except KeyError:
                raise ValueError("unknown protocol for: %s" % url)
        self._real_host = host
        self._real_port = port
        six.moves.http_client.HTTPConnection.request(self, method, url, body, headers)


    def connect(self):
        six.moves.http_client.HTTPConnection.connect(self)
        #send proxy CONNECT request
        self.send("CONNECT %s:%d HTTP/1.0\r\n\r\n" % (self._real_host, self._real_port))
        #expect a HTTP/1.0 200 Connection established
        response = self.response_class(self.sock, strict=self.strict, method=self._method)
        (version, code, message) = response._read_status()
        #probably here we can handle auth requests...
        if code != 200:
            #proxy returned and error, abort connection, and raise exception
            self.close()
            raise socket.error("Proxy connection failed: %d %s" % (code, message.strip()))
        #eat up header block from proxy....
        while True:
            #should not use directly fp probably
            line = response.fp.readline()
            if line == '\r\n': break

class ProxyHTTPSConnection(ProxyHTTPConnection):

    default_port = 443

    def __init__(self, host, port = None, key_file = None, cert_file = None,
        strict = None, context = None):
        ProxyHTTPConnection.__init__(self, host, port)
        self.key_file = key_file
        self.cert_file = cert_file
        self.context = context

    def connect(self):
        ProxyHTTPConnection.connect(self)
        #make the sock ssl-aware
        if sys.hexversion < 0x20709f0:
            ssl = socket.ssl(self.sock, self.key_file, self.cert_file)
            self.sock = six.moves.http_client.FakeSocket(self.sock, ssl)
        else:
            self.sock = self.context.wrap_socket(self.sock)

#-----------------------------------------------------------------
# Generic GSI REST

class GsiRest(object):
    """docstring for GracedbRest"""
    def __init__(self,
            url=DEFAULT_SERVICE_URL,
            proxy_host=None,
            proxy_port=3128,
            cred=None):
        if not cred:
            cred = findUserCredentials()
        if not cred:
            msg = "\nERROR: No certificate (or proxy) found. \n\n"
            msg += "Please run ligo-proxy-init or grid-proxy-init (as appropriate) "
            msg += "to generate one.\n\n"
            self.output_and_die(msg)
        if isinstance(cred, (list, tuple)):
            self.cert, self.key = cred
        elif cred:
            self.cert = self.key = cred

        o = urlparse(url)
        port = o.port
        host = o.hostname
        port = port or 443

        # Versions of Python earlier than 2.7.9 don't use SSL Context
        # objects for this purpose, and do not do any server cert verification.
        ssl_context = None
        if sys.hexversion >= 0x20709f0:
            # Use the new method with SSL Context
            # Prepare SSL context
            ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
            try:
                ssl_context.load_cert_chain(self.cert, self.key)
            except ssl.SSLError as e:
                msg = "\nERROR: Unable to load cert/key pair. \n\n"
                msg += "Please run ligo-proxy-init or grid-proxy-init again "
                msg += "or make sure your robot certificate is readable.\n\n"
                self.output_and_die(msg)
            # Generally speaking, test boxes use cheap/free certs from the LIGO CA.
            # These cannot be verified by the client.
            if host in KNOWN_TEST_HOSTS:
                ssl_context.verify_mode = ssl.CERT_NONE
            else:
                ssl_context.verify_mode = ssl.CERT_REQUIRED
                ssl_context.check_hostname = True
                # Find the various CA cert bundles stored on the system
                ssl_context.load_default_certs()

            if proxy_host:
                self.connector = lambda: ProxyHTTPSConnection(proxy_host, proxy_port, context=ssl_context)
            else:
                self.connector = lambda: six.moves.http_client.HTTPSConnection(host, port, context=ssl_context)
        else:
            # Using and older version of python. We'll pass in the cert and key files.
            if proxy_host:
                self.connector = lambda: ProxyHTTPSConnection(proxy_host, proxy_port,
                    key_file=self.key, cert_file=self.cert)
            else:
                self.connector = lambda: six.moves.http_client.HTTPSConnection(host, port,
                    key_file=self.key, cert_file=self.cert)


    def getConnection(self):
        return self.connector()

    # When there is a problem with the SSL connection or cert authentication,
    # either conn.request() or conn.getresponse() will throw an exception.
    # The following two wrappers are intended to catch these exceptions and
    # return an intelligible error message to the user.
    # A wrapper for getting the response:
    def get_response(self, conn):
        try:
            return conn.getresponse()
        except ssl.SSLError as e:
            # Check for a valid user proxy cert.
            expired, error = is_expired(self.cert)
            if expired is not None:
                if expired:
                    msg = "\nERROR: Your certificate or proxy has expired. \n\n"
                    msg += "Please run ligo-proxy-init or grid-proxy-init (as appropriate) "
                    msg += "to generate a fresh one.\n\n"
                else:
                    msg = "\nERROR \n\n"
                    msg += "Your certificate appears valid, but there was a problem "
                    msg += "establishing a secure connection: \n\n"
                    msg += "%s \n\n" % str(e)
            else:
                msg = "\nERROR \n\n"
                msg += "Unable to check certificate expiry date: %s \n\n" %error
                msg += "Problem establishing secure connection: %s \n\n" % str(e)
            self.output_and_die(msg)

    # A wrapper for making the request.
    def make_request(self, conn, *args, **kwargs):
        try:
            conn.request(*args, **kwargs)
        except ssl.SSLError as e:
            msg = "\nERROR \n\n"
            msg += "Problem establishing secure connection: %s \n\n" % str(e)
            self.output_and_die(msg)

    def request(self, method, url, body=None, headers=None, priming_url=None):
        # Bug in Python (versions < 2.7.1 (?))
        # http://bugs.python.org/issue11898
        # if the URL is unicode and the body of a request is binary,
        # the POST/PUT action fails because it tries to concatenate
        # the two which fails due to encoding problems.
        # Workaround is to cast all URLs to str.
        # This is probably bad in general,
        # but for our purposes, today, this will do.
        url = url and str(url)
        priming_url = priming_url and str(priming_url)

        conn = self.getConnection()
        if priming_url:
            self.make_request(conn, "GET", priming_url, headers={'connection' : 'keep-alive'})
            response = self.get_response(conn)
            if response.status != 200:
                response = self.adjustResponse(response)
            else:
                # Throw away the response and make sure to read the body.
                response = response.read()
        self.make_request(conn, method, url, body, headers or {})
        response = self.get_response(conn)
        return self.adjustResponse(response)

    def adjustResponse(self, response):
#       XXX WRONG.
        if response.status >= 400:
            response_content = response.read()
            if response.getheader('x-throttle-wait-seconds', None):
                try:
                    rdict = json.loads(response_content)
                    rdict['retry-after'] = response.getheader('x-throttle-wait-seconds')
                    response_content = json.dumps(rdict)
                except:
                    pass
            raise HTTPError(response.status, response.reason, response_content)
        response.json = lambda: self.load_json_or_die(response)
        return response

    def get(self, url, headers=None):
        return self.request("GET", url, headers=headers)

    def head(self, url, headers=None):
        return self.request("HEAD", url, headers=headers)

    def delete(self, url, headers=None):
        return self.request("DELETE", url, headers=headers)

    def options(self, url, headers=None):
        return self.request("OPTIONS", url, headers=headers)

    def post(self, *args, **kwargs):
        return self.post_or_put_or_patch("POST", *args, **kwargs)

    def put(self, *args, **kwargs):
        return self.post_or_put_or_patch("PUT", *args, **kwargs)

    def patch(self, *args, **kwargs):
        return self.post_or_put_or_patch("PATCH", *args, **kwargs)

    def post_or_put_or_patch(self, method, url, body=None, headers=None,
        files=None):
        headers = headers or {}
        if not files:
            # Simple urlencoded body
            if isinstance(body, dict):
            # XXX What about the headers in the params?
                if 'content-type' not in headers:
                    headers['content-type'] = "application/json"
                body = json.dumps(body)
        else:
            body = body or {}
            if isinstance(body, dict):
                body = list(body.items())
            content_type, body = encode_multipart_formdata(body, files)
            # XXX What about the headers in the params?
            headers = {
                'content-type': content_type,
                'content-length': str(len(body)),
                #'connection': 'keep-alive',
            }
        return self.request(method, url, body, headers)

    # A utility for writing out an error message to the user and then stopping
    # execution. This seems to behave sensibly in both the interpreter and in
    # a script.
    @classmethod
    def output_and_die(cls, msg):
        raise RuntimeError(msg)

    # Given an HTTPResponse object, try to read its content and interpret as
    # JSON--or die trying.
    @classmethod
    def load_json_or_die(cls, response):

        # First check that the response object actually exists.
        if not response:
            raise ValueError("No response object")

        # Next, try to read the content of the response.
        response_content = response.read()
        response_content = response_content.decode('utf-8')
        if not response_content:
            response_content = '{}'

        # Finally, try to create a dict by decoding the response as JSON.
        rdict = None
        try:
            rdict = json.loads(response_content)
        except ValueError:
            msg = "ERROR: got unexpected content from the server:\n"
            msg += response_content
            raise ValueError(msg)

        return rdict

#------------------------------------------------------------------
# GraceDB
#
# Example Gracedb REST client.

class GraceDb(GsiRest):
    """Example GraceDb REST client

    The GraceDB service URL may be passed to the constructor
    if an alternate GraceDb instance is desired:

        >>> g = GraceDb("https://alternate.gracedb.edu/api/")
        >>> r = g.ping()

    The proxy_host and proxy_port may also be passed in if accessing
    GraceDB behind a proxy. For other kwargs accepted by the constructor,
    consult the source code.
    """
    def __init__(self, service_url=DEFAULT_SERVICE_URL,
            proxy_host=None, proxy_port=3128, *args, **kwargs):
        GsiRest.__init__(self, service_url, proxy_host, proxy_port, *args, **kwargs)

        self.service_url = service_url
        self._service_info = None

    @property
    def service_info(self):
        if not self._service_info:
            r = self.request("GET", self.service_url)
            self._service_info = r.json()
        return self._service_info

    @property
    def links(self):
        return self.service_info.get('links')

    @property
    def templates(self):
        return self.service_info.get('templates')

    @property
    def groups(self):
        return self.service_info.get('groups')

    @property
    def pipelines(self):
        return self.service_info.get('pipelines')

    @property
    def searches(self):
        return self.service_info.get('searches')

    # Would like to call this 'labels' to keep in line with how
    # other properties are named, but it's already used for a function.
    @property
    def allowed_labels(self):
        return self.service_info.get('labels')

    @property
    def em_groups(self):
        return self.service_info.get('em-groups')

    @property
    def wavebands(self):
        return self.service_info.get('wavebands')

    @property
    def eel_statuses(self):
        return self.service_info.get('eel-statuses')

    @property
    def obs_statuses(self):
        return self.service_info.get('obs-statuses')

    @property
    def voevent_types(self):
        return self.service_info.get('voevent-types')

    def request(self, method, *args, **kwargs):
        if method.lower() in ['post', 'put']:
            kwargs['priming_url'] = self.service_url
        return GsiRest.request(self, method, *args, **kwargs)

    def _getCode(self, input_value, code_dict):
        """Check if input is valid.
           Return coded version if it is"""
        #  code_dict is dict of { code : descriptive_name }
        if input_value in list(code_dict.keys()):
            # Already coded
            return input_value
        if not input_value in list(code_dict.values()):
            # Not valid
            return None
        return [code
                for code, name in code_dict.items()
                if name == input_value][0]

    # Search and filecontents are optional when creating an event.
    def createEvent(self, group, pipeline, filename, search=None, labels=None,
                    offline=False, filecontents=None, **kwargs):
        """Create a new GraceDB event

        Required args: group, pipeline, filename

        Optional args: search, labels, offline, filecontents

        The filename is the path to a file containing information about the
        event. The values for 'group', 'pipeline', and 'search' are restricted
        to those stored in the database. 'labels' should be a list of strings
        corresponding to labels (values restricted to those stored in the
        database).  'labels' can be a string if only a single label is
        being applied. 'offline' is a boolean which indicates whether the
        analysis is offline (True) or online/low-latency (False).

        Example:

            >>> g = GraceDb()
            >>> r = g.createEvent('CBC', 'gstlal', '/path/to/something.xml',
            ... labels='INJ', 'LowMass')
            >>> r.status
            201

        """
        errors = []
        if group not in self.groups:
            errors += ["bad group"]
        if pipeline not in self.pipelines:
            errors += ["bad pipeline"]
        if search and search not in self.searches:
            errors += ["bad search"]
        # Process offline arg
        if not isinstance(offline, bool):
            errors += ["offline should be True or False"]
        # Process label args - convert non-empty strings to list
        # to ensure consistent processing
        if labels:
            if isinstance(labels, six.string_types):
                # Convert to list
                labels = [labels]
            elif isinstance(labels, list):
                pass
            else:
                # Raise exception instead of adding errors. The next for loop
                # will break (before errors exception is raised) if labels
                # is of the wrong type
                raise TypeError("labels arg is {0}, should be str or list" \
                    .format(type(labels)))
            # Check labels against those in database
            for l in labels:
                if l not in self.allowed_labels:
                    raise NameError(("Label '{0}' does not exist in the "
                        "database").format(l))
        if errors:
            # XXX Terrible error messages / weak exception type
            raise Exception(str(errors))
        if filecontents is None:
            if filename == '-':
                filename = 'initial.data'
                filecontents = sys.stdin.read()
            else:
                filecontents = open(filename, 'rb').read()

        fields = [
            ('group', group),
            ('pipeline', pipeline),
            ('offline', offline),
        ]
        if search:
            fields.append(('search', search))
        if labels:
            for l in labels:
                fields.append(('labels', l))

        # Update fields with additional keyword arguments
        for key, value in six.iteritems(kwargs):
            fields.append((key, value))

        files = [('eventFile', filename, filecontents)]
        # Python httplib bug?  unicode link
        uri = str(self.links['events'])
        return self.post(uri, fields, files=files)

    def replaceEvent(self, graceid, filename, filecontents=None):
        """Replace an existing GraceDB event

        Required args: graceid, filename

        This function uploads a new event file, hence changing the basic details
        of an existing event.

        Example:

            >>> g = GraceDb()
            >>> r = g.replaceEvent('T101383', '/path/to/new/something.xml')

        """
        if filecontents is None:
            # Note: not allowing filename '-' here.  We want the event datafile
            # to be versioned.
            filecontents = open(filename, 'rb').read()
        return self.put(
                self.templates['event-detail-template'].format(graceid=graceid),
                files=[('eventFile', filename, filecontents)])

    def event(self, graceid):
        """Get information about a specific event

        Args: graceid

        Example:

            >>> g = GraceDb()
            >>> event_dict = g.event('T101383').json()

        """
        return self.get(
                self.templates['event-detail-template'].format(graceid=graceid))

    def events(self, query=None, orderby=None, count=None, columns=None):
        """Get a iterator of events in response to a query

        This function returns an iterator which yields event dictionaries.
        Optional arguments are query, orderby, count, and columns. The
        columns argument is a comma separated list of attributes that the
        user would like in each event dictionary. If columns are not specified,
        all attributes of the events are returned.

        Example:

            >>> g = GraceDb()
            >>> for event in g.events('ER5 submitter: "gstlalcbc"', columns='graceid,far,gpstime'):
            ...     print "%s %e %d" % (event['graceid'], event['far'], event['gpstime'])


        """
        uri = self.links['events']
        qdict = {}
        if query:   qdict['query'] = query
        if count:   qdict['count'] = count
        if orderby: qdict['sort'] = orderby
        if columns: qdict['columns'] = columns
        if qdict:
            uri += "?" + urlencode(qdict)
        while uri:
            response = self.get(uri).json()
            events = response.get('events',[])
            uri = response.get('links',{}).get('next')
            for event in events:
                 yield event

    def numEvents(self, query=None):
        """Get the number of events satisfying a query

        Example:

            >>> g = GraceDb()
            >>> g.numEvents('ER5 submitter: "gstlalcbc"')
            213

        """
        uri = self.links['events']
        if query:
            uri += "?" + urlencode({'query': query})
        return self.get(uri).json()['numRows']

    def createSuperevent(self, t_start, t_0, t_end, preferred_event,
        events=[], labels=None):
        """
        Create a superevent.

        Signature:
        createSuperevent(t_start, t_0, t_end, preferred_event,
            events=[], labels=None)

        Arguments:
            t_start:         t_start of superevent
            t_0:             t_0 of superevent
            t_end:           t_end of superevent
            preferred_event: graceid corresponding to event which will be set
                             as the preferred event for this superevent
            events:          list of graceids corresponding to events which
                             should be attached to this superevent (optional)
            labels:          list of labels which should be attached to this
                             superevent at creation (optional)

        Example:

            >>> g = GraceDb()
            >>> r = g.createSuperevent(1,2,3, preferred_event='G123456',
            ... events=['G100', 'G101'], labels=['EM_READY', 'DQV'])
            >>> r.status
            201
        """
        # Process label args - convert non-empty strings to list
        # to ensure consistent processing
        if labels:
            if isinstance(labels, six.string_types):
                labels = [labels]
            elif isinstance(labels, list):
                pass
            else:
                raise TypeError("labels arg is {0}, should be str or list" \
                    .format(type(labels)))
            # Check labels against those in database
            for l in labels:
                if l not in self.allowed_labels:
                    raise NameError(("Label '{0}' does not exist in the "
                        "database").format(l))
        if events:
            if isinstance(events, six.string_types):
                events = [events]
            elif isinstance(events, list):
                pass
            else:
                raise TypeError("events arg is {0}, should be str or list" \
                    .format(type(events)))

        # Set up request body for POST
        request_body = {
            't_start': t_start,
            't_0': t_0,
            't_end': t_end,
            'preferred_event': preferred_event,
        }
        if events:
            request_body['events'] = events
        if labels:
            request_body['labels'] = labels

        # Python httplib bug?  unicode link
        uri = self.links['superevents']
        return self.post(uri, body=request_body)

    def updateSuperevent(self, superevent_id, t_start=None, t_0=None,
        t_end=None, preferred_event=None):
        """
        Update a superevent's parameters.

        Signature:
        updateSuperevent(superevent_id, t_start=None, t_0=None,
            t_end=None, preferred_event=None)

        Arguments:
            superevent_id:   id of superevent to update
            t_start:         t_start of superevent
            t_0:             t_0 of superevent
            t_end:           t_end of superevent
            preferred_event: graceid corresponding to event which will be set
                             as the preferred event for this superevent

            Any combination of these parameters (other than superevent_id)
            may be used; none are required.

        Example:

            >>> g = GraceDb()
            >>> r = g.updateSuperevent('S0001', t_start=12, preferred_event=
            ... 'G654321')
            >>> r.status
            200
        """

        request_body = {}
        if t_start:
            request_body['t_start'] = t_start
        if t_0:
            request_body['t_0'] = t_0
        if t_end:
            request_body['t_end'] = t_end
        if preferred_event:
            request_body['preferred_event'] = preferred_event
        template = self.templates['superevent-detail-template']
        uri = template.format(superevent_id=superevent_id)
        return self.patch(uri, body=request_body)

    def addEventToSuperevent(self, superevent_id, graceid):
        """
        Add an event to a superevent. Events can only be part of one superevent
        so the server will throw an error if the event is part of another
        superevent already.

        Signature:
            addEventToSuperevent(superevent_id, graceid)

        Arguments:
            superevent_id: id of superevent to which the event will be added
            graceid:       graceid of event to add to superevent

        Example:

            >>> g = GraceDb()
            >>> r = addEventToSuperevent('S0001', 'G123456')
            >>> r.status
            201
        """
        request_body = {'event': graceid}
        template = self.templates['superevent-event-list-template']
        uri = template.format(superevent_id=superevent_id)
        return self.post(uri, body=request_body)

    def removeEventFromSuperevent(self, superevent_id, graceid):
        """
        Remove an event from a superevent.

        Signature:
            removeEventFromSuperevent(superevent_id, graceid)

        Arguments:
            superevent_id, graceid

        Example:

            >>> g = GraceDb()
            >>> r = removeEventFromSuperevent('S0001', 'G123456')
            >>> r.status
            204
        """
        template = self.templates['superevent-event-detail-template']
        uri = template.format(superevent_id=superevent_id, graceid=graceid)
        return self.delete(uri)

    def superevent(self, superevent_id):
        """
        Get information about a specific superevent.

        Signature:
            superevent(superevent_id)

        Arguments:
            superevent_id

        Example:

            >>> g = GraceDb()
            >>> superevent = g.superevent('S0001').json()
        """
        return self.get(self.templates['superevent-detail-template'].format(
            superevent_id=superevent_id))

    def superevents(self, query='', orderby=[], count=None, columns=[]):
        """
        Get an iterator of superevents in response to a query.

        Signature:
            superevents(query='', orderby=[], count=None, columns=[])

        Arguments:
            query: query string for filtering superevents (same as on the
                   web interface)
            orderby: list of strings corresponding to attribute(s) of the
                     superevents used to order the results (optional).
                     Available options: created, t_start, t_0, t_end, is_gw,
                     id, preferred_event. Default is ascending order, but
                     prefix any option with "-" to apply descending order.
            count: each generator iteration will yield this many objects
                   (optional; default determined by the server)
            columns: which attributes of the superevents to return
                     (default: all).

        Example:

            >>> g = GraceDb()
            >>> for s in g.superevents(query='is_gw=True', orderby=['-preferred_event'], columns='superevent_id,events'):
            ...     print(s['superevent_id'])
        """
        # If columns is a comma-separated string, split it to a list
        if isinstance(columns, six.string_types):
            columns = columns.split(',')

        # If orderby is a list (should be), convert it to a comma-separated
        # string (that's what the server expects)
        if isinstance(orderby, list):
            orderby = ",".join(orderby)

        # Get URI
        uri = self.links['superevents']

        # Compile URL parameters
        qdict = {}
        if query:   qdict['query'] = query
        if count:   qdict['count'] = count
        if orderby: qdict['sort'] = orderby
        if qdict:
            uri += "?" + urlencode(qdict)

        # Get superevent information and construct a generator
        while uri:
            response = self.get(uri).json()
            superevents = response.get('superevents',[])
            uri = response.get('links',{}).get('next')
            for superevent in superevents:
                # If columns are specified, only return specific values
                if columns:
                    yield {k: superevent[k] for k in columns}
                else:
                    yield superevent

    def confirm_superevent_as_gw(self, superevent_id):
        """
        Upgrade a superevent's state to 'confirmed GW'.
        Requires specific server-side permissions.

        Signature:
            confirm_superevent_as_gw(superevent_id)

        Arguments:
            superevent_id
        """
        template = self.templates['superevent-confirm-as-gw-template']
        uri = template.format(superevent_id=superevent_id)
        return self.post(uri)

    @event_or_superevent
    def files(self, object_id, filename="", *args, **kwargs):
        """
        Files for a particular event or superevent.

        Signature:
            files(object_id, filename="")

        Arguments:
            object_id: event graceid or superevent id
            filename:  name of file (optional)

        If a filename is not specified, a list of filenames associated with the
        event or superevent is returned.
        If a filename is specified, the file's content is returned.

        Example 1: get a list of files

            >>> g = GraceDb()
            >>> event_files = g.files('T101383').json()
            >>> for filename in event_files.keys():
            ...     # do something
            ...     pass

        Example 2: get a file's content

            >>> outfile = open('my_skymap.png', 'w')
            >>> r = g.files('T101383', 'skymap.png')
            >>> outfile.write(r.read())
            >>> outfile.close()

        """
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            uri_kwargs = {'superevent_id': object_id}
            if filename:
                # Get specific file
                uri_kwargs['file_name'] = filename
                template = self.templates['superevent-file-detail-template']
            else:
                # Get list of files
                template = self.templates['superevent-file-list-template']
        else:
            template = self.templates['files-template']
            uri_kwargs = {'graceid': object_id, 'filename': filename}
        uri = template.format(**uri_kwargs)
        return self.get(uri)

    def writeFile(self, object_id, filename, filecontents=None):
        """
        Upload a file for an event or superevent.

        Signature:
            writeFile(object_id, filename, filecontents=None)

        Required args:
            object_id: event graceid or superevent id
            filename:  path to file

        This method creates a new log message with your file attached. It is
        strongly preferred to use writeLog() instead of writeFile() so that you
        can add a more suitable comment. That will make it easier for other
        users to know what your file contains.

        Example:

            >>> g = GraceDb()
            >>> r = g.writeFile('T101383', '/path/to/my_interesting_plot.png')
            >>> print r.status

        """
        print("WARNING: the writeFile() method is deprecated in favor "
            "of writeLog() and will be removed in a future release.")
        return self.writeLog(object_id, "FILE UPLOAD", filename, filecontents)

    @event_or_superevent
    def logs(self, object_id, log_number=None, *args, **kwargs):
        """
        Get log messages associated with an event or superevent

        Signature:
            logs(object_id, log_number=None)

        Arguments:
            object_id:  event graceid or superevent id
            log_number: log message number (N) of log message to retrieve
                        (optional)

        If a log_number is specified, only a single log message is returned.
        If a log_number is not specified, a list of all log messages attached
        to the event or superevent in questions is returned.

        Example 1: get all log messages

            >>> g = GraceDb()
            >>> response_dict = g.logs('T101383').json()
            >>> print "Num logs = %d" % response_dict['numRows']
            >>> log_list = response_dict['log']
            >>> for log in log_list:
            ...     print log['comment']

        Example 2: get a single log message

            >>> g = GraceDb()
            >>> log_info = g.logs('T101383', 10).json()
        """
        if log_number and not isinstance(log_number, int):
            raise TypeError('log_number should be an int')

        # Set up template and object id
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            uri_kwargs = {'superevent_id': object_id}
            if log_number:
                template = self.templates['superevent-log-detail-template']
            else:
                template = self.templates['superevent-log-list-template']
        else:
            uri_kwargs = {'graceid': object_id}
            if log_number:
                template = self.templates['event-log-detail-template']
            else:
                template = self.templates['event-log-template']

        if log_number:
            uri_kwargs['N'] = log_number

        uri = template.format(**uri_kwargs)
        return self.get(uri)

    @event_or_superevent
    def writeLog(self, object_id, message, filename=None, filecontents=None,
            tag_name=[], displayName=[], *args, **kwargs):
        """
        Create a new log message.

        Signature:
        writeLog(object_id, message, filename=None, filecontents=None,
            tag_name=[], displayName=[])

        Arguments:
            object_id:    event graceid or superevent id
            message:      comment to post on the event log
            filename:     path to file to be uploaded (optional)
            filecontents: handler pointing to a file to be read
                          (optional)
            tag_name:     tag name string or list of tag names to be attached
                          to the log message
            displayName:  tag display name string or list of display names for
                          tags. If provided, there should be one for each tag.
                          (optional)

        If only object_id and message are provided, a text comment will be
        created in the event or superevent log. If a filename is also
        specified, the file will be attached to the log message and displayed
        alongside the message text. If a tag_name is provided, the message will
        be tagged.

        Example:

            >>> g = GraceDb()
            >>> r = g.writeLog('T101383', 'Good stuff.', '/path/to/plot.png',
            ... tag_name='analyst_comments')
            >>> print r.status
            201
        """
        # Check displayName length - should be 0 or same as tag_name
        if displayName and isinstance(tag_name, list) and \
            len(displayName) != len(tag_name):
            raise ValueError("For a list of tags, either provide no display "
                "names or a display name for each tag")

        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            template = self.templates['superevent-log-list-template']
            uri_kwargs = {'superevent_id': object_id}
        else:
            template = self.templates['event-log-template']
            uri_kwargs = {'graceid': object_id}
        uri = template.format(**uri_kwargs)
        files = None
        if filename:
            if filecontents is None:
                if filename == '-':
                    filename = 'stdin'
                    filecontents = sys.stdin.read()
                else:
                    filecontents = open(filename, "rb").read()
            elif hasattr(filecontents, 'read'):
                # XXX Does not scale well.
                filecontents = filecontents.read()
            files = [('upload', os.path.basename(filename), filecontents)]

        # Handle cases where tag_name or displayName are strings
        if isinstance(tag_name, str):
            tag_name = [tag_name]
        elif isinstance(tag_name, (tuple, set)):
            tag_name = list(tag_name)
        elif tag_name is None:
            tag_name = []

        if isinstance(displayName, str):
            displayName = [displayName]
        elif isinstance(displayName, (tuple, set)):
            displayName = list(displayName)
        elif displayName is None:
            displayName = []

        # Set up body of request
        body = {
            'comment': message,
            'tagname': tag_name,
            'displayName': displayName,
        }

        # If files are attached, we have to encode the request body
        # differently, so we convert from a dict to a list of tuples.
        if files:
            fields = []
            for k,v in body.items():
                if isinstance(v, list):
                    for item in v: fields.append((k, item))
                else:
                    fields.append((k,v))
            body = fields

        return self.post(uri, body, files=files)

    def eels(self, graceid):
        """Given a GraceID, get a list of EMBB log entries

        Example:

            >>> g = GraceDb()
            >>> r = g.eels('T101383')
            >>> full_dictionary = r.json()            # Convert the response to a dictionary
            >>> eel_list = full_dictionary['embblog'] # Pull out a list of EEL dicts

        """

        template = self.templates['embb-event-log-template']
        uri = template.format(graceid=graceid)
        return self.get(uri)

    def writeEel(self, graceid, group, waveband, eel_status,
            obs_status, **kwargs):
        """Write an EMBB event log entry

        Required args: graceid, group, waveband, eel_status, obs_status

        (Note that 'group' here is the name of the EM MOU group, not
        the LVC data analysis group responsible for the original detection.)

        Additional keyword arguments may be passed in to be sent in the POST
        data. Only the following kwargs are recognized:
            ra
            dec
            raWidth
            decWidth
            gpstime
            duration
            comment
            extra_info_dict

        Most of these are self-explanatory, but the 'extra_info_dict' is meant
        to be a JSON string containing any additional information the user may
        wish to convey.

        Any other kwargs will be ignored.
        """
        # validate facility, waveband, eel_status, and obs_status
        if not group in self.em_groups:
            raise ValueError("group must be one of %s" % self.em_groups)

        if not waveband in list(self.wavebands.keys()):
            raise ValueError("waveband must be one of %s" % list(self.wavebands.keys()))

        eel_status = self._getCode(eel_status, self.eel_statuses)
        if not eel_status:
            raise ValueError("EEL status must be one of %s" % list(self.eel_statuses.values()))

        obs_status = self._getCode(obs_status, self.obs_statuses)
        if not obs_status:
            raise ValueError("Observation status must be one of %s" % list(self.obs_statuses.values()))

        template = self.templates['embb-event-log-template']
        uri = template.format(graceid=graceid)

        body = {
            'group' : group,
            'waveband' : waveband,
            'eel_status' : eel_status,
            'obs_status' : obs_status,
        }
        body.update(**kwargs)
        return self.post(uri, body=body)

    @event_or_superevent
    def emobservations(self, object_id, emobservation_num=None, *args,
        **kwargs):
        """
        Given an event graceid or superevent id, get a list of EM observation
        entries or a specific EM observation.

        Signature:
            emobservations(object_id, emobservation_num=None)

        Arguments:
            object_id: event graceid or superevent id
            emobservation_num: number of the EM observation (N) (optional)

        Example 1: get a list of EM observations

            >>> g = GraceDb()
            >>> r = g.emobservations('T101383')
            >>> full_dictionary = r.json()
            >>> emo_list = full_dictionary['observations']

        Example 2: get a single EM observation

            >>> g = GraceDb()
            >>> r = g.emobservations('T101383', 2)
            >>> observation_dict = r.json()
        """
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            uri_kwargs = {'superevent_id': object_id}
            if emobservation_num:
                template = self.templates['superevent-emobservation-detail-template']
            else:
                template = self.templates['superevent-emobservation-list-template']
        else:
            uri_kwargs = {'graceid': object_id}
            if emobservation_num:
                template = self.templates['emobservation-detail-template']
            else:
                template = self.templates['emobservation-list-template']

        if emobservation_num:
            uri_kwargs['N'] = emobservation_num

        uri = template.format(**uri_kwargs)
        return self.get(uri)

    @event_or_superevent
    def writeEMObservation(self, object_id, group, raList, raWidthList,
        decList, decWidthList, startTimeList, durationList, comment="",
        *args, **kwargs):
        """
        Write an EM observation entry.

        Signature:
            writeEMObservation(object_id, group, raList, raWidthList,
                decList, decWidthList, startTimeList, durationList,
                comment=None)

        Arguments:
            object_id: event graceid or superevent id
            group: name of EM MOU group making the observation
            raList: list of right ascensions (deg.)
            raWidthList: list of right ascension widths OR a single number
                         if all measurements have the same width (deg.)
            decList: list of declinations (deg.)
            decWidthList: list of declination widths OR a single number
                          if all measurements have the same width (deg.)
            startTimeList: list of measurement start times (ISO 8601 UTC)
                           format example: 2018-05-25T16:30:12+00:00
            durationList: list of exposure times OR a single number if all
                          measurements have the same exposure (seconds)
            comment: comment on observation (optional)
        """
        # validate facility, waveband, eel_status, and obs_status
        if not group in self.em_groups:
            raise ValueError("group must be one of %s" % self.em_groups)

        # Argument checking
        num_measurements = len(raList)
        # convert any single number widths or durations into lists
        raWidthList, decWidthList, durationList = \
            [[l] * num_measurements if not isinstance(l, (list, tuple)) else l
            for l in [raWidthList, decWidthList, durationList]]

        # Compare all list lengths
        all_lists = [raList, decList, startTimeList, raWidthList,
            decWidthList, durationList]
        if not all(map(lambda l: len(l) == num_measurements, all_lists)):
            raise ValueError('raList, decList, startTimeList, raWidthList, '
                'decWidthList, and durationList should be the same length')

        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            template = self.templates['superevent-emobservation-list-template']
            uri_kwargs = {'superevent_id': object_id}
        else:
            template = self.templates['emobservation-list-template']
            uri_kwargs = {'graceid': object_id}
        uri = template.format(**uri_kwargs)

        body = {
            'group' : group,
            'ra_list' : raList,
            'ra_width_list': raWidthList,
            'dec_list' : decList,
            'dec_width_list': decWidthList,
            'start_time_list': startTimeList,
            'duration_list': durationList,
            'comment': comment,
        }
        return self.post(uri, body=body)

    @event_or_superevent
    def labels(self, object_id, label="", *args, **kwargs):
        """
        Get a list of labels or a single label for an event or superevent.

        Signature:
            labels(object_id, label="")

        Arguments:
            object_id: event graceid or superevent id
            label:     name of label (optional)

        Example 1: get list of labels

            >>> g = GraceDb()
            >>> label_list = g.labels('T101383').json()['labels']
            >>> for label in label_list:
            ...     print label['name']

        Example 2: get a single label

            >>> g = GraceDb()
            >>> dqv_label = g.labels('T101383', 'DQV').json()

        """
        # Check label name
        if label and label not in self.allowed_labels:
            raise NameError(("Label '{0}' does not exist in the "
                "database").format(label))

        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            uri_kwargs = {'superevent_id': object_id}
            if label:
                template = self.templates['superevent-label-detail-template']
                uri_kwargs['label_name'] = label
            else:
                template = self.templates['superevent-label-list-template']
        else:
            template = self.templates['event-label-template']
            uri_kwargs = {'graceid': object_id}
            uri_kwargs['label'] = label

        uri = template.format(**uri_kwargs)
        return self.get(uri)

    @event_or_superevent
    def writeLabel(self, object_id, label, *args, **kwargs):
        """
        Add a new label to an event or superevent.

        Signature:
            writeLabel(object_id, label)

        Arguments:
            object_id: event graceid or superevent id
            label:     label name

        The label name must correspond to one of the existing
        GraceDB labels (see self.allowed_labels), or an error will result.

        Example:

            >>> g = GraceDb()
            >>> r = g.writeLabel('T101383', 'DQV')
            >>> r.status
            201

        """
        # Check label name
        if label not in self.allowed_labels:
            raise NameError(("Label '{0}' does not exist in the "
                "database").format(label))

        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            template = self.templates['superevent-label-list-template']
            uri_kwargs = {'superevent_id': object_id}
            request_body = {'name': label}
            method = 'POST'
        else:
            template = self.templates['event-label-template']
            uri_kwargs = {'graceid': object_id}
            uri_kwargs['label'] = label
            request_body = {}
            method = 'PUT'
        uri = template.format(**uri_kwargs)
        return self.post_or_put_or_patch(method, uri, body=request_body)

    @event_or_superevent
    def removeLabel(self, object_id, label, *args, **kwargs):
        """
        Remove a label from an event or superevent.

        Signature:
            removeLabel(object_id, label)

        Arguments:
            object_id: event graceid or superevent id
            label:     name of label to be removed

        Example:

            >>> g = GraceDb()
            >>> r = g.removeLabel('T101383', 'DQV')
            >>> r.status
            204
        """
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            template = self.templates['superevent-label-detail-template']
            uri_kwargs = {'superevent_id': object_id}
            uri_kwargs['label_name'] = label
        else:
            template = self.templates['event-label-template']
            uri_kwargs = {'graceid': object_id}
            uri_kwargs['label'] = label
        uri = template.format(**uri_kwargs)
        return self.delete(uri)

    @event_or_superevent
    def tags(self, object_id, N, *args, **kwargs):
        """
        Get a list of tags associated with a particular event or superevent
        log message.

        Signature:
            tags(object_id, N)

        Arguments:
            object_id: event graceid or superevent id
            N:         log message number

        Example:

            >>> g = GraceDb()
            >>> tag_list = g.tags('T101383', 56).json()['tags']
            >>> print "Number of tags for message 56: %d" % len(tag_list)
        """
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            uri_kwargs = {'superevent_id': object_id}
            template = self.templates['superevent-log-tag-list-template']
        else:
            uri_kwargs = {'graceid': object_id}
            template = self.templates['taglist-template']
        uri_kwargs['N'] = N
        uri = template.format(**uri_kwargs)
        return self.get(uri)

    def createTag(self, object_id, N, tag_name, displayName=None, *args,
        **kwargs):
        print('WARNING: the createTag() method is deprecated in favor of '
            'addTag() and will be removed in a future release.')
        return self.createTag(object_id, N, tag_name, displayName, *args,
            **kwargs)

    @event_or_superevent
    def addTag(self, object_id, N, tag_name, displayName=None, *args,
        **kwargs):
        """
        Add a new tag to an event or superevent log message.

        Signature:
            addTag(object_id, N, tag_name, displayName=None)

        Arguments:
            object_id:   event graceid or superevent id
            N:           log message number
            tag_name:    name of tag to add; tags which don't exist already
                         will be created.
            displayName: tag display name (optional)

        If a displayName is provided (and if the tag doesn't already exist), a
        new tag will be created with the provided displayName.

        Example:

            >>> g = GraceDb()
            >>> r = g.createTag('T101383', 56, 'sky_loc')
            >>> r.status
            201
        """
        is_superevent = kwargs.pop('is_superevent', False)
        request_body = {'displayName': displayName}
        if is_superevent:
            template = self.templates['superevent-log-tag-list-template']
            uri_kwargs = {'superevent_id': object_id}
            request_body['name'] = tag_name
            method = 'POST'
        else:
            template = self.templates['tag-template']
            uri_kwargs = {'graceid': object_id, 'tag_name': tag_name}
            method = 'PUT'
    
        uri_kwargs['N'] = N
        uri = template.format(**uri_kwargs)
        return self.post_or_put_or_patch(method, uri, body=request_body)

    def deleteTag(self, object_id, N, tag_name, *args, **kwargs):
        print('WARNING: the deleteTag() method is deprecated in favor of '
            'removeTag() and will be removed in a future release.')
        return self.removeTag(object_id, N, tag_name, *args, **kwargs)

    @event_or_superevent
    def removeTag(self, object_id, N, tag_name, *args, **kwargs):
        """
        Remove a tag from a given event or superevent log message.

        Signature:
            removeTag(object_id, N, tag_name)

        Arguments:
            object_id: event graceid or superevent id
            N:         log message number
            tag_name:  name of the tag to be removed

        Example:

            >>> g = GraceDb()
            >>> r = g.deleteTag('T101383', 56, 'sky_loc')
            >>> r.status
            200
        """
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            template = self.templates['superevent-log-tag-detail-template']
            uri_kwargs = {'superevent_id': object_id}
        else:
            template = self.templates['tag-template']
            uri_kwargs = {'graceid': object_id}
        uri_kwargs['N'] = N
        uri_kwargs['tag_name'] = tag_name
        uri = template.format(**uri_kwargs)
        return self.delete(uri)

    def ping(self):
        """Ping the server

        If you get back an HTTPResponse object, it's alive.
        The JSON content is the service info, which is normally not of
        interest. But you can use it to get the known Groups, Searches,
        Pipelines, etc.

        Example:

            >>> g = GraceDb()
            >>> groups = g.ping().json()['groups']

        """
        return self.get(self.links['self'])

    @event_or_superevent
    def voevents(self, object_id, voevent_num=None, get_voevent_file_text=True,
        *args, **kwargs):
        """
        Get a list of VOEvents or a single VOEvent for an event or superevent.

        Signature:
            voevents(object_id, voevent_num=None, get_voevent_file_text=True)

        Arguments:
            object_id:   event graceid or superevent id
            voevent_num: number of VOEvent to retrieve (optional)

            get_voevent_file_text is a temporary deprecation measure to provide
            the text of the VOEvent file in the output of this function. This
            will not be provided in the future.

        Example 1: get a list of VOEvents

            >>> g = GraceDb()
            >>> r = g.voevents('T101383')
            >>> voevent_list = r.json()['voevents']

        Example 2: get a single VOEvent

            >>> g = GraceDb()
            >>> r = g.voevents('T101383')
            >>> voevent_list = r.json()['voevents']
        """
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            uri_kwargs = {'superevent_id': object_id}
            if voevent_num:
                template = self.templates['superevent-voevent-detail-template']
            else:
                template = self.templates['superevent-voevent-list-template']
        else:
            uri_kwargs = {'graceid': object_id}
            if voevent_num:
                template = self.templates['voevent-detail-template']
            else:
                template = self.templates['voevent-list-template']

        if voevent_num:
            uri_kwargs['N'] = voevent_num

        uri = template.format(**uri_kwargs)
        response = self.get(uri)

        # Temporary hack to get voevent file text and include it in a
        # response-like object for user consumption. Will be removed in a
        # future release.
        if get_voevent_file_text:
            # print a deprecation warning
            print("WARNING: the VOEvent file text will not be included in the "
                "response JSON in a future release. Users can still get the "
                "content of this file by using the files() method along with "
                "the filename which is included in the response JSON. You may "
                "call this method with the keyword argument "
                "get_voevent_file_text=False if you do not need the file "
                "content and would like to remove this warning message.")

            # Get actual response JSON
            r_json = response.json()

            if 'voevents' in r_json:
                # list of voevents - iterate and get file text
                for v in r_json['voevents']:
                    v[u'text'] = self.files(object_id, v['filename']).read()
            else:
                file_text = self.files(object_id, r_json['filename']).read()
                r_json[u'text'] = file_text

            # Set up fake response object with json() and read() methods
            class FakeResponse(object):
                def __init__(self, json, *args, **kwargs): self._data = json
                def json(self): return self._data
                def read(self): return json.dumps(self._data) 
            response = FakeResponse(r_json)

        # Return response
        return response

    @event_or_superevent
    def createVOEvent(self, object_id, voevent_type, skymap_type=None,
        skymap_filename=None, skymap_image_filename=None, internal=True,
        vetted=False, open_alert=False, hardware_inj=False,
        CoincComment=False, ProbHasNS=None, ProbHasRemnant=None,
        *args, **kwargs):
        # TBD for superevents - requires a lot of changes to server code
        """Create a new VOEvent

        Signature:
            createVOEvent(self, object_id, voevent_type, skymap_type=None,
                skymap_filename=None, skymap_image_filename=None,
                internal=True, vetted=False, open_alert=False,
                hardware_inj=False, CoincComment=False, ProbHasNS=None,
                ProbHasRemnant=None)

        Arguments:
            object_id: event graceid or superevent id
            voevent_type: VOEvent type (options: 'preliminary', 'initial',
                          'update', 'retraction')
            skymap_type: skymap type (required for voevents which include a
                         skymap)
            skymap_filename: name of skymap file in GraceDB (required for
                             'initial' and 'update' alerts, optional for
                             'preliminary' alerts.
            skymap_image_filename: name of skymap image file in GraceDB
                                   (same requirements as skymap_filename)
            internal: whether the VOEvent should be distributed to LVC members
                      only (True/False)
            hardware_inj: whether the candidate is a hardware injection
                          (True/False)
            vetted: whether candidate has undergone basic human vetting
                    (True/False)
            open_alert: whether the candidate is an open alert or not
                        (True/False)
            CoincComment: whether the candidate has a possible counterpart
                          GRB (True/False)
            ProbHasNS: probability that at least one object in the binary is
                       less than 3 M_sun (optional)
            ProbHasRemnant: probability that there is matter in the
                            surroundings of the central object (optional)

        """
        is_superevent = kwargs.pop('is_superevent', False)
        if is_superevent:
            template = self.templates['superevent-voevent-list-template']
            uri_kwargs = {'superevent_id': object_id}
        else:
            template = self.templates['voevent-list-template']
            uri_kwargs = {'graceid': object_id}
        uri = template.format(**uri_kwargs)

        # validate voevent_type, convert to short form if necessary
        voevent_type = self._getCode(voevent_type.lower(), self.voevent_types)
        if not voevent_type:
            raise ValueError("voevent_type must be one of: {0}".format(
                list(self.voevent_types.values())))

        # TODO:
        # Require skymaps for 'update' and 'initial'

        # Construct request body
        body = {
            'voevent_type': voevent_type,
            'internal': internal,
            'vetted': vetted,
            'open_alert': open_alert,
            'hardware_inj': hardware_inj,
            'CoincComment': CoincComment
        }
        # Add optional args
        if skymap_type:
            body['skymap_type'] = skymap_type
        if skymap_filename:
            body['skymap_filename'] = skymap_filename
        if skymap_image_filename:
            body['skymap_image_filename'] = skymap_image_filename
        if ProbHasNS:
            body['ProbHasNS'] = ProbHasNS
        if ProbHasRemnant:
            body['ProbHasRemnant'] = ProbHasRemnant

        return self.post(uri, body=body)


#-----------------------------------------------------------------
# TBD
# Media Types

# Root

# Collection

# Event

# Log

# File

# Label

#-----------------------------------------------------------------
# Basic auth for the LV-EM users

class GraceDbBasic(GraceDb):
    """Example GraceDb REST client with basic auth

    The GraceDB service URL may be passed to the constructor
    if an alternate GraceDb instance is desired:

        >>> g = GraceDb("https://alternate.gracedb.edu/api/")
        >>> r = g.ping()

    The proxy_host and proxy_port may also be passed in if accessing
    GraceDB behind a proxy. For other kwargs accepted by the constructor,
    consult the source code.
    """
    def __init__(self, service_url=DEFAULT_BASIC_SERVICE_URL,
            proxy_host=None, proxy_port=3128, username=None, password=None,
            *args, **kwargs):

        o = urlparse(service_url)
        port = o.port
        host = o.hostname
        port = port or 443

        if not username or not password:
            try:
                username, account, password = safe_netrc().authenticators(host)
            except IOError:
                pass

        if not username or not password:
            msg = "\nERROR: Could not find user credentials. \n\n"
            msg +="Please use a .netrc file or provide username and password.\n\n"
            self.output_and_die(msg)

        # Construct authorization header
        userAndPass = b64encode(b"%s:%s" % (username, password)).decode("ascii")
        self.authn_header = { 'Authorization' : 'Basic %s' %  userAndPass }

        # Versions of Python earlier than 2.7.9 don't use SSL Context
        # objects for this purpose, and do not do any server cert verification.
        ssl_context = None
        if sys.hexversion >= 0x20709f0:
            # Use the new method with SSL Context
            # Prepare SSL context
            ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
            # Generally speaking, test boxes use cheap/free certs from the LIGO CA.
            # These cannot be verified by the client.
            if host in KNOWN_TEST_HOSTS:
                ssl_context.verify_mode = ssl.CERT_NONE
            else:
                ssl_context.verify_mode = ssl.CERT_REQUIRED
                ssl_context.check_hostname = True
                # Find the various CA cert bundles stored on the system
                ssl_context.load_default_certs()

            if proxy_host:
                self.connector = lambda: ProxyHTTPSConnection(proxy_host, proxy_port, context=ssl_context)
            else:
                self.connector = lambda: six.moves.http_client.HTTPSConnection(host, port, context=ssl_context)
        else:
            # Using and older version of python. We'll pass in the cert and key files.
            if proxy_host:
                self.connector = lambda: ProxyHTTPSConnection(proxy_host, proxy_port)
            else:
                self.connector = lambda: six.moves.http_client.HTTPSConnection(host, port)

        self.service_url = service_url
        self._service_info = None

    # When there is a problem with the SSL connection or authentication,
    # either conn.request() or conn.getresponse() will throw an exception.
    def get_response(self, conn):
        try:
            return conn.getresponse()
        except ssl.SSLError as e:
            msg = "\nERROR \n\n"
            msg += "Problem establishing secure connection: %s \n\n" % str(e)
            self.output_and_die(msg)

    def request(self, method, url, body=None, headers=None):
        # Bug in Python (versions < 2.7.1 (?))
        # http://bugs.python.org/issue11898
        # if the URL is unicode and the body of a request is binary,
        # the POST/PUT action fails because it tries to concatenate
        # the two which fails due to encoding problems.
        # Workaround is to cast all URLs to str.
        # This is probably bad in general,
        # but for our purposes, today, this will do.
        url = url and str(url)
        conn = self.getConnection()
        headers = headers or {}
        headers.update(self.authn_header)
        self.make_request(conn, method, url, body, headers)
        response = self.get_response(conn)
        # Catch the 401 unauthorized response before sending to adjust
        # response. Effectively, the 401 response will have special status.
        if response.status == 401:
            try:
                msg = "\nERROR: %s \n\n" % json.loads(response.read())['detail']
            except:
                msg = "\nERROR: \n\n"
            msg += "Please check the username/password in your .netrc file. \n"
            msg += "Note: If your password is more than a year old, you will \n"
            msg += "need to use the web interface to generate a new one. \n\n"
            self.output_and_die(msg)
        return self.adjustResponse(response)

#-----------------------------------------------------------------
# HTTP upload encoding
# Taken from http://code.activestate.com/recipes/146306/

def encode_multipart_formdata(fields, files):
    """
    fields is a sequence of (name, value) elements for regular form fields.
    files is a sequence of (name, filename, value) elements for data to be uploaded as files
    Return (content_type, body) ready for httplib.HTTP instance
    """
    BOUNDARY = b'----------ThIs_Is_tHe_bouNdaRY_$'
    CRLF = b'\r\n'
    L = []
    for (key, value) in fields:
        if value is None: continue
        L.append(b'--' + BOUNDARY)
        L.append(b'Content-Disposition: form-data; name="%s"' % key.encode())
        L.append(b'')
        # encode in case it is unicode
        if isinstance(value, six.binary_type):
            L.append(value)
        elif isinstance(value, six.text_type):
            L.append(value.encode())
        else:
            L.append(str(value).encode())
    for (key, filename, value) in files:
        if value is None: continue
        L.append(b'--' + BOUNDARY)
        # str(filename) in case it is unicode
        L.append(b'Content-Disposition: form-data; name="%s"; filename="%s"' % (key.encode(), filename.encode()))
        L.append(b'Content-Type: %s' % get_content_type(filename).encode())
        L.append(b'')
        if isinstance(value, six.text_type):
            L.append(value.encode())
        else:
            L.append(value)
    L.append(b'--' + BOUNDARY + b'--')
    L.append(b'')
    body = CRLF.join(L)
    content_type = b'multipart/form-data; boundary=%s' % BOUNDARY
    return content_type, body

def get_content_type(filename):
    return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

#-----------------------------------------------------------------
# X509 Credentials

# XXX No longer checking whether this is a pre-RFC proxy.  Is this okay?
def findUserCredentials(warnOnOldProxy=1):

    proxyFile = os.environ.get('X509_USER_PROXY')
    certFile = os.environ.get('X509_USER_CERT')
    keyFile = os.environ.get('X509_USER_KEY')

    if certFile and keyFile:
        return certFile, keyFile

    if proxyFile:
        return proxyFile, proxyFile

    # Try default proxy
    proxyFile = os.path.join('/tmp', "x509up_u%d" % os.getuid())
    if os.path.exists(proxyFile):
        return proxyFile, proxyFile

    # Try default cert/key
    homeDir = os.environ.get('HOME')
    certFile = os.path.join(homeDir, '.globus', 'usercert.pem')
    keyFile = os.path.join(homeDir, '.globus', 'userkey.pem')

    if os.path.exists(certFile) and os.path.exists(keyFile):
        return certFile, keyFile

