<commit_before># Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Functions for centroiding sources and measuring their morphological
properties.
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import collections
import numpy as np
from astropy.modeling.models import Gaussian1D, Gaussian2D, Const1D, Const2D
from astropy.modeling.fitting import LevMarLSQFitter
from astropy.nddata.utils import overlap_slices
from .segmentation import SegmentProperties
import warnings
from astropy.utils.exceptions import AstropyUserWarning

__all__ = ['GaussianConst2D', 'centroid_com', 'gaussian1d_moments',
           'marginalize_data2d', 'centroid_1dg', 'centroid_2dg',
           'fit_2dgaussian', 'data_properties', 'cutout_footprint']

class _GaussianConst1D(Const1D + Gaussian1D):
    """A 1D Gaussian plus a constant model."""

class GaussianConst2D(Const2D + Gaussian2D):
    """
    A 2D Gaussian plus a constant model.

    Parameters
    ----------
    amplitude_0 : float
        Value of the constant.
    amplitude_1 : float
        Amplitude of the Gaussian.
    x_mean_1 : float
        Mean of the Gaussian in x.
    y_mean_1 : float
        Mean of the Gaussian in y.
    x_stddev_1 : float
        Standard deviation of the Gaussian in x.
        ``x_stddev`` and ``y_stddev`` must be specified unless a covariance
        matrix (``cov_matrix``) is input.
    y_stddev_1 : float
        Standard deviation of the Gaussian in y.
        ``x_stddev`` and ``y_stddev`` must be specified unless a covariance
        matrix (``cov_matrix``) is input.
    theta_1 : float, optional
        Rotation angle in radians. The rotation angle increases
        counterclockwise.
    cov_matrix_1 : ndarray, optional
        A 2x2 covariance matrix. If specified, overrides the ``x_stddev``,
        ``y_stddev``, and ``theta`` specification.
    """

def _convert_image(data, mask=None):
    """
    Convert the input data to a float64 (double) `numpy.ndarray`,
    required for input to `skimage.measure.moments` and
    `skimage.measure.moments_central`.

    The input ``data`` is copied unless it already has that
    `numpy.dtype`.

    If ``mask`` is input, then masked pixels are set to zero in the
    output ``data``.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.
        Masked pixels are set to zero in the output ``data``.

    Returns
    -------
    image : `numpy.ndarray`, float64
        The converted 2D array of the image, where masked pixels have
        been set to zero.
    """

    try:
        if mask is None:
            copy = False
        else:
            copy = True
        image = np.asarray(data).astype(np.float, copy=copy)
    except TypeError:    # pragma: no cover
        image = np.asarray(data).astype(np.float)    # for numpy <= 1.6
    if mask is not None:
        mask = np.asanyarray(mask)
        if data.shape != mask.shape:
            raise ValueError('data and mask must have the same shape')
        image[mask] = 0.0
    return image

def centroid_com(data, mask=None):
    """
    Calculate the centroid of a 2D array as its center of mass
    determined from image moments.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    xcen, ycen : float
        (x, y) coordinates of the centroid.
    """

    from skimage.measure import moments
    data = _convert_image(data, mask=mask)
    m = moments(data, 1)
    xcen = m[1, 0] / m[0, 0]
    ycen = m[0, 1] / m[0, 0]
    return xcen, ycen

def gaussian1d_moments(data, mask=None):
    """
    Estimate 1D Gaussian parameters from the moments of 1D data.

    This function can be useful for providing initial parameter values
    when fitting a 1D Gaussian to the ``data``.

    Parameters
    ----------
    data : array_like (1D)
        The 1D array.

    mask : array_like (1D bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    amplitude, mean, stddev : float
        The estimated parameters of a 1D Gaussian.
    """

    if mask is not None:
        mask = np.asanyarray(mask)
        data = data.copy()
        data[mask] = 0.
    x = np.arange(data.size)
    x_mean = np.sum(x * data) / np.sum(data)
    x_stddev = np.sqrt(abs(np.sum(data * (x - x_mean)**2) / np.sum(data)))
    amplitude = np.nanmax(data) - np.nanmin(data)
    return amplitude, x_mean, x_stddev

def marginalize_data2d(data, error=None, mask=None):
    """
    Generate the marginal x and y distributions from a 2D data array.

    Parameters
    ----------
    data : array_like
        The 2D data array.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    marginal_data : list of `~numpy.ndarray`
        The marginal x and y distributions of the input ``data``.

    marginal_error : list of `~numpy.ndarray`
        The marginal x and y distributions of the input ``error``.

    marginal_mask : list of `~numpy.ndarray` (bool)
        The marginal x and y distributions of the input ``mask``.
    """

    if error is not None:
        marginal_error = np.array(
            [np.sqrt(np.sum(error**2, axis=i)) for i in [0, 1]])
    else:
        marginal_error = [None, None]

    if mask is not None:
        mask = np.asanyarray(mask)
        marginal_mask = [np.sum(mask, axis=i).astype(np.bool) for i in [0, 1]]
    else:
        marginal_mask = [None, None]

    marginal_data = [np.sum(data, axis=i) for i in [0, 1]]

    return marginal_data, marginal_error, marginal_mask

def centroid_1dg(data, error=None, mask=None):
    """
    Calculate the centroid of a 2D array by fitting 1D Gaussians to the
    marginal x and y distributions of the array.

    Parameters
    ----------
    data : array_like
        The 2D data array.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    xcen, ycen : float
        (x, y) coordinates of the centroid.
    """

    mdata, merror, mmask = marginalize_data2d(data, error=error, mask=mask)

    if merror[0] is None and mmask[0] is None:
        mweights = [None, None]
    else:
        if merror[0] is not None:
            mweights = [(1.0 / merror[i].clip(min=1.e-30)) for i in [0, 1]]
        else:
            mweights = np.array([np.ones(data.shape[1]),
                                 np.ones(data.shape[0])])
        # down-weight masked pixels
        for i in [0, 1]:
            mweights[i][mmask[i]] = 1.e-20

    const_init = np.min(data)
    centroid = []
    for (mdata_i, mweights_i, mmask_i) in zip(mdata, mweights, mmask):
        params_init = gaussian1d_moments(mdata_i, mask=mmask_i)
        g_init = _GaussianConst1D(const_init, *params_init)
        fitter = LevMarLSQFitter()
        x = np.arange(mdata_i.size)
        g_fit = fitter(g_init, x, mdata_i, weights=mweights_i)
        centroid.append(g_fit.mean_1.value)
    return tuple(centroid)

def centroid_2dg(data, error=None, mask=None):
    """
    Calculate the centroid of a 2D array by fitting a 2D Gaussian (plus
    a constant) to the array.

    Parameters
    ----------
    data : array_like
        The 2D data array.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    xcen, ycen : float
        (x, y) coordinates of the centroid.
    """

    gfit = fit_2dgaussian(data, error=error, mask=mask)
    return gfit.x_mean_1.value, gfit.y_mean_1.value

def fit_2dgaussian(data, error=None, mask=None):
    """
    Fit a 2D Gaussian plus a constant to a 2D image.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    result : A `GaussianConst2D` model instance.
        The best-fitting Gaussian 2D model.
    """

    if data.size < 7:
        warnings.warn('data array must have a least 7 values to fit a 2D '
                      'Gaussian plus a constant', AstropyUserWarning)
        return None

    if error is not None:
        weights = 1.0 / error
    else:
        weights = None

    if mask is not None:
        mask = np.asanyarray(mask)
        if weights is None:
            weights = np.ones_like(data)
        # down-weight masked pixels
        weights[mask] = 1.e-30

    # Subtract the minimum of the data as a crude background estimate.
    # This will also make the data values positive, preventing issues with
    # the moment estimation in data_properties (moments from negative data
    # values can yield undefined Gaussian parameters, e.g. x/y_stddev).
    shift = np.min(data)
    data = np.copy(data) - shift
    props = data_properties(data, mask=mask)
    init_values = np.array([props.xcentroid.value, props.ycentroid.value,
                            props.semimajor_axis_sigma.value,
                            props.semiminor_axis_sigma.value,
                            props.orientation.value])

    init_const = 0.    # subtracted data minimum above
    init_amplitude = np.nanmax(data) - np.nanmin(data)
    g_init = GaussianConst2D(init_const, init_amplitude, *init_values)
    fitter = LevMarLSQFitter()
    y, x = np.indices(data.shape)
    gfit = fitter(g_init, x, y, data, weights=weights)
    gfit.amplitude_0 = gfit.amplitude_0 + shift
    return gfit

def data_properties(data, mask=None, background=None):
    """
    Calculate the centroid and morphological properties of a 2D array,
    e.g., an image cutout of an object.

    Parameters
    ----------
    data : array_like or `~astropy.units.Quantity`
        The 2D array of the image.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.
        Masked data are excluded from all calculations.

    background : float, array_like, or `~astropy.units.Quantity`, optional
        The background level that was previously present in the input
        ``data``.  ``background`` may either be a scalar value or a 2D
        image with the same shape as the input ``data``.  Inputting the
        ``background`` merely allows for its properties to be measured
        within each source segment.  The input ``background`` does *not*
        get subtracted from the input ``data``, which should already be
        background-subtracted.

    Returns
    -------
    result : `~photutils.segmentation.SegmentProperties` instance
        A `~photutils.segmentation.SegmentProperties` object.
    """

    segment_image = np.ones(data.shape, dtype=np.int)
    return SegmentProperties(data, segment_image, label=1, mask=mask,
                             background=background)

def cutout_footprint(data, position, box_size=3, footprint=None, mask=None,
                     error=None):
    """
    Cut out a region from data (and optional mask and error) centered at
    specified (x, y) position.

    The size of the region is specified via the ``box_size`` or
    ``footprint`` keywords.  The output mask for the cutout region
    represents the combination of the input mask and footprint mask.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    position : 2 tuple
        The ``(x, y)`` pixel coordinate of the center of the region.

    box_size : scalar or tuple, optional
        The size of the region to cutout from ``data``.  If ``box_size``
        is a scalar, then the region shape will be ``(box_size,
        box_size)``.  Either ``box_size`` or ``footprint`` must be
        defined.  If they are both defined, then ``footprint`` overrides
        ``box_size``.

    footprint : `~numpy.ndarray` of bools, optional
        A boolean array where `True` values describe the local footprint
        region.  ``box_size=(n, m)`` is equivalent to
        ``footprint=np.ones((n, m))``.  Either ``box_size`` or
        ``footprint`` must be defined.  If they are both defined, then
        ``footprint`` overrides ``box_size``.

    mask : array_like, bool, optional
        A boolean mask with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    Returns
    -------
    region_data : `~numpy.ndarray`
        The ``data`` cutout.

    region_mask : `~numpy.ndarray`
        The ``mask`` cutout.

    region_error : `~numpy.ndarray`
        The ``error`` cutout.

    slices : tuple of slices
        Slices in each dimension of the ``data`` array used to define
        the cutout region.
    """

    if len(position) != 2:
        raise ValueError('position must have a length of 2')

    if footprint is None:
        if box_size is None:
            raise ValueError('box_size or footprint must be defined.')
        if not isinstance(box_size, collections.Iterable):
            shape = (box_size, box_size)
        else:
            if len(box_size) != 2:
                raise ValueError('box_size must have a length of 2')
            shape = box_size
        footprint = np.ones(shape, dtype=bool)
    else:
        footprint = np.asanyarray(footprint, dtype=bool)

    slices_large, slices_small = overlap_slices(data.shape, footprint.shape,
                                                position[::-1])
    region_data = data[slices_large]

    if error is not None:
        region_error = error[slices_large]
    else:
        region_error = None

    if mask is not None:
        region_mask = mask[slices_large]
    else:
        region_mask = np.zeros_like(region_data, dtype=bool)
    footprint_mask = ~footprint
    footprint_mask = footprint_mask[slices_small]    # trim if necessary
    region_mask = np.logical_or(region_mask, footprint_mask)

    return region_data, region_mask, region_error, slices_large
<commit_msg>PEP8 morphology<commit_after># Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Functions for centroiding sources and measuring their morphological
properties.
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import collections
import numpy as np
from astropy.modeling.models import Gaussian1D, Gaussian2D, Const1D, Const2D
from astropy.modeling.fitting import LevMarLSQFitter
from astropy.nddata.utils import overlap_slices
from .segmentation import SegmentProperties
import warnings
from astropy.utils.exceptions import AstropyUserWarning

__all__ = ['GaussianConst2D', 'centroid_com', 'gaussian1d_moments',
           'marginalize_data2d', 'centroid_1dg', 'centroid_2dg',
           'fit_2dgaussian', 'data_properties', 'cutout_footprint']

class _GaussianConst1D(Const1D + Gaussian1D):
    """A 1D Gaussian plus a constant model."""

class GaussianConst2D(Const2D + Gaussian2D):
    """
    A 2D Gaussian plus a constant model.

    Parameters
    ----------
    amplitude_0 : float
        Value of the constant.
    amplitude_1 : float
        Amplitude of the Gaussian.
    x_mean_1 : float
        Mean of the Gaussian in x.
    y_mean_1 : float
        Mean of the Gaussian in y.
    x_stddev_1 : float
        Standard deviation of the Gaussian in x.
        ``x_stddev`` and ``y_stddev`` must be specified unless a covariance
        matrix (``cov_matrix``) is input.
    y_stddev_1 : float
        Standard deviation of the Gaussian in y.
        ``x_stddev`` and ``y_stddev`` must be specified unless a covariance
        matrix (``cov_matrix``) is input.
    theta_1 : float, optional
        Rotation angle in radians. The rotation angle increases
        counterclockwise.
    cov_matrix_1 : ndarray, optional
        A 2x2 covariance matrix. If specified, overrides the ``x_stddev``,
        ``y_stddev``, and ``theta`` specification.
    """

def _convert_image(data, mask=None):
    """
    Convert the input data to a float64 (double) `numpy.ndarray`,
    required for input to `skimage.measure.moments` and
    `skimage.measure.moments_central`.

    The input ``data`` is copied unless it already has that
    `numpy.dtype`.

    If ``mask`` is input, then masked pixels are set to zero in the
    output ``data``.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.
        Masked pixels are set to zero in the output ``data``.

    Returns
    -------
    image : `numpy.ndarray`, float64
        The converted 2D array of the image, where masked pixels have
        been set to zero.
    """

    try:
        if mask is None:
            copy = False
        else:
            copy = True
        image = np.asarray(data).astype(np.float, copy=copy)
    except TypeError:    # pragma: no cover
        image = np.asarray(data).astype(np.float)    # for numpy <= 1.6
    if mask is not None:
        mask = np.asanyarray(mask)
        if data.shape != mask.shape:
            raise ValueError('data and mask must have the same shape')
        image[mask] = 0.0
    return image

def centroid_com(data, mask=None):
    """
    Calculate the centroid of a 2D array as its center of mass
    determined from image moments.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    xcen, ycen : float
        (x, y) coordinates of the centroid.
    """

    from skimage.measure import moments
    data = _convert_image(data, mask=mask)
    m = moments(data, 1)
    xcen = m[1, 0] / m[0, 0]
    ycen = m[0, 1] / m[0, 0]
    return xcen, ycen

def gaussian1d_moments(data, mask=None):
    """
    Estimate 1D Gaussian parameters from the moments of 1D data.

    This function can be useful for providing initial parameter values
    when fitting a 1D Gaussian to the ``data``.

    Parameters
    ----------
    data : array_like (1D)
        The 1D array.

    mask : array_like (1D bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    amplitude, mean, stddev : float
        The estimated parameters of a 1D Gaussian.
    """

    if mask is not None:
        mask = np.asanyarray(mask)
        data = data.copy()
        data[mask] = 0.
    x = np.arange(data.size)
    x_mean = np.sum(x * data) / np.sum(data)
    x_stddev = np.sqrt(abs(np.sum(data * (x - x_mean)**2) / np.sum(data)))
    amplitude = np.nanmax(data) - np.nanmin(data)
    return amplitude, x_mean, x_stddev

def marginalize_data2d(data, error=None, mask=None):
    """
    Generate the marginal x and y distributions from a 2D data array.

    Parameters
    ----------
    data : array_like
        The 2D data array.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    marginal_data : list of `~numpy.ndarray`
        The marginal x and y distributions of the input ``data``.

    marginal_error : list of `~numpy.ndarray`
        The marginal x and y distributions of the input ``error``.

    marginal_mask : list of `~numpy.ndarray` (bool)
        The marginal x and y distributions of the input ``mask``.
    """

    if error is not None:
        marginal_error = np.array(
            [np.sqrt(np.sum(error**2, axis=i)) for i in [0, 1]])
    else:
        marginal_error = [None, None]

    if mask is not None:
        mask = np.asanyarray(mask)
        marginal_mask = [np.sum(mask, axis=i).astype(np.bool) for i in [0, 1]]
    else:
        marginal_mask = [None, None]

    marginal_data = [np.sum(data, axis=i) for i in [0, 1]]

    return marginal_data, marginal_error, marginal_mask

def centroid_1dg(data, error=None, mask=None):
    """
    Calculate the centroid of a 2D array by fitting 1D Gaussians to the
    marginal x and y distributions of the array.

    Parameters
    ----------
    data : array_like
        The 2D data array.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    xcen, ycen : float
        (x, y) coordinates of the centroid.
    """

    mdata, merror, mmask = marginalize_data2d(data, error=error, mask=mask)

    if merror[0] is None and mmask[0] is None:
        mweights = [None, None]
    else:
        if merror[0] is not None:
            mweights = [(1.0 / merror[i].clip(min=1.e-30)) for i in [0, 1]]
        else:
            mweights = np.array([np.ones(data.shape[1]),
                                 np.ones(data.shape[0])])
        # down-weight masked pixels
        for i in [0, 1]:
            mweights[i][mmask[i]] = 1.e-20

    const_init = np.min(data)
    centroid = []
    for (mdata_i, mweights_i, mmask_i) in zip(mdata, mweights, mmask):
        params_init = gaussian1d_moments(mdata_i, mask=mmask_i)
        g_init = _GaussianConst1D(const_init, *params_init)
        fitter = LevMarLSQFitter()
        x = np.arange(mdata_i.size)
        g_fit = fitter(g_init, x, mdata_i, weights=mweights_i)
        centroid.append(g_fit.mean_1.value)
    return tuple(centroid)

def centroid_2dg(data, error=None, mask=None):
    """
    Calculate the centroid of a 2D array by fitting a 2D Gaussian (plus
    a constant) to the array.

    Parameters
    ----------
    data : array_like
        The 2D data array.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    xcen, ycen : float
        (x, y) coordinates of the centroid.
    """

    gfit = fit_2dgaussian(data, error=error, mask=mask)
    return gfit.x_mean_1.value, gfit.y_mean_1.value

def fit_2dgaussian(data, error=None, mask=None):
    """
    Fit a 2D Gaussian plus a constant to a 2D image.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    Returns
    -------
    result : A `GaussianConst2D` model instance.
        The best-fitting Gaussian 2D model.
    """

    if data.size < 7:
        warnings.warn('data array must have a least 7 values to fit a 2D '
                      'Gaussian plus a constant', AstropyUserWarning)
        return None

    if error is not None:
        weights = 1.0 / error
    else:
        weights = None

    if mask is not None:
        mask = np.asanyarray(mask)
        if weights is None:
            weights = np.ones_like(data)
        # down-weight masked pixels
        weights[mask] = 1.e-30

    # Subtract the minimum of the data as a crude background estimate.
    # This will also make the data values positive, preventing issues with
    # the moment estimation in data_properties (moments from negative data
    # values can yield undefined Gaussian parameters, e.g. x/y_stddev).
    shift = np.min(data)
    data = np.copy(data) - shift
    props = data_properties(data, mask=mask)
    init_values = np.array([props.xcentroid.value, props.ycentroid.value,
                            props.semimajor_axis_sigma.value,
                            props.semiminor_axis_sigma.value,
                            props.orientation.value])

    init_const = 0.    # subtracted data minimum above
    init_amplitude = np.nanmax(data) - np.nanmin(data)
    g_init = GaussianConst2D(init_const, init_amplitude, *init_values)
    fitter = LevMarLSQFitter()
    y, x = np.indices(data.shape)
    gfit = fitter(g_init, x, y, data, weights=weights)
    gfit.amplitude_0 = gfit.amplitude_0 + shift
    return gfit

def data_properties(data, mask=None, background=None):
    """
    Calculate the centroid and morphological properties of a 2D array,
    e.g., an image cutout of an object.

    Parameters
    ----------
    data : array_like or `~astropy.units.Quantity`
        The 2D array of the image.

    mask : array_like (bool), optional
        A boolean mask, with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.
        Masked data are excluded from all calculations.

    background : float, array_like, or `~astropy.units.Quantity`, optional
        The background level that was previously present in the input
        ``data``.  ``background`` may either be a scalar value or a 2D
        image with the same shape as the input ``data``.  Inputting the
        ``background`` merely allows for its properties to be measured
        within each source segment.  The input ``background`` does *not*
        get subtracted from the input ``data``, which should already be
        background-subtracted.

    Returns
    -------
    result : `~photutils.segmentation.SegmentProperties` instance
        A `~photutils.segmentation.SegmentProperties` object.
    """

    segment_image = np.ones(data.shape, dtype=np.int)
    return SegmentProperties(data, segment_image, label=1, mask=mask,
                             background=background)

def cutout_footprint(data, position, box_size=3, footprint=None, mask=None,
                     error=None):
    """
    Cut out a region from data (and optional mask and error) centered at
    specified (x, y) position.

    The size of the region is specified via the ``box_size`` or
    ``footprint`` keywords.  The output mask for the cutout region
    represents the combination of the input mask and footprint mask.

    Parameters
    ----------
    data : array_like
        The 2D array of the image.

    position : 2 tuple
        The ``(x, y)`` pixel coordinate of the center of the region.

    box_size : scalar or tuple, optional
        The size of the region to cutout from ``data``.  If ``box_size``
        is a scalar, then the region shape will be ``(box_size,
        box_size)``.  Either ``box_size`` or ``footprint`` must be
        defined.  If they are both defined, then ``footprint`` overrides
        ``box_size``.

    footprint : `~numpy.ndarray` of bools, optional
        A boolean array where `True` values describe the local footprint
        region.  ``box_size=(n, m)`` is equivalent to
        ``footprint=np.ones((n, m))``.  Either ``box_size`` or
        ``footprint`` must be defined.  If they are both defined, then
        ``footprint`` overrides ``box_size``.

    mask : array_like, bool, optional
        A boolean mask with the same shape as ``data``, where a `True`
        value indicates the corresponding element of ``data`` is masked.

    error : array_like, optional
        The 2D array of the 1-sigma errors of the input ``data``.

    Returns
    -------
    region_data : `~numpy.ndarray`
        The ``data`` cutout.

    region_mask : `~numpy.ndarray`
        The ``mask`` cutout.

    region_error : `~numpy.ndarray`
        The ``error`` cutout.

    slices : tuple of slices
        Slices in each dimension of the ``data`` array used to define
        the cutout region.
    """

    if len(position) != 2:
        raise ValueError('position must have a length of 2')

    if footprint is None:
        if box_size is None:
            raise ValueError('box_size or footprint must be defined.')
        if not isinstance(box_size, collections.Iterable):
            shape = (box_size, box_size)
        else:
            if len(box_size) != 2:
                raise ValueError('box_size must have a length of 2')
            shape = box_size
        footprint = np.ones(shape, dtype=bool)
    else:
        footprint = np.asanyarray(footprint, dtype=bool)

    slices_large, slices_small = overlap_slices(data.shape, footprint.shape,
                                                position[::-1])
    region_data = data[slices_large]

    if error is not None:
        region_error = error[slices_large]
    else:
        region_error = None

    if mask is not None:
        region_mask = mask[slices_large]
    else:
        region_mask = np.zeros_like(region_data, dtype=bool)
    footprint_mask = ~footprint
    footprint_mask = footprint_mask[slices_small]    # trim if necessary
    region_mask = np.logical_or(region_mask, footprint_mask)

    return region_data, region_mask, region_error, slices_large
<|endoftext|>


<commit_before>###############################################################################
# Caleydo - Visualization for Molecular Biology - http://caleydo.org
# Copyright (c) The Caleydo Team. All rights reserved.
# Licensed under the new BSD license, available at http://caleydo.org/license
###############################################################################

import logging.config

# set configured registry
def _get_config():
  from . import config
  return config.view('phovea_server')

# append the plugin directories as primary lookup path
cc = _get_config()
# configure logging
logging.config.dictConfig(cc.logging)
_log = logging.getLogger(__name__)

def _add_no_cache_header(response):
  """
  disable caching on the response
  :param response:
  :return:
  """
  #  response.headers['Last-Modified'] = datetime.now()
  response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
  response.headers['Pragma'] = 'no-cache'
  response.headers['Expires'] = '-1'
  return response

def _rest_cache(namespace):
  import os
  from os import path
  import hashlib
  from werkzeug import url_quote

  dir_name = path.join(cc.restCacheDir, namespace[1:])
  if not path.exists(dir_name):
    os.makedirs(dir_name)

  def to_filename(request):
    secure = url_quote(request.full_path[1:].replace('/', ' ').replace('?', '_').replace('=', '-').replace('&', '_'), safe=' ')
    key = hashlib.sha256(namespace + secure).hexdigest()
    file_name_old = '{}_{}.json'.format(secure[:min(128, len(secure))], key)
    file_name = '{}_{}.json'.format(secure[:min(64, len(secure))], key)  # use a shorter filename for restCache, to avoid file system errors with too long file names
    return {
      "file_name_128": file_name_old.replace('%', '_'),  # file_name_128 = file name with 128 characters in front of the hash
      "file_name_64": file_name.replace('%', '_')  # file_name_64 = file name with 64 characters in front of the hash
    }

  def save(response):
    from flask import request

    if request.method != 'GET' or response.status_code != 200 or response.mimetype != 'application/json' or response.is_streamed or response.cache_control.no_cache:
      return response

    file_name = to_filename(request)['file_name_64']  # only use the short file name for storing new files

    _log.info('cache %s %s -> %s %s', namespace, request.full_path, dir_name, file_name)

    with open(path.join(dir_name, file_name), 'w') as f:
      f.write(response.get_data())

    # print(response.mimetype, data)
    return response

  def load():
    from flask import request, send_from_directory

    file_names = to_filename(request)

    # use the legacy file name ('file_name_128') if a file with this name exists and the new file name format otherwise
    file_name = file_names['file_name_128'] if path.exists(file_names['file_name_128']) else file_names['file_name_64']

    full = path.join(dir_name, file_name)

    if path.exists(full):
      _log.info('use cache cache %s %s <- %s', namespace, request.full_path, full)
      return send_from_directory(path.abspath(dir_name), file_name, add_etags=False)
    return None

  return save, load

def _to_stack_trace():
  import traceback
  return traceback.format_exc()

def _exception_handler(error):
  return 'Internal Server Error\n' + _to_stack_trace(), 500

def _init_app(app, namespace, is_default_app=False):
  """
  initializes an application by setting common properties and options
  :param app:
  :param is_default_app:
  :return:
  """
  from . import security

  if hasattr(app, 'got_first_request') and app.got_first_request:
    _log.warn('already inited: ' + str(app))
    return

  _log.info('init application: ' + str(app))

  if hasattr(app, 'debug'):
    app.debug = cc.debug
  if cc.nocache and hasattr(app, 'after_request'):
    app.after_request(_add_no_cache_header)
  if cc.error_stack_trace and hasattr(app, 'register_error_handler'):
    app.register_error_handler(500, _exception_handler)

  if cc['restCache'] == 'record' and hasattr(app, 'after_request'):
    app.after_request(_rest_cache(namespace)[0])
  if cc['restCache'] is True and hasattr(app, 'before_request'):
    app.before_request(_rest_cache(namespace)[1])

  if cc.secret_key:
    app.config['SECRET_KEY'] = cc.secret_key

  if cc.max_file_size:
    app.config['MAX_CONTENT_LENGTH'] = cc.max_file_size

  security.init_app(app)
  if is_default_app:
    security.add_login_routes(app)

# helper to plugin in function scope
def _loader(p):
  _log.info('add application: ' + p.id + ' at namespace: ' + p.namespace)

  def load_app():
    app = p.load().factory()
    _init_app(app, p.namespace)
    return app

  return load_app

def _pre_load_caches():
  """
  preload some manager to start them up
  :return:
  """
  c = _get_config().coldstart

  if c['assigner']:
    from .dataset import get_idmanager
    _log.info('initialize id assigner')
    get_idmanager()

  if c['mapping']:
    from .dataset import get_mappingmanager
    _log.info('initialize mapping manager')
    get_mappingmanager()

def create_application():
  from . import dispatcher
  from . import mainapp
  from .plugin import list as list_plugins
  from werkzeug.middleware.proxy_fix import ProxyFix

  # create a path dispatcher
  _default_app = mainapp.default_app()
  _init_app(_default_app, '/', True)
  _applications = {p.namespace: _loader(p) for p in list_plugins('namespace')}

  # create a dispatcher for all the applications
  application = dispatcher.PathDispatcher(_default_app, _applications)

  _pre_load_caches()

  return ProxyFix(application)

def create(parser):
  """
  Add arguments to the parser and return a launcher function, which in-turn creates a server instance
  parser: ArgumentParser that allows to add custom arguments. The arguments will be passed as parameter to the launcher function.
  """
  parser.add_argument('--port', '-p', type=int, default=cc.getint('port'),  # get default value from config.json
                      help='server port')
  parser.add_argument('--address', '-a', default=cc.get('address'),  # get default value from config.json
                      help='server address')
  parser.add_argument('--certfile', '-c', default=cc.get('certfile'),  # get default value from config.json
                      help='server ss. certificate')
  parser.add_argument('--keyfile', '-k', default=cc.get('keyfile'),  # get default value from config.json
                      help='server keyfile for ssl certificate')

  def _launcher(args):
    """
    Prepare the launch of the server instance
    args: contains the arguments that are parsed from the command line (or set in the config as default value)
    """
    from geventwebsocket.handler import WebSocketHandler
    from gevent.pywsgi import WSGIServer

    # create phovea server application
    application = create_application()

    _log.info('prepare server that will listen on %s:%s', args.address, args.port, args.certfile, args.keyfile)

    if args.certfile and args.keyfile:
      http_server = WSGIServer((args.address, args.port), application, keyfile=args.keyfile, certfile=args.certfile, handler_class=WebSocketHandler)
    else:
      http_server = WSGIServer((args.address, args.port), application, handler_class=WebSocketHandler)

    return http_server.serve_forever  # return function name only; initialization will be done later

  return _launcher
<commit_msg>Update server.py<commit_after>###############################################################################
# Caleydo - Visualization for Molecular Biology - http://caleydo.org
# Copyright (c) The Caleydo Team. All rights reserved.
# Licensed under the new BSD license, available at http://caleydo.org/license
###############################################################################

import logging.config

# set configured registry
def _get_config():
  from . import config
  return config.view('phovea_server')

# append the plugin directories as primary lookup path
cc = _get_config()
# configure logging
logging.config.dictConfig(cc.logging)
_log = logging.getLogger(__name__)

def _add_no_cache_header(response):
  """
  disable caching on the response
  :param response:
  :return:
  """
  #  response.headers['Last-Modified'] = datetime.now()
  response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
  response.headers['Pragma'] = 'no-cache'
  response.headers['Expires'] = '-1'
  return response

def _rest_cache(namespace):
  import os
  from os import path
  import hashlib
  from werkzeug import url_quote

  dir_name = path.join(cc.restCacheDir, namespace[1:])
  if not path.exists(dir_name):
    os.makedirs(dir_name)

  def to_filename(request):
    secure = url_quote(request.full_path[1:].replace('/', ' ').replace('?', '_').replace('=', '-').replace('&', '_'), safe=' ')
    key = hashlib.sha256(namespace + secure).hexdigest()
    file_name_old = '{}_{}.json'.format(secure[:min(128, len(secure))], key)
    file_name = '{}_{}.json'.format(secure[:min(64, len(secure))], key)  # use a shorter filename for restCache, to avoid file system errors with too long file names
    return {
      "file_name_128": file_name_old.replace('%', '_'),  # file_name_128 = file name with 128 characters in front of the hash
      "file_name_64": file_name.replace('%', '_')  # file_name_64 = file name with 64 characters in front of the hash
    }

  def save(response):
    from flask import request

    if request.method != 'GET' or response.status_code != 200 or response.mimetype != 'application/json' or response.is_streamed or response.cache_control.no_cache:
      return response

    file_name = to_filename(request)['file_name_64']  # only use the short file name for storing new files

    _log.info('cache %s %s -> %s %s', namespace, request.full_path, dir_name, file_name)

    with open(path.join(dir_name, file_name), 'w') as f:
      f.write(response.get_data())

    # print(response.mimetype, data)
    return response

  def load():
    from flask import request, send_from_directory

    file_names = to_filename(request)

    # use the legacy file name ('file_name_128') if a file with this name exists and the new file name format otherwise
    file_name = file_names['file_name_128'] if path.exists(file_names['file_name_128']) else file_names['file_name_64']

    full = path.join(dir_name, file_name)

    if path.exists(full):
      _log.info('use cache cache %s %s <- %s', namespace, request.full_path, full)
      return send_from_directory(path.abspath(dir_name), file_name, add_etags=False)
    return None

  return save, load

def _to_stack_trace():
  import traceback
  return traceback.format_exc()

def _exception_handler(error):
  return 'Internal Server Error\n' + _to_stack_trace(), 500

def _init_app(app, namespace, is_default_app=False):
  """
  initializes an application by setting common properties and options
  :param app:
  :param is_default_app:
  :return:
  """
  from . import security

  if hasattr(app, 'got_first_request') and app.got_first_request:
    _log.warn('already inited: ' + str(app))
    return

  _log.info('init application: ' + str(app))

  if hasattr(app, 'debug'):
    app.debug = cc.debug
  if cc.nocache and hasattr(app, 'after_request'):
    app.after_request(_add_no_cache_header)
  if cc.error_stack_trace and hasattr(app, 'register_error_handler'):
    app.register_error_handler(500, _exception_handler)

  if cc['restCache'] == 'record' and hasattr(app, 'after_request'):
    app.after_request(_rest_cache(namespace)[0])
  if cc['restCache'] is True and hasattr(app, 'before_request'):
    app.before_request(_rest_cache(namespace)[1])

  if cc.secret_key:
    app.config['SECRET_KEY'] = cc.secret_key

  if cc.max_file_size:
    app.config['MAX_CONTENT_LENGTH'] = cc.max_file_size

  security.init_app(app)
  if is_default_app:
    security.add_login_routes(app)

# helper to plugin in function scope
def _loader(p):
  _log.info('add application: ' + p.id + ' at namespace: ' + p.namespace)

  def load_app():
    app = p.load().factory()
    _init_app(app, p.namespace)
    return app

  return load_app

def _pre_load_caches():
  """
  preload some manager to start them up
  :return:
  """
  c = _get_config().coldstart

  if c['assigner']:
    from .dataset import get_idmanager
    _log.info('initialize id assigner')
    get_idmanager()

  if c['mapping']:
    from .dataset import get_mappingmanager
    _log.info('initialize mapping manager')
    get_mappingmanager()

def create_application():
  from . import dispatcher
  from . import mainapp
  from .plugin import list as list_plugins
  from werkzeug.middleware.proxy_fix import ProxyFix

  # create a path dispatcher
  _default_app = mainapp.default_app()
  _init_app(_default_app, '/', True)
  _applications = {p.namespace: _loader(p) for p in list_plugins('namespace')}

  # create a dispatcher for all the applications
  application = dispatcher.PathDispatcher(_default_app, _applications)

  _pre_load_caches()

  return ProxyFix(application)

def create(parser):
  """
  Add arguments to the parser and return a launcher function, which in-turn creates a server instance
  parser: ArgumentParser that allows to add custom arguments. The arguments will be passed as parameter to the launcher function.
  """
  parser.add_argument('--port', '-p', type=int, default=cc.getint('port'),  # get default value from config.json
                      help='server port')
  parser.add_argument('--address', '-a', default=cc.get('address'),  # get default value from config.json
                      help='server address')
  parser.add_argument('--certfile', '-sslc', default=cc.get('certfile'),  # get default value from config.json
                      help='ssl certificate')
  parser.add_argument('--keyfile', '-sslk', default=cc.get('keyfile'),  # get default value from config.json
                      help='keyfile for ssl certificate')

  def _launcher(args):
    """
    Prepare the launch of the server instance
    args: contains the arguments that are parsed from the command line (or set in the config as default value)
    """
    from geventwebsocket.handler import WebSocketHandler
    from gevent.pywsgi import WSGIServer

    # create phovea server application
    application = create_application()

    _log.info('prepare server that will listen on %s:%s', args.address, args.port, args.certfile, args.keyfile)

    if args.certfile and args.keyfile:
      http_server = WSGIServer((args.address, args.port), application, keyfile=args.keyfile, certfile=args.certfile, handler_class=WebSocketHandler)
    else:
      http_server = WSGIServer((args.address, args.port), application, handler_class=WebSocketHandler)

    return http_server.serve_forever  # return function name only; initialization will be done later

  return _launcher
<|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# name:             meta.py
# author:           Harold Bradley III
# email:            harold@bradleystudio.net
# created on:       01/28/2016
#

"""
ext_pylib.meta.meta
~~~~~~~~~~~~~~~~~~~

Classes and functions for meta-programming with python.
This is mostly experimental stuff. For now, class DynamicProperty is merely a
close reimplementation of python's native property().

Credit for much of this goes to:
    http://eev.ee/blog/2012/05/23/python-faq-descriptors/
"""

class DynamicProperty(object):
    """A re-implementation of python's native property().
    Most of this code derived from: http://eev.ee/blog/2012/05/23/python-faq-descriptors/."""

    def __init__(self, getter=None):
        """Initializes the DynamicProperty's getter function."""
        self.getter = getter

    def __get__(self, instance, owner):
        """Returns the result of running the getter function.
        This is a wrapper for the property's actual getter function."""
        self.owner = owner  # Not used pylint: disable=attribute-defined-outside-init
        if instance is None:
            return self
        return self.getter(instance)

    def __set__(self, instance, value):
        """Calls the setter function.
        This is a wrapper for the property's actual setter function."""
        self.setter(instance, value)

    def create_setter(self, setter):
        """Sets the setter function to the passed arg setter (a function).
        Returns self. (Composite Pattern)."""
        self.setter = setter
        return self

    @staticmethod
    def setter(*args, **kwargs):  # pylint: disable=method-hidden,unused-argument
        """A stub. If it isn't overwritten, the property is read-only."""
        raise TypeError("Cannot modify property.  It doesn't have a setter function.")

def setdynattr(obj, attribute, getter_func=None, setter_func=None):
    """Creates a dynamic property on an object using DynamicProperty class."""
    if not getter_func:
        def getter_func(self):  # pylint: disable=function-redefined
            """Default getter function.
            Uses '_' + attribute name as a private variable to hold the
            property's value."""
            return getattr(self, '_' + attribute, None)

    if not setter_func:
        def setter_func(self, value): # pylint: disable=function-redefined
            """Default setter function.
            Uses '_' + attribute name as a private variable to hold the
            property's value."""
            return setattr(self, '_' + attribute, value)

    setattr(obj.__class__, attribute, DynamicProperty(getter_func))
    prop = getattr(obj.__class__, attribute)
    prop = prop.create_setter(setter_func)
<commit_msg>Adds usage example.<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# name:             meta.py
# author:           Harold Bradley III
# email:            harold@bradleystudio.net
# created on:       01/28/2016
#

"""
ext_pylib.meta.meta
~~~~~~~~~~~~~~~~~~~

Classes and functions for meta-programming with python.
This is mostly experimental stuff. For now, class DynamicProperty is merely a
close reimplementation of python's native property().

Credit for much of this goes to:
    http://eev.ee/blog/2012/05/23/python-faq-descriptors/
"""

class DynamicProperty(object):
    """A re-implementation of python's native property().
    Most of this code derived from: http://eev.ee/blog/2012/05/23/python-faq-descriptors/.

    :param getter: The getter function of the property.

    Usage::

        >>> from ext_pylib.meta import DynamicProperty

        >>> def getter_func(self):
        >>>     return self._property

        >>> def setter_func(self, value):
        >>>     self._property = value

        >>> class Cls(object): pass
        >>> instance = Cls()
        >>> instance.__class__.new_property = DynamicProperty(getter_func)
        >>> instance.__class__.new_property = \
                instance.__class__.new_propety.create_setter(setter_func)

        >>> instance.new_property = 'value'
        >>> instance.new_property
        'value'
    """

    def __init__(self, getter=None):
        """Initializes the DynamicProperty's getter function."""
        self.getter = getter

    def __get__(self, instance, owner):
        """Returns the result of running the getter function.
        This is a wrapper for the property's actual getter function."""
        self.owner = owner  # Not used pylint: disable=attribute-defined-outside-init
        if instance is None:
            return self
        return self.getter(instance)

    def __set__(self, instance, value):
        """Calls the setter function.
        This is a wrapper for the property's actual setter function."""
        self.setter(instance, value)

    def create_setter(self, setter):
        """Sets the setter function to the passed arg setter (a function).
        Returns self. (Composite Pattern)."""
        self.setter = setter
        return self

    @staticmethod
    def setter(*args, **kwargs):  # pylint: disable=method-hidden,unused-argument
        """A stub. If it isn't overwritten, the property is read-only."""
        raise TypeError("Cannot modify property.  It doesn't have a setter function.")

def setdynattr(obj, attribute, getter_func=None, setter_func=None):
    """Creates a dynamic property on an object using DynamicProperty class."""
    if not getter_func:
        def getter_func(self):  # pylint: disable=function-redefined
            """Default getter function.
            Uses '_' + attribute name as a private variable to hold the
            property's value."""
            return getattr(self, '_' + attribute, None)

    if not setter_func:
        def setter_func(self, value): # pylint: disable=function-redefined
            """Default setter function.
            Uses '_' + attribute name as a private variable to hold the
            property's value."""
            return setattr(self, '_' + attribute, value)

    setattr(obj.__class__, attribute, DynamicProperty(getter_func))
    prop = getattr(obj.__class__, attribute)
    prop = prop.create_setter(setter_func)
<|endoftext|>


<commit_before>#!/usr/bin/python

# This script runs inside the RPi and listens for rover commands.
# It also takes pictures and uploads to URL.

import time
import picamera
from poster.encode import multipart_encode
from poster.streaminghttp import register_openers
import urllib2
import select
import sys
import io
from PIL import Image, ImageDraw
from datetime import datetime

def add_text(stream):
    image = Image.open(stream)
    draw = ImageDraw.Draw(image)
    draw.line([(42, 240),(80, 200)], width=2, fill=128)
    draw.line([(207, 240),(190, 190)], width=2, fill=128)
    now = datetime.now()
    str = now.strftime("%H:%M")
    draw.text((0, 0), str)
    #PIL cannot save to a stream, so write to a file and read it back.
    image.save("tmp.jpg", format="jpeg")
    fp = open("tmp.jpg", "rb")
    return fp

def process_photo(camera, text=False):
    #print "Capturing image.."
    ostream = io.BytesIO()
    camera.capture(ostream, format='jpeg', resize=(320, 240), use_video_port=True)
    #print "Done. Now uploading it..."
    ostream.seek(0)

    if text:
        ostream = add_text(ostream)

    # Start the multipart/form-data encoding of the file "image.jpg"

    # headers contains the necessary Content-Type and Content-Length
    # datagen is a generator object that yields the encoded parameters
    datagen, headers = multipart_encode({"image": ostream})

    # Create the Request object
    request = urllib2.Request(UPLOAD_URL, datagen, headers)
    # Actually do the request, and get the response
    resp = urllib2.urlopen(request).read()
    #print "Upload done."
    # Ignore response.
    ostream.close()

############## Main

if len(sys.argv) != 2:
    raise RuntimeError("usage: photogen.py upload-url")

UPLOAD_URL = sys.argv[1]

endtime = time.time() + 60 # At most for 1 min.
camera = picamera.PiCamera()
camera.vflip = True
camera.hflip = True
register_openers()
poll = select.poll()
poll.register(0, select.POLLIN)
time.sleep(2) # Allow for camera warm-up

process_photo(camera)
evts = poll.poll(0)
while len(evts) == 0 and time.time() < endtime:
    process_photo(camera)
    evts = poll.poll(0)

print "Final photo.."
process_photo(camera, text=True)
camera.close()
print "Done."
<commit_msg>Removed some unnecessary output.<commit_after>#!/usr/bin/python

# This script runs inside the RPi and listens for rover commands.
# It also takes pictures and uploads to URL.

import time
import picamera
from poster.encode import multipart_encode
from poster.streaminghttp import register_openers
import urllib2
import select
import sys
import io
from PIL import Image, ImageDraw
from datetime import datetime

def add_text(stream):
    image = Image.open(stream)
    draw = ImageDraw.Draw(image)
    draw.line([(42, 240),(80, 200)], width=2, fill=128)
    draw.line([(207, 240),(190, 190)], width=2, fill=128)
    now = datetime.now()
    str = now.strftime("%H:%M")
    draw.text((0, 0), str)
    #PIL cannot save to a stream, so write to a file and read it back.
    image.save("tmp.jpg", format="jpeg")
    fp = open("tmp.jpg", "rb")
    return fp

def process_photo(camera, text=False):
    #print "Capturing image.."
    ostream = io.BytesIO()
    camera.capture(ostream, format='jpeg', resize=(320, 240), use_video_port=True)
    #print "Done. Now uploading it..."
    ostream.seek(0)

    if text:
        ostream = add_text(ostream)

    # Start the multipart/form-data encoding of the file "image.jpg"

    # headers contains the necessary Content-Type and Content-Length
    # datagen is a generator object that yields the encoded parameters
    datagen, headers = multipart_encode({"image": ostream})

    # Create the Request object
    request = urllib2.Request(UPLOAD_URL, datagen, headers)
    # Actually do the request, and get the response
    try:
        resp = urllib2.urlopen(request).read()
        #print "Upload done."
    except:
        print "Could not open URL:" + UPLOAD_URL
    # Ignore response.
    ostream.close()

############## Main

if len(sys.argv) != 2:
    raise RuntimeError("usage: photogen.py upload-url")

UPLOAD_URL = sys.argv[1]

endtime = time.time() + 60 # At most for 1 min.
camera = picamera.PiCamera()
camera.vflip = True
camera.hflip = True
register_openers()
poll = select.poll()
poll.register(0, select.POLLIN)
time.sleep(2) # Allow for camera warm-up

process_photo(camera)
evts = poll.poll(0)
while len(evts) == 0 and time.time() < endtime:
    process_photo(camera)
    evts = poll.poll(0)

#print "Final photo.."
process_photo(camera, text=True)
camera.close()
print "Photo processing done."
<|endoftext|>


<commit_before>import os
import csv

try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

try:
    from urllib.parse import urljoin
    from urllib.request import urlopen
    from urllib.error import URLError
except ImportError:
    # for Python 2.7 compatibility
    from urlparse import urljoin
    from urllib2 import urlopen
    from urllib2 import URLError

from django.shortcuts import render
from django.views import View
from django.http import HttpResponse, HttpResponseBadRequest
from django.conf import settings
from uw_sws.term import get_term_before, get_previous_term

class DataFileView(View):
    file_name = None

    def get(self, request):
        csv = self._get_csv()
        return HttpResponse(csv)

    def _get_csv(self):
        try:
            url = urljoin(getattr(settings, 'CSV_ROOT', None), self.file_name)
            response = urlopen(url)
            data = response.read()
        except ValueError:
            url = urljoin('file://', getattr(settings, 'CSV_ROOT', None))
            url = urljoin(url, self.file_name)
            response = urlopen(url)
            data = response.read()
        except Exception as err:
            data = "Error {}: {}".format(err.errno, err.reason)

        si = StringIO()
        cw = csv.writer(si)
        # csv.reader has to take in string not bytes...
        csv_reader = csv.reader(data.decode("utf-8").splitlines())

        # csv_data = [line.split(b",") for line in data.splitlines()][0]
        header = [str.lower() for str in next(csv_reader)]
        cw.writerow(header)
        # Columns we have to scrub out an & (note double quotes are included)
        # because thats how it is formatted in the csv files...
        scrub = ['major_path', 'code', 'key']
        check_index = []
        for s in scrub:
            if s in header:
                check_index.append(header.index(s))

        if len(check_index) == 0:
            for row in csv_reader:
                cw.writerow(row)
            return si.getvalue().strip('\r\n')
        else:
            for row in csv_reader:
                for index in check_index:
                    row[index] = row[index].replace(" ", "-")
                    row[index] = row[index].replace("&", "_AND_")
                    row[index] = row[index].replace(":", "_")
                cw.writerow(row)
            return si.getvalue().strip('\r\n')

class DataFileByQuarterView(DataFileView):
    """ Base class for views that take a time period queries
    """

    base_file_name = None

    def get(self, request):
        end_term = None
        end_year = request.GET.get("end_yr")

        if end_year is None:
            if end_term is None:
                end_term = get_term_before(get_previous_term())
            end_year = end_term.year % 100

        if len(str(end_year)) > 2:
            return HttpResponseBadRequest("Year must be in a 2 digit format")

        end_quarter = request.GET.get("end_qtr").lower()

        if end_quarter is None:
            if end_term is None:
                end_term = get_term_before(get_previous_term())
            end_quarter = end_term.quarter[:2]

        if end_quarter not in ["au", "wi", "sp", "su"]:
            return HttpResponseBadRequest("Quarter must be one of 'au',"
                                          + " 'wi', 'sp', or 'su'")

        num_qtrs = request.GET.get("num_qtrs", "8")

        try:
            int(num_qtrs)
        except ValueError:
            return HttpResponseBadRequest("Number of quarters must be"
                                          + " an integer")

        if int(num_qtrs) < 1:
            return HttpResponseBadRequest("Number of quarters must be"
                                          + " at least 1")

        self.file_name = end_quarter + str(end_year) + "_" + num_qtrs\
            + "qtrs_" + self.base_file_name

        try:
            return super(DataFileByQuarterView, self).get(request)
        except URLError:
            return HttpResponse("There is no data for the"
                                + " requested time period", status=416)

class MajorCourse(DataFileByQuarterView):
    base_file_name = "majors_and_courses.csv"

class DataMap(DataFileView):
    file_name = "data_map.csv"

class StudentData(DataFileByQuarterView):
    base_file_name = "student_data_all_majors.csv"

class StatusLookup(DataFileView):
    file_name = "status_lookup.csv"
<commit_msg>Swapped the point at which the casing is changed for the data api<commit_after>import os
import csv

try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

try:
    from urllib.parse import urljoin
    from urllib.request import urlopen
    from urllib.error import URLError
except ImportError:
    # for Python 2.7 compatibility
    from urlparse import urljoin
    from urllib2 import urlopen
    from urllib2 import URLError

from django.shortcuts import render
from django.views import View
from django.http import HttpResponse, HttpResponseBadRequest
from django.conf import settings
from uw_sws.term import get_term_before, get_previous_term

class DataFileView(View):
    file_name = None

    def get(self, request):
        csv = self._get_csv()
        return HttpResponse(csv)

    def _get_csv(self):
        try:
            url = urljoin(getattr(settings, 'CSV_ROOT', None), self.file_name)
            response = urlopen(url)
            data = response.read()
        except ValueError:
            url = urljoin('file://', getattr(settings, 'CSV_ROOT', None))
            url = urljoin(url, self.file_name)
            response = urlopen(url)
            data = response.read()
        except Exception as err:
            data = "Error {}: {}".format(err.errno, err.reason)

        si = StringIO()
        cw = csv.writer(si)
        # csv.reader has to take in string not bytes...
        csv_reader = csv.reader(data.decode("utf-8").splitlines())

        # csv_data = [line.split(b",") for line in data.splitlines()][0]
        header = [str.lower() for str in next(csv_reader)]
        cw.writerow(header)
        # Columns we have to scrub out an & (note double quotes are included)
        # because thats how it is formatted in the csv files...
        scrub = ['major_path', 'code', 'key']
        check_index = []
        for s in scrub:
            if s in header:
                check_index.append(header.index(s))

        if len(check_index) == 0:
            for row in csv_reader:
                cw.writerow(row)
            return si.getvalue().strip('\r\n')
        else:
            for row in csv_reader:
                for index in check_index:
                    row[index] = row[index].replace(" ", "-")
                    row[index] = row[index].replace("&", "_AND_")
                    row[index] = row[index].replace(":", "_")
                cw.writerow(row)
            return si.getvalue().strip('\r\n')

class DataFileByQuarterView(DataFileView):
    """ Base class for views that take a time period queries
    """

    base_file_name = None

    def get(self, request):
        end_term = None
        end_year = request.GET.get("end_yr")

        if end_year is None:
            if end_term is None:
                end_term = get_term_before(get_previous_term())
            end_year = end_term.year % 100

        if len(str(end_year)) > 2:
            return HttpResponseBadRequest("Year must be in a 2 digit format")

        end_quarter = request.GET.get("end_qtr")

        if end_quarter is None:
            if end_term is None:
                end_term = get_term_before(get_previous_term())
            end_quarter = end_term.quarter[:2].lower()

        if end_quarter not in ["au", "wi", "sp", "su"]:
            return HttpResponseBadRequest("Quarter must be one of 'au',"
                                          + " 'wi', 'sp', or 'su'")

        num_qtrs = request.GET.get("num_qtrs", "8")

        try:
            int(num_qtrs)
        except ValueError:
            return HttpResponseBadRequest("Number of quarters must be"
                                          + " an integer")

        if int(num_qtrs) < 1:
            return HttpResponseBadRequest("Number of quarters must be"
                                          + " at least 1")

        self.file_name = end_quarter + str(end_year) + "_" + num_qtrs\
            + "qtrs_" + self.base_file_name

        try:
            return super(DataFileByQuarterView, self).get(request)
        except URLError:
            return HttpResponse("There is no data for the"
                                + " requested time period", status=416)

class MajorCourse(DataFileByQuarterView):
    base_file_name = "majors_and_courses.csv"

class DataMap(DataFileView):
    file_name = "data_map.csv"

class StudentData(DataFileByQuarterView):
    base_file_name = "student_data_all_majors.csv"

class StatusLookup(DataFileView):
    file_name = "status_lookup.csv"
<|endoftext|>


<commit_before>import generator
from parser   import Parser
from compiler import SyntaxCompiler

def compile_string(syntax):
    """
    Builds a converter from the given syntax and returns it.

    @type  syntax: str
    @param syntax: A Gelatin syntax.
    @rtype:  compiler.Context
    @return: The compiled converter.
    """
    return Parser().parse_string(syntax, SyntaxCompiler())

def compile(syntax_file):
    """
    Like compile_string(), but reads the syntax from the file with the
    given name.

    @type  syntax_file: str
    @param syntax_file: Name of a file containing Gelatin syntax.
    @rtype:  compiler.Context
    @return: The compiled converter.
    """
    return Parser().parse(syntax_file, SyntaxCompiler())

def generate(converter, input_file, format = 'xml'):
    """
    Given a converter (as returned by compile()), this function reads
    the given input file and converts it to the requested output format.

    Supported output formats are 'xml', 'yaml', 'json', or 'none'.

    @type  converter: compiler.Context
    @param converter: The compiled converter.
    @type  input_file: str
    @param input_file: Name of a file to convert.
    @type  format: str
    @param format: The output format.
    @rtype:  str
    @return: The resulting output.
    """
    with open(input_file) as thefile:
        return generate_string(converter, thefile.read(), format = format)

def generate_to_file(converter, input_file, output_file, format = 'xml'):
    """
    Like generate(), but writes the output to the given output file
    instead.

    @type  converter: compiler.Context
    @param converter: The compiled converter.
    @type  input_file: str
    @param input_file: Name of a file to convert.
    @type  output_file: str
    @param output_file: The output filename.
    @type  format: str
    @param format: The output format.
    @rtype:  str
    @return: The resulting output.
    """
    with open(output_file, 'w') as thefile:
        result = generate(converter, input_file, format = format)
        thefile.write(result)

def generate_string(converter, input, format = 'xml'):
    """
    Like generate(), but reads the input from a string instead of
    from a file.

    @type  converter: compiler.Context
    @param converter: The compiled converter.
    @type  input: str
    @param input: The string to convert.
    @type  format: str
    @param format: The output format.
    @rtype:  str
    @return: The resulting output.
    """
    builder = generator.new(format)
    if builder is None:
        raise TypeError('invalid output format ' + repr(format))
    converter.parse_string(input, builder)
    return builder.serialize()

def generate_string_to_file(converter, input, output_file, format = 'xml'):
    """
    Like generate(), but reads the input from a string instead of
    from a file, and writes the output to the given output file.

    @type  converter: compiler.Context
    @param converter: The compiled converter.
    @type  input: str
    @param input: The string to convert.
    @type  output_file: str
    @param output_file: The output filename.
    @type  format: str
    @param format: The output format.
    @rtype:  str
    @return: The resulting output.
    """
    with open(output_file, 'w') as thefile:
        result = generate_string(converter, input_file, format = format)
        thefile.write(result)
<commit_msg>convert API docs to sphinx<commit_after>import generator
from parser   import Parser
from compiler import SyntaxCompiler

def compile_string(syntax):
    """
    Builds a converter from the given syntax and returns it.

    :type  syntax: str
    :param syntax: A Gelatin syntax.
    :rtype:  compiler.Context
    :return: The compiled converter.
    """
    return Parser().parse_string(syntax, SyntaxCompiler())

def compile(syntax_file):
    """
    Like compile_string(), but reads the syntax from the file with the
    given name.

    :type  syntax_file: str
    :param syntax_file: Name of a file containing Gelatin syntax.
    :rtype:  compiler.Context
    :return: The compiled converter.
    """
    return Parser().parse(syntax_file, SyntaxCompiler())

def generate(converter, input_file, format = 'xml'):
    """
    Given a converter (as returned by compile()), this function reads
    the given input file and converts it to the requested output format.

    Supported output formats are 'xml', 'yaml', 'json', or 'none'.

    :type  converter: compiler.Context
    :param converter: The compiled converter.
    :type  input_file: str
    :param input_file: Name of a file to convert.
    :type  format: str
    :param format: The output format.
    :rtype:  str
    :return: The resulting output.
    """
    with open(input_file) as thefile:
        return generate_string(converter, thefile.read(), format = format)

def generate_to_file(converter, input_file, output_file, format = 'xml'):
    """
    Like generate(), but writes the output to the given output file
    instead.

    :type  converter: compiler.Context
    :param converter: The compiled converter.
    :type  input_file: str
    :param input_file: Name of a file to convert.
    :type  output_file: str
    :param output_file: The output filename.
    :type  format: str
    :param format: The output format.
    :rtype:  str
    :return: The resulting output.
    """
    with open(output_file, 'w') as thefile:
        result = generate(converter, input_file, format = format)
        thefile.write(result)

def generate_string(converter, input, format = 'xml'):
    """
    Like generate(), but reads the input from a string instead of
    from a file.

    :type  converter: compiler.Context
    :param converter: The compiled converter.
    :type  input: str
    :param input: The string to convert.
    :type  format: str
    :param format: The output format.
    :rtype:  str
    :return: The resulting output.
    """
    builder = generator.new(format)
    if builder is None:
        raise TypeError('invalid output format ' + repr(format))
    converter.parse_string(input, builder)
    return builder.serialize()

def generate_string_to_file(converter, input, output_file, format = 'xml'):
    """
    Like generate(), but reads the input from a string instead of
    from a file, and writes the output to the given output file.

    :type  converter: compiler.Context
    :param converter: The compiled converter.
    :type  input: str
    :param input: The string to convert.
    :type  output_file: str
    :param output_file: The output filename.
    :type  format: str
    :param format: The output format.
    :rtype:  str
    :return: The resulting output.
    """
    with open(output_file, 'w') as thefile:
        result = generate_string(converter, input_file, format = format)
        thefile.write(result)
<|endoftext|>


<commit_before># _*_ coding: utf-8 _*_

data = {('Roberts', 'John'): [20.00, 15.00],
        ('Edwards', 'Mary'): [5.00, 30.50],
        ('asdf', 'brice'): [2.35],
        }

def test_sortbydonation():
    """Test Sort By donation function in mailroom"""
    from mailroom import sort_by_donation
    assert type(sort_by_donation(data)) == list
    assert len(sort_by_donation(data)) == 3

def test_getname():
	from mailroom import get_name
	name = 'Mike Kang Harrison'
	assert type(get_name(name)) == tuple
	assert len(get_name(name)) == 2<commit_msg>Added more tests. Fixed sortbydonation<commit_after># _*_ coding: utf-8 _*_

data = {('Roberts', 'John'): [20.00, 15.00],
        ('Edwards', 'Mary'): [5.00, 30.50],
        ('asdf', 'brice'): [2.35],
        }

def test_sortbydonation():
    """Test Sort By donation function in mailroom"""
    from mailroom import sort_by_donation
    assert type(sort_by_donation(data)) == list
    #assert len(sort_by_donation(data)) == 3
    data2 = sort_by_donation(data)
    for i in range(len(data2) - 1):
        assert data2[i] > data2[i + 1]  or len(data2) < 2

def test_getname():
    """Test Get Name function in mailroom"""
    from mailroom import get_name
    name = 'Mike Kang Harrison'
    assert type(get_name(name)) == tuple
    assert len(get_name(name)) == 2

def test_menuprompot():
	""" Test menu prompt"""
	from mailroom import menu_prompt
	assert menu_prompt('2') == '2'

def test_getdonation():
	"""Test Get Donation"""
	from mailroom import get_donation
	assert get_donation('2') == 2.0

<|endoftext|>


<commit_before>"""
Extract tracebacks.
"""

import re

TRACEBACK_RE = re.compile(
    """.*(Traceback[ ]\(most[ ]recent[ ]call[ ]last\):[ ]*[\n] # traceback line
       ([ ]+[^\n]*[\n])+ # at least one File/code line, start with spaces
       ([^ ]+[^\n]*[\n])? # first error line, if there is one
       ([^\n]+[\n]?)*) # any remaining error lines, to a blank line or eos
    """,
    re.DOTALL | re.MULTILINE | re.VERBOSE)

def extract_tracebacks(txt):
    """
    Extract any and all tracebacks from txt.

    We make the simplifying assumption that if there are multiple
    tracebacks, they are separated by at least one blank line.  LATER
    make this smarter.
    """
    txt = _normalize_linebreaks(txt)
    tbs = [extract_traceback(chunk) for chunk in txt.split('\n\n')]
    return [tb for tb in tbs if tb]

def extract_traceback(chunk):
    """
    Attempts to extract a single traceback from a chunk of text,
    returning just the traceback if found, else None.

    Note that if chunk contains multiple tracebacks, this function will
    become violently confused.

    Chunk should have only \n's as line separators, not \r or \r\n.
    """
    matched = TRACEBACK_RE.match(chunk)
    if matched:
        return matched.group(1)
    else:
        return None

def _normalize_linebreaks(txt):
    """
    Make all line breaks \n only.
    """
    return txt.replace('\r\n', '\n').replace('\r', '\n')
<commit_msg>Specify this is python tracebacks, leave space for future langs.<commit_after>"""
Extract tracebacks.
"""

import re

PY_TRACEBACK_RE = re.compile(
    """.*(Traceback[ ]\(most[ ]recent[ ]call[ ]last\):[ ]*[\n] # traceback line
       ([ ]+[^\n]*[\n])+ # at least one File/code line, start with spaces
       ([^ ]+[^\n]*[\n])? # first error line, if there is one
       ([^\n]+[\n]?)*) # any remaining error lines, to a blank line or eos
    """,
    re.DOTALL | re.MULTILINE | re.VERBOSE)

ALL_RES = [PY_TRACEBACK_RE]

def extract_tracebacks(txt):
    """
    Extract any and all tracebacks from txt.

    We make the simplifying assumption that if there are multiple
    tracebacks, they are separated by at least one blank line.  LATER
    make this smarter.
    """
    txt = _normalize_linebreaks(txt)
    tbs = [extract_traceback(chunk) for chunk in txt.split('\n\n')]
    return [tb for tb in tbs if tb]

def extract_traceback(chunk):
    """
    Attempts to extract a single traceback from a chunk of text,
    returning just the traceback if found, else None.

    Note that if chunk contains multiple tracebacks, this function will
    become violently confused.

    Chunk should have only \n's as line separators, not \r or \r\n.
    """
    for matcher in ALL_RES:
        matched = matcher.match(chunk)
        if matched:
            return matched.group(1)
    return None

def _normalize_linebreaks(txt):
    """
    Make all line breaks \n only.
    """
    return txt.replace('\r\n', '\n').replace('\r', '\n')
<|endoftext|>


<commit_before># Copyright Least Authority Enterprises.
# See LICENSE for details.

"""
Functional structures for representing different kinds of Kubernetes
state.
"""

from uuid import uuid4

from zope.interface import provider, implementer

from pyrsistent import CheckedPVector, PClass, field, pmap_field, pset, freeze, thaw

from twisted.python.filepath import FilePath

from . import IObject, IObjectLoader
from ._invariants import instance_of, provider_of
from ._swagger import Swagger

spec = Swagger.from_path(FilePath(__file__).sibling(u"kubernetes-1.5.json"))

class ObjectMeta(spec.pclass_for_definition(u"v1.ObjectMeta")):
    pass

class NamespaceStatus(PClass):
    """
    ``NamespaceStatus`` instances model `Kubernetes namespace status
    <https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_namespacestatus>`_.
    """
    phase = field(mandatory=True)

    @classmethod
    def active(cls):
        return cls(phase=u"Active")

    @classmethod
    def terminating(cls):
        return cls(phase=u"Terminating")

    @classmethod
    def from_raw(cls, status):
        return cls(phase=status[u"phase"])

    def to_raw(self):
        return {u"phase": self.phase}

@provider(IObjectLoader)
@implementer(IObject)
class Namespace(PClass):
    """
    ``Namespace`` instances model `Kubernetes namespaces
    <https://kubernetes.io/docs/user-guide/namespaces/>`_.
    """
    kind = u"Namespace"

    metadata = field(
        mandatory=True,
        invariant=instance_of(ObjectMeta),
    )

    status = field(mandatory=True, type=(NamespaceStatus, type(None)))

    @classmethod
    def default(cls):
        """
        Get the default namespace.
        """
        return cls.named(u"default")

    @classmethod
    def from_raw(cls, raw):
        try:
            status_raw = raw[u"status"]
        except KeyError:
            status = None
        else:
            status = NamespaceStatus.from_raw(status_raw)
        return cls(
            metadata=ObjectMeta(**raw[u"metadata"]),
            status=status,
        )

    @classmethod
    def named(cls, name):
        """
        Create an object with only the name metadata item.
        """
        return cls(
            metadata=ObjectMeta(name=name),
            status=None,
        )

    def fill_defaults(self):
        return self.transform(
            # TODO Also creationTimestamp, resourceVersion, maybe selfLink.
            # Also, should this clobber existing values or leave them alone?
            [u"metadata", u"uid"], unicode(uuid4()),
            [u"status"], NamespaceStatus.active(),
        )

    def to_raw(self):
        result = {
            u"kind": self.kind,
            u"apiVersion": u"v1",
            u"metadata": self.metadata.serialize(),
            u"spec": {},
        }
        if self.status is not None:
            result[u"status"] = self.status.to_raw()
        return result

@provider(IObjectLoader)
@implementer(IObject)
class ConfigMap(PClass):
    """
    ``ConfigMap`` instances model `ConfigMap objects
    <https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_configmap>`_.
    """
    kind = u"ConfigMap"

    metadata = field(
        mandatory=True,
        invariant=instance_of(ObjectMeta),
    )

    data = pmap_field(unicode, unicode, optional=True)

    @classmethod
    def from_raw(cls, raw):
        return cls(
            metadata=ObjectMeta(**raw[u"metadata"]),
            data=raw.get(u"data", None),
        )

    def fill_defaults(self):
        # TODO Surely some stuff to fill.
        return self

    def to_raw(self):
        result = {
            u"kind": self.kind,
            u"apiVersion": u"v1",
            u"metadata": self.metadata.serialize(),
        }
        if self.data is not None:
            # Kubernetes only includes the item if there is some data.
            #
            # XXX I'm not sure if there's a difference between no data and
            # data with no items.
            result[u"data"] = thaw(self.data)
        return result

def object_sort_key(obj):
    """
    Define a predictable sort ordering for Kubernetes objects.

    This should be the same ordering that Kubernetes itself imposes.
    """
    return (
        # Not all objects have a namespace.
        getattr(obj.metadata, "namespace", None),
        obj.metadata.name,
    )

def _pvector_field(iface):
    class _CheckedIObjectPVector(CheckedPVector):
        __invariant__ = provider_of(iface)

    return field(
        mandatory=True,
        type=_CheckedIObjectPVector,
        factory=lambda v: _CheckedIObjectPVector.create(sorted(v, key=object_sort_key)),
        initial=_CheckedIObjectPVector(),
    )

@provider(IObjectLoader)
@implementer(IObject)
class ObjectCollection(PClass):
    """
    ``ObjectList`` is a collection of Kubernetes objects.

    This roughly corresponds to the `*List` Kubernetes types.  It's not clear
    this is actually more useful than a native Python collection such as a set
    but we'll try it out.

    :ivar pvector items: The objects belonging to this collection.
    """
    kind = u"List"
    items = _pvector_field(IObject)

    @classmethod
    def from_raw(cls, raw):
        items = (
            # Unfortunately `kind` is an optional field.  Fortunately, the
            # top-level `kind` is something like `ConfigMapList` if you
            # asked for `.../configmaps/`.  So pass that down as a hint.
            object_from_raw(obj, raw[u"kind"][:-len(u"List")])
            for obj
            in raw[u"items"]
        )
        return cls(items=items)

    def to_raw(self):
        return {
            u"kind": self.kind,
            u"apiVersion": u"v1",
            u"metadata": {},
            u"items": list(
                obj.to_raw()
                for obj
                in self.items
            ),
        }

    def item_by_name(self, name):
        """
        Find an item in this collection by its name metadata.

        :param unicode name: The name of the object for which to search.

        :raise KeyError: If no object matching the given name is found.
        :return IObject: The object with the matching name.
        """
        for obj in self.items:
            if obj.metadata.name == name:
                return obj
        raise KeyError(name)

    def add(self, obj):
        return self.transform([u"items"], add(obj))

    def replace(self, old, new):
        return self.transform(
            [u"items"], remove(old),
            [u"items"], add(new),
        )

def add(value):
    def evolver(pvector):
        return sorted(pvector.append(value), key=object_sort_key)
    return evolver

def remove(value):
    def evolver(pset):
        return pset.remove(value)
    return evolver

_loaders = {
    loader.kind: loader
    for loader
    in {Namespace, ConfigMap}
}

def object_from_raw(raw, kind_hint=None):
    """
    Load an object of unspecified type from the raw representation of it.

    :raise KeyError: If the kind of object is unsupported.

    :return IObject: The loaded object.
    """
    kind = raw.get(u"kind", kind_hint)
    if kind.endswith(u"List"):
        loader = ObjectCollection
    else:
        loader = _loaders[kind]
    return loader.from_raw(raw)
<commit_msg>Add an issue link for this TODO.<commit_after># Copyright Least Authority Enterprises.
# See LICENSE for details.

"""
Functional structures for representing different kinds of Kubernetes
state.
"""

from uuid import uuid4

from zope.interface import provider, implementer

from pyrsistent import CheckedPVector, PClass, field, pmap_field, pset, freeze, thaw

from twisted.python.filepath import FilePath

from . import IObject, IObjectLoader
from ._invariants import instance_of, provider_of
from ._swagger import Swagger

spec = Swagger.from_path(FilePath(__file__).sibling(u"kubernetes-1.5.json"))

class ObjectMeta(spec.pclass_for_definition(u"v1.ObjectMeta")):
    pass

class NamespaceStatus(PClass):
    """
    ``NamespaceStatus`` instances model `Kubernetes namespace status
    <https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_namespacestatus>`_.
    """
    phase = field(mandatory=True)

    @classmethod
    def active(cls):
        return cls(phase=u"Active")

    @classmethod
    def terminating(cls):
        return cls(phase=u"Terminating")

    @classmethod
    def from_raw(cls, status):
        return cls(phase=status[u"phase"])

    def to_raw(self):
        return {u"phase": self.phase}

@provider(IObjectLoader)
@implementer(IObject)
class Namespace(PClass):
    """
    ``Namespace`` instances model `Kubernetes namespaces
    <https://kubernetes.io/docs/user-guide/namespaces/>`_.
    """
    kind = u"Namespace"

    metadata = field(
        mandatory=True,
        invariant=instance_of(ObjectMeta),
    )

    status = field(mandatory=True, type=(NamespaceStatus, type(None)))

    @classmethod
    def default(cls):
        """
        Get the default namespace.
        """
        return cls.named(u"default")

    @classmethod
    def from_raw(cls, raw):
        try:
            status_raw = raw[u"status"]
        except KeyError:
            status = None
        else:
            status = NamespaceStatus.from_raw(status_raw)
        return cls(
            metadata=ObjectMeta(**raw[u"metadata"]),
            status=status,
        )

    @classmethod
    def named(cls, name):
        """
        Create an object with only the name metadata item.
        """
        return cls(
            metadata=ObjectMeta(name=name),
            status=None,
        )

    def fill_defaults(self):
        return self.transform(
            # TODO Also creationTimestamp, resourceVersion, maybe selfLink.
            # Also, should this clobber existing values or leave them alone?
            # See https://github.com/LeastAuthority/txkube/issues/36
            [u"metadata", u"uid"], unicode(uuid4()),
            [u"status"], NamespaceStatus.active(),
        )

    def to_raw(self):
        result = {
            u"kind": self.kind,
            u"apiVersion": u"v1",
            u"metadata": self.metadata.serialize(),
            u"spec": {},
        }
        if self.status is not None:
            result[u"status"] = self.status.to_raw()
        return result

@provider(IObjectLoader)
@implementer(IObject)
class ConfigMap(PClass):
    """
    ``ConfigMap`` instances model `ConfigMap objects
    <https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_configmap>`_.
    """
    kind = u"ConfigMap"

    metadata = field(
        mandatory=True,
        invariant=instance_of(ObjectMeta),
    )

    data = pmap_field(unicode, unicode, optional=True)

    @classmethod
    def from_raw(cls, raw):
        return cls(
            metadata=ObjectMeta(**raw[u"metadata"]),
            data=raw.get(u"data", None),
        )

    def fill_defaults(self):
        # TODO Surely some stuff to fill.
        return self

    def to_raw(self):
        result = {
            u"kind": self.kind,
            u"apiVersion": u"v1",
            u"metadata": self.metadata.serialize(),
        }
        if self.data is not None:
            # Kubernetes only includes the item if there is some data.
            #
            # XXX I'm not sure if there's a difference between no data and
            # data with no items.
            result[u"data"] = thaw(self.data)
        return result

def object_sort_key(obj):
    """
    Define a predictable sort ordering for Kubernetes objects.

    This should be the same ordering that Kubernetes itself imposes.
    """
    return (
        # Not all objects have a namespace.
        getattr(obj.metadata, "namespace", None),
        obj.metadata.name,
    )

def _pvector_field(iface):
    class _CheckedIObjectPVector(CheckedPVector):
        __invariant__ = provider_of(iface)

    return field(
        mandatory=True,
        type=_CheckedIObjectPVector,
        factory=lambda v: _CheckedIObjectPVector.create(sorted(v, key=object_sort_key)),
        initial=_CheckedIObjectPVector(),
    )

@provider(IObjectLoader)
@implementer(IObject)
class ObjectCollection(PClass):
    """
    ``ObjectList`` is a collection of Kubernetes objects.

    This roughly corresponds to the `*List` Kubernetes types.  It's not clear
    this is actually more useful than a native Python collection such as a set
    but we'll try it out.

    :ivar pvector items: The objects belonging to this collection.
    """
    kind = u"List"
    items = _pvector_field(IObject)

    @classmethod
    def from_raw(cls, raw):
        items = (
            # Unfortunately `kind` is an optional field.  Fortunately, the
            # top-level `kind` is something like `ConfigMapList` if you
            # asked for `.../configmaps/`.  So pass that down as a hint.
            object_from_raw(obj, raw[u"kind"][:-len(u"List")])
            for obj
            in raw[u"items"]
        )
        return cls(items=items)

    def to_raw(self):
        return {
            u"kind": self.kind,
            u"apiVersion": u"v1",
            u"metadata": {},
            u"items": list(
                obj.to_raw()
                for obj
                in self.items
            ),
        }

    def item_by_name(self, name):
        """
        Find an item in this collection by its name metadata.

        :param unicode name: The name of the object for which to search.

        :raise KeyError: If no object matching the given name is found.
        :return IObject: The object with the matching name.
        """
        for obj in self.items:
            if obj.metadata.name == name:
                return obj
        raise KeyError(name)

    def add(self, obj):
        return self.transform([u"items"], add(obj))

    def replace(self, old, new):
        return self.transform(
            [u"items"], remove(old),
            [u"items"], add(new),
        )

def add(value):
    def evolver(pvector):
        return sorted(pvector.append(value), key=object_sort_key)
    return evolver

def remove(value):
    def evolver(pset):
        return pset.remove(value)
    return evolver

_loaders = {
    loader.kind: loader
    for loader
    in {Namespace, ConfigMap}
}

def object_from_raw(raw, kind_hint=None):
    """
    Load an object of unspecified type from the raw representation of it.

    :raise KeyError: If the kind of object is unsupported.

    :return IObject: The loaded object.
    """
    kind = raw.get(u"kind", kind_hint)
    if kind.endswith(u"List"):
        loader = ObjectCollection
    else:
        loader = _loaders[kind]
    return loader.from_raw(raw)
<|endoftext|>