<commit_before>from ..compat import standard_library

from os.path import relpath, splitext
from urllib.parse import urljoin
from base64 import b64decode
from uuid import uuid4
import json, os

from flask import Flask, request, Response, current_app, jsonify
from uritemplate import expand
from requests import get, post
from psycopg2 import connect
from pq import PQ

def load_config():
    return dict(GITHUB_AUTH=(os.environ['GITHUB_TOKEN'], 'x-oauth-basic'),
                DATABASE_URL=os.environ['DATABASE_URL'])

app = Flask(__name__)
app.config.update(load_config())

MAGIC_OK_MESSAGE = 'Everything is fine'
TASK_QUEUE, DONE_QUEUE = 'tasks', 'finished'

@app.route('/')
def index():
    return 'Yo.'

@app.route('/hook', methods=['POST'])
def hook():
    github_auth = current_app.config['GITHUB_AUTH']
    webhook_payload = json.loads(request.data.decode('utf8'))
    files = process_payload_files(webhook_payload, github_auth)
    status_url = get_status_url(webhook_payload)

    if not files:
        update_empty_status(status_url, github_auth)
        return jsonify({'url': None, 'files': []})

    filenames = list(files.keys())
    file_states = {name: None for name in filenames}
    file_results = {name: None for name in filenames}

    job_id = calculate_job_id(files)
    job_url = urljoin(request.url, '/jobs/{id}'.format(id=job_id))
    job_status = None

    with db_connect(current_app.config['DATABASE_URL']) as conn:
        queue = db_queue(conn)
        with queue as db:
            try:
                # Add the touched files to a task queue.
                task_files = add_files_to_queue(queue, job_id, job_url, files)
            except Exception as e:
                # Oops, tell Github something went wrong.
                update_error_status(status_url, str(e), filenames, github_auth)
                return Response(json.dumps({'error': str(e), 'files': files}),
                                500, content_type='application/json')
            else:
                # That worked, remember them in the database.
                add_job(db, job_id, job_status, task_files, file_states, file_results, status_url)
                update_pending_status(status_url, job_url, filenames, github_auth)
                return jsonify({'id': job_id, 'url': job_url, 'files': files})

@app.route('/jobs/<job_id>', methods=['GET'])
def get_job(job_id):
    '''
    '''
    with db_connect(current_app.config['DATABASE_URL']) as conn:
        with db_cursor(conn) as db:
            try:
                status, task_files, file_states, file_results, github_status_url = read_job(db, job_id)
            except TypeError:
                return Response('Job {} not found'.format(job_id), 404)

    job = dict(status=status, task_files=task_files, file_states=file_states,
               file_results=file_results, github_status_url=github_status_url)

    return jsonify(job)

def get_touched_payload_files(payload):
    ''' Return a set of files modified in payload commits.
    '''
    touched = set()

    # Iterate over commits in chronological order.
    for commit in payload['commits']:
        for filelist in (commit['added'], commit['modified']):
            # Include any potentially-new files.
            touched.update(filelist)

        for filename in commit['removed']:
            # Skip files that no longer exist.
            touched.remove(filename)

    current_app.logger.debug('Touched files {}'.format(', '.join(touched)))

    return touched

def get_touched_branch_files(payload, github_auth):
    ''' Return a set of files modified between master and payload head.
    '''
    branch_sha = payload['head_commit']['id']

    compare1_url = payload['repository']['compare_url']
    compare1_url = expand(compare1_url, dict(base='master', head=branch_sha))
    current_app.logger.debug('Compare URL 1 {}'.format(compare1_url))

    compare1 = get(compare1_url, auth=github_auth).json()
    merge_base_sha = compare1['merge_base_commit']['sha']

    # That's no branch.
    if merge_base_sha == branch_sha:
        return set()

    compare2_url = payload['repository']['compare_url']
    compare2_url = expand(compare2_url, dict(base=merge_base_sha, head=branch_sha))
    current_app.logger.debug('Compare URL 2 {}'.format(compare2_url))

    compare2 = get(compare2_url, auth=github_auth).json()
    touched = set([file['filename'] for file in compare2['files']])
    current_app.logger.debug('Touched files {}'.format(', '.join(touched)))

    return touched

def process_payload_files(payload, github_auth):
    ''' Return a dictionary of file paths and raw JSON contents.
    '''
    files = dict()

    touched = get_touched_payload_files(payload)
    touched |= get_touched_branch_files(payload, github_auth)

    commit_sha = payload['head_commit']['id']

    for filename in touched:
        if relpath(filename, 'sources').startswith('..'):
            # Skip things outside of sources directory.
            continue

        if splitext(filename)[1] != '.json':
            # Skip non-JSON files.
            continue

        contents_url = payload['repository']['contents_url']
        contents_url = expand(contents_url, dict(path=filename))
        contents_url = '{contents_url}?ref={commit_sha}'.format(**locals())

        current_app.logger.debug('Contents URL {}'.format(contents_url))

        got = get(contents_url, auth=github_auth)
        contents = got.json()

        if got.status_code not in range(200, 299):
            current_app.logger.warning('Skipping {} - {}'.format(filename, got.status_code))
            continue

        content, encoding = contents['content'], contents['encoding']

        current_app.logger.debug('Contents SHA {}'.format(contents['sha']))

        if encoding == 'base64':
            files[filename] = b64decode(content).decode('utf8')
        else:
            raise ValueError('Unrecognized encoding "{}"'.format(encoding))

    return files

def get_status_url(payload):
    ''' Get Github status API URL from webhook payload.
    '''
    commit_sha = payload['head_commit']['id']
    status_url = payload['repository']['statuses_url']
    status_url = expand(status_url, dict(sha=commit_sha))

    current_app.logger.debug('Status URL {}'.format(status_url))

    return status_url

def post_github_status(status_url, status_json, github_auth):
    ''' POST status JSON to Github status API.
    '''
    # Github only wants 140 chars of description.
    status_json['description'] = status_json['description'][:140]

    posted = post(status_url, data=json.dumps(status_json), auth=github_auth,
                  headers={'Content-Type': 'application/json'})

    if posted.status_code not in range(200, 299):
        raise ValueError('Failed status post to {}'.format(status_url))

    if posted.json()['state'] != status_json['state']:
        raise ValueError('Mismatched status post to {}'.format(status_url))

def update_pending_status(status_url, job_url, filenames, github_auth):
    ''' Push pending status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='pending',
                  description='Checking {}'.format(', '.join(filenames)),
                  target_url=job_url)

    return post_github_status(status_url, status, github_auth)

def update_error_status(status_url, message, filenames, github_auth):
    ''' Push error status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='error',
                  description='Errored on {}: {}'.format(', '.join(filenames), message))

    return post_github_status(status_url, status, github_auth)

def update_failing_status(status_url, job_url, bad_files, filenames, github_auth):
    ''' Push failing status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='failure',
                  description='Failed on {} from {}'.format(', '.join(bad_files), ', '.join(filenames)),
                  target_url=job_url)

    return post_github_status(status_url, status, github_auth)

def update_empty_status(status_url, github_auth):
    ''' Push success status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='success',
                  description='Nothing to check')

    return post_github_status(status_url, status, github_auth)

def update_success_status(status_url, job_url, filenames, github_auth):
    ''' Push success status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='success',
                  description='Succeeded on {}'.format(', '.join(filenames)),
                  target_url=job_url)

    return post_github_status(status_url, status, github_auth)

def calculate_job_id(files):
    '''
    '''
    return str(uuid4())

    #
    # Previously, we created a deterministic hash of
    # the files, but for now that might be too optimistic.
    #
    blob = json.dumps(files, ensure_ascii=True, sort_keys=True)
    job_id = sha1(blob).hexdigest()

    return job_id

def add_files_to_queue(queue, job_id, job_url, files):
    ''' Make a new task for each file, return dict of taks IDs to file names.
    '''
    tasks = {}

    for (name, content) in files.items():
        task = queue.put(dict(id=job_id, url=job_url, name=name, content=content))

        tasks[str(task)] = name

    return tasks

def add_job(db, job_id, status, task_files, file_states, file_results, status_url):
    ''' Save information about a job to the database.

        Throws an IntegrityError exception if the job ID exists.
    '''
    db.execute('''INSERT INTO jobs
                  (task_files, file_states, file_results, github_status_url, status, id)
                  VALUES (%s::json, %s::json, %s::json, %s, %s, %s)''',
               (json.dumps(task_files), json.dumps(file_states),
                json.dumps(file_results), status_url, status, job_id))

def write_job(db, job_id, status, task_files, file_states, file_results, status_url):
    ''' Save information about a job to the database.
    '''
    db.execute('''UPDATE jobs
                  SET task_files=%s::json, file_states=%s::json,
                      file_results=%s::json, github_status_url=%s, status=%s
                  WHERE id = %s''',
               (json.dumps(task_files), json.dumps(file_states),
                json.dumps(file_results), status_url, status, job_id))

def read_job(db, job_id):
    ''' Read information about a job from the database.

        Returns (status, task_files, file_states, file_results, github_status_url) or None.
    '''
    db.execute('''SELECT status, task_files, file_states, file_results, github_status_url
                  FROM jobs WHERE id = %s''', (job_id, ))

    try:
        status, task_files, states, file_results, github_status_url = db.fetchone()
    except TypeError:
        return None
    else:
        return status, task_files, states, file_results, github_status_url

def pop_task_from_donequeue(queue, github_auth):
    ''' Look for a completed job in the "done" task queue, update Github status.
    '''
    with queue as db:
        task = queue.get()

        if task is None:
            return

        results = task.data['result']
        message = results['message']
        job_url = task.data['url']
        filename = task.data['name']
        job_id = task.data['id']

        try:
            _, task_files, file_states, file_results, status_url = read_job(db, job_id)
        except TypeError:
            raise Exception('Job {} not found'.format(job_id))

        if filename not in file_states:
            raise Exception('Unknown file from job {}: "{}"'.format(job_id, filename))

        filenames = list(task_files.values())
        file_states[filename] = bool(message == MAGIC_OK_MESSAGE)
        file_results[filename] = results

        if False in file_states.values():
            # Any task failure means the whole job has failed.
            job_status = False
        elif None in file_states.values():
            job_status = None
        else:
            job_status = True

        write_job(db, job_id, job_status, task_files, file_states, file_results, status_url)

        if job_status is False:
            bad_files = [name for (name, state) in file_states.items() if state is False]
            update_failing_status(status_url, job_url, bad_files, filenames, github_auth)

        elif job_status is None:
            update_pending_status(status_url, job_url, filenames, github_auth)

        elif job_status is True:
            update_success_status(status_url, job_url, filenames, github_auth)

def db_connect(dsn):
    ''' Connect to database using DSN string.
    '''
    return connect(dsn)

def db_queue(conn, name=None):
    return PQ(conn, table='queue')[name or TASK_QUEUE]

def db_cursor(conn):
    return conn.cursor()

if __name__ == '__main__':
    app.run(debug=True)
<commit_msg>Including file ID in return of openaddr.ci.process_payload_files()<commit_after>from ..compat import standard_library

from os.path import relpath, splitext
from urllib.parse import urljoin
from base64 import b64decode
from uuid import uuid4
import json, os

from flask import Flask, request, Response, current_app, jsonify
from uritemplate import expand
from requests import get, post
from psycopg2 import connect
from pq import PQ

def load_config():
    return dict(GITHUB_AUTH=(os.environ['GITHUB_TOKEN'], 'x-oauth-basic'),
                DATABASE_URL=os.environ['DATABASE_URL'])

app = Flask(__name__)
app.config.update(load_config())

MAGIC_OK_MESSAGE = 'Everything is fine'
TASK_QUEUE, DONE_QUEUE = 'tasks', 'finished'

@app.route('/')
def index():
    return 'Yo.'

@app.route('/hook', methods=['POST'])
def hook():
    github_auth = current_app.config['GITHUB_AUTH']
    webhook_payload = json.loads(request.data.decode('utf8'))
    files = process_payload_files(webhook_payload, github_auth)
    status_url = get_status_url(webhook_payload)

    if not files:
        update_empty_status(status_url, github_auth)
        return jsonify({'url': None, 'files': []})

    filenames = list(files.keys())
    file_states = {name: None for name in filenames}
    file_results = {name: None for name in filenames}

    job_id = calculate_job_id(files)
    job_url = urljoin(request.url, '/jobs/{id}'.format(id=job_id))
    job_status = None

    with db_connect(current_app.config['DATABASE_URL']) as conn:
        queue = db_queue(conn)
        with queue as db:
            try:
                # Add the touched files to a task queue.
                task_files = add_files_to_queue(queue, job_id, job_url, files)
            except Exception as e:
                # Oops, tell Github something went wrong.
                update_error_status(status_url, str(e), filenames, github_auth)
                return Response(json.dumps({'error': str(e), 'files': files}),
                                500, content_type='application/json')
            else:
                # That worked, remember them in the database.
                add_job(db, job_id, job_status, task_files, file_states, file_results, status_url)
                update_pending_status(status_url, job_url, filenames, github_auth)
                return jsonify({'id': job_id, 'url': job_url, 'files': files})

@app.route('/jobs/<job_id>', methods=['GET'])
def get_job(job_id):
    '''
    '''
    with db_connect(current_app.config['DATABASE_URL']) as conn:
        with db_cursor(conn) as db:
            try:
                status, task_files, file_states, file_results, github_status_url = read_job(db, job_id)
            except TypeError:
                return Response('Job {} not found'.format(job_id), 404)

    job = dict(status=status, task_files=task_files, file_states=file_states,
               file_results=file_results, github_status_url=github_status_url)

    return jsonify(job)

def get_touched_payload_files(payload):
    ''' Return a set of files modified in payload commits.
    '''
    touched = set()

    # Iterate over commits in chronological order.
    for commit in payload['commits']:
        for filelist in (commit['added'], commit['modified']):
            # Include any potentially-new files.
            touched.update(filelist)

        for filename in commit['removed']:
            # Skip files that no longer exist.
            touched.remove(filename)

    current_app.logger.debug('Touched files {}'.format(', '.join(touched)))

    return touched

def get_touched_branch_files(payload, github_auth):
    ''' Return a set of files modified between master and payload head.
    '''
    branch_sha = payload['head_commit']['id']

    compare1_url = payload['repository']['compare_url']
    compare1_url = expand(compare1_url, dict(base='master', head=branch_sha))
    current_app.logger.debug('Compare URL 1 {}'.format(compare1_url))

    compare1 = get(compare1_url, auth=github_auth).json()
    merge_base_sha = compare1['merge_base_commit']['sha']

    # That's no branch.
    if merge_base_sha == branch_sha:
        return set()

    compare2_url = payload['repository']['compare_url']
    compare2_url = expand(compare2_url, dict(base=merge_base_sha, head=branch_sha))
    current_app.logger.debug('Compare URL 2 {}'.format(compare2_url))

    compare2 = get(compare2_url, auth=github_auth).json()
    touched = set([file['filename'] for file in compare2['files']])
    current_app.logger.debug('Touched files {}'.format(', '.join(touched)))

    return touched

def process_payload_files(payload, github_auth):
    ''' Return a dictionary of file paths to raw JSON contents and file IDs.
    '''
    files = dict()

    touched = get_touched_payload_files(payload)
    touched |= get_touched_branch_files(payload, github_auth)

    commit_sha = payload['head_commit']['id']

    for filename in touched:
        if relpath(filename, 'sources').startswith('..'):
            # Skip things outside of sources directory.
            continue

        if splitext(filename)[1] != '.json':
            # Skip non-JSON files.
            continue

        contents_url = payload['repository']['contents_url']
        contents_url = expand(contents_url, dict(path=filename))
        contents_url = '{contents_url}?ref={commit_sha}'.format(**locals())

        current_app.logger.debug('Contents URL {}'.format(contents_url))

        got = get(contents_url, auth=github_auth)
        contents = got.json()

        if got.status_code not in range(200, 299):
            current_app.logger.warning('Skipping {} - {}'.format(filename, got.status_code))
            continue

        content, encoding = contents['content'], contents['encoding']

        current_app.logger.debug('Contents SHA {}'.format(contents['sha']))

        if encoding == 'base64':
            files[filename] = b64decode(content).decode('utf8'), contents['sha']
        else:
            raise ValueError('Unrecognized encoding "{}"'.format(encoding))

    return files

def get_status_url(payload):
    ''' Get Github status API URL from webhook payload.
    '''
    commit_sha = payload['head_commit']['id']
    status_url = payload['repository']['statuses_url']
    status_url = expand(status_url, dict(sha=commit_sha))

    current_app.logger.debug('Status URL {}'.format(status_url))

    return status_url

def post_github_status(status_url, status_json, github_auth):
    ''' POST status JSON to Github status API.
    '''
    # Github only wants 140 chars of description.
    status_json['description'] = status_json['description'][:140]

    posted = post(status_url, data=json.dumps(status_json), auth=github_auth,
                  headers={'Content-Type': 'application/json'})

    if posted.status_code not in range(200, 299):
        raise ValueError('Failed status post to {}'.format(status_url))

    if posted.json()['state'] != status_json['state']:
        raise ValueError('Mismatched status post to {}'.format(status_url))

def update_pending_status(status_url, job_url, filenames, github_auth):
    ''' Push pending status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='pending',
                  description='Checking {}'.format(', '.join(filenames)),
                  target_url=job_url)

    return post_github_status(status_url, status, github_auth)

def update_error_status(status_url, message, filenames, github_auth):
    ''' Push error status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='error',
                  description='Errored on {}: {}'.format(', '.join(filenames), message))

    return post_github_status(status_url, status, github_auth)

def update_failing_status(status_url, job_url, bad_files, filenames, github_auth):
    ''' Push failing status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='failure',
                  description='Failed on {} from {}'.format(', '.join(bad_files), ', '.join(filenames)),
                  target_url=job_url)

    return post_github_status(status_url, status, github_auth)

def update_empty_status(status_url, github_auth):
    ''' Push success status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='success',
                  description='Nothing to check')

    return post_github_status(status_url, status, github_auth)

def update_success_status(status_url, job_url, filenames, github_auth):
    ''' Push success status for head commit to Github status API.
    '''
    status = dict(context='openaddresses/hooked', state='success',
                  description='Succeeded on {}'.format(', '.join(filenames)),
                  target_url=job_url)

    return post_github_status(status_url, status, github_auth)

def calculate_job_id(files):
    '''
    '''
    return str(uuid4())

    #
    # Previously, we created a deterministic hash of
    # the files, but for now that might be too optimistic.
    #
    blob = json.dumps(files, ensure_ascii=True, sort_keys=True)
    job_id = sha1(blob).hexdigest()

    return job_id

def add_files_to_queue(queue, job_id, job_url, files):
    ''' Make a new task for each file, return dict of taks IDs to file names.
    '''
    tasks = {}

    for (name, (content, file_id)) in files.items():
        task = queue.put(dict(id=job_id, url=job_url, name=name,
                              content=content, file_id=file_id))

        tasks[str(task)] = name

    return tasks

def add_job(db, job_id, status, task_files, file_states, file_results, status_url):
    ''' Save information about a job to the database.

        Throws an IntegrityError exception if the job ID exists.
    '''
    db.execute('''INSERT INTO jobs
                  (task_files, file_states, file_results, github_status_url, status, id)
                  VALUES (%s::json, %s::json, %s::json, %s, %s, %s)''',
               (json.dumps(task_files), json.dumps(file_states),
                json.dumps(file_results), status_url, status, job_id))

def write_job(db, job_id, status, task_files, file_states, file_results, status_url):
    ''' Save information about a job to the database.
    '''
    db.execute('''UPDATE jobs
                  SET task_files=%s::json, file_states=%s::json,
                      file_results=%s::json, github_status_url=%s, status=%s
                  WHERE id = %s''',
               (json.dumps(task_files), json.dumps(file_states),
                json.dumps(file_results), status_url, status, job_id))

def read_job(db, job_id):
    ''' Read information about a job from the database.

        Returns (status, task_files, file_states, file_results, github_status_url) or None.
    '''
    db.execute('''SELECT status, task_files, file_states, file_results, github_status_url
                  FROM jobs WHERE id = %s''', (job_id, ))

    try:
        status, task_files, states, file_results, github_status_url = db.fetchone()
    except TypeError:
        return None
    else:
        return status, task_files, states, file_results, github_status_url

def pop_task_from_donequeue(queue, github_auth):
    ''' Look for a completed job in the "done" task queue, update Github status.
    '''
    with queue as db:
        task = queue.get()

        if task is None:
            return

        results = task.data['result']
        message = results['message']
        job_url = task.data['url']
        filename = task.data['name']
        job_id = task.data['id']

        try:
            _, task_files, file_states, file_results, status_url = read_job(db, job_id)
        except TypeError:
            raise Exception('Job {} not found'.format(job_id))

        if filename not in file_states:
            raise Exception('Unknown file from job {}: "{}"'.format(job_id, filename))

        filenames = list(task_files.values())
        file_states[filename] = bool(message == MAGIC_OK_MESSAGE)
        file_results[filename] = results

        if False in file_states.values():
            # Any task failure means the whole job has failed.
            job_status = False
        elif None in file_states.values():
            job_status = None
        else:
            job_status = True

        write_job(db, job_id, job_status, task_files, file_states, file_results, status_url)

        if job_status is False:
            bad_files = [name for (name, state) in file_states.items() if state is False]
            update_failing_status(status_url, job_url, bad_files, filenames, github_auth)

        elif job_status is None:
            update_pending_status(status_url, job_url, filenames, github_auth)

        elif job_status is True:
            update_success_status(status_url, job_url, filenames, github_auth)

def db_connect(dsn):
    ''' Connect to database using DSN string.
    '''
    return connect(dsn)

def db_queue(conn, name=None):
    return PQ(conn, table='queue')[name or TASK_QUEUE]

def db_cursor(conn):
    return conn.cursor()

if __name__ == '__main__':
    app.run(debug=True)
<|endoftext|>


<commit_before># The MIT License (MIT)
#
# Copyright (c) 2014-2015 WUSTL ZPLAB
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Authors: Erik Hvatum <ice.rikh@gmail.com>

from contextlib import ExitStack
from .gl_resources import GL
import math
import numpy
from PyQt5 import Qt
from .shader_scene import ShaderScene, ShaderItem, ShaderTexture, UNIQUE_QGRAPHICSITEM_TYPE
from .shader_view import ShaderView
import sys

class ImageScene(ShaderScene):
    def __init__(self, parent):
        super().__init__(parent)
        self.image_item = ImageItem()
        self.addItem(self.image_item)
        self._histogram_scene = None

    def on_image_changing(self, image):
        self.image_item.on_image_changing(image)
        self.setSceneRect(self.image_item.boundingRect())
        for view in self.views():
            view.on_image_changing(image)

    def on_histogram_gamma_or_min_max_changed(self):
        self.image_item.update()

    @property
    def histogram_scene(self):
        return self._histogram_scene

    @histogram_scene.setter
    def histogram_scene(self, histogram_scene):
        if self._histogram_scene is not None:
            self._histogram_scene.gamma_or_min_max_changed.disconnect(self.on_histogram_gamma_or_min_max_changed)
        histogram_scene.gamma_or_min_max_changed.connect(self.on_histogram_gamma_or_min_max_changed)
        self._histogram_scene = histogram_scene
        self.on_histogram_gamma_or_min_max_changed()

class ImageItem(ShaderItem):
    QGRAPHICSITEM_TYPE = UNIQUE_QGRAPHICSITEM_TYPE()
    NUMPY_DTYPE_TO_GL_PIXEL_DATA_TYPE_STR = {
        numpy.uint8  : 'GL_UNSIGNED_BYTE',
        numpy.uint16 : 'GL_UNSIGNED_SHORT',
        numpy.uint32 : 'GL_UNSIGNED_INT',
        numpy.float32: 'GL_FLOAT'}
    NUMPY_DTYPE_TO_GL_INTERNAL_FORMAT_TYPE_STR = {
        numpy.uint8  : '8UI_EXT',
        numpy.uint16 : '16UI_EXT',
        numpy.uint32 : '32UI_EXT',
        numpy.float32: ''}
    IMAGE_TYPE_TO_GL_FORMAT_NAME_STR = {
        'g'   : 'LUMINANCE',
        'ga'  : 'LUMINANCE_ALPHA',
        'rgb' : 'RGB',
        'rgba': 'RGBA'}
    IMAGE_TYPE_TO_COMBINE_VT_COMPONENTS = {
        'g'   : 'vec4(vcomponents, vcomponents, vcomponents, 1.0f)',
        'ga'  : 'vec4(vcomponents, vcomponents, vcomponents, tcomponents.a)',
        'rgb' : 'vec4(vcomponents.rgb, 1.0f)',
        'rgba': 'vec4(vcomponents.rgb, tcomponents.a)'}

    def type(self):
        return GammaItem.QGRAPHICSITEM_TYPE

    def boundingRect(self):
        return Qt.QRectF() if self.image is None else Qt.QRectF(Qt.QPointF(), Qt.QSizeF(self.image.size))

    def paint(self, qpainter, option, widget):
        if widget is None:
            print('WARNING: image_view.ImageItem.paint called with widget=None.  Ensure that view caching is disabled.')
        elif self.image is None:
            if widget.view in self.view_resources:
                vrs = self.view_resources[widget.view]
                if 'tex' in vrs:
                    vrs['tex'].destroy()
                    del vrs['tex']
        else:
            with ExitStack() as stack:
                qpainter.beginNativePainting()
                stack.callback(qpainter.endNativePainting)
                gl = GL()
                image = self.image
                view = widget.view
                if view in self.view_resources:
                    vrs = self.view_resources[view]
                else:
                    self.view_resources[view] = vrs = {}
                tex_format_name_str = ImageItem.IMAGE_TYPE_TO_GL_FORMAT_NAME_STR[image.type]
                tex_internal_format_type_str = ImageItem.NUMPY_DTYPE_TO_GL_INTERNAL_FORMAT_TYPE_STR[image.dtype]
                tex_internal_format = getattr(gl, 'GL_{}{}'.format(tex_format_name_str, tex_internal_format_type_str))
                shader_desc = '{}_{}_{}'.format(image.type, tex_format_name_str, tex_internal_format_type_str)
                if 'progs' in vrs:
                    progs = vrs['progs']
                else:
                    progs = vrs['progs'] = {}
                if shader_desc in progs:
                    prog = progs[shader_desc]
                else:
                    if image.dtype == numpy.float32:
                        sampler_t = 'sampler2D'
                        raw_tcomponents_t = 'vec4'
                        raw_tcomponents_to_tcomponents = 'raw_tcomponents'
                    else:
                        sampler_t = 'usampler2D'
                        raw_tcomponents_t = 'uvec4'
                        raw_tcomponents_to_tcomponents = 'vec4(raw_tcomponents)'
                    if image.is_grayscale:
                        vcomponents_t = 'float'
                        extract_vcomponents = 'tcomponents.r'
                        vcomponents_ones_vector = '1.0f'
                    else:
                        vcomponents_t = 'vec3'
                        extract_vcomponents = 'tcomponents.rgb'
                        vcomponents_ones_vector = 'vec3(1.0f, 1.0f, 1.0f)'
                    self.build_shader_prog(shader_desc,
                                           'image_widget_vertex_shader.glsl',
                                           'image_widget_fragment_shader_template.glsl',
                                           view,
                                           sampler_t=sampler_t,
                                           raw_tcomponents_t=raw_tcomponents_t,
                                           raw_tcomponents_to_tcomponents=raw_tcomponents_to_tcomponents,
                                           vcomponents_t=vcomponents_t,
                                           extract_vcomponents=extract_vcomponents,
                                           vcomponents_ones_vector=vcomponents_ones_vector,
                                           combine_vt_components=ImageItem.IMAGE_TYPE_TO_COMBINE_VT_COMPONENTS[image.type])
                    prog = progs[shader_desc]
                prog.bind()
                stack.callback(prog.release)
                if 'tex' in vrs:
                    tex = vrs['tex']
                    if tex.size != image.size or tex.internal_format != tex_internal_format:
                        tex.destroy()
                        del vrs['tex']
                if 'tex' not in vrs:
                    tex = ShaderTexture(gl.GL_TEXTURE_2D)
                    tex.bind()
                    stack.callback(tex.release)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
                    tex.image_id = -1
                    tex.size = None
                    vrs['tex'] = tex
                else:
                    tex.bind()
                    stack.callback(tex.release)
                if tex.image_id != self._image_id:
                    orig_unpack_alignment = gl.glGetIntegerv(gl.GL_UNPACK_ALIGNMENT)
                    if orig_unpack_alignment != 1:
                        gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
                        # QPainter font rendering for OpenGL surfaces will become broken if we do not restore GL_UNPACK_ALIGNMENT
                        # to whatever QPainter had it set to (when it prepared the OpenGL context for our use as a result of
                        # qpainter.beginNativePainting()).
                        stack.callback(lambda oua=orig_unpack_alignment: gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, oua))
                    src_format_str = 'GL_' + tex_format_name_str
                    if image.type != numpy.float32:
                        src_format_str += '_INTEGER_EXT'
                    src_format = getattr(gl, src_format_str)
                    src_gl_data_type = getattr(gl, ImageItem.NUMPY_DTYPE_TO_GL_PIXEL_DATA_TYPE_STR[image.dtype])
                    gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, tex_internal_format,
                                    image.size.width(), image.size.height(), 0,
                                    src_format,
                                    src_gl_data_type,
                                    memoryview(image.data.data))
                    tex.internal_format = tex_internal_format
                    tex.size = image.size
                    tex.image_id = self._image_id
                view.quad_buffer.bind()
                stack.callback(view.quad_buffer.release)
                view.quad_vao.bind()
                stack.callback(view.quad_vao.release)
                vert_coord_loc = prog.attributeLocation('vert_coord')
                prog.enableAttributeArray(vert_coord_loc)
                prog.setAttributeBuffer(vert_coord_loc, gl.GL_FLOAT, 0, 2, 0)
                prog.setUniformValue('tex', 0)
                frag_to_tex = Qt.QTransform()
                frame = Qt.QPolygonF(view.mapFromScene(self.boundingRect()))
                if not qpainter.transform().quadToSquare(frame, frag_to_tex):
                    raise RuntimeError('Failed to compute gl_FragCoord to texture coordinate transformation matrix.')
                prog.setUniformValue('frag_to_tex', frag_to_tex)
                prog.setUniformValue('viewport_height', float(widget.size().height()))
                histogram_scene = view.scene().histogram_scene
                if image.is_grayscale:
                    if histogram_scene.rescale_enabled:
                        gamma = histogram_scene.gamma
                        min_max = numpy.array((histogram_scene.min, histogram_scene.max), dtype=float)
                        self._normalize_min_max(min_max)
                    else:
                        gamma = 1
                        min_max = numpy.array((0,1), dtype=float)
                    prog.setUniformValue('gammas', gamma)
                    prog.setUniformValue('vcomponent_rescale_mins', min_max[0])
                    prog.setUniformValue('vcomponent_rescale_ranges', min_max[1] - min_max[0])
                else:
                    if histogram_scene.rescale_enabled:
                        gammas = (histogram_scene.gamma_red, histogram_scene.gamma_green, histogram_scene.gamma_blue)
                        min_maxs = numpy.array(((histogram_scene.min_red, histogram_scene.min_green, histogram_scene.min_blue),
                                                (histogram_scene.max_red, histogram_scene.max_green, histogram_scene.max_blue)))
                    else:
                        gammas = (1,1,1)
                        min_max = self.image.range
                        min_maxs = numpy.array((min_max,)*3).T
                    prog.setUniformValue('gammas', *gammas)
                    self._normalize_min_max(min_maxs)
                    prog.setUniformValue('vcomponent_rescale_mins', *min_maxs[0])
                    prog.setUniformValue('vcomponent_rescale_ranges', *(min_maxs[1]-min_maxs[0]))
                gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
                gl.glDrawArrays(gl.GL_TRIANGLE_FAN, 0, 4)

    def hoverMoveEvent(self, event):
        if self.image is not None:
            # NB: event.pos() is a QPointF, and one may call QPointF.toPoint(), as in the following line,
            # to get a QPoint from it.  However, toPoint() rounds x and y coordinates to the nearest int,
            # which would cause us to erroneously report mouse position as being over the pixel to the
            # right and/or below if the view with the mouse cursor is zoomed in such that an image pixel
            # occupies more than one screen pixel and the cursor is over the right and/or bottom half
            # of a pixel.
#           pos = event.pos().toPoint()
            pos = Qt.QPoint(event.pos().x(), event.pos().y())
            if Qt.QRect(Qt.QPoint(), self.image.size).contains(pos):
                mst = 'x:{} y:{} '.format(pos.x(), pos.y())
                image_type = self.image.type
                vt = '(' + ' '.join((c + ':{}' for c in image_type)) + ')'
                if len(image_type) == 1:
                    vt = vt.format(self.image.data[pos.x(), pos.y()])
                else:
                    vt = vt.format(*self.image.data[pos.x(), pos.y()])
                self.scene().update_mouseover_info(mst + vt, False, self)

    def hoverLeaveEvent(self, event):
        self.scene().clear_mouseover_info(self)

    def on_image_changing(self, image):
        if self.image is None and image is not None or \
           self.image is not None and (image is None or self.image.size != image.size):
            self.prepareGeometryChange()
        super().on_image_changing(image)

    def free_shader_view_resources(self, shader_view):
        # TODO: replace QOpenGLTexture usage with luminance-extension-format-supporting ShaderTexture and eliminate this
        # override
        if shader_view in self.view_resources:
            vrs = self.view_resources[shader_view]
            if 'tex' in vrs:
                vrs['tex'][0].destroy()
                del vrs['tex']
        super().free_shader_view_resources(shader_view)
<commit_msg>Use ravel to present flattened view.<commit_after># The MIT License (MIT)
#
# Copyright (c) 2014-2015 WUSTL ZPLAB
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Authors: Erik Hvatum <ice.rikh@gmail.com>

from contextlib import ExitStack
from .gl_resources import GL
import math
import numpy
from PyQt5 import Qt
from .shader_scene import ShaderScene, ShaderItem, ShaderTexture, UNIQUE_QGRAPHICSITEM_TYPE
from .shader_view import ShaderView
import sys
import time

class ImageScene(ShaderScene):
    def __init__(self, parent):
        super().__init__(parent)
        self.image_item = ImageItem()
        self.addItem(self.image_item)
        self._histogram_scene = None

    def on_image_changing(self, image):
        self.image_item.on_image_changing(image)
        self.setSceneRect(self.image_item.boundingRect())
        for view in self.views():
            view.on_image_changing(image)

    def on_histogram_gamma_or_min_max_changed(self):
        self.image_item.update()

    @property
    def histogram_scene(self):
        return self._histogram_scene

    @histogram_scene.setter
    def histogram_scene(self, histogram_scene):
        if self._histogram_scene is not None:
            self._histogram_scene.gamma_or_min_max_changed.disconnect(self.on_histogram_gamma_or_min_max_changed)
        histogram_scene.gamma_or_min_max_changed.connect(self.on_histogram_gamma_or_min_max_changed)
        self._histogram_scene = histogram_scene
        self.on_histogram_gamma_or_min_max_changed()

class ImageItem(ShaderItem):
    QGRAPHICSITEM_TYPE = UNIQUE_QGRAPHICSITEM_TYPE()
    NUMPY_DTYPE_TO_GL_PIXEL_DATA_TYPE_STR = {
        numpy.uint8  : 'GL_UNSIGNED_BYTE',
        numpy.uint16 : 'GL_UNSIGNED_SHORT',
        numpy.uint32 : 'GL_UNSIGNED_INT',
        numpy.float32: 'GL_FLOAT'}
    NUMPY_DTYPE_TO_GL_INTERNAL_FORMAT_TYPE_STR = {
        numpy.uint8  : '8UI_EXT',
        numpy.uint16 : '16UI_EXT',
        numpy.uint32 : '32UI_EXT',
        numpy.float32: ''}
    IMAGE_TYPE_TO_GL_FORMAT_NAME_STR = {
        'g'   : 'LUMINANCE',
        'ga'  : 'LUMINANCE_ALPHA',
        'rgb' : 'RGB',
        'rgba': 'RGBA'}
    IMAGE_TYPE_TO_COMBINE_VT_COMPONENTS = {
        'g'   : 'vec4(vcomponents, vcomponents, vcomponents, 1.0f)',
        'ga'  : 'vec4(vcomponents, vcomponents, vcomponents, tcomponents.a)',
        'rgb' : 'vec4(vcomponents.rgb, 1.0f)',
        'rgba': 'vec4(vcomponents.rgb, tcomponents.a)'}

    def type(self):
        return GammaItem.QGRAPHICSITEM_TYPE

    def boundingRect(self):
        return Qt.QRectF() if self.image is None else Qt.QRectF(Qt.QPointF(), Qt.QSizeF(self.image.size))

    def paint(self, qpainter, option, widget):
        if widget is None:
            print('WARNING: image_view.ImageItem.paint called with widget=None.  Ensure that view caching is disabled.')
        elif self.image is None:
            if widget.view in self.view_resources:
                vrs = self.view_resources[widget.view]
                if 'tex' in vrs:
                    vrs['tex'].destroy()
                    del vrs['tex']
        else:
            with ExitStack() as stack:
                qpainter.beginNativePainting()
                stack.callback(qpainter.endNativePainting)
                gl = GL()
                image = self.image
                view = widget.view
                if view in self.view_resources:
                    vrs = self.view_resources[view]
                else:
                    self.view_resources[view] = vrs = {}
                tex_format_name_str = ImageItem.IMAGE_TYPE_TO_GL_FORMAT_NAME_STR[image.type]
                tex_internal_format_type_str = ImageItem.NUMPY_DTYPE_TO_GL_INTERNAL_FORMAT_TYPE_STR[image.dtype]
                tex_internal_format = getattr(gl, 'GL_{}{}'.format(tex_format_name_str, tex_internal_format_type_str))
                shader_desc = '{}_{}_{}'.format(image.type, tex_format_name_str, tex_internal_format_type_str)
                if 'progs' in vrs:
                    progs = vrs['progs']
                else:
                    progs = vrs['progs'] = {}
                if shader_desc in progs:
                    prog = progs[shader_desc]
                else:
                    if image.dtype == numpy.float32:
                        sampler_t = 'sampler2D'
                        raw_tcomponents_t = 'vec4'
                        raw_tcomponents_to_tcomponents = 'raw_tcomponents'
                    else:
                        sampler_t = 'usampler2D'
                        raw_tcomponents_t = 'uvec4'
                        raw_tcomponents_to_tcomponents = 'vec4(raw_tcomponents)'
                    if image.is_grayscale:
                        vcomponents_t = 'float'
                        extract_vcomponents = 'tcomponents.r'
                        vcomponents_ones_vector = '1.0f'
                    else:
                        vcomponents_t = 'vec3'
                        extract_vcomponents = 'tcomponents.rgb'
                        vcomponents_ones_vector = 'vec3(1.0f, 1.0f, 1.0f)'
                    self.build_shader_prog(shader_desc,
                                           'image_widget_vertex_shader.glsl',
                                           'image_widget_fragment_shader_template.glsl',
                                           view,
                                           sampler_t=sampler_t,
                                           raw_tcomponents_t=raw_tcomponents_t,
                                           raw_tcomponents_to_tcomponents=raw_tcomponents_to_tcomponents,
                                           vcomponents_t=vcomponents_t,
                                           extract_vcomponents=extract_vcomponents,
                                           vcomponents_ones_vector=vcomponents_ones_vector,
                                           combine_vt_components=ImageItem.IMAGE_TYPE_TO_COMBINE_VT_COMPONENTS[image.type])
                    prog = progs[shader_desc]
                prog.bind()
                stack.callback(prog.release)
                if 'tex' in vrs:
                    tex = vrs['tex']
                    if tex.size != image.size or tex.internal_format != tex_internal_format:
                        tex.destroy()
                        del vrs['tex']
                if 'tex' not in vrs:
                    tex = ShaderTexture(gl.GL_TEXTURE_2D)
                    tex.bind()
                    stack.callback(tex.release)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
                    gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
                    tex.image_id = -1
                    tex.size = None
                    vrs['tex'] = tex
                else:
                    tex.bind()
                    stack.callback(tex.release)
                if tex.image_id != self._image_id:
                    orig_unpack_alignment = gl.glGetIntegerv(gl.GL_UNPACK_ALIGNMENT)
                    if orig_unpack_alignment != 1:
                        gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
                        # QPainter font rendering for OpenGL surfaces will become broken if we do not restore GL_UNPACK_ALIGNMENT
                        # to whatever QPainter had it set to (when it prepared the OpenGL context for our use as a result of
                        # qpainter.beginNativePainting()).
                        stack.callback(lambda oua=orig_unpack_alignment: gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, oua))
                    src_format_str = 'GL_' + tex_format_name_str
                    if image.type != numpy.float32:
                        src_format_str += '_INTEGER_EXT'
                    src_format = getattr(gl, src_format_str)
                    src_gl_data_type = getattr(gl, ImageItem.NUMPY_DTYPE_TO_GL_PIXEL_DATA_TYPE_STR[image.dtype])
                    t0=time.time()
                    gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, tex_internal_format,
                                    image.size.width(), image.size.height(), 0,
                                    src_format,
                                    src_gl_data_type,
                                    image.data.ravel(order='K').data)
                    t1=time.time()
                    print('{}ms / {}fps'.format(1000*(t1-t0), 1/(t1-t0)))
                    tex.internal_format = tex_internal_format
                    tex.size = image.size
                    tex.image_id = self._image_id
                view.quad_buffer.bind()
                stack.callback(view.quad_buffer.release)
                view.quad_vao.bind()
                stack.callback(view.quad_vao.release)
                vert_coord_loc = prog.attributeLocation('vert_coord')
                prog.enableAttributeArray(vert_coord_loc)
                prog.setAttributeBuffer(vert_coord_loc, gl.GL_FLOAT, 0, 2, 0)
                prog.setUniformValue('tex', 0)
                frag_to_tex = Qt.QTransform()
                frame = Qt.QPolygonF(view.mapFromScene(self.boundingRect()))
                if not qpainter.transform().quadToSquare(frame, frag_to_tex):
                    raise RuntimeError('Failed to compute gl_FragCoord to texture coordinate transformation matrix.')
                prog.setUniformValue('frag_to_tex', frag_to_tex)
                prog.setUniformValue('viewport_height', float(widget.size().height()))
                histogram_scene = view.scene().histogram_scene
                if image.is_grayscale:
                    if histogram_scene.rescale_enabled:
                        gamma = histogram_scene.gamma
                        min_max = numpy.array((histogram_scene.min, histogram_scene.max), dtype=float)
                        self._normalize_min_max(min_max)
                    else:
                        gamma = 1
                        min_max = numpy.array((0,1), dtype=float)
                    prog.setUniformValue('gammas', gamma)
                    prog.setUniformValue('vcomponent_rescale_mins', min_max[0])
                    prog.setUniformValue('vcomponent_rescale_ranges', min_max[1] - min_max[0])
                else:
                    if histogram_scene.rescale_enabled:
                        gammas = (histogram_scene.gamma_red, histogram_scene.gamma_green, histogram_scene.gamma_blue)
                        min_maxs = numpy.array(((histogram_scene.min_red, histogram_scene.min_green, histogram_scene.min_blue),
                                                (histogram_scene.max_red, histogram_scene.max_green, histogram_scene.max_blue)))
                    else:
                        gammas = (1,1,1)
                        min_max = self.image.range
                        min_maxs = numpy.array((min_max,)*3).T
                    prog.setUniformValue('gammas', *gammas)
                    self._normalize_min_max(min_maxs)
                    prog.setUniformValue('vcomponent_rescale_mins', *min_maxs[0])
                    prog.setUniformValue('vcomponent_rescale_ranges', *(min_maxs[1]-min_maxs[0]))
                gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
                gl.glDrawArrays(gl.GL_TRIANGLE_FAN, 0, 4)

    def hoverMoveEvent(self, event):
        if self.image is not None:
            # NB: event.pos() is a QPointF, and one may call QPointF.toPoint(), as in the following line,
            # to get a QPoint from it.  However, toPoint() rounds x and y coordinates to the nearest int,
            # which would cause us to erroneously report mouse position as being over the pixel to the
            # right and/or below if the view with the mouse cursor is zoomed in such that an image pixel
            # occupies more than one screen pixel and the cursor is over the right and/or bottom half
            # of a pixel.
#           pos = event.pos().toPoint()
            pos = Qt.QPoint(event.pos().x(), event.pos().y())
            if Qt.QRect(Qt.QPoint(), self.image.size).contains(pos):
                mst = 'x:{} y:{} '.format(pos.x(), pos.y())
                image_type = self.image.type
                vt = '(' + ' '.join((c + ':{}' for c in image_type)) + ')'
                if len(image_type) == 1:
                    vt = vt.format(self.image.data[pos.x(), pos.y()])
                else:
                    vt = vt.format(*self.image.data[pos.x(), pos.y()])
                self.scene().update_mouseover_info(mst + vt, False, self)

    def hoverLeaveEvent(self, event):
        self.scene().clear_mouseover_info(self)

    def on_image_changing(self, image):
        if self.image is None and image is not None or \
           self.image is not None and (image is None or self.image.size != image.size):
            self.prepareGeometryChange()
        super().on_image_changing(image)

    def free_shader_view_resources(self, shader_view):
        # TODO: replace QOpenGLTexture usage with luminance-extension-format-supporting ShaderTexture and eliminate this
        # override
        if shader_view in self.view_resources:
            vrs = self.view_resources[shader_view]
            if 'tex' in vrs:
                vrs['tex'][0].destroy()
                del vrs['tex']
        super().free_shader_view_resources(shader_view)
<|endoftext|>


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

from decimal import Decimal
import time
import logging
import os
from distutils.util import strtobool

from ripple_api import call_api, tx

logger = logging.getLogger('ripple_trade')

def sell_all(buy_expected, sell_needed,
             account, secret, timeout=5, fee=10000,
             default_precission=Decimal('0.00000001'),
             servers=None):
    """
    Exchange the entire sell needed amount, even if it means obtaining more
    than the buy expected amount in exchange.

    takes:
    buy_expected -  {
        'value:     - Decimal - min amount to buy
        'currency': - str     - currency
        'issuer':   - str     - issuer
    }
    sell_needed -  {
        'value':    - Decimal - amount to sell
        'currency': - str     - currency
        'issuer':   - str     - issuer
    }
    account            - ripple account
    secret             - ripple secret
    timeout            - trade timeout (default: 5)
    fee                - ripple fee (default: 10000)
    default_precission - compare precission(default: 0.00000001)
    servers            - ripple servers (default: none)

    returns: {
        'status':      - str    - 'success' / 'error',
        'status_msg':  - str    - 'status description'
        'sold':        - float  - the amount sold
        'bought':      - float  - the amount bought
    }

    """
    logger.info('Trading %s %s -> %s %s' %
                (sell_needed['value'], sell_needed['currency'],
                 buy_expected['value'], buy_expected['currency']))
    offer = sell_all_or_cancel(buy_expected, sell_needed, account, secret,
                               timeout=timeout, fee=fee, servers=servers)
    offer_result = get_trade_result(offer, timeout, servers)
    offer_result['sell_amount_left'] = Decimal(
        "%.12f" % float(sell_needed['value'] - offer_result['sold']))

    if offer_result['sold'] >= sell_needed['value'] - default_precission:
        logger.info("Trade fully funded")
    elif offer_result['sold']:
        logger.info("Trade partially happen")
    else:
        logger.info("Trade didn't happen")

    return offer_result

def get_trade_result(created_offer, timeout, servers=None):
    """
    Get created offer result and process it to human readable format.

    takes:
        ripple offer result returned by CreateOffer call

    returns: {
        'status':      - str   - 'success' / 'error',
        'status_msg':  - str   - 'status description'
        'sold':        - float - the amount sold
        'bought':      - float - the amount bought
    }

    """
    # check offer result
    if not created_offer or created_offer['engine_result'] != 'tesSUCCESS':

        status_msg = "Offer creation failed: %s" % \
            created_offer['engine_result'] if created_offer \
            else 'Offer was not created'
        logger.info(status_msg)
        return {'status': 'error',
                'status_msg': status_msg,
                'sold': 0,
                'bought': 0}

    logger.info("Offer created: %s" % created_offer)
    tr_hash = created_offer['tx_json']['hash']
    logger.info("Transaction: %s" % tr_hash)

    # check transaction result
    transaction = get_transaction_result(tr_hash, timeout, servers)

    # if trade didn't happen
    if 'AffectedNodes' not in transaction.get('meta', ''):
        status_msg = "Offer was not identified."
        logger.info("%s AffectedNodes weren't found." % status_msg)
        return {'status': 'error',
                'status_msg': status_msg,
                'sold': 0,
                'bought': 0}

    logger.info("Offer identified: looking for sold, received amount. \n %s" %
                transaction)
    sold, received = get_sold_received(transaction)
    logger.info("Amount sold: %s, Amount received: %s" %
                (sold, received))

    return {'status': 'success',
            'status_msg': 'Trade successfully happened',
            'sold': sold,
            'bought': received}

def get_transaction_result(tr_hash, timeout, servers, attempts=5, wait=2):
    """
    Get created offer hash and look for offer result after it's processing.

    takes:
        tr_hash  - str  - ripple transaction hash
        timeout  - int  - transaction timeout
        servers  - list - ripple servers
        attempts - int  - number of attempts to check if trade happened
        wait     - int  - seconds to wait until next check
    returns:
        transaction result if it's happened or None

    """
    transaction = {}

    # wait for ripple path find
    if not strtobool(os.environ.get("TESTING", "no")):
        time.sleep(wait)

    for i in xrange(attempts):
        transaction = tx(tr_hash, timeout=timeout, servers=servers)
        # check if offer happened
        if 'AffectedNodes' in transaction:
            break
        # wait for ripple path find a little more
        if not strtobool(os.environ.get("TESTING", "no")):
            time.sleep(wait)
    return transaction

def get_sold_received(transaction):
    """
    Calculate amount sold and amount bought from processed offer result.

    takes:
        transaction  -  dict  - ripple tx call result

    returns:
        pays_sum     -  float - amount sold
        gets_sum     -  float - amount bought

    """
    nodes = transaction['meta']['AffectedNodes']
    pays_sum = 0
    gets_sum = 0
    for n in nodes:

        if n.get('DeletedNode', ''):
            played = n['DeletedNode']
            if played.get('LedgerEntryType', '') == 'Offer':
                fields = played['PreviousFields']
                pays_curr, pays_val = restore_taker_values(fields['TakerPays'])
                gets_curr, gets_val = restore_taker_values(fields['TakerGets'])

                pays_sum += pays_val
                gets_sum += gets_val

        if n.get('ModifiedNode', ''):
            played = n['ModifiedNode']
            if played.get('LedgerEntryType', '') == 'Offer':
                prev_fields = played['PreviousFields']
                final_fields = played['FinalFields']
                pf_pays_curr, pf_pays_val = \
                    restore_taker_values(prev_fields['TakerPays'])
                pf_gets_curr, pf_gets_val = \
                    restore_taker_values(prev_fields['TakerGets'])
                ff_pays_currency, ff_pays_value = \
                    restore_taker_values(final_fields['TakerPays'])
                ff_gets_currency, ff_gets_amount = \
                    restore_taker_values(final_fields['TakerGets'])

                pays_sum += pf_pays_val - ff_pays_value
                gets_sum += pf_gets_val - ff_gets_amount

    return pays_sum, gets_sum

def restore_taker_values(taker):
    if isinstance(taker, dict):
        currency = taker['currency']
        value = Decimal(taker['value'])
    return currency, value

def sell_all_or_cancel(taker_pays, taker_gets,
                       account, secret,
                       timeout, fee=10000, servers=None):

    return create_offer(
        taker_pays, taker_gets, account, secret, timeout, fee,
        flags=0x00080000 | 0x00020000, servers=servers)

def create_offer(taker_pays, taker_gets,
                 account, secret,
                 timeout, fee=10000, flags=0, servers=None):
    """
    Create trading offer.

    takes:
        taker_pays -  {
            'value':    - float - amount to buy
            'currency': - str   - currency
            'issuer':   - str   - issuer
        }
        taker_gets -  {
            'value':    - float - amount to sell
            'currency': - str   - currency
            'issuer':   - str   - issuer
        }
        account    - ripple account
        secret     - ripple secret
        timeout    - trade timeout
        fee        - ripple fee (default: 10000)
        flags      - trade flags (default: 0)
        servers    - ripple servers (default: none)

    returns:
        offer

    taker - user, that accepts your offer

    """

    taker_pays = taker_pays.copy()
    taker_gets = taker_gets.copy()
    taker_pays['value'] = "%.12f" % taker_pays['value']
    taker_gets['value'] = "%.12f" % taker_gets['value']
    offer = {
        "method": "submit",
        "params": [{
            "secret": secret,
            "tx_json": {
                "TransactionType": "OfferCreate",
                "Fee": str(fee),
                "Flags": flags,
                "Account": account,
                "TakerPays": taker_pays,
                "TakerGets": taker_gets,
            },
        }]
    }
    logger.info('Trade offer: %s' % offer)
    return call_api(offer, timeout=timeout, servers=servers)
<commit_msg>ticket:20 Logger creation fix<commit_after># -*- coding: utf-8 -*-

from decimal import Decimal
import time
import logging
import os
from distutils.util import strtobool

from ripple_api import call_api, tx

logger = logging.getLogger(__name__)

def sell_all(buy_expected, sell_needed,
             account, secret, timeout=5, fee=10000,
             default_precission=Decimal('0.00000001'),
             servers=None):
    """
    Exchange the entire sell needed amount, even if it means obtaining more
    than the buy expected amount in exchange.

    takes:
    buy_expected -  {
        'value:     - Decimal - min amount to buy
        'currency': - str     - currency
        'issuer':   - str     - issuer
    }
    sell_needed -  {
        'value':    - Decimal - amount to sell
        'currency': - str     - currency
        'issuer':   - str     - issuer
    }
    account            - ripple account
    secret             - ripple secret
    timeout            - trade timeout (default: 5)
    fee                - ripple fee (default: 10000)
    default_precission - compare precission(default: 0.00000001)
    servers            - ripple servers (default: none)

    returns: {
        'status':      - str    - 'success' / 'error',
        'status_msg':  - str    - 'status description'
        'sold':        - float  - the amount sold
        'bought':      - float  - the amount bought
    }

    """
    logger.info('Trading %s %s -> %s %s' %
                (sell_needed['value'], sell_needed['currency'],
                 buy_expected['value'], buy_expected['currency']))
    offer = sell_all_or_cancel(buy_expected, sell_needed, account, secret,
                               timeout=timeout, fee=fee, servers=servers)
    offer_result = get_trade_result(offer, timeout, servers)
    offer_result['sell_amount_left'] = Decimal(
        "%.12f" % float(sell_needed['value'] - offer_result['sold']))

    if offer_result['sold'] >= sell_needed['value'] - default_precission:
        logger.info("Trade fully funded")
    elif offer_result['sold']:
        logger.info("Trade partially happen")
    else:
        logger.info("Trade didn't happen")

    return offer_result

def get_trade_result(created_offer, timeout, servers=None):
    """
    Get created offer result and process it to human readable format.

    takes:
        ripple offer result returned by CreateOffer call

    returns: {
        'status':      - str   - 'success' / 'error',
        'status_msg':  - str   - 'status description'
        'sold':        - float - the amount sold
        'bought':      - float - the amount bought
    }

    """
    # check offer result
    if not created_offer or created_offer['engine_result'] != 'tesSUCCESS':

        status_msg = "Offer creation failed: %s" % \
            created_offer['engine_result'] if created_offer \
            else 'Offer was not created'
        logger.info(status_msg)
        return {'status': 'error',
                'status_msg': status_msg,
                'sold': 0,
                'bought': 0}

    logger.info("Offer created: %s" % created_offer)
    tr_hash = created_offer['tx_json']['hash']
    logger.info("Transaction: %s" % tr_hash)

    # check transaction result
    transaction = get_transaction_result(tr_hash, timeout, servers)

    # if trade didn't happen
    if 'AffectedNodes' not in transaction.get('meta', ''):
        status_msg = "Offer was not identified."
        logger.info("%s AffectedNodes weren't found." % status_msg)
        return {'status': 'error',
                'status_msg': status_msg,
                'sold': 0,
                'bought': 0}

    logger.info("Offer identified: looking for sold, received amount. \n %s" %
                transaction)
    sold, received = get_sold_received(transaction)
    logger.info("Amount sold: %s, Amount received: %s" %
                (sold, received))

    return {'status': 'success',
            'status_msg': 'Trade successfully happened',
            'sold': sold,
            'bought': received}

def get_transaction_result(tr_hash, timeout, servers, attempts=5, wait=2):
    """
    Get created offer hash and look for offer result after it's processing.

    takes:
        tr_hash  - str  - ripple transaction hash
        timeout  - int  - transaction timeout
        servers  - list - ripple servers
        attempts - int  - number of attempts to check if trade happened
        wait     - int  - seconds to wait until next check
    returns:
        transaction result if it's happened or None

    """
    transaction = {}

    # wait for ripple path find
    if not strtobool(os.environ.get("TESTING", "no")):
        time.sleep(wait)

    for i in xrange(attempts):
        transaction = tx(tr_hash, timeout=timeout, servers=servers)
        # check if offer happened
        if 'AffectedNodes' in transaction:
            break
        # wait for ripple path find a little more
        if not strtobool(os.environ.get("TESTING", "no")):
            time.sleep(wait)
    return transaction

def get_sold_received(transaction):
    """
    Calculate amount sold and amount bought from processed offer result.

    takes:
        transaction  -  dict  - ripple tx call result

    returns:
        pays_sum     -  float - amount sold
        gets_sum     -  float - amount bought

    """
    nodes = transaction['meta']['AffectedNodes']
    pays_sum = 0
    gets_sum = 0
    for n in nodes:

        if n.get('DeletedNode', ''):
            played = n['DeletedNode']
            if played.get('LedgerEntryType', '') == 'Offer':
                fields = played['PreviousFields']
                pays_curr, pays_val = restore_taker_values(fields['TakerPays'])
                gets_curr, gets_val = restore_taker_values(fields['TakerGets'])

                pays_sum += pays_val
                gets_sum += gets_val

        if n.get('ModifiedNode', ''):
            played = n['ModifiedNode']
            if played.get('LedgerEntryType', '') == 'Offer':
                prev_fields = played['PreviousFields']
                final_fields = played['FinalFields']
                pf_pays_curr, pf_pays_val = \
                    restore_taker_values(prev_fields['TakerPays'])
                pf_gets_curr, pf_gets_val = \
                    restore_taker_values(prev_fields['TakerGets'])
                ff_pays_currency, ff_pays_value = \
                    restore_taker_values(final_fields['TakerPays'])
                ff_gets_currency, ff_gets_amount = \
                    restore_taker_values(final_fields['TakerGets'])

                pays_sum += pf_pays_val - ff_pays_value
                gets_sum += pf_gets_val - ff_gets_amount

    return pays_sum, gets_sum

def restore_taker_values(taker):
    if isinstance(taker, dict):
        currency = taker['currency']
        value = Decimal(taker['value'])
    return currency, value

def sell_all_or_cancel(taker_pays, taker_gets,
                       account, secret,
                       timeout, fee=10000, servers=None):

    return create_offer(
        taker_pays, taker_gets, account, secret, timeout, fee,
        flags=0x00080000 | 0x00020000, servers=servers)

def create_offer(taker_pays, taker_gets,
                 account, secret,
                 timeout, fee=10000, flags=0, servers=None):
    """
    Create trading offer.

    takes:
        taker_pays -  {
            'value':    - float - amount to buy
            'currency': - str   - currency
            'issuer':   - str   - issuer
        }
        taker_gets -  {
            'value':    - float - amount to sell
            'currency': - str   - currency
            'issuer':   - str   - issuer
        }
        account    - ripple account
        secret     - ripple secret
        timeout    - trade timeout
        fee        - ripple fee (default: 10000)
        flags      - trade flags (default: 0)
        servers    - ripple servers (default: none)

    returns:
        offer

    taker - user, that accepts your offer

    """

    taker_pays = taker_pays.copy()
    taker_gets = taker_gets.copy()
    taker_pays['value'] = "%.12f" % taker_pays['value']
    taker_gets['value'] = "%.12f" % taker_gets['value']
    offer = {
        "method": "submit",
        "params": [{
            "secret": secret,
            "tx_json": {
                "TransactionType": "OfferCreate",
                "Fee": str(fee),
                "Flags": flags,
                "Account": account,
                "TakerPays": taker_pays,
                "TakerGets": taker_gets,
            },
        }]
    }
    logger.info('Trade offer: %s' % offer)
    return call_api(offer, timeout=timeout, servers=servers)
<|endoftext|>


<commit_before><commit_msg>cosmetics<commit_after><|endoftext|>


<commit_before># -*- coding: utf-8 -*-
"""
    nwdiag.sphinx_ext
    ~~~~~~~~~~~~~~~~~~~~

    Allow nwdiag-formatted diagrams to be included in Sphinx-generated
    documents inline.

    :copyright: Copyright 2010 by Takeshi Komiya.
    :license: BSDL.
"""

import posixpath
import os
import codecs
try:
    from hashlib import sha1 as sha
except ImportError:
    from sha import sha

from docutils import nodes
from docutils.parsers.rst import directives

from sphinx.errors import SphinxError
from sphinx.util.osutil import ensuredir, ENOENT, EPIPE
from sphinx.util.compat import Directive

from nwdiag_sphinxhelper import diagparser, builder, DiagramDraw

class NwdiagError(SphinxError):
    category = 'Nwdiag error'

class nwdiag(nodes.General, nodes.Element):
    pass

class Nwdiag(Directive):
    """
    Directive to insert arbitrary dot markup.
    """
    has_content = True
    required_arguments = 0
    optional_arguments = 1
    final_argument_whitespace = False
    option_spec = {
        'alt': directives.unchanged,
        'desctable': directives.flag,
        'maxwidth': directives.nonnegative_int,
    }

    def run(self):
        if self.arguments:
            document = self.state.document
            if self.content:
                return [document.reporter.warning(
                    'nwdiag directive cannot have both content and '
                    'a filename argument', line=self.lineno)]
            env = self.state.document.settings.env
            rel_filename, filename = relfn2path(env, self.arguments[0])
            env.note_dependency(rel_filename)
            try:
                fp = codecs.open(filename, 'r', 'utf-8')
                try:
                    dotcode = fp.read()
                finally:
                    fp.close()
            except (IOError, OSError):
                return [document.reporter.warning(
                    'External nwdiag file %r not found or reading '
                    'it failed' % filename, line=self.lineno)]
        else:
            dotcode = '\n'.join(self.content)
            if not dotcode.strip():
                return [self.state_machine.reporter.warning(
                    'Ignoring "nwdiag" directive without content.',
                    line=self.lineno)]

        node = nwdiag()
        node['code'] = dotcode
        node['options'] = {}
        if 'alt' in self.options:
            node['alt'] = self.options['alt']
        if 'maxwidth' in self.options:
            node['options']['maxwidth'] = self.options['maxwidth']
        if 'desctable' in self.options:
            node['options']['desctable'] = self.options['desctable']
        return [node]

# compatibility to sphinx 1.0 (ported from sphinx trunk)
def relfn2path(env, filename, docname=None):
    if filename.startswith('/') or filename.startswith(os.sep):
        rel_fn = filename[1:]
    else:
        docdir = os.path.dirname(env.doc2path(docname or env.docname,
                                              base=None))
        rel_fn = os.path.join(docdir, filename)
    try:
        return rel_fn, os.path.join(env.srcdir, rel_fn)
    except UnicodeDecodeError:
        # the source directory is a bytestring with non-ASCII characters;
        # let's try to encode the rel_fn in the file system encoding
        enc_rel_fn = rel_fn.encode(sys.getfilesystemencoding())
        return rel_fn, os.path.join(env.srcdir, enc_rel_fn)

def get_image_filename(self, code, format, options, prefix='nwdiag'):
    """
    Get path of output file.
    """
    if format not in ('PNG', 'PDF'):
        raise NwdiagError('nwdiag error:\nunknown format: %s\n' % format)

    if format == 'PDF':
        try:
            import reportlab
        except ImportError:
            msg = 'nwdiag error:\n' + \
                  'colud not output PDF format; Install reportlab\n'
            raise NwdiagError(msg)

    hashkey = code.encode('utf-8') + str(options)
    fname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), format.lower())
    if hasattr(self.builder, 'imgpath'):
        # HTML
        relfn = posixpath.join(self.builder.imgpath, fname)
        outfn = os.path.join(self.builder.outdir, '_images', fname)
    else:
        # LaTeX
        relfn = fname
        outfn = os.path.join(self.builder.outdir, fname)

    if os.path.isfile(outfn):
        return relfn, outfn

    ensuredir(os.path.dirname(outfn))

    return relfn, outfn

def get_fontpath(self):
    fontpath = None
    if self.builder.config.nwdiag_fontpath:
        nwdiag_fontpath = self.builder.config.nwdiag_fontpath

        if isinstance(nwdiag_fontpath, (str, unicode)):
            nwdiag_fontpath = [nwdiag_fontpath]

        for path in nwdiag_fontpath:
            if os.path.isfile(path):
                fontpath = path

        if fontpath is None:
            attrname = '_nwdiag_fontpath_warned'
            if not hasattr(self.builder, attrname):
                msg = ('nwdiag cannot load "%s" as truetype font, '
                       'check the nwdiag_path setting' % fontpath)
                self.builder.warn(msg)

                setattr(self.builder, attrname, True)

    return fontpath

def create_nwdiag(self, code, format, filename, options, prefix='nwdiag'):
    """
    Render nwdiag code into a PNG output file.
    """
    draw = None
    fontpath = get_fontpath(self)
    try:
        tree = diagparser.parse(diagparser.tokenize(code))
        screen = builder.ScreenNodeBuilder.build(tree)

        antialias = self.builder.config.nwdiag_antialias
        draw = DiagramDraw.DiagramDraw(format, screen, filename, font=fontpath,
                                       antialias=antialias)
    except Exception, e:
        raise NwdiagError('nwdiag error:\n%s\n' % e)

    return draw

def render_dot_html(self, node, code, options, prefix='nwdiag',
                    imgcls=None, alt=None):
    has_thumbnail = False
    try:
        format = 'PNG'
        relfn, outfn = get_image_filename(self, code, format, options, prefix)

        image = create_nwdiag(self, code, format, outfn, options, prefix)
        if not os.path.isfile(outfn):
            image.draw()
            image.save()

        # generate description table
        descriptions = []
        if 'desctable' in options:
            for n in image.diagram.nodes:
                if n.description:
                    descriptions.append((n.id, n.numbered, n.description))

        # generate thumbnails
        image_size = image.drawer.image.size
        if 'maxwidth' in options and options['maxwidth'] < image_size[0]:
            has_thumbnail = True
            thumb_prefix = prefix + '_thumb'
            trelfn, toutfn = get_image_filename(self, code, format,
                                                options, thumb_prefix)

            thumb_size = (options['maxwidth'], image_size[1])
            if not os.path.isfile(toutfn):
                image.draw()
                image.save(toutfn, thumb_size)
            thumb_size = image.drawer.image.size

    except NwdiagError, exc:
        self.builder.warn('dot code %r: ' % code + str(exc))
        raise nodes.SkipNode

    self.body.append(self.starttag(node, 'p', CLASS='nwdiag'))
    if relfn is None:
        self.body.append(self.encode(code))
    else:
        if alt is None:
            alt = node.get('alt', self.encode(code).strip())

        imgtag_format = '\n'
        if has_thumbnail:
            self.body.append('' % relfn)
            self.body.append(imgtag_format %
                             (trelfn, alt, thumb_size[0], thumb_size[1]))
            self.body.append('')
        else:
            self.body.append(imgtag_format %
                             (relfn, alt, image_size[0], image_size[1]))

    if descriptions:
        numbered = [x for x in descriptions if x[1]]

        self.body.append('
')
        self.body.append('
')
        if numbered:
            self.body.append('
No	Name	Description
')
        else:
            self.body.append('
Name	Description
')
        self.body.append('
')
        self.body.append('
')

        if numbered:
            def cmp_number(a, b):
                if a[1]:
                    n1 = int(a[1])
                else:
                    n1 = 0

                if b[1]:
                    n2 = int(b[1])
                else:
                    n2 = 0

                return cmp(n1, n2)

            descriptions.sort(cmp_number)

        for desc in descriptions:
            id, number, text = desc
            self.body.append('
')
            if numbered:
                if number is not None:
                    self.body.append('%s	' % number)
                else:
                    self.body.append('	')
            self.body.append('%s	' % id)
            self.body.append('%s	' % text)
            self.body.append('
')

        self.body.append('
')
        self.body.append('
')

    self.body.append('
\n')
    raise nodes.SkipNode

def html_visit_nwdiag(self, node):
    render_dot_html(self, node, node['code'], node['options'])

def render_dot_latex(self, node, code, options, prefix='nwdiag'):
    try:
        format = self.builder.config.nwdiag_tex_image_format
        fname, outfn = get_image_filename(self, code, format, options, prefix)

        image = create_nwdiag(self, code, format, outfn, options, prefix)
        if not os.path.isfile(outfn):
            image.draw()
            image.save()

    except NwdiagError, exc:
        self.builder.warn('dot code %r: ' % code + str(exc))
        raise nodes.SkipNode

    if fname is not None:
        self.body.append('\\par\\includegraphics{%s}\\par' % fname)
    raise nodes.SkipNode

def latex_visit_nwdiag(self, node):
    render_dot_latex(self, node, node['code'], node['options'])

def setup(app):
    app.add_node(nwdiag,
                 html=(html_visit_nwdiag, None),
                 latex=(latex_visit_nwdiag, None))
    app.add_directive('nwdiag', Nwdiag)
    app.add_config_value('nwdiag_fontpath', None, 'html')
    app.add_config_value('nwdiag_antialias', False, 'html')
    app.add_config_value('nwdiag_tex_image_format', 'PNG', 'html')
<commit_msg> * Display node.address on desctable<commit_after># -*- coding: utf-8 -*-
"""
    nwdiag.sphinx_ext
    ~~~~~~~~~~~~~~~~~~~~

    Allow nwdiag-formatted diagrams to be included in Sphinx-generated
    documents inline.

    :copyright: Copyright 2010 by Takeshi Komiya.
    :license: BSDL.
"""

import posixpath
import os
import codecs
try:
    from hashlib import sha1 as sha
except ImportError:
    from sha import sha

from docutils import nodes
from docutils.parsers.rst import directives

from sphinx.errors import SphinxError
from sphinx.util.osutil import ensuredir, ENOENT, EPIPE
from sphinx.util.compat import Directive

from nwdiag_sphinxhelper import diagparser, builder, DiagramDraw

class NwdiagError(SphinxError):
    category = 'Nwdiag error'

class nwdiag(nodes.General, nodes.Element):
    pass

class Nwdiag(Directive):
    """
    Directive to insert arbitrary dot markup.
    """
    has_content = True
    required_arguments = 0
    optional_arguments = 1
    final_argument_whitespace = False
    option_spec = {
        'alt': directives.unchanged,
        'desctable': directives.flag,
        'maxwidth': directives.nonnegative_int,
    }

    def run(self):
        if self.arguments:
            document = self.state.document
            if self.content:
                return [document.reporter.warning(
                    'nwdiag directive cannot have both content and '
                    'a filename argument', line=self.lineno)]
            env = self.state.document.settings.env
            rel_filename, filename = relfn2path(env, self.arguments[0])
            env.note_dependency(rel_filename)
            try:
                fp = codecs.open(filename, 'r', 'utf-8')
                try:
                    dotcode = fp.read()
                finally:
                    fp.close()
            except (IOError, OSError):
                return [document.reporter.warning(
                    'External nwdiag file %r not found or reading '
                    'it failed' % filename, line=self.lineno)]
        else:
            dotcode = '\n'.join(self.content)
            if not dotcode.strip():
                return [self.state_machine.reporter.warning(
                    'Ignoring "nwdiag" directive without content.',
                    line=self.lineno)]

        node = nwdiag()
        node['code'] = dotcode
        node['options'] = {}
        if 'alt' in self.options:
            node['alt'] = self.options['alt']
        if 'maxwidth' in self.options:
            node['options']['maxwidth'] = self.options['maxwidth']
        if 'desctable' in self.options:
            node['options']['desctable'] = self.options['desctable']
        return [node]

# compatibility to sphinx 1.0 (ported from sphinx trunk)
def relfn2path(env, filename, docname=None):
    if filename.startswith('/') or filename.startswith(os.sep):
        rel_fn = filename[1:]
    else:
        docdir = os.path.dirname(env.doc2path(docname or env.docname,
                                              base=None))
        rel_fn = os.path.join(docdir, filename)
    try:
        return rel_fn, os.path.join(env.srcdir, rel_fn)
    except UnicodeDecodeError:
        # the source directory is a bytestring with non-ASCII characters;
        # let's try to encode the rel_fn in the file system encoding
        enc_rel_fn = rel_fn.encode(sys.getfilesystemencoding())
        return rel_fn, os.path.join(env.srcdir, enc_rel_fn)

def get_image_filename(self, code, format, options, prefix='nwdiag'):
    """
    Get path of output file.
    """
    if format not in ('PNG', 'PDF'):
        raise NwdiagError('nwdiag error:\nunknown format: %s\n' % format)

    if format == 'PDF':
        try:
            import reportlab
        except ImportError:
            msg = 'nwdiag error:\n' + \
                  'colud not output PDF format; Install reportlab\n'
            raise NwdiagError(msg)

    hashkey = code.encode('utf-8') + str(options)
    fname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), format.lower())
    if hasattr(self.builder, 'imgpath'):
        # HTML
        relfn = posixpath.join(self.builder.imgpath, fname)
        outfn = os.path.join(self.builder.outdir, '_images', fname)
    else:
        # LaTeX
        relfn = fname
        outfn = os.path.join(self.builder.outdir, fname)

    if os.path.isfile(outfn):
        return relfn, outfn

    ensuredir(os.path.dirname(outfn))

    return relfn, outfn

def get_fontpath(self):
    fontpath = None
    if self.builder.config.nwdiag_fontpath:
        nwdiag_fontpath = self.builder.config.nwdiag_fontpath

        if isinstance(nwdiag_fontpath, (str, unicode)):
            nwdiag_fontpath = [nwdiag_fontpath]

        for path in nwdiag_fontpath:
            if os.path.isfile(path):
                fontpath = path

        if fontpath is None:
            attrname = '_nwdiag_fontpath_warned'
            if not hasattr(self.builder, attrname):
                msg = ('nwdiag cannot load "%s" as truetype font, '
                       'check the nwdiag_path setting' % fontpath)
                self.builder.warn(msg)

                setattr(self.builder, attrname, True)

    return fontpath

def create_nwdiag(self, code, format, filename, options, prefix='nwdiag'):
    """
    Render nwdiag code into a PNG output file.
    """
    draw = None
    fontpath = get_fontpath(self)
    try:
        tree = diagparser.parse(diagparser.tokenize(code))
        screen = builder.ScreenNodeBuilder.build(tree)

        antialias = self.builder.config.nwdiag_antialias
        draw = DiagramDraw.DiagramDraw(format, screen, filename, font=fontpath,
                                       antialias=antialias)
    except Exception, e:
        raise NwdiagError('nwdiag error:\n%s\n' % e)

    return draw

def render_dot_html(self, node, code, options, prefix='nwdiag',
                    imgcls=None, alt=None):
    has_thumbnail = False
    try:
        format = 'PNG'
        relfn, outfn = get_image_filename(self, code, format, options, prefix)

        image = create_nwdiag(self, code, format, outfn, options, prefix)
        if not os.path.isfile(outfn):
            image.draw()
            image.save()

        # generate description table
        descriptions = []
        if 'desctable' in options:
            for n in image.diagram.nodes:
                if n.description:
                    descriptions.append((n.id, n.numbered, n.address.values(), n.description))

        # generate thumbnails
        image_size = image.drawer.image.size
        if 'maxwidth' in options and options['maxwidth'] < image_size[0]:
            has_thumbnail = True
            thumb_prefix = prefix + '_thumb'
            trelfn, toutfn = get_image_filename(self, code, format,
                                                options, thumb_prefix)

            thumb_size = (options['maxwidth'], image_size[1])
            if not os.path.isfile(toutfn):
                image.draw()
                image.save(toutfn, thumb_size)
            thumb_size = image.drawer.image.size

    except NwdiagError, exc:
        self.builder.warn('dot code %r: ' % code + str(exc))
        raise nodes.SkipNode

    self.body.append(self.starttag(node, 'p', CLASS='nwdiag'))
    if relfn is None:
        self.body.append(self.encode(code))
    else:
        if alt is None:
            alt = node.get('alt', self.encode(code).strip())

        imgtag_format = '\n'
        if has_thumbnail:
            self.body.append('' % relfn)
            self.body.append(imgtag_format %
                             (trelfn, alt, thumb_size[0], thumb_size[1]))
            self.body.append('')
        else:
            self.body.append(imgtag_format %
                             (relfn, alt, image_size[0], image_size[1]))

    if descriptions:
        numbered = [x for x in descriptions if x[1]]

        self.body.append('
')
        self.body.append('
')
        if numbered:
            self.body.append('
No	Name	Address	Description
')
        else:
            self.body.append('
Name	Address	Description
')
        self.body.append('
')
        self.body.append('
')

        if numbered:
            def cmp_number(a, b):
                if a[1]:
                    n1 = int(a[1])
                else:
                    n1 = 0

                if b[1]:
                    n2 = int(b[1])
                else:
                    n2 = 0

                return cmp(n1, n2)

            descriptions.sort(cmp_number)

        for desc in descriptions:
            id, number, address, text = desc
            self.body.append('
')
            if numbered:
                if number is not None:
                    self.body.append('%s	' % number)
                else:
                    self.body.append('	')
            self.body.append('%s	' % id)
            self.body.append('%s	' % "
".join(address))
            self.body.append('%s	' % text)
            self.body.append('
')

        self.body.append('
')
        self.body.append('
')

    self.body.append('
\n')
    raise nodes.SkipNode

def html_visit_nwdiag(self, node):
    render_dot_html(self, node, node['code'], node['options'])

def render_dot_latex(self, node, code, options, prefix='nwdiag'):
    try:
        format = self.builder.config.nwdiag_tex_image_format
        fname, outfn = get_image_filename(self, code, format, options, prefix)

        image = create_nwdiag(self, code, format, outfn, options, prefix)
        if not os.path.isfile(outfn):
            image.draw()
            image.save()

    except NwdiagError, exc:
        self.builder.warn('dot code %r: ' % code + str(exc))
        raise nodes.SkipNode

    if fname is not None:
        self.body.append('\\par\\includegraphics{%s}\\par' % fname)
    raise nodes.SkipNode

def latex_visit_nwdiag(self, node):
    render_dot_latex(self, node, node['code'], node['options'])

def setup(app):
    app.add_node(nwdiag,
                 html=(html_visit_nwdiag, None),
                 latex=(latex_visit_nwdiag, None))
    app.add_directive('nwdiag', Nwdiag)
    app.add_config_value('nwdiag_fontpath', None, 'html')
    app.add_config_value('nwdiag_antialias', False, 'html')
    app.add_config_value('nwdiag_tex_image_format', 'PNG', 'html')
<|endoftext|>


<commit_before>from biothings.settings import BiothingSettings
import requests

biothing_settings = BiothingSettings()

class Neo4jQuery(object):
    def __init__(self):
        pass

    def query(q, **kwargs):
        # very simple query wrapper
        res = requests.post(biothing_settings.neo4j_host,
                            auth=(biothing_settings.neo4j_username, biothing_settings.neo4j_password),
                            data = {"query": q, "params": {}})
        if isinstance(res, requests.models.Response) and res.status_code == 200:
            return rrr.json()
        else:
            return {'success': False, 'message': 'Invalid query.'}
<commit_msg>Cypher query works now on biothings.<commit_after>from biothings.settings import BiothingSettings
import requests

biothing_settings = BiothingSettings()

class Neo4jQuery(object):
    def __init__(self):
        pass

    def query(self, q, **kwargs):
        # very simple query wrapper
        res = requests.post(biothing_settings.neo4j_host,
                            auth=(biothing_settings.neo4j_username, biothing_settings.neo4j_password),
                            data = {"query": q, "params": {}})
        if isinstance(res, requests.models.Response) and res.status_code == 200:
            return res.json()
        else:
            return {'success': False, 'message': 'Invalid query.'}
<|endoftext|>