from __future__ import absolute_import
from PIL import Image
import numpy
import os
import glob

def load(png_filename):
    """
    Import a png file into a numpy array.

    Arguments:
        png_filename (str): A string filename of a png datafile

    Returns:
        A numpy array with data from the png file
    """
    # Expand filename to be absolute
    png_filename = os.path.expanduser(png_filename)

    try:
        img = Image.open(png_filename)
    except Exception as e:
        raise ValueError("Could not load file {0} for conversion."
                         .format(png_filename))
        raise

    return numpy.array(img)

def save(filename, numpy_data):
    """
    Export a numpy array to a png file.

    Arguments:
        filename (str): A filename to which to save the png data
        numpy_data (numpy.ndarray OR str): The numpy array to save to png.
            OR a string: If a string is provded, it should be a binary png str

    Returns:
        str. The expanded filename that now holds the png data

    Raises:
        ValueError: If the save fails; for instance if the binary string data
            cannot be coerced into a png, or perhaps your numpy.ndarray is
            ill-formed?
    """
    # Expand filename to be absolute
    png_filename = os.path.expanduser(filename)

    if type(numpy_data) is str:
        fp = open(png_filename, "wb")
        fp.write(numpy_data)
        fp.close()
        return png_filename

    try:
        if numpy_data.dtype.name != 'uint8':
            m = 'I'
            img = Image.fromarray(numpy_data, mode=m)
        else:
            img = Image.fromarray(numpy_data)
        img.save(png_filename)
    except Exception as e:
        raise ValueError("Could not save png file {0}.".format(png_filename))
    return png_filename

def save_collection(png_filename_base, numpy_data, start_layers_at=1):
    """
    Export a numpy array to a set of png files, with each Z-index 2D
    array as its own 2D file.

    Arguments:
        png_filename_base:     A filename template, such as "my-image-*.png"
                                which will lead to a collection of files named
                                "my-image-0.png", "my-image-1.png", etc.
        numpy_data:             The numpy array data to save to png.

    Returns:
        Array. A list of expanded filenames that hold png data.
    """
    file_ext = png_filename_base.split('.')[-1]
    if file_ext in ['png']:
        # Filename is "name*.ext", set file_base to "name*".
        file_base = '.'.join(png_filename_base.split('.')[:-1])
    else:
        # Filename is "name*", set file_base to "name*".
        # That is, extension wasn't included.
        file_base = png_filename_base
        file_ext = ".png"

    file_base_array = file_base.split('*')

    # The array of filenames to return
    output_files = []

    # Filename 0-padding
    i = start_layers_at
    for layer in numpy_data:
        layer_filename = (str(i).zfill(6)).join(file_base_array) + file_ext
        output_files.append(save(layer_filename, layer))
        i += 1

    return output_files

def load_collection(png_filename_base):
    """
    Import all files matching the filename base given with `png_filename_base`.
    Images are ordered by alphabetical order, which means that you *MUST* 0-pad
    your numbers if they span a power of ten (e.g. 0999-1000 or 09-10). This is
    handled automatically by its complementary function, `png.save_collection`.
    Also, look at how nicely these documentation lines are all the same length!

    Arguments:
        png_filename_base (str): An asterisk-wildcard string that should refer
            to all PNGs in the stack. All *s are replaced according to regular
            cmd-line expansion rules. See the 'glob' documentation for details
    Returns:
        A numpy array holding a 3D dataset
    """
    # We expect images to be indexed by their alphabetical order.
    files = glob.glob(png_filename_base)
    files.sort()

    numpy_data = []
    for f in files:
        numpy_data.append(load(f))

    return numpy.concatenate(numpy_data)


# This source file is part of Yoda.
#
# Yoda 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.
#
# Yoda 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
# Yoda. If not, see <http://www.gnu.org/licenses/gpl-3.0.html>.

import logging

from pycolorizer import Color

from yoda import find_path
from yoda import Repository
from yoda import RepositoryError
from yoda.subcommands import Subcommand

class Status(Subcommand, object):

    def setup(self, name, config, subparser):
        self.subparser = subparser
        self.logger = logging.getLogger(__name__)
        super(Status, self).setup(name, config, subparser)

    def parse(self):
        """Parse status subcommand."""
        parser = self.subparser.add_parser(
            "status",
            help="Show repositories status",
            description="Show repositories status from name.")

        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument('--all', action='store_true', help="All workspaces")
        group.add_argument('name', type=str, help="Repo name", nargs='?')

    def execute(self, args):
        """Execute status subcommand."""
        if args.name is not None:
            self.print_workspace(args.name)
        elif args.all is not None:
            self.print_all()

    def print_all(self):
        """Print all workspaces status."""
        for ws_name in self.config['workspaces']:
            self.print_workspace(ws_name)

    def print_workspace(self, name):
        """Print workspace status."""
        path_list = find_path(name, self.config)

        if len(path_list) == 0:
            self.logger.error("No matches for `%s`" % name)
            return False

        for name, path in path_list.items():
            self.print_status(name, path)

    def print_status(self, repo_name, repo_path):
        """Print repository status."""
        color = Color()
        self.logger.info(color.colored(
            "=> [%s] %s" % (repo_name, repo_path), "green"))
        try:
            repo = Repository(repo_path)
            repo.status()
        except RepositoryError as e:
            self.logger.error(e)
            pass
        print("\n")


from __future__ import absolute_import
import ndio
import requests
import os
import numpy
from io import BytesIO
import zlib
import tempfile
import blosc
import h5py
from .remote_utils import remote_utils

from .Remote import Remote
from .errors import *
import ndio.ramon as ramon
from six.moves import range
import six

from functools import wraps

try:
    import urllib.request as urllib2
except ImportError:
    import urllib2

from .neuroRemote import neuroRemote
from .neuroRemote import DEFAULT_HOSTNAME
from .neuroRemote import DEFAULT_SUFFIX
from .neuroRemote import DEFAULT_PROTOCOL
from .neuroRemote import DEFAULT_BLOCK_SIZE

from .metadata import metadata

class data(neuroRemote, metadata):
    """
    Data class with data wrappers for ndio.
    """

    def __init__(self,
                 user_token='placeholder',
                 hostname=DEFAULT_HOSTNAME,
                 protocol=DEFAULT_PROTOCOL,
                 meta_root="http://lims.neurodata.io/",
                 meta_protocol=DEFAULT_PROTOCOL, **kwargs):
        """
        Initializer for the data class.

        Arguments:
            hostname (str: "openconnecto.me"): The hostname to connect to
            protocol (str: "http"): The protocol (http or https) to use
            meta_root (str: "http://lims.neurodata.io/"): The metadata server
            meta_protocol (str: "http"): The protocol to use for the md server
            check_tokens (boolean: False): Whether functions that take `token`
                as an argument should check for the existance of that token and
                fail immediately if it is not found. This is a good idea for
                functions that take more time to complete, and might not fail
                until the very end otherwise.
            chunk_threshold (int: 1e9 / 4): The maximum size of a numpy array
                that will be uploaded in one HTTP request. If you find that
                your data requests are commonly timing out, try reducing this.
                Default is 1e9 / 4, or a 0.25GiB.
            suffix (str: "ocp"): The URL suffix to specify ndstore/microns. If
                you aren't sure what to do with this, don't specify one.
        """
        super(data, self).__init__(user_token,
                                   hostname,
                                   protocol,
                                   meta_root,
                                   meta_protocol, **kwargs)

    # SECTION:
    # Data Download

    def url(self, suffix=""):
        """
        Overloaded url method from neuroRemote.

        Arugments:
            suffix (str): url extension

        Returns:
            str: properly formatted url
        """
        return super(data, self).url() + '/sd/' + suffix

    def get_block_size(self, token, resolution=None):
        """
        Gets the block-size for a given token at a given resolution.

        Arguments:
            token (str): The token to inspect
            resolution (int : None): The resolution at which to inspect data.
                If none is specified, uses the minimum available.

        Returns:
            int[3]: The xyz blocksize.
        """
        cdims = self.get_metadata(token)['dataset']['cube_dimension']
        if resolution is None:
            resolution = min(cdims.keys())
        return cdims[str(resolution)]

    def get_image_offset(self, token, resolution=0):
        """
        Gets the image offset for a given token at a given resolution. For
        instance, the `kasthuri11` dataset starts at (0, 0, 1), so its 1850th
        slice is slice 1850, not 1849. When downloading a full dataset, the
        result of this function should be your x/y/z starts.

        Arguments:
            token (str): The token to inspect
            resolution (int : 0): The resolution at which to gather the offset

        Returns:
            int[3]: The origin of the dataset, as a list
        """
        info = self.get_proj_info(token)
        res = str(resolution)
        if res not in info['dataset']['offset']:
            raise RemoteDataNotFoundError("Resolution " + res +
                                          " is not available.")
        return info['dataset']['offset'][str(resolution)]

    def get_xy_slice(self, token, channel,
                     x_start, x_stop,
                     y_start, y_stop,
                     z_index,
                     resolution=0):
        """
        Return a binary-encoded, decompressed 2d image. You should
        specify a 'token' and 'channel' pair.  For image data, users
        should use the channel 'image.'

        Arguments:
            token (str): Token to identify data to download
            channel (str): Channel
            resolution (int): Resolution level
            Q_start (int):` The lower bound of dimension 'Q'
            Q_stop (int): The upper bound of dimension 'Q'
            z_index (int): The z-slice to image

        Returns:
            str: binary image data
        """
        vol = self.get_cutout(token, channel, x_start, x_stop, y_start,
                              y_stop, z_index, z_index + 1, resolution)

        vol = numpy.squeeze(vol)  # 3D volume to 2D slice

        return vol

    def get_image(self, token, channel,
                  x_start, x_stop,
                  y_start, y_stop,
                  z_index,
                  resolution=0):
        """
        Alias for the `get_xy_slice` function for backwards compatibility.
        """
        return self.get_xy_slice(token, channel,
                                 x_start, x_stop,
                                 y_start, y_stop,
                                 z_index,
                                 resolution)

    def get_volume(self, token, channel,
                   x_start, x_stop,
                   y_start, y_stop,
                   z_start, z_stop,
                   resolution=1,
                   block_size=DEFAULT_BLOCK_SIZE,
                   neariso=False):
        """
        Get a RAMONVolume volumetric cutout from the neurodata server.

        Arguments:
            token (str): Token to identify data to download
            channel (str): Channel
            resolution (int): Resolution level
            Q_start (int): The lower bound of dimension 'Q'
            Q_stop (int): The upper bound of dimension 'Q'
            block_size (int[3]): Block size of this dataset
            neariso (bool : False): Passes the 'neariso' param to the cutout.
                If you don't know what this means, ignore it!

        Returns:
            ndio.ramon.RAMONVolume: Downloaded data.
        """
        size = (x_stop - x_start) * (y_stop - y_start) * (z_stop - z_start)
        volume = ramon.RAMONVolume()
        volume.xyz_offset = [x_start, y_start, z_start]
        volume.resolution = resolution

        volume.cutout = self.get_cutout(token, channel, x_start,
                                        x_stop, y_start, y_stop,
                                        z_start, z_stop,
                                        resolution=resolution,
                                        block_size=block_size,
                                        neariso=neariso)
        return volume

    def get_cutout(self, token, channel,
                   x_start, x_stop,
                   y_start, y_stop,
                   z_start, z_stop,
                   t_start=0, t_stop=1,
                   resolution=1,
                   block_size=DEFAULT_BLOCK_SIZE,
                   neariso=False):
        """
        Get volumetric cutout data from the neurodata server.

        Arguments:
            token (str): Token to identify data to download
            channel (str): Channel
            resolution (int): Resolution level
            Q_start (int): The lower bound of dimension 'Q'
            Q_stop (int): The upper bound of dimension 'Q'
            block_size (int[3]): Block size of this dataset. If not provided,
                ndio uses the metadata of this tokenchannel to set. If you find
                that your downloads are timing out or otherwise failing, it may
                be wise to start off by making this smaller.
            neariso (bool : False): Passes the 'neariso' param to the cutout.
                If you don't know what this means, ignore it!

        Returns:
            numpy.ndarray: Downloaded data.
        """
        if block_size is None:
            # look up block size from metadata
            block_size = self.get_block_size(token, resolution)

        origin = self.get_image_offset(token, resolution)

        # If z_stop - z_start is < 16, backend still pulls minimum 16 slices
        if (z_stop - z_start) < 16:
            z_slices = 16
        else:
            z_slices = z_stop - z_start

        # Calculate size of the data to be downloaded.
        size = (x_stop - x_start) * (y_stop - y_start) * z_slices * 4

        # Switch which download function to use based on which libraries are
        # available in this version of python.
        if six.PY2:
            dl_func = self._get_cutout_blosc_no_chunking
        elif six.PY3:
            dl_func = self._get_cutout_no_chunking
        else:
            raise ValueError("Invalid Python version.")

        if size < self._chunk_threshold:
            vol = dl_func(token, channel, resolution,
                          x_start, x_stop,
                          y_start, y_stop,
                          z_start, z_stop,
                          t_start, t_stop,
                          neariso=neariso)
            vol = numpy.rollaxis(vol, 1)
            vol = numpy.rollaxis(vol, 2)
            return vol
        else:
            from ndio.utils.parallel import block_compute
            blocks = block_compute(x_start, x_stop,
                                   y_start, y_stop,
                                   z_start, z_stop,
                                   origin, block_size)

            vol = numpy.zeros(((z_stop - z_start),
                               (y_stop - y_start),
                               (x_stop - x_start)))
            for b in blocks:

                data = dl_func(token, channel, resolution,
                               b[0][0], b[0][1],
                               b[1][0], b[1][1],
                               b[2][0], b[2][1],
                               0, 1,
                               neariso=neariso)

                if b == blocks[0]:  # first block
                    vol = numpy.zeros(((z_stop - z_start),
                                       (y_stop - y_start),
                                       (x_stop - x_start)), dtype=data.dtype)

                vol[b[2][0] - z_start: b[2][1] - z_start,
                    b[1][0] - y_start: b[1][1] - y_start,
                    b[0][0] - x_start: b[0][1] - x_start] = data

            vol = numpy.rollaxis(vol, 1)
            vol = numpy.rollaxis(vol, 2)
            return vol

    def _get_cutout_no_chunking(self, token, channel, resolution,
                                x_start, x_stop, y_start, y_stop,
                                z_start, z_stop, t_start, t_stop,
                                neariso=False):
        url = self.url() + "{}/{}/hdf5/{}/{},{}/{},{}/{},{}/{},{}".format(
            token, channel, resolution,
            x_start, x_stop,
            y_start, y_stop,
            z_start, z_stop,
            t_start, t_stop,
        )

        if neariso:
            url += "neariso/"

        req = self.remote_utils.get_url(url)
        if req.status_code is not 200:
            raise IOError("Bad server response for {}: {}: {}".format(
                          url,
                          req.status_code,
                          req.text))

        with tempfile.NamedTemporaryFile() as tmpfile:
            tmpfile.write(req.content)
            tmpfile.seek(0)
            h5file = h5py.File(tmpfile.name, "r")
            return h5file.get(channel).get('CUTOUT')[:]
        raise IOError("Failed to make tempfile.")

    def _get_cutout_blosc_no_chunking(self, token, channel, resolution,
                                      x_start, x_stop, y_start, y_stop,
                                      z_start, z_stop, t_start, t_stop,
                                      neariso=False):

        url = self.url() + "{}/{}/blosc/{}/{},{}/{},{}/{},{}/{},{}/".format(
            token, channel, resolution,
            x_start, x_stop,
            y_start, y_stop,
            z_start, z_stop,
            t_start, t_stop,
        )

        if neariso:
            url += "neariso/"

        req = self.remote_utils.get_url(url)
        if req.status_code is not 200:
            raise IOError("Bad server response for {}: {}: {}".format(
                          url,
                          req.status_code,
                          req.text))

        # This will need modification for >3D blocks
        return blosc.unpack_array(req.content)[0]

        raise IOError("Failed to retrieve blosc cutout.")

    # SECTION:
    # Data Upload

    def post_cutout(self, token, channel,
                    x_start,
                    y_start,
                    z_start,
                    data,
                    resolution=0):
        """
        Post a cutout to the server.

        Arguments:
            token (str)
            channel (str)
            x_start (int)
            y_start (int)
            z_start (int)
            data (numpy.ndarray): A numpy array of data. Pass in (x, y, z)
            resolution (int : 0): Resolution at which to insert the data

        Returns:
            bool: True on success

        Raises:
            RemoteDataUploadError: if there's an issue during upload.
        """
        datatype = self.get_proj_info(token)['channels'][channel]['datatype']
        if data.dtype.name != datatype:
            data = data.astype(datatype)

        data = numpy.rollaxis(data, 1)
        data = numpy.rollaxis(data, 2)

        if six.PY3 or data.nbytes > 1.5e9:
            ul_func = self._post_cutout_no_chunking_npz
        else:
            ul_func = self._post_cutout_no_chunking_blosc

        if data.size < self._chunk_threshold:
            return ul_func(token, channel, x_start,
                           y_start, z_start, data,
                           resolution)

        return self._post_cutout_with_chunking(token, channel,
                                               x_start, y_start, z_start, data,
                                               resolution, ul_func)

    def _post_cutout_with_chunking(self, token, channel, x_start,
                                   y_start, z_start, data,
                                   resolution, ul_func):
        # must chunk first
        from ndio.utils.parallel import block_compute
        blocks = block_compute(x_start, x_start + data.shape[2],
                               y_start, y_start + data.shape[1],
                               z_start, z_start + data.shape[0])
        for b in blocks:
            # data coordinate relative to the size of the arra
            subvol = data[b[2][0] - z_start: b[2][1] - z_start,
                          b[1][0] - y_start: b[1][1] - y_start,
                          b[0][0] - x_start: b[0][1] - x_start]
            # upload the chunk:
            # upload coordinate relative to x_start, y_start, z_start
            ul_func(token, channel, b[0][0],
                    b[1][0], b[2][0], subvol,
                    resolution)
        return True

    def _post_cutout_no_chunking_npz(self, token, channel,
                                     x_start, y_start, z_start,
                                     data, resolution):

        data = numpy.expand_dims(data, axis=0)
        tempfile = BytesIO()
        numpy.save(tempfile, data)
        compressed = zlib.compress(tempfile.getvalue())

        url = self.url("{}/{}/npz/{}/{},{}/{},{}/{},{}/".format(
            token, channel,
            resolution,
            x_start, x_start + data.shape[3],
            y_start, y_start + data.shape[2],
            z_start, z_start + data.shape[1]
        ))

        req = self.remote_utils.post_url(url, data=compressed, headers={
            'Content-Type': 'application/octet-stream'
        })

        if req.status_code is not 200:
            raise RemoteDataUploadError(req.text)
        else:
            return True

    def _post_cutout_no_chunking_blosc(self, token, channel,
                                       x_start, y_start, z_start,
                                       data, resolution):
        """
        Accepts data in zyx. !!!
        """
        data = numpy.expand_dims(data, axis=0)
        blosc_data = blosc.pack_array(data)
        url = self.url("{}/{}/blosc/{}/{},{}/{},{}/{},{}/0,0/".format(
            token, channel,
            resolution,
            x_start, x_start + data.shape[3],
            y_start, y_start + data.shape[2],
            z_start, z_start + data.shape[1]
        ))

        req = self.remote_utils.post_url(url, data=blosc_data, headers={
            'Content-Type': 'application/octet-stream'
        })

        if req.status_code is not 200:
            raise RemoteDataUploadError(req.text)
        else:
            return True


from __future__ import absolute_import
import numpy
import os
import glob
import tifffile as tiff

def load(tiff_filename):
    """
    Import a TIFF file into a numpy array.

    Arguments:
        tiff_filename:  A string filename of a TIFF datafile

    Returns:
        A numpy array with data from the TIFF file
    """
    # Expand filename to be absolute
    tiff_filename = os.path.expanduser(tiff_filename)

    try:
        img = tiff.imread(tiff_filename)
    except Exception as e:
        raise ValueError("Could not load file {0} for conversion."
                         .format(tiff_filename))
        raise

    return numpy.array(img)

def save(tiff_filename, numpy_data):
    """
    Export a numpy array to a TIFF file.

    Arguments:
        tiff_filename:  A filename to which to save the TIFF data
        numpy_data:     The numpy array to save to TIFF

    Returns:
        String. The expanded filename that now holds the TIFF data
    """
    # Expand filename to be absolute
    tiff_filename = os.path.expanduser(tiff_filename)

    if type(numpy_data) is str:
        fp = open(png_filename, "wb")
        fp.write(numpy_data)
        fp.close()
        return png_filename

    try:
        img = tiff.imsave(tiff_filename, numpy_data)
    except Exception as e:
        raise ValueError("Could not save TIFF file {0}.".format(tiff_filename))

    return tiff_filename

def save_collection(tiff_filename_base, numpy_data, start_layers_at=1):
    """
    Export a numpy array to a set of TIFF files, with each Z-index 2D
    array as its own 2D file.

    Arguments:
        tiff_filename_base:     A filename template, such as "my-image-*.tiff"
                                which will lead to a collection of files named
                                "my-image-0.tiff", "my-image-1.tiff", etc.
        numpy_data:             The numpy array data to save to TIFF.

    Returns:
        Array. A list of expanded filenames that hold TIFF data.
    """
    file_ext = tiff_filename_base.split('.')[-1]
    if file_ext in ['tif', 'tiff']:
        # Filename is "name*.tif[f]", set file_base to "name*".
        file_base = '.'.join(tiff_filename_base.split('.')[:-1])
    else:
        # Filename is "name*", set file_base to "name*".
        # That is, extension wasn't included.
        file_base = tiff_filename_base
        file_ext = ".tiff"

    file_base_array = file_base.split('*')

    # The array of filenames to return
    output_files = []

    # Filename 0-padding
    i = start_layers_at
    for layer in numpy_data:
        layer_filename = (str(i).zfill(6)).join(file_base_array) + file_ext
        output_files.append(save(layer_filename, layer))
        i += 1

    return output_files

def load_tiff_multipage(tiff_filename, dtype='float32'):
    """
    Load a multipage tiff into a single variable in x,y,z format.

    Arguments:
        tiff_filename:     Filename of source data
        dtype:             data type to use for the returned tensor

    Returns:
        Array containing contents from input tiff file in xyz order
    """
    if not os.path.isfile(tiff_filename):
        raise RuntimeError('could not find file "%s"' % tiff_filename)

    # load the data from multi-layer TIF files
    data = tiff.imread(tiff_filename)

    im = []

    while True:

        Xi = numpy.array(data, dtype=dtype)
        if Xi.ndim == 2:
            Xi = Xi[numpy.newaxis, ...]  # add slice dimension
        im.append(Xi)

        try:
            data.seek(data.tell()+1)
        except EOFError:
            break  # this just means hit end of file (not really an error)

    im = numpy.concatenate(im, axis=0)  # list of 2d -> tensor
    im = numpy.rollaxis(im, 1)
    im = numpy.rollaxis(im, 2)

    return im

def load_collection(tiff_filename_base):
    """
    Import all files matching the filename base given via `tiff_filename_base`.
    Images are ordered by alphabetical order, which means that you *MUST* 0-pad
    your numbers if they span a power of ten (e.g. 0999-1000 or 09-10). This is
    handled automatically by the complement function, `save_collection`.
    Also, look at how nicely these documentation lines are all the same length!

    Arguments:
        tiff_filename_base:     An asterisk-wildcard string that should refer
                                to all TIFFs in the stack. All * are replaced
                                according to command-line expansion rules.

    Returns:
        A numpy array holding a 3D dataset
    """
    # We expect images to be indexed by their alphabetical order.
    files = glob.glob(tiff_filename_base)
    files.sort()

    numpy_data = []
    for f in files:
        numpy_data.append(load(f))

    return numpy.concatenate(numpy_data)


# This source file is part of Yoda.
#
# Yoda 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.
#
# Yoda 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
# Yoda. If not, see <http://www.gnu.org/licenses/gpl-3.0.html>.

import os.path
import yaml

class Config(dict):

    config_file = None

    def __init__(self, config_file, *args, **kwargs):
        """Workspace object initialization."""
        self.config_file = config_file
        super(Config, self).__init__(*args, **kwargs)
        if not os.path.exists(config_file) or config_file is None:
            self.update({"workspaces": {}})
            self.write()
        else:
            file = open(self.config_file)
            config = yaml.safe_load(file.read())
            file.close()
            if config is not None:
                self.update(config)

    def __delitem__(self, key):
        super(Config, self).__delitem__(key)
        self.write()

    def __setitem__(self, key, value):
        super(Config, self).__setitem__(key, value)
        self.write()

    def update(self, *args, **kwargs):
        super(Config, self).update(*args, **kwargs)
        self.write()

    def write(self):
        """
        Write config in configuration file.
        Data must me a dict.
        """
        file = open(self.config_file, "w+")
        file.write(yaml.dump(dict(self), default_flow_style=False))
        file.close()


# This source file is part of Yoda.
#
# Yoda 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.
#
# Yoda 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
# Yoda. If not, see <http://www.gnu.org/licenses/gpl-3.0.html>.

from yoda.adapter import Abstract

class Bzr(Abstract):
    """Bzr Adapter."""
    executable = "bzr"

    def status(self):
        """Show bzr status."""
        return self.exec_on_path("%s status" % self.executable)

    def update(self):
        """Update repository."""
        return self.exec_on_path("%s pull" % self.executable)

    def clone(self, url):
        """Clone repository from url."""
        return self.execute("%s branch %s %s" % (self.executable,
                                                 url, self.path))

    def show(self):
        """Show repository details."""
        raise NotImplemented("TODO: Not implemented")


#!/usr/bin/env python
# This source file is part of Yoda.
#
# Yoda 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.
#
# Yoda 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
# Yoda. If not, see <http://www.gnu.org/licenses/gpl-3.0.html>.

import pkg_resources

def get_version():
    """Get version from package resources."""
    requirement = pkg_resources.Requirement.parse("yoda")
    provider = pkg_resources.get_provider(requirement)
    return provider.version


import compago

app = compago.Application()
app.add_option('--debug', dest='debug', action='store_true', default=False)

if app.args['debug']:
    import logging
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s: %(message)s')

@app.command
def simple():
    '''This is a simple command that takes no args.'''
    print 'Doing something simple!'

@app.command
def positional_args(one, two):
    '''This command takes two (required) positional arguments.'''
    # The "positional" arguments will be required. On the command line,
    # the user will call this command as:
    # complex_example.py positional_args <arg1> <arg2>
    print 'Positional args: one=%s, two=%s' % (one, two)

@app.command
def keyword_options(three='three', four=False):
    '''This command takes some keyword options.'''
    # Keyword arguments in your function def are parsed as --options on the
    # command line. For example, this command will be called thusly:
    # complex_example.py keyword_options --three=33333 --four
    # Note: keywords with boolean defaults will be toggle options
    print 'Options: three=%s, four=%s' % (three, four)

@app.command
def mix_and_match(name, greeting='Hello', yell=False):
    '''Mixing and matching positional args and keyword options.'''
    say = '%s, %s' % (greeting, name)
    if yell:
        print '%s!' % say.upper()
    else:
        print '%s.' % say

@app.option('-G', '--greeting', dest='greeting', default='Hello')
@app.option('--yell', dest='yell', action='store_true', default=False)
def option_decorator(name, greeting, yell):
    '''Same as mix_and_match, but using the @option decorator.'''
    # Use the @option decorator when you need more control over the
    # command line options.
    say = '%s, %s' % (greeting, name)
    if yell:
        print '%s!' % say.upper()
    else:
        print '%s.' % say

if __name__ == '__main__': app.run()


from __future__ import absolute_import
import nibabel as nib
import numpy
import os
import glob

def load(nifti_filename):
    """
    Import a nifti file into a numpy array. TODO:  Currently only
    transfers raw data for compatibility with annotation and ND formats

    Arguments:
        nifti_filename (str):  A string filename of a nifti datafile

    Returns:
        A numpy array with data from the nifti file
    """
    # Expand filename to be absolute
    nifti_filename = os.path.expanduser(nifti_filename)

    try:
        data = nib.load(nifti_filename)
        img = data.get_data()

    except Exception as e:
        raise ValueError("Could not load file {0} for conversion."
                         .format(nifti_filename))
        raise

    return img

def save(nifti_filename, numpy_data):
    """
    Export a numpy array to a nifti file.  TODO: currently using dummy
    headers and identity matrix affine transform. This can be expanded.

    Arguments:
        nifti_filename (str): A filename to which to save the nifti data
        numpy_data (numpy.ndarray): The numpy array to save to nifti

    Returns:
        String. The expanded filename that now holds the nifti data
    """
    # Expand filename to be absolute
    nifti_filename = os.path.expanduser(nifti_filename)

    try:
        nifti_img = nib.Nifti1Image(numpy_data, numpy.eye(4))
        nib.save(nifti_img, nifti_filename)

    except Exception as e:
        raise ValueError("Could not save file {0}.".format(nifti_filename))
    return nifti_filename


from __future__ import absolute_import
import ndio
import requests
import os
import numpy
from io import BytesIO
import zlib
import tempfile
import blosc
import h5py
from .remote_utils import remote_utils

from .Remote import Remote
from .errors import *
import ndio.ramon as ramon
from six.moves import range
import six

from functools import wraps

try:
    import urllib.request as urllib2
except ImportError:
    import urllib2

DEFAULT_HOSTNAME = "openconnecto.me"
DEFAULT_SUFFIX = "nd"
DEFAULT_PROTOCOL = "https"  # originally was https in master
DEFAULT_BLOCK_SIZE = (1024, 1024, 16)

class neuroRemote(Remote):
    """
    The neuroRemote remote, for interfacing with ndstore, ndlims, and friends.
    """

    # SECTION:
    # Enumerables
    IMAGE = IMG = 'image'
    ANNOTATION = ANNO = 'annotation'

    def __init__(self,
                 user_token='placeholder',
                 hostname=DEFAULT_HOSTNAME,
                 protocol=DEFAULT_PROTOCOL,
                 meta_root="http://lims.neurodata.io/",
                 meta_protocol=DEFAULT_PROTOCOL, **kwargs):
        """
        Initializer for the neuroRemote remote class.

        Arguments:
            hostname (str: "openconnecto.me"): The hostname to connect to
            protocol (str: "http"): The protocol (http or https) to use
            meta_root (str: "http://lims.neurodata.io/"): The metadata server
            meta_protocol (str: "http"): The protocol to use for the md server
            check_tokens (boolean: False): Whether functions that take `token`
                as an argument should check for the existance of that token and
                fail immediately if it is not found. This is a good idea for
                functions that take more time to complete, and might not fail
                until the very end otherwise.
            chunk_threshold (int: 1e9 / 4): The maximum size of a numpy array
                that will be uploaded in one HTTP request. If you find that
                your data requests are commonly timing out, try reducing this.
                Default is 1e9 / 4, or a 0.25GiB.
            suffix (str: "ocp"): The URL suffix to specify ndstore/microns. If
                you aren't sure what to do with this, don't specify one.
        """
        self._check_tokens = kwargs.get('check_tokens', False)
        self._chunk_threshold = kwargs.get('chunk_threshold', 1E9 / 4)
        self._ext = kwargs.get('suffix', DEFAULT_SUFFIX)
        self._known_tokens = []
        self._user_token = user_token

        # Prepare meta url
        self.meta_root = meta_root
        if not self.meta_root.endswith('/'):
            self.meta_root = self.meta_root + "/"
        if self.meta_root.startswith('https'):
            self.meta_root = self.meta_root[self.meta_root.index('://') + 3:]
        self.meta_protocol = meta_protocol

        self.remote_utils = remote_utils(self._user_token)
        super(neuroRemote, self).__init__(hostname, protocol)

    # SECTION:
    # Legacy code
    def getURL(self, url):
        """
        Old getURL method. Use remote_utils.get_url() instead.

        Arguments:
            url (str): The url to send get request to.

        Returns:
            str: result
        """
        return self.remote_utils.get_url(url)

    # SECTION:
    # Decorators
    def _check_token(f):
        @wraps(f)
        def wrapped(self, *args, **kwargs):
            if self._check_tokens:
                if 'token' in kwargs:
                    token = kwargs['token']
                else:
                    token = args[0]
                if token not in self._known_tokens:
                    if self.ping('{}/info/'.format(token)) != 200:
                        raise RemoteDataNotFoundError("Bad token {}".format(
                                                      token))
                    else:
                        self._known_tokens.append(token)
            return f(self, *args, **kwargs)
        return wrapped

    # SECTION:
    # neuroRemote Utilities
    def ping(self, suffix='public_tokens/'):
        """
        Return the status-code of the API (estimated using the public-tokens
        lookup page).

        Arguments:
            suffix (str : 'public_tokens/'): The url endpoint to check

        Returns:
            int: status code
        """
        return self.remote_utils.ping(super(neuroRemote, self).url(), suffix)

    def url(self, suffix=""):
        """
        Return a constructed URL, appending an optional suffix (uri path).

        Arguments:
            suffix (str : ""): The suffix to append to the end of the URL

        Returns:
            str: The complete URL
        """
        return super(neuroRemote,
                     self).url('{}/'.format(self._ext) + suffix)

    def meta_url(self, suffix=""):
        """
        Return a constructed URL, appending an optional suffix (uri path),
        for the metadata server. (Should be temporary, until the LIMS shim
        is fixed.)

        Arguments:
            suffix (str : ""): The suffix to append to the end of the URL

        Returns:
            str: The complete URL
        """
        return self.meta_protocol + "://" + self.meta_root + suffix

    def __repr__(self):
        """
        Return a string representation that can be used to reproduce this
        instance. `eval(repr(this))` should return an identical copy.

        Arguments:
            None

        Returns:
            str: Representation of reproducible instance.
        """
        return "ndio.remote.neuroRemote('{}', '{}')".format(
            self.hostname,
            self.protocol,
            self.meta_url,
            self.meta_protocol
        )

    # SECTION:
    # ID Manipulation

    @_check_token
    def reserve_ids(self, token, channel, quantity):
        """
        Requests a list of next-available-IDs from the server.

        Arguments:
            quantity (int): The number of IDs to reserve

        Returns:
            int[quantity]: List of IDs you've been granted
        """
        quantity = str(quantity)
        url = self.url("{}/{}/reserve/{}/".format(token, channel, quantity))
        req = self.remote_utils.get_url(url)
        if req.status_code is not 200:
            raise RemoteDataNotFoundError('Invalid req: ' + req.status_code)
        out = req.json()
        return [out[0] + i for i in range(out[1])]

    @_check_token
    def merge_ids(self, token, channel, ids, delete=False):
        """
        Call the restful endpoint to merge two RAMON objects into one.

        Arguments:
            token (str): The token to inspect
            channel (str): The channel to inspect
            ids (int[]): the list of the IDs to merge
            delete (bool : False): Whether to delete after merging.

        Returns:
            json: The ID as returned by ndstore
        """
        url = self.url() + "/merge/{}/".format(','.join([str(i) for i in ids]))
        req = self.remote_utils.get_url(url)
        if req.status_code is not 200:
            raise RemoteDataUploadError('Could not merge ids {}'.format(
                                        ','.join([str(i) for i in ids])))
        if delete:
            self.delete_ramon(token, channel, ids[1:])
        return True

    @_check_token
    def create_channels(self, dataset, token, new_channels_data):
        """
        Creates channels given a dictionary in 'new_channels_data'
        , 'dataset' name, and 'token' (project) name.

        Arguments:
            token (str): Token to identify project
            dataset (str): Dataset name to identify dataset to download from
            new_channels_data (dict): New channel data to upload into new
                channels

        Returns:
            bool: Process completed succesfully or not
        """
        channels = {}
        for channel_new in new_channels_data:

            self._check_channel(channel_new.name)

            if channel_new.channel_type not in ['image', 'annotation']:
                raise ValueError('Channel type must be ' +
                                 'neuroRemote.IMAGE or ' +
                                 'neuroRemote.ANNOTATION.')

            if channel_new.readonly * 1 not in [0, 1]:
                raise ValueError("readonly must be 0 (False) or 1 (True).")

            channels[channel_new.name] = {
                "channel_name": channel_new.name,
                "channel_type": channel_new.channel_type,
                "datatype": channel_new.dtype,
                "readonly": channel_new.readonly * 1
            }
        req = requests.post(self.url("/{}/project/".format(dataset) +
                                     "{}".format(token)),
                            json={"channels": {channels}}, verify=False)

        if req.status_code is not 201:
            raise RemoteDataUploadError('Could not upload {}'.format(req.text))
        else:
            return True

    # Propagation

    # @_check_token
    def propagate(self, token, channel):
        """
        Kick off the propagate function on the remote server.

        Arguments:
            token (str): The token to propagate
            channel (str): The channel to propagate

        Returns:
            boolean: Success
        """
        if self.get_propagate_status(token, channel) != u'0':
            return
        url = self.url('sd/{}/{}/setPropagate/1/'.format(token, channel))
        req = self.remote_utils.get_url(url)
        if req.status_code is not 200:
            raise RemoteDataUploadError('Propagate fail: {}'.format(req.text))
        return True

    # @_check_token
    def get_propagate_status(self, token, channel):
        """
        Get the propagate status for a token/channel pair.

        Arguments:
            token (str): The token to check
            channel (str): The channel to check

        Returns:
            str: The status code
        """
        url = self.url('sd/{}/{}/getPropagate/'.format(token, channel))
        req = self.remote_utils.get_url(url)
        if req.status_code is not 200:
            raise ValueError('Bad pair: {}/{}'.format(token, channel))
        return req.text


from __future__ import absolute_import
import ndio
import requests
import os
import numpy
from io import BytesIO
import zlib
import tempfile
import blosc
import h5py
from .remote_utils import remote_utils

from .Remote import Remote
from .errors import *
import ndio.ramon as ramon
from six.moves import range
import six

from functools import wraps

try:
    import urllib.request as urllib2
except ImportError:
    import urllib2

from .neuroRemote import neuroRemote as nd
from .neuroRemote import DEFAULT_HOSTNAME
from .neuroRemote import DEFAULT_SUFFIX
from .neuroRemote import DEFAULT_PROTOCOL
from .neuroRemote import DEFAULT_BLOCK_SIZE

class resources(nd):
    """
    Resource api wrapper class.
    """

    def __init__(self,
                 user_token='placeholder',
                 hostname=DEFAULT_HOSTNAME,
                 protocol=DEFAULT_PROTOCOL,
                 meta_root="http://lims.neurodata.io/",
                 meta_protocol=DEFAULT_PROTOCOL, **kwargs):
        """
        Initializer for resources class.

        Arguments:
            hostname (str: "openconnecto.me"): The hostname to connect to
            protocol (str: "http"): The protocol (http or https) to use
            meta_root (str: "http://lims.neurodata.io/"): The metadata server
            meta_protocol (str: "http"): The protocol to use for the md server
            check_tokens (boolean: False): Whether functions that take `token`
                as an argument should check for the existance of that token and
                fail immediately if it is not found. This is a good idea for
                functions that take more time to complete, and might not fail
                until the very end otherwise.
            chunk_threshold (int: 1e9 / 4): The maximum size of a numpy array
                that will be uploaded in one HTTP request. If you find that
                your data requests are commonly timing out, try reducing this.
                Default is 1e9 / 4, or a 0.25GiB.
            suffix (str: "ocp"): The URL suffix to specify ndstore/microns. If
                you aren't sure what to do with this, don't specify one.
        """
        super(resources, self).__init__(user_token,
                                        hostname,
                                        protocol,
                                        meta_root,
                                        meta_protocol, **kwargs)

    def create_project(self,
                       project_name,
                       dataset_name,
                       hostname,
                       is_public,
                       s3backend=0,
                       kvserver='localhost',
                       kvengine='MySQL',
                       mdengine='MySQL',
                       description=''):
        """
        Creates a project with the given parameters.

        Arguments:
            project_name (str): Project name
            dataset_name (str): Dataset name project is based on
            hostname (str): Hostname
            s3backend (str): S3 region to save the data in
            is_public (int): 1 is public. 0 is not public.
            kvserver (str): Server to store key value pairs in
            kvengine (str): Database to store key value pairs in
            mdengine (str): ???
            description (str): Description for your project

        Returns:
            bool: True if project created, false if not created.
        """
        url = self.url() + "/resource/dataset/{}".format(
            dataset_name) + "/project/{}/".format(project_name)

        json = {
            "project_name": project_name,
            "host": hostname,
            "s3backend": s3backend,
            "public": is_public,
            "kvserver": kvserver,
            "kvengine": kvengine,
            "mdengine": mdengine,
            "project_description": description
        }

        req = self.remote_utils.post_url(url, json=json)

        if req.status_code is not 201:
            raise RemoteDataUploadError('Could not upload {}'.format(req))
        if req.content == "" or req.content == b'':

            return True
        else:
            return False

    def get_project(self, project_name, dataset_name):
        """
        Get details regarding a project given its name and dataset its linked
        to.

        Arguments:
            project_name (str): Project name
            dataset_name (str): Dataset name project is based on

        Returns:
            dict: Project info
        """
        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/{}/".format(project_name)
        req = self.remote_utils.get_url(url)

        if req.status_code is not 200:
            raise RemoteDataNotFoundError('Could not find {}'.format(req.text))
        else:
            return req.json()

    def delete_project(self, project_name, dataset_name):
        """
        Deletes a project once given its name and its related dataset.

        Arguments:
            project_name (str): Project name
            dataset_name (str): Dataset name project is based on

        Returns:
            bool: True if project deleted, False if not.
        """
        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/{}/".format(project_name)
        req = self.remote_utils.delete_url(url)

        if req.status_code is not 204:
            raise RemoteDataUploadError('Could not delete {}'.format(req.text))
        if req.content == "" or req.content == b'':
            return True
        else:
            return False

    def list_projects(self, dataset_name):
        """
        Lists a set of projects related to a dataset.

        Arguments:
            dataset_name (str): Dataset name to search projects for

        Returns:
            dict: Projects found based on dataset query
        """
        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/"

        req = self.remote_utils.get_url(url)

        if req.status_code is not 200:
            raise RemoteDataNotFoundError('Could not find {}'.format(req.text))
        else:
            return req.json()

    def create_token(self,
                     token_name,
                     project_name,
                     dataset_name,
                     is_public):
        """
        Creates a token with the given parameters.
        Arguments:
            project_name (str): Project name
            dataset_name (str): Dataset name project is based on
            token_name (str): Token name
            is_public (int): 1 is public. 0 is not public
        Returns:
            bool: True if project created, false if not created.
        """
        url = self.url() + '/nd/resource/dataset/{}'.format(
            dataset_name) + '/project/{}'.format(project_name) + \
            '/token/{}/'.format(token_name)

        json = {
            "token_name": token_name,
            "public": is_public
        }

        req = self.remote_utils.post_url(url, json=json)

        if req.status_code is not 201:
            raise RemoteDataUploadError('Cout not upload {}:'.format(req.text))
        if req.content == "" or req.content == b'':
            return True
        else:
            return False

    def get_token(self,
                  token_name,
                  project_name,
                  dataset_name):
        """
        Get a token with the given parameters.
        Arguments:
            project_name (str): Project name
            dataset_name (str): Dataset name project is based on
            token_name (str): Token name
        Returns:
            dict: Token info
        """
        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/{}".format(project_name)\
            + "/token/{}/".format(token_name)
        req = self.remote_utils.get_url(url)

        if req.status_code is not 200:
            raise RemoteDataUploadError('Could not find {}'.format(req.text))
        else:
            return req.json()

    def delete_token(self,
                     token_name,
                     project_name,
                     dataset_name):
        """
        Delete a token with the given parameters.
        Arguments:
            project_name (str): Project name
            dataset_name (str): Dataset name project is based on
            token_name (str): Token name
            channel_name (str): Channel name project is based on
        Returns:
            bool: True if project deleted, false if not deleted.
        """
        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/{}".format(project_name)\
            + "/token/{}/".format(token_name)
        req = self.remote_utils.delete_url(url)

        if req.status_code is not 204:
            raise RemoteDataUploadError("Could not delete {}".format(req.text))
        if req.content == "" or req.content == b'':
            return True
        else:
            return False

    def list_tokens(self):
        """
        Lists a set of tokens that are public in Neurodata.
        Arguments:
        Returns:
            dict: Public tokens found in Neurodata
        """
        url = self.url() + "/nd/resource/public/token/"
        req = self.remote_utils.get_url(url)

        if req.status_code is not 200:
            raise RemoteDataNotFoundError('Coud not find {}'.format(req.text))
        else:
            return req.json()

    def create_dataset(self,
                       name,
                       x_img_size,
                       y_img_size,
                       z_img_size,
                       x_vox_res,
                       y_vox_res,
                       z_vox_res,
                       x_offset=0,
                       y_offset=0,
                       z_offset=0,
                       scaling_levels=0,
                       scaling_option=0,
                       dataset_description="",
                       is_public=0):
        """
        Creates a dataset.

        Arguments:
            name (str): Name of dataset
            x_img_size (int): max x coordinate of image size
            y_img_size (int): max y coordinate of image size
            z_img_size (int): max z coordinate of image size
            x_vox_res (float): x voxel resolution
            y_vox_res (float): y voxel resolution
            z_vox_res (float): z voxel resolution
            x_offset (int): x offset amount
            y_offset (int): y offset amount
            z_offset (int): z offset amount
            scaling_levels (int): Level of resolution scaling
            scaling_option (int): Z slices is 0 or Isotropic is 1
            dataset_description (str): Your description of the dataset
            is_public (int): 1 'true' or 0 'false' for viewability of data set
                in public

        Returns:
            bool: True if dataset created, False if not
        """
        url = self.url() + "/resource/dataset/{}".format(name)
        json = {
            "dataset_name": name,
            "ximagesize": x_img_size,
            "yimagesize": y_img_size,
            "zimagesize": z_img_size,
            "xvoxelres": x_vox_res,
            "yvoxelres": y_vox_res,
            "zvoxelres": z_vox_res,
            "xoffset": x_offset,
            "yoffset": y_offset,
            "zoffset": z_offset,
            "scalinglevels": scaling_levels,
            "scalingoption": scaling_option,
            "dataset_description": dataset_description,
            "public": is_public
        }
        req = self.remote_utils.post_url(url, json=json)

        if req.status_code is not 201:
            raise RemoteDataUploadError('Could not upload {}'.format(req.text))
        if req.content == "" or req.content == b'':
            return True
        else:
            return False

    def get_dataset(self, name):
        """
        Returns info regarding a particular dataset.

        Arugments:
            name (str): Dataset name

        Returns:
            dict: Dataset information
        """
        url = self.url() + "/resource/dataset/{}".format(name)
        req = self.remote_utils.get_url(url)

        if req.status_code is not 200:
            raise RemoteDataNotFoundError('Could not find {}'.format(req.text))
        else:
            return req.json()

    def list_datasets(self, get_global_public):
        """
        Lists datasets in resources. Setting 'get_global_public' to 'True'
        will retrieve all public datasets in cloud. 'False' will get user's
        public datasets.

        Arguments:
            get_global_public (bool): True if user wants all public datasets in
                                      cloud. False if user wants only their
                                      public datasets.

        Returns:
            dict: Returns datasets in JSON format

        """
        appending = ""
        if get_global_public:
            appending = "public"
        url = self.url() + "/resource/{}dataset/".format(appending)
        req = self.remote_utils.get_url(url)

        if req.status_code is not 200:
            raise RemoteDataNotFoundError('Could not find {}'.format(req.text))
        else:
            return req.json()

    def delete_dataset(self, name):
        """
        Arguments:
            name (str): Name of dataset to delete

        Returns:
            bool: True if dataset deleted, False if not
        """
        url = self.url() + "/resource/dataset/{}".format(name)
        req = self.remote_utils.delete_url(url)

        if req.status_code is not 204:
            raise RemoteDataUploadError('Could not delete {}'.format(req.text))
        if req.content == "" or req.content == b'':
            return True
        else:
            return False

    def _check_channel(self, channel):
        for c in channel:
            if not c.isalnum():
                raise ValueError(
                    "Channel name cannot contain character {}.".format(c)
                )
        return True

    def create_channel(self,
                       channel_name,
                       project_name,
                       dataset_name,
                       channel_type,
                       dtype,
                       startwindow,
                       endwindow,
                       readonly=0,
                       start_time=0,
                       end_time=0,
                       propagate=0,
                       resolution=0,
                       channel_description=''):
        """
        Create a new channel on the Remote, using channel_data.

        Arguments:
            channel_name (str): Channel name
            project_name (str): Project name
            dataset_name (str): Dataset name
            channel_type (str): Type of the channel (e.g. `neurodata.IMAGE`)
            dtype (str): The datatype of the channel's data (e.g. `uint8`)
            startwindow (int): Window to start in
            endwindow (int): Window to end in
            readonly (int): Can others write to this channel?
            propagate (int): Allow propogation? 1 is True, 0 is False
            resolution (int): Resolution scaling
            channel_description (str): Your description of the channel

        Returns:
            bool: `True` if successful, `False` otherwise.

        Raises:
            ValueError: If your args were bad :(
            RemoteDataUploadError: If the channel data is valid but upload
                fails for some other reason.
        """
        self._check_channel(channel_name)

        if channel_type not in ['image', 'annotation', 'timeseries']:
            raise ValueError('Channel type must be ' +
                             'neurodata.IMAGE or neurodata.ANNOTATION.')

        if readonly * 1 not in [0, 1]:
            raise ValueError("readonly must be 0 (False) or 1 (True).")

        # Good job! You supplied very nice arguments.

        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/{}".format(project_name) + \
            "/channel/{}/".format(channel_name)
        json = {
            "channel_name": channel_name,
            "channel_type": channel_type,
            "channel_datatype": dtype,
            "startwindow": startwindow,
            "endwindow": endwindow,
            'starttime': start_time,
            'endtime': end_time,
            'readonly': readonly,
            'propagate': propagate,
            'resolution': resolution,
            'channel_description': channel_description
        }
        req = self.remote_utils.post_url(url, json=json)

        if req.status_code is not 201:
            raise RemoteDataUploadError('Could not upload {}'.format(req.text))
        if req.content == "" or req.content == b'':
            return True
        else:
            return False

    def get_channel(self, channel_name, project_name, dataset_name):
        """
        Gets info about a channel given its name, name of its project
        , and name of its dataset.

        Arguments:
            channel_name (str): Channel name
            project_name (str): Project name
            dataset_name (str): Dataset name

        Returns:
            dict: Channel info
        """
        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/{}".format(project_name) + \
            "/channel/{}/".format(channel_name)

        req = self.remote_utils.get_url(url)

        if req.status_code is not 200:
            raise RemoteDataNotFoundError('Could not find {}'.format(req.text))
        else:
            return req.json()

    def delete_channel(self, channel_name, project_name, dataset_name):
        """
        Deletes a channel given its name, name of its project
        , and name of its dataset.

        Arguments:
            channel_name (str): Channel name
            project_name (str): Project name
            dataset_name (str): Dataset name

        Returns:
            bool: True if channel deleted, False if not
        """
        url = self.url() + "/nd/resource/dataset/{}".format(dataset_name)\
            + "/project/{}".format(project_name) + \
            "/channel/{}/".format(channel_name)

        req = self.remote_utils.delete_url(url)

        if req.status_code is not 204:
            raise RemoteDataUploadError('Could not delete {}'.format(req.text))
        if req.content == "" or req.content == b'':
            return True
        else:
            return False


# This source file is part of Yoda.
#
# Yoda 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.
#
# Yoda 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
# Yoda. If not, see <http://www.gnu.org/licenses/gpl-3.0.html>.

import logging

from prettytable import PrettyTable
from pycolorizer import Color

from yoda import find_path
from yoda import Repository
from yoda import RepositoryAdapterNotFound
from yoda import slashes2dash
from yoda.subcommands import Subcommand
from yoda import Workspace

class Show(Subcommand, object):

    workspace = None
    logger = None

    def setup(self, name, config, subparser):
        self.workspace = Workspace(config)
        self.subparser = subparser
        self.logger = logging.getLogger(__name__)
        super(Show, self).setup(name, config, subparser)

    def parse(self):
        """Parse show subcommand."""
        parser = self.subparser.add_parser(
            "show",
            help="Show workspace details",
            description="Show workspace details.")

        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument('--all', action='store_true', help="All workspaces")
        group.add_argument('name', type=str, help="Workspace name", nargs='?')

    def execute(self, args):
        """Execute show subcommand."""
        if args.name is not None:
            self.show_workspace(slashes2dash(args.name))
        elif args.all is not None:
            self.show_all()

    def show_workspace(self, name):
        """Show specific workspace."""
        if not self.workspace.exists(name):
            raise ValueError("Workspace `%s` doesn't exists." % name)

        color = Color()
        workspaces = self.workspace.list()

        self.logger.info("<== %s workspace ==>" % color.colored(name, "green"))
        self.logger.info("\tPath: %s" % workspaces[name]["path"])
        self.logger.info("\tNumber of repositories: %s"
                         % color.colored(
                             len(workspaces[name]["repositories"]),
                             "yellow"))

        repo_colored = color.colored("Repositories", "blue")
        path_colored = color.colored("Path", "blue")
        trepositories = PrettyTable(
            [repo_colored, path_colored, color.colored("+", "blue")])
        trepositories.align[repo_colored] = "l"
        trepositories.align[path_colored] = "l"

        for repo_name in workspaces[name]["repositories"]:
            fullname = "%s/%s" % (name, repo_name)
            fullpath = find_path(fullname, self.config)[fullname]
            try:
                repo = Repository(fullpath)
                repo_scm = repo.get_scm()
            except RepositoryAdapterNotFound:
                repo_scm = None
            trepositories.add_row(
                [color.colored(repo_name, "cyan"), fullpath, repo_scm])

        self.logger.info(trepositories)

    def show_all(self):
        """Show details for all workspaces."""
        for ws in self.workspace.list().keys():
            self.show_workspace(ws)
            print("\n\n")


from __future__ import absolute_import
import requests

class Remote(object):
    """
    All Remotes in this module should inherit from this base class.

    This base class itself should not be used.
    """

    def __init__(self, hostname, protocol):
        """
        Arguments:
            `hostname`:     Hostname (e.g. google.com)
            `protocol`:     HTTP protocol (e.g. https)
        """
        self.protocol = protocol
        self.hostname = hostname

    def url(self, endpoint=''):
        """
        Get the base URL of the Remote.

        Arguments:
            None
        Returns:
            `str` base URL
        """
        if not endpoint.startswith('/'):
            endpoint = "/" + endpoint
        return self.protocol + "://" + self.hostname + endpoint

    def ping(self, endpoint=''):
        """
        Ping the server to make sure that you can access the base URL.

        Arguments:
            None
        Returns:
            `boolean` Successful access of server (or status code)
        """
        r = requests.get(self.url() + "/" + endpoint)
        return r.status_code


from __future__ import absolute_import
import mcubes

def export_dae(filename, cutout, level=0):
    """
    Converts a dense annotation to a DAE, using Marching Cubes (PyMCubes).

    Arguments:
        filename (str): The filename to write out to
        cutout (numpy.ndarray): The dense annotation
        level (int): The level at which to run mcubes

    Returns:
        boolean success
    """
    if ".dae" not in filename:
        filename = filename + ".dae"

    vs, fs = mcubes.marching_cubes(cutout, level)
    mcubes.export_mesh(vs, fs, filename, "ndioexport")

def export_obj(filename, cutout, level=0):
    """
    Converts a dense annotation to a obj, using Marching Cubes (PyMCubes).

    Arguments:
        filename (str): The filename to write out to
        cutout (numpy.ndarray): The dense annotation
        level (int): The level at which to run mcubes

    Returns:
        boolean success
    """
    if ".obj" not in filename:
        filename = filename + ".obj"

    vs, fs = mcubes.marching_cubes(cutout, level)
    mcubes.export_obj(vs, fs, filename)

def export_ply(filename, cutout, level=0):
    """
    Converts a dense annotation to a .PLY, using Marching Cubes (PyMCubes).

    Arguments:
        filename (str): The filename to write out to
        cutout (numpy.ndarray): The dense annotation
        level (int): The level at which to run mcubes

    Returns:
        boolean success
    """
    if ".ply" not in filename:
        filename = filename + ".ply"

    vs, fs = mcubes.marching_cubes(cutout, level)

    with open(filename, 'w') as fh:
        lines = [
            "ply"
            "format ascii 1.0",
            "comment generated by ndio",
            "element vertex " + str(len(vs)),
            "property float32 x",
            "property float32 y",
            "property float32 z",
            "element face " + str(len(fs)),
            "property list uint8 int32 vertex_index",
            "end_header"
        ]
        fh.writelines(lines)
        for v in vs:
            fh.write("{} {} {}".format(v[0], v[1], v[2]))
        for f in fs:
            fh.write("3 {} {} {}".format(f[0], f[1], f[2]))


from __future__ import absolute_import
import os
import shutil
from PIL import Image

FILE_FORMATS = {
    # Format.lower()    # File ext. By convention, use the first by default
    'hdf5':             ['hdf5', 'h5', 'hdf'],
    'tiff':             ['tiff', 'tif'],
    'png':              ['png'],
    'ramon':            ['m'],
    'matlab':           ['m', 'mat'],
}

def _fail_pair_conversion(i, o):
    """
    Helper-function to print failure and pass back False.
    """
    raise ValueError("Conversion from {0} to {1} " +
                     "is not currently supported.".format(i, o))

def _get_extension_for_format(fmt):
    """
    Get the appropriate file extension for a given format.

    Arguments:
        fmt:        The format to find an extension for

    Returns:
        String. The format (without leading period),
                or False if none was found.
    """
    if fmt in FILE_FORMATS:
        # Use first file extension by default
        return FILE_FORMATS[fmt][0]

    # Otherwise, we don't recognize the format...
    return False

def _guess_format_from_extension(ext):
    """
    Guess the appropriate data type from file extension.

    Arguments:
        ext:        The file extension (period optional)

    Returns:
        String. The format (without leading period),
                or False if none was found or couldn't be guessed
    """
    ext = ext.strip('.')

    # We look through FILE_FORMATS for this extension.
    # - If it appears zero times, return False. We can't guess.
    # - If it appears once, we can simply return that format.
    # - If it appears more than once, we can't guess (it's ambiguous,
    #   e.g .m = RAMON or MATLAB)

    formats = []
    for fmt in FILE_FORMATS:
        if ext in FILE_FORMATS[fmt]:
            formats.append(fmt)

    if formats == [] or len(formats) > 1:
        return False

    return formats[0]

def open(in_file, in_fmt=None):
    """
    Reads in a file from disk.

    Arguments:
        in_file: The name of the file to read in
        in_fmt: The format of in_file, if you want to be explicit

    Returns:
        numpy.ndarray
    """
    fmt = in_file.split('.')[-1]
    if in_fmt:
        fmt = in_fmt
    fmt = fmt.lower()

    if fmt in ['png', 'jpg', 'tiff', 'tif', 'jpeg']:
        return Image.open(in_file)
    else:
        raise NotImplementedError("Cannot open file of type {fmt}".format(fmt))

def convert(in_file, out_file, in_fmt="", out_fmt=""):
    """
    Converts in_file to out_file, guessing datatype in the absence of
    in_fmt and out_fmt.

    Arguments:
        in_file:    The name of the (existing) datafile to read
        out_file:   The name of the file to create with converted data
        in_fmt:     Optional. The format of incoming data, if not guessable
        out_fmt:    Optional. The format of outgoing data, if not guessable

    Returns:
        String. Output filename
    """
    # First verify that in_file exists and out_file doesn't.
    in_file = os.path.expanduser(in_file)
    out_file = os.path.expanduser(out_file)

    if not os.path.exists(in_file):
        raise IOError("Input file {0} does not exist, stopping..."
                      .format(in_file))

    # Get formats, either by explicitly naming them or by guessing.
    # TODO: It'd be neat to check here if an explicit fmt matches the guess.
    in_fmt = in_fmt.lower() or _guess_format_from_extension(
        in_file.split('.')[-1].lower())
    out_fmt = out_fmt.lower() or _guess_format_from_extension(
        out_file.split('.')[-1].lower())

    if not in_fmt or not out_fmt:
        raise ValueError("Cannot determine conversion formats.")
        return False

    if in_fmt is out_fmt:
        # This is the case when this module (intended for LONI) is used
        # indescriminately to 'funnel' data into one format.
        shutil.copyfileobj(in_file, out_file)
        return out_file

    # Import
    if in_fmt == 'hdf5':
        from . import hdf5
        data = hdf5.load(in_file)
    elif in_fmt == 'tiff':
        from . import tiff
        data = tiff.load(in_file)
    elif in_fmt == 'png':
        from . import png
        data = png.load(in_file)
    else:
        return _fail_pair_conversion(in_fmt, out_fmt)

    # Export
    if out_fmt == 'hdf5':
        from . import hdf5
        return hdf5.save(out_file, data)
    elif out_fmt == 'tiff':
        from . import tiff
        return tiff.save(out_file, data)
    elif out_fmt == 'png':
        from . import png
        return png.export_png(out_file, data)
    else:
        return _fail_pair_conversion(in_fmt, out_fmt)

    return _fail_pair_conversion(in_fmt, out_fmt)


from __future__ import absolute_import

try:
    # For Python 3:
    import urllib.request as urllib2
except ImportError:
    import urllib2

import zipfile
import tempfile
import inspect
import threading
import os

from .Remote import Remote
from .errors import *
import ndio.ramon as ramon

DEFAULT_HOSTNAME = "openconnecto.me"
DEFAULT_PROTOCOL = "http"
DEFAULT_EMAIL = ""
DEFAULT_FIBER_FILE = "fiber.dat"

class Invariants:
    """
    An array of invariant enumerables.
    """

    SCAN_STATISTIC = ss1 = "ss1"        # the Scan Statistic 1
    TRIANGLE_COUNT = tri = "tri"        # the Triangle Count
    CLUSTERING = cc = "cc"              # the Clustering Coefficient
    MAX_AVG_DEGREE = mad = "mad"        # the Maximum Average Degree
    LOCAL_DEGREE = deg = "deg"          # The Local Degree Count
    EIGENS = eig = "eig"                # top 100 or max avail eigvecs, eigvals

    ALL = [
        SCAN_STATISTIC,
        TRIANGLE_COUNT,
        CLUSTERING,
        MAX_AVG_DEGREE,
        LOCAL_DEGREE,
        EIGENS
    ]

class GraphFormats:
    """
    The set of graph format enumerables.
    """

    GRAPHML = graphml = "graphml"
    NCOL = ncol = "ncol"
    EDGELIST = edgelist = "edgelist"
    LGL = lgl = "lgl"
    PAJEK = pajek = "pajek"
    GRAPHDB = graphdb = "graphdb"
    NUMPY = numpy = "numpy"
    MAT = mat = "mat"

    _any = [
        GRAPHML,
        NCOL,
        EDGELIST,
        LGL,
        PAJEK,
        GRAPHDB,
        NUMPY,
        MAT
    ]

class grute(Remote):
    """
    The grute remote, for interfacing with ndgrutedb (NeuroData.io)
    """

    SMALL = S = 's'
    BIG = B = 'b'

    def __init__(self,
                 hostname=DEFAULT_HOSTNAME,
                 protocol=DEFAULT_PROTOCOL,
                 email=DEFAULT_EMAIL):
        """
        Initialize a new grute remote.

        Arguments:
            hostname (str: "openconnecto.me"): The hostname where grute lives
            protocol (str: "http"): The protocol over which to access grute
            email (str: ""): The email to which completion notifications should
                be sent (unless overridden in individual calls). Note that the
                completion URLs are also accessible via this Python API.
        """
        super(grute, self).__init__(hostname, protocol)
        self.email = email

    def __repr__(self):
        """
        Returns a string representation that can be used to reproduce this
        instance. `eval(repr(this))` should return an identical copy.

        Arguments:
            None

        Returns:
            str: Representation of reproducible instance.
        """
        return "ndio.remote.grute('{}', '{}', '{}')".format(
            self.hostname,
            self.protocol,
            self.email
        )

    def ping(self):
        """
        Check the server for proof of life.
        """
        return super(grute, self).ping()

    def url(self, suffix=""):
        """
        Construct a valid URL from the base grutedb url.

        Arguments:
            suffix (str: ""): The suffix that should be appended to the base

        Returns:
            str: Fully formed absolute URL
        """
        return super(grute, self).url('/graph-services/' + suffix)

    def set_default_email(self, email):
        """
        Sets the default email to use for notifications if none is specified.
        Does only basic error-checking.

        Arguments:
            email (str): The email to notify for all server graph services.

        Returns:
            None

        Raises:
            ValueError: If an invalid email is supplied.
        """
        if "@" not in email:
            raise ValueError("Invalid email.")
        self.email = email

    def build_graph(self, project, site, subject, session, scan,
                    size, email=None, invariants=Invariants.ALL,
                    fiber_file=DEFAULT_FIBER_FILE, atlas_file=None,
                    use_threads=False, callback=None):
        """
        Builds a graph using the graph-services endpoint.

        Arguments:
            project (str): The project to use
            site (str): The site in question
            subject (str): The subject's identifier
            session (str): The session (per subject)
            scan (str): The scan identifier
            size (str): Whether to return a big (grute.BIG) or small
                (grute.SMALL) graph. For a better explanation, see m2g.io.
            email (str : self.email)*: An email to notify
            invariants (str[]: Invariants.ALL)*: An array of invariants to
                compute. You can use the grute.Invariants class to construct a
                list, or simply pass grute.Invariants.ALL to compute them all.
            fiber_file (str: DEFAULT_FIBER_FILE)*: A local filename of an
                MRI Studio .dat file
            atlas_file (str: None)*: A local atlas file, in NIFTI .nii format.
                If none is specified, the Desikan atlas is used by default.
            use_threads (bool: False)*: Whether to run the download in a Python
                thread. If set to True, the call to `build_graph` will end
                quickly, and the `callback` will be called with the returned
                status-code of the restful call as its only argument.
            callback (function: None)*: The function to run upon completion of
                the call, if using threads. (Will not be called if use_threads
                is set to False.)

        Returns:
            HTTP Response if use_threads is False. Otherwise, None

        Raises:
            ValueError: When the supplied values are invalid (contain invalid
                characters, bad email address supplied, etc.)
            RemoteDataNotFoundError: When the data cannot be processed due to
                a server error.
        """
        if email is None:
            email = self.email

        if not set(invariants) <= set(Invariants.ALL):
            raise ValueError("Invariants must be a subset of Invariants.ALL.")

        if use_threads and callback is not None:
            if not hasattr(callback, '__call__'):
                raise ValueError("callback must be a function.")
            if len(inspect.getargspec(callback).args) != 1:
                raise ValueError("callback must take exactly 1 argument.")

        # Once we get here, we know the callback is
        if size not in [self.BIG, self.SMALL]:
            raise ValueError("size must be either grute.BIG or grute.SMALL.")

        url = "buildgraph/{}/{}/{}/{}/{}/{}/{}/{}/".format(
            project,
            site,
            subject,
            session,
            scan,
            size,
            email,
            "/".join(invariants)
        )

        if " " in url:
            raise ValueError("Arguments must not contain spaces.")

        if use_threads:
            # Run in the background.
            download_thread = threading.Thread(
                target=self._run_build_graph,
                args=[url, fiber_file, atlas_file, callback]
            )
            download_thread.start()
        else:
            # Run in the foreground.
            return self._run_build_graph(url, fiber_file, atlas_file)
        return

    def _run_build_graph(self, url,
                         fiber_file, atlas_file=None, callback=None):

        try:
            tmpfile = tempfile.NamedTemporaryFile()
            zfile = zipfile.ZipFile(tmpfile.name, "w", allowZip64=True)
            zfile.write(file_file)
            if atlas_file:
                zfile.write(atlas_file)
                zfile.close()
                tmpfile.flush()
                tmpfile.seek(0)
        except:
            raise ValueError("Invalid atlas or fiber file. I don't have " +
                             "any more information than that, sorry!")

        try:
            req = urllib2.Request(self.url(url), tmpfile.read())
            response = urllib2.urlopen(req)

            if callback is not None:
                callback(response)
            else:
                # TODO: Download from the response.read() URL instead of
                #       returning the http uri itself
                return response.read()
        except:
            raise RemoteDataUploadError("Failed to upload data at " + url)

    def compute_invariants(self, graph_file, input_format,
                           invariants=Invariants.ALL, email=None,
                           use_threads=False, callback=None):
        """
        Compute invariants from an existing GraphML file using the remote
        grute graph services.

        Arguments:
            graph_file (str): The filename of the graphml file
            input_format (str): One of grute.GraphFormats
            invariants (str[]: Invariants.ALL)*: An array of grute.Invariants
                to compute on the graph
            email (str: self.email)*: The email to notify upon completion
            use_threads (bool: False)*: Whether to use Python threads to run
                computation in the background when waiting for the server to
                return the invariants
            callback (function: None)*: The function to run upon completion of
                the call, if using threads. (Will not be called if use_threads
                is set to False.)

        Returns:
            HTTP Response if use_threads is False. Otherwise, None

        Raises:
            ValueError: If the graph file does not exist, or if there are
                issues with the passed arguments
            RemoteDataUploadError: If there is an issue packing the file
            RemoteError: If the server experiences difficulty computing invs
        """
        if email is None:
            email = self.email

        if input_format not in GraphFormats._any:
            raise ValueError("Invalid input format, {}.".format(input_format))

        if not set(invariants) <= set(Invariants.ALL):
            raise ValueError("Invariants must be a subset of Invariants.ALL.")

        if use_threads and callback is not None:
            if not hasattr(callback, '__call__'):
                raise ValueError("callback must be a function.")
            if len(inspect.getargspec(callback).args) != 1:
                raise ValueError("callback must take exactly 1 argument.")

        url = "graphupload/{}/{}/{}/".format(
            email,
            input_format,
            "/".join(invariants)
        )

        if " " in url:
            raise ValueError("Arguments cannot have spaces in them.")

        if not (os.path.exists(graph_file)):
            raise ValueError("File {} does not exist.".format(graph_file))

        if use_threads:
            # Run in the background.
            upload_thread = threading.Thread(
                target=self._run_compute_invariants,
                args=[url, graph_file, callback]
            )
            upload_thread.start()

        else:
            # Run in the foreground.
            return self._run_compute_invariants(url, graph_file)
        return

    def _run_compute_invariants(self, url, graph_file, callback=None):
        try:
            tmpfile = tempfile.NamedTemporaryFile()
            zfile = zipfile.ZipFile(tmpfile.name, "w", allowZip64=True)
            zfile.write(graph_file)
            zfile.close()
            tmpfile.flush()
            tmpfile.seek(0)
        except:
            raise ValueError("Unable to zip graph file for upload.")

        try:
            req = urllib2.Request(self.url(url), tmpfile.read())
            response = urllib2.urlopen(req)

            if callback is not None:
                callback(response.read())
            else:
                # TODO: Download from the response.read() URL instead of
                #       returning the http uri itself
                return response.read()
        except:
            raise RemoteDataUploadError("Failed to upload graph file. Try " +
                                        "troubleshooting with a ping()?")

    def convert_graph(self, graph_file, input_format, output_formats,
                      email=None, use_threads=False, callback=None):
        """
        Convert a graph from one GraphFormat to another.

        Arguments:
            graph_file (str): Filename of the file to convert
            input_format (str): A grute.GraphFormats
            output_formats (str[]): A grute.GraphFormats
            email (str: self.email)*: The email to notify
            use_threads (bool: False)*: Whether to use Python threads to run
                computation in the background when waiting for the server
            callback (function: None)*: The function to run upon completion of
                the call, if using threads. (Will not be called if use_threads
                is set to False.)

        Returns:
            HTTP Response if use_threads=False. Else, no return value.

        Raises:
            RemoteDataUploadError: If there's an issue uploading the data
            RemoteError: If there's a server-side issue
            ValueError: If there's a problem with the supplied arguments
        """
        if email is None:
            email = self.email

        if input_format not in GraphFormats._any:
            raise ValueError("Invalid input format {}.".format(input_format))

        if not set(output_formats) <= set(GraphFormats._any):
            raise ValueError("Output formats must be a GraphFormats.")

        if use_threads and callback is not None:
            if not hasattr(callback, '__call__'):
                raise ValueError("callback must be a function.")
            if len(inspect.getargspec(callback).args) != 1:
                raise ValueError("callback must take exactly 1 argument.")

        if not (os.path.exists(graph_file)):
            raise ValueError("No such file, {}!".format(graph_file))

        url = "convert/{}/{}/{}/l".format(
            email,
            input_format,
            ','.join(output_formats)
        )

        if " " in url:
            raise ValueError("Spaces are not permitted in arguments.")

        if use_threads:
            # Run in the background.
            convert_thread = threading.Thread(
                target=self._run_convert_graph,
                args=[url, graph_file, callback]
            )
            convert_thread.start()
        else:
            # Run in the foreground.
            return self._run_convert_graph(url, graph_file)
        return

    def _run_convert_graph(self, url, graph_file, callback=None):

        try:
            tmpfile = tempfile.NamedTemporaryFile()
            zfile = zipfile.ZipFile(tmpfile.name, "w", allowZip64=True)
            zfile.write(graph_file)
            zfile.close()
            tmpfile.flush()
            tmpfile.seek(0)
        except:
            raise ValueError("Unable to zip graph file for upload.")

        try:
            req = urllib2.Request(self.url(url), tmpfile.read())
            response = urllib2.urlopen(req)

            if callback is not None:
                callback(response.read())
            else:
                # TODO: Download from the response.read() URL instead of
                #       returning the http uri itself
                return response.read()
        except:
            raise RemoteDataUploadError("Failed to upload graph file. Try " +
                                        "troubleshooting with a ping()?")