<commit_before>"""
cargo/log.py

Application-wide logging configuration.

@author: Bryan Silverthorn <bcs@cargo-cult.org>
"""

import sys
import logging

from logging     import (
    Filter,
    Logger,
    Formatter,
    FileHandler,
    StreamHandler,
    )
from cargo.sugar import run_once
from cargo.flags import (
    Flag,
    Flags,
    )

class DefaultLogger(logging.getLoggerClass()):
    """
    Simple standard logging.
    """

    def __init__(self, name, level = logging.NOTSET):
        """
        Initialize.
        """

        Logger.__init__(self, name, level)

        self.is_squeaky_clean = True

    def detail(self, message, *args, **kwargs):
        """
        Write a log message at the DETAIL level.
        """

        return self.log(logging.DETAIL, message, *args, **kwargs)

    def note(self, message, *args, **kwargs):
        """
        Write a log message at the NOTE level.
        """

        return self.log(logging.NOTE, message, *args, **kwargs)

# global customization (unfortunate, but whatever)
logging.setLoggerClass(DefaultLogger)
logging.DETAIL = 15
logging.NOTE   = 25

logging.addLevelName(logging.DETAIL, "DETAIL")
logging.addLevelName(logging.NOTE, "NOTE")

def level_to_number(level):
    """
    Convert a level description to a level number, if necessary.
    """

    if type(level) is str:
        return logging._levelNames[level]
    else:
        return level

def get_logger(name, level = None, default_level = logging.WARNING):
    """
    Get or create a logger.
    """

    logger = logging.getLogger(name)

    # set the default level, if the logger is new
    if logger.is_squeaky_clean:
        if default_level is not None:
            logger.setLevel(level_to_number(default_level))

    # unconditionally set the logger level, if requested
    if level is not None:
        logger.setLevel(level_to_number(level))

        logger.is_squeaky_clean = False

    return logger

log = get_logger(__name__)

#    log_to_console_flag = \
#        Flag(
#            "--log-to-console",
#            action = "store_true",
#            help = "force console-style logging to stdout [%default]",
#            )
#    log_to_file_flag = \
#        Flag(
#            "--log-to-file",
#            action = "store_true",
#            help = "force logging to script.log.N",
#            )

flags = \
    Flags(
        "Logging",
        Flag(
            "--log-file-prefix",
            default = "script.log",
            metavar = "PREFIX",
            help    = "file logging will write to PREFIX.N [script.log]",
            ),
#         Flag(
#             "-v",
#             "--verbosity",
#             default = logging.INFO,
#             metavar = "N",
#             help    = "log messages of at least level N [%default]",
#             ),
        )

class TTY_ConciseFormatter(Formatter):
    """
    A concise log formatter for console output.
    """

    _DATE_FORMAT = "%y%m%d%H%M%S"

    def __init__(self, stream = None):
        """
        Construct this formatter.
        """

        # construct the format string
        format = "%(message)s"

        # initialize this formatter
        Formatter.__init__(self, format, TTY_ConciseFormatter._DATE_FORMAT)

class TTY_VerboseFormatter(Formatter):
    """
    A verbose log formatter for console output.
    """

    _DATE_FORMAT = "%y%m%d%H%M%S"
    _TIME_COLOR  = "\x1b[34m"
    _NAME_COLOR  = "\x1b[35m"
    _LEVEL_COLOR = "\x1b[33m"
    _COLOR_END   = "\x1b[00m"

    def __init__(self, stream = None):
        """
        Construct this formatter.

        Provides colored output if the stream parameter is specified and is an acceptable TTY.
        We print hardwired escape sequences, which will probably break in some circumstances;
        for this unfortunate shortcoming, we apologize.
        """

        # select and construct format string
        import curses

        format = None

        if stream and hasattr(stream, "isatty") and stream.isatty():
            curses.setupterm()

            # FIXME do nice block formatting, increasing column sizes as necessary
            if curses.tigetnum("colors") > 2:
                format = \
                    "%s%%(name)s%s - %s%%(levelname)s%s - %%(message)s" % (
                        TTY_VerboseFormatter._NAME_COLOR,
                        TTY_VerboseFormatter._COLOR_END,
                        TTY_VerboseFormatter._LEVEL_COLOR,
                        TTY_VerboseFormatter._COLOR_END)

        if format is None:
            format = "%(name)s - %(levelname)s - %(message)s"

        # initialize this formatter
        Formatter.__init__(self, format, TTY_VerboseFormatter._DATE_FORMAT)

class VerboseFileFormatter(Formatter):
    """
    A verbose log formatter for file output.
    """

    _FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    _DATE_FORMAT = "%y%m%d%H%M%S"

    def __init__(self):
        """
        Construct this formatter.
        """

        Formatter.__init__(self, VerboseFileFormatter._FORMAT, VerboseFileFormatter._DATE_FORMAT)

class OpaqueFilter(Filter):
    """
    Exclude all records.
    """

    def __init__(self):
        """
        Initialize.
        """

        Filter.__init__(self)

    def filter(self, record):
        """
        Allow no records.
        """

        return False

class ExactExcludeFilter(Filter):
    """
    Exclude records with a specific name.
    """

    def __init__(self, exclude):
        """
        Initialize this filter.
        """

        # members
        self.__exclude = exclude

        # base
        Filter.__init__(self)

    def filter(self, record):
        """
        Allow all records save those with the excluded name.
        """

        if record.name != self.__exclude:
            return True
        else:
            return False

def enable_console(level = logging.NOTSET, verbose = True):
    """
    Enable typical logging to the console.
    """

    import datetime

    handler = StreamHandler(sys.stdout)

    if verbose:
        formatter = TTY_VerboseFormatter
    else:
        formatter = TTY_ConciseFormatter

    handler.setFormatter(formatter(sys.stdout))
    handler.setLevel(level)

    logging.root.addHandler(handler)

    log.debug("enabled logging to stdout at %s", datetime.datetime.today().isoformat())

    return handler

def enable_disk(prefix = None, level = logging.NOTSET):
    """
    Enable typical logging to disk.
    """

    import os.path
    import datetime

    from itertools import count

    # generate an unused log file path
    if prefix is None:
        #prefix = flags.given.log_file_prefix
        prefix = "script.log" # FIXME use the flag

    for i in count():
        path = "%s.%i" % (prefix, i)

        if not os.path.lexists(path):
            break

    # set up logging
    handler = FileHandler(path, encoding = "UTF-8")

    handler.setFormatter(VerboseFileFormatter())
    handler.setLevel(level)

    logging.root.addHandler(handler)

    log.debug("enabled logging to %s at %s", path, datetime.datetime.today().isoformat())

    return handler

# enable logging by default
@run_once
def enable_default_logging(add_handlers = True):
    # by default, be moderately verbose
    logging.root.setLevel(logging.NOTSET) # FIXME should use flag

    # default setup (FIXME which is silly: should enable the disk log via flag or environment)
    if add_handlers:
        if sys.stdout.isatty():
            enable_console()
        else:
            enable_disk()

<commit_msg>Note a FIXME.<commit_after>"""
cargo/log.py

Application-wide logging configuration.

@author: Bryan Silverthorn <bcs@cargo-cult.org>
"""

import sys
import logging

from logging     import (
    Filter,
    Logger,
    Formatter,
    FileHandler,
    StreamHandler,
    )
from cargo.sugar import run_once
from cargo.flags import (
    Flag,
    Flags,
    )

class DefaultLogger(logging.getLoggerClass()):
    """
    Simple standard logging.
    """

    def __init__(self, name, level = logging.NOTSET):
        """
        Initialize.
        """

        Logger.__init__(self, name, level)

        self.is_squeaky_clean = True

    def detail(self, message, *args, **kwargs):
        """
        Write a log message at the DETAIL level.
        """

        return self.log(logging.DETAIL, message, *args, **kwargs)

    def note(self, message, *args, **kwargs):
        """
        Write a log message at the NOTE level.
        """

        return self.log(logging.NOTE, message, *args, **kwargs)

# global customization (unfortunate, but whatever)
logging.setLoggerClass(DefaultLogger)
logging.DETAIL = 15
logging.NOTE   = 25

logging.addLevelName(logging.DETAIL, "DETAIL")
logging.addLevelName(logging.NOTE, "NOTE")

def level_to_number(level):
    """
    Convert a level description to a level number, if necessary.
    """

    if type(level) is str:
        return logging._levelNames[level]
    else:
        return level

def get_logger(name, level = None, default_level = logging.WARNING):
    """
    Get or create a logger.
    """

    logger = logging.getLogger(name)

    # FIXME the defaults mechanism still isn't quite right

    # set the default level, if the logger is new
    if logger.is_squeaky_clean:
        if default_level is not None:
            logger.setLevel(level_to_number(default_level))

    # unconditionally set the logger level, if requested
    if level is not None:
        logger.setLevel(level_to_number(level))

        logger.is_squeaky_clean = False

    return logger

log = get_logger(__name__)

#    log_to_console_flag = \
#        Flag(
#            "--log-to-console",
#            action = "store_true",
#            help = "force console-style logging to stdout [%default]",
#            )
#    log_to_file_flag = \
#        Flag(
#            "--log-to-file",
#            action = "store_true",
#            help = "force logging to script.log.N",
#            )

flags = \
    Flags(
        "Logging",
        Flag(
            "--log-file-prefix",
            default = "script.log",
            metavar = "PREFIX",
            help    = "file logging will write to PREFIX.N [script.log]",
            ),
#         Flag(
#             "-v",
#             "--verbosity",
#             default = logging.INFO,
#             metavar = "N",
#             help    = "log messages of at least level N [%default]",
#             ),
        )

class TTY_ConciseFormatter(Formatter):
    """
    A concise log formatter for console output.
    """

    _DATE_FORMAT = "%y%m%d%H%M%S"

    def __init__(self, stream = None):
        """
        Construct this formatter.
        """

        # construct the format string
        format = "%(message)s"

        # initialize this formatter
        Formatter.__init__(self, format, TTY_ConciseFormatter._DATE_FORMAT)

class TTY_VerboseFormatter(Formatter):
    """
    A verbose log formatter for console output.
    """

    _DATE_FORMAT = "%y%m%d%H%M%S"
    _TIME_COLOR  = "\x1b[34m"
    _NAME_COLOR  = "\x1b[35m"
    _LEVEL_COLOR = "\x1b[33m"
    _COLOR_END   = "\x1b[00m"

    def __init__(self, stream = None):
        """
        Construct this formatter.

        Provides colored output if the stream parameter is specified and is an acceptable TTY.
        We print hardwired escape sequences, which will probably break in some circumstances;
        for this unfortunate shortcoming, we apologize.
        """

        # select and construct format string
        import curses

        format = None

        if stream and hasattr(stream, "isatty") and stream.isatty():
            curses.setupterm()

            # FIXME do nice block formatting, increasing column sizes as necessary
            if curses.tigetnum("colors") > 2:
                format = \
                    "%s%%(name)s%s - %s%%(levelname)s%s - %%(message)s" % (
                        TTY_VerboseFormatter._NAME_COLOR,
                        TTY_VerboseFormatter._COLOR_END,
                        TTY_VerboseFormatter._LEVEL_COLOR,
                        TTY_VerboseFormatter._COLOR_END)

        if format is None:
            format = "%(name)s - %(levelname)s - %(message)s"

        # initialize this formatter
        Formatter.__init__(self, format, TTY_VerboseFormatter._DATE_FORMAT)

class VerboseFileFormatter(Formatter):
    """
    A verbose log formatter for file output.
    """

    _FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    _DATE_FORMAT = "%y%m%d%H%M%S"

    def __init__(self):
        """
        Construct this formatter.
        """

        Formatter.__init__(self, VerboseFileFormatter._FORMAT, VerboseFileFormatter._DATE_FORMAT)

class OpaqueFilter(Filter):
    """
    Exclude all records.
    """

    def __init__(self):
        """
        Initialize.
        """

        Filter.__init__(self)

    def filter(self, record):
        """
        Allow no records.
        """

        return False

class ExactExcludeFilter(Filter):
    """
    Exclude records with a specific name.
    """

    def __init__(self, exclude):
        """
        Initialize this filter.
        """

        # members
        self.__exclude = exclude

        # base
        Filter.__init__(self)

    def filter(self, record):
        """
        Allow all records save those with the excluded name.
        """

        if record.name != self.__exclude:
            return True
        else:
            return False

def enable_console(level = logging.NOTSET, verbose = True):
    """
    Enable typical logging to the console.
    """

    import datetime

    handler = StreamHandler(sys.stdout)

    if verbose:
        formatter = TTY_VerboseFormatter
    else:
        formatter = TTY_ConciseFormatter

    handler.setFormatter(formatter(sys.stdout))
    handler.setLevel(level)

    logging.root.addHandler(handler)

    log.debug("enabled logging to stdout at %s", datetime.datetime.today().isoformat())

    return handler

def enable_disk(prefix = None, level = logging.NOTSET):
    """
    Enable typical logging to disk.
    """

    import os.path
    import datetime

    from itertools import count

    # generate an unused log file path
    if prefix is None:
        #prefix = flags.given.log_file_prefix
        prefix = "script.log" # FIXME use the flag

    for i in count():
        path = "%s.%i" % (prefix, i)

        if not os.path.lexists(path):
            break

    # set up logging
    handler = FileHandler(path, encoding = "UTF-8")

    handler.setFormatter(VerboseFileFormatter())
    handler.setLevel(level)

    logging.root.addHandler(handler)

    log.debug("enabled logging to %s at %s", path, datetime.datetime.today().isoformat())

    return handler

# enable logging by default
@run_once
def enable_default_logging(add_handlers = True):
    # by default, be moderately verbose
    logging.root.setLevel(logging.NOTSET) # FIXME should use flag

    # default setup (FIXME which is silly: should enable the disk log via flag or environment)
    if add_handlers:
        if sys.stdout.isatty():
            enable_console()
        else:
            enable_disk()

<|endoftext|>


<commit_before>#! /usr/bin/env python2
"""Script to see if some standard files have changed

A 'change' is any change to size, mtime or md5sum
"""

from __future__ import print_function
import os
import csv
import md5
import sys
import argparse
from datetime import datetime

from bdb import BdbQuit
from dotsite import paths

__version__ = '1.0.0'

def run_args(args, methods):
    """Run any methods eponymous with args"""
    if not args:
        return False
    valuable_args = {k for k, v in args.__dict__.items() if v}
    arg_methods = {methods[a] for a in valuable_args if a in methods}
    for method in arg_methods:
        method(args)

def version(args):
    print('%s %s' % (args, __version__))
    raise SystemExit

def Use_debugger(_args):
    try:
        import pudb as pdb
    except ImportError:
        import pdb
    pdb.set_trace()

def parse_args(methods):
    """Parse out command line arguments"""
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument('-v', '--version', action='store_true',
                        help='Show version')
    parser.add_argument('-U', '--Use_debugger', action='store_true',
                        help='Run the script with pdb (or pudb if available)')
    args = parser.parse_args()
    run_args(args, methods)
    return args

def plastic_date():
    """Looks like a date, quacks like a date. Not a date"""
    return 'Zun, 99 Zun 9999 99:61:61'

class Signature(object):
    """Identifying details for a file

    Includes time, size and md5sum
    """
    def __init__(self, t, z, s):
        self._time = t
        self._size = z
        self._sum = s

    def __str__(self):
        return ', '.join([self._time_str(), str(self._size)])

    def __nonzero__(self):
        return bool(self._time) or self._size > -1 or bool(self._sum)

    def __cmp__(self, other):
        return cmp(self.strings(), other.strings())

    def _time_str(self):
        try:
            if not self._time:
                raise ValueError
            format_ = '%a, %d %b %Y %H:%M:%S'
            return datetime.fromtimestamp(float(self._time)).strftime(format_)
        except ValueError:
            return plastic_date()

    def strings(self):
        return [str(self._time), str(self._size), str(self._sum)]

class EmptySignature(Signature):
    def __init__(self):
        super(EmptySignature, self).__init__(0, -1, '')

def pad_keys(items, keys):
    """Make sure all keys are in items

    If any key is missing add an empty signature"""
    for key in keys:
        if key not in items:
            items[key] = EmptySignature()
    return items

def path_to_data():
    """Where we store old values"""
    return paths.environ_path('JAB') / 'local/login_sums.csv'

def write_files(items):
    """Write items to csv file"""
    with path_to_data().open('wb') as stream:
        writer = csv.writer(stream)
        for k, v in items.iteritems():
            if not v:
                continue
            row = [k] + v.strings()
            writer.writerow(row)

def old_values(basenames):
    """Read old date, size and md5sum for those basenames"""
    result = {}
    with path_to_data().open() as stream:
        reader = csv.reader(stream)
        for row in reader:
            result[row[0]] = Signature(*row[1:])
    return pad_keys(result, basenames)

def new_values(basenames):
    """Get date, size and md5sum for those basenames in $HOME"""
    result = {}
    for basename in basenames:
        p = paths.home() / basename
        if not p.isfile():
            continue
        size = '%d' % p.size
        mtime = '%0.8f' % p.mtime
        m = md5.new()
        m.update(p.text())
        result[basename] = Signature(mtime, size, m.hexdigest())
    return pad_keys(result, basenames)

def script(args):
    """Compare date, size and md5sum for known files in $HOME"""
    basenames = [
        '.bashrc',
        '.vimrc',
        '.gitconfig',
        '.netrc',
        '.profile',
    ]
    old = old_values(basenames)
    new = new_values(basenames)
    result = True
    for basename in basenames:
        if old[basename] == new[basename]:
            continue
        print('%s changed:' % basename)
        print('\tOld: %s' % old[basename])
        print('\tNew: %s' % new[basename])
        result = False
    if not result:
        print('')
        write_files(new)
    return result

def main():
    """Run the script"""
    try:
        args = parse_args(globals())
        return os.EX_OK if script(args) else not os.EX_OK
    except BdbQuit:
        pass
    except SystemExit as e:
        return e.code
    except Exception, e:  # pylint: disable=broad-except
        if __version__[0] < '1':
            raise
        print(e, sys.stderr)
        return not os.EX_OK
    return os.EX_OK

if __name__ == '__main__':
    sys.exit(main())
<commit_msg>Explain non-dunder methods in Signature class<commit_after>#! /usr/bin/env python2
"""Script to see if some standard files have changed

A 'change' is any change to size, mtime or md5sum
"""

from __future__ import print_function
import os
import csv
import md5
import sys
import argparse
from datetime import datetime

from bdb import BdbQuit
from dotsite import paths

__version__ = '1.0.0'

def run_args(args, methods):
    """Run any methods eponymous with args"""
    if not args:
        return False
    valuable_args = {k for k, v in args.__dict__.items() if v}
    arg_methods = {methods[a] for a in valuable_args if a in methods}
    for method in arg_methods:
        method(args)

def version(args):
    print('%s %s' % (args, __version__))
    raise SystemExit

def Use_debugger(_args):
    try:
        import pudb as pdb
    except ImportError:
        import pdb
    pdb.set_trace()

def parse_args(methods):
    """Parse out command line arguments"""
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument('-v', '--version', action='store_true',
                        help='Show version')
    parser.add_argument('-U', '--Use_debugger', action='store_true',
                        help='Run the script with pdb (or pudb if available)')
    args = parser.parse_args()
    run_args(args, methods)
    return args

def plastic_date():
    """Looks like a date, quacks like a date. Not a date"""
    return 'Zun, 99 Zun 9999 99:61:61'

class Signature(object):
    """Identifying details for a file

    Includes time, size and md5sum
    """
    def __init__(self, t, z, s):
        self._time = t
        self._size = z
        self._sum = s

    def __str__(self):
        return ', '.join([self._time_str(), str(self._size)])

    def __nonzero__(self):
        return bool(self._time) or self._size > -1 or bool(self._sum)

    def __cmp__(self, other):
        return cmp(self.strings(), other.strings())

    def _time_str(self):
        """A formatted string for the time"""
        try:
            if not self._time:
                raise ValueError
            format_ = '%a, %d %b %Y %H:%M:%S'
            return datetime.fromtimestamp(float(self._time)).strftime(format_)
        except ValueError:
            return plastic_date()

    def strings(self):
        """A list of time, size and sum as strings"""
        return [str(self._time), str(self._size), str(self._sum)]

class EmptySignature(Signature):
    def __init__(self):
        super(EmptySignature, self).__init__(0, -1, '')

def pad_keys(items, keys):
    """Make sure all keys are in items

    If any key is missing add an empty signature"""
    for key in keys:
        if key not in items:
            items[key] = EmptySignature()
    return items

def path_to_data():
    """Where we store old values"""
    return paths.environ_path('JAB') / 'local/login_sums.csv'

def write_files(items):
    """Write items to csv file"""
    with path_to_data().open('wb') as stream:
        writer = csv.writer(stream)
        for k, v in items.iteritems():
            if not v:
                continue
            row = [k] + v.strings()
            writer.writerow(row)

def old_values(basenames):
    """Read old date, size and md5sum for those basenames"""
    result = {}
    with path_to_data().open() as stream:
        reader = csv.reader(stream)
        for row in reader:
            result[row[0]] = Signature(*row[1:])
    return pad_keys(result, basenames)

def new_values(basenames):
    """Get date, size and md5sum for those basenames in $HOME"""
    result = {}
    for basename in basenames:
        p = paths.home() / basename
        if not p.isfile():
            continue
        size = '%d' % p.size
        mtime = '%0.8f' % p.mtime
        m = md5.new()
        m.update(p.text())
        result[basename] = Signature(mtime, size, m.hexdigest())
    return pad_keys(result, basenames)

def script(args):
    """Compare date, size and md5sum for known files in $HOME"""
    basenames = [
        '.bashrc',
        '.vimrc',
        '.gitconfig',
        '.netrc',
        '.profile',
    ]
    old = old_values(basenames)
    new = new_values(basenames)
    result = True
    for basename in basenames:
        if old[basename] == new[basename]:
            continue
        print('%s changed:' % basename)
        print('\tOld: %s' % old[basename])
        print('\tNew: %s' % new[basename])
        result = False
    if not result:
        print('')
        write_files(new)
    return result

def main():
    """Run the script"""
    try:
        args = parse_args(globals())
        return os.EX_OK if script(args) else not os.EX_OK
    except BdbQuit:
        pass
    except SystemExit as e:
        return e.code
    except Exception, e:  # pylint: disable=broad-except
        if __version__[0] < '1':
            raise
        print(e, sys.stderr)
        return not os.EX_OK
    return os.EX_OK

if __name__ == '__main__':
    sys.exit(main())
<|endoftext|>


<commit_before>#!/usr/bin/python
#
# Copyright (c) 2012 Mikkel Schubert <MSchubert@snm.ku.dk>
#
# 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.
#
import time

from pypeline.atomiccmd import AtomicCmd, CmdError
from pypeline.common.utilities import safe_coerce_to_tuple

class _CommandSet:
    def __init__(self, commands):
        self._commands = safe_coerce_to_tuple(commands)
        if not self._commands:
            raise CmdError("Empty list passed to command set")

    def ready(self):
        return all(cmd.ready() for cmd in self._commands)

    def commit(self, temp):
        for command in self._commands:
            command.commit(temp)

    def _collect_properties(key):
        def _collector(self):
            values = []
            for command in self._commands:
                values.extend(getattr(command, key))
            return values
        return property(_collector)

    input_files     = _collect_properties("input_files")
    output_files    = _collect_properties("output_files")
    auxiliary_files = _collect_properties("auxiliary_files")
    executables     = _collect_properties("executables")
    requirements    = _collect_properties("requirements")

    def __str__(self):
        return "[%s]" % ", ".join(str(command) for command in self._commands)

class ParallelCmds(_CommandSet):
    """This class wraps a set of AtomicCmds, running them in parallel.
    This corresponds to a set of piped commands, which only terminate
    when all parts of the pipe have terminated. For example:
    $ dmesg | grep -i segfault | gzip > log.txt.gz

    Note that only AtomicCmds and ParallelCmds are allowed as
    sub-commands for this class, since the model requires non-
    blocking commands."""

    def __init__(self, commands):
        _CommandSet.__init__(self, commands)

        for command in self._commands:
            if not isinstance(command, (AtomicCmd, ParallelCmds)):
                raise CmdError("ParallelCmds must only contain AtomicCmds or other ParallelCmds!")

    def run(self, temp):
        for command in self._commands:
            command.run(temp)

    def join(self):
        commands = list(enumerate(self._commands))
        return_codes = [[None]] * len(commands)
        while commands:
            for (index, command) in commands:
                if command.ready():
                    return_codes[index] = command.join()
                    commands.remove((index, command))
                elif any(any(codes) for codes in return_codes):
                    command.terminate()
                    return_codes[index] = ["SIGTERM"]
                    commands.remove((index, command))

            time.sleep(1)

        return sum(return_codes, [])

    def terminate(self):
        for command in self._commands:
            command.terminate()

class SequentialCmds(_CommandSet):
    """This class wraps a set of AtomicCmds, running them sequentially.
    This class therefore corresponds a set of lines in a bash script,
    each of which invokes a forground job. For example:
    $ bcftools view snps.bcf | bgzip > snps.vcf.bgz
    $ tabix snps.vcf.bgz

    The list of commands may include any type of command. Note that
    the run function only returns once each sub-command has completed.
    A command is only executed if the previous command in the sequence
    was succesfully completed."""

    def __init__(self, commands):
        _CommandSet.__init__(self, commands)

        for command in self._commands:
            if not isinstance(command, (AtomicCmd, _CommandSet)):
                raise CmdError("ParallelCmds must only contain AtomicCmds or other ParallelCmds!")
        self._ready = False

    def run(self, temp):
        for command in self._commands:
            command.run(temp)
            if any(command.join()):
                break

        self._ready = True

    def ready(self):
        return self._ready

    def join(self):
        return_codes = []
        for command in self._commands:
            return_codes.extend(command.join())

        return return_codes
<commit_msg>Improve handling of failures in ParallelCmds.<commit_after>#!/usr/bin/python
#
# Copyright (c) 2012 Mikkel Schubert <MSchubert@snm.ku.dk>
#
# 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.
#
import time

from pypeline.atomiccmd import AtomicCmd, CmdError
from pypeline.common.utilities import safe_coerce_to_tuple

class _CommandSet:
    def __init__(self, commands):
        self._commands = safe_coerce_to_tuple(commands)
        if not self._commands:
            raise CmdError("Empty list passed to command set")

    def ready(self):
        return all(cmd.ready() for cmd in self._commands)

    def commit(self, temp):
        for command in self._commands:
            command.commit(temp)

    def _collect_properties(key):
        def _collector(self):
            values = []
            for command in self._commands:
                values.extend(getattr(command, key))
            return values
        return property(_collector)

    input_files     = _collect_properties("input_files")
    output_files    = _collect_properties("output_files")
    auxiliary_files = _collect_properties("auxiliary_files")
    executables     = _collect_properties("executables")
    requirements    = _collect_properties("requirements")

    def __str__(self):
        return "[%s]" % ", ".join(str(command) for command in self._commands)

class ParallelCmds(_CommandSet):
    """This class wraps a set of AtomicCmds, running them in parallel.
    This corresponds to a set of piped commands, which only terminate
    when all parts of the pipe have terminated. For example:
    $ dmesg | grep -i segfault | gzip > log.txt.gz

    Note that only AtomicCmds and ParallelCmds are allowed as
    sub-commands for this class, since the model requires non-
    blocking commands."""

    def __init__(self, commands):
        _CommandSet.__init__(self, commands)

        for command in self._commands:
            if not isinstance(command, (AtomicCmd, ParallelCmds)):
                raise CmdError("ParallelCmds must only contain AtomicCmds or other ParallelCmds!")
        self._joinable = False

    def run(self, temp):
        for command in self._commands:
            command.run(temp)
        self._joinable = True

    def join(self):
        commands = list(enumerate(self._commands))
        return_codes = [[None]] * len(commands)
        while commands and self._joinable:
            for (index, command) in commands:
                if command.ready():
                    return_codes[index] = command.join()
                    commands.remove((index, command))
                elif any(any(codes) for codes in return_codes):
                    command.terminate()
                    return_codes[index] = ["SIGTERM"]
                    commands.remove((index, command))

            time.sleep(1)
        return sum(return_codes, [])

    def terminate(self):
        for command in self._commands:
            command.terminate()

class SequentialCmds(_CommandSet):
    """This class wraps a set of AtomicCmds, running them sequentially.
    This class therefore corresponds a set of lines in a bash script,
    each of which invokes a forground job. For example:
    $ bcftools view snps.bcf | bgzip > snps.vcf.bgz
    $ tabix snps.vcf.bgz

    The list of commands may include any type of command. Note that
    the run function only returns once each sub-command has completed.
    A command is only executed if the previous command in the sequence
    was succesfully completed."""

    def __init__(self, commands):
        _CommandSet.__init__(self, commands)

        for command in self._commands:
            if not isinstance(command, (AtomicCmd, _CommandSet)):
                raise CmdError("ParallelCmds must only contain AtomicCmds or other ParallelCmds!")
        self._ready = False

    def run(self, temp):
        for command in self._commands:
            command.run(temp)
            if any(command.join()):
                break

        self._ready = True

    def ready(self):
        return self._ready

    def join(self):
        return_codes = []
        for command in self._commands:
            return_codes.extend(command.join())

        return return_codes
<|endoftext|>


<commit_before>from __future__ import division, unicode_literals

"""
This module implements the workflow to design new materials.
"""

__author__ = "Chuck-Hou Yee"
__copyright__ = "Copyright 2015, The Correlated Materials Laboratory"
__version__ = "0.1"
__maintainer__ = "Chuck-Hou Yee"
__email__ = "chuckyee@physics.rutgers.edu"
__status__ = "Development"
__date__ = "Apr 21, 2015"

import six
from six.moves import filter, map, zip

import pymatgen as mp
from pymatgen.core.composition import Composition
from pymatgen.core.periodic_table import PeriodicTable, Element

if __name__ == '__main__':
    # Find all binary compositions AX2 which have 2- charge.
    #   Example: (HgSe2)Ba2CuO2

    pt = PeriodicTable()
    elements = pt.all_elements

    possible_compositions = []
    for A in elements:
        for X in elements:
            possible_oxidations = set()
            for oxi_A in A.oxidation_states:
                for oxi_X in X.oxidation_states:
                    possible_oxidations.add(oxi_A + 2*oxi_X)
            if -2 in possible_oxidations:
                possible_compositions.append(Composition(A.symbol + X.symbol + "2"))
    print possible_compositions
<commit_msg>First implementation of generate_compositions_ns_by_oxidation.<commit_after>from __future__ import division, print_function, unicode_literals

"""
This module implements the workflow to design new materials.
"""

__author__ = "Chuck-Hou Yee"
__copyright__ = "Copyright 2015, Correlated Materials Laboratory"
__version__ = "0.1"
__maintainer__ = "Chuck-Hou Yee"
__email__ = "chuckyee@physics.rutgers.edu"
__status__ = "Development"
__date__ = "Apr 21, 2015"

import six
from six.moves import filter, map, zip

import pymatgen as mp
from pymatgen.core.composition import Composition
from pymatgen.core.periodic_table import PeriodicTable, Element

def __composition_generator(partial_composition, partial_oxidation,
                            abstract_composition, total_oxidation_state):
    pt = PeriodicTable()

    num_elements = len(abstract_composition.elements)
    abstract_element = abstract_composition.elements[0]
    abstract_stoichiometry = abstract_composition[abstract_element]

    possible_compositions = set()
    if num_elements == 1:
        for X in pt.all_elements:
            possible_oxidations = set()
            for oxi_state in X.oxidation_states:
                possible_oxidations.add(partial_oxidation + oxi_state * abstract_stoichiometry)
            if total_oxidation_state in possible_oxidations:
                this_composition = partial_composition + Composition({X.symbol: abstract_stoichiometry})
                if partial_composition.num_atoms == 0 or (partial_composition.num_atoms > 0 and not this_composition.is_element):
                    possible_compositions.add(this_composition)
    else:
        new_abstract_composition = abstract_composition - Composition({abstract_element: abstract_stoichiometry})
        for X in pt.all_elements:
            new_composition = partial_composition + Composition({X.symbol: abstract_stoichiometry})
            for oxi_state in X.oxidation_states:
                new_compositions = __composition_generator(new_composition,
                                                           partial_oxidation + oxi_state*abstract_stoichiometry,
                                                           new_abstract_composition,
                                                           total_oxidation_state)
                possible_compositions = possible_compositions.union(new_compositions)
    return possible_compositions

def generate_compositions_by_oxidation(composition, constraints = None,
                                       total_oxidation_state = 0,
                                       only_common_oxidation_states = True):
    """
    Generate all possible realizations of a given abstract composition formula.

    Args:
        composition: abstract composition. Ex: Composition("ABC3")
        constraints: dictionary of functions specifying constraints on
            oxidation states (or anything else, for that matter). Ex:
            {"A": lambda el: el.oxidation_state < 0}
        total_oxidation_state: constraint on total charge of composition,
            defaults to 0 (neutral).
        only_common_oxidation_states: whether to include rare oxidation states
            of elements

    Returns:
        A set of compositions satisfying specified constraints.
    """
    composition = Composition(composition)
    num_elements = len(composition.elements)
    if num_elements == 0:
        compositions = set()
    else:
        compositions = __composition_generator(Composition(), 0, composition, total_oxidation_state)
    return compositions

def test_binary():
    pt = PeriodicTable()
    elements = pt.all_elements

    common_oxidations = False
    stoichiometry_A = 1
    stoichiometry_X = 1
    total_oxidation = 0

    possible_compositions = set()
    for A in elements:
        for X in elements:
            possible_oxidations = set()
            oxidations_A = A.common_oxidation_states if common_oxidations else A.oxidation_states
            oxidations_X = X.common_oxidation_states if common_oxidations else X.oxidation_states
            for oxi_A in oxidations_A:
                for oxi_X in oxidations_X:
                    possible_oxidations.add(oxi_A*stoichiometry_A + oxi_X*stoichiometry_X)
            if total_oxidation in possible_oxidations:
                possible_compositions.add(Composition({A.symbol: stoichiometry_A, X.symbol: stoichiometry_X}))

    return possible_compositions

if __name__ == '__main__':
    # Find all binary compositions AX2 which have 2- charge.
    #   Example: (HgSe2)Ba2CuO2

    for comp in generate_compositions_by_oxidation("AX"):
        print(comp.reduced_formula)

#     for comp in test_binary():
#         print(comp.reduced_formula)
<|endoftext|>


<commit_before>#! /usr/bin/env python2
"""Script to see if some standard files have changed

A 'change' is any change to size, mtime or md5sum
"""

from __future__ import print_function
import os
import csv
import md5
import sys
import argparse
from datetime import datetime

from bdb import BdbQuit
from dotsite import paths

__version__ = '1.0.0'

def run_args(args, methods):
    """Run any methods eponymous with args"""
    if not args:
        return False
    valuable_args = {k for k, v in args.__dict__.items() if v}
    arg_methods = {methods[a] for a in valuable_args if a in methods}
    for method in arg_methods:
        method(args)

def version(args):
    print('%s %s' % (args, __version__))
    raise SystemExit

def Use_debugger(_args):
    try:
        import pudb as pdb
    except ImportError:
        import pdb
    pdb.set_trace()

def parse_args(methods):
    """Parse out command line arguments"""
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument('-v', '--version', action='store_true',
                        help='Show version')
    parser.add_argument('-U', '--Use_debugger', action='store_true',
                        help='Run the script with pdb (or pudb if available)')
    args = parser.parse_args()
    run_args(args, methods)
    return args

def plastic_date():
    """Looks like a date, quacks like a date. Not a date"""
    return 'Zun, 99 Zun 9999 99:61:61'

class Signature(object):
    """Identifying details for a file

    Includes time, size and md5sum
    """
    def __init__(self, t, z, s):
        self._time = t
        self._size = z
        self._sum = s

    def __str__(self):
        return ', '.join([self._time_str(), str(self._size)])

    def __nonzero__(self):
        return bool(self._time) or self._size > -1 or bool(self._sum)

    def __cmp__(self, other):
        return cmp(self.strings(), other.strings())

    def _time_str(self):
        """A formatted string for the time"""
        try:
            if not self._time:
                raise ValueError
            format_ = '%a, %d %b %Y %H:%M:%S'
            return datetime.fromtimestamp(float(self._time)).strftime(format_)
        except ValueError:
            return plastic_date()

    def strings(self):
        """A list of time, size and sum as strings"""
        return [str(self._time), str(self._size), str(self._sum)]

class EmptySignature(Signature):
    def __init__(self):
        super(EmptySignature, self).__init__(0, -1, '')

def pad_keys(items, keys):
    """Make sure all keys are in items

    If any key is missing add an empty signature"""
    for key in keys:
        if key not in items:
            items[key] = EmptySignature()
    return items

def path_to_data():
    """Where we store old values"""
    return paths.environ_path('JAB') / 'local/login_sums.csv'

def write_files(items):
    """Write items to csv file"""
    with path_to_data().open('wb') as stream:
        writer = csv.writer(stream)
        for k, v in items.iteritems():
            if not v:
                continue
            row = [k] + v.strings()
            writer.writerow(row)

def old_values(basenames):
    """Read old date, size and md5sum for those basenames"""
    result = {}
    with path_to_data().open() as stream:
        reader = csv.reader(stream)
        for row in reader:
            result[row[0]] = Signature(*row[1:])
    return pad_keys(result, basenames)

def new_values(basenames):
    """Get date, size and md5sum for those basenames in $HOME"""
    result = {}
    for basename in basenames:
        p = paths.home() / basename
        if not p.isfile():
            continue
        size = '%d' % p.size
        mtime = '%0.8f' % p.mtime
        m = md5.new()
        m.update(p.text())
        result[basename] = Signature(mtime, size, m.hexdigest())
    return pad_keys(result, basenames)

def script(args):
    """Compare date, size and md5sum for known files in $HOME"""
    basenames = [
        '.bashrc',
        '.vimrc',
        '.gitconfig',
        '.netrc',
        '.profile',
    ]
    old = old_values(basenames)
    new = new_values(basenames)
    result = True
    for basename in basenames:
        if old[basename] == new[basename]:
            continue
        print('%s changed:' % basename)
        print('\tOld: %s' % old[basename])
        print('\tNew: %s' % new[basename])
        result = False
    if not result:
        print('')
        write_files(new)
    return result

def main():
    """Run the script"""
    try:
        args = parse_args(globals())
        return os.EX_OK if script(args) else not os.EX_OK
    except BdbQuit:
        pass
    except SystemExit as e:
        return e.code
    except Exception, e:  # pylint: disable=broad-except
        if __version__[0] < '1':
            raise
        print(e, sys.stderr)
        return not os.EX_OK
    return os.EX_OK

if __name__ == '__main__':
    sys.exit(main())
<commit_msg>Add arg to specify a path to be written<commit_after>#! /usr/bin/env python2
"""Script to see if some standard files have changed

A 'change' is any change to size, mtime or md5sum
"""

from __future__ import print_function
import os
import csv
import md5
import sys
import argparse
from datetime import datetime

from bdb import BdbQuit
from dotsite import paths

__version__ = '1.0.0'

def run_args(args, methods):
    """Run any methods eponymous with args"""
    if not args:
        return False
    valuable_args = {k for k, v in args.__dict__.items() if v}
    arg_methods = {methods[a] for a in valuable_args if a in methods}
    for method in arg_methods:
        method(args)

def version(args):
    print('%s %s' % (args, __version__))
    raise SystemExit

def Use_debugger(_args):
    try:
        import pudb as pdb
    except ImportError:
        import pdb
    pdb.set_trace()

def parse_args(methods):
    """Parse out command line arguments"""
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument('-v', '--version', action='store_true',
                        help='Show version')
    parser.add_argument('-U', '--Use_debugger', action='store_true',
                        help='Run the script with pdb (or pudb if available)')
    args = parser.parse_args()
    run_args(args, methods)
    return args

def plastic_date():
    """Looks like a date, quacks like a date. Not a date"""
    return 'Zun, 99 Zun 9999 99:61:61'

class Signature(object):
    """Identifying details for a file

    Includes time, size and md5sum
    """
    def __init__(self, t, z, s):
        self._time = t
        self._size = z
        self._sum = s

    def __str__(self):
        return ', '.join([self._time_str(), str(self._size)])

    def __nonzero__(self):
        return bool(self._time) or self._size > -1 or bool(self._sum)

    def __cmp__(self, other):
        return cmp(self.strings(), other.strings())

    def _time_str(self):
        """A formatted string for the time"""
        try:
            if not self._time:
                raise ValueError
            format_ = '%a, %d %b %Y %H:%M:%S'
            return datetime.fromtimestamp(float(self._time)).strftime(format_)
        except ValueError:
            return plastic_date()

    def strings(self):
        """A list of time, size and sum as strings"""
        return [str(self._time), str(self._size), str(self._sum)]

class EmptySignature(Signature):
    def __init__(self):
        super(EmptySignature, self).__init__(0, -1, '')

def pad_keys(items, keys):
    """Make sure all keys are in items

    If any key is missing add an empty signature"""
    for key in keys:
        if key not in items:
            items[key] = EmptySignature()
    return items

def path_to_data():
    """Where we store old values"""
    return paths.environ_path('JAB') / 'local/login_sums.csv'

def write_files(items, path):
    """Write items to csv file"""
    with path.open('wb') as stream:
        writer = csv.writer(stream)
        for k, v in items.iteritems():
            if not v:
                continue
            row = [k] + v.strings()
            writer.writerow(row)

def old_values(basenames):
    """Read old date, size and md5sum for those basenames"""
    result = {}
    with path_to_data().open() as stream:
        reader = csv.reader(stream)
        for row in reader:
            result[row[0]] = Signature(*row[1:])
    return pad_keys(result, basenames)

def new_values(basenames):
    """Get date, size and md5sum for those basenames in $HOME"""
    result = {}
    for basename in basenames:
        p = paths.home() / basename
        if not p.isfile():
            continue
        size = '%d' % p.size
        mtime = '%0.8f' % p.mtime
        m = md5.new()
        m.update(p.text())
        result[basename] = Signature(mtime, size, m.hexdigest())
    return pad_keys(result, basenames)

def script(args):
    """Compare date, size and md5sum for known files in $HOME"""
    basenames = [
        '.bashrc',
        '.vimrc',
        '.gitconfig',
        '.netrc',
        '.profile',
    ]
    old = old_values(basenames)
    new = new_values(basenames)
    result = True
    for basename in basenames:
        if old[basename] == new[basename]:
            continue
        print('%s changed:' % basename)
        print('\tOld: %s' % old[basename])
        print('\tNew: %s' % new[basename])
        result = False
    if not result:
        print('')
        write_files(new, path_to_data())
    return result

def main():
    """Run the script"""
    try:
        args = parse_args(globals())
        return os.EX_OK if script(args) else not os.EX_OK
    except BdbQuit:
        pass
    except SystemExit as e:
        return e.code
    except Exception, e:  # pylint: disable=broad-except
        if __version__[0] < '1':
            raise
        print(e, sys.stderr)
        return not os.EX_OK
    return os.EX_OK

if __name__ == '__main__':
    sys.exit(main())
<|endoftext|>


<commit_before>from math import pi
import cmath

import numpy as np
try:
    import cairocffi as cairo
except ImportError:
    import cairo

from pyqis.label import create_label

# we use the cielab color space so each hue has the same perceived intensity.
def cielchToRGB(l, c, h):
    # l and c go from 0 to 100
    # h is an angle in radians

    # based on http://www.easyrgb.com/index.php?X=MATH

    # convert to XYZ
    y = (l + 16) / 116.
    x = c * np.cos(h) / 500. + y
    z = y - c * np.sin(h) / 200.
    mp = lambda y: y * y * y if y > 0.206893 else (y - 16 / 116.) / 7.787
    y = mp(y)
    x = mp(x) * 0.95047
    z = mp(z) * 1.08883

    # convert to RGB
    r = x *  3.2406 + y * -1.5372 + z * -0.4986
    g = x * -0.9689 + y *  1.8758 + z *  0.0415
    b = x *  0.0557 + y * -0.2040 + z *  1.0570
    mp = lambda r: 1.055 * (r ** (1 / 2.4)) - 0.055 if r > 0.0031308 else 12.92 * r
    r = mp(r)
    g = mp(g)
    b = mp(b)

    # ensure it's in bounds and scale it to be between 0 and 255
    m = lambda v: max((min((1., v)), 0.))
    return (m(r), m(g), m(b))

def render_state(state, png_file=None, svg_file=None, use_color=True, label_format=None, states_per_row=4):
    """

    `svg_file` and `png_file` can be either a filename or a file object.
    """
    if png_file is None and svg_file is None:
        # there is nothing to do!
        return

    vec = state.state / np.linalg.norm(state.state)
    basis_size = 1 << state.nqubits
    labels = [create_label(i, state.nqubits, label_format)
              for i in range(basis_size)]

    # Determine dimensions of image
    if states_per_row <= 0 or (states_per_row & (states_per_row - 1)) != 0:
        raise RuntimeError("`states_per_row` must be a power of two.")
    rows = (basis_size - 1) // states_per_row + 1
    cols = min([basis_size, states_per_row])

    hpadding = 10
    vpadding = 10
    label_height = 30
    img_width = cols * 100 + (cols - 1) * hpadding
    img_height = rows * (100 + label_height) + (rows - 1) * vpadding

    s = cairo.SVGSurface(svg_file, img_width, img_height)
    c = cairo.Context(s)

    for i, label in enumerate(labels):
        row = i // states_per_row
        col = i % states_per_row

        radius, phi = cmath.polar(vec[i])

        c.save()
        ctrx = col * (100 + hpadding) + 50
        ctry = row * (100 + vpadding + label_height) + 50
        c.translate(ctrx, ctry)

        if radius > 1e-6:
            c.save()
            c.scale(3.2 * radius, 3.2 * radius)
            c.rotate(-phi)

            # box
            c.save()
            c.rectangle(-9, -9, 18, 18)
            c.set_line_width(2.)
            r, g, b = cielchToRGB(80, 35, phi)
            c.set_source_rgba(r, g, b, 0.8)
            c.fill_preserve()
            c.set_source_rgb(0, 0, 0)
            c.stroke()
            c.restore()

            # arrow
            c.save()
            c.move_to(0, -8)
            c.line_to(5, -3)
            c.line_to(1, -4)
            c.line_to(1, 8)
            c.line_to(-1, 8)
            c.line_to(-1, -4)
            c.line_to(-5, -3)
            c.close_path()
            c.set_source_rgba(0, 0, 0)
            c.fill()
            c.restore()

            c.restore()

        # label
        c.set_font_size(14)
        text_width, text_height = c.text_extents(label)[2:4]
        c.move_to(-text_width / 2., 50 + 10 + text_height)
        c.show_text(label)

        c.restore()

    # Save as a SVG and PNG
    if png_file is not None:
        s.write_to_png(png_file)
    s.finish()
<commit_msg>removed unnecessary import; formatting changes; docstring fix<commit_after>import cmath

import numpy as np
try:
    import cairocffi as cairo
except ImportError:
    import cairo

from pyqis.label import create_label

# we use the cielab color space so each hue has the same perceived intensity.
def cielchToRGB(l, c, h):
    # l and c go from 0 to 100
    # h is an angle in radians

    # based on http://www.easyrgb.com/index.php?X=MATH

    # convert to XYZ
    y = (l + 16) / 116.
    x = c * np.cos(h) / 500. + y
    z = y - c * np.sin(h) / 200.
    mp = lambda y: y * y * y if y > 0.206893 else (y - 16 / 116.) / 7.787
    y = mp(y)
    x = mp(x) * 0.95047
    z = mp(z) * 1.08883

    # convert to RGB
    r = x *  3.2406 + y * -1.5372 + z * -0.4986
    g = x * -0.9689 + y *  1.8758 + z *  0.0415
    b = x *  0.0557 + y * -0.2040 + z *  1.0570
    mp = lambda r: 1.055 * (r ** (1 / 2.4)) - 0.055 if r > 0.0031308 else 12.92 * r
    r = mp(r)
    g = mp(g)
    b = mp(b)

    # ensure it's in bounds and scale it to be between 0 and 255
    m = lambda v: max((min((1., v)), 0.))
    return (m(r), m(g), m(b))

def render_state(state, png_file=None, svg_file=None,
                 use_color=True, label_format=None, states_per_row=4):
    """Render a state to either png or svg format (or both)

    `svg_file` and `png_file` can be either a filename or a binary file object.
    """
    if png_file is None and svg_file is None:
        # there is nothing to do!
        return

    vec = state.state / np.linalg.norm(state.state)
    basis_size = 1 << state.nqubits
    labels = [create_label(i, state.nqubits, label_format)
              for i in range(basis_size)]

    # Determine dimensions of image
    if states_per_row <= 0 or (states_per_row & (states_per_row - 1)) != 0:
        raise RuntimeError("`states_per_row` must be a power of two.")
    rows = (basis_size - 1) // states_per_row + 1
    cols = min([basis_size, states_per_row])

    hpadding = 10
    vpadding = 10
    label_height = 30
    img_width = cols * 100 + (cols - 1) * hpadding
    img_height = rows * (100 + label_height) + (rows - 1) * vpadding

    s = cairo.SVGSurface(svg_file, img_width, img_height)
    c = cairo.Context(s)

    for i, label in enumerate(labels):
        row = i // states_per_row
        col = i % states_per_row

        radius, phi = cmath.polar(vec[i])

        c.save()
        ctrx = col * (100 + hpadding) + 50
        ctry = row * (100 + vpadding + label_height) + 50
        c.translate(ctrx, ctry)

        if radius > 1e-6:
            c.save()
            c.scale(3.2 * radius, 3.2 * radius)
            c.rotate(-phi)

            # box
            c.save()
            c.rectangle(-9, -9, 18, 18)
            c.set_line_width(2.)
            r, g, b = cielchToRGB(80, 35, phi)
            c.set_source_rgba(r, g, b, 0.8)
            c.fill_preserve()
            c.set_source_rgb(0, 0, 0)
            c.stroke()
            c.restore()

            # arrow
            c.save()
            c.move_to(0, -8)
            c.line_to(5, -3)
            c.line_to(1, -4)
            c.line_to(1, 8)
            c.line_to(-1, 8)
            c.line_to(-1, -4)
            c.line_to(-5, -3)
            c.close_path()
            c.set_source_rgba(0, 0, 0)
            c.fill()
            c.restore()

            c.restore()

        # label
        c.set_font_size(14)
        text_width, text_height = c.text_extents(label)[2:4]
        c.move_to(-text_width / 2., 50 + 10 + text_height)
        c.show_text(label)

        c.restore()

    # Save as a SVG and PNG
    if png_file is not None:
        s.write_to_png(png_file)
    s.finish()
<|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#The MIT License (MIT)
#
#Copyright (c) <2013> <Colin Duquesnoy and others, see AUTHORS.txt>
#
#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.
#
"""
Create an abstraction layer over the qt bindings to use.

Requirements
---------------
  - PyQt4 or PySide
  - For PyQt4, you must use the sip API version 2 for QString and QVariant
    otherwise you will be purchased by fanatic vampire killer bees on a deadly
    minefield... :(

Force a specific Qt bindings
-----------------------------

From the command line using the **--PyQt** or **--PySide** options ::

    python pyqode_app.py --PyQt

By settings the QT_API environment variable to "PyQt" or "PySide" at the first
line of your main script::

    import os
    os.environ.setdefault("QT_API", "PyQt")

If nothing is found in sys.arg or in the environment variables, pyQode will try
to detect which API has already been imported by the client code
(be aware of the vampire killer bees if you use PyQt4).

pyQode will choose PyQt if nothing has been specified or imported and if PyQt
is available.

If no qt bindings were found a RuntimeError is raised.

Usage
------------------------
To access the Qt bindings you just need to import pyqode and prefix your
Qt classes by QtGui. ::

    import os
    import sys
    import pyqode

    app = QtGui.QApplication(sys.argv)
    editor = pyqode.QCodeEdit()
    # show the QT api in the window title
    editor.setWindowTitle(os.environ["QT_API"])
    editor.show()
    app.exec_()
"""
import logging
__logger = logging.getLogger("pyqode")
import os
import sys

def read_args():
    """
    Safely read command line args

    :return: sys.argv or empty list
    """
    try:
        argv = sys.argv
    except:
        argv = []
    return argv

def read_env():
    """
    Safely read QT_API env var

    :return: QT_API value
    """
    try:
        from_env = os.environ["QT_API"]
    except KeyError:
        from_env = ""
    return from_env

def try_pyside():
    """
    Tries to import pyside.

    :return: Return True if pyside can be used
    """
    try:
        import PySide
    except ImportError:
        # Fatal error, no qt bindings found
        __logger.critical("PySide not found!")
        return False
    else:
        os.environ.setdefault("QT_API", "PySide")
        return True

def setup_apiv2():
    """
    Setup apiv2 on the chose binding.
    """
    # setup PyQt api to version 2
    if sys.version_info[0] == 2:
        import sip
        try:
            sip.setapi("QString", 2)
            sip.setapi("QVariant", 2)
        except:
            __logger.critical("pyQode: failed to set PyQt api to version 2"
                              "\nTo solve this problem, import "
                              "pyqode before any other PyQt modules "
                              "in your main script...")

def try_pyqt():
    """
    Tries to import pyqt and setup sip api v2 in case of success

    :return: Return True if pyqt can be used
    """
    try:
        import PyQt4
    except ImportError:
        # Fatal error, no qt bindings found
        __logger.critical("PyQt not found!")
        return False
    else:
        os.environ.setdefault("QT_API", "PyQt")
        setup_apiv2()
        return True

def select():
    """
    Selects a qt bindings. The selected qt bindings is set to the
    QT_API environment variable.

    :return: True if a binding could be selected.
    """
    argv = read_args()
    env = read_env()
    if "PyQt4" in sys.modules or "--PyQt" in argv or env == "PyQt":
        os.environ.setdefault("QT_API", "PyQt")
        return try_pyqt()
    elif "PySide" in sys.modules or "--PySide" in argv or env == "PySide":
        os.environ.setdefault("QT_API", "PySide")
        return try_pyside()
    else:
        __logger.warning("No qt bindings specified, pyqode will try to pick one "
                         "on its own")
        if not try_pyqt():
            __logger.warning("Cannot use PyQt, will try PySide")
            return try_pyside()
        __logger.warning("The chosen qt binding is %s" % os.environ["QT_API"])
        return True

#
# Select a qt binding
#
if not select():
    __logger.critical("Failed to find a qt bindings, please install "
                      "PyQt or PySide to user pyqode. Returning with "
                      "error code.")
    raise RuntimeError("No Qt bindings found")
    #sys.exit(-1)
# __logger.debug("Using %s" % os.environ["QT_API"])

from pyqode.qt import QtCore, QtGui
__all__ = ["QtCore", "QtGui"]
<commit_msg>#25 Forget about sip mocking<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#The MIT License (MIT)
#
#Copyright (c) <2013> <Colin Duquesnoy and others, see AUTHORS.txt>
#
#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.
#
"""
Create an abstraction layer over the qt bindings to use.

Requirements
---------------
  - PyQt4 or PySide
  - For PyQt4, you must use the sip API version 2 for QString and QVariant
    otherwise you will be purchased by fanatic vampire killer bees on a deadly
    minefield... :(

Force a specific Qt bindings
-----------------------------

From the command line using the **--PyQt** or **--PySide** options ::

    python pyqode_app.py --PyQt

By settings the QT_API environment variable to "PyQt" or "PySide" at the first
line of your main script::

    import os
    os.environ.setdefault("QT_API", "PyQt")

If nothing is found in sys.arg or in the environment variables, pyQode will try
to detect which API has already been imported by the client code
(be aware of the vampire killer bees if you use PyQt4).

pyQode will choose PyQt if nothing has been specified or imported and if PyQt
is available.

If no qt bindings were found a RuntimeError is raised.

Usage
------------------------
To access the Qt bindings you just need to import pyqode and prefix your
Qt classes by QtGui. ::

    import os
    import sys
    import pyqode

    app = QtGui.QApplication(sys.argv)
    editor = pyqode.QCodeEdit()
    # show the QT api in the window title
    editor.setWindowTitle(os.environ["QT_API"])
    editor.show()
    app.exec_()
"""
import logging
__logger = logging.getLogger("pyqode")
import os
import sys

def read_args():
    """
    Safely read command line args

    :return: sys.argv or empty list
    """
    try:
        argv = sys.argv
    except:
        argv = []
    return argv

def read_env():
    """
    Safely read QT_API env var

    :return: QT_API value
    """
    try:
        from_env = os.environ["QT_API"]
    except KeyError:
        from_env = ""
    return from_env

def try_pyside():
    """
    Tries to import pyside.

    :return: Return True if pyside can be used
    """
    try:
        import PySide
    except ImportError:
        # Fatal error, no qt bindings found
        __logger.critical("PySide not found!")
        return False
    else:
        os.environ.setdefault("QT_API", "PySide")
        return True

def setup_apiv2():
    """
    Setup apiv2 on the chose binding.
    """
    # setup PyQt api to version 2
    if sys.version_info[0] == 2:
        try:
            import sip
            sip.setapi("QString", 2)
            sip.setapi("QVariant", 2)
        except:
            __logger.critical("pyQode: failed to set PyQt api to version 2"
                              "\nTo solve this problem, import "
                              "pyqode before any other PyQt modules "
                              "in your main script...")

def try_pyqt():
    """
    Tries to import pyqt and setup sip api v2 in case of success

    :return: Return True if pyqt can be used
    """
    try:
        import PyQt4
    except ImportError:
        # Fatal error, no qt bindings found
        __logger.critical("PyQt not found!")
        return False
    else:
        os.environ.setdefault("QT_API", "PyQt")
        setup_apiv2()
        return True

def select():
    """
    Selects a qt bindings. The selected qt bindings is set to the
    QT_API environment variable.

    :return: True if a binding could be selected.
    """
    argv = read_args()
    env = read_env()
    if "PyQt4" in sys.modules or "--PyQt" in argv or env == "PyQt":
        os.environ.setdefault("QT_API", "PyQt")
        return try_pyqt()
    elif "PySide" in sys.modules or "--PySide" in argv or env == "PySide":
        os.environ.setdefault("QT_API", "PySide")
        return try_pyside()
    else:
        __logger.warning("No qt bindings specified, pyqode will try to pick one "
                         "on its own")
        if not try_pyqt():
            __logger.warning("Cannot use PyQt, will try PySide")
            return try_pyside()
        __logger.warning("The chosen qt binding is %s" % os.environ["QT_API"])
        return True

#
# Select a qt binding
#
if not select():
    __logger.critical("Failed to find a qt bindings, please install "
                      "PyQt or PySide to user pyqode. Returning with "
                      "error code.")
    raise RuntimeError("No Qt bindings found")
    #sys.exit(-1)
# __logger.debug("Using %s" % os.environ["QT_API"])

from pyqode.qt import QtCore, QtGui
__all__ = ["QtCore", "QtGui"]
<|endoftext|>


<commit_before># coding: utf-8
"""
Main pyramlson decorators: api_service and api_method
"""
import re
import os
import logging

from email.utils import parsedate
from inspect import getmembers
from collections import namedtuple, defaultdict

import venusian

from pyramid.path import (
    AssetResolver,
    DottedNameResolver
)
from pyramid.httpexceptions import (
    HTTPBadRequest,
    HTTPInternalServerError,
    HTTPNoContent,
)
from pyramid.interfaces import IExceptionResponse
from pyramid.settings import asbool

from .apidef import IRamlApiDefinition
from .utils import (
    prepare_json_body,
    render_view,
    validate_and_convert
)

LOG = logging.getLogger(__name__)

DEFAULT_METHOD_MAP = {
    'get': 200,
    'post': 200,
    'put': 201,
    'delete': 204,
    'options': 200,
    'patch': 200,
}

MethodRestConfig = namedtuple('MethodRestConfig', [
    'http_method',
    'permission',
    'returns'
])

MARKER = object()

class api_method(object):
    # pylint: disable=invalid-name

    def __init__(self, http_method, permission=None, returns=None):
        """Configure a resource method corresponding with a RAML resource path

        This decorator must be used to declare REST resources.

        :param http_method: The HTTP method this method maps to.

        :param permission: Permission for this method.

        :param returns: A custom HTTP code to return in case of success.

            Configure the HTTP code to return when the method call was successful.
            Per default the codes are expected to match the configured HTTP method:
                - GET/POST/PATCH: 200
                - PUT: 201
                - DELETE: 204

        """
        self.http_method = http_method
        self.permission = permission
        self.returns = returns if returns is not None else DEFAULT_METHOD_MAP[self.http_method]

    def __call__(self, method):
        method._rest_config = MethodRestConfig(
            self.http_method,
            self.permission,
            self.returns
        )
        return method

class api_service(object):
    """Configures a resource by its REST path.

    This decorator configures a class as a REST resource. All endpoints
    must be defined in a RAML file.
    """
    # pylint: disable=invalid-name

    def __init__(self, resource_path):
        LOG.debug("Resource path: %s", resource_path)
        self.resource_path = resource_path
        self.resources = []
        self.apidef = None
        self.cls = None
        self.module = None

    def callback(self, scanner, name, cls):
        config = scanner.config.with_package(self.module)
        self.apidef = config.registry.queryUtility(IRamlApiDefinition)
        self.create_route(config)
        LOG.debug("registered routes with base route '%s'", self.apidef.base_path)
        self.create_views(config)

    def create_route(self, config):
        LOG.debug("Creating route for %s", self.resource_path)
        supported_methods = []
        route_name = None

        # Find all methods for this resource path
        for resource in self.apidef.get_resources(self.resource_path):
            if route_name is None:
                path = self.resource_path
                if self.apidef.base_path:
                    path = "{}{}".format(self.apidef.base_path, path)
                route_name = "{}-{}".format(resource.display_name, path)

            method = resource.method.upper()
            self.resources.append((route_name, method, resource, None))
            supported_methods.append(method)

        # Add one route for all the methods at this resource path
        if supported_methods:
            LOG.debug("Registering route with path %s", path)
            config.add_route(route_name, path, factory=self.cls)
            # add a default OPTIONS view if none was defined by the resource
            opts_meth = 'OPTIONS'
            if opts_meth not in supported_methods:
                methods = supported_methods + [opts_meth]
                self.resources.append((
                    route_name,
                    'OPTIONS',
                    resource,
                    create_options_view(methods)
                ))

    def create_views(self, config):
        for (route_name, method, resource, default_view) in self.resources:
            LOG.debug("Creating view %s %s", route_name, method)
            if default_view:
                config.add_view(
                    default_view,
                    route_name=route_name,
                    request_method=method
                )
            else:
                (view, permission) = self.create_view(resource)
                LOG.debug(
                    "Registering view %s for route name '%s', resource '%s', method '%s'",
                    view,
                    route_name,
                    resource,
                    method
                )
                config.add_view(
                    view,
                    route_name=route_name,
                    request_method=method,
                    permission=permission
                )

    def __call__(self, cls):
        self.cls = cls
        info = venusian.attach(cls, self.callback, 'pyramid', depth=1)
        self.module = info.module
        return cls

    def create_view(self, resource):
        (meth, cfg) = self.get_service_class_method(resource)
        LOG.debug("Got method %s for resource %s", meth, resource)
        if not meth:
            msg = "Could not find a method in class {} suitable for resource {}.".format(
                self.cls,
                resource
            )
            raise ValueError(msg)
        transform = self.apidef.args_transform_cb
        transform = transform if callable(transform) else lambda arg: arg
        convert = self.apidef.convert_params
        def view(context, request):
            required_params = [context]
            optional_params = dict()
            # URI parameters have the highest prio
            if resource.uri_params:
                for param in resource.uri_params:
                    param_value = request.matchdict[param.name]
                    converted = validate_and_convert(param, param_value)
                    # pyramid router makes sure the URI params are all
                    # set, otherwise the view isn't called all, because
                    # a NotFound error is triggered before the request
                    # can be routed to this view
                    required_params.append(converted if convert else param_value)
            # If there's a body defined - include it before traits or query params
            if resource.body:
                required_params.append(prepare_json_body(request, resource.body))
            if resource.query_params:
                for param in resource.query_params:
                    # query params are always named (i.e. not positional)
                    # so they effectively become keyword agruments in a
                    # method call, we just make sure they are present
                    # in the request if marked as 'required'
                    if param.required and param.name not in request.params:
                        raise HTTPBadRequest("{} ({}) is required".format(param.name, param.type))
                    param_value = request.params.get(param.name, MARKER)
                    absent = param_value is MARKER
                    # If there's no default value defined in RAML let the decorated
                    # method decide which defaults to use. Unfortunatelly there is
                    # no way to tell whether a default value was declared as 'null'
                    # in RAML or if it was omitted - it's None in both cases
                    if absent and param.default is None:
                        continue
                    if not absent:
                        if convert:
                            param_value = validate_and_convert(param, param_value)
                    else:
                        if convert:
                            param_value = validate_and_convert(param, param.default)
                        else:
                            param_value = param.default
                    optional_params[transform(param.name)] = param_value
            print("optional", optional_params)
            result = meth(*required_params, **optional_params)
            return render_view(request, result, cfg.returns)

        return (view, cfg.permission)

    def get_service_class_method(self, resource):
        rel_path = resource.path[len(self.resource_path):]
        LOG.debug("Relative path for %s: '%s'", resource, rel_path)
        http_method = resource.method.lower()
        for (_, member) in getmembers(self.cls):
            if not hasattr(member, '_rest_config'):
                continue
            cfg = member._rest_config  # pylint: disable=protected-access
            if (cfg.http_method.lower() == http_method) and callable(member):
                return (member, cfg)
        return (None, None)

def create_options_view(supported_methods):
    """ Create a view callable for the OPTIONS request """
    def view(context, request):  # pylint: disable=unused-argument,missing-docstring
        response = HTTPNoContent()
        response.headers['Access-Control-Allow-Methods'] =\
                ', '.join(supported_methods)
        return response
    return view

def includeme(config):
    """Configure basic RAML REST settings for a Pyramid application.

    You should not call this function directly, but use
    :py:func:`pyramid.config.Configurator.include` to initialise
    the RAML routing.

    .. code-block:: python
       :linenos:
       config = Configurator()
       config.include('pyramlson')
    """
    from pyramlson.apidef import RamlApiDefinition
    settings = config.registry.settings
    settings['pyramlson.debug'] = \
            settings.get('debug_all') or \
            settings.get('pyramid.debug_all') or \
            settings.get('pyramlson.debug')
    config.add_view('pyramlson.error.generic', context=Exception, renderer='json')
    config.add_view('pyramlson.error.http_error', context=IExceptionResponse, renderer='json')
    config.add_notfound_view('pyramlson.error.notfound', renderer='json')
    config.add_forbidden_view('pyramlson.error.forbidden', renderer='json')

    if 'pyramlson.apidef_path' not in settings:
        raise ValueError("Cannot create RamlApiDefinition without a RAML file.")

    args_transform_cb = None
    if 'pyramlson.arguments_transformation_callback' in settings:
        args_transform_cb = DottedNameResolver().maybe_resolve(
                settings['pyramlson.arguments_transformation_callback']
                )

    convert_params = False
    if 'pyramlson.convert_parameters' in settings:
        convert_params = asbool(settings['pyramlson.convert_parameters'])

    res = AssetResolver()
    apidef_path = res.resolve(settings['pyramlson.apidef_path'])
    apidef = RamlApiDefinition(
            apidef_path.abspath(),
            args_transform_cb=args_transform_cb,
            convert_params=convert_params
            )
    config.registry.registerUtility(apidef, IRamlApiDefinition)

<commit_msg>Squelch debug output (optional params)<commit_after># coding: utf-8
"""
Main pyramlson decorators: api_service and api_method
"""
import re
import os
import logging

from email.utils import parsedate
from inspect import getmembers
from collections import namedtuple, defaultdict

import venusian

from pyramid.path import (
    AssetResolver,
    DottedNameResolver
)
from pyramid.httpexceptions import (
    HTTPBadRequest,
    HTTPInternalServerError,
    HTTPNoContent,
)
from pyramid.interfaces import IExceptionResponse
from pyramid.settings import asbool

from .apidef import IRamlApiDefinition
from .utils import (
    prepare_json_body,
    render_view,
    validate_and_convert
)

LOG = logging.getLogger(__name__)

DEFAULT_METHOD_MAP = {
    'get': 200,
    'post': 200,
    'put': 201,
    'delete': 204,
    'options': 200,
    'patch': 200,
}

MethodRestConfig = namedtuple('MethodRestConfig', [
    'http_method',
    'permission',
    'returns'
])

MARKER = object()

class api_method(object):
    # pylint: disable=invalid-name

    def __init__(self, http_method, permission=None, returns=None):
        """Configure a resource method corresponding with a RAML resource path

        This decorator must be used to declare REST resources.

        :param http_method: The HTTP method this method maps to.

        :param permission: Permission for this method.

        :param returns: A custom HTTP code to return in case of success.

            Configure the HTTP code to return when the method call was successful.
            Per default the codes are expected to match the configured HTTP method:
                - GET/POST/PATCH: 200
                - PUT: 201
                - DELETE: 204

        """
        self.http_method = http_method
        self.permission = permission
        self.returns = returns if returns is not None else DEFAULT_METHOD_MAP[self.http_method]

    def __call__(self, method):
        method._rest_config = MethodRestConfig(
            self.http_method,
            self.permission,
            self.returns
        )
        return method

class api_service(object):
    """Configures a resource by its REST path.

    This decorator configures a class as a REST resource. All endpoints
    must be defined in a RAML file.
    """
    # pylint: disable=invalid-name

    def __init__(self, resource_path):
        LOG.debug("Resource path: %s", resource_path)
        self.resource_path = resource_path
        self.resources = []
        self.apidef = None
        self.cls = None
        self.module = None

    def callback(self, scanner, name, cls):
        config = scanner.config.with_package(self.module)
        self.apidef = config.registry.queryUtility(IRamlApiDefinition)
        self.create_route(config)
        LOG.debug("registered routes with base route '%s'", self.apidef.base_path)
        self.create_views(config)

    def create_route(self, config):
        LOG.debug("Creating route for %s", self.resource_path)
        supported_methods = []
        route_name = None

        # Find all methods for this resource path
        for resource in self.apidef.get_resources(self.resource_path):
            if route_name is None:
                path = self.resource_path
                if self.apidef.base_path:
                    path = "{}{}".format(self.apidef.base_path, path)
                route_name = "{}-{}".format(resource.display_name, path)

            method = resource.method.upper()
            self.resources.append((route_name, method, resource, None))
            supported_methods.append(method)

        # Add one route for all the methods at this resource path
        if supported_methods:
            LOG.debug("Registering route with path %s", path)
            config.add_route(route_name, path, factory=self.cls)
            # add a default OPTIONS view if none was defined by the resource
            opts_meth = 'OPTIONS'
            if opts_meth not in supported_methods:
                methods = supported_methods + [opts_meth]
                self.resources.append((
                    route_name,
                    'OPTIONS',
                    resource,
                    create_options_view(methods)
                ))

    def create_views(self, config):
        for (route_name, method, resource, default_view) in self.resources:
            LOG.debug("Creating view %s %s", route_name, method)
            if default_view:
                config.add_view(
                    default_view,
                    route_name=route_name,
                    request_method=method
                )
            else:
                (view, permission) = self.create_view(resource)
                LOG.debug(
                    "Registering view %s for route name '%s', resource '%s', method '%s'",
                    view,
                    route_name,
                    resource,
                    method
                )
                config.add_view(
                    view,
                    route_name=route_name,
                    request_method=method,
                    permission=permission
                )

    def __call__(self, cls):
        self.cls = cls
        info = venusian.attach(cls, self.callback, 'pyramid', depth=1)
        self.module = info.module
        return cls

    def create_view(self, resource):
        (meth, cfg) = self.get_service_class_method(resource)
        LOG.debug("Got method %s for resource %s", meth, resource)
        if not meth:
            msg = "Could not find a method in class {} suitable for resource {}.".format(
                self.cls,
                resource
            )
            raise ValueError(msg)
        transform = self.apidef.args_transform_cb
        transform = transform if callable(transform) else lambda arg: arg
        convert = self.apidef.convert_params
        def view(context, request):
            required_params = [context]
            optional_params = dict()
            # URI parameters have the highest prio
            if resource.uri_params:
                for param in resource.uri_params:
                    param_value = request.matchdict[param.name]
                    converted = validate_and_convert(param, param_value)
                    # pyramid router makes sure the URI params are all
                    # set, otherwise the view isn't called all, because
                    # a NotFound error is triggered before the request
                    # can be routed to this view
                    required_params.append(converted if convert else param_value)
            # If there's a body defined - include it before traits or query params
            if resource.body:
                required_params.append(prepare_json_body(request, resource.body))
            if resource.query_params:
                for param in resource.query_params:
                    # query params are always named (i.e. not positional)
                    # so they effectively become keyword agruments in a
                    # method call, we just make sure they are present
                    # in the request if marked as 'required'
                    if param.required and param.name not in request.params:
                        raise HTTPBadRequest("{} ({}) is required".format(param.name, param.type))
                    param_value = request.params.get(param.name, MARKER)
                    absent = param_value is MARKER
                    # If there's no default value defined in RAML let the decorated
                    # method decide which defaults to use. Unfortunatelly there is
                    # no way to tell whether a default value was declared as 'null'
                    # in RAML or if it was omitted - it's None in both cases
                    if absent and param.default is None:
                        continue
                    if not absent:
                        if convert:
                            param_value = validate_and_convert(param, param_value)
                    else:
                        if convert:
                            param_value = validate_and_convert(param, param.default)
                        else:
                            param_value = param.default
                    optional_params[transform(param.name)] = param_value
            result = meth(*required_params, **optional_params)
            return render_view(request, result, cfg.returns)

        return (view, cfg.permission)

    def get_service_class_method(self, resource):
        rel_path = resource.path[len(self.resource_path):]
        LOG.debug("Relative path for %s: '%s'", resource, rel_path)
        http_method = resource.method.lower()
        for (_, member) in getmembers(self.cls):
            if not hasattr(member, '_rest_config'):
                continue
            cfg = member._rest_config  # pylint: disable=protected-access
            if (cfg.http_method.lower() == http_method) and callable(member):
                return (member, cfg)
        return (None, None)

def create_options_view(supported_methods):
    """ Create a view callable for the OPTIONS request """
    def view(context, request):  # pylint: disable=unused-argument,missing-docstring
        response = HTTPNoContent()
        response.headers['Access-Control-Allow-Methods'] =\
                ', '.join(supported_methods)
        return response
    return view

def includeme(config):
    """Configure basic RAML REST settings for a Pyramid application.

    You should not call this function directly, but use
    :py:func:`pyramid.config.Configurator.include` to initialise
    the RAML routing.

    .. code-block:: python
       :linenos:
       config = Configurator()
       config.include('pyramlson')
    """
    from pyramlson.apidef import RamlApiDefinition
    settings = config.registry.settings
    settings['pyramlson.debug'] = \
            settings.get('debug_all') or \
            settings.get('pyramid.debug_all') or \
            settings.get('pyramlson.debug')
    config.add_view('pyramlson.error.generic', context=Exception, renderer='json')
    config.add_view('pyramlson.error.http_error', context=IExceptionResponse, renderer='json')
    config.add_notfound_view('pyramlson.error.notfound', renderer='json')
    config.add_forbidden_view('pyramlson.error.forbidden', renderer='json')

    if 'pyramlson.apidef_path' not in settings:
        raise ValueError("Cannot create RamlApiDefinition without a RAML file.")

    args_transform_cb = None
    if 'pyramlson.arguments_transformation_callback' in settings:
        args_transform_cb = DottedNameResolver().maybe_resolve(
                settings['pyramlson.arguments_transformation_callback']
                )

    convert_params = False
    if 'pyramlson.convert_parameters' in settings:
        convert_params = asbool(settings['pyramlson.convert_parameters'])

    res = AssetResolver()
    apidef_path = res.resolve(settings['pyramlson.apidef_path'])
    apidef = RamlApiDefinition(
            apidef_path.abspath(),
            args_transform_cb=args_transform_cb,
            convert_params=convert_params
            )
    config.registry.registerUtility(apidef, IRamlApiDefinition)

<|endoftext|>