<commit_before>#
# This file is part of Gruvi. Gruvi is free software available under the
# terms of the MIT license. See the file "LICENSE" that was provided
# together with this source file for the licensing terms.
#
# Copyright (c) 2012-2013 the Gruvi authors. See the file "AUTHORS" for a
# complete list.

"""
This module implements a D-BUS client.

The implementation uses Tom Cocagne's excellent `txdbus
<https://github.com/cocagne/txdbus>`_ for marshalling and demarshalling
messages. A cut down copy of it, containing only the functionality needed by
Gruvi, is available as :mod:`gruvi.txdbus`. You need this if you are providing
a message handler (see below).

Currently only a client is implemented. The missing piece for a server
implementation is the server-side authentication mechanism.

The D-BUS client can react to incoming messages by providing a message handler
to the constructor. This message handler will be called for incoming messages
that are not replies to method calls made with :meth:`DBusClient.call_method`.

The signature of the message handler is: ``message_handler(message, protocol,
client)``. Here, the *message* argument is an instance of a subclass of
:class:`txdbus.DBusMessage`. The *protocol* will always be the DBusClient
instance since currently there is no DBusServer. The *client* will be the
transport this message was received on, which will currently always be
:attr:`DBusClient.transport`.

The return value of the message handler may be ``None``, or an instance of a
:class:`txdbus.DBusMessage` subclass. The latter is useful when you are
responding to a method call.

Message handlers runs in their own fiber. This allows a message handler to call
into a switchpoint. There will be one fiber for every transport.
"""

from __future__ import absolute_import, print_function

import os
import struct

from . import hub, error, txdbus, protocols, dbus_ffi, compat
from .hub import switchpoint
from .util import objref
from .protocols import errno, ProtocolError

__all__ = ['DBusError', 'DBusClient']

class DBusError(ProtocolError):
    """Exception that is raised in case of D-BUS protocol errors."""

class DBusClientAuthenticator(object):
    """A client-side D-BUS authenticator."""

    # Currently this only supports the EXTERNAL mechanism. EXTERNAL works for
    # any type of local sockets except TCP sockets on Windows. In real life
    # this should not be a limitation, at least not for now.

    s_start, s_auth_external, s_authenticated, s_failed = range(4)

    def __init__(self):
        self._state = self.s_start
        self._username = b''

    @property
    def username(self):
        return self._username

    def feed(self, line):
        """Feed *line* into the authenticator. Return authentication data that
        must be sent to the remote peer."""
        if self._state == self.s_authenticated:
            return b''
        elif self._state == self.s_failed:
            raise ValueError('authentication failed')
        if self._state == self.s_start:
            if line:
                raise ValueError('client must initiate handshake')
            self._state = self.s_auth_external
            return b'\0AUTH EXTERNAL\r\n'
        if not line.endswith(b'\r\n'):
            raise ValueError('Incomplete line')
        pos = line.find(b' ')
        if pos != -1:
            command = line[:pos]
            args = line[pos+1:-2]
        else:
            command = line[:-2]
            args = b''
        if command == b'OK':
            self._state = self.s_authenticated
            self._username = '<external>'
            return b'BEGIN\r\n'
        elif command == b'REJECTED':
            self._state = self.s_failed
            raise ValueError('authentication failed')
        elif command == b'DATA':
            return b'DATA\r\n'
        else:
            self._state = self.s_failed
            raise ValueError('unexpected command {0!r}'.format(command))

class DBusParser(protocols.Parser):
    """A D-BUS message parser."""

    # According to the D-BUS spec the max message size is 128MB. However since
    # we want to limited memory usage we are much more conservative here.
    max_message_size = 128*1024

    def __init__(self):
        super(DBusParser, self).__init__()
        self._buffer = bytearray()
        self._context = dbus_ffi.ffi.new('struct context *')

    def feed(self, buf):
        # len(buf) == 0 means EOF received
        if len(buf) == 0:
            if len(self._buffer) > 0:
                self._error = errno.FRAMING_ERROR
                self._error_message = 'partial message'
                return -1
            return 0
        # "struct context" is a C object and does *not* take a reference
        # Therefore use a Python variable to keep the cdata object alive
        cdata = self._context.buf = dbus_ffi.ffi.new('char[]', buf)
        self._context.buflen = len(buf)
        offset = self._context.offset = 0
        while offset != len(buf):
            error = dbus_ffi.lib.split(self._context)
            if error and error != dbus_ffi.lib.INCOMPLETE:
                self._error = errno.FRAMING_ERROR
                self._error_message = 'dbus_ffi.split(): error {0}'.format(error)
                offset = self._context.offset
                break
            nbytes = self._context.offset - offset
            if len(self._buffer) + nbytes > self.max_message_size:
                self._error = errno.MESSAGE_TOO_LARGE
                self._offset = 0
                break
            if error == dbus_ffi.lib.INCOMPLETE:
                self._buffer.extend(buf[offset:])
                return
            chunk = buf[offset:self._context.offset]
            if self._buffer:
                self._buffer.extend(chunk)
                chunk = bytes(self._buffer)
                self._buffer = bytearray()
            try:
                message = txdbus.parseMessage(chunk)
            except (txdbus.MarshallingError, struct.error) as e:
                self._error = errno.PARSE_ERROR
                self._error_message = 'txdbus.parseMessage(): {0!s}'.format(e)
                offset = 0
                break
            self._messages.append(message)
            offset = self._context.offset
        return offset

def parse_dbus_address(address):
    """Parse a D-BUS address string into a list of addresses."""
    if address == 'session':
        address = os.environ.get('DBUS_SESSION_BUS_ADDRESS')
        if not address:
            raise ValueError('$DBUS_SESSION_BUS_ADDRESS not set')
    elif address == 'system':
        address = os.environ.get('DBUS_SYSTEM_BUS_ADDRES',
                                'unix:path=/var/run/dbus/system_bus_socket')
    addresses = []
    for addr in address.split(';'):
        p1 = addr.find(':')
        if p1 == -1:
            raise ValueError('illegal address string: {0}'.formnat(addr))
        kind = addr[:p1]
        args = dict((kv.split('=') for kv in addr[p1+1:].split(',')))
        if kind == 'unix':
            if 'path' in args:
                addr = args['path']
            elif 'abstract' in args:
                addr = '\0' + args['abstract']
            else:
                raise ValueError('require "path" or "abstract" for unix')
        elif kind == 'tcp':
            if 'host' not in args or 'port' not in args:
                raise ValueError('require "host" and "port" for tcp')
            addr = (args['host'], int(args['port']))
        else:
            raise ValueError('unknown transport: {0}'.format(kind))
        addresses.append(addr)
    return addresses

class DBusBase(protocols.RequestResponseProtocol):

    _exception = DBusError
    _authenticator = None

    def __init__(self, message_handler=None, timeout=None):
        """The constructor accepts the following arguments. The
        *message_handler* argument specifies an optional message handler that
        can be used to react to inbound messages. See the notes at the top for
        more information on the message handler.

        The optional *timeout* argument can be used to specify a timeout for
        the various network operations used in the client.
        """
        super(DBusBase, self).__init__(DBusParser, timeout)
        self._message_handler = message_handler
        self._buffer = b''

    def _init_transport(self, transport):
        super(DBusBase, self)._init_transport(transport)
        transport._authenticator = self._authenticator()
        transport._authenticated = False
        transport._unique_name = None
        transport._queue._sizefunc = lambda msg: len(msg.rawMessage or b'')

    def _start_authentication(self, transport):
        authdata = transport._authenticator.feed(b'')
        assert authdata != b''
        transport.write(authdata)

    def _on_transport_readable(self, transport, data, error):
        if transport._authenticated or error:
            super(DBusBase, self)._on_transport_readable(transport, data, error)
            return
        # Handle authentication
        self._buffer += data
        pos = self._buffer.find(b'\r\n')
        if pos == -1:
            if len(self._buffer) > 1024:
                self._close_transport(transport)
            return
        line = self._buffer[:pos+2]
        self._buffer = self._buffer[pos+2:]
        try:
            authdata = transport._authenticator.feed(line)
        except ValueError as e:
            transport._log.error('authentication error: {!s}', e)
            error = DBusError(errno.AUTH_ERROR, str(e))
            self._close_transport(transport, error)
            return
        if authdata:
            transport.write(authdata)
        if transport._authenticator.username:
            transport._authenticated = True
            transport._events.emit('AuthenticationComplete')
            super(DBusBase, self)._on_transport_readable(transport,
                                        self._buffer, 0)
            self._buffer = b''

    def _dispatch_fast_path(self, transport, message):
        if isinstance(message, txdbus.MethodReturnMessage):
            event = 'MethodResponse:{0}'.format(message.reply_serial)
            if transport._events.emit(event, message):
                return True
        if not self._message_handler:
            transport._log.debug('no handler, dropping incoming message')
            return True
        return False

    def _dispatch_message(self, transport, message):
        """Dispatch a single message."""
        result = self._message_handler(message, self, transport)
        if result:
            self._write(transport, result.rawMessage)

    @switchpoint
    def _send_message(self, transport, message):
        if self._transport is None and not self._transport.closed:
            raise RuntimeError('not connected')
        if not isinstance(message, txdbus.DBusMessage):
            raise TypeError('expecting DBusMessage instance')
        self._write(transport, message.rawMessage)

class DBusClient(DBusBase):
    """A D-BUS client."""

    _authenticator = DBusClientAuthenticator

    @switchpoint
    def connect(self, address='session'):
        """Connect to *address* and wait until the connection is established.

        The *address* argument must be a D-BUS "server address", as explained
        in the `D-BUS specification
        <http://dbus.freedesktop.org/doc/dbus-specification.html>`_. It may
        also be one of the special addresses ``'session'`` or ``'system'``, to
        connect to the D-BUS session and system bus, respectively.
        """
        if isinstance(address, (compat.binary_type, compat.text_type)):
            addresses = parse_dbus_address(address)
        elif hasattr(address, 'connect'):
            addresses = [address]
        else:
            raise TypeError('address: expecting a string or a transport')
        for addr in addresses:
            try:
                self._connect(addr)
            except error.Error:
                continue
            break
        else:
            raise DBusError('could not connect to any address')
        self._start_authentication(self._transport)
        events = ('AuthenticationComplete', 'HandleError')
        self._transport._events.wait(self._timeout, waitfor=events)
        if self._transport._error:
            raise self._transport._error
        elif not self._transport._authenticated:
            raise DBusError(errno.TIMEOUT, 'timeout authenticating to the bus')
        unique_name = self.call_method('org.freedesktop.DBus',
                                       '/org/freedesktop/DBus',
                                       'org.freedesktop.DBus', 'Hello')
        self._transport._unique_name = unique_name

    def unique_name(self):
        """Return the unique bus name of the connection."""
        if self._transport is None or self._transport.closed:
            raise RuntimeError('not connected')
        return self._transport._unique_name

    @switchpoint
    def call_method(self, service, path, interface, method, signature=None,
                    args=None, no_reply=False, auto_start=False):
        """Call a D-BUS method and wait for its reply.

        This method calls the D-BUS method that resides on the object at bus
        address *service*, at path *path*, on interface *interface*.

        The *signature* and *args* are optional arguments that can be used to
        add parameters to the method call. The signature is a D-BUS signature
        string, while *args* must be a sequence of python types that can be
        converted into the types specified by the signature. See the `D-BUS
        specification
        <http://dbus.freedesktop.org/doc/dbus-specification.html>`_ for a
        reference on signature strings.

        The flags *no_reply* and *auto_start* control the NO_REPLY_EXPECTED and
        NO_AUTO_START flags on the D-BUS message.

        The return value is the result of the D-BUS method call, in the
        following way: if there were no values in the response, then None is
        returned. If there was one value in the response, then that value is
        returned. And if there were multiple values in the response then the
        values are returned in a list.
        """
        if self._transport is None or self._transport.closed:
            raise RuntimeError('not connected')
        message = txdbus.MethodCallMessage(path, method, interface=interface,
                                           destination=service,
                                           signature=signature, body=args,
                                           expectReply=not no_reply,
                                           autoStart=auto_start)
        self.send_message(message)
        events = ('MethodResponse:{0}'.format(message.serial), 'HandleError')
        response = self._transport._events.wait(self._timeout, waitfor=events)
        if response == 'HandleError':
            raise self._transport._error
        event, response = response
        assert isinstance(response, txdbus.DBusMessage)
        assert response.reply_serial == message.serial
        if isinstance(response, txdbus.ErrorMessage):
            raise DBusError(errno.REQUEST_ERROR, response)
        body = response.body
        if body is None or len(body) == 0:
            body = None
        elif len(body) == 1:
            body = body[0]
        return body

    @switchpoint
    def send_message(self, message):
        """Send a D-BUS message.

        The *message* argument must a valid :class:`txdbus.DBusMessage`
        instance.
        """
        self._send_message(self.transport, message)
<commit_msg>fixed spelling mistake for bus type<commit_after>#
# This file is part of Gruvi. Gruvi is free software available under the
# terms of the MIT license. See the file "LICENSE" that was provided
# together with this source file for the licensing terms.
#
# Copyright (c) 2012-2013 the Gruvi authors. See the file "AUTHORS" for a
# complete list.

"""
This module implements a D-BUS client.

The implementation uses Tom Cocagne's excellent `txdbus
<https://github.com/cocagne/txdbus>`_ for marshalling and demarshalling
messages. A cut down copy of it, containing only the functionality needed by
Gruvi, is available as :mod:`gruvi.txdbus`. You need this if you are providing
a message handler (see below).

Currently only a client is implemented. The missing piece for a server
implementation is the server-side authentication mechanism.

The D-BUS client can react to incoming messages by providing a message handler
to the constructor. This message handler will be called for incoming messages
that are not replies to method calls made with :meth:`DBusClient.call_method`.

The signature of the message handler is: ``message_handler(message, protocol,
client)``. Here, the *message* argument is an instance of a subclass of
:class:`txdbus.DBusMessage`. The *protocol* will always be the DBusClient
instance since currently there is no DBusServer. The *client* will be the
transport this message was received on, which will currently always be
:attr:`DBusClient.transport`.

The return value of the message handler may be ``None``, or an instance of a
:class:`txdbus.DBusMessage` subclass. The latter is useful when you are
responding to a method call.

Message handlers runs in their own fiber. This allows a message handler to call
into a switchpoint. There will be one fiber for every transport.
"""

from __future__ import absolute_import, print_function

import os
import struct

from . import hub, error, txdbus, protocols, dbus_ffi, compat
from .hub import switchpoint
from .util import objref
from .protocols import errno, ProtocolError

__all__ = ['DBusError', 'DBusClient']

class DBusError(ProtocolError):
    """Exception that is raised in case of D-BUS protocol errors."""

class DBusClientAuthenticator(object):
    """A client-side D-BUS authenticator."""

    # Currently this only supports the EXTERNAL mechanism. EXTERNAL works for
    # any type of local sockets except TCP sockets on Windows. In real life
    # this should not be a limitation, at least not for now.

    s_start, s_auth_external, s_authenticated, s_failed = range(4)

    def __init__(self):
        self._state = self.s_start
        self._username = b''

    @property
    def username(self):
        return self._username

    def feed(self, line):
        """Feed *line* into the authenticator. Return authentication data that
        must be sent to the remote peer."""
        if self._state == self.s_authenticated:
            return b''
        elif self._state == self.s_failed:
            raise ValueError('authentication failed')
        if self._state == self.s_start:
            if line:
                raise ValueError('client must initiate handshake')
            self._state = self.s_auth_external
            return b'\0AUTH EXTERNAL\r\n'
        if not line.endswith(b'\r\n'):
            raise ValueError('Incomplete line')
        pos = line.find(b' ')
        if pos != -1:
            command = line[:pos]
            args = line[pos+1:-2]
        else:
            command = line[:-2]
            args = b''
        if command == b'OK':
            self._state = self.s_authenticated
            self._username = '<external>'
            return b'BEGIN\r\n'
        elif command == b'REJECTED':
            self._state = self.s_failed
            raise ValueError('authentication failed')
        elif command == b'DATA':
            return b'DATA\r\n'
        else:
            self._state = self.s_failed
            raise ValueError('unexpected command {0!r}'.format(command))

class DBusParser(protocols.Parser):
    """A D-BUS message parser."""

    # According to the D-BUS spec the max message size is 128MB. However since
    # we want to limited memory usage we are much more conservative here.
    max_message_size = 128*1024

    def __init__(self):
        super(DBusParser, self).__init__()
        self._buffer = bytearray()
        self._context = dbus_ffi.ffi.new('struct context *')

    def feed(self, buf):
        # len(buf) == 0 means EOF received
        if len(buf) == 0:
            if len(self._buffer) > 0:
                self._error = errno.FRAMING_ERROR
                self._error_message = 'partial message'
                return -1
            return 0
        # "struct context" is a C object and does *not* take a reference
        # Therefore use a Python variable to keep the cdata object alive
        cdata = self._context.buf = dbus_ffi.ffi.new('char[]', buf)
        self._context.buflen = len(buf)
        offset = self._context.offset = 0
        while offset != len(buf):
            error = dbus_ffi.lib.split(self._context)
            if error and error != dbus_ffi.lib.INCOMPLETE:
                self._error = errno.FRAMING_ERROR
                self._error_message = 'dbus_ffi.split(): error {0}'.format(error)
                offset = self._context.offset
                break
            nbytes = self._context.offset - offset
            if len(self._buffer) + nbytes > self.max_message_size:
                self._error = errno.MESSAGE_TOO_LARGE
                self._offset = 0
                break
            if error == dbus_ffi.lib.INCOMPLETE:
                self._buffer.extend(buf[offset:])
                return
            chunk = buf[offset:self._context.offset]
            if self._buffer:
                self._buffer.extend(chunk)
                chunk = bytes(self._buffer)
                self._buffer = bytearray()
            try:
                message = txdbus.parseMessage(chunk)
            except (txdbus.MarshallingError, struct.error) as e:
                self._error = errno.PARSE_ERROR
                self._error_message = 'txdbus.parseMessage(): {0!s}'.format(e)
                offset = 0
                break
            self._messages.append(message)
            offset = self._context.offset
        return offset

def parse_dbus_address(address):
    """Parse a D-BUS address string into a list of addresses."""
    if address == 'session':
        address = os.environ.get('DBUS_SESSION_BUS_ADDRESS')
        if not address:
            raise ValueError('$DBUS_SESSION_BUS_ADDRESS not set')
    elif address == 'system':
        address = os.environ.get('DBUS_SYSTEM_BUS_ADDRESS',
                                'unix:path=/var/run/dbus/system_bus_socket')
    addresses = []
    for addr in address.split(';'):
        p1 = addr.find(':')
        if p1 == -1:
            raise ValueError('illegal address string: {0}'.formnat(addr))
        kind = addr[:p1]
        args = dict((kv.split('=') for kv in addr[p1+1:].split(',')))
        if kind == 'unix':
            if 'path' in args:
                addr = args['path']
            elif 'abstract' in args:
                addr = '\0' + args['abstract']
            else:
                raise ValueError('require "path" or "abstract" for unix')
        elif kind == 'tcp':
            if 'host' not in args or 'port' not in args:
                raise ValueError('require "host" and "port" for tcp')
            addr = (args['host'], int(args['port']))
        else:
            raise ValueError('unknown transport: {0}'.format(kind))
        addresses.append(addr)
    return addresses

class DBusBase(protocols.RequestResponseProtocol):

    _exception = DBusError
    _authenticator = None

    def __init__(self, message_handler=None, timeout=None):
        """The constructor accepts the following arguments. The
        *message_handler* argument specifies an optional message handler that
        can be used to react to inbound messages. See the notes at the top for
        more information on the message handler.

        The optional *timeout* argument can be used to specify a timeout for
        the various network operations used in the client.
        """
        super(DBusBase, self).__init__(DBusParser, timeout)
        self._message_handler = message_handler
        self._buffer = b''

    def _init_transport(self, transport):
        super(DBusBase, self)._init_transport(transport)
        transport._authenticator = self._authenticator()
        transport._authenticated = False
        transport._unique_name = None
        transport._queue._sizefunc = lambda msg: len(msg.rawMessage or b'')

    def _start_authentication(self, transport):
        authdata = transport._authenticator.feed(b'')
        assert authdata != b''
        transport.write(authdata)

    def _on_transport_readable(self, transport, data, error):
        if transport._authenticated or error:
            super(DBusBase, self)._on_transport_readable(transport, data, error)
            return
        # Handle authentication
        self._buffer += data
        pos = self._buffer.find(b'\r\n')
        if pos == -1:
            if len(self._buffer) > 1024:
                self._close_transport(transport)
            return
        line = self._buffer[:pos+2]
        self._buffer = self._buffer[pos+2:]
        try:
            authdata = transport._authenticator.feed(line)
        except ValueError as e:
            transport._log.error('authentication error: {!s}', e)
            error = DBusError(errno.AUTH_ERROR, str(e))
            self._close_transport(transport, error)
            return
        if authdata:
            transport.write(authdata)
        if transport._authenticator.username:
            transport._authenticated = True
            transport._events.emit('AuthenticationComplete')
            super(DBusBase, self)._on_transport_readable(transport,
                                        self._buffer, 0)
            self._buffer = b''

    def _dispatch_fast_path(self, transport, message):
        if isinstance(message, txdbus.MethodReturnMessage):
            event = 'MethodResponse:{0}'.format(message.reply_serial)
            if transport._events.emit(event, message):
                return True
        if not self._message_handler:
            transport._log.debug('no handler, dropping incoming message')
            return True
        return False

    def _dispatch_message(self, transport, message):
        """Dispatch a single message."""
        result = self._message_handler(message, self, transport)
        if result:
            self._write(transport, result.rawMessage)

    @switchpoint
    def _send_message(self, transport, message):
        if self._transport is None and not self._transport.closed:
            raise RuntimeError('not connected')
        if not isinstance(message, txdbus.DBusMessage):
            raise TypeError('expecting DBusMessage instance')
        self._write(transport, message.rawMessage)

class DBusClient(DBusBase):
    """A D-BUS client."""

    _authenticator = DBusClientAuthenticator

    @switchpoint
    def connect(self, address='session'):
        """Connect to *address* and wait until the connection is established.

        The *address* argument must be a D-BUS "server address", as explained
        in the `D-BUS specification
        <http://dbus.freedesktop.org/doc/dbus-specification.html>`_. It may
        also be one of the special addresses ``'session'`` or ``'system'``, to
        connect to the D-BUS session and system bus, respectively.
        """
        if isinstance(address, (compat.binary_type, compat.text_type)):
            addresses = parse_dbus_address(address)
        elif hasattr(address, 'connect'):
            addresses = [address]
        else:
            raise TypeError('address: expecting a string or a transport')
        for addr in addresses:
            try:
                self._connect(addr)
            except error.Error:
                continue
            break
        else:
            raise DBusError('could not connect to any address')
        self._start_authentication(self._transport)
        events = ('AuthenticationComplete', 'HandleError')
        self._transport._events.wait(self._timeout, waitfor=events)
        if self._transport._error:
            raise self._transport._error
        elif not self._transport._authenticated:
            raise DBusError(errno.TIMEOUT, 'timeout authenticating to the bus')
        unique_name = self.call_method('org.freedesktop.DBus',
                                       '/org/freedesktop/DBus',
                                       'org.freedesktop.DBus', 'Hello')
        self._transport._unique_name = unique_name

    def unique_name(self):
        """Return the unique bus name of the connection."""
        if self._transport is None or self._transport.closed:
            raise RuntimeError('not connected')
        return self._transport._unique_name

    @switchpoint
    def call_method(self, service, path, interface, method, signature=None,
                    args=None, no_reply=False, auto_start=False):
        """Call a D-BUS method and wait for its reply.

        This method calls the D-BUS method that resides on the object at bus
        address *service*, at path *path*, on interface *interface*.

        The *signature* and *args* are optional arguments that can be used to
        add parameters to the method call. The signature is a D-BUS signature
        string, while *args* must be a sequence of python types that can be
        converted into the types specified by the signature. See the `D-BUS
        specification
        <http://dbus.freedesktop.org/doc/dbus-specification.html>`_ for a
        reference on signature strings.

        The flags *no_reply* and *auto_start* control the NO_REPLY_EXPECTED and
        NO_AUTO_START flags on the D-BUS message.

        The return value is the result of the D-BUS method call, in the
        following way: if there were no values in the response, then None is
        returned. If there was one value in the response, then that value is
        returned. And if there were multiple values in the response then the
        values are returned in a list.
        """
        if self._transport is None or self._transport.closed:
            raise RuntimeError('not connected')
        message = txdbus.MethodCallMessage(path, method, interface=interface,
                                           destination=service,
                                           signature=signature, body=args,
                                           expectReply=not no_reply,
                                           autoStart=auto_start)
        self.send_message(message)
        events = ('MethodResponse:{0}'.format(message.serial), 'HandleError')
        response = self._transport._events.wait(self._timeout, waitfor=events)
        if response == 'HandleError':
            raise self._transport._error
        event, response = response
        assert isinstance(response, txdbus.DBusMessage)
        assert response.reply_serial == message.serial
        if isinstance(response, txdbus.ErrorMessage):
            raise DBusError(errno.REQUEST_ERROR, response)
        body = response.body
        if body is None or len(body) == 0:
            body = None
        elif len(body) == 1:
            body = body[0]
        return body

    @switchpoint
    def send_message(self, message):
        """Send a D-BUS message.

        The *message* argument must a valid :class:`txdbus.DBusMessage`
        instance.
        """
        self._send_message(self.transport, message)
<|endoftext|>


<commit_before>import attr
import pandas as pd
import re

from .base import TohuBaseGenerator, SeedGenerator
from .logging import logger
from .spawn_context import SpawnContextCG

__all__ = ['CustomGenerator']

def set_item_class_name_on_custom_generator_class(cls):
    """
    Set the attribute `cls.__tohu_items_name__` to a string which defines the name
    of the namedtuple class which will be used to produce items for the custom
    generator.

    By default this will be the first part of the class name (before '...Generator'),
    for example:

        FoobarGenerator -> Foobar
        QuuxGenerator   -> Quux

    However, it can be set explicitly by the user by defining `__tohu_items_name__`
    in the class definition, for example:

        class Quux(CustomGenerator):
            __tohu_items_name__ = 'MyQuuxItem'
    """

    if '__tohu_items_name__' in cls.__dict__:
        logger.debug(f"Using item class name '{cls.__tohu_items_name__}' (derived from attribute '__tohu_items_name__')")
    else:
        m = re.match('^(.*)Generator$', cls.__name__)
        if m is not None:
            cls.__tohu_items_name__ = m.group(1)
            logger.debug(f"Using item class name '{cls.__tohu_items_name__}' (derived from custom generator name)")
        else:
            raise ValueError("Cannot derive class name for items to be produced by custom generator. "
                             "Please set '__tohu_items_name__' at the top of the custom generator's "
                             "definition or change its name so that it ends in '...Generator'")

def make_item_class(clsname, attr_names):
    """
    Parameters
    ----------
    clsname: string
        Name of the class to be created

    attr_names: list of strings
        Names of the attributes of the class to be created
    """

    item_cls = attr.make_class(clsname, {name: attr.ib() for name in attr_names}, repr=False, cmp=True)

    def new_repr(self):
        all_fields = ', '.join([f'{name}={repr(value)}' for name, value in attr.asdict(self).items()])
        return f'{clsname}({all_fields})'

    orig_eq = item_cls.__eq__

    def new_eq(self, other):
        """
        Custom __eq__() method which also allows comparisons with
        tuples and dictionaries. This is mostly for convenience
        during testing.
        """

        if isinstance(other, self.__class__):
            return orig_eq(self, other)
        else:
            if isinstance(other, tuple):
                return attr.astuple(self) == other
            elif isinstance(other, dict):
                return attr.asdict(self) == other
            else:
                return NotImplemented

    item_cls.__repr__ = new_repr
    item_cls.__eq__ = new_eq
    item_cls.keys = lambda self: attr_names
    item_cls.__getitem__ = lambda self, key: getattr(self, key)
    item_cls.as_dict = lambda self: attr.asdict(self)
    item_cls.to_series = lambda self: pd.Series(attr.asdict(self))

    return item_cls

class CustomGenerator(TohuBaseGenerator):

    def __init__(self, *args, **kwargs):
        super().__init__()

        self.orig_args = args
        self.orig_kwargs = kwargs

        self.seed_generator = SeedGenerator()
        self.field_gen_templates = self._find_field_generator_templates()

        spawn_context = SpawnContextCG()
        for name, g in self.field_gen_templates.items():
            spawn_context.spawn_template(g, name=name)

        self.field_gens = spawn_context.named_spawns

        self.__dict__.update(self.field_gens)

        set_item_class_name_on_custom_generator_class(self.__class__)
        self._set_item_class()

    def _set_item_class(self):
        """
        cls:
            The custom generator class for which to create an item-class
        """
        clsname = self.__tohu_items_name__
        attr_names = self.field_gens.keys()
        self.item_cls = make_item_class(clsname, attr_names)

    def __next__(self):
        field_values = [next(g) for g in self.field_gens.values()]
        return self.item_cls(*field_values)

    def _find_field_generator_templates(self):
        """
        Return a dictionary of the form {name: field_generator} containing
        all tohu generators defined in the class and instance namespace
        of this custom generator.
        """
        field_gen_templates = {}

        # Extract field generators from class dict
        for name, g in self.__class__.__dict__.items():
            if isinstance(g, TohuBaseGenerator):
                field_gen_templates[name] = g.set_tohu_name(f'{name} (TPL)')

        # Extract field generators from instance dict
        for name, g in self.__dict__.items():
            if isinstance(g, TohuBaseGenerator):
                field_gen_templates[name] = g.set_tohu_name(f'{name} (TPL)')

        return field_gen_templates

    def reset(self, seed):
        super().reset(seed)
        self.seed_generator.reset(seed)
        for name, gen in self.field_gens.items():
            next_seed = next(self.seed_generator)
            gen.reset(next_seed)

    def spawn(self):
        new_obj = self.__class__(*self.orig_args, **self.orig_kwargs)
        new_obj._set_random_state_from(self)

        # Explicitly set item_cls. This is necessary because due to
        # the way in which `attr` works, explicit comparisons between
        # generated items will return False even though they contain
        # the same elements (because the underlying attr classes are
        # different, so attr plays it safe).
        new_obj.item_cls = self.item_cls

        return new_obj

    def _set_random_state_from(self, other):
        self.seed_generator._set_random_state_from(other.seed_generator)

        for name in self.field_gens.keys():
            self.field_gens[name]._set_random_state_from(other.field_gens[name])
<commit_msg>Add FIXME note<commit_after>import attr
import pandas as pd
import re

from .base import TohuBaseGenerator, SeedGenerator
from .logging import logger
from .spawn_context import SpawnContextCG

__all__ = ['CustomGenerator']

def set_item_class_name_on_custom_generator_class(cls):
    """
    Set the attribute `cls.__tohu_items_name__` to a string which defines the name
    of the namedtuple class which will be used to produce items for the custom
    generator.

    By default this will be the first part of the class name (before '...Generator'),
    for example:

        FoobarGenerator -> Foobar
        QuuxGenerator   -> Quux

    However, it can be set explicitly by the user by defining `__tohu_items_name__`
    in the class definition, for example:

        class Quux(CustomGenerator):
            __tohu_items_name__ = 'MyQuuxItem'
    """

    if '__tohu_items_name__' in cls.__dict__:
        logger.debug(f"Using item class name '{cls.__tohu_items_name__}' (derived from attribute '__tohu_items_name__')")
    else:
        m = re.match('^(.*)Generator$', cls.__name__)
        if m is not None:
            cls.__tohu_items_name__ = m.group(1)
            logger.debug(f"Using item class name '{cls.__tohu_items_name__}' (derived from custom generator name)")
        else:
            raise ValueError("Cannot derive class name for items to be produced by custom generator. "
                             "Please set '__tohu_items_name__' at the top of the custom generator's "
                             "definition or change its name so that it ends in '...Generator'")

def make_item_class(clsname, attr_names):
    """
    Parameters
    ----------
    clsname: string
        Name of the class to be created

    attr_names: list of strings
        Names of the attributes of the class to be created
    """

    item_cls = attr.make_class(clsname, {name: attr.ib() for name in attr_names}, repr=False, cmp=True)

    def new_repr(self):
        all_fields = ', '.join([f'{name}={repr(value)}' for name, value in attr.asdict(self).items()])
        return f'{clsname}({all_fields})'

    orig_eq = item_cls.__eq__

    def new_eq(self, other):
        """
        Custom __eq__() method which also allows comparisons with
        tuples and dictionaries. This is mostly for convenience
        during testing.
        """

        if isinstance(other, self.__class__):
            return orig_eq(self, other)
        else:
            if isinstance(other, tuple):
                return attr.astuple(self) == other
            elif isinstance(other, dict):
                return attr.asdict(self) == other
            else:
                return NotImplemented

    item_cls.__repr__ = new_repr
    item_cls.__eq__ = new_eq
    item_cls.keys = lambda self: attr_names
    item_cls.__getitem__ = lambda self, key: getattr(self, key)
    item_cls.as_dict = lambda self: attr.asdict(self)
    item_cls.to_series = lambda self: pd.Series(attr.asdict(self))

    return item_cls

class CustomGenerator(TohuBaseGenerator):

    def __init__(self, *args, **kwargs):
        super().__init__()

        self.orig_args = args
        self.orig_kwargs = kwargs

        self.seed_generator = SeedGenerator()
        self.field_gen_templates = self._find_field_generator_templates()

        spawn_context = SpawnContextCG()
        for name, g in self.field_gen_templates.items():
            spawn_context.spawn_template(g, name=name)

        self.field_gens = spawn_context.named_spawns

        self.__dict__.update(self.field_gens)

        set_item_class_name_on_custom_generator_class(self.__class__)
        self._set_item_class()

    def _set_item_class(self):
        """
        cls:
            The custom generator class for which to create an item-class
        """
        clsname = self.__tohu_items_name__
        attr_names = self.field_gens.keys()
        self.item_cls = make_item_class(clsname, attr_names)

    def __next__(self):
        field_values = [next(g) for g in self.field_gens.values()]
        return self.item_cls(*field_values)

    def _find_field_generator_templates(self):
        """
        Return a dictionary of the form {name: field_generator} containing
        all tohu generators defined in the class and instance namespace
        of this custom generator.
        """
        field_gen_templates = {}

        # Extract field generators from class dict
        for name, g in self.__class__.__dict__.items():
            if isinstance(g, TohuBaseGenerator):
                field_gen_templates[name] = g.set_tohu_name(f'{name} (TPL)')

        # Extract field generators from instance dict
        for name, g in self.__dict__.items():
            if isinstance(g, TohuBaseGenerator):
                field_gen_templates[name] = g.set_tohu_name(f'{name} (TPL)')

        return field_gen_templates

    def reset(self, seed):
        super().reset(seed)
        self.seed_generator.reset(seed)
        for name, gen in self.field_gens.items():
            next_seed = next(self.seed_generator)
            gen.reset(next_seed)

    def spawn(self):
        new_obj = self.__class__(*self.orig_args, **self.orig_kwargs)
        new_obj._set_random_state_from(self)

        # Explicitly set item_cls. This is necessary because due to
        # the way in which `attr` works, explicit comparisons between
        # generated items will return False even though they contain
        # the same elements (because the underlying attr classes are
        # different, so attr plays it safe).
        new_obj.item_cls = self.item_cls

        return new_obj

    def _set_random_state_from(self, other):
        self.seed_generator._set_random_state_from(other.seed_generator)

        # FIXME: should also set random state for unnamed field generators
        #        (these can occur in chains of derived generators)
        for name in self.field_gens.keys():
            self.field_gens[name]._set_random_state_from(other.field_gens[name])
<|endoftext|>


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

import re

import lxml.html

from venues.abstract_venue import AbstractVenue

class PluginParseError(Exception): pass

class Olympia(AbstractVenue):
    def __init__(self):
        super().__init__()
        self.url = "http://olympiakortteli.fi/keikat/"
        self.name = "Olympia-kortteli"
        self.city = "Tampere"
        self.country = "Finland"

        # Parsing patterns
        self.datepat = re.compile("[0-9.]+")
        self.monetary = re.compile("[0-9]+(\s+)?€")

    def parse_price(self, t):
        # XXX Fix this
        return 0
        # tag = t.xpath('./div[2]/strong/text()')

        # foundprice = re.findall(self.monetary, " ".join(tag))

        # if foundprice is None:
        #    foundprice = ""

        # return "0" if len(foundprice) == 0 else "%s" % (foundprice[0])

    def parse_date(self, t):
        tag = t.xpath('./div[2]/text()')

        if tag:
            tag = " ".join(tag)
            tag = re.sub("\s+", " ", tag).lstrip(" ").rstrip(" ")

        founddate = re.search(self.datepat, tag)

        if founddate is None:
            return ""

        day, month, year = founddate.group().split(".")
        return "%.4d-%.2d-%.2d" % (int(year), int(month), int(day))

    def parse_events(self, data: str):
        doc = lxml.html.fromstring(data)

        for event in doc.xpath('//div[contains(@class, "keikkalista")]/div'):
            name = "".join(event.xpath("./div/h3/text()"))
            name = re.sub("\s+", " ", name).lstrip(" ").rstrip(" ")
            date = self.parse_date(event)
            price = self.parse_price(event)

            yield {"venue": self.get_venue_name(),
                   "date": date,
                   "name": name,
                   "price": price}

if __name__ == '__main__':
    import requests

    l = Olympia()
    r = requests.get(l.url)

    for e in l.parse_events(r.content):
        for k, v in e.items():
            print(f"{k:>10s}: {v}")
        print()
<commit_msg>Fix venue plugin_olympia.<commit_after>#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re

import lxml.html

from venues.abstract_venue import AbstractVenue

class PluginParseError(Exception):
    pass

class Olympia(AbstractVenue):
    def __init__(self):
        super().__init__()
        self.url = "http://olympiakortteli.fi/keikat/"
        self.name = "Olympia-kortteli"
        self.city = "Tampere"
        self.country = "Finland"

        # Parsing patterns
        self.datepat = re.compile("[0-9.]{1,2}\.[0-9]{1,2}\.[0-9]{4}")
        self.pricepat_monetary = re.compile("[0-9,]+.€")
        self.pricepat_plain = re.compile("[0-9,]+")

    def parse_price(self, info_tag: str) -> str:
        prices_with_mon = self.pricepat_monetary.findall(info_tag)
        prices = []
        for price in prices_with_mon:
            parsed_price = self.pricepat_plain.findall(price)
            if len(parsed_price) == 0:
                continue
            prices.append("".join(parsed_price))

        if len(prices) == 0:
            return "0€"
        elif len(prices) == 2:
            in_advance, from_door = prices[0], prices[1]
            return f"{in_advance}€/{from_door}€"
        return "{}€".format("".join(prices))

    def parse_date(self, tag: lxml.html.HtmlElement) -> str:
        pvm_tag = tag.xpath('./div[contains(@class, "pvm")]')[0].text_content()
        if (parsed_pvm := self.datepat.search(pvm_tag)) is not None:
            day, month, year = parsed_pvm.group().split(".")
            day = int(day)
            month = int(month)
            year = int(year)
            return f"{year:04d}-{month:02d}-{day:02d}"

        return ""

    def parse_event(self, tag: lxml.html.HtmlElement) -> str:
        info_tag = tag.xpath('./div[contains(@class, "infot")]')[0].text_content()
        info_tag = re.sub("\s+", " ", info_tag).lstrip(" ").rstrip(" ")
        return info_tag

    def parse_events(self, data: bytes):
        doc = lxml.html.fromstring(data)

        for event in doc.xpath('/html[@class="no-js"]/body//div[contains(@class, "keikka")]'):
            date = self.parse_date(event)
            event_info = self.parse_event(event)
            price = self.parse_price(event_info)

            yield {"venue": self.get_venue_name(),
                   "date": date,
                   "name": event_info,
                   "price": price}

if __name__ == '__main__':
    import requests

    olympia = Olympia()
    r = requests.get(olympia.url)

    for e in olympia.parse_events(r.content):
        for k, v in e.items():
            print(f"{k:>10s}: {v}")
        print()
<|endoftext|>


<commit_before>from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import ListView
import magic
from docs.models import Document, Folder
from mailer.tasks import send_doc
from website.views import ClubbableContextMixin, get_context_data

class DocList(LoginRequiredMixin, ListView, ClubbableContextMixin):
    context_object_name = 'docs'

    def get_folder(self):
        return get_object_or_404(Folder, pk=self.kwargs['folder_id'])

    def get_context_data(self, **kwargs):
        context_data = super().get_context_data(**kwargs)
        context_data['folder'] = self.get_folder()
        return context_data

    def get_queryset(self):
        return self.get_folder().document_set.all()

@user_passes_test(lambda u: u.is_staff)
def send(request, folder_id, pk):
    if request.method == 'POST':
        to = request.POST['to']
        send_doc.delay(
            to=request.user.email if to == 'Myself' else to,
            subject=request.POST['subject'],
            message=request.POST['text'],
            doc_id=pk,
        )
        messages.info(request, 'Your message is queued for sending.')
        return HttpResponseRedirect(
            reverse('doc_list', kwargs={'folder_id': folder_id})
        )
    context_data = get_context_data(request)
    context_data['doc'] = get_object_or_404(Document, pk=pk)
    return render(request, 'docs/send_doc.html', context_data)

@login_required
def download(request, folder_id, pk, filename):
    """
    Return doc as an HTTP response attachment

    :param request: HTTP Request
    :param folder_id: Keeps the URLs logical, but not used
    :param pk: The primary key of the document
    :param filename: Makes the URL to look nice, but not used
    """
    doc = get_object_or_404(Document, pk=pk)
    mime = magic.Magic(mime=True)
    file_path = settings.MEDIA_ROOT + doc.file.name
    content_type = mime.from_file(file_path)
    response = HttpResponse(doc.file.read(), content_type=content_type)
    response['Content-Length'] = doc.file.size
    response['Content-Disposition'] = ('attachment; '
                                       'filename="%s"' % doc.filename)
    return response
<commit_msg>Use FileResponse<commit_after>from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect, FileResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views.generic import ListView
import magic
from docs.models import Document, Folder
from mailer.tasks import send_doc
from website.views import ClubbableContextMixin, get_context_data

class DocList(LoginRequiredMixin, ListView, ClubbableContextMixin):
    context_object_name = 'docs'

    def get_folder(self):
        return get_object_or_404(Folder, pk=self.kwargs['folder_id'])

    def get_context_data(self, **kwargs):
        context_data = super().get_context_data(**kwargs)
        context_data['folder'] = self.get_folder()
        return context_data

    def get_queryset(self):
        return self.get_folder().document_set.all()

@user_passes_test(lambda u: u.is_staff)
def send(request, folder_id, pk):
    if request.method == 'POST':
        to = request.POST['to']
        send_doc.delay(
            to=request.user.email if to == 'Myself' else to,
            subject=request.POST['subject'],
            message=request.POST['text'],
            doc_id=pk,
        )
        messages.info(request, 'Your message is queued for sending.')
        return HttpResponseRedirect(
            reverse('doc_list', kwargs={'folder_id': folder_id})
        )
    context_data = get_context_data(request)
    context_data['doc'] = get_object_or_404(Document, pk=pk)
    return render(request, 'docs/send_doc.html', context_data)

@login_required
def download(request, folder_id, pk, filename):
    """
    Return doc as an HTTP response attachment

    :param request: HTTP Request
    :param folder_id: Keeps the URLs logical, but not used
    :param pk: The primary key of the document
    :param filename: Makes the URL to look nice, but not used
    """
    doc = get_object_or_404(Document, pk=pk)
    return FileResponse(doc.file, as_attachment=True)
<|endoftext|>


<commit_before>import collections
import datetime
import discount

from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Max, Sum
from django.utils import timezone

from registrasion import models as rego

from category import CategoryController
from conditions import ConditionController
from product import ProductController

class CartController(object):

    def __init__(self, cart):
        self.cart = cart

    @staticmethod
    def for_user(user):
        ''' Returns the user's current cart, or creates a new cart
        if there isn't one ready yet. '''

        try:
            existing = rego.Cart.objects.get(user=user, active=True)
        except ObjectDoesNotExist:
            existing = rego.Cart.objects.create(
                user=user,
                time_last_updated=timezone.now(),
                reservation_duration=datetime.timedelta(),
                 )
            existing.save()
        return CartController(existing)

    def extend_reservation(self):
        ''' Updates the cart's time last updated value, which is used to
        determine whether the cart has reserved the items and discounts it
        holds. '''

        reservations = [datetime.timedelta()]

        # If we have vouchers, we're entitled to an hour at minimum.
        if len(self.cart.vouchers.all()) >= 1:
            reservations.append(rego.Voucher.RESERVATION_DURATION)

        # Else, it's the maximum of the included products
        items = rego.ProductItem.objects.filter(cart=self.cart)
        agg = items.aggregate(Max("product__reservation_duration"))
        product_max = agg["product__reservation_duration__max"]

        if product_max is not None:
            reservations.append(product_max)

        self.cart.time_last_updated = timezone.now()
        self.cart.reservation_duration = max(reservations)

    def end_batch(self):
        ''' Performs operations that occur occur at the end of a batch of
        product changes/voucher applications etc.
        THIS SHOULD BE PRIVATE
        '''

        self.recalculate_discounts()

        self.extend_reservation()
        self.cart.revision += 1
        self.cart.save()

    @transaction.atomic
    def set_quantities(self, product_quantities):

        items_in_cart = rego.ProductItem.objects.filter(cart=self.cart)

        # Remove all items that we're updating
        items_in_cart.filter(
            product__in=(i[0] for i in product_quantities),
        ).delete()

        all_product_quantities = list(product_quantities) + [
            (i.product, i.quantity) for i in items_in_cart.all()
        ]
        self._test_limits(all_product_quantities)

        for product, quantity in product_quantities:
            self._set_quantity_old(product, quantity)

        self.end_batch()

    def _test_limits(self, product_quantities):
        # Collect by category
        by_cat = collections.defaultdict(list)
        for product, quantity in product_quantities:
            by_cat[product.category].append((product, quantity))

        # Test each category limit here
        for cat in by_cat:
            ctrl = CategoryController(cat)
            limit = ctrl.user_quantity_remaining(self.cart.user)

            # Get the amount so far in the cart
            to_add = sum(i[1] for i in by_cat[cat])

            if to_add > limit:
                # TODO: batch errors
                raise ValidationError(
                    "You may only have %d items in category: %s" % (
                        limit, cat.name,
                    )
                )

        # Test each product limit here
        for product, quantity in product_quantities:
            prod = ProductController(product)
            limit = prod.user_quantity_remaining(self.cart.user)

            if quantity > limit:
                # TODO: batch errors
                raise ValidationError(
                    "You may only have %d of product: %s" % (
                        limit, cat.name,
                    )
                )

        # Test the enabling conditions
        errs = ConditionController.test_enabling_conditions(
            self.cart.user,
            product_quantities=product_quantities,
        )

        if errs:
            # TODO: batch errors
            raise ValidationError("An enabling condition failed")

    def set_quantity(self, product, quantity, batched=False):
        ''' Sets the _quantity_ of the given _product_ in the cart to the given
        _quantity_. '''

        self.set_quantities(((product, quantity),))

    def _set_quantity_old(self, product, quantity):
        ''' Sets the _quantity_ of the given _product_ in the cart to the given
        _quantity_. '''

        if quantity < 0:
            raise ValidationError("Cannot have fewer than 0 items in cart.")

        try:
            product_item = rego.ProductItem.objects.get(
                cart=self.cart,
                product=product)
            old_quantity = product_item.quantity

            if quantity == 0:
                product_item.delete()
                return
        except ObjectDoesNotExist:
            if quantity == 0:
                return

            product_item = rego.ProductItem.objects.create(
                cart=self.cart,
                product=product,
                quantity=0,
            )

            old_quantity = 0

        # Validate the addition to the cart
        adjustment = quantity - old_quantity
        prod = ProductController(product)

        product_item.quantity = quantity
        product_item.save()

    def add_to_cart(self, product, quantity):
        ''' Adds _quantity_ of the given _product_ to the cart. Raises
        ValidationError if constraints are violated.'''

        try:
            product_item = rego.ProductItem.objects.get(
                cart=self.cart,
                product=product)
            old_quantity = product_item.quantity
        except ObjectDoesNotExist:
            old_quantity = 0
        self.set_quantity(product, old_quantity + quantity)

    def apply_voucher(self, voucher_code):
        ''' Applies the voucher with the given code to this cart. '''

        # Is voucher exhausted?
        active_carts = rego.Cart.reserved_carts()

        # Try and find the voucher
        voucher = rego.Voucher.objects.get(code=voucher_code.upper())

        # It's invalid for a user to enter a voucher that's exhausted
        carts_with_voucher = active_carts.filter(vouchers=voucher)
        if len(carts_with_voucher) >= voucher.limit:
            raise ValidationError("This voucher is no longer available")

        # It's not valid for users to re-enter a voucher they already have
        user_carts_with_voucher = rego.Cart.objects.filter(
            user=self.cart.user,
            released=False,
            vouchers=voucher,
        )
        if len(user_carts_with_voucher) > 0:
            raise ValidationError("You have already entered this voucher.")

        # If successful...
        self.cart.vouchers.add(voucher)
        self.end_batch()

    def validate_cart(self):
        ''' Determines whether the status of the current cart is valid;
        this is normally called before generating or paying an invoice '''

        is_reserved = self.cart in rego.Cart.reserved_carts()

        # TODO: validate vouchers

        items = rego.ProductItem.objects.filter(cart=self.cart)

        product_quantities = list((i.product, i.quantity) for i in items)
        self._test_limits(product_quantities)

        # Validate the discounts
        discount_items = rego.DiscountItem.objects.filter(cart=self.cart)
        seen_discounts = set()

        for discount_item in discount_items:
            discount = discount_item.discount
            if discount in seen_discounts:
                continue
            seen_discounts.add(discount)
            real_discount = rego.DiscountBase.objects.get_subclass(
                pk=discount.pk)
            cond = ConditionController.for_condition(real_discount)

            if not cond.is_met(self.cart.user):
                raise ValidationError("Discounts are no longer available")

    def recalculate_discounts(self):
        ''' Calculates all of the discounts available for this product.
        NB should be transactional, and it's terribly inefficient.
        '''

        # Delete the existing entries.
        rego.DiscountItem.objects.filter(cart=self.cart).delete()

        product_items = self.cart.productitem_set.all()

        products = [i.product for i in product_items]
        discounts = discount.available_discounts(self.cart.user, [], products)

        # The highest-value discounts will apply to the highest-value
        # products first.
        product_items = self.cart.productitem_set.all()
        product_items = product_items.order_by('product__price')
        product_items = reversed(product_items)
        for item in product_items:
            self._add_discount(item.product, item.quantity, discounts)

    def _add_discount(self, product, quantity, discounts):
        ''' Applies the best discounts on the given product, from the given
        discounts.'''

        def matches(discount):
            ''' Returns True if and only if the given discount apples to
            our product. '''
            if isinstance(discount.clause, rego.DiscountForCategory):
                return discount.clause.category == product.category
            else:
                return discount.clause.product == product

        def value(discount):
            ''' Returns the value of this discount clause
            as applied to this product '''
            if discount.clause.percentage is not None:
                return discount.clause.percentage * product.price
            else:
                return discount.clause.price

        discounts = [i for i in discounts if matches(i)]
        discounts.sort(key=value)

        for candidate in reversed(discounts):
            if quantity == 0:
                break
            elif candidate.quantity == 0:
                # This discount clause has been exhausted by this cart
                continue

            # Get a provisional instance for this DiscountItem
            # with the quantity set to as much as we have in the cart
            discount_item = rego.DiscountItem.objects.create(
                product=product,
                cart=self.cart,
                discount=candidate.discount,
                quantity=quantity,
            )

            # Truncate the quantity for this DiscountItem if we exceed quantity
            ours = discount_item.quantity
            allowed = candidate.quantity
            if ours > allowed:
                discount_item.quantity = allowed
                # Update the remaining quantity.
                quantity = ours - allowed
            else:
                quantity = 0

            candidate.quantity -= discount_item.quantity

            discount_item.save()
<commit_msg>Adds negative quantity tests to _test_limits, and removes _set_quantity_old.<commit_after>import collections
import datetime
import discount
import itertools

from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Max, Sum
from django.utils import timezone

from registrasion import models as rego

from category import CategoryController
from conditions import ConditionController
from product import ProductController

class CartController(object):

    def __init__(self, cart):
        self.cart = cart

    @staticmethod
    def for_user(user):
        ''' Returns the user's current cart, or creates a new cart
        if there isn't one ready yet. '''

        try:
            existing = rego.Cart.objects.get(user=user, active=True)
        except ObjectDoesNotExist:
            existing = rego.Cart.objects.create(
                user=user,
                time_last_updated=timezone.now(),
                reservation_duration=datetime.timedelta(),
                 )
            existing.save()
        return CartController(existing)

    def extend_reservation(self):
        ''' Updates the cart's time last updated value, which is used to
        determine whether the cart has reserved the items and discounts it
        holds. '''

        reservations = [datetime.timedelta()]

        # If we have vouchers, we're entitled to an hour at minimum.
        if len(self.cart.vouchers.all()) >= 1:
            reservations.append(rego.Voucher.RESERVATION_DURATION)

        # Else, it's the maximum of the included products
        items = rego.ProductItem.objects.filter(cart=self.cart)
        agg = items.aggregate(Max("product__reservation_duration"))
        product_max = agg["product__reservation_duration__max"]

        if product_max is not None:
            reservations.append(product_max)

        self.cart.time_last_updated = timezone.now()
        self.cart.reservation_duration = max(reservations)

    def end_batch(self):
        ''' Performs operations that occur occur at the end of a batch of
        product changes/voucher applications etc.
        THIS SHOULD BE PRIVATE
        '''

        self.recalculate_discounts()

        self.extend_reservation()
        self.cart.revision += 1
        self.cart.save()

    @transaction.atomic
    def set_quantities(self, product_quantities):
        ''' Sets the quantities on each of the products on each of the
        products specified. Raises an exception (ValidationError) if a limit
        is violated. `product_quantities` is an iterable of (product, quantity)
        pairs. '''

        items_in_cart = rego.ProductItem.objects.filter(cart=self.cart)
        product_quantities = list(product_quantities)

        # n.b need to add have the existing items first so that the new
        # items override the old ones.
        all_product_quantities = dict(itertools.chain(
            ((i.product, i.quantity) for i in items_in_cart.all()),
            product_quantities,
        )).items()

        # Validate that the limits we're adding are OK
        self._test_limits(all_product_quantities)

        for product, quantity in product_quantities:
            try:
                product_item = rego.ProductItem.objects.get(
                    cart=self.cart,
                    product=product,
                )
                product_item.quantity = quantity
                product_item.save()
            except ObjectDoesNotExist:
                rego.ProductItem.objects.create(
                    cart=self.cart,
                    product=product,
                    quantity=quantity,
                )

        items_in_cart.filter(quantity=0).delete()

        self.end_batch()

    def _test_limits(self, product_quantities):
        ''' Tests that the quantity changes we intend to make do not violate
        the limits and enabling conditions imposed on the products. '''

        # Test each product limit here
        for product, quantity in product_quantities:
            if quantity < 0:
                # TODO: batch errors
                raise ValidationError("Value must be zero or greater.")

            prod = ProductController(product)
            limit = prod.user_quantity_remaining(self.cart.user)

            if quantity > limit:
                # TODO: batch errors
                raise ValidationError(
                    "You may only have %d of product: %s" % (
                        limit, product.name,
                    )
                )

        # Collect by category
        by_cat = collections.defaultdict(list)
        for product, quantity in product_quantities:
            by_cat[product.category].append((product, quantity))

        # Test each category limit here
        for cat in by_cat:
            ctrl = CategoryController(cat)
            limit = ctrl.user_quantity_remaining(self.cart.user)

            # Get the amount so far in the cart
            to_add = sum(i[1] for i in by_cat[cat])

            if to_add > limit:
                # TODO: batch errors
                raise ValidationError(
                    "You may only have %d items in category: %s" % (
                        limit, cat.name,
                    )
                )

        # Test the enabling conditions
        errs = ConditionController.test_enabling_conditions(
            self.cart.user,
            product_quantities=product_quantities,
        )

        if errs:
            # TODO: batch errors
            raise ValidationError("An enabling condition failed")

    def set_quantity(self, product, quantity, batched=False):
        ''' Sets the _quantity_ of the given _product_ in the cart to the given
        _quantity_. '''

        self.set_quantities(((product, quantity),))

    def add_to_cart(self, product, quantity):
        ''' Adds _quantity_ of the given _product_ to the cart. Raises
        ValidationError if constraints are violated.'''

        try:
            product_item = rego.ProductItem.objects.get(
                cart=self.cart,
                product=product)
            old_quantity = product_item.quantity
        except ObjectDoesNotExist:
            old_quantity = 0
        self.set_quantity(product, old_quantity + quantity)

    def apply_voucher(self, voucher_code):
        ''' Applies the voucher with the given code to this cart. '''

        # Is voucher exhausted?
        active_carts = rego.Cart.reserved_carts()

        # Try and find the voucher
        voucher = rego.Voucher.objects.get(code=voucher_code.upper())

        # It's invalid for a user to enter a voucher that's exhausted
        carts_with_voucher = active_carts.filter(vouchers=voucher)
        if len(carts_with_voucher) >= voucher.limit:
            raise ValidationError("This voucher is no longer available")

        # It's not valid for users to re-enter a voucher they already have
        user_carts_with_voucher = rego.Cart.objects.filter(
            user=self.cart.user,
            released=False,
            vouchers=voucher,
        )
        if len(user_carts_with_voucher) > 0:
            raise ValidationError("You have already entered this voucher.")

        # If successful...
        self.cart.vouchers.add(voucher)
        self.end_batch()

    def validate_cart(self):
        ''' Determines whether the status of the current cart is valid;
        this is normally called before generating or paying an invoice '''

        is_reserved = self.cart in rego.Cart.reserved_carts()

        # TODO: validate vouchers

        items = rego.ProductItem.objects.filter(cart=self.cart)

        product_quantities = list((i.product, i.quantity) for i in items)
        self._test_limits(product_quantities)

        # Validate the discounts
        discount_items = rego.DiscountItem.objects.filter(cart=self.cart)
        seen_discounts = set()

        for discount_item in discount_items:
            discount = discount_item.discount
            if discount in seen_discounts:
                continue
            seen_discounts.add(discount)
            real_discount = rego.DiscountBase.objects.get_subclass(
                pk=discount.pk)
            cond = ConditionController.for_condition(real_discount)

            if not cond.is_met(self.cart.user):
                raise ValidationError("Discounts are no longer available")

    def recalculate_discounts(self):
        ''' Calculates all of the discounts available for this product.
        NB should be transactional, and it's terribly inefficient.
        '''

        # Delete the existing entries.
        rego.DiscountItem.objects.filter(cart=self.cart).delete()

        product_items = self.cart.productitem_set.all()

        products = [i.product for i in product_items]
        discounts = discount.available_discounts(self.cart.user, [], products)

        # The highest-value discounts will apply to the highest-value
        # products first.
        product_items = self.cart.productitem_set.all()
        product_items = product_items.order_by('product__price')
        product_items = reversed(product_items)
        for item in product_items:
            self._add_discount(item.product, item.quantity, discounts)

    def _add_discount(self, product, quantity, discounts):
        ''' Applies the best discounts on the given product, from the given
        discounts.'''

        def matches(discount):
            ''' Returns True if and only if the given discount apples to
            our product. '''
            if isinstance(discount.clause, rego.DiscountForCategory):
                return discount.clause.category == product.category
            else:
                return discount.clause.product == product

        def value(discount):
            ''' Returns the value of this discount clause
            as applied to this product '''
            if discount.clause.percentage is not None:
                return discount.clause.percentage * product.price
            else:
                return discount.clause.price

        discounts = [i for i in discounts if matches(i)]
        discounts.sort(key=value)

        for candidate in reversed(discounts):
            if quantity == 0:
                break
            elif candidate.quantity == 0:
                # This discount clause has been exhausted by this cart
                continue

            # Get a provisional instance for this DiscountItem
            # with the quantity set to as much as we have in the cart
            discount_item = rego.DiscountItem.objects.create(
                product=product,
                cart=self.cart,
                discount=candidate.discount,
                quantity=quantity,
            )

            # Truncate the quantity for this DiscountItem if we exceed quantity
            ours = discount_item.quantity
            allowed = candidate.quantity
            if ours > allowed:
                discount_item.quantity = allowed
                # Update the remaining quantity.
                quantity = ours - allowed
            else:
                quantity = 0

            candidate.quantity -= discount_item.quantity

            discount_item.save()
<|endoftext|>


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

import random

ENABLE = 1
NEWS_ID = 31
NEWS_DEBUG = False

DEFAULT_WEIGHT = 10

def random_by_weight(p):
    choice_id = []
    for i in range(len(p)):
        choice_id += [i for _ in range(DEFAULT_WEIGHT + p[i]["news_weight"])]

    return p[random.choice(choice_id)]

def random_news():
    news_list = [
        {
            "news_title": "航海王奪寶 模咒逃走中",
            "news_image": "http://i.imgur.com/LVcpWi9.jpg",
            "news_url": "http://goo.gl/forms/6BbIyZXHdd",
            "news_content": "",
            "news_weight": 0
        }
    ]

    if NEWS_DEBUG:
        return news_list[0]
    else:
        return random_by_weight(news_list)

def news_status():
    return [ENABLE, NEWS_ID]

def news():
    """
    News for kuas.

    return [enable, news_id, news_title, news_template, news_url]
        enable: bool
        news_id: int
        news_title: string
        news_tempalte: string
        news_url: string
    """

    # Get news from random news
    news = random_news()

    news_title = news["news_title"]
    news_template = (
            "
"
            "
" + news["news_content"] + "
" +
            "
"

        )
    news_url = news["news_url"]

    return [ENABLE, NEWS_ID, news_title, news_template, news_url]
<commit_msg>Update news<commit_after># -*- coding: utf-8 -*-

import random

ENABLE = 1
NEWS_ID = 31
NEWS_DEBUG = False

DEFAULT_WEIGHT = 10

def random_by_weight(p):
    choice_id = []
    for i in range(len(p)):
        choice_id += [i for _ in range(DEFAULT_WEIGHT + p[i]["news_weight"])]

    return p[random.choice(choice_id)]

def random_news():
    news_list = [
        {
            "news_title": "航海王奪寶 模咒逃走中",
            "news_image": "http://i.imgur.com/LVcpWi9.jpg",
            "news_url": "http://goo.gl/forms/6BbIyZXHdd",
            "news_content": "",
            "news_weight": 0
        },
        {
            "news_title": "文創CCI發報團隊 ─ 句句到味",
            "news_image": "http://i.imgur.com/IoVDFVA.jpg",
            "news_url": "https://www.facebook.com/whataboutcci",
            "news_content": "",
            "news_weight": 0
        },
        {
            "news_title": "2015 高應大校園歌唱大賽",
            "news_image": "http://i.imgur.com/lhBojIw.jpg",
            "news_url": "https://www.facebook.com/pages/2015%E9%AB%98%E6%87%89%E5%A4%A7%E6%A0%A1%E5%9C%92%E6%AD%8C%E5%94%B1%E5%A4%A7%E8%B3%BD/503404693131297?fref=ts",
            "news_content": "",
            "news_weight": ""
        }

    ]

    if NEWS_DEBUG:
        return news_list[0]
    else:
        return random_by_weight(news_list)

def news_status():
    return [ENABLE, NEWS_ID]

def news():
    """
    News for kuas.

    return [enable, news_id, news_title, news_template, news_url]
        enable: bool
        news_id: int
        news_title: string
        news_tempalte: string
        news_url: string
    """

    # Get news from random news
    news = random_news()

    news_title = news["news_title"]
    news_template = (
            "
"
            "
" + news["news_content"] + "
" +
            "
"

        )
    news_url = news["news_url"]

    return [ENABLE, NEWS_ID, news_title, news_template, news_url]
<|endoftext|>


<commit_before>import random

import matplotlib.pyplot as plt
import networkx as nx

from louvain import utils

def generate_color_map(partition):
    communities = utils.partition_to_comm_nodes_map(partition).keys()
    color_map = {}
    r = lambda: random.randint(0, 255)
    for comm in communities:
        color_map[comm] = '#%02X%02X%02X' % (r(), r(), r())
    return color_map

def show_graph_communities(graph, partition, color_map=None):
    if color_map is None:
        color_map = generate_color_map(partition)
    pos = nx.spring_layout(graph)
    comm_nodes = utils.partition_to_comm_nodes_map(partition)
    for comm, nodes in comm_nodes.items():
        nx.draw_networkx_nodes(graph, pos, nodelist=nodes, node_size=400,
                               node_color=color_map.get(comm), label=comm, cmap=plt.cm.jet)
    nx.draw_networkx_labels(graph, pos, font_size=12)
    nx.draw_networkx_edges(graph, pos, alpha=0.3)
    plt.legend()
    plt.axis('off')
    plt.show()

def print_graph_communities(partition):
    comm_nodes = utils.partition_to_comm_nodes_map(partition)
    for comm, nodes in comm_nodes.items():
        print ("{}: {}".format(comm, nodes))<commit_msg>Added 'with_labels' parameter<commit_after>import random

import matplotlib.pyplot as plt
import networkx as nx

from louvain import utils

def generate_color_map(partition):
    communities = utils.partition_to_comm_nodes_map(partition).keys()
    color_map = {}
    r = lambda: random.randint(0, 255)
    for comm in communities:
        color_map[comm] = '#%02X%02X%02X' % (r(), r(), r())
    return color_map

def show_graph_communities(graph, partition, color_map=None, with_labels=False):
    if color_map is None:
        color_map = generate_color_map(partition)
    pos = nx.spring_layout(graph)
    comm_nodes = utils.partition_to_comm_nodes_map(partition)
    for comm, nodes in comm_nodes.items():
        nx.draw_networkx_nodes(graph, pos, nodelist=nodes, node_size=400,
                               node_color=color_map.get(comm), label=comm, cmap=plt.cm.jet)
    if with_labels:
        nx.draw_networkx_labels(graph, pos, font_size=12)
    nx.draw_networkx_edges(graph, pos, alpha=0.3)
    plt.legend()
    plt.axis('off')
    plt.show()

def print_graph_communities(partition):
    comm_nodes = utils.partition_to_comm_nodes_map(partition)
    for comm, nodes in comm_nodes.items():
        print ("{}: {}".format(comm, nodes))<|endoftext|>


<commit_before>"""Module containing class `Channel`."""

from vesper.util.named import Named

'''
A `Channel` is intended for use as a standalone object, and so has a
full set of attributes. For example, signal metadata like the time axis,
the sample array shape, and the sample type are available as channel
attributes, even though they are also available as attributes of the
channels' signal.

c.signal

c.number           # channel number, in [0, `c.signal.channel_count`]

c.name

c.time_axis        # `TimeAxis`

len(c)             # sample array count, `c.time_axis.length`

c.array_shape      # sample array shape

c.shape            # `(len(c),) + c.array_shape`

c.sample_type      # NumPy `dtype` of samples

r[...]             # synchronous, reads one channel segment, raises exception
                   # if not all of segment available

r.read(...)        # asynchronous, can read multiple channel segments, yields
                   # intersection of requested segments and available
                   # segments
'''

# TODO: Make channel name mutable?

class Channel(Named):

    """
    One channel of a signal.

    A channel is a sequence of sample arrays and associated metadata.
    """

    def __init__(self, signal, number, name=None):

        if name is None:
            name = str(number)

        super().__init__(name)

        self._signal = signal
        self._number = number

    @property
    def signal(self):
        return self._signal

    @property
    def number(self):
        return self._number

    @property
    def time_axis(self):
        return self.signal.time_axis

    def __len__(self):
        return self.time_axis.frame_count

    @property
    def array_shape(self):
        return self.signal.array_shape

    @property
    def shape(self):
        return (len(self),) + self.array_shape

    @property
    def sample_type(self):
        return self.signal.sample_type

    def __getitem__(self, key):

        # Build `Signal.as_channels` key from `Channel` key.
        if isinstance(key, tuple):
            key = (self.number,) + key
        else:
            key = (self.number, key)

        return self.signal.as_channels[key]
<commit_msg>Fix bug in signal channel class.<commit_after>"""Module containing class `Channel`."""

from vesper.util.named import Named

'''
A `Channel` is intended for use as a standalone object, and so has a
full set of attributes. For example, signal metadata like the time axis,
the sample array shape, and the sample type are available as channel
attributes, even though they are also available as attributes of the
channels' signal.

c.signal

c.number           # channel number, in [0, `c.signal.channel_count`]

c.name

c.time_axis        # `TimeAxis`

len(c)             # sample array count, `c.time_axis.length`

c.array_shape      # sample array shape

c.shape            # `(len(c),) + c.array_shape`

c.sample_type      # NumPy `dtype` of samples

r[...]             # synchronous, reads one channel segment, raises exception
                   # if not all of segment available

r.read(...)        # asynchronous, can read multiple channel segments, yields
                   # intersection of requested segments and available
                   # segments
'''

# TODO: Make channel name mutable?

class Channel(Named):

    """
    One channel of a signal.

    A channel is a sequence of sample arrays and associated metadata.
    """

    def __init__(self, signal, number, name=None):

        if name is None:
            name = str(number)

        super().__init__(name)

        self._signal = signal
        self._number = number

    @property
    def signal(self):
        return self._signal

    @property
    def number(self):
        return self._number

    @property
    def time_axis(self):
        return self.signal.time_axis

    def __len__(self):
        return self.time_axis.length

    @property
    def array_shape(self):
        return self.signal.array_shape

    @property
    def shape(self):
        return (len(self),) + self.array_shape

    @property
    def sample_type(self):
        return self.signal.sample_type

    def __getitem__(self, key):

        # Build `Signal.as_channels` key from `Channel` key.
        if isinstance(key, tuple):
            key = (self.number,) + key
        else:
            key = (self.number, key)

        return self.signal.as_channels[key]
<|endoftext|>