<commit_before>import logging
import time
import math

import tabulate

from parsl.executors.ipp import IPyParallelExecutor

logger = logging.getLogger(__name__)

class Strategy(object):
    """FlowControl strategy.

    As a workflow dag is processed by Parsl, new tasks are added and completed
    asynchronously. Parsl interfaces executors with execution providers to construct
    scalable executors to handle the variable work-load generated by the
    workflow. This component is responsible for periodically checking outstanding
    tasks and available compute capacity and trigger scaling events to match
    workflow needs.

    Here's a diagram of an executor. An executor consists of blocks, which are usually
    created by single requests to a Local Resource Manager (LRM) such as slurm,
    condor, torque, or even AWS API. The blocks could contain several task blocks
    which are separate instances on workers.

    .. code:: python

                |<--min_blocks     |<-init_blocks              max_blocks-->|
                +----------------------------------------------------------+
                |  +--------block----------+       +--------block--------+ |
     executor = |  | task          task    | ...   |    task      task   | |
                |  +-----------------------+       +---------------------+ |
                +----------------------------------------------------------+

    The relevant specification options are:
       1. min_blocks: Minimum number of blocks to maintain
       2. init_blocks: number of blocks to provision at initialization of workflow
       3. max_blocks: Maximum number of blocks that can be active due to one workflow

    .. code:: python

          slots = current_capacity * tasks_per_node * nodes_per_block

          active_tasks = pending_tasks + running_tasks

          Parallelism = slots / tasks
                      = [0, 1] (i.e,  0 <= p <= 1)

    For example:

    When p = 0,
         => compute with the least resources possible.
         infinite tasks are stacked per slot.

         .. code:: python

               blocks =  min_blocks           { if active_tasks = 0
                         max(min_blocks, 1)   {  else

    When p = 1,
         => compute with the most resources.
         one task is stacked per slot.

         .. code:: python

               blocks = min ( max_blocks,
                        ceil( active_tasks / slots ) )

    When p = 1/2,
         => We stack upto 2 tasks per slot before we overflow
         and request a new block

    let's say min:init:max = 0:0:4 and task_blocks=2
    Consider the following example:
    min_blocks = 0
    init_blocks = 0
    max_blocks = 4
    tasks_per_node = 2
    nodes_per_block = 1

    In the diagram, X <- task

    at 2 tasks:

    .. code:: python

        +---Block---|
        |           |
        | X      X  |
        |slot   slot|
        +-----------+

    at 5 tasks, we overflow as the capacity of a single block is fully used.

    .. code:: python

        +---Block---|       +---Block---|
        | X      X  | ----> |           |
        | X      X  |       | X         |
        |slot   slot|       |slot   slot|
        +-----------+       +-----------+

    """

    def __init__(self, dfk):
        """Initialize strategy."""
        self.dfk = dfk
        self.config = dfk.config
        self.executors = {}
        self.max_idletime = 60 * 2  # 2 minutes

        for e in self.dfk.config.executors:
            self.executors[e.label] = {'idle_since': None, 'config': e.label}

        self.strategies = {None: self._strategy_noop, 'simple': self._strategy_simple}

        self.strategize = self.strategies[self.config.strategy]
        self.logger_flag = False
        self.prior_loghandlers = set(logging.getLogger().handlers)

        logger.debug("Scaling strategy: {0}".format(self.config.strategy))

    def _strategy_noop(self, tasks, *args, kind=None, **kwargs):
        """Do nothing.

        Args:
            - tasks (task_ids): Not used here.

        KWargs:
            - kind (Not used)
        """

    def unset_logging(self):
        """ Mute newly added handlers to the root level, right after calling executor.status
        """
        if self.logger_flag is True:
            return

        root_logger = logging.getLogger()

        for hndlr in root_logger.handlers:
            if hndlr not in self.prior_loghandlers:
                hndlr.setLevel(logging.ERROR)

        self.logger_flag = True

    def _strategy_simple(self, tasks, *args, kind=None, **kwargs):
        """Peek at the DFK and the executors specified.

        We assume here that tasks are not held in a runnable
        state, and that all tasks from an app would be sent to
        a single specific executor, i.e tasks cannot be specified
        to go to one of more executors.

        Args:
            - tasks (task_ids): Not used here.

        KWargs:
            - kind (Not used)
        """

        headers = ['executor', 'active tasks', 'running blocks', 'submitting blocks', 'pending blocks', 'connected engines']
        data = []
        for label, executor in self.dfk.executors.items():
            if not executor.scaling_enabled:
                continue

            # Tasks that are either pending completion
            active_tasks = executor.executor.outstanding

            status = executor.status()
            self.unset_logging()

            # FIXME we need to handle case where provider does not define these
            # FIXME probably more of this logic should be moved to the provider
            min_blocks = executor.provider.min_blocks
            max_blocks = executor.provider.max_blocks
            tasks_per_node = executor.provider.tasks_per_node
            nodes_per_block = executor.provider.nodes_per_block
            parallelism = executor.provider.parallelism

            running = sum([1 for x in status if x == 'RUNNING'])
            submitting = sum([1 for x in status if x == 'SUBMITTING'])
            pending = sum([1 for x in status if x == 'PENDING'])
            active_blocks = running + submitting + pending
            active_slots = active_blocks * tasks_per_node * nodes_per_block

            data.append([
                label,
                len(active_tasks),
                running,
                submitting,
                pending,
                len(executor.executor) if isinstance(executor, IPyParallelExecutor) else 'N/A'
            ])

            # Case 1
            # No tasks.
            if len(active_tasks) == 0:
                # Case 1a
                # Fewer blocks that min_blocks
                if active_blocks <= min_blocks:
                    # Ignore
                    # logger.debug("Strategy: Case.1a")
                    pass

                # Case 1b
                # More blocks than min_blocks. Scale down
                else:
                    # We want to make sure that max_idletime is reached
                    # before killing off resources
                    if not self.executors[executor.label]['idle_since']:
                        logger.debug("Strategy: Scale_in, tasks=0 starting kill timer")
                        self.executors[executor.label]['idle_since'] = time.time()

                    idle_since = self.executors[executor.label]['idle_since']
                    if (time.time() - idle_since) > self.max_idletime:
                        # We have resources idle for the max duration,
                        # we have to scale_in now.
                        logger.debug("Strategy: Scale_in, tasks=0")
                        executor.scale_in(active_blocks - min_blocks)

                    else:
                        pass
                        # logger.debug("Strategy: Case.1b. Waiting for timer : {0}".format(idle_since))

            # Case 2
            # More tasks than the available slots.
            elif (float(active_slots) / len(active_tasks)) < parallelism:
                # Case 2a
                # We have the max blocks possible
                if active_blocks >= max_blocks:
                    # Ignore since we already have the max nodes
                    # logger.debug("Strategy: Case.2a")
                    pass

                # Case 2b
                else:
                    # logger.debug("Strategy: Case.2b")
                    excess = math.ceil((len(active_tasks) * parallelism) - active_slots)
                    excess_blocks = math.ceil(float(excess) / (tasks_per_node * nodes_per_block))
                    logger.debug("Requesting {} more blocks".format(excess_blocks))
                    executor.scale_out(excess_blocks)

            elif active_slots == 0 and len(active_tasks) > 0:
                # Case 4
                # Check if slots are being lost quickly ?
                logger.debug("Requesting single slot")
                executor.scale_out(1)
            # Case 3
            # tasks ~ slots
            else:
                # logger.debug("Strategy: Case 3")
                pass
        logger.debug('\n' + tabulate.tabulate(data, headers=headers))

if __name__ == '__main__':

    pass
<commit_msg>Remove table in logging<commit_after>import logging
import time
import math

from parsl.executors.ipp import IPyParallelExecutor

logger = logging.getLogger(__name__)

class Strategy(object):
    """FlowControl strategy.

    As a workflow dag is processed by Parsl, new tasks are added and completed
    asynchronously. Parsl interfaces executors with execution providers to construct
    scalable executors to handle the variable work-load generated by the
    workflow. This component is responsible for periodically checking outstanding
    tasks and available compute capacity and trigger scaling events to match
    workflow needs.

    Here's a diagram of an executor. An executor consists of blocks, which are usually
    created by single requests to a Local Resource Manager (LRM) such as slurm,
    condor, torque, or even AWS API. The blocks could contain several task blocks
    which are separate instances on workers.

    .. code:: python

                |<--min_blocks     |<-init_blocks              max_blocks-->|
                +----------------------------------------------------------+
                |  +--------block----------+       +--------block--------+ |
     executor = |  | task          task    | ...   |    task      task   | |
                |  +-----------------------+       +---------------------+ |
                +----------------------------------------------------------+

    The relevant specification options are:
       1. min_blocks: Minimum number of blocks to maintain
       2. init_blocks: number of blocks to provision at initialization of workflow
       3. max_blocks: Maximum number of blocks that can be active due to one workflow

    .. code:: python

          slots = current_capacity * tasks_per_node * nodes_per_block

          active_tasks = pending_tasks + running_tasks

          Parallelism = slots / tasks
                      = [0, 1] (i.e,  0 <= p <= 1)

    For example:

    When p = 0,
         => compute with the least resources possible.
         infinite tasks are stacked per slot.

         .. code:: python

               blocks =  min_blocks           { if active_tasks = 0
                         max(min_blocks, 1)   {  else

    When p = 1,
         => compute with the most resources.
         one task is stacked per slot.

         .. code:: python

               blocks = min ( max_blocks,
                        ceil( active_tasks / slots ) )

    When p = 1/2,
         => We stack upto 2 tasks per slot before we overflow
         and request a new block

    let's say min:init:max = 0:0:4 and task_blocks=2
    Consider the following example:
    min_blocks = 0
    init_blocks = 0
    max_blocks = 4
    tasks_per_node = 2
    nodes_per_block = 1

    In the diagram, X <- task

    at 2 tasks:

    .. code:: python

        +---Block---|
        |           |
        | X      X  |
        |slot   slot|
        +-----------+

    at 5 tasks, we overflow as the capacity of a single block is fully used.

    .. code:: python

        +---Block---|       +---Block---|
        | X      X  | ----> |           |
        | X      X  |       | X         |
        |slot   slot|       |slot   slot|
        +-----------+       +-----------+

    """

    def __init__(self, dfk):
        """Initialize strategy."""
        self.dfk = dfk
        self.config = dfk.config
        self.executors = {}
        self.max_idletime = 60 * 2  # 2 minutes

        for e in self.dfk.config.executors:
            self.executors[e.label] = {'idle_since': None, 'config': e.label}

        self.strategies = {None: self._strategy_noop, 'simple': self._strategy_simple}

        self.strategize = self.strategies[self.config.strategy]
        self.logger_flag = False
        self.prior_loghandlers = set(logging.getLogger().handlers)

        logger.debug("Scaling strategy: {0}".format(self.config.strategy))

    def _strategy_noop(self, tasks, *args, kind=None, **kwargs):
        """Do nothing.

        Args:
            - tasks (task_ids): Not used here.

        KWargs:
            - kind (Not used)
        """

    def unset_logging(self):
        """ Mute newly added handlers to the root level, right after calling executor.status
        """
        if self.logger_flag is True:
            return

        root_logger = logging.getLogger()

        for hndlr in root_logger.handlers:
            if hndlr not in self.prior_loghandlers:
                hndlr.setLevel(logging.ERROR)

        self.logger_flag = True

    def _strategy_simple(self, tasks, *args, kind=None, **kwargs):
        """Peek at the DFK and the executors specified.

        We assume here that tasks are not held in a runnable
        state, and that all tasks from an app would be sent to
        a single specific executor, i.e tasks cannot be specified
        to go to one of more executors.

        Args:
            - tasks (task_ids): Not used here.

        KWargs:
            - kind (Not used)
        """

        for label, executor in self.dfk.executors.items():
            if not executor.scaling_enabled:
                continue

            # Tasks that are either pending completion
            active_tasks = executor.executor.outstanding

            status = executor.status()
            self.unset_logging()

            # FIXME we need to handle case where provider does not define these
            # FIXME probably more of this logic should be moved to the provider
            min_blocks = executor.provider.min_blocks
            max_blocks = executor.provider.max_blocks
            tasks_per_node = executor.provider.tasks_per_node
            nodes_per_block = executor.provider.nodes_per_block
            parallelism = executor.provider.parallelism

            running = sum([1 for x in status if x == 'RUNNING'])
            submitting = sum([1 for x in status if x == 'SUBMITTING'])
            pending = sum([1 for x in status if x == 'PENDING'])
            active_blocks = running + submitting + pending
            active_slots = active_blocks * tasks_per_node * nodes_per_block

            if isinstance(executor, IPyParallelExecutor):
                logger.debug('Executor {} has {} active tasks, {}/{}/{} running/submitted/pending blocks, and {} connected engines'.format(
                    label, len(active_tasks), running, submitting, pending, len(executor.executor)))
            else:
                logger.debug('Executor {} has {} active tasks and {}/{}/{} running/submitted/pending blocks'.format(
                    label, len(active_tasks), running, submitting, pending))

            # Case 1
            # No tasks.
            if len(active_tasks) == 0:
                # Case 1a
                # Fewer blocks that min_blocks
                if active_blocks <= min_blocks:
                    # Ignore
                    # logger.debug("Strategy: Case.1a")
                    pass

                # Case 1b
                # More blocks than min_blocks. Scale down
                else:
                    # We want to make sure that max_idletime is reached
                    # before killing off resources
                    if not self.executors[executor.label]['idle_since']:
                        logger.debug("Strategy: Scale_in, tasks=0 starting kill timer")
                        self.executors[executor.label]['idle_since'] = time.time()

                    idle_since = self.executors[executor.label]['idle_since']
                    if (time.time() - idle_since) > self.max_idletime:
                        # We have resources idle for the max duration,
                        # we have to scale_in now.
                        logger.debug("Strategy: Scale_in, tasks=0")
                        executor.scale_in(active_blocks - min_blocks)

                    else:
                        pass
                        # logger.debug("Strategy: Case.1b. Waiting for timer : {0}".format(idle_since))

            # Case 2
            # More tasks than the available slots.
            elif (float(active_slots) / len(active_tasks)) < parallelism:
                # Case 2a
                # We have the max blocks possible
                if active_blocks >= max_blocks:
                    # Ignore since we already have the max nodes
                    # logger.debug("Strategy: Case.2a")
                    pass

                # Case 2b
                else:
                    # logger.debug("Strategy: Case.2b")
                    excess = math.ceil((len(active_tasks) * parallelism) - active_slots)
                    excess_blocks = math.ceil(float(excess) / (tasks_per_node * nodes_per_block))
                    logger.debug("Requesting {} more blocks".format(excess_blocks))
                    executor.scale_out(excess_blocks)

            elif active_slots == 0 and len(active_tasks) > 0:
                # Case 4
                # Check if slots are being lost quickly ?
                logger.debug("Requesting single slot")
                executor.scale_out(1)
            # Case 3
            # tasks ~ slots
            else:
                # logger.debug("Strategy: Case 3")
                pass

if __name__ == '__main__':

    pass
<|endoftext|>


<commit_before>import logging

from pajbot.apiwrappers.twitch.base import BaseTwitchApi

log = logging.getLogger(__name__)

class TwitchKrakenV5Api(BaseTwitchApi):
    authorization_header_prefix = "OAuth"

    def __init__(self, client_credentials, redis):
        super().__init__(base_url="https://api.twitch.tv/kraken/", redis=redis)
        self.session.headers["Accept"] = "application/vnd.twitchtv.v5+json"
        self.client_credentials = client_credentials

    @property
    def default_authorization(self):
        return self.client_credentials

    def get_stream_status(self, user_id):
        data = self.get(["streams", user_id])

        def rest_data_offline():
            return {
                "viewers": -1,
                "game": None,
                "title": None,
                "created_at": None,
                "followers": -1,
                "views": -1,
                "broadcast_id": None,
            }

        def rest_data_online():
            stream = data["stream"]

            return {
                "viewers": stream["viewers"],
                "game": stream["game"],
                "title": stream["channel"]["status"],
                "created_at": stream["created_at"],
                "followers": stream["channel"]["followers"],
                "views": stream["channel"]["views"],
                "broadcast_id": stream["_id"],
            }

        online = "stream" in data and data["stream"] is not None

        def rest_data():
            nonlocal online
            if online:
                return rest_data_online()
            else:
                return rest_data_offline()

        return {"online": online, **rest_data()}

    def set_game(self, user_id, game, authorization):
        self.put(["channels", user_id], json={"channel": {"game": game}}, authorization=authorization)

    def set_title(self, user_id, title, authorization):
        self.put(["channels", user_id], json={"channel": {"status": title}}, authorization=authorization)

    def get_vod_videos(self, channel_name):
        return self.get(["channels", channel_name, "videos"], {"broadcast_type": "archive"})

    def get_global_emotes(self):
        # circular import prevention
        from pajbot.managers.emote import EmoteManager

        resp = self.get("chat/emoticon_images", params={"emotesets": "0"})
        return [EmoteManager.twitch_emote(data["id"], data["code"]) for data in resp["emoticon_sets"]["0"]]
<commit_msg>Small consistency fix in Kraken API wrapper<commit_after>import logging

from pajbot.apiwrappers.twitch.base import BaseTwitchApi

log = logging.getLogger(__name__)

class TwitchKrakenV5Api(BaseTwitchApi):
    authorization_header_prefix = "OAuth"

    def __init__(self, client_credentials, redis):
        super().__init__(base_url="https://api.twitch.tv/kraken/", redis=redis)
        self.session.headers["Accept"] = "application/vnd.twitchtv.v5+json"
        self.client_credentials = client_credentials

    @property
    def default_authorization(self):
        return self.client_credentials

    def get_stream_status(self, user_id):
        data = self.get(["streams", user_id])

        def rest_data_offline():
            return {
                "viewers": -1,
                "game": None,
                "title": None,
                "created_at": None,
                "followers": -1,
                "views": -1,
                "broadcast_id": None,
            }

        def rest_data_online():
            stream = data["stream"]

            return {
                "viewers": stream["viewers"],
                "game": stream["game"],
                "title": stream["channel"]["status"],
                "created_at": stream["created_at"],
                "followers": stream["channel"]["followers"],
                "views": stream["channel"]["views"],
                "broadcast_id": stream["_id"],
            }

        online = "stream" in data and data["stream"] is not None

        def rest_data():
            nonlocal online
            if online:
                return rest_data_online()
            else:
                return rest_data_offline()

        return {"online": online, **rest_data()}

    def set_game(self, user_id, game, authorization):
        self.put(["channels", user_id], json={"channel": {"game": game}}, authorization=authorization)

    def set_title(self, user_id, title, authorization):
        self.put(["channels", user_id], json={"channel": {"status": title}}, authorization=authorization)

    def get_vod_videos(self, channel_name):
        return self.get(["channels", channel_name, "videos"], {"broadcast_type": "archive"})

    def get_global_emotes(self):
        # circular import prevention
        from pajbot.managers.emote import EmoteManager

        resp = self.get("/chat/emoticon_images", params={"emotesets": "0"})
        return [EmoteManager.twitch_emote(data["id"], data["code"]) for data in resp["emoticon_sets"]["0"]]
<|endoftext|>


<commit_before>#!/usr/bin/python3

#A simple script to assemble Small8 ASM files into MIF binary files

#The Small8 CPU is a two-register multicycle microprocessor core consisting of an accumulator
#	and a data register as primary components. It contains an 8-bit data bus with 16-bit addressing.
#	This core for which this program assembles is designed to minimize the number
#	of discreet components to fit on small FPGAs. This simple processor does not
#	provide interrupts by design, as these would be added into the HDL and memory-mapped
#	for the desired application. The architecture is small and intuitive, but not
#	necessarily the most efficient in other aspects.

INS_SET = {'LDAI':(2,'84'), #Name, length, opcode
	'LDAA':(3,'88'), #Load A Addr
	'LDAD':(1,'81'), #A<-D
	'STAA':(3,'F6'), #Store A Addr
	'STAR':(1,'F1'), #D<-A
	'ADCR':(1,'01'), #Add with carry, store in A
	'SBCR':(1,'11'), #Subtract with borrow
	'CMPR':(1,'91'), #Compare
	'ANDR':(1,'21'), #AND
	'ORR':(1,'31'), #OR
	'XORR':(1,'41'), #XOR
	'SLRL':(1,'51'), #Shift left logical
	'SRRL':(1,'61'), #Shift right logical
	'ROLC':(1,'52'), #Rotate left through carry
	'RORC':(1,'62'), #Rotate right through carry
	'BCCA':(3,'B0'), #Branch on NOT C (SREG=CVSZ,Carry,oVerflow,Sign,Zero)
	'BCSA':(3,'B1'), #Branch on C
	'BEQA':(3,'B2'), #Branch on Z
	'BMIA':(3,'B3'), #Branch on negative
	'BNEA':(3,'B4'), #Branch on NOT Z
	'BPLA':(3,'B5'), #Branch on positive
	'BVCA':(3,'B6'), #Branch on no overflow
	'BVSA':(3,'B7'), #Branch on overflow
	'DECA':(1,'FB'), #Decrement A
	'INCA':(1,'FA'), #Increment A
	'SETC':(1,'F8'), #Set carry flag
	'CLRC':(1,'F9'), #Clear carry flag
<commit_msg>Completed instruction set definition<commit_after>#!/usr/bin/python3

#A simple script to assemble Small8 ASM files into MIF binary files

#The Small8 CPU is a two-register multicycle microprocessor core consisting of an accumulator
#	and a data register as primary components. It contains an 8-bit data bus with 16-bit addressing.
#	This core for which this program assembles is designed to minimize the number
#	of discreet components to fit on small FPGAs. This simple processor does not
#	provide interrupts by design, as these would be added into the HDL and memory-mapped
#	for the desired application. The architecture is small and intuitive, but not
#	necessarily the most efficient in other aspects.

INS_SET = {'LDAI':(2,'84'), #Name, length, opcode
	'LDAA':(3,'88'), #Load A Addr
	'LDAD':(1,'81'), #A<-D
	'STAA':(3,'F6'), #Store A Addr
	'STAR':(1,'F1'), #D<-A
	'ADCR':(1,'01'), #Add with carry, store in A
	'SBCR':(1,'11'), #Subtract with borrow
	'CMPR':(1,'91'), #Compare
	'ANDR':(1,'21'), #AND
	'ORR':(1,'31'), #OR
	'XORR':(1,'41'), #XOR
	'SLRL':(1,'51'), #Shift left logical
	'SRRL':(1,'61'), #Shift right logical
	'ROLC':(1,'52'), #Rotate left through carry
	'RORC':(1,'62'), #Rotate right through carry
	'BCCA':(3,'B0'), #Branch on NOT C (SREG=CVSZ,Carry,oVerflow,Sign,Zero)
	'BCSA':(3,'B1'), #Branch on C
	'BEQA':(3,'B2'), #Branch on Z
	'BMIA':(3,'B3'), #Branch on negative
	'BNEA':(3,'B4'), #Branch on NOT Z
	'BPLA':(3,'B5'), #Branch on positive
	'BVCA':(3,'B6'), #Branch on no overflow
	'BVSA':(3,'B7'), #Branch on overflow
	'DECA':(1,'FB'), #Decrement A
	'INCA':(1,'FA'), #Increment A
	'SETC':(1,'F8'), #Set carry flag
	'CLRC':(1,'F9'), #Clear carry flag
	'LDSI':(3,'89'), #Load the stack pointer (SP)
	'CALL':(3,'C8'), #Call into subroutine
	'RET':(1,'C0'), #Return from subroutine
	'LDXI':(3,'8A'), #Load index register
	'LDAA_with_index':(1,'BC'), #Load A with data pointed to by index register
	'STAA_with_index':(1,'EC'), #Store A to index (STAA v,X)
	'INCX':(1,'FC'), #Increment index register
	'DECX':(1,'FD')} #Decrement index register

<|endoftext|>


<commit_before>from escher.plots import Builder

from os.path import join, dirname, realpath

def generate_static_site():
    build_path = realpath(join(dirname(realpath(__file__)), '..'))
    print build_path

    for kind in ['viewer', 'builder']:
        js_source = 'web'
        enable_editing = (kind=='builder')

        # make the builder
        builder = Builder(safe=True)

        html = builder.save_html(filepath=join(build_path, kind+'.html'),
                                 js_source=js_source, enable_editing=enable_editing,
                                 js_url_parse=True)

if __name__=='__main__':
    generate_static_site()
<commit_msg>non-minified static files<commit_after>from escher.plots import Builder

from os.path import join, dirname, realpath

def generate_static_site():
    build_path = realpath(join(dirname(realpath(__file__)), '..'))
    print build_path

    for kind in ['viewer', 'builder']:
        for minified_js in [True, False]:
            js_source = 'web'
            enable_editing = (kind=='builder')

            # make the builder
            builder = Builder(safe=True)

            filepath = join(build_path,
                            '%s%s.html' % (kind, '' if minified_js else '_not_minified'))
            html = builder.save_html(filepath=filepath,
                                     js_source=js_source,
                                     minified_js=minified_js,
                                     enable_editing=enable_editing,
                                     js_url_parse=True)

if __name__=='__main__':
    generate_static_site()
<|endoftext|>


<commit_before># Copyright 2019-present Kensho Technologies, LLC.
"""Convert lowered IR basic blocks to Cypher query strings."""
from itertools import chain

from . import cypher_helpers
from .blocks import Fold, QueryRoot, Recurse, Traverse
from .cypher_query import CypherStep
from .helpers import FoldScopeLocation

def _emit_code_from_cypher_step(cypher_step):
    """Return a Cypher query pattern match expression corresponding to the given CypherStep."""
    no_linked_location = cypher_step.linked_location is None
    step_is_query_root = isinstance(cypher_step.step_block, QueryRoot)

    if no_linked_location ^ step_is_query_root:
        raise AssertionError(u'Received an illegal CypherStep. Not having a linked location is '
                             u'allowed if and only if the step is with a QueryRoot object. {}'
                             .format(cypher_step))

    has_where_block = cypher_step.where_block is not None
    is_optional_step = (
        isinstance(cypher_step.step_block, Traverse) and
        cypher_step.step_block.optional
    )
    if has_where_block and is_optional_step:
        raise AssertionError(u'Received an illegal CypherStep containing an optional step together '
                             u'with a "where" Filter block. This is a bug in the lowering code, as '
                             u'it should have moved the filtering to the global operations '
                             u'section. {}'.format(cypher_step))

    step_location = cypher_step.as_block.location  # destination vertex for current step's traversal
    step_location_name = cypher_helpers.get_unique_vertex_name_from_location(step_location)

    is_fold_step = isinstance(step_location, FoldScopeLocation)

    template_data = {
        'step_location': step_location_name,
        'step_vertex_type': u':'.join(sorted(cypher_step.step_types)),
        'quantifier': u'',
        'left_edge_mark': u'',
        'right_edge_mark': u'',
    }
    step_vertex_pattern = u'(%(step_location)s:%(step_vertex_type)s)'

    if cypher_step.linked_location is None:
        pattern = u'MATCH ' + step_vertex_pattern
    else:
        pattern = (
            u'MATCH (%(linked_location)s)'
            u'%(left_edge_mark)s-[:%(edge_type)s%(quantifier)s]-%(right_edge_mark)s' +
            step_vertex_pattern
        )
        linked_location_name = cypher_helpers.get_unique_vertex_name_from_location(
            cypher_step.linked_location)
        template_data['linked_location'] = linked_location_name

    if has_where_block:
        pattern += u'\n  WHERE %(predicate)s'
        template_data['predicate'] = cypher_step.where_block.predicate.to_cypher()

    if is_optional_step or is_fold_step:
        # OPTIONAL for fold too because if there is no such path for the given fold traversal, we
        # still want to return an empty list. Without OPTIONAL, the entire row would be missing
        # from the output.
        pattern = u'OPTIONAL ' + pattern

    if isinstance(cypher_step.step_block, (Traverse, Recurse, Fold)):
        if isinstance(cypher_step.step_block, Fold):
            direction, edge_name = cypher_step.step_block.fold_scope_location.fold_path[0]
        else:
            direction = cypher_step.step_block.direction
            edge_name = cypher_step.step_block.edge_name

        template_data['edge_type'] = edge_name

        direction_lookup = {
            'in': ('left_edge_mark', u'<'),
            'out': ('right_edge_mark', u'>'),
        }
        direction_symbol_name, direction_symbol = direction_lookup[direction]
        template_data[direction_symbol_name] = direction_symbol

    if isinstance(cypher_step.step_block, Recurse):
        template_data['quantifier'] = u'*0..%d' % cypher_step.step_block.depth

    # Comply with Cypher style guidebook on whitespace a bit.
    pattern += u'\n'

    return pattern % template_data

def _emit_with_clause_components(cypher_steps):
    """Emit a list of strings, one for each vertex or list that passes through a WITH clause."""
    if not cypher_steps:
        return []

    result = []
    location_names = set()
    for cypher_step in cypher_steps:
        location = cypher_step.as_block.location
        location_name = cypher_helpers.get_unique_vertex_name_from_location(location)
        if isinstance(location, FoldScopeLocation):
            location_name = cypher_helpers.get_collected_vertex_list_name(location_name)
        location_names.add(location_name)

    # Sort the locations, to ensure a deterministic order.
    for index, location_name in enumerate(sorted(location_names)):
        if index > 0:
            result.append(u',')

        # We intentionally "rename" each location to its own name, to work around a limitation
        # in RedisGraph where un-aliased "WITH" clauses are not supported:
        # https://oss.redislabs.com/redisgraph/known_limitations/#unaliased-with-entities
        result.append(u'\n  %(name)s AS %(name)s' % {'name': location_name})

    return result

def _emit_with_clause_components_for_current_fold_scope(current_fold_scope_cypher_steps):
    """Emit a list of strings, one for each vertex or list passing through a WITH clause."""
    result = []

    vertex_names = {}
    for cypher_step in current_fold_scope_cypher_steps:
        if not isinstance(cypher_step, CypherStep):
            raise TypeError(u'Expected current_fold_scope_cypher_steps to contain only CypherStep '
                            u'objects. Instead, got object {} of type {}. '
                            u'current_fold_scope_cypher_steps: {}'
                            .format(cypher_step, type(cypher_step),
                                    current_fold_scope_cypher_steps))
        fold_scope_location = cypher_step.as_block.location
        full_vertex_name = cypher_helpers.get_fold_scope_location_full_path_name(
            fold_scope_location)
        collected_name = cypher_helpers.get_collected_vertex_list_name(full_vertex_name)
        vertex_names[u'collect(' + full_vertex_name + ')'] = collected_name

    # Sort the locations, to ensure a deterministic order.
    for index, collect_call in enumerate(sorted(vertex_names)):
        if index > 0:
            result.append(u',')
        result.append(u'\n  {collect_call} AS {collected_name}'
                      .format(collect_call=collect_call, collected_name=vertex_names[collect_call]))
    return result

def _emit_fold_scope(cypher_query):
    """Return a Cypher query pattern match expression for each fold scope in cypher_query.

    Consider the following CYPHER query, corresponding to test_input_data.multiple_folds()

    MATCH (Animal___1:Animal)
    OPTIONAL MATCH (Animal___1)-[:Animal_ParentOf]->(Animal__out_Animal_ParentOf___1:Animal)
    WITH
      Animal___1 AS Animal___1,
      collect(Animal__out_Animal_ParentOf___1) AS collected_Animal__out_Animal_ParentOf___1
    OPTIONAL MATCH (Animal___1)<-[:Animal_ParentOf]-(Animal__in_Animal_ParentOf___1:Animal)
    WITH
      Animal___1 AS Animal___1,
      collected_Animal__out_Animal_ParentOf___1 AS
        collected_Animal__out_Animal_ParentOf___1,
      collect(Animal__in_Animal_ParentOf___1) AS
        collected_Animal__in_Animal_ParentOf___1
    RETURN
      Animal___1.name AS `animal_name`,
      [x IN collected_Animal__out_Animal_ParentOf___1 | x.name] AS `child_names_list`,
      [x IN collected_Animal__out_Animal_ParentOf___1 | x.uuid] AS `child_uuids_list`,
      [x IN collected_Animal__in_Animal_ParentOf___1 | x.name] AS `parent_names_list`,
      [x IN collected_Animal__in_Animal_ParentOf___1 | x.uuid] AS `parent_uuids_list`

    For each fold scope, the structure generally works as follows:

    step 1. First, traverse to the vertex just outside the fold scope. Each traversal is mandatory
    here, so each traversal here is represented by a MATCH clause. Corresponds to:
        MATCH (Animal___1:Animal)

    step 2. Then, for each vertex at/ inside the fold scope, add a corresponding OPTIONAL MATCH
    clause. This traversal is marked OPTIONAL MATCH and not MATCH since we want the result to be an
    empty list and still show up in the final result even if nothing matches the traversal.
    Corresponds to:
        OPTIONAL MATCH (Animal___1)-[:Animal_ParentOf]->(Animal__out_Animal_ParentOf___1:Animal)

    step 3. After traversing to the innermost scope (at which point all the OPTIONAL MATCH clauses
    are generated, create a WITH clause. For each previously-expanded vertex V not in a fold scope,
    pass it through the WITH clause as itself (i.e. `V AS V`). For each vertex at or inside a
    previously-seen fold scope, return the name given to the list. For vertices at or inside the
    current fold scope, collect the vertices in the WITH clause as well, which materializes the list
    and guarantees that the order of elements within the output of a @fold is stable within each
    result set. Corresponds to:
        WITH
          Animal___1 AS Animal___1,
          collect(Animal__out_Animal_ParentOf___1) AS collected_Animal__out_Animal_ParentOf___1

    step 4. This process is repeated for other fold scopes in the query. Corresponds to:
        OPTIONAL MATCH (Animal___1)<-[:Animal_ParentOf]-(Animal__in_Animal_ParentOf___1:Animal)
        WITH
          Animal___1 AS Animal___1,
          collected_Animal__out_Animal_ParentOf___1 AS
            collected_Animal__out_Animal_ParentOf___1,
          collect(Animal__in_Animal_ParentOf___1) AS
            collected_Animal__in_Animal_ParentOf___1

    Args:
        cypher_query: CypherQuery object compiled from the given GraphQL query.

    Returns:
        list of strings that, when concatenated in order, form the part of the query that
        corresponds to each fold in the GraphQL query.
    """
    query_data = []
    previous_fold_scope_cypher_steps = []
    for fold_scope_location in cypher_query.folds:
        # step 2
        current_fold_scope_cypher_steps = cypher_query.folds[fold_scope_location]
        for cypher_step in current_fold_scope_cypher_steps:
            query_data.append(_emit_code_from_cypher_step(cypher_step))

        # step 3
        query_data.append(u'WITH')

        # First for all non-fold-scope CypherSteps, then all previous fold scope CypherSteps
        with_clause_steps = chain(cypher_query.steps, previous_fold_scope_cypher_steps)
        query_data.extend(_emit_with_clause_components(with_clause_steps))

        # Then for all current fold scope CypherSteps
        if current_fold_scope_cypher_steps:
            query_data.append(u',')
        query_data.extend(
            _emit_with_clause_components_for_current_fold_scope(current_fold_scope_cypher_steps))

        query_data.append(u'\n')

        # step 4 preparation:
        # Now that we've finished out this fold scope, we need to ensure these vertices get
        # passed on through all later WITH clauses as well.
        previous_fold_scope_cypher_steps.extend(current_fold_scope_cypher_steps)
    return query_data

##############
# Public API #
##############

def emit_code_from_ir(cypher_query, compiler_metadata):
    """Return a Cypher query string from a CypherQuery object."""
    # According to the Cypher Query Language Reference [0], the standard Cypher version is
    # Cypher 9 (page 196) and we should be able to specify the Cypher version in the query.
    # Unfortunately, this turns out to be invalid in both Neo4j and RedisGraph-- Neo4j supports
    # Cypher version 2.3, 3.4, and 3.5 [1] while Redisgraph doesn't support the syntax at all [2].
    # When this does eventually get resolved, we can change `query_data` back to `[u'CYPHER 9']`
    # [0] https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf
    # [1] https://github.com/neo4j/neo4j/issues/12239
    # [2] https://github.com/RedisGraph/RedisGraph/issues/552
    query_data = [u'']

    # if we have any fold directives in the query, this loop corresponds to step 1 described in
    # the comment in the function _emit_fold_scope().
    for cypher_step in cypher_query.steps:
        query_data.append(_emit_code_from_cypher_step(cypher_step))

    if cypher_query.folds:
        query_data.extend(_emit_fold_scope(cypher_query))

    if cypher_query.global_where_block is not None:
        query_data.append(u'WITH')
        query_data.extend(_emit_with_clause_components(cypher_query.steps))

        query_data.append(u'WHERE ')
        query_data.append(cypher_query.global_where_block.predicate.to_cypher())
        query_data.append(u'\n')

    query_data.append(u'RETURN')
    output_fields = cypher_query.output_block.fields
    sorted_output_keys = sorted(output_fields.keys())
    break_and_indent = u'\n  '
    for output_index, output_name in enumerate(sorted_output_keys):
        if output_index > 0:
            query_data.append(u',')

        output_expression = output_fields[output_name]
        query_data.append(break_and_indent)
        query_data.append(u'%s AS `%s`' % (output_expression.to_cypher(), output_name))

    return u''.join(query_data)
<commit_msg>Sort FoldScopeLocations to ensure predictable output order for multiple folds (#470)<commit_after># Copyright 2019-present Kensho Technologies, LLC.
"""Convert lowered IR basic blocks to Cypher query strings."""
from itertools import chain

from . import cypher_helpers
from .blocks import Fold, QueryRoot, Recurse, Traverse
from .cypher_query import CypherStep
from .helpers import FoldScopeLocation

def _emit_code_from_cypher_step(cypher_step):
    """Return a Cypher query pattern match expression corresponding to the given CypherStep."""
    no_linked_location = cypher_step.linked_location is None
    step_is_query_root = isinstance(cypher_step.step_block, QueryRoot)

    if no_linked_location ^ step_is_query_root:
        raise AssertionError(u'Received an illegal CypherStep. Not having a linked location is '
                             u'allowed if and only if the step is with a QueryRoot object. {}'
                             .format(cypher_step))

    has_where_block = cypher_step.where_block is not None
    is_optional_step = (
        isinstance(cypher_step.step_block, Traverse) and
        cypher_step.step_block.optional
    )
    if has_where_block and is_optional_step:
        raise AssertionError(u'Received an illegal CypherStep containing an optional step together '
                             u'with a "where" Filter block. This is a bug in the lowering code, as '
                             u'it should have moved the filtering to the global operations '
                             u'section. {}'.format(cypher_step))

    step_location = cypher_step.as_block.location  # destination vertex for current step's traversal
    step_location_name = cypher_helpers.get_unique_vertex_name_from_location(step_location)

    is_fold_step = isinstance(step_location, FoldScopeLocation)

    template_data = {
        'step_location': step_location_name,
        'step_vertex_type': u':'.join(sorted(cypher_step.step_types)),
        'quantifier': u'',
        'left_edge_mark': u'',
        'right_edge_mark': u'',
    }
    step_vertex_pattern = u'(%(step_location)s:%(step_vertex_type)s)'

    if cypher_step.linked_location is None:
        pattern = u'MATCH ' + step_vertex_pattern
    else:
        pattern = (
            u'MATCH (%(linked_location)s)'
            u'%(left_edge_mark)s-[:%(edge_type)s%(quantifier)s]-%(right_edge_mark)s' +
            step_vertex_pattern
        )
        linked_location_name = cypher_helpers.get_unique_vertex_name_from_location(
            cypher_step.linked_location)
        template_data['linked_location'] = linked_location_name

    if has_where_block:
        pattern += u'\n  WHERE %(predicate)s'
        template_data['predicate'] = cypher_step.where_block.predicate.to_cypher()

    if is_optional_step or is_fold_step:
        # OPTIONAL for fold too because if there is no such path for the given fold traversal, we
        # still want to return an empty list. Without OPTIONAL, the entire row would be missing
        # from the output.
        pattern = u'OPTIONAL ' + pattern

    if isinstance(cypher_step.step_block, (Traverse, Recurse, Fold)):
        if isinstance(cypher_step.step_block, Fold):
            direction, edge_name = cypher_step.step_block.fold_scope_location.fold_path[0]
        else:
            direction = cypher_step.step_block.direction
            edge_name = cypher_step.step_block.edge_name

        template_data['edge_type'] = edge_name

        direction_lookup = {
            'in': ('left_edge_mark', u'<'),
            'out': ('right_edge_mark', u'>'),
        }
        direction_symbol_name, direction_symbol = direction_lookup[direction]
        template_data[direction_symbol_name] = direction_symbol

    if isinstance(cypher_step.step_block, Recurse):
        template_data['quantifier'] = u'*0..%d' % cypher_step.step_block.depth

    # Comply with Cypher style guidebook on whitespace a bit.
    pattern += u'\n'

    return pattern % template_data

def _emit_with_clause_components(cypher_steps):
    """Emit a list of strings, one for each vertex or list that passes through a WITH clause."""
    if not cypher_steps:
        return []

    result = []
    location_names = set()
    for cypher_step in cypher_steps:
        location = cypher_step.as_block.location
        location_name = cypher_helpers.get_unique_vertex_name_from_location(location)
        if isinstance(location, FoldScopeLocation):
            location_name = cypher_helpers.get_collected_vertex_list_name(location_name)
        location_names.add(location_name)

    # Sort the locations, to ensure a deterministic order.
    for index, location_name in enumerate(sorted(location_names)):
        if index > 0:
            result.append(u',')

        # We intentionally "rename" each location to its own name, to work around a limitation
        # in RedisGraph where un-aliased "WITH" clauses are not supported:
        # https://oss.redislabs.com/redisgraph/known_limitations/#unaliased-with-entities
        result.append(u'\n  %(name)s AS %(name)s' % {'name': location_name})

    return result

def _emit_with_clause_components_for_current_fold_scope(current_fold_scope_cypher_steps):
    """Emit a list of strings, one for each vertex or list passing through a WITH clause."""
    result = []

    vertex_names = {}
    for cypher_step in current_fold_scope_cypher_steps:
        if not isinstance(cypher_step, CypherStep):
            raise TypeError(u'Expected current_fold_scope_cypher_steps to contain only CypherStep '
                            u'objects. Instead, got object {} of type {}. '
                            u'current_fold_scope_cypher_steps: {}'
                            .format(cypher_step, type(cypher_step),
                                    current_fold_scope_cypher_steps))
        fold_scope_location = cypher_step.as_block.location
        full_vertex_name = cypher_helpers.get_fold_scope_location_full_path_name(
            fold_scope_location)
        collected_name = cypher_helpers.get_collected_vertex_list_name(full_vertex_name)
        vertex_names[u'collect(' + full_vertex_name + ')'] = collected_name

    # Sort the locations, to ensure a deterministic order.
    for index, collect_call in enumerate(sorted(vertex_names)):
        if index > 0:
            result.append(u',')
        result.append(u'\n  {collect_call} AS {collected_name}'
                      .format(collect_call=collect_call, collected_name=vertex_names[collect_call]))
    return result

def _emit_fold_scope(cypher_query):
    """Return a Cypher query pattern match expression for each fold scope in cypher_query.

    Consider the following CYPHER query, corresponding to test_input_data.multiple_folds()

    MATCH (Animal___1:Animal)
    OPTIONAL MATCH (Animal___1)-[:Animal_ParentOf]->(Animal__out_Animal_ParentOf___1:Animal)
    WITH
      Animal___1 AS Animal___1,
      collect(Animal__out_Animal_ParentOf___1) AS collected_Animal__out_Animal_ParentOf___1
    OPTIONAL MATCH (Animal___1)<-[:Animal_ParentOf]-(Animal__in_Animal_ParentOf___1:Animal)
    WITH
      Animal___1 AS Animal___1,
      collected_Animal__out_Animal_ParentOf___1 AS
        collected_Animal__out_Animal_ParentOf___1,
      collect(Animal__in_Animal_ParentOf___1) AS
        collected_Animal__in_Animal_ParentOf___1
    RETURN
      Animal___1.name AS `animal_name`,
      [x IN collected_Animal__out_Animal_ParentOf___1 | x.name] AS `child_names_list`,
      [x IN collected_Animal__out_Animal_ParentOf___1 | x.uuid] AS `child_uuids_list`,
      [x IN collected_Animal__in_Animal_ParentOf___1 | x.name] AS `parent_names_list`,
      [x IN collected_Animal__in_Animal_ParentOf___1 | x.uuid] AS `parent_uuids_list`

    For each fold scope, the structure generally works as follows:

    step 1. First, traverse to the vertex just outside the fold scope. Each traversal is mandatory
    here, so each traversal here is represented by a MATCH clause. Corresponds to:
        MATCH (Animal___1:Animal)

    step 2. Then, for each vertex at/ inside the fold scope, add a corresponding OPTIONAL MATCH
    clause. This traversal is marked OPTIONAL MATCH and not MATCH since we want the result to be an
    empty list and still show up in the final result even if nothing matches the traversal.
    Corresponds to:
        OPTIONAL MATCH (Animal___1)-[:Animal_ParentOf]->(Animal__out_Animal_ParentOf___1:Animal)

    step 3. After traversing to the innermost scope (at which point all the OPTIONAL MATCH clauses
    are generated, create a WITH clause. For each previously-expanded vertex V not in a fold scope,
    pass it through the WITH clause as itself (i.e. `V AS V`). For each vertex at or inside a
    previously-seen fold scope, return the name given to the list. For vertices at or inside the
    current fold scope, collect the vertices in the WITH clause as well, which materializes the list
    and guarantees that the order of elements within the output of a @fold is stable within each
    result set. Corresponds to:
        WITH
          Animal___1 AS Animal___1,
          collect(Animal__out_Animal_ParentOf___1) AS collected_Animal__out_Animal_ParentOf___1

    step 4. This process is repeated for other fold scopes in the query. Corresponds to:
        OPTIONAL MATCH (Animal___1)<-[:Animal_ParentOf]-(Animal__in_Animal_ParentOf___1:Animal)
        WITH
          Animal___1 AS Animal___1,
          collected_Animal__out_Animal_ParentOf___1 AS
            collected_Animal__out_Animal_ParentOf___1,
          collect(Animal__in_Animal_ParentOf___1) AS
            collected_Animal__in_Animal_ParentOf___1

    Args:
        cypher_query: CypherQuery object compiled from the given GraphQL query.

    Returns:
        list of strings that, when concatenated in order, form the part of the query that
        corresponds to each fold in the GraphQL query.
    """
    query_data = []
    previous_fold_scope_cypher_steps = []
    for fold_scope_location in sorted(cypher_query.folds.keys()):
        # step 2
        current_fold_scope_cypher_steps = cypher_query.folds[fold_scope_location]
        for cypher_step in current_fold_scope_cypher_steps:
            query_data.append(_emit_code_from_cypher_step(cypher_step))

        # step 3
        query_data.append(u'WITH')

        # First for all non-fold-scope CypherSteps, then all previous fold scope CypherSteps
        with_clause_steps = chain(cypher_query.steps, previous_fold_scope_cypher_steps)
        query_data.extend(_emit_with_clause_components(with_clause_steps))

        # Then for all current fold scope CypherSteps
        if current_fold_scope_cypher_steps:
            query_data.append(u',')
        query_data.extend(
            _emit_with_clause_components_for_current_fold_scope(current_fold_scope_cypher_steps))

        query_data.append(u'\n')

        # step 4 preparation:
        # Now that we've finished out this fold scope, we need to ensure these vertices get
        # passed on through all later WITH clauses as well.
        previous_fold_scope_cypher_steps.extend(current_fold_scope_cypher_steps)
    return query_data

##############
# Public API #
##############

def emit_code_from_ir(cypher_query, compiler_metadata):
    """Return a Cypher query string from a CypherQuery object."""
    # According to the Cypher Query Language Reference [0], the standard Cypher version is
    # Cypher 9 (page 196) and we should be able to specify the Cypher version in the query.
    # Unfortunately, this turns out to be invalid in both Neo4j and RedisGraph-- Neo4j supports
    # Cypher version 2.3, 3.4, and 3.5 [1] while Redisgraph doesn't support the syntax at all [2].
    # When this does eventually get resolved, we can change `query_data` back to `[u'CYPHER 9']`
    # [0] https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf
    # [1] https://github.com/neo4j/neo4j/issues/12239
    # [2] https://github.com/RedisGraph/RedisGraph/issues/552
    query_data = [u'']

    # if we have any fold directives in the query, this loop corresponds to step 1 described in
    # the comment in the function _emit_fold_scope().
    for cypher_step in cypher_query.steps:
        query_data.append(_emit_code_from_cypher_step(cypher_step))

    if cypher_query.folds:
        query_data.extend(_emit_fold_scope(cypher_query))

    if cypher_query.global_where_block is not None:
        query_data.append(u'WITH')
        query_data.extend(_emit_with_clause_components(cypher_query.steps))

        query_data.append(u'WHERE ')
        query_data.append(cypher_query.global_where_block.predicate.to_cypher())
        query_data.append(u'\n')

    query_data.append(u'RETURN')
    output_fields = cypher_query.output_block.fields
    sorted_output_keys = sorted(output_fields.keys())
    break_and_indent = u'\n  '
    for output_index, output_name in enumerate(sorted_output_keys):
        if output_index > 0:
            query_data.append(u',')

        output_expression = output_fields[output_name]
        query_data.append(break_and_indent)
        query_data.append(u'%s AS `%s`' % (output_expression.to_cypher(), output_name))

    return u''.join(query_data)
<|endoftext|>


<commit_before>import graphene

from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
from graphql_relay.node.node import from_global_id

from .models import Building, Floor, Room

class UserOnlyMixin:
    @classmethod
    def get_node(cls, id, context, info):
        try:
            obj = cls._meta.model.objects.get(id=id)
        except cls._meta.model.DoesNotExist:
            return None

        if context.user == obj.user:
            return obj
        return None

class BuildingNode(UserOnlyMixin, DjangoObjectType):
    pk = graphene.Int()

    class Meta:
        model = Building
        interfaces = (graphene.Node, )
        filter_fields = {
            'name': ['icontains'],
            'size': ['gte', 'lte']
        }

class CreateBuilding(graphene.Mutation):
    class Input:
        name = graphene.String(required=True)
        size = graphene.Int(required=True)

    ok = graphene.Boolean()
    building = graphene.Field(BuildingNode)

    @staticmethod
    def mutate(root, args, context, info):
        building = Building.objects.create(name=args.get('name'), size=args.get('size'), user=context.user)
        ok = True
        return CreateBuilding(building=building, ok=ok)

class ModifyBuilding(graphene.Mutation):
    class Input:
        building_id = graphene.Int(required=True)
        new_name = graphene.String(required=True)
        #size = graphene.Int()

    ok = graphene.Boolean()
    building = graphene.Field(BuildingNode)

    @staticmethod
    def mutate(root, args, context, info):
        building_to_modify = Building.objects.get(pk=args.get('building_id'))
        building_to_modify.name = args.get('new_name')
        #building_to_modify.size = args.get('size')
        ok = True
        return ModifyBuilding(building=building_to_modify, ok=ok)

class FloorNode(UserOnlyMixin, DjangoObjectType):
    pk = graphene.Int()

    class Meta:
        model = Floor
        interfaces = (graphene.Node, )
        filter_fields = ['number']

class RoomNode(UserOnlyMixin, DjangoObjectType):
    pk = graphene.Int()

    class Meta:
        model = Room
        interfaces = (graphene.Node, )
        filter_fields = {
            'name': ['icontains']
        }

class Query(graphene.ObjectType):
    buildings = DjangoFilterConnectionField(BuildingNode)
    floors = DjangoFilterConnectionField(FloorNode)
    rooms = DjangoFilterConnectionField(RoomNode)
    building = graphene.Node.Field(BuildingNode)
    floor = graphene.Node.Field(FloorNode)
    room = graphene.Node.Field(RoomNode)

    def resolve_buildings(self, args, context, info):
        return Building.objects.filter(user=context.user)

    def resolve_floors(self, args, context, info):
        return Floor.objects.filter(building__user=context.user)

    def resolve_rooms(self, args, context, info):
        return Room.objects.filter(floor__building__user=context.user)

class Mutation(graphene.ObjectType):
    create_building = CreateBuilding.Field()
    modify_building = ModifyBuilding.Field()

schema = graphene.Schema(query=Query, mutation=Mutation)
<commit_msg>save() mods<commit_after>import graphene

from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
from graphql_relay.node.node import from_global_id

from .models import Building, Floor, Room

class UserOnlyMixin:
    @classmethod
    def get_node(cls, id, context, info):
        try:
            obj = cls._meta.model.objects.get(id=id)
        except cls._meta.model.DoesNotExist:
            return None

        if context.user == obj.user:
            return obj
        return None

class BuildingNode(UserOnlyMixin, DjangoObjectType):
    pk = graphene.Int()

    class Meta:
        model = Building
        interfaces = (graphene.Node, )
        filter_fields = {
            'name': ['icontains'],
            'size': ['gte', 'lte']
        }

class CreateBuilding(graphene.Mutation):
    class Input:
        name = graphene.String(required=True)
        size = graphene.Int(required=True)

    ok = graphene.Boolean()
    building = graphene.Field(BuildingNode)

    @staticmethod
    def mutate(root, args, context, info):
        building = Building.objects.create(name=args.get('name'),
                                           size=args.get('size'),
                                           user=context.user)
        ok = True
        return CreateBuilding(building=building, ok=ok)

class ModifyBuilding(graphene.Mutation):
    class Input:
        building_id = graphene.Int(required=True)
        new_name = graphene.String(required=True)
        #size = graphene.Int()

    ok = graphene.Boolean()
    building = graphene.Field(BuildingNode)

    @staticmethod
    def mutate(root, args, context, info):
        building_to_modify = Building.objects.get(pk=args.get('building_id'))
        building_to_modify.name = args.get('new_name')
        building_to_modify.save()
        #building_to_modify.size = args.get('size')
        ok = True
        return ModifyBuilding(building=building_to_modify, ok=ok)

class FloorNode(UserOnlyMixin, DjangoObjectType):
    pk = graphene.Int()

    class Meta:
        model = Floor
        interfaces = (graphene.Node, )
        filter_fields = ['number']

class RoomNode(UserOnlyMixin, DjangoObjectType):
    pk = graphene.Int()

    class Meta:
        model = Room
        interfaces = (graphene.Node, )
        filter_fields = {
            'name': ['icontains']
        }

class Query(graphene.ObjectType):
    buildings = DjangoFilterConnectionField(BuildingNode)
    floors = DjangoFilterConnectionField(FloorNode)
    rooms = DjangoFilterConnectionField(RoomNode)
    building = graphene.Node.Field(BuildingNode)
    floor = graphene.Node.Field(FloorNode)
    room = graphene.Node.Field(RoomNode)

    def resolve_buildings(self, args, context, info):
        return Building.objects.filter(user=context.user)

    def resolve_floors(self, args, context, info):
        return Floor.objects.filter(building__user=context.user)

    def resolve_rooms(self, args, context, info):
        return Room.objects.filter(floor__building__user=context.user)

class Mutation(graphene.ObjectType):
    create_building = CreateBuilding.Field()
    modify_building = ModifyBuilding.Field()

schema = graphene.Schema(query=Query, mutation=Mutation)
<|endoftext|>


<commit_before>from .base_settings import *
from google.oauth2 import service_account
import os

INSTALLED_APPS += [
    'course_grader.apps.CourseGraderConfig',
    'supporttools',
    'userservice',
    'persistent_message',
    'rc_django',
    'grade_conversion_calculator',
    'django.contrib.humanize',
    'django_user_agents',
    'compressor',
]

MIDDLEWARE += [
    'userservice.user.UserServiceMiddleware',
    'django_user_agents.middleware.UserAgentMiddleware',
]

TEMPLATES[0]['OPTIONS']['context_processors'] += [
    'supporttools.context_processors.supportools_globals',
    'course_grader.context_processors.user',
    'course_grader.context_processors.has_less_compiled',
    'course_grader.context_processors.debug_mode',
]

COMPRESS_ROOT = '/static/'

STATICFILES_FINDERS += (
    'compressor.finders.CompressorFinder',
)

COMPRESS_PRECOMPILERS = (
    ('text/less', 'lessc {infile} {outfile}'),
    ('text/x-sass', 'pyscss {infile} > {outfile}'),
    ('text/x-scss', 'pyscss {infile} > {outfile}'),
)

COMPRESS_CSS_FILTERS = [
    'compressor.filters.css_default.CssAbsoluteFilter',
    'compressor.filters.cssmin.CSSMinFilter'
]

COMPRESS_JS_FILTERS = [
    'compressor.filters.jsmin.JSMinFilter',
]

COMPRESS_OFFLINE = True
COMPRESS_OFFLINE_CONTEXT = {
    'wrapper_template': 'persistent_message/manage_wrapper.html',
}

if os.getenv('ENV', 'localdev') == 'localdev':
    DEBUG = True
    GRADEPAGE_SUPPORT_GROUP = 'u_test_group'
    GRADEPAGE_ADMIN_GROUP = 'u_test_group'
    CURRENT_DATETIME_OVERRIDE = '2013-06-17 10:00:00'
    PAST_TERMS_VIEWABLE = 1
    DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
    MEDIA_ROOT = os.getenv('IMPORT_DATA_ROOT', '/app/csv')
else:
    GRADEPAGE_SUPPORT_GROUP = os.getenv('SUPPORT_GROUP', 'u_acadev_gradepage_support')
    GRADEPAGE_ADMIN_GROUP = os.getenv('ADMIN_GROUP', 'u_acadev_gradepage_admins')
    RESTCLIENTS_DAO_CACHE_CLASS = 'course_grader.cache.RestClientsCache'
    PAST_TERMS_VIEWABLE = 4
    DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
    GS_PROJECT_ID = os.getenv('STORAGE_PROJECT_ID', '')
    GS_BUCKET_NAME = os.getenv('STORAGE_BUCKET_NAME', '')
    GS_LOCATION = os.path.join(os.getenv('IMPORT_DATA_ROOT', ''))
    GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
        '/gcs/credentials.json')

ALLOW_GRADE_SUBMISSION_OVERRIDE = (os.getenv('ENV', 'localdev') != 'prod')
USERSERVICE_VALIDATION_MODULE = 'course_grader.dao.person.is_netid'
USERSERVICE_OVERRIDE_AUTH_MODULE = 'course_grader.views.support.can_override_user'
RESTCLIENTS_ADMIN_AUTH_MODULE = 'course_grader.views.support.can_proxy_restclient'
PERSISTENT_MESSAGE_AUTH_MODULE = 'course_grader.views.support.can_manage_persistent_messages'

EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = 587
EMAIL_TIMEOUT = 15
EMAIL_USE_TLS = True
EMAIL_SSL_CERTFILE = os.getenv('CERT_PATH', '')
EMAIL_SSL_KEYFILE = os.getenv('KEY_PATH', '')

EMAIL_NOREPLY_ADDRESS = os.getenv('EMAIL_NOREPLY_ADDRESS')
if os.getenv('ENV', 'localdev') == 'prod':
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
else:
    EMAIL_BACKEND = 'saferecipient.EmailBackend'
    SAFE_EMAIL_RECIPIENT = os.getenv('SAFE_EMAIL_RECIPIENT')

GRADEPAGE_HOST = 'https://' + os.getenv('CLUSTER_CNAME', 'localhost')
SUBMISSION_DEADLINE_WARNING_HOURS = 41
GRADE_RETENTION_YEARS = 5

GRADEPAGE_SUPPORT_EMAIL = os.getenv('GRADEPAGE_SUPPORT_EMAIL', '')
REGISTRAR_SUPPORT_EMAIL = os.getenv('REGISTRAR_SUPPORT_EMAIL', '')
REGISTRAR_SUPPORT_PHONE = os.getenv('REGISTRAR_SUPPORT_PHONE', '')

SUPPORTTOOLS_PARENT_APP = 'GradePage'
SUPPORTTOOLS_PARENT_APP_URL = '/'

if os.getenv('AUTH', 'NONE') == 'SAML_MOCK':
    MOCK_SAML_ATTRIBUTES = {
        'uwnetid': ['bill'],
        'affiliations': ['instructor', 'member'],
        'eppn': ['bill@washington.edu'],
        'scopedAffiliations': ['student@washington.edu', 'member@washington.edu'],
        'isMemberOf': ['u_test_group', 'u_test_another_group'],
    }

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'info_only': {
            '()': 'course_grader.log.InfoFilter'
        },
        'add_user': {
            '()': 'course_grader.log.UserFilter'
        },
        'stdout_stream': {
            '()': 'django.utils.log.CallbackFilter',
            'callback': lambda record: record.levelno < logging.WARNING
        },
        'stderr_stream': {
            '()': 'django.utils.log.CallbackFilter',
            'callback': lambda record: record.levelno > logging.ERROR
        }
    },
    'formatters': {
        'course_grader': {
            'format': '%(levelname)-4s %(asctime)s %(user)s %(actas)s %(message)s [%(name)s]',
            'datefmt': '[%Y-%m-%d %H:%M:%S]',
        },
        'restclients_timing': {
            'format': '%(levelname)-4s restclients_timing %(module)s %(asctime)s %(message)s [%(name)s]',
            'datefmt': '[%Y-%m-%d %H:%M:%S]',
        },
    },
    'handlers': {
        'stdout': {
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'filters': ['add_user', 'stdout_stream'],
            'formatter': 'course_grader',
        },
        'stderr': {
            'class': 'logging.StreamHandler',
            'stream': sys.stderr,
            'filters': ['add_user', 'stderr_stream'],
            'formatter': 'course_grader',
        },
        'course_grader': {
            'filters': ['info_only', 'add_user', 'stdout_stream'],
            'formatter': 'course_grader',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
        },
        'course_grader_errors': {
            'level': 'ERROR',
            'filters': ['add_user', 'stderr_stream'],
            'formatter': 'course_grader',
            'class': 'logging.StreamHandler',
            'stream': sys.stderr,
        },
        'restclients_timing': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'filters': ['stdout_stream'],
            'formatter': 'restclients_timing',
        },
        'null': {
            'class': 'logging.NullHandler',
        },
    },
    'loggers': {
        'django.security.DisallowedHost': {
            'handlers': ['null'],
            'propagate': False,
        },
        'django.request': {
            'handlers': ['stderr'],
            'level': 'ERROR',
            'propagate': True,
        },
        'course_grader': {
            'handlers': ['course_grader', 'course_grader_errors'],
            'level': 'INFO',
            'propagate': True,
        },
        'restclients_core': {
            'handlers': ['restclients_timing'],
            'level': 'INFO',
            'propagate': False,
        },
        '': {
            'handlers': ['stdout', 'stderr'],
            'level': 'INFO' if os.getenv('ENV', 'localdev') == 'prod' else 'DEBUG'
        }
    }
}
<commit_msg>disable override saving for testing<commit_after>from .base_settings import *
from google.oauth2 import service_account
import os

INSTALLED_APPS += [
    'course_grader.apps.CourseGraderConfig',
    'supporttools',
    'userservice',
    'persistent_message',
    'rc_django',
    'grade_conversion_calculator',
    'django.contrib.humanize',
    'django_user_agents',
    'compressor',
]

MIDDLEWARE += [
    'userservice.user.UserServiceMiddleware',
    'django_user_agents.middleware.UserAgentMiddleware',
]

TEMPLATES[0]['OPTIONS']['context_processors'] += [
    'supporttools.context_processors.supportools_globals',
    'course_grader.context_processors.user',
    'course_grader.context_processors.has_less_compiled',
    'course_grader.context_processors.debug_mode',
]

COMPRESS_ROOT = '/static/'

STATICFILES_FINDERS += (
    'compressor.finders.CompressorFinder',
)

COMPRESS_PRECOMPILERS = (
    ('text/less', 'lessc {infile} {outfile}'),
    ('text/x-sass', 'pyscss {infile} > {outfile}'),
    ('text/x-scss', 'pyscss {infile} > {outfile}'),
)

COMPRESS_CSS_FILTERS = [
    'compressor.filters.css_default.CssAbsoluteFilter',
    'compressor.filters.cssmin.CSSMinFilter'
]

COMPRESS_JS_FILTERS = [
    'compressor.filters.jsmin.JSMinFilter',
]

COMPRESS_OFFLINE = True
COMPRESS_OFFLINE_CONTEXT = {
    'wrapper_template': 'persistent_message/manage_wrapper.html',
}

if os.getenv('ENV', 'localdev') == 'localdev':
    DEBUG = True
    GRADEPAGE_SUPPORT_GROUP = 'u_test_group'
    GRADEPAGE_ADMIN_GROUP = 'u_test_group'
    CURRENT_DATETIME_OVERRIDE = '2013-06-17 10:00:00'
    PAST_TERMS_VIEWABLE = 1
    DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
    MEDIA_ROOT = os.getenv('IMPORT_DATA_ROOT', '/app/csv')
else:
    GRADEPAGE_SUPPORT_GROUP = os.getenv('SUPPORT_GROUP', 'u_acadev_gradepage_support')
    GRADEPAGE_ADMIN_GROUP = os.getenv('ADMIN_GROUP', 'u_acadev_gradepage_admins')
    RESTCLIENTS_DAO_CACHE_CLASS = 'course_grader.cache.RestClientsCache'
    PAST_TERMS_VIEWABLE = 4
    DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
    GS_PROJECT_ID = os.getenv('STORAGE_PROJECT_ID', '')
    GS_BUCKET_NAME = os.getenv('STORAGE_BUCKET_NAME', '')
    GS_LOCATION = os.path.join(os.getenv('IMPORT_DATA_ROOT', ''))
    GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
        '/gcs/credentials.json')

ALLOW_GRADE_SUBMISSION_OVERRIDE = False  # (os.getenv('ENV', 'localdev') != 'prod')
USERSERVICE_VALIDATION_MODULE = 'course_grader.dao.person.is_netid'
USERSERVICE_OVERRIDE_AUTH_MODULE = 'course_grader.views.support.can_override_user'
RESTCLIENTS_ADMIN_AUTH_MODULE = 'course_grader.views.support.can_proxy_restclient'
PERSISTENT_MESSAGE_AUTH_MODULE = 'course_grader.views.support.can_manage_persistent_messages'

EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = 587
EMAIL_TIMEOUT = 15
EMAIL_USE_TLS = True
EMAIL_SSL_CERTFILE = os.getenv('CERT_PATH', '')
EMAIL_SSL_KEYFILE = os.getenv('KEY_PATH', '')

EMAIL_NOREPLY_ADDRESS = os.getenv('EMAIL_NOREPLY_ADDRESS')
if os.getenv('ENV', 'localdev') == 'prod':
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
else:
    EMAIL_BACKEND = 'saferecipient.EmailBackend'
    SAFE_EMAIL_RECIPIENT = os.getenv('SAFE_EMAIL_RECIPIENT')

GRADEPAGE_HOST = 'https://' + os.getenv('CLUSTER_CNAME', 'localhost')
SUBMISSION_DEADLINE_WARNING_HOURS = 41
GRADE_RETENTION_YEARS = 5

GRADEPAGE_SUPPORT_EMAIL = os.getenv('GRADEPAGE_SUPPORT_EMAIL', '')
REGISTRAR_SUPPORT_EMAIL = os.getenv('REGISTRAR_SUPPORT_EMAIL', '')
REGISTRAR_SUPPORT_PHONE = os.getenv('REGISTRAR_SUPPORT_PHONE', '')

SUPPORTTOOLS_PARENT_APP = 'GradePage'
SUPPORTTOOLS_PARENT_APP_URL = '/'

if os.getenv('AUTH', 'NONE') == 'SAML_MOCK':
    MOCK_SAML_ATTRIBUTES = {
        'uwnetid': ['bill'],
        'affiliations': ['instructor', 'member'],
        'eppn': ['bill@washington.edu'],
        'scopedAffiliations': ['student@washington.edu', 'member@washington.edu'],
        'isMemberOf': ['u_test_group', 'u_test_another_group'],
    }

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'info_only': {
            '()': 'course_grader.log.InfoFilter'
        },
        'add_user': {
            '()': 'course_grader.log.UserFilter'
        },
        'stdout_stream': {
            '()': 'django.utils.log.CallbackFilter',
            'callback': lambda record: record.levelno < logging.WARNING
        },
        'stderr_stream': {
            '()': 'django.utils.log.CallbackFilter',
            'callback': lambda record: record.levelno > logging.ERROR
        }
    },
    'formatters': {
        'course_grader': {
            'format': '%(levelname)-4s %(asctime)s %(user)s %(actas)s %(message)s [%(name)s]',
            'datefmt': '[%Y-%m-%d %H:%M:%S]',
        },
        'restclients_timing': {
            'format': '%(levelname)-4s restclients_timing %(module)s %(asctime)s %(message)s [%(name)s]',
            'datefmt': '[%Y-%m-%d %H:%M:%S]',
        },
    },
    'handlers': {
        'stdout': {
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'filters': ['add_user', 'stdout_stream'],
            'formatter': 'course_grader',
        },
        'stderr': {
            'class': 'logging.StreamHandler',
            'stream': sys.stderr,
            'filters': ['add_user', 'stderr_stream'],
            'formatter': 'course_grader',
        },
        'course_grader': {
            'filters': ['info_only', 'add_user', 'stdout_stream'],
            'formatter': 'course_grader',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
        },
        'course_grader_errors': {
            'level': 'ERROR',
            'filters': ['add_user', 'stderr_stream'],
            'formatter': 'course_grader',
            'class': 'logging.StreamHandler',
            'stream': sys.stderr,
        },
        'restclients_timing': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'filters': ['stdout_stream'],
            'formatter': 'restclients_timing',
        },
        'null': {
            'class': 'logging.NullHandler',
        },
    },
    'loggers': {
        'django.security.DisallowedHost': {
            'handlers': ['null'],
            'propagate': False,
        },
        'django.request': {
            'handlers': ['stderr'],
            'level': 'ERROR',
            'propagate': True,
        },
        'course_grader': {
            'handlers': ['course_grader', 'course_grader_errors'],
            'level': 'INFO',
            'propagate': True,
        },
        'restclients_core': {
            'handlers': ['restclients_timing'],
            'level': 'INFO',
            'propagate': False,
        },
        '': {
            'handlers': ['stdout', 'stderr'],
            'level': 'INFO' if os.getenv('ENV', 'localdev') == 'prod' else 'DEBUG'
        }
    }
}
<|endoftext|>


<commit_before>from pecan import request, redirect

import json
import notario

__all__ = ['with_schema']

def with_schema(schema, handler=None, **kw):
    """
    Used to decorate a Pecan controller with form creation for GET | HEAD and
    form validation for anything else (e.g., POST | PUT | DELETE ).

    For an HTTP POST or PUT (RFC2616 unsafe methods) request, the schema is
    used to validate the request body. Errors from validation (if any) are
    accessible at ``request.pecan['schema'].errors``.

    Optionally, validation errors can be made to trigger an internal HTTP
    redirect by specifying a ``handler`` in the ``error_cfg`` argument.

    :param schema: A JSON schema.
    :param handler: A URI path to redirect to when schema validation fails.
                    Can also be a callable that returns a URI path.
    """
    def deco(f):

        def wrapped(*args, **kwargs):
            request.validation_error = None
            request.foo = 1

            if request.method in ('POST', 'PUT'):
                try:
                    data = json.loads(request.body)
                    notario.validate(data, schema)
                except ValueError, error:
                    request.validation_error = error
                    if handler:
                        redirect_to_handler(error, handler)

            return f(*args, **kwargs)

        return wrapped

    return deco

def redirect_to_handler(error, location):
    """
    Cause a requset with an error to internally redirect to a URI path.

    This is generally for internal use, but can be called from within a Pecan
    controller to trigger a validation failure from *within* the controller
    itself, e.g.::

        @expose()
        @with_schema(some_schema, error_cfg={
            'handler': '/some/handler'
        })
        def some_controller(self, **kw):
            if some_bad_condition():
                error_exception = ...
                redirect_to_handler(error_exception, '/some/handler')
    """
    if callable(location):
        location = location()
    request.environ['REQUEST_METHOD'] = 'GET'
    redirect(location, internal=True)
<commit_msg>allow to slap an HTTP response code when invalid<commit_after>from pecan import request, response, redirect

import json
import notario
from notario.exceptions import Invalid

__all__ = ['with_schema']

def with_schema(schema, handler=None, status=400, **kw):
    """
    Used to decorate a Pecan controller with form creation for GET | HEAD and
    form validation for anything else (e.g., POST | PUT | DELETE ).

    For an HTTP POST or PUT (RFC2616 unsafe methods) request, the schema is
    used to validate the request body. Errors from validation (if any) are
    accessible at ``request.pecan['schema'].errors``.

    Optionally, validation errors can be made to trigger an internal HTTP
    redirect by specifying a ``handler`` in the ``error_cfg`` argument.

    :param schema: A JSON schema.
    :param handler: A URI path to redirect to when schema validation fails.
                    Can also be a callable that returns a URI path.
    """
    def deco(f):

        def wrapped(*args, **kwargs):
            request.validation_error = None

            if request.method in ('POST', 'PUT'):
                try:
                    data = json.loads(request.body)
                    notario.validate(data, schema)
                except (Invalid, ValueError) as error:
                    request.validation_error = error
                    if handler:
                        redirect_to_handler(error, handler)
                    response.status = 400

            return f(*args, **kwargs)

        return wrapped

    return deco

def redirect_to_handler(error, location):
    """
    Cause a requset with an error to internally redirect to a URI path.

    This is generally for internal use, but can be called from within a Pecan
    controller to trigger a validation failure from *within* the controller
    itself, e.g.::

        @expose()
        @with_schema(some_schema, error_cfg={
            'handler': '/some/handler'
        })
        def some_controller(self, **kw):
            if some_bad_condition():
                error_exception = ...
                redirect_to_handler(error_exception, '/some/handler')
    """
    if callable(location):
        location = location()
    request.environ['REQUEST_METHOD'] = 'GET'
    redirect(location, internal=True)
<|endoftext|>