<commit_before>import StringIO
import base64
import datetime
import time
import numpy
import aplpy
from scipy.stats import scoreatpercentile
import matplotlib
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
from .image import open_image
import dbase

def image(dbimage, scale=0.9, plotsources=None, database=None):
    figure = Figure(figsize=(5, 5))
    canvas = FigureCanvasAgg(figure)
    image = aplpy.FITSFigure(dbimage['url'], figure=figure, auto_refresh=False)
    image.show_grayscale()
    image.tick_labels.set_font(size=5)
    if plotsources:
        ra = [source['ra'] for source in plotsources]
        dec = [source['decl'] for source in plotsources]
        image.show_markers(ra, dec, s=40, facecolor='none', edgecolor='green')
    memfig = StringIO.StringIO()
    canvas.print_figure(memfig, format='png', transparent=True)
    encoded_png = StringIO.StringIO()
    encoded_png.write('data:image/png;base64,\n')
    encoded_png.write(base64.b64encode(memfig.getvalue()))
    return encoded_png.getvalue()

def lightcurve(lc, T0=None, response=None, images=None, trigger_index=None, size=(8, 8)):
    #dates = matplotlib.dates.date2num([point[0] for point in lc])
    times = numpy.array([time.mktime(point[0].timetuple()) for point in lc])
    inttimes = [point[1]/2. for point in lc]
    fluxes = [point[2] for point in lc]
    errors = [point[3] for point in lc]
    if T0 is None:
        tmin = sorted(times)[0]
        if images:
            tmin2 = time.mktime(sorted(zip(*images)[0])[0].timetuple())
            if tmin2  < tmin:
                tmin = tmin2
        tmin = datetime.datetime.fromtimestamp(tmin)
        T0 = datetime.datetime(tmin.year, tmin.month, tmin.day, 0, 0, 0)
    tdiff = T0 - datetime.datetime(1970, 1, 1)
    tdiff = (tdiff.microseconds + (tdiff.seconds + tdiff.days * 86400) * 1e6) / 1e6
    times -= tdiff
    figure = Figure(figsize=size)
    axes = figure.add_subplot(1, 1, 1)
    axes.errorbar(x=times, y=fluxes, yerr=errors, xerr=numpy.array(inttimes)/2., fmt='bo')
    if trigger_index is not None:
        axes.errorbar(x=times[trigger_index], y=fluxes[trigger_index], fmt='o', mec='r', ms=15., mfc='None')
    ylimits = axes.get_ylim()
    if images:
        images = zip(*images)
        x = numpy.array([time.mktime(x.timetuple()) for x in images[0]]) - tdiff
        xerr = numpy.array(images[1])/2.
        patches = [Rectangle((xx - xxerr, ylimits[0]), xxerr, ylimits[1]-ylimits[0])
                for xx, xxerr in zip(x, xerr)]
        patches = PatchCollection(patches, alpha=0.3, linewidth=0, visible=True, color='r')
        axes.add_collection(patches)
    axes.set_xlabel('Seconds since %s' % T0.strftime('%Y-%m-%dT%H:%M:%S'))
    axes.set_ylabel('Flux (Jy)')
    canvas = FigureCanvasAgg(figure)
    if response:
       canvas.print_figure(response, format='png')
       return response
    memfig = StringIO.StringIO()
    canvas.print_figure(memfig, format='png')
    encoded_png = StringIO.StringIO()
    encoded_png.write('data:image/png;base64,\n')
    encoded_png.write(base64.b64encode(memfig.getvalue()))
    return encoded_png.getvalue()
<commit_msg>Updated image plots: show Gaussian fits<commit_after>import StringIO
import base64
import datetime
import time
import numpy
import aplpy
from scipy.stats import scoreatpercentile
import matplotlib
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
from .image import open_image
import dbase

def image(dbimage, scale=0.9, plotsources=None, database=None):
    figure = Figure(figsize=(5, 5))
    canvas = FigureCanvasAgg(figure)
    image = aplpy.FITSFigure(dbimage['url'], figure=figure, auto_refresh=False)
    image.show_grayscale()
    image.tick_labels.set_font(size=5)
    if plotsources:
        ra = [source['ra'] for source in plotsources]
        dec = [source['decl'] for source in plotsources]
        major = [source['major']/900 for source in plotsources]
        minor = [source['minor']/900 for source in plotsources]
        angle = [source['theta']+90 for source in plotsources]
        image.show_ellipses(ra, dec, major, minor, angle, facecolor='none', edgecolor='green')
        #show_ellipses(self, xw, yw, width, height, angle=0, layer=False, zorder=None, **kwargs):
        #image.show_markers(ra, dec, s=40, facecolor='none', edgecolor='green')
    memfig = StringIO.StringIO()
    canvas.print_figure(memfig, format='png', transparent=True)
    encoded_png = StringIO.StringIO()
    encoded_png.write('data:image/png;base64,\n')
    encoded_png.write(base64.b64encode(memfig.getvalue()))
    return encoded_png.getvalue()

def lightcurve(lc, T0=None, response=None, images=None, trigger_index=None, size=(8, 8)):
    #dates = matplotlib.dates.date2num([point[0] for point in lc])
    times = numpy.array([time.mktime(point[0].timetuple()) for point in lc])
    inttimes = [point[1]/2. for point in lc]
    fluxes = [point[2] for point in lc]
    errors = [point[3] for point in lc]
    if T0 is None:
        tmin = sorted(times)[0]
        if images:
            tmin2 = time.mktime(sorted(zip(*images)[0])[0].timetuple())
            if tmin2  < tmin:
                tmin = tmin2
        tmin = datetime.datetime.fromtimestamp(tmin)
        T0 = datetime.datetime(tmin.year, tmin.month, tmin.day, 0, 0, 0)
    tdiff = T0 - datetime.datetime(1970, 1, 1)
    tdiff = (tdiff.microseconds + (tdiff.seconds + tdiff.days * 86400) * 1e6) / 1e6
    times -= tdiff
    figure = Figure(figsize=size)
    axes = figure.add_subplot(1, 1, 1)
    axes.errorbar(x=times, y=fluxes, yerr=errors, xerr=numpy.array(inttimes)/2., fmt='bo')
    if trigger_index is not None:
        axes.errorbar(x=times[trigger_index], y=fluxes[trigger_index], fmt='o', mec='r', ms=15., mfc='None')
    ylimits = axes.get_ylim()
    if images:
        images = zip(*images)
        x = numpy.array([time.mktime(x.timetuple()) for x in images[0]]) - tdiff
        xerr = numpy.array(images[1])/2.
        patches = [Rectangle((xx - xxerr, ylimits[0]), xxerr, ylimits[1]-ylimits[0])
                for xx, xxerr in zip(x, xerr)]
        patches = PatchCollection(patches, alpha=0.3, linewidth=0, visible=True, color='r')
        axes.add_collection(patches)
    axes.set_xlabel('Seconds since %s' % T0.strftime('%Y-%m-%dT%H:%M:%S'))
    axes.set_ylabel('Flux (Jy)')
    canvas = FigureCanvasAgg(figure)
    if response:
       canvas.print_figure(response, format='png')
       return response
    memfig = StringIO.StringIO()
    canvas.print_figure(memfig, format='png')
    encoded_png = StringIO.StringIO()
    encoded_png.write('data:image/png;base64,\n')
    encoded_png.write(base64.b64encode(memfig.getvalue()))
    return encoded_png.getvalue()
<|endoftext|>


<commit_before>from collections import defaultdict
from datetime import time, timedelta
from django.utils import timezone

import pytz

from .user_utils import to_utc

def timezone_lookup(tz):
    """Given a timezone name (e.g. EST, CDT) or a timezone offset (e.g. -0600)
    construct a dictionary of timezone names from pytz, and return the matching
    timezone string.

    """
    tz_lookups = defaultdict(set)
    for tzname in pytz.all_timezones:
        dt = timezone.now().astimezone(pytz.timezone(tzname))
        tz_lookups[dt.tzname()].add(tzname)
        tz_lookups[dt.strftime("%z")].add(tzname)
    return (tz_lookups[tz], tz_lookups)

def dates_range(num_days):
    """Return a generator that yields a datetime.date object for `num_days`
    in the past.

    """
    today = timezone.now()
    for d in range(num_days):
        yield today - timedelta(days=d)

def date_range(datetime_obj, tz=timezone.utc):
    """Return a range, `(start, end)`, tuple that spans the full day for the
    given datetime object.

    """
    start = datetime_obj.combine(datetime_obj, time.min)
    end = datetime_obj.combine(datetime_obj, time.max)

    if timezone.is_naive(start) and timezone.is_naive(end):
        start = timezone.make_aware(start, timezone=tz)
        end = timezone.make_aware(end, timezone=tz)

    return (start, end)

def print_utc_to_localtime(local="America/Chicago", tz=None):
    """Prints a UTC -> localtime table. Useful for wrapping your head around
    timezone issues.

    Arguments:

    * local -- the local timzone that you want information about. It must be
      a timzone string that is understood by pytz (e.g. America/Chicago)
    * tz -- optional: a timezone object to use as the local time (instead of
      the local keyword).

    """
    if tz is None and local:
        tz = pytz.timezone(local)

    s = "{} ---> {}"
    ltime = timezone.now().astimezone(tz)
    ltime = ltime.replace(hour=0, minute=0, second=0, microsecond=0)
    fmt = "%a %b %d, %H:%M %Z"  # Wed Oct 14, 15:00 CST
    for hour in range(0, 23):
        ltime = ltime.replace(hour=hour)
        utctime = to_utc(ltime)
        print(s.format(utctime.strftime(fmt), ltime.strftime(fmt)))
<commit_msg>adding a function to format datetimes using DRF settings<commit_after>from collections import defaultdict
from datetime import time, timedelta

from django.conf import settings
from django.utils import timezone

import pytz

from .user_utils import to_utc

def format_datetime(value):
    """Given a datetime object, format it using the datetime format specified
    for djagno rest framework."""
    return value.strftime(settings.REST_FRAMEWORK['DATETIME_FORMAT'])

def timezone_lookup(tz):
    """Given a timezone name (e.g. EST, CDT) or a timezone offset (e.g. -0600)
    construct a dictionary of timezone names from pytz, and return the matching
    timezone string.

    """
    tz_lookups = defaultdict(set)
    for tzname in pytz.all_timezones:
        dt = timezone.now().astimezone(pytz.timezone(tzname))
        tz_lookups[dt.tzname()].add(tzname)
        tz_lookups[dt.strftime("%z")].add(tzname)
    return (tz_lookups[tz], tz_lookups)

def dates_range(num_days):
    """Return a generator that yields a datetime.date object for `num_days`
    in the past.

    """
    today = timezone.now()
    for d in range(num_days):
        yield today - timedelta(days=d)

def date_range(datetime_obj, tz=timezone.utc):
    """Return a range, `(start, end)`, tuple that spans the full day for the
    given datetime object.

    """
    start = datetime_obj.combine(datetime_obj, time.min)
    end = datetime_obj.combine(datetime_obj, time.max)

    if timezone.is_naive(start) and timezone.is_naive(end):
        start = timezone.make_aware(start, timezone=tz)
        end = timezone.make_aware(end, timezone=tz)

    return (start, end)

def print_utc_to_localtime(local="America/Chicago", tz=None):
    """Prints a UTC -> localtime table. Useful for wrapping your head around
    timezone issues.

    Arguments:

    * local -- the local timzone that you want information about. It must be
      a timzone string that is understood by pytz (e.g. America/Chicago)
    * tz -- optional: a timezone object to use as the local time (instead of
      the local keyword).

    """
    if tz is None and local:
        tz = pytz.timezone(local)

    s = "{} ---> {}"
    ltime = timezone.now().astimezone(tz)
    ltime = ltime.replace(hour=0, minute=0, second=0, microsecond=0)
    fmt = "%a %b %d, %H:%M %Z"  # Wed Oct 14, 15:00 CST
    for hour in range(0, 23):
        ltime = ltime.replace(hour=hour)
        utctime = to_utc(ltime)
        print(s.format(utctime.strftime(fmt), ltime.strftime(fmt)))
<|endoftext|>


<commit_before>#!/usr/bin/python
# -*- coding: utf-8 -*-

# thumbor imaging service
# https://github.com/globocom/thumbor/wiki

# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com

import sys
import functools
from os.path import splitext
import datetime
import traceback

import tornado.web

from thumbor import __version__
from thumbor.storages.no_storage import Storage as NoStorage
from thumbor.storages.mixed_storage import Storage as MixedStorage
from thumbor.context import Context
from thumbor.transformer import Transformer
from thumbor.engines import BaseEngine
from thumbor.engines.gif import Engine as GifEngine
from thumbor.engines.json_engine import JSONEngine
from thumbor.utils import logger
import thumbor.filters

CONTENT_TYPE = {
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.gif': 'image/gif',
    '.png': 'image/png',
    '.webp': 'image/webp'
}

class BaseHandler(tornado.web.RequestHandler):
    def _error(self, status, msg=None):
        self.set_status(status)
        if msg is not None:
            logger.warn(msg)
        self.finish()

    def execute_image_operations(self):
        self.context.request.quality = None

        req = self.context.request
        conf = self.context.config

        req.extension = splitext(req.image_url)[-1].lower()

        should_store = self.context.config.RESULT_STORAGE_STORES_UNSAFE or not self.context.request.unsafe
        if self.context.modules.result_storage and should_store:
            result = self.context.modules.result_storage.get()
            if result is not None:
                logger.debug('[RESULT_STORAGE] IMAGE FOUND: %s' % req.url)
                self.finish_request(self.context, result)
                return

        if conf.MAX_WIDTH and (not isinstance(req.width, basestring)) and req.width > conf.MAX_WIDTH:
            req.width = conf.MAX_WIDTH
        if conf.MAX_HEIGHT and (not isinstance(req.height, basestring)) and req.height > conf.MAX_HEIGHT:
            req.height = conf.MAX_HEIGHT

        req.meta_callback = conf.META_CALLBACK_NAME or self.request.arguments.get('callback', [None])[0]

        self.filters_runner = self.context.filters_factory.create_instances(self.context, self.context.request.filters)
        self.filters_runner.apply_filters(thumbor.filters.PHASE_PRE_LOAD, self.get_image)

    def get_image(self):
        def callback(normalized, buffer=None, engine=None):
            req = self.context.request

            if engine is None:
                if buffer is None:
                    self._error(404)
                    return

                engine = self.context.request.engine
                engine.load(buffer, req.extension)

            self.normalize_crops(normalized, req, engine)

            if req.meta:
                self.context.request.engine = JSONEngine(engine, req.image_url, req.meta_callback)

            after_transform_cb = functools.partial(self.after_transform, self.context)
            Transformer(self.context).transform(after_transform_cb)

        self._fetch(self.context.request.image_url, self.context.request.extension, callback)

    def normalize_crops(self, normalized, req, engine):
        new_crops = None
        if normalized and req.should_crop:
            crop_left = req.crop['left']
            crop_top = req.crop['top']
            crop_right = req.crop['right']
            crop_bottom = req.crop['bottom']

            actual_width, actual_height = engine.size

            if not req.width and not req.height:
                actual_width = engine.size[0]
                actual_height = engine.size[1]
            elif req.width:
                actual_height = engine.get_proportional_height(engine.size[0])
            elif req.height:
                actual_width = engine.get_proportional_width(engine.size[1])

            new_crops = self.translate_crop_coordinates(
                engine.source_width,
                engine.source_height,
                actual_width,
                actual_height,
                crop_left,
                crop_top,
                crop_right,
                crop_bottom
            )
            req.crop['left'] = new_crops[0]
            req.crop['top'] = new_crops[1]
            req.crop['right'] = new_crops[2]
            req.crop['bottom'] = new_crops[3]

    def after_transform(self, context):
        finish_callback = functools.partial(self.finish_request, context)
        if context.request.extension == '.gif' and context.config.USE_GIFSICLE_ENGINE:
            finish_callback()
        else:
            self.filters_runner.apply_filters(thumbor.filters.PHASE_POST_TRANSFORM, finish_callback)

    def define_image_type(self, context, result):
        if result is not None:
            image_extension = BaseEngine.get_mimetype(result)
        else:
            image_extension = context.request.format
            if image_extension is not None:
                image_extension = '.%s' % image_extension
                logger.debug('Image format specified as %s.' % image_extension)
            elif context.config.AUTO_WEBP and context.request.accepts_webp and not context.request.engine.is_multiple():
                image_extension = '.webp'
                logger.debug('Image format set by AUTO_WEBP as %s.' % image_extension)
            else:
                image_extension = context.request.engine.extension
                logger.debug('No image format specified. Retrieving from the image extension: %s.' % image_extension)

        content_type = CONTENT_TYPE.get(image_extension, CONTENT_TYPE['.jpg'])

        if context.request.meta:
            context.request.meta_callback = context.config.META_CALLBACK_NAME or self.request.arguments.get('callback', [None])[0]
            content_type = 'text/javascript' if context.request.meta_callback else 'application/json'
            logger.debug('Metadata requested. Serving content type of %s.' % content_type)

        logger.debug('Content Type of %s detected.' % content_type)

        return image_extension, content_type

    def finish_request(self, context, result=None):
        image_extension, content_type = self.define_image_type(context, result)

        quality = self.context.request.quality
        if quality is None:
            if image_extension == '.webp':
                quality = self.context.config.get('WEBP_QUALITY', self.context.config.QUALITY)
            else:
                quality = self.context.config.QUALITY

        self.set_header('Content-Type', content_type)
        self.set_header('Server', 'Thumbor/%s' % __version__)

        if context.config.AUTO_WEBP and not context.request.engine.is_multiple() and context.request.engine.extension != '.webp':
            self.set_header('Vary', 'Accept')

        max_age = self.context.config.MAX_AGE
        if context.request.prevent_result_storage or context.request.detection_error:
            max_age = self.context.config.MAX_AGE_TEMP_IMAGE

        if max_age:
            self.set_header('Cache-Control', 'max-age=' + str(max_age) + ',public')
            self.set_header('Expires', datetime.datetime.utcnow() + datetime.timedelta(seconds=max_age))

        # needs to be decided before loading results from the engine
        should_store = result is None and (context.config.RESULT_STORAGE_STORES_UNSAFE or not context.request.unsafe)

        if result is None:
            results = context.request.engine.read(image_extension, quality)
            if context.request.max_bytes is not None:
                results = self.reload_to_fit_in_kb(
                    context.request.engine,
                    results,
                    image_extension,
                    quality,
                    context.request.max_bytes
                )
        else:
            results = result

        self.write(results)
        self.finish()

        if should_store:
            if context.modules.result_storage and not context.request.prevent_result_storage:
                context.modules.result_storage.put(results)

    def reload_to_fit_in_kb(self, engine, initial_results, extension, initial_quality, max_bytes):
        if extension not in ['.webp', '.jpg', '.jpeg'] or len(initial_results) <= max_bytes:
            return initial_results

        results = initial_results
        quality = initial_quality

        while len(results) > max_bytes:
            quality = int(quality * 0.75)

            if quality < 10:
                logger.debug('Could not find any reduction that matches required size of %d bytes.' % max_bytes)
                return initial_results

            logger.debug('Trying to downsize image with quality of %d...' % quality)
            results = engine.read(extension, quality)

        prev_result = results
        while len(results) <= max_bytes:
            quality = int(quality * 1.1)
            logger.debug('Trying to upsize image with quality of %d...' % quality)
            prev_result = results
            results = engine.read(extension, quality)

        return prev_result

    @classmethod
    def translate_crop_coordinates(
            cls,
            original_width,
            original_height,
            width,
            height,
            crop_left,
            crop_top,
            crop_right,
            crop_bottom):

        if original_width == width and original_height == height:
            return

        crop_left = crop_left * width / original_width
        crop_top = crop_top * height / original_height

        crop_right = crop_right * width / original_width
        crop_bottom = crop_bottom * height / original_height

        return (crop_left, crop_top, crop_right, crop_bottom)

    def validate(self, path):
        if not hasattr(self.context.modules.loader, 'validate'):
            return True

        is_valid = self.context.modules.loader.validate(self.context, path)

        if not is_valid:
            logger.warn('Request denied because the specified path "%s" was not identified by the loader as a valid path' % path)

        return is_valid

    def _fetch(self, url, extension, callback):
        storage = self.context.modules.storage
        buffer = storage.get(url)

        if buffer is not None:
            mime = BaseEngine.get_mimetype(buffer)
            if mime == '.gif' and self.context.config.USE_GIFSICLE_ENGINE:
                self.context.request.engine = GifEngine(self.context)
            else:
                self.context.request.engine = self.context.modules.engine

            callback(False, buffer=buffer)
        else:
            def handle_loader_loaded(buffer):
                if buffer is None:
                    callback(False, None)
                    return

                original_preserve = self.context.config.PRESERVE_EXIF_INFO
                self.context.config.PRESERVE_EXIF_INFO = True

                try:
                    mime = BaseEngine.get_mimetype(buffer)

                    if mime == '.gif' and self.context.config.USE_GIFSICLE_ENGINE:
                        self.context.request.engine = GifEngine(self.context)
                    else:
                        self.context.request.engine = self.context.modules.engine

                    self.context.request.engine.load(buffer, extension)
                    normalized = self.context.request.engine.normalize()
                    is_no_storage = isinstance(storage, NoStorage)
                    is_mixed_storage = isinstance(storage, MixedStorage)
                    is_mixed_no_file_storage = is_mixed_storage and isinstance(storage.file_storage, NoStorage)

                    if not (is_no_storage or is_mixed_no_file_storage):
                        buffer = self.context.request.engine.read()
                        storage.put(url, buffer)

                    storage.put_crypto(url)
                finally:
                    self.context.config.PRESERVE_EXIF_INFO = original_preserve

                callback(normalized, engine=self.context.request.engine)

            self.context.modules.loader.load(self.context, url, handle_loader_loaded)

class ContextHandler(BaseHandler):
    def initialize(self, context):
        self.context = Context(context.server, context.config, context.modules.importer, self)

    def log_exception(self, *exc_info):
        if isinstance(exc_info[1], tornado.web.HTTPError):
            # Delegate HTTPError's to the base class
            # We don't want these through normal exception handling
            return super(ContextHandler, self).log_exception(*exc_info)

        msg = traceback.format_exception(*exc_info)

        try:
            if self.context.config.USE_CUSTOM_ERROR_HANDLING:
                self.context.modules.importer.error_handler.handle_error(context=self.context, handler=self, exception=exc_info)
        finally:
            del exc_info
            logger.error('ERROR: %s' % "".join(msg))

##
# Base handler for Image API operations
##
class ImageApiHandler(ContextHandler):

    def validate(self, body):
        conf = self.context.config
        mime = BaseEngine.get_mimetype(body)

        if mime == '.gif' and self.context.config.USE_GIFSICLE_ENGINE:
            engine = GifEngine(self.context)
        else:
            engine = self.context.modules.engine

        # Check if image is valid
        try:
            engine.load(body, None)
        except IOError:
            self._error(415, 'Unsupported Media Type')
            return False

        # Check weight constraints
        if (conf.UPLOAD_MAX_SIZE != 0 and len(self.request.body) > conf.UPLOAD_MAX_SIZE):
            self._error(
                412,
                'Image exceed max weight (Expected : %s, Actual : %s)' % (conf.UPLOAD_MAX_SIZE, len(self.request.body)))
            return False

        # Check size constraints
        size = engine.size
        if (conf.MIN_WIDTH > size[0] or conf.MIN_HEIGHT > size[1]):
            self._error(
                412,
                'Image is too small (Expected: %s/%s , Actual : %s/%s) % (conf.MIN_WIDTH, conf.MIN_HEIGHT, size[0], size[1])')
            return False
        return True

    def write_file(self, id, body):
        storage = self.context.modules.upload_photo_storage
        storage.put(id, body)
<commit_msg>Fix #327 - AutoWebp with result storage<commit_after>#!/usr/bin/python
# -*- coding: utf-8 -*-

# thumbor imaging service
# https://github.com/globocom/thumbor/wiki

# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com

import sys
import functools
from os.path import splitext
import datetime
import traceback

import tornado.web

from thumbor import __version__
from thumbor.storages.no_storage import Storage as NoStorage
from thumbor.storages.mixed_storage import Storage as MixedStorage
from thumbor.context import Context
from thumbor.transformer import Transformer
from thumbor.engines import BaseEngine
from thumbor.engines.gif import Engine as GifEngine
from thumbor.engines.json_engine import JSONEngine
from thumbor.utils import logger
import thumbor.filters

CONTENT_TYPE = {
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.gif': 'image/gif',
    '.png': 'image/png',
    '.webp': 'image/webp'
}

class BaseHandler(tornado.web.RequestHandler):
    def _error(self, status, msg=None):
        self.set_status(status)
        if msg is not None:
            logger.warn(msg)
        self.finish()

    def execute_image_operations(self):
        self.context.request.quality = None

        req = self.context.request
        conf = self.context.config

        req.extension = splitext(req.image_url)[-1].lower()

        should_store = self.context.config.RESULT_STORAGE_STORES_UNSAFE or not self.context.request.unsafe
        if self.context.modules.result_storage and should_store:
            result = self.context.modules.result_storage.get()
            if result is not None:
                mime = BaseEngine.get_mimetype(result)
                if mime == '.gif' and self.context.config.USE_GIFSICLE_ENGINE:
                    self.context.request.engine = GifEngine(self.context)
                else:
                    self.context.request.engine = self.context.modules.engine

                logger.debug('[RESULT_STORAGE] IMAGE FOUND: %s' % req.url)
                self.finish_request(self.context, result)
                return

        if conf.MAX_WIDTH and (not isinstance(req.width, basestring)) and req.width > conf.MAX_WIDTH:
            req.width = conf.MAX_WIDTH
        if conf.MAX_HEIGHT and (not isinstance(req.height, basestring)) and req.height > conf.MAX_HEIGHT:
            req.height = conf.MAX_HEIGHT

        req.meta_callback = conf.META_CALLBACK_NAME or self.request.arguments.get('callback', [None])[0]

        self.filters_runner = self.context.filters_factory.create_instances(self.context, self.context.request.filters)
        self.filters_runner.apply_filters(thumbor.filters.PHASE_PRE_LOAD, self.get_image)

    def get_image(self):
        def callback(normalized, buffer=None, engine=None):
            req = self.context.request

            if engine is None:
                if buffer is None:
                    self._error(404)
                    return

                engine = self.context.request.engine
                engine.load(buffer, req.extension)

            self.normalize_crops(normalized, req, engine)

            if req.meta:
                self.context.request.engine = JSONEngine(engine, req.image_url, req.meta_callback)

            after_transform_cb = functools.partial(self.after_transform, self.context)
            Transformer(self.context).transform(after_transform_cb)

        self._fetch(self.context.request.image_url, self.context.request.extension, callback)

    def normalize_crops(self, normalized, req, engine):
        new_crops = None
        if normalized and req.should_crop:
            crop_left = req.crop['left']
            crop_top = req.crop['top']
            crop_right = req.crop['right']
            crop_bottom = req.crop['bottom']

            actual_width, actual_height = engine.size

            if not req.width and not req.height:
                actual_width = engine.size[0]
                actual_height = engine.size[1]
            elif req.width:
                actual_height = engine.get_proportional_height(engine.size[0])
            elif req.height:
                actual_width = engine.get_proportional_width(engine.size[1])

            new_crops = self.translate_crop_coordinates(
                engine.source_width,
                engine.source_height,
                actual_width,
                actual_height,
                crop_left,
                crop_top,
                crop_right,
                crop_bottom
            )
            req.crop['left'] = new_crops[0]
            req.crop['top'] = new_crops[1]
            req.crop['right'] = new_crops[2]
            req.crop['bottom'] = new_crops[3]

    def after_transform(self, context):
        finish_callback = functools.partial(self.finish_request, context)
        if context.request.extension == '.gif' and context.config.USE_GIFSICLE_ENGINE:
            finish_callback()
        else:
            self.filters_runner.apply_filters(thumbor.filters.PHASE_POST_TRANSFORM, finish_callback)

    def define_image_type(self, context, result):
        if result is not None:
            image_extension = BaseEngine.get_mimetype(result)
        else:
            image_extension = context.request.format
            if image_extension is not None:
                image_extension = '.%s' % image_extension
                logger.debug('Image format specified as %s.' % image_extension)
            elif context.config.AUTO_WEBP and context.request.accepts_webp and not context.request.engine.is_multiple():
                image_extension = '.webp'
                logger.debug('Image format set by AUTO_WEBP as %s.' % image_extension)
            else:
                image_extension = context.request.engine.extension
                logger.debug('No image format specified. Retrieving from the image extension: %s.' % image_extension)

        content_type = CONTENT_TYPE.get(image_extension, CONTENT_TYPE['.jpg'])

        if context.request.meta:
            context.request.meta_callback = context.config.META_CALLBACK_NAME or self.request.arguments.get('callback', [None])[0]
            content_type = 'text/javascript' if context.request.meta_callback else 'application/json'
            logger.debug('Metadata requested. Serving content type of %s.' % content_type)

        logger.debug('Content Type of %s detected.' % content_type)

        return image_extension, content_type

    def finish_request(self, context, result=None):
        image_extension, content_type = self.define_image_type(context, result)

        quality = self.context.request.quality
        if quality is None:
            if image_extension == '.webp':
                quality = self.context.config.get('WEBP_QUALITY', self.context.config.QUALITY)
            else:
                quality = self.context.config.QUALITY

        self.set_header('Content-Type', content_type)
        self.set_header('Server', 'Thumbor/%s' % __version__)

        if context.config.AUTO_WEBP and not context.request.engine.is_multiple() and context.request.engine.extension != '.webp':
            self.set_header('Vary', 'Accept')

        max_age = self.context.config.MAX_AGE
        if context.request.prevent_result_storage or context.request.detection_error:
            max_age = self.context.config.MAX_AGE_TEMP_IMAGE

        if max_age:
            self.set_header('Cache-Control', 'max-age=' + str(max_age) + ',public')
            self.set_header('Expires', datetime.datetime.utcnow() + datetime.timedelta(seconds=max_age))

        # needs to be decided before loading results from the engine
        should_store = result is None and (context.config.RESULT_STORAGE_STORES_UNSAFE or not context.request.unsafe)

        if result is None:
            results = context.request.engine.read(image_extension, quality)
            if context.request.max_bytes is not None:
                results = self.reload_to_fit_in_kb(
                    context.request.engine,
                    results,
                    image_extension,
                    quality,
                    context.request.max_bytes
                )
        else:
            results = result

        self.write(results)
        self.finish()

        if should_store:
            if context.modules.result_storage and not context.request.prevent_result_storage:
                context.modules.result_storage.put(results)

    def reload_to_fit_in_kb(self, engine, initial_results, extension, initial_quality, max_bytes):
        if extension not in ['.webp', '.jpg', '.jpeg'] or len(initial_results) <= max_bytes:
            return initial_results

        results = initial_results
        quality = initial_quality

        while len(results) > max_bytes:
            quality = int(quality * 0.75)

            if quality < 10:
                logger.debug('Could not find any reduction that matches required size of %d bytes.' % max_bytes)
                return initial_results

            logger.debug('Trying to downsize image with quality of %d...' % quality)
            results = engine.read(extension, quality)

        prev_result = results
        while len(results) <= max_bytes:
            quality = int(quality * 1.1)
            logger.debug('Trying to upsize image with quality of %d...' % quality)
            prev_result = results
            results = engine.read(extension, quality)

        return prev_result

    @classmethod
    def translate_crop_coordinates(
            cls,
            original_width,
            original_height,
            width,
            height,
            crop_left,
            crop_top,
            crop_right,
            crop_bottom):

        if original_width == width and original_height == height:
            return

        crop_left = crop_left * width / original_width
        crop_top = crop_top * height / original_height

        crop_right = crop_right * width / original_width
        crop_bottom = crop_bottom * height / original_height

        return (crop_left, crop_top, crop_right, crop_bottom)

    def validate(self, path):
        if not hasattr(self.context.modules.loader, 'validate'):
            return True

        is_valid = self.context.modules.loader.validate(self.context, path)

        if not is_valid:
            logger.warn('Request denied because the specified path "%s" was not identified by the loader as a valid path' % path)

        return is_valid

    def _fetch(self, url, extension, callback):
        storage = self.context.modules.storage
        buffer = storage.get(url)

        if buffer is not None:
            mime = BaseEngine.get_mimetype(buffer)
            if mime == '.gif' and self.context.config.USE_GIFSICLE_ENGINE:
                self.context.request.engine = GifEngine(self.context)
            else:
                self.context.request.engine = self.context.modules.engine

            callback(False, buffer=buffer)
        else:
            def handle_loader_loaded(buffer):
                if buffer is None:
                    callback(False, None)
                    return

                original_preserve = self.context.config.PRESERVE_EXIF_INFO
                self.context.config.PRESERVE_EXIF_INFO = True

                try:
                    mime = BaseEngine.get_mimetype(buffer)

                    if mime == '.gif' and self.context.config.USE_GIFSICLE_ENGINE:
                        self.context.request.engine = GifEngine(self.context)
                    else:
                        self.context.request.engine = self.context.modules.engine

                    self.context.request.engine.load(buffer, extension)
                    normalized = self.context.request.engine.normalize()
                    is_no_storage = isinstance(storage, NoStorage)
                    is_mixed_storage = isinstance(storage, MixedStorage)
                    is_mixed_no_file_storage = is_mixed_storage and isinstance(storage.file_storage, NoStorage)

                    if not (is_no_storage or is_mixed_no_file_storage):
                        buffer = self.context.request.engine.read()
                        storage.put(url, buffer)

                    storage.put_crypto(url)
                finally:
                    self.context.config.PRESERVE_EXIF_INFO = original_preserve

                callback(normalized, engine=self.context.request.engine)

            self.context.modules.loader.load(self.context, url, handle_loader_loaded)

class ContextHandler(BaseHandler):
    def initialize(self, context):
        self.context = Context(context.server, context.config, context.modules.importer, self)

    def log_exception(self, *exc_info):
        if isinstance(exc_info[1], tornado.web.HTTPError):
            # Delegate HTTPError's to the base class
            # We don't want these through normal exception handling
            return super(ContextHandler, self).log_exception(*exc_info)

        msg = traceback.format_exception(*exc_info)

        try:
            if self.context.config.USE_CUSTOM_ERROR_HANDLING:
                self.context.modules.importer.error_handler.handle_error(context=self.context, handler=self, exception=exc_info)
        finally:
            del exc_info
            logger.error('ERROR: %s' % "".join(msg))

##
# Base handler for Image API operations
##
class ImageApiHandler(ContextHandler):

    def validate(self, body):
        conf = self.context.config
        mime = BaseEngine.get_mimetype(body)

        if mime == '.gif' and self.context.config.USE_GIFSICLE_ENGINE:
            engine = GifEngine(self.context)
        else:
            engine = self.context.modules.engine

        # Check if image is valid
        try:
            engine.load(body, None)
        except IOError:
            self._error(415, 'Unsupported Media Type')
            return False

        # Check weight constraints
        if (conf.UPLOAD_MAX_SIZE != 0 and len(self.request.body) > conf.UPLOAD_MAX_SIZE):
            self._error(
                412,
                'Image exceed max weight (Expected : %s, Actual : %s)' % (conf.UPLOAD_MAX_SIZE, len(self.request.body)))
            return False

        # Check size constraints
        size = engine.size
        if (conf.MIN_WIDTH > size[0] or conf.MIN_HEIGHT > size[1]):
            self._error(
                412,
                'Image is too small (Expected: %s/%s , Actual : %s/%s) % (conf.MIN_WIDTH, conf.MIN_HEIGHT, size[0], size[1])')
            return False
        return True

    def write_file(self, id, body):
        storage = self.context.modules.upload_photo_storage
        storage.put(id, body)
<|endoftext|>


<commit_before>import json as _json
import logging
from importlib import import_module

from django.utils import timezone

from api.indy.agent import Holder
from api.indy import eventloop

from von_agent.util import schema_key

from api_v2.models.Issuer import Issuer
from api_v2.models.Schema import Schema
from api_v2.models.Subject import Subject
from api_v2.models.CredentialType import CredentialType
from api_v2.models.Claim import Claim

from api_v2.models.Name import Name
from api_v2.models.Address import Address
from api_v2.models.Person import Person
from api_v2.models.Contact import Contact

logger = logging.getLogger(__name__)

PROCESSOR_FUNCTION_BASE_PATH = "api_v2.processor"

SUPPORTED_MODELS_MAPPING = {
    "name": Name,
    "address": Address,
    "person": Person,
    "contact": Contact,
}

class CredentialException(Exception):
    pass

class Credential(object):
    """A python-idiomatic representation of an indy credential

    Claim values are made available as class members.

    for example:

    ```json
    "postal_code": {
        "raw": "N2L 6P3",
        "encoded": "1062703188233012330691500488799027"
    }
    ```

    becomes:

    ```python
    self.postal_code = "N2L 6P3"
    ```

    on the class object.

    Arguments:
        credential_data {object} -- Valid credential data as sent by an issuer
    """

    def __init__(self, credential_data: object) -> None:
        self._raw = credential_data
        self._schema_id = credential_data["schema_id"]
        self._cred_def_id = credential_data["cred_def_id"]
        self._rev_reg_id = credential_data["rev_reg_id"]
        self._signature = credential_data["signature"]
        self._signature_correctness_proof = credential_data[
            "signature_correctness_proof"
        ]
        self._rev_reg = credential_data["rev_reg"]
        self._witness = credential_data["witness"]

        self._claim_attributes = []

        # Parse claim attributes into array
        # Values are available as class attributes
        claim_data = credential_data["values"]
        for claim_attribute in claim_data:
            self._claim_attributes.append(claim_attribute)

    def __getattr__(self, name: str):
        """Make claim values accessible on class instance"""
        try:
            claim_value = self.raw["values"][name]["raw"]
            return claim_value
        except KeyError:
            raise AttributeError(
                "'Credential' object has no attribute '{}'".format(name)
            )

    @property
    def raw(self) -> dict:
        """Accessor for raw credential data

        Returns:
            dict -- Python dict representation of raw credential data
        """
        return self._raw

    @property
    def json(self) -> str:
        """Accessor for json credential data

        Returns:
            str -- JSON representation of raw credential data
        """
        return _json.dumps(self._raw)

    @property
    def origin_did(self) -> str:
        """Accessor for schema origin did

        Returns:
            str -- origin did
        """
        return schema_key(self._schema_id).origin_did

    @property
    def schema_name(self) -> str:
        """Accessor for schema name

        Returns:
            str -- schema name
        """
        return schema_key(self._schema_id).name

    @property
    def schema_version(self) -> str:
        """Accessor for schema version

        Returns:
            str -- schema version
        """
        return schema_key(self._schema_id).version

    @property
    def claim_attributes(self) -> list:
        """Accessor for claim attributes

        Returns:
            list -- claim attributes
        """
        return self._claim_attributes

class CredentialManager(object):
    def __init__(
        self, credential: Credential, credential_definition_metadata: dict
    ) -> None:
        self.credential = credential
        self.credential_definition_metadata = credential_definition_metadata

    def process(self):
        # Get context for this credential if it exists
        try:
            issuer = Issuer.objects.get(did=self.credential.origin_did)
            schema = Schema.objects.get(
                origin_did=self.credential.origin_did,
                name=self.credential.schema_name,
                version=self.credential.schema_version,
            )
        except Issuer.DoesNotExist:
            raise CredentialException(
                "Issuer with did '{}' does not exist.".format(
                    self.credential.origin_did
                )
            )
        except Schema.DoesNotExist:
            raise CredentialException(
                "Schema with origin_did"
                + " '{}', name '{}', and version '{}' ".format(
                    self.credential.origin_did,
                    self.credential.schema_name,
                    self.credential.schema_version,
                )
                + " does not exist."
            )

        credential_type = CredentialType.objects.get(
            schema=schema, issuer=issuer
        )

        try:
            source_id = getattr(self.credential, credential_type.source_claim)
        except AttributeError as error:
            raise CredentialException(
                "Credential does not contain the configured source_claim "
                + "'{}'. Claims are: {}".format(
                    credential_type.source_claim,
                    ", ".join(self.credential.claim_attributes),
                )
            )

        credential = self.populate_application_database(
            credential_type, source_id
        )
        eventloop.do(self.store(source_id))
        return credential

    def populate_application_database(self, credential_type, source_id):
        # Obtain required models from database

        # Create subject, credential, claim models
        subject, created = Subject.objects.get_or_create(source_id=source_id)

        credential = subject.credentials.create(
            subject=subject, credential_type=credential_type
        )

        # TODO: optimize into a single insert
        for claim_attribute in self.credential.claim_attributes:
            claim_value = getattr(self.credential, claim_attribute)
            Claim.objects.create(
                credential=credential, name=claim_attribute, value=claim_value
            )

        # Update optional models based on processor config
        processor_config = credential_type.processor_config
        if not processor_config:
            return credential.id

        # Iterate model types in processor mapping
        for i, model_mapper in enumerate(processor_config):
            model_name = model_mapper["model"]
            cardinality_fields = model_mapper.get("cardinality_fields") or []

            # We currently support 4 model types
            # see SUPPORTED_MODELS_MAPPING
            try:
                Model = SUPPORTED_MODELS_MAPPING[model_name]
            except KeyError as error:
                raise CredentialException(
                    "Unsupported model type '{}'".format(model_name)
                )

            # Iterate fields on model mapping config
            processed_values = {}
            for field in processor_config[i]["fields"]:
                field_data = processor_config[i]["fields"][field]

                # Get required values from config
                try:
                    _input = field_data["input"]
                    _from = field_data["from"]
                except KeyError as error:
                    raise CredentialException(
                        "Every field must specify 'input' and 'from' values."
                    )

                # Pocessor is optional
                try:
                    processor = field_data["processor"]
                except KeyError as error:
                    processor = None

                # Get model field value from string literal or claim value
                if _from == "value":
                    field_value = _input
                elif _from == "claim":
                    field_value = getattr(self.credential, _input)
                else:
                    raise CredentialException(
                        "Supported field from values are 'value' and 'claim'"
                        + " but received '{}'".format(_from)
                    )

                # If we have a processor config, build pipeline of functions
                # and run field value through pipeline
                if processor is not None:
                    pipeline = []
                    # Construct pipeline by dot notation. Last token is the function name
                    # and all preceeding dots denote path of module starting from
                    # `PROCESSOR_FUNCTION_BASE_PATH``
                    for function_path_with_name in processor:
                        function_path, function_name = function_path_with_name.rsplit(
                            ".", 1
                        )

                        # Does the file exist?
                        try:
                            function_module = import_module(
                                "{}.{}".format(
                                    PROCESSOR_FUNCTION_BASE_PATH, function_path
                                )
                            )
                        except ModuleNotFoundError as error:
                            raise CredentialException(
                                "No processor module named '{}'".format(
                                    function_path
                                )
                            )

                        # Does the function exist?
                        try:
                            function = getattr(function_module, function_name)
                        except AttributeError as error:
                            raise CredentialException(
                                "Module '{}' has no function '{}'.".format(
                                    function_path, function_name
                                )
                            )

                        # Build up a list of functions to call
                        pipeline.append(function)

                    # We want to run the pipeline in logical order
                    pipeline.reverse()

                    # Run pipeline
                    while len(pipeline) > 0:
                        function = pipeline.pop()
                        field_value = function(field_value)

                processed_values[field] = field_value

                # This is ugly. von-agent currently serializes null values
                # to the string 'None'
                if processed_values[field] == "None":
                    processed_values[field] = None

            model = None
            # Try to get an existing model based on cardinality_fields.
            # We always limit query by this subject and without an end_date.
            model_args = {
                "credentials__subject__id": subject.id,
                "end_date": None,
            }
            for cardinality_field in cardinality_fields:
                try:
                    model_args[cardinality_field] = processed_values[
                        cardinality_field
                    ]
                except KeyError as error:
                    raise CredentialException(
                        "Issuer configuration specifies field '{}' ".format(
                            cardinality_field
                        )
                        + "in cardinality_fields value does not exist in "
                        + "credential processor. Values are: {}".format(
                            ", ".join(list(processed_values.keys()))
                        )
                    )

            # If the issuer changes its `cardinality_fields` it's possible for
            # this query to return multiple records.
            #
            # To handle this, we always get the _last created_ record for this
            # subject, with no end_date, and with values in
            # `cardinality_fields` equal to their resulting values after
            # running through the function pipeline. We update the most
            # recently created record and we implicitly set the end_date
            # for `now` for all other returned records.
            try:
                query = (
                    Model.objects.filter(**model_args)
                    .order_by("-create_timestamp")
                    .distinct()
                )

                # We care about the most recent model if there
                # are more than one
                model = query[0]
                # If there are other records for this query, then the issuer
                # changed its `cardinality_fields` to something less specific
                # than it was previously.
                query.exclude(pk=model.id).update(end_date=timezone.now())
            except IndexError as error:
                logger.warn(error)

            # If it doesn't exist, we create a new one
            if not model:
                model = Model()

            # Either way, we update the fields based on results of
            # mapping and processor
            for value in processed_values:
                setattr(model, value, processed_values[value])

            # Save and associate with credential
            model.save()
            model.credentials.add(credential)

        return credential.id

    async def store(self, legal_entity_id):

        # Store credential in wallet
        async with Holder(legal_entity_id) as holder:
            await holder.store_cred(
                self.credential.json,
                _json.dumps(self.credential_definition_metadata),
            )
<commit_msg>Add explicit error for missing claim<commit_after>import json as _json
import logging
from importlib import import_module

from django.utils import timezone

from api.indy.agent import Holder
from api.indy import eventloop

from von_agent.util import schema_key

from api_v2.models.Issuer import Issuer
from api_v2.models.Schema import Schema
from api_v2.models.Subject import Subject
from api_v2.models.CredentialType import CredentialType
from api_v2.models.Claim import Claim

from api_v2.models.Name import Name
from api_v2.models.Address import Address
from api_v2.models.Person import Person
from api_v2.models.Contact import Contact

logger = logging.getLogger(__name__)

PROCESSOR_FUNCTION_BASE_PATH = "api_v2.processor"

SUPPORTED_MODELS_MAPPING = {
    "name": Name,
    "address": Address,
    "person": Person,
    "contact": Contact,
}

class CredentialException(Exception):
    pass

class Credential(object):
    """A python-idiomatic representation of an indy credential

    Claim values are made available as class members.

    for example:

    ```json
    "postal_code": {
        "raw": "N2L 6P3",
        "encoded": "1062703188233012330691500488799027"
    }
    ```

    becomes:

    ```python
    self.postal_code = "N2L 6P3"
    ```

    on the class object.

    Arguments:
        credential_data {object} -- Valid credential data as sent by an issuer
    """

    def __init__(self, credential_data: object) -> None:
        self._raw = credential_data
        self._schema_id = credential_data["schema_id"]
        self._cred_def_id = credential_data["cred_def_id"]
        self._rev_reg_id = credential_data["rev_reg_id"]
        self._signature = credential_data["signature"]
        self._signature_correctness_proof = credential_data[
            "signature_correctness_proof"
        ]
        self._rev_reg = credential_data["rev_reg"]
        self._witness = credential_data["witness"]

        self._claim_attributes = []

        # Parse claim attributes into array
        # Values are available as class attributes
        claim_data = credential_data["values"]
        for claim_attribute in claim_data:
            self._claim_attributes.append(claim_attribute)

    def __getattr__(self, name: str):
        """Make claim values accessible on class instance"""
        try:
            claim_value = self.raw["values"][name]["raw"]
            return claim_value
        except KeyError:
            raise AttributeError(
                "'Credential' object has no attribute '{}'".format(name)
            )

    @property
    def raw(self) -> dict:
        """Accessor for raw credential data

        Returns:
            dict -- Python dict representation of raw credential data
        """
        return self._raw

    @property
    def json(self) -> str:
        """Accessor for json credential data

        Returns:
            str -- JSON representation of raw credential data
        """
        return _json.dumps(self._raw)

    @property
    def origin_did(self) -> str:
        """Accessor for schema origin did

        Returns:
            str -- origin did
        """
        return schema_key(self._schema_id).origin_did

    @property
    def schema_name(self) -> str:
        """Accessor for schema name

        Returns:
            str -- schema name
        """
        return schema_key(self._schema_id).name

    @property
    def schema_version(self) -> str:
        """Accessor for schema version

        Returns:
            str -- schema version
        """
        return schema_key(self._schema_id).version

    @property
    def claim_attributes(self) -> list:
        """Accessor for claim attributes

        Returns:
            list -- claim attributes
        """
        return self._claim_attributes

class CredentialManager(object):
    def __init__(
        self, credential: Credential, credential_definition_metadata: dict
    ) -> None:
        self.credential = credential
        self.credential_definition_metadata = credential_definition_metadata

    def process(self):
        # Get context for this credential if it exists
        try:
            issuer = Issuer.objects.get(did=self.credential.origin_did)
            schema = Schema.objects.get(
                origin_did=self.credential.origin_did,
                name=self.credential.schema_name,
                version=self.credential.schema_version,
            )
        except Issuer.DoesNotExist:
            raise CredentialException(
                "Issuer with did '{}' does not exist.".format(
                    self.credential.origin_did
                )
            )
        except Schema.DoesNotExist:
            raise CredentialException(
                "Schema with origin_did"
                + " '{}', name '{}', and version '{}' ".format(
                    self.credential.origin_did,
                    self.credential.schema_name,
                    self.credential.schema_version,
                )
                + " does not exist."
            )

        credential_type = CredentialType.objects.get(
            schema=schema, issuer=issuer
        )

        try:
            source_id = getattr(self.credential, credential_type.source_claim)
        except AttributeError as error:
            raise CredentialException(
                "Credential does not contain the configured source_claim "
                + "'{}'. Claims are: {}".format(
                    credential_type.source_claim,
                    ", ".join(self.credential.claim_attributes),
                )
            )

        credential = self.populate_application_database(
            credential_type, source_id
        )
        eventloop.do(self.store(source_id))
        return credential

    def populate_application_database(self, credential_type, source_id):
        # Obtain required models from database

        # Create subject, credential, claim models
        subject, created = Subject.objects.get_or_create(source_id=source_id)

        credential = subject.credentials.create(
            subject=subject, credential_type=credential_type
        )

        # TODO: optimize into a single insert
        for claim_attribute in self.credential.claim_attributes:
            claim_value = getattr(self.credential, claim_attribute)
            Claim.objects.create(
                credential=credential, name=claim_attribute, value=claim_value
            )

        # Update optional models based on processor config
        processor_config = credential_type.processor_config
        if not processor_config:
            return credential.id

        # Iterate model types in processor mapping
        for i, model_mapper in enumerate(processor_config):
            model_name = model_mapper["model"]
            cardinality_fields = model_mapper.get("cardinality_fields") or []

            # We currently support 4 model types
            # see SUPPORTED_MODELS_MAPPING
            try:
                Model = SUPPORTED_MODELS_MAPPING[model_name]
            except KeyError as error:
                raise CredentialException(
                    "Unsupported model type '{}'".format(model_name)
                )

            # Iterate fields on model mapping config
            processed_values = {}
            for field in processor_config[i]["fields"]:
                field_data = processor_config[i]["fields"][field]

                # Get required values from config
                try:
                    _input = field_data["input"]
                    _from = field_data["from"]
                except KeyError as error:
                    raise CredentialException(
                        "Every field must specify 'input' and 'from' values."
                    )

                # Pocessor is optional
                try:
                    processor = field_data["processor"]
                except KeyError as error:
                    processor = None

                # Get model field value from string literal or claim value
                if _from == "value":
                    field_value = _input
                elif _from == "claim":
                    try:
                        field_value = getattr(self.credential, _input)
                    except AttributeError as error:

                        raise CredentialException(
                            "Credential does not contain the configured claim "
                            + "'{}' which is mapped to field '{}'. ".format(
                                _input, field
                            )
                            + "Claims are: {}".format(
                                ", ".join(self.credential.claim_attributes)
                            )
                        )
                else:
                    raise CredentialException(
                        "Supported field from values are 'value' and 'claim'"
                        + " but received '{}'".format(_from)
                    )

                # If we have a processor config, build pipeline of functions
                # and run field value through pipeline
                if processor is not None:
                    pipeline = []
                    # Construct pipeline by dot notation. Last token is the function name
                    # and all preceeding dots denote path of module starting from
                    # `PROCESSOR_FUNCTION_BASE_PATH``
                    for function_path_with_name in processor:
                        function_path, function_name = function_path_with_name.rsplit(
                            ".", 1
                        )

                        # Does the file exist?
                        try:
                            function_module = import_module(
                                "{}.{}".format(
                                    PROCESSOR_FUNCTION_BASE_PATH, function_path
                                )
                            )
                        except ModuleNotFoundError as error:
                            raise CredentialException(
                                "No processor module named '{}'".format(
                                    function_path
                                )
                            )

                        # Does the function exist?
                        try:
                            function = getattr(function_module, function_name)
                        except AttributeError as error:
                            raise CredentialException(
                                "Module '{}' has no function '{}'.".format(
                                    function_path, function_name
                                )
                            )

                        # Build up a list of functions to call
                        pipeline.append(function)

                    # We want to run the pipeline in logical order
                    pipeline.reverse()

                    # Run pipeline
                    while len(pipeline) > 0:
                        function = pipeline.pop()
                        field_value = function(field_value)

                processed_values[field] = field_value

                # This is ugly. von-agent currently serializes null values
                # to the string 'None'
                if processed_values[field] == "None":
                    processed_values[field] = None

            model = None
            # Try to get an existing model based on cardinality_fields.
            # We always limit query by this subject and without an end_date.
            model_args = {
                "credentials__subject__id": subject.id,
                "end_date": None,
            }
            for cardinality_field in cardinality_fields:
                try:
                    model_args[cardinality_field] = processed_values[
                        cardinality_field
                    ]
                except KeyError as error:
                    raise CredentialException(
                        "Issuer configuration specifies field '{}' ".format(
                            cardinality_field
                        )
                        + "in cardinality_fields value does not exist in "
                        + "credential processor. Values are: {}".format(
                            ", ".join(list(processed_values.keys()))
                        )
                    )

            # If the issuer changes its `cardinality_fields` it's possible for
            # this query to return multiple records.
            #
            # To handle this, we always get the _last created_ record for this
            # subject, with no end_date, and with values in
            # `cardinality_fields` equal to their resulting values after
            # running through the function pipeline. We update the most
            # recently created record and we implicitly set the end_date
            # for `now` for all other returned records.
            try:
                query = (
                    Model.objects.filter(**model_args)
                    .order_by("-create_timestamp")
                    .distinct()
                )

                # We care about the most recent model if there
                # are more than one
                model = query[0]
                # If there are other records for this query, then the issuer
                # changed its `cardinality_fields` to something less specific
                # than it was previously.
                query.exclude(pk=model.id).update(end_date=timezone.now())
            except IndexError as error:
                logger.warn(error)

            # If it doesn't exist, we create a new one
            if not model:
                model = Model()

            # Either way, we update the fields based on results of
            # mapping and processor
            for value in processed_values:
                setattr(model, value, processed_values[value])

            # Save and associate with credential
            model.save()
            model.credentials.add(credential)

        return credential.id

    async def store(self, legal_entity_id):

        # Store credential in wallet
        async with Holder(legal_entity_id) as holder:
            await holder.store_cred(
                self.credential.json,
                _json.dumps(self.credential_definition_metadata),
            )
<|endoftext|>


<commit_before>import asyncio
import html
import inspect
import traceback
import typing

from starlette.concurrency import run_in_threadpool
from starlette.requests import Request
from starlette.responses import HTMLResponse, PlainTextResponse, Response
from starlette.types import ASGIApp, Message, Receive, Scope, Send

STYLES = """
p {
    color: #211c1c;
}
.traceback-container {
    border: 1px solid #038BB8;
}
.traceback-title {
    background-color: #038BB8;
    color: lemonchiffon;
    padding: 12px;
    font-size: 20px;
    margin-top: 0px;
}
.frame-line {
    padding-left: 10px;
}
.center-line {
    background-color: #038BB8;
    color: #f9f6e1;
    padding: 5px 0px 5px 5px;
}
.lineno {
    margin-right: 5px;
}
.frame-filename {
    font-weight: unset;
    padding: 10px 10px 10px 0px;
    background-color: #E4F4FD;
    margin-right: 10px;
    font: #394D54;
    color: #191f21;
    font-size: 17px;
    border: 1px solid #c7dce8;
}
.collapse-btn {
    float: right;
    padding: 0px 5px 1px 5px;
    border: solid 1px #96aebb;
    cursor: pointer;
}
.collapsed {
  display: none;
}
.source-code {
  font-family: courier;
  font-size: small;
  padding-bottom: 10px;
}
"""

JS = """

"""

TEMPLATE = """

        <title>Starlette Debugger</title>

500 Server Error

{error}

Traceback

{exc_html}

        {js}

"""

FRAME_TEMPLATE = """

File {frame_filename},
    line {frame_lineno},
    in {frame_name}
    {collapse_button}

{code_context}

"""

LINE = """

{lineno}. {line}

"""

CENTER_LINE = """

{lineno}. {line}

"""

class ServerErrorMiddleware:
    """
    Handles returning 500 responses when a server error occurs.

    If 'debug' is set, then traceback responses will be returned,
    otherwise the designated 'handler' will be called.

    This middleware class should generally be used to wrap *everything*
    else up, so that unhandled exceptions anywhere in the stack
    always result in an appropriate 500 response.
    """

    def __init__(
        self, app: ASGIApp, handler: typing.Callable = None, debug: bool = False
    ) -> None:
        self.app = app
        self.handler = handler
        self.debug = debug

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        response_started = False

        async def _send(message: Message) -> None:
            nonlocal response_started, send

            if message["type"] == "http.response.start":
                response_started = True
            await send(message)

        try:
            await self.app(scope, receive, _send)
        except Exception as exc:
            if not response_started:
                request = Request(scope)
                if self.debug:
                    # In debug mode, return traceback responses.
                    response = self.debug_response(request, exc)
                elif self.handler is None:
                    # Use our default 500 error handler.
                    response = self.error_response(request, exc)
                else:
                    # Use an installed 500 error handler.
                    if asyncio.iscoroutinefunction(self.handler):
                        response = await self.handler(request, exc)
                    else:
                        response = await run_in_threadpool(self.handler, request, exc)

                await response(scope, receive, send)

            # We always continue to raise the exception.
            # This allows servers to log the error, or allows test clients
            # to optionally raise the error within the test case.
            raise exc from None

    def format_line(
        self, index: int, line: str, frame_lineno: int, frame_index: int
    ) -> str:
        values = {
            "line": line.replace(" ", "&nbsp"),
            "lineno": (frame_lineno - frame_index) + index,
        }

        if index != frame_index:
            return LINE.format(**values)
        return CENTER_LINE.format(**values)

    def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str:
        code_context = "".join(
            self.format_line(index, line, frame.lineno, frame.index)  # type: ignore
            for index, line in enumerate(frame.code_context or [])
        )

        values = {
            "frame_filename": frame.filename,
            "frame_lineno": frame.lineno,
            "frame_name": frame.function,
            "code_context": code_context,
            "collapsed": "collapsed" if is_collapsed else "",
            "collapse_button": "+" if is_collapsed else "‒",
        }
        return FRAME_TEMPLATE.format(**values)

    def generate_html(self, exc: Exception, limit: int = 7) -> str:
        traceback_obj = traceback.TracebackException.from_exception(
            exc, capture_locals=True
        )
        frames = inspect.getinnerframes(
            traceback_obj.exc_traceback, limit  # type: ignore
        )

        exc_html = ""
        is_collapsed = False
        for frame in reversed(frames):
            exc_html += self.generate_frame_html(frame, is_collapsed)
            is_collapsed = True

        error = f"{traceback_obj.exc_type.__name__}: {html.escape(str(traceback_obj))}"

        return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)

    def generate_plain_text(self, exc: Exception) -> str:
        return "".join(traceback.format_tb(exc.__traceback__))

    def debug_response(self, request: Request, exc: Exception) -> Response:
        accept = request.headers.get("accept", "")

        if "text/html" in accept:
            content = self.generate_html(exc)
            return HTMLResponse(content, status_code=500)
        content = self.generate_plain_text(exc)
        return PlainTextResponse(content, status_code=500)

    def error_response(self, request: Request, exc: Exception) -> Response:
        return PlainTextResponse("Internal Server Error", status_code=500)
<commit_msg>More debugger improvements (#825)<commit_after>import asyncio
import html
import inspect
import traceback
import typing

from starlette.concurrency import run_in_threadpool
from starlette.requests import Request
from starlette.responses import HTMLResponse, PlainTextResponse, Response
from starlette.types import ASGIApp, Message, Receive, Scope, Send

STYLES = """
p {
    color: #211c1c;
}
.traceback-container {
    border: 1px solid #038BB8;
}
.traceback-title {
    background-color: #038BB8;
    color: lemonchiffon;
    padding: 12px;
    font-size: 20px;
    margin-top: 0px;
}
.frame-line {
    padding-left: 10px;
    font-family: monospace;
}
.frame-filename {
    font-family: monospace;
}
.center-line {
    background-color: #038BB8;
    color: #f9f6e1;
    padding: 5px 0px 5px 5px;
}
.lineno {
    margin-right: 5px;
}
.frame-title {
    font-weight: unset;
    padding: 10px 10px 10px 10px;
    background-color: #E4F4FD;
    margin-right: 10px;
    color: #191f21;
    font-size: 17px;
    border: 1px solid #c7dce8;
}
.collapse-btn {
    float: right;
    padding: 0px 5px 1px 5px;
    border: solid 1px #96aebb;
    cursor: pointer;
}
.collapsed {
  display: none;
}
.source-code {
  font-family: courier;
  font-size: small;
  padding-bottom: 10px;
}
"""

JS = """

"""

TEMPLATE = """

        <title>Starlette Debugger</title>

500 Server Error

{error}

Traceback

{exc_html}

        {js}

"""

FRAME_TEMPLATE = """

File {frame_filename},
    line {frame_lineno},
    in {frame_name}
    {collapse_button}

{code_context}

"""

LINE = """

{lineno}. {line}

"""

CENTER_LINE = """

{lineno}. {line}

"""

class ServerErrorMiddleware:
    """
    Handles returning 500 responses when a server error occurs.

    If 'debug' is set, then traceback responses will be returned,
    otherwise the designated 'handler' will be called.

    This middleware class should generally be used to wrap *everything*
    else up, so that unhandled exceptions anywhere in the stack
    always result in an appropriate 500 response.
    """

    def __init__(
        self, app: ASGIApp, handler: typing.Callable = None, debug: bool = False
    ) -> None:
        self.app = app
        self.handler = handler
        self.debug = debug

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        response_started = False

        async def _send(message: Message) -> None:
            nonlocal response_started, send

            if message["type"] == "http.response.start":
                response_started = True
            await send(message)

        try:
            await self.app(scope, receive, _send)
        except Exception as exc:
            if not response_started:
                request = Request(scope)
                if self.debug:
                    # In debug mode, return traceback responses.
                    response = self.debug_response(request, exc)
                elif self.handler is None:
                    # Use our default 500 error handler.
                    response = self.error_response(request, exc)
                else:
                    # Use an installed 500 error handler.
                    if asyncio.iscoroutinefunction(self.handler):
                        response = await self.handler(request, exc)
                    else:
                        response = await run_in_threadpool(self.handler, request, exc)

                await response(scope, receive, send)

            # We always continue to raise the exception.
            # This allows servers to log the error, or allows test clients
            # to optionally raise the error within the test case.
            raise exc from None

    def format_line(
        self, index: int, line: str, frame_lineno: int, frame_index: int
    ) -> str:
        values = {
            # HTML escape - line could contain < or >
            "line": html.escape(line).replace(" ", "&nbsp"),
            "lineno": (frame_lineno - frame_index) + index,
        }

        if index != frame_index:
            return LINE.format(**values)
        return CENTER_LINE.format(**values)

    def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str:
        code_context = "".join(
            self.format_line(index, line, frame.lineno, frame.index)  # type: ignore
            for index, line in enumerate(frame.code_context or [])
        )

        values = {
            # HTML escape - filename could contain < or >, especially if it's a virtual file e.g. <stdin> in the REPL
            "frame_filename": html.escape(frame.filename),
            "frame_lineno": frame.lineno,
            # HTML escape - if you try very hard it's possible to name a function with < or >
            "frame_name": html.escape(frame.function),
            "code_context": code_context,
            "collapsed": "collapsed" if is_collapsed else "",
            "collapse_button": "+" if is_collapsed else "‒",
        }
        return FRAME_TEMPLATE.format(**values)

    def generate_html(self, exc: Exception, limit: int = 7) -> str:
        traceback_obj = traceback.TracebackException.from_exception(
            exc, capture_locals=True
        )
        frames = inspect.getinnerframes(
            traceback_obj.exc_traceback, limit  # type: ignore
        )

        exc_html = ""
        is_collapsed = False
        for frame in reversed(frames):
            exc_html += self.generate_frame_html(frame, is_collapsed)
            is_collapsed = True

        # escape error class and text
        error = f"{html.escape(traceback_obj.exc_type.__name__)}: {html.escape(str(traceback_obj))}"

        return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)

    def generate_plain_text(self, exc: Exception) -> str:
        return "".join(traceback.format_tb(exc.__traceback__))

    def debug_response(self, request: Request, exc: Exception) -> Response:
        accept = request.headers.get("accept", "")

        if "text/html" in accept:
            content = self.generate_html(exc)
            return HTMLResponse(content, status_code=500)
        content = self.generate_plain_text(exc)
        return PlainTextResponse(content, status_code=500)

    def error_response(self, request: Request, exc: Exception) -> Response:
        return PlainTextResponse("Internal Server Error", status_code=500)
<|endoftext|>


<commit_before>#   encoding: utf8
#   command_handlers.py
"""Defines handlers for bot_commands."""

import logging

from .db_interaction import add_timelapse, add_user, edit_timelapse
from .interaction_utils import UNITS_KEYBOARD, create_reply_markup, \
        send_keyboard
from .models import UnitEnum
from .telegram import answer_callback_query, edit_message_text, send_message

def on_units_button(timelapse_id, chat_id, message_id, database):
    new_text = 'Please specify units to measure your timelapse ' \
                'progress in'

    new_reply_markup = create_reply_markup(UNITS_KEYBOARD, timelapse_id)
    edit_message_text(chat_id, message_id, new_text, new_reply_markup)

def on_duration_button(timelapse_id, chat_id, message_id, database):
    pass

def on_start_time_button(timelapse_id, chat_id, message_id, database):
    pass

def on_progress_button(timelapse_id, chat_id, message_id, database):
    pass

def on_units_keyboard_button(unit, timelapse_id, chat_id,
                             message_id, database):
    new_text = 'Got it. Your timelapse countdown is in %s.' % unit.value

    edit_timelapse(timelapse_id, 'units', unit, database)
    edit_message_text(chat_id, message_id, new_text)

def on_hours_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.h, timelapse_id,
                                    chat_id, message_id, database)

def on_days_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.d, timelapse_id,
                                    chat_id, message_id, database)

def on_weeks_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.w, timelapse_id,
                                    chat_id, message_id, database)

def on_months_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.m, timelapse_id,
                                    chat_id, message_id, database)

def on_years_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.y, timelapse_id,
                                    chat_id, message_id, database)

# def on_units_keyboard_button(data, timelapse_id, database):
    # TODO think of a way to use something like this...
    # to avoid multiple almost identical functions and
    # also ifelse blocks in on_query_updte_message
#    new_text = 'Got it. Your timelapse countdown is in %s.' % data

#    edit_timelapse(timelapse_id, 'units', data, database)
#    return lambda chat_id, message_id: edit_message_text(
#            chat_id, message_id, new_text)

def on_query_update_message(data, query, database):
    # TODO move separator to top as DATA_SEPARATOR

    data, timelapse_id = data.split('|')

    switch_on_data = {
            'units': on_units_button,
            'duration': on_duration_button,
            'start time': on_start_time_button,
            'progress': on_progress_button,
            'hours': on_hours_button,
            'days': on_days_button,
            'weeks': on_weeks_button,
            'months': on_months_button,
            'years': on_years_button,
    }

    handle_query = switch_on_data.get(data)
    if handle_query:
        message = query.get('message')
        chat_id = message.get('chat').get('id')
        message_id = message.get('message_id')

        handle_query(timelapse_id, chat_id, message_id, database)

def handle_callback_query(callback_query, database):
    callback_query_id = callback_query.get('id')
    answer_callback_query(callback_query_id)

    callback_data = callback_query.get('data')
    on_query_update_message(callback_data, callback_query, database)

def handle_start(message, database):
    user_info = get_user_info(message)
    chat_id = message.get('chat').get('id')

    first_name = add_user(user_info, database)
    if first_name:
        text = 'Welcome %s!' % first_name
    else:
        text = 'Welcome back %s!' % user_info['first_name']
    return send_message(chat_id, text)

def handle_add(message, database):
    chat_id = message.get('chat').get('id')
    user_id = message.get('from').get('id')
    timelapse_info = get_timelapse_info(message)
    if timelapse_info:
        timelapse_id = add_timelapse(timelapse_info, user_id, database)
        send_keyboard(chat_id, timelapse_info, timelapse_id)
    else:
        logging.error('Poor user input. User %d', chat_id)
        text = 'Please provide a name for your brand new timelapse ' \
            '(up to 3 words). Example: \n /add MY BEST SUMMER'
        send_message(chat_id, text)
    return timelapse_info

def handle_command(command, message, database):
    for command_name, command_handler in COMMAND_HANDLERS.items():
        if command_name == command:
            command_handler(message, database)

def handle_commands(commands, message, database):
    for command in commands:
        handle_command(command, message, database)

# FIXME; fix cycle dependencies due to wrong module isolation.
from .update_parser import COMMAND_HANDLERS, get_timelapse_info, get_user_info
<commit_msg>Specify callback transition table in globals.<commit_after>#   encoding: utf8
#   command_handlers.py
"""Defines handlers for bot_commands."""

import logging

from .db_interaction import add_timelapse, add_user, edit_timelapse
from .interaction_utils import UNITS_KEYBOARD, create_reply_markup, \
        send_keyboard
from .models import UnitEnum
from .telegram import answer_callback_query, edit_message_text, send_message

def on_units_button(timelapse_id, chat_id, message_id, database):
    new_text = 'Please specify units to measure your timelapse ' \
                'progress in'

    new_reply_markup = create_reply_markup(UNITS_KEYBOARD, timelapse_id)
    edit_message_text(chat_id, message_id, new_text, new_reply_markup)

def on_duration_button(timelapse_id, chat_id, message_id, database):
    pass

def on_start_time_button(timelapse_id, chat_id, message_id, database):
    pass

def on_progress_button(timelapse_id, chat_id, message_id, database):
    pass

def on_units_keyboard_button(unit, timelapse_id, chat_id,
                             message_id, database):
    new_text = 'Got it. Your timelapse countdown is in %s.' % unit.value

    edit_timelapse(timelapse_id, 'units', unit, database)
    edit_message_text(chat_id, message_id, new_text)

def on_hours_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.h, timelapse_id,
                                    chat_id, message_id, database)

def on_days_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.d, timelapse_id,
                                    chat_id, message_id, database)

def on_weeks_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.w, timelapse_id,
                                    chat_id, message_id, database)

def on_months_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.m, timelapse_id,
                                    chat_id, message_id, database)

def on_years_button(timelapse_id, chat_id, message_id, database):
    return on_units_keyboard_button(UnitEnum.y, timelapse_id,
                                    chat_id, message_id, database)

# def on_units_keyboard_button(data, timelapse_id, database):
    # TODO think of a way to use something like this...
    # to avoid multiple almost identical functions and
    # also ifelse blocks in on_query_updte_message
#    new_text = 'Got it. Your timelapse countdown is in %s.' % data

#    edit_timelapse(timelapse_id, 'units', data, database)
#    return lambda chat_id, message_id: edit_message_text(
#            chat_id, message_id, new_text)

CALLBACK_HANDLERS = {
    'units': on_units_button,
    'duration': on_duration_button,
    'start time': on_start_time_button,
    'progress': on_progress_button,
    'hours': on_hours_button,
    'days': on_days_button,
    'weeks': on_weeks_button,
    'months': on_months_button,
    'years': on_years_button,
}

def on_query_update_message(data, query, database):
    # TODO move separator to top as DATA_SEPARATOR

    data, timelapse_id = data.split('|')

    if data in CALLBACK_HANDLERS:
        message = query.get('message')
        chat_id = message.get('chat').get('id')
        message_id = message.get('message_id')

        CALLBACK_HANDLERS[data](timelapse_id, chat_id, message_id, database)

def handle_callback_query(callback_query, database):
    callback_query_id = callback_query.get('id')
    answer_callback_query(callback_query_id)

    callback_data = callback_query.get('data')
    on_query_update_message(callback_data, callback_query, database)

def handle_start(message, database):
    user_info = get_user_info(message)
    chat_id = message.get('chat').get('id')

    first_name = add_user(user_info, database)
    if first_name:
        text = 'Welcome %s!' % first_name
    else:
        text = 'Welcome back %s!' % user_info['first_name']
    return send_message(chat_id, text)

def handle_add(message, database):
    chat_id = message.get('chat').get('id')
    user_id = message.get('from').get('id')
    timelapse_info = get_timelapse_info(message)
    if timelapse_info:
        timelapse_id = add_timelapse(timelapse_info, user_id, database)
        send_keyboard(chat_id, timelapse_info, timelapse_id)
    else:
        logging.error('Poor user input. User %d', chat_id)
        text = 'Please provide a name for your brand new timelapse ' \
            '(up to 3 words). Example: \n /add MY BEST SUMMER'
        send_message(chat_id, text)
    return timelapse_info

def handle_command(command, message, database):
    for command_name, command_handler in COMMAND_HANDLERS.items():
        if command_name == command:
            command_handler(message, database)

def handle_commands(commands, message, database):
    for command in commands:
        handle_command(command, message, database)

# FIXME; fix cycle dependencies due to wrong module isolation.
from .update_parser import COMMAND_HANDLERS, get_timelapse_info, get_user_info
<|endoftext|>