<commit_before>import logging

import os.path
import os
import yaml
import uuid
import time
import json

from flask import request
from flask import jsonify

import flask.ext.login

from . import app
from . import SR

LOG = logging.getLogger(__name__)
FEED_INTERVAL = 100
REDIS_KEY_PREFIX = 'mm:config:'
REDIS_KEY_CONFIG = REDIS_KEY_PREFIX+'candidate'
REDIS_NODES_LIST = 'nodes'
LOCK_TIMEOUT = 3000

class VersionMismatchError(Exception):
    pass

class MMConfigVersion(object):
    def __init__(self, version=None):
        if version is None:
            self.config = str(uuid.uuid4())
            self.counter = 0
            return

        LOG.error('version: %s', version)

        self.config, self.counter = version.split('+', 1)
        self.counter = int(self.counter)

    def __str__(self):
        return '%s+%d' % (self.config, self.counter)

    def __repr__(self):
        return 'MMConfigVersion(%s+%d)' % (self.config, self.counter)

    def __eq__(self, other):
        return self.config == other.config and self.counter == other.counter

    def __ne__(self, other):
        return not self.__eq__(other)

    def __iadd__(self, y):
        self.counter += y
        return self

def _lock(resource):
    resname = resource+':lock'
    value = str(uuid.uuid4())
    result = SR.set(resname, value,
                    nx=True, px=LOCK_TIMEOUT)

    if result is None:
        return None

    return value

def _lock_timeout(resource, timeout=30):
    t1 = time.time()
    tt = t1+timeout

    while t1 < tt:
        result = _lock(resource)
        if result is not None:
            return result

        t1 = time.sleep(0.01)

    return None

def _unlock(resource, value):
    resname = resource+':lock'
    result = SR.get(resname)

    if result == value:
        SR.delete(resname)
        return True

    LOG.error('lost lock %s - %s', value, result)

    return False

def _redlock(f):
    def _redlocked(*args, **kwargs):
        lock = kwargs.pop('lock', False)
        timeout = kwargs.pop('timeout', 30)

        if lock:
            clock = _lock_timeout(REDIS_KEY_CONFIG, timeout=timeout)
            if clock is None:
                raise ValueError('Unable to lock config')
            LOG.info('lock set %s', clock)

        result = f(*args, **kwargs)

        if lock:
            _unlock(REDIS_KEY_CONFIG, clock)
            LOG.info('lock cleared %s', clock)

        return result

    return _redlocked

def _set_stanza(stanza, value, version, config_key=REDIS_KEY_CONFIG):
    version_key = stanza+':version'
    cversion = SR.hget(config_key, version_key)
    if cversion is not None:
        if version != MMConfigVersion(version=cversion):
            raise VersionMismatchError('version mismatch, current version %s' % cversion)
        version += 1

    SR.hset(config_key, version_key, str(version))
    SR.hset(config_key, stanza, json.dumps(value))

    return version

@_redlock
def _get_stanza(stanza, config_key=REDIS_KEY_CONFIG):
    version_key = stanza+':version'

    version = SR.hget(config_key, version_key)
    if version is None:
        return None

    value = SR.hget(config_key, stanza)
    if value is None:
        return None

    value = json.loads(value)
    value['version'] = version

    return value

def _load_running_config():
    rcpath = os.path.join(
        os.path.dirname(os.environ.get('MM_CONFIG')),
        'running-config.yml'
    )
    with open(rcpath, 'r') as f:
        rcconfig = yaml.safe_load(f)

    version = MMConfigVersion()
    tempconfigkey = REDIS_KEY_PREFIX+str(version)

    SR.hset(tempconfigkey, 'version', version.config)

    if 'fabric' in rcconfig:
        _set_stanza(
            'fabric',
            {'name': 'fabric', 'properties': rcconfig['fabric']},
            config_key=tempconfigkey,
            version=version
        )

    if 'mgmtbus' in rcconfig:
        _set_stanza(
            'mgmtbus',
            {'name': 'mgmtbus', 'properties': rcconfig['mgmtbus']},
            config_key=tempconfigkey,
            version=version
        )

    nodes = rcconfig.get('nodes', {})
    for idx, (nodename, nodevalue) in enumerate(nodes.iteritems()):
        _set_stanza(
            'node%d' % idx,
            {'name': nodename, 'properties': nodevalue},
            config_key=tempconfigkey,
            version=version
        )

    clock = _lock_timeout(REDIS_KEY_CONFIG)
    if clock is None:
        SR.delete(tempconfigkey)
        raise ValueError('Unable to lock config')

    SR.delete(REDIS_KEY_CONFIG)
    SR.rename(tempconfigkey, REDIS_KEY_CONFIG)

    _unlock(REDIS_KEY_CONFIG, clock)

    return version.config

def _commit_config(version):
    ccpath = os.path.join(
        os.path.dirname(os.environ.get('MM_CONFIG')),
        'candidate-config.yml'
    )

    clock = _lock_timeout(REDIS_KEY_CONFIG)
    if clock is None:
        raise ValueError('Unable to lock config')

    config_info = _config_info()

    if version != config_info['version']:
        raise VersionMismatchError('Versions mismatch')

    newconfig = {}

    fabric = _get_stanza('fabric')
    if fabric is not None:
        newconfig['fabric'] = json.loads(fabric)['properties']

    mgmtbus = _get_stanza('mgmtbus')
    if mgmtbus is not None:
        newconfig['mgmtbus'] = json.loads(mgmtbus)['properties']

    newconfig['nodes'] = {}
    for n in range(config_info['num_nodes']):
        node = _get_stanza('node%d' % n)
        if node['name'] in newconfig:
            raise ValueError('Error in config: duplicate node name - %s' %
                             node['name'])
        if 'properties' not in node:
            raise ValueError('Error in config: no properties for node %s' %
                             node['name'])
        newconfig['nodes'][node['name']] = node['properties']

    _unlock(REDIS_KEY_CONFIG, clock)

    with open(ccpath, 'w') as f:
        yaml.safe_dump(
            newconfig,
            f,
            encoding='utf-8',
            default_flow_style=False
        )

    return 'OK'

@_redlock
def _config_info():
    version = SR.hget(REDIS_KEY_CONFIG, 'version')
    if version is None:
        raise ValueError('candidate config not initialized')

    fabric = SR.hget(REDIS_KEY_CONFIG, 'fabric') is not None
    mgmtbus = SR.hget(REDIS_KEY_CONFIG, 'mgmtbus') is not None

    numnodes = (SR.hlen(REDIS_KEY_CONFIG)-1)/2
    if fabric:
        numnodes -= 1
    if mgmtbus:
        numnodes -= 1

    return {
        'fabric': fabric,
        'mgmtbus': mgmtbus,
        'version': version,
        'num_nodes': numnodes
    }

@_redlock
def _create_node(nodebody):
    info = _config_info()

    version = nodebody.pop('version', None)
    if version != config_info['version']:
        raise ValueError('version mismatch')

    cversion = MMConfigVersion(version=info['version']+'+0')

    _set_stanza(
        'node%d' % info['num_nodes'],
        nodebody,
        MMConfigVersion(version=info['version']+'+0')
    )

    return {
        'version': str(version),
        'id': info['num_nodes']
    }

@_redlock
def _delete_node(nodenum, version):
    node = _get_stanza('node%d' % nodenum)
    if node is None:
        raise ValueError('node %d does not exist' % nodenum)

    if MMConfigVersion(version=version) != MMConfigVersion(node['version']):
        raise VersionMismatchError('version mismatch')

    SR.hdel(REDIS_KEY_CONFIG, 'node%d' % nodenum)

    return 'OK'

@_redlock
def _set_node(nodenum, nodebody):
    if 'version' not in nodebody:
        raise ValueError('version is required')
    version = MMConfigVersion(version=nodebody.pop('version'))

    result = _set_stanza(
        'node%d' % nodenum,
        nodebody,
        version,
    )

    return str(result)

@app.route('/config/reload', methods=['GET'])
@flask.ext.login.login_required
def reload_running_config():
    LOG.error('current user: %s', flask.ext.login.current_user.get_id())

    try:
        version = _load_running_config()
    except Exception as e:
        LOG.exception('Error in _load_running_config')
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=str(version))

@app.route('/config/commit', methods=['POST'])
def commit():
    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    version = body.get('version', None)
    if body is None:
        return jsonify(error={'message': 'version required'}), 400

    try:
        _commit_config(version)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        LOG.exception('exception in commit')
        return jsonify(error={'message': str(e)}), 400

    return jsonify(result='OK')

@app.route('/config/info', methods=['GET'])
def get_config_info():
    try:
        result = _config_info(lock=True)
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)

@app.route('/config/fabric', methods=['GET'])
def get_fabric():
    try:
        result = _get_stanza('fabric', lock=True)
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    if result is None:
        return jsonify(error={'message': 'Not Found'}), 404

    return jsonify(result=result)

@app.route('/config/mgmtbus', methods=['GET'])
def get_mgmtbus():
    try:
        result = _get_stanza('mgmtbus', lock=True)
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    if result is None:
        return jsonify(error={'message': 'Not Found'}), 404

    return jsonify(result=result)

@app.route('/config/node', methods=['POST'])
def create_node():
    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    try:
        result = _create_node(body, lock=True)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)

@app.route('/config/node/<nodenum>', methods=['GET'])
def get_node(nodenum):
    try:
        nodenum = int(nodenum)
    except ValueError:
        return jsonify(error='invalid node number'), 400

    try:
        result = _get_stanza('node%d' % nodenum, lock=True)
    except Exception as e:
        LOG.exception('error in get_node')
        return jsonify(error={'message': str(e)}), 500

    if result is None:
        return jsonify(error={'message': 'Not Found'}), 404

    return jsonify(result=result)

@app.route('/config/node/<nodenum>', methods=['PUT'])
def set_node(nodenum):
    try:
        nodenum = int(nodenum)
    except ValueError:
        return jsonify(error='invalid node number'), 400

    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    try:
        result = _set_node(nodenum, body, lock=True)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)

@app.route('/config/node/<nodenum>', methods=['DELETE'])
def delete_node(nodenum):
    try:
        nodenum = int(nodenum)
    except ValueError:
        return jsonify(error='invalid node number'), 400

    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    version = body.get('version', None)
    if version is None:
        return jsonify(error={'message': 'version required'})

    try:
        result = _delete_node(nodenum, version, lock=True)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)
<commit_msg>Added authentication<commit_after>import logging

import os.path
import os
import yaml
import uuid
import time
import json

from flask import request
from flask import jsonify

import flask.ext.login

from . import app
from . import SR

LOG = logging.getLogger(__name__)
FEED_INTERVAL = 100
REDIS_KEY_PREFIX = 'mm:config:'
REDIS_KEY_CONFIG = REDIS_KEY_PREFIX+'candidate'
REDIS_NODES_LIST = 'nodes'
LOCK_TIMEOUT = 3000

class VersionMismatchError(Exception):
    pass

class MMConfigVersion(object):
    def __init__(self, version=None):
        if version is None:
            self.config = str(uuid.uuid4())
            self.counter = 0
            return

        LOG.error('version: %s', version)

        self.config, self.counter = version.split('+', 1)
        self.counter = int(self.counter)

    def __str__(self):
        return '%s+%d' % (self.config, self.counter)

    def __repr__(self):
        return 'MMConfigVersion(%s+%d)' % (self.config, self.counter)

    def __eq__(self, other):
        return self.config == other.config and self.counter == other.counter

    def __ne__(self, other):
        return not self.__eq__(other)

    def __iadd__(self, y):
        self.counter += y
        return self

def _lock(resource):
    resname = resource+':lock'
    value = str(uuid.uuid4())
    result = SR.set(resname, value,
                    nx=True, px=LOCK_TIMEOUT)

    if result is None:
        return None

    return value

def _lock_timeout(resource, timeout=30):
    t1 = time.time()
    tt = t1+timeout

    while t1 < tt:
        result = _lock(resource)
        if result is not None:
            return result

        t1 = time.sleep(0.01)

    return None

def _unlock(resource, value):
    resname = resource+':lock'
    result = SR.get(resname)

    if result == value:
        SR.delete(resname)
        return True

    LOG.error('lost lock %s - %s', value, result)

    return False

def _redlock(f):
    def _redlocked(*args, **kwargs):
        lock = kwargs.pop('lock', False)
        timeout = kwargs.pop('timeout', 30)

        if lock:
            clock = _lock_timeout(REDIS_KEY_CONFIG, timeout=timeout)
            if clock is None:
                raise ValueError('Unable to lock config')
            LOG.info('lock set %s', clock)

        result = f(*args, **kwargs)

        if lock:
            _unlock(REDIS_KEY_CONFIG, clock)
            LOG.info('lock cleared %s', clock)

        return result

    return _redlocked

def _set_stanza(stanza, value, version, config_key=REDIS_KEY_CONFIG):
    version_key = stanza+':version'
    cversion = SR.hget(config_key, version_key)
    if cversion is not None:
        if version != MMConfigVersion(version=cversion):
            raise VersionMismatchError('version mismatch, current version %s' % cversion)
        version += 1

    SR.hset(config_key, version_key, str(version))
    SR.hset(config_key, stanza, json.dumps(value))

    return version

@_redlock
def _get_stanza(stanza, config_key=REDIS_KEY_CONFIG):
    version_key = stanza+':version'

    version = SR.hget(config_key, version_key)
    if version is None:
        return None

    value = SR.hget(config_key, stanza)
    if value is None:
        return None

    value = json.loads(value)
    value['version'] = version

    return value

def _load_running_config():
    rcpath = os.path.join(
        os.path.dirname(os.environ.get('MM_CONFIG')),
        'running-config.yml'
    )
    with open(rcpath, 'r') as f:
        rcconfig = yaml.safe_load(f)

    version = MMConfigVersion()
    tempconfigkey = REDIS_KEY_PREFIX+str(version)

    SR.hset(tempconfigkey, 'version', version.config)

    if 'fabric' in rcconfig:
        _set_stanza(
            'fabric',
            {'name': 'fabric', 'properties': rcconfig['fabric']},
            config_key=tempconfigkey,
            version=version
        )

    if 'mgmtbus' in rcconfig:
        _set_stanza(
            'mgmtbus',
            {'name': 'mgmtbus', 'properties': rcconfig['mgmtbus']},
            config_key=tempconfigkey,
            version=version
        )

    nodes = rcconfig.get('nodes', {})
    for idx, (nodename, nodevalue) in enumerate(nodes.iteritems()):
        _set_stanza(
            'node%d' % idx,
            {'name': nodename, 'properties': nodevalue},
            config_key=tempconfigkey,
            version=version
        )

    clock = _lock_timeout(REDIS_KEY_CONFIG)
    if clock is None:
        SR.delete(tempconfigkey)
        raise ValueError('Unable to lock config')

    SR.delete(REDIS_KEY_CONFIG)
    SR.rename(tempconfigkey, REDIS_KEY_CONFIG)

    _unlock(REDIS_KEY_CONFIG, clock)

    return version.config

def _commit_config(version):
    ccpath = os.path.join(
        os.path.dirname(os.environ.get('MM_CONFIG')),
        'candidate-config.yml'
    )

    clock = _lock_timeout(REDIS_KEY_CONFIG)
    if clock is None:
        raise ValueError('Unable to lock config')

    config_info = _config_info()

    if version != config_info['version']:
        raise VersionMismatchError('Versions mismatch')

    newconfig = {}

    fabric = _get_stanza('fabric')
    if fabric is not None:
        newconfig['fabric'] = json.loads(fabric)['properties']

    mgmtbus = _get_stanza('mgmtbus')
    if mgmtbus is not None:
        newconfig['mgmtbus'] = json.loads(mgmtbus)['properties']

    newconfig['nodes'] = {}
    for n in range(config_info['num_nodes']):
        node = _get_stanza('node%d' % n)
        if node['name'] in newconfig:
            raise ValueError('Error in config: duplicate node name - %s' %
                             node['name'])
        if 'properties' not in node:
            raise ValueError('Error in config: no properties for node %s' %
                             node['name'])
        newconfig['nodes'][node['name']] = node['properties']

    _unlock(REDIS_KEY_CONFIG, clock)

    with open(ccpath, 'w') as f:
        yaml.safe_dump(
            newconfig,
            f,
            encoding='utf-8',
            default_flow_style=False
        )

    return 'OK'

@_redlock
def _config_info():
    version = SR.hget(REDIS_KEY_CONFIG, 'version')
    if version is None:
        raise ValueError('candidate config not initialized')

    fabric = SR.hget(REDIS_KEY_CONFIG, 'fabric') is not None
    mgmtbus = SR.hget(REDIS_KEY_CONFIG, 'mgmtbus') is not None

    numnodes = (SR.hlen(REDIS_KEY_CONFIG)-1)/2
    if fabric:
        numnodes -= 1
    if mgmtbus:
        numnodes -= 1

    return {
        'fabric': fabric,
        'mgmtbus': mgmtbus,
        'version': version,
        'num_nodes': numnodes
    }

@_redlock
def _create_node(nodebody):
    info = _config_info()

    version = nodebody.pop('version', None)
    if version != info['version']:
        raise ValueError('version mismatch')

    cversion = MMConfigVersion(version=info['version']+'+0')

    _set_stanza(
        'node%d' % info['num_nodes'],
        nodebody,
        cversion
    )

    return {
        'version': str(cversion),
        'id': info['num_nodes']
    }

@_redlock
def _delete_node(nodenum, version):
    node = _get_stanza('node%d' % nodenum)
    if node is None:
        raise ValueError('node %d does not exist' % nodenum)

    if MMConfigVersion(version=version) != MMConfigVersion(node['version']):
        raise VersionMismatchError('version mismatch')

    SR.hdel(REDIS_KEY_CONFIG, 'node%d' % nodenum)
    SR.hdel(REDIS_KEY_CONFIG, 'node%d:version' % nodenum)

    return 'OK'

@_redlock
def _set_node(nodenum, nodebody):
    if 'version' not in nodebody:
        raise ValueError('version is required')
    version = MMConfigVersion(version=nodebody.pop('version'))

    result = _set_stanza(
        'node%d' % nodenum,
        nodebody,
        version,
    )

    return str(result)

@app.route('/config/reload', methods=['GET'])
@flask.ext.login.login_required
def reload_running_config():
    LOG.error('current user: %s', flask.ext.login.current_user.get_id())

    try:
        version = _load_running_config()
    except Exception as e:
        LOG.exception('Error in _load_running_config')
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=str(version))

@app.route('/config/commit', methods=['POST'])
@flask.ext.login.login_required
def commit():
    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    version = body.get('version', None)
    if body is None:
        return jsonify(error={'message': 'version required'}), 400

    try:
        _commit_config(version)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        LOG.exception('exception in commit')
        return jsonify(error={'message': str(e)}), 400

    return jsonify(result='OK')

@app.route('/config/info', methods=['GET'])
@flask.ext.login.login_required
def get_config_info():
    try:
        result = _config_info(lock=True)
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)

@app.route('/config/fabric', methods=['GET'])
@flask.ext.login.login_required
def get_fabric():
    try:
        result = _get_stanza('fabric', lock=True)
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    if result is None:
        return jsonify(error={'message': 'Not Found'}), 404

    return jsonify(result=result)

@app.route('/config/mgmtbus', methods=['GET'])
@flask.ext.login.login_required
def get_mgmtbus():
    try:
        result = _get_stanza('mgmtbus', lock=True)
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    if result is None:
        return jsonify(error={'message': 'Not Found'}), 404

    return jsonify(result=result)

@app.route('/config/node', methods=['POST'])
@flask.ext.login.login_required
def create_node():
    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    try:
        result = _create_node(body, lock=True)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)

@app.route('/config/node/<nodenum>', methods=['GET'])
@flask.ext.login.login_required
def get_node(nodenum):
    try:
        nodenum = int(nodenum)
    except ValueError:
        return jsonify(error='invalid node number'), 400

    try:
        result = _get_stanza('node%d' % nodenum, lock=True)
    except Exception as e:
        LOG.exception('error in get_node')
        return jsonify(error={'message': str(e)}), 500

    if result is None:
        return jsonify(error={'message': 'Not Found'}), 404

    return jsonify(result=result)

@app.route('/config/node/<nodenum>', methods=['PUT'])
@flask.ext.login.login_required
def set_node(nodenum):
    try:
        nodenum = int(nodenum)
    except ValueError:
        return jsonify(error='invalid node number'), 400

    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    try:
        result = _set_node(nodenum, body, lock=True)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        LOG.exception('exception is _set_node')
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)

@app.route('/config/node/<nodenum>', methods=['DELETE'])
@flask.ext.login.login_required
def delete_node(nodenum):
    try:
        nodenum = int(nodenum)
    except ValueError:
        return jsonify(error='invalid node number'), 400

    try:
        body = request.get_json()
    except Exception as e:
        return jsonify(error={'message': str(e)}), 400

    version = body.get('version', None)
    if version is None:
        return jsonify(error={'message': 'version required'})

    try:
        result = _delete_node(nodenum, version, lock=True)
    except VersionMismatchError:
        return jsonify(error={'message': 'version mismatch'}), 409
    except Exception as e:
        return jsonify(error={'message': str(e)}), 500

    return jsonify(result=result)
<|endoftext|>


<commit_before># -*- test-case-name: automat._test.test_methodical -*-

from functools import wraps
from itertools import count
from inspect import getargspec

from characteristic import attributes, Attribute

from ._core import Transitioner, Automaton
from ._introspection import preserveName

def _keywords_only(f):
    """
    Decorate a function so all its arguments must be passed by keyword.

    A useful utility for decorators that take arguments so that they don't
    accidentally get passed the thing they're decorating as their first
    argument.

    Only works for methods right now.
    """
    @wraps(f)
    def g(self, **kw):
        return f(self, **kw)
    return g

@attributes(['machine', 'method'])
class MethodicalState(object):
    """
    A state for a L{MethodicalMachine}.
    """

    def upon(self, input, enter, outputs, collector=list):
        """
        Declare a state transition within the L{MethodicalMachine} associated
        with this L{MethodicalState}: upon the receipt of the input C{input},
        enter the state C{enter}, emitting each output in C{outputs}.
        """
        inputSpec = getargspec(input.method)
        for output in outputs:
            outputSpec = getargspec(output.method)
            if inputSpec != outputSpec:
                raise TypeError(
                    "method {input} signature {inputSignature} "
                    "does not match output {output} "
                    "signature {outputSignature}".format(
                        input=input.method.__name__,
                        output=output.method.__name__,
                        inputSignature=inputSpec,
                        outputSignature=outputSpec,
                ))
        self.machine._oneTransition(self, input, enter, outputs, collector)

def _transitionerFromInstance(oself, symbol, automaton):
    """
    Get a L{Transitioner}
    """
    transitioner = getattr(oself, symbol, None)
    if transitioner is None:
        transitioner = Transitioner(
            automaton,
            # FIXME: public API on Automaton for getting the initial state.
            list(automaton._initialStates)[0],
        )
        setattr(oself, symbol, transitioner)
    return transitioner

@attributes(['automaton', 'method', 'symbol',
             Attribute('collectors', default_factory=dict)],
            apply_with_cmp=False)
class MethodicalInput(object):
    """
    An input for a L{MethodicalMachine}.
    """

    def __get__(self, oself, type=None):
        """
        Return a function that takes no arguments and returns values returned
        by output functions produced by the given L{MethodicalInput} in
        C{oself}'s current state.
        """
        transitioner = _transitionerFromInstance(oself, self.symbol,
                                                 self.automaton)
        @preserveName(self.method)
        @wraps(self.method)
        def doInput(*args, **kwargs):
            self.method(oself, *args, **kwargs)
            collector = self.collectors[transitioner._state]
            return collector(output(oself, *args, **kwargs)
                             for output in transitioner.transition(self))
        return doInput

@attributes(['machine', 'method'])
class MethodicalOutput(object):
    """
    An output for a L{MethodicalMachine}.
    """

    def __get__(self, oself, type=None):
        """
        Outputs are private, so raise an exception when we attempt to get one.
        """
        raise AttributeError(
            "{cls}.{method} is a state-machine output method; "
            "to produce this output, call an input method instead.".format(
                cls=type.__name__,
                method=self.method.__name__
            )
        )

    def __call__(self, oself, *args, **kwargs):
        """
        Call the underlying method.
        """
        return self.method(oself, *args, **kwargs)

counter = count()
def gensym():
    """
    Create a unique Python identifier.
    """
    return "_symbol_" + str(next(counter))

class MethodicalMachine(object):
    """
    A L{MethodicalMachine} is an interface to an L{Automaton} that uses methods
    on a class.
    """

    def __init__(self):
        self._automaton = Automaton()
        self._reducers = {}
        self._symbol = gensym()

    def __get__(self, oself, type=None):
        """
        L{MethodicalMachine} is an implementation detail for setting up
        class-level state; applications should never need to access it on an
        instance.
        """
        if oself is not None:
            raise AttributeError(
                "MethodicalMachine is an implementation detail.")
        return self

    @_keywords_only
    def state(self, initial=False, terminal=False):
        """
        Declare a state, possibly an initial state or a terminal state.

        This is a decorator for methods, but it will modify the method so as
        not to be callable any more.

        @param initial: is this state the initial state?  Only one state on
            this L{MethodicalMachine} may be an initial state; more than one is
            an error.
        @type initial: L{bool}

        @param terminal: Is this state a terminal state, i.e. a state that the
            machine can end up in?  (This is purely informational at this
            point.)
        @type terminal: L{bool}
        """
        def decorator(stateMethod):
            state = MethodicalState(machine=self,
                                    method=stateMethod)
            if initial:
                self._automaton.addInitialState(state)
            return state
        return decorator

    @_keywords_only
    def input(self):
        """
        Declare an input.

        This is a decorator for methods.
        """
        def decorator(inputMethod):
            return MethodicalInput(automaton=self._automaton,
                                   method=inputMethod,
                                   symbol=self._symbol)
        return decorator

    @_keywords_only
    def output(self):
        """
        Declare an output.

        This is a decorator for methods.

        This method will be called when the state machine transitions to this
        state as specified in the L{MethodicalMachine.output} method.
        """
        def decorator(outputMethod):
            return MethodicalOutput(machine=self, method=outputMethod)
        return decorator

    def _oneTransition(self, startState, inputToken, endState, outputTokens,
                       collector):
        """
        See L{MethodicalState.upon}.
        """
        # FIXME: tests for all of this (some of it is wrong)
        # if not isinstance(startState, MethodicalState):
        #     raise NotImplementedError("start state {} isn't a state"
        #                               .format(startState))
        # if not isinstance(inputToken, MethodicalInput):
        #     raise NotImplementedError("start state {} isn't an input"
        #                               .format(inputToken))
        # if not isinstance(endState, MethodicalState):
        #     raise NotImplementedError("end state {} isn't a state"
        #                               .format(startState))
        # for output in outputTokens:
        #     if not isinstance(endState, MethodicalState):
        #         raise NotImplementedError("output state {} isn't a state"
        #                                   .format(endState))
        self._automaton.addTransition(startState, inputToken, endState,
                                      tuple(outputTokens))
        inputToken.collectors[startState] = collector

    def graphviz(self):
        """
        Visualize this state machine using graphviz.

        @return: an iterable of lines of graphviz-format data suitable for
            feeding to C{dot} or C{neato} which visualizes the state machine
            described by this L{MethodicalMachine}.
        """
        from ._visualize import graphviz
        for line in graphviz(
                self._automaton,
                stateAsString=lambda state: state.method.__name__,
                inputAsString=lambda input: input.method.__name__,
                outputAsString=lambda output: output.method.__name__,
        ):
            yield line
<commit_msg>new 'serialized' parameter<commit_after># -*- test-case-name: automat._test.test_methodical -*-

from functools import wraps
from itertools import count
from inspect import getargspec

from characteristic import attributes, Attribute

from ._core import Transitioner, Automaton
from ._introspection import preserveName

def _keywords_only(f):
    """
    Decorate a function so all its arguments must be passed by keyword.

    A useful utility for decorators that take arguments so that they don't
    accidentally get passed the thing they're decorating as their first
    argument.

    Only works for methods right now.
    """
    @wraps(f)
    def g(self, **kw):
        return f(self, **kw)
    return g

@attributes(['machine', 'method', 'serialized'])
class MethodicalState(object):
    """
    A state for a L{MethodicalMachine}.
    """

    def upon(self, input, enter, outputs, collector=list):
        """
        Declare a state transition within the L{MethodicalMachine} associated
        with this L{MethodicalState}: upon the receipt of the input C{input},
        enter the state C{enter}, emitting each output in C{outputs}.
        """
        inputSpec = getargspec(input.method)
        for output in outputs:
            outputSpec = getargspec(output.method)
            if inputSpec != outputSpec:
                raise TypeError(
                    "method {input} signature {inputSignature} "
                    "does not match output {output} "
                    "signature {outputSignature}".format(
                        input=input.method.__name__,
                        output=output.method.__name__,
                        inputSignature=inputSpec,
                        outputSignature=outputSpec,
                ))
        self.machine._oneTransition(self, input, enter, outputs, collector)

def _transitionerFromInstance(oself, symbol, automaton):
    """
    Get a L{Transitioner}
    """
    transitioner = getattr(oself, symbol, None)
    if transitioner is None:
        transitioner = Transitioner(
            automaton,
            # FIXME: public API on Automaton for getting the initial state.
            list(automaton._initialStates)[0],
        )
        setattr(oself, symbol, transitioner)
    return transitioner

@attributes(['automaton', 'method', 'symbol',
             Attribute('collectors', default_factory=dict)],
            apply_with_cmp=False)
class MethodicalInput(object):
    """
    An input for a L{MethodicalMachine}.
    """

    def __get__(self, oself, type=None):
        """
        Return a function that takes no arguments and returns values returned
        by output functions produced by the given L{MethodicalInput} in
        C{oself}'s current state.
        """
        transitioner = _transitionerFromInstance(oself, self.symbol,
                                                 self.automaton)
        @preserveName(self.method)
        @wraps(self.method)
        def doInput(*args, **kwargs):
            self.method(oself, *args, **kwargs)
            collector = self.collectors[transitioner._state]
            return collector(output(oself, *args, **kwargs)
                             for output in transitioner.transition(self))
        return doInput

@attributes(['machine', 'method'])
class MethodicalOutput(object):
    """
    An output for a L{MethodicalMachine}.
    """

    def __get__(self, oself, type=None):
        """
        Outputs are private, so raise an exception when we attempt to get one.
        """
        raise AttributeError(
            "{cls}.{method} is a state-machine output method; "
            "to produce this output, call an input method instead.".format(
                cls=type.__name__,
                method=self.method.__name__
            )
        )

    def __call__(self, oself, *args, **kwargs):
        """
        Call the underlying method.
        """
        return self.method(oself, *args, **kwargs)

counter = count()
def gensym():
    """
    Create a unique Python identifier.
    """
    return "_symbol_" + str(next(counter))

class MethodicalMachine(object):
    """
    A L{MethodicalMachine} is an interface to an L{Automaton} that uses methods
    on a class.
    """

    def __init__(self):
        self._automaton = Automaton()
        self._reducers = {}
        self._symbol = gensym()

    def __get__(self, oself, type=None):
        """
        L{MethodicalMachine} is an implementation detail for setting up
        class-level state; applications should never need to access it on an
        instance.
        """
        if oself is not None:
            raise AttributeError(
                "MethodicalMachine is an implementation detail.")
        return self

    @_keywords_only
    def state(self, initial=False, terminal=False,
              serialized=None):
        """
        Declare a state, possibly an initial state or a terminal state.

        This is a decorator for methods, but it will modify the method so as
        not to be callable any more.

        @param initial: is this state the initial state?  Only one state on
            this L{MethodicalMachine} may be an initial state; more than one is
            an error.
        @type initial: L{bool}

        @param terminal: Is this state a terminal state, i.e. a state that the
            machine can end up in?  (This is purely informational at this
            point.)
        @type terminal: L{bool}

        @param serialized: a serializable value to be used to represent this
            state to external systems.  This value should be hashable;
            L{unicode} is a good type to use.
        @type serialized: a hashable (comparable) value
        """
        def decorator(stateMethod):
            state = MethodicalState(machine=self,
                                    method=stateMethod,
                                    serialized=serialized)
            if initial:
                self._automaton.addInitialState(state)
            return state
        return decorator

    @_keywords_only
    def input(self):
        """
        Declare an input.

        This is a decorator for methods.
        """
        def decorator(inputMethod):
            return MethodicalInput(automaton=self._automaton,
                                   method=inputMethod,
                                   symbol=self._symbol)
        return decorator

    @_keywords_only
    def output(self):
        """
        Declare an output.

        This is a decorator for methods.

        This method will be called when the state machine transitions to this
        state as specified in the L{MethodicalMachine.output} method.
        """
        def decorator(outputMethod):
            return MethodicalOutput(machine=self, method=outputMethod)
        return decorator

    def _oneTransition(self, startState, inputToken, endState, outputTokens,
                       collector):
        """
        See L{MethodicalState.upon}.
        """
        # FIXME: tests for all of this (some of it is wrong)
        # if not isinstance(startState, MethodicalState):
        #     raise NotImplementedError("start state {} isn't a state"
        #                               .format(startState))
        # if not isinstance(inputToken, MethodicalInput):
        #     raise NotImplementedError("start state {} isn't an input"
        #                               .format(inputToken))
        # if not isinstance(endState, MethodicalState):
        #     raise NotImplementedError("end state {} isn't a state"
        #                               .format(startState))
        # for output in outputTokens:
        #     if not isinstance(endState, MethodicalState):
        #         raise NotImplementedError("output state {} isn't a state"
        #                                   .format(endState))
        self._automaton.addTransition(startState, inputToken, endState,
                                      tuple(outputTokens))
        inputToken.collectors[startState] = collector

    def graphviz(self):
        """
        Visualize this state machine using graphviz.

        @return: an iterable of lines of graphviz-format data suitable for
            feeding to C{dot} or C{neato} which visualizes the state machine
            described by this L{MethodicalMachine}.
        """
        from ._visualize import graphviz
        for line in graphviz(
                self._automaton,
                stateAsString=lambda state: state.method.__name__,
                inputAsString=lambda input: input.method.__name__,
                outputAsString=lambda output: output.method.__name__,
        ):
            yield line
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
"""
Created on Mon Jul 10 21:01:02 2017

Script for generating solutions to the game "Four fours".

THE GAME: The object of the game is to generate as many of the positive integers (in order) as possible,
using only four fours and a selected set of mathematical functions (such as addition, multiplication,
factorial, exponentiation, etc).
Example:
    0 = (4+4)-(4+4)
    1 = (4/4)*(4/4)
    2 = (4*4)/(4+4)
    etc...

THE THEORY: Each allowed function can be divided into two subsets: unary and binary. Unary functions take
a single argument and return a single number. Binary functions take two arguments and return a single
number. Therefore, any valid expression must contain 3 binary functions (the # of 4's used is #binary+1)
and any number of unary functions. Once we have constructed an expression containing 3 binary functions,
it is a valid expression and may be evaluated. Whether it is the one we are looking for is not so sure...

THE ALGORITHM: The algorithm for solving this problem involves the generation of successive dictionaries
containing expressions that make use of one 4, two 4's, three 4's, and finally four 4's. To construct
the first dictionary (expressions with one 4), we start simply with the number 4, and then repeatedly
apply all unary operators to it. Any time an expression evaluates to a number that is not in the dictionary,
we add it to the dictionary. We also set a limit on the max number of unary functions that can be applied,
and the maximum value that we will keep in our dictionary (repeated factorials become astronomically
large after a short time). Once this is done, our first dictionary is complete. Then, to construct the
second dictionary (expressions containing two 4's), we apply all binary functions to all possible
combinations of expressions from the first dictionary, saving new values. We then apply all unary
functions to these expressions, also saving new values. Then we build the third dictionary in the same
way, using all pairs of the first and second dictionary (1+2=3), and finally the fourth dictionary
using all the previous dictionaries (1+3=2+2=4). Then, we simply list the keys and associated expressions
of the fourth dictionary in numerical order.
This algorithm can easily be generalized to numbers other than 4 (five 5's, six 6's, etc.), as has been
done in the script.

@author: 27182_000
"""

# import important modules for special functions
import math as m
import scipy as sp

# global variables important for the running of the algorithm
FOUR = 4                    # the most important number - it determines that we're playing 4 4's (could be modified so that we're playing 3 3's, 5 5's, etc.)
MAXVALUE = 10**3            # maximum value to be stored in hash tables/dictionaries (repeated factorials can quickly produce astronomically large numbers)
EPSILON = 10**-4            # if floats are produced by the calculations, and they are within <EPSILON> of the closest integer, it just rounds them
ROUNDING_LEVEL = 6          # the digits of rounding to be used on floats (only relevant if not requiring values at each step)
MAXUNARY = 3                # maximum number of unary functions to apply between each application of binary functions
NUMTOPRINT = MAXVALUE       # number of entries in the final table to print

# initialize list of empty dictionaries (except for initial value <FOUR>)
dictList = [{} for i in range(FOUR)]
dictList[0][FOUR] = str(FOUR)
# define the usable functions and their string representations
unaryFunc = {'fact': lambda x: m.factorial(x),                          # factorial
             'dfact': lambda x: sp.misc.factorial2(x, exact=True),      # double factorial
             'gamma': lambda x: sp.special.gamma(x),                    # gamma function
             'sqrt': lambda x: m.sqrt(x)}                               # square root
unaryFuncStr = {'fact': lambda x: '(' + str(x) + '!)',
                'dfact': lambda x: '(' + str(x) + '!!)',
                'gamma': lambda x: '\u0393(' + str(x) + ')',
                'sqrt': lambda x: '\u221a(' + str(x) + ')'}
binaryFunc = {'add': lambda x,y: x+y,                                   # addition
              'sub': lambda x,y: x-y,                                   # subtraction
              'mult': lambda x,y: x*y,                                  # multiplication
              'div': lambda x,y: x/y,                                   # division
              'exp': lambda x,y: x**y,                                  # exponentiation
              'con': lambda x,y: int(str(x) + str(y)),                  # string concatenation
              'binom': lambda x,y: sp.special.binom(x,y)}               # binomial coefficient
binaryFuncStr = {'add': lambda x,y: '(' + str(x) + '+' + str(y) + ')',
                 'sub': lambda x,y: '(' + str(x) + '-' + str(y) + ')',
                 'mult': lambda x,y: '(' + str(x) + '*' + str(y) + ')',
                 'div': lambda x,y: '(' + str(x) + '/' + str(y) + ')',
                 'exp': lambda x,y: '(' + str(x) + '^' + str(y) + ')',
                 'con': lambda x,y: '(\'' + str(x) + '\'+\'' + str(y) + '\')',
                 'binom': lambda x,y: '(' + str(x) + 'C' + str(y) + ')'}

# define function for rounding results, keeping floats as floats, but converting floats within <EPSILON> of an integer into integers
def roundResult(result):
    integer_part = round(result)
    if abs(result - integer_part) <= EPSILON:
        return int(integer_part)
#    else:
#        return float(round(result, ROUNDING_LEVEL))

# define function for applying unary operators to each expression in a dictionary
def applyUnary(Dict):
    # define recursive helper function
    def applyUnaryRecursive(dictionary, nUnary):
        dictKeys = list(dictionary.keys())
        # for every value in the dictionary
        for value in dictKeys:
            # for each possible unary function
            for func in unaryFunc:
                try:
                    # apply function to each available expression in dictionary
                    result = unaryFunc[func](value)
                    result = roundResult(result)
                    # add new expression to dictionary (if value less than some max value and not already in dictionary)
                    if abs(result) <= MAXVALUE:
                        if result not in dictionary:
                            dictionary[result] = unaryFuncStr[func](dictionary[value])
                # if any of the expressions returns an error, just discard it
                except:
                    pass
        nUnary += 1
        # apply function recursively on the updated dictionary (if number of unary functions has not exceeded some number)
        if nUnary < MAXUNARY:
            applyUnaryRecursive(dictionary, nUnary)
    # call the recursive function
    applyUnaryRecursive(Dict, 0)

# define function for applying binary operators to each pair of expressions in a dictionary
def applyBinary(dict1, dict2):
    dict1Keys = list(dict1.keys())
    dict2Keys = list(dict2.keys())
    newDict = {}
    # iterate over each pair of elements in the dictionary
    for val1 in dict1Keys:
        for val2 in dict2Keys:
            # iterate over all binary functions
            for func in binaryFunc:
                try:
                    # apply function to each available pair of expressions in dictionary
                    result = binaryFunc[func](val1, val2)
                    result = roundResult(result)
                    # add new expression to dictionary (if less thena max value and not already in dictionary)
                    if abs(result) <= MAXVALUE:
                        if result not in newDict:
                            newDict[result] = binaryFuncStr[func](dict1[val1], dict2[val2])
                # if any expressions returns an error, discard it
                except:
                    pass
    return newDict

# generate dictionary with all expressions built with a single number
applyUnary(dictList[0])
# generate dictionary with all expressions built from n numbers, given the dictionary for <n numbers
# want to combine the n-1 and 1 dictionaries, (n-2, 2), (n-3, 3), ... (floor(n/2), ceiling(n/2))
for n1 in range(1, FOUR):
    for n2 in range(n1):
        newDict = applyBinary(dictList[n2], dictList[n1-n2-1])
        for val in newDict:
            if val not in dictList[n1]:
                dictList[n1][val] = newDict[val]
#        dictList[n1].update(newDict)
    applyUnary(dictList[n1])
# return the final dictionary built from all <FOUR> numbers
for i in range(NUMTOPRINT+1):
    try:
        print('{0} = {1}'.format(i, dictList[FOUR-1][i]))   # use .get(i) to skip entries that don't exist in the dictionary
    except KeyError:
        print('\tCan\'t compute expression for {}.'.format(i))

<commit_msg>Expanded description of FourFours.py<commit_after># -*- coding: utf-8 -*-
"""
Created on Mon Jul 10 21:01:02 2017

Script for generating solutions to the game "Four fours".

THE GAME: The object of the game is to generate as many of the positive integers (in order) as possible,
using only four fours and a selected set of mathematical functions (such as addition, multiplication,
factorial, exponentiation, etc).
Example:
    0 = (4+4)-(4+4)
    1 = (4/4)*(4/4)
    2 = (4*4)/(4+4)
    etc...

THE THEORY: Each allowed function can be divided into two subsets: unary and binary. Unary functions take
a single argument and return a single number. Binary functions take two arguments and return a single
number. Therefore, any valid expression must contain 3 binary functions (the # of 4's used is #binary+1)
and any number of unary functions. Once we have constructed an expression containing 3 binary functions,
it is a valid expression and may be evaluated. Whether it is the one we are looking for is not so sure...

THE ALGORITHM: The algorithm for solving this problem involves the generation of successive dictionaries
containing expressions that make use of one 4, two 4's, three 4's, and finally four 4's. To construct
the first dictionary (expressions with one 4), we start simply with the number 4, and then repeatedly
apply all unary operators to it. Any time an expression evaluates to a number that is not in the dictionary,
we add it to the dictionary. We also set a limit on the max number of unary functions that can be applied,
and the maximum value that we will keep in our dictionary (repeated factorials become astronomically
large after a short time). Once this is done, our first dictionary is complete. Then, to construct the
second dictionary (expressions containing two 4's), we apply all binary functions to all possible
combinations of expressions from the first dictionary, saving new values. We then apply all unary
functions to these expressions, also saving new values. Then we build the third dictionary in the same
way, using all pairs of the first and second dictionary (1+2=3), and finally the fourth dictionary
using all the previous dictionaries (1+3=2+2=4). Then, we simply list the keys and associated expressions
of the fourth dictionary in numerical order.
This algorithm can easily be generalized to numbers other than 4 (five 5's, six 6's, etc.), as has been
done in the script.

@author: 27182_000
"""

# import important modules for special functions
import math as m
import scipy as sp

# global variables important for the running of the algorithm
FOUR = 4                    # the most important number - it determines that we're playing 4 4's (could be modified so that we're playing 3 3's, 5 5's, etc.)
MAXVALUE = 5*10**3            # maximum value to be stored in hash tables/dictionaries (repeated factorials can quickly produce astronomically large numbers)
EPSILON = 10**-4            # if floats are produced by the calculations, and they are within <EPSILON> of the closest integer, it just rounds them
ROUNDING_LEVEL = 6          # the digits of rounding to be used on floats (only relevant if not requiring values at each step)
MAXUNARY = 3                # maximum number of unary functions to apply between each application of binary functions
NUMTOPRINT = MAXVALUE       # number of entries in the final table to print

# initialize list of empty dictionaries (except for initial value <FOUR>)
dictList = [{} for i in range(FOUR)]
dictList[0][FOUR] = str(FOUR)
# define the usable functions and their string representations
unaryFunc = {'fact': lambda x: m.factorial(x),                          # factorial
             'dfact': lambda x: sp.misc.factorial2(x, exact=True),      # double factorial
             'gamma': lambda x: sp.special.gamma(x),                    # gamma function
             'sqrt': lambda x: m.sqrt(x)}                               # square root
unaryFuncStr = {'fact': lambda x: '(' + str(x) + '!)',
                'dfact': lambda x: '(' + str(x) + '!!)',
                'gamma': lambda x: '\u0393(' + str(x) + ')',
                'sqrt': lambda x: '\u221a(' + str(x) + ')'}
binaryFunc = {'add': lambda x,y: x+y,                                   # addition
              'sub': lambda x,y: x-y,                                   # subtraction
              'mult': lambda x,y: x*y,                                  # multiplication
              'div': lambda x,y: x/y,                                   # division
              'exp': lambda x,y: x**y,                                  # exponentiation
              'con': lambda x,y: int(str(x) + str(y)),                  # string concatenation
              'binom': lambda x,y: sp.special.binom(x,y)}               # binomial coefficient
binaryFuncStr = {'add': lambda x,y: '(' + str(x) + '+' + str(y) + ')',
                 'sub': lambda x,y: '(' + str(x) + '-' + str(y) + ')',
                 'mult': lambda x,y: '(' + str(x) + '*' + str(y) + ')',
                 'div': lambda x,y: '(' + str(x) + '/' + str(y) + ')',
                 'exp': lambda x,y: '(' + str(x) + '^' + str(y) + ')',
                 'con': lambda x,y: '(\'' + str(x) + '\'+\'' + str(y) + '\')',
                 'binom': lambda x,y: '(' + str(x) + 'C' + str(y) + ')'}

# define function for rounding results, keeping floats as floats, but converting floats within <EPSILON> of an integer into integers
def roundResult(result):
    integer_part = round(result)
    if abs(result - integer_part) <= EPSILON:
        return int(integer_part)
#    else:
#        return float(round(result, ROUNDING_LEVEL))

# define function for applying unary operators to each expression in a dictionary
def applyUnary(Dict):
    # define recursive helper function
    def applyUnaryRecursive(dictionary, nUnary):
        dictKeys = list(dictionary.keys())
        # for every value in the dictionary
        for value in dictKeys:
            # for each possible unary function
            for func in unaryFunc:
                try:
                    # apply function to each available expression in dictionary
                    result = unaryFunc[func](value)
                    result = roundResult(result)
                    # add new expression to dictionary (if value less than some max value and not already in dictionary)
                    if abs(result) <= MAXVALUE:
                        if result not in dictionary:
                            dictionary[result] = unaryFuncStr[func](dictionary[value])
                # if any of the expressions returns an error, just discard it
                except:
                    pass
        nUnary += 1
        # apply function recursively on the updated dictionary (if number of unary functions has not exceeded some number)
        if nUnary < MAXUNARY:
            applyUnaryRecursive(dictionary, nUnary)
    # call the recursive function
    applyUnaryRecursive(Dict, 0)

# define function for applying binary operators to each pair of expressions in a dictionary
def applyBinary(dict1, dict2):
    dict1Keys = list(dict1.keys())
    dict2Keys = list(dict2.keys())
    newDict = {}
    # iterate over each pair of elements in the dictionary
    for val1 in dict1Keys:
        for val2 in dict2Keys:
            # iterate over all binary functions
            for func in binaryFunc:
                try:
                    # apply function to each available pair of expressions in dictionary
                    result = binaryFunc[func](val1, val2)
                    result = roundResult(result)
                    # add new expression to dictionary (if less thena max value and not already in dictionary)
                    if abs(result) <= MAXVALUE:
                        if result not in newDict:
                            newDict[result] = binaryFuncStr[func](dict1[val1], dict2[val2])
                # if any expressions returns an error, discard it
                except:
                    pass
    return newDict

# generate dictionary with all expressions built with a single number
applyUnary(dictList[0])
# generate dictionary with all expressions built from n numbers, given the dictionary for <n numbers
# want to combine the n-1 and 1 dictionaries, (n-2, 2), (n-3, 3), ... (floor(n/2), ceiling(n/2))
for n1 in range(1, FOUR):
    for n2 in range(n1):
        newDict = applyBinary(dictList[n2], dictList[n1-n2-1])
        for val in newDict:
            if val not in dictList[n1]:
                dictList[n1][val] = newDict[val]
#        dictList[n1].update(newDict)
    applyUnary(dictList[n1])
# return the final dictionary built from all <FOUR> numbers
for i in range(NUMTOPRINT+1):
    try:
        print('{0} = {1}'.format(i, dictList[FOUR-1][i]))   # use .get(i) to skip entries that don't exist in the dictionary
    except KeyError:
        print('\tCan\'t compute expression for {}.'.format(i))

<|endoftext|>


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

"""
redi.models
~~~~~~~~~~~

This module contains most of the functionality of redi.

"""

from UserDict import DictMixin

from . import config
from .ext import expand_key
from .utils import ListMixin, is_collection

from clint.textui import colored

class BaseRedis(object):
    """Base Redis interface object. Provides Redis instance and
    datatype mapping."""

    def __init__(self, redis=None):
        super(BaseRedis, self).__init__()

        if redis is None:
            self.redis = config.redis

        self.redis = redis

    @staticmethod
    def to_redis(o):
        """Converts Python datatypes to Redis values."""

        # don't serialize internal datatype classesf
        if isinstance(o, SubList) or isinstance(o, SubDict):
            o = o.data

        return config.encoder(o)

    def to_python(self, o):
        """Converts Redis values to Python datatypes."""
        try:
            v = config.decoder(o)

            if isinstance(v, dict):
                return SubDict(v, self.save)

            elif is_collection(v):
                return SubList(v, self.save)

            try:
                if not isinstance(v, float):
                    return int(v)
            except ValueError:
                pass

            try:
                if not isinstance(v, int):
                    return float(v)
            except ValueError:
                pass

            return v

        except (ValueError, TypeError):
            try:
                return unicode(o, config.str_codec)
            except (UnicodeDecodeError, TypeError):
                return o

    def save(self, *args):
        pass

class RedisKey(BaseRedis):
    """Contains methods that can be applied to any Redis key."""

    def __init__(self, key, redis=None):
        super(RedisKey, self).__init__(redis=redis)

        self.key = key

    def __repr__(self):
        return '<redis-key {0}>'.format(self.key)

    def delete(self):
        """Removes this key from Redis."""
        return self.redis.delete(self.key)

    def expire(self, s):
        """Expires this key from Redis in given seconds."""
        # TODO: Accept datetime for expire_at

        return self.redis.expire(self.key, s)

    def rename(self, new_name, safe=True):
        """Renames this key."""

        new_name = expand_key(new_name)

        if safe:
            if self.redis.renamenx(self.key, new_name):
                self.key = new_name
                return True
            return False
        else:
            self.redis.rename(self.key, new_name)
            self.key = new_name
            return True

    @property
    def children(self):
        """Lists all children of current key."""

        keys = self.redis.keys('{0}*'.format(self.key))

        for key in keys:
            yield key

class RedisValue(RedisKey):
    """Redis value of awesomeness."""

    def __init__(self, key, redis=None):
        super(RedisValue, self).__init__(key, redis=redis)

        self.key = key

    @property
    def _raw(self):
        return self.redis.get(self.key)

    def __repr__(self):
        return '<redis-value {0}>'.format(self.key)

    def save(self, value):
        v = self.to_redis(value)
        return self.redis.set(self.key, v)

    @property
    def data(self):
        v = self.redis.get(self.key)
        return self.to_python(v)

    @data.setter
    def data(self, value):
        self.save(value)

    @property
    def type(self):
        v = self.redis.get(self.key)
        return type(self.value)

class RedisList(RedisKey):
    """Redis list of awesomeness."""

    def __init__(self, key, redis=None):
        super(RedisList, self).__init__(key, redis=redis)
        # print r.db
        # print self.redis
        self.key = key

    def __repr__(self):
        return '<redis-list {0}>'.format(self.key)

    @property
    def _raw(self):
        for v in self.redis.lrange(self.key, 0, -1):
            yield v

    def save(self, value, i=None):
        """Save list."""

        self[i] = value

    def __getitem__(self, i):
        is_single = not isinstance(i, slice)

        if is_single:
            value = self.redis.lindex(self.key, i)
            values = self.to_python(value)
            try:
                values.i = i
            except AttributeError:
                pass

        else:
            start = 0 if i.start is None else i.start
            stop = -1 if i.stop is None else i.stop

            values = self.redis.lrange(self.key, start, stop)
            values = map(self.to_python, values)

            for i in range(start, start+len(values)):
                try:
                    values[i].i = i
                except AttributeError:
                    pass

        return values

    def __setitem__(self, i, value):
        v = self.to_redis(value)
        return self.redis.lset(self.key, i, v)

    def __delitem__(self, i):

        for value in self[i]:
            self.redis.lrem(self.key, value)

    def __iter__(self):
        for v in self[:]:
            yield v

    def __len__(self):
        return self.redis.llen(self.key)

    def __contains__(self, value):
        i = self.index(value)
        return (i is not None)

    def insert(self, index, value, before=True):

        refvalue = self[index]
        where = 'BEFORE' if before else 'AFTER'

        return self.redis.linsert(self.key, where, refvalue, value)

    def index(self, value):
        """Returns first found index of given value."""

        for i, v in enumerate(self):
            try:
                if value.__dict__ == v.__dict__:
                    return i
            except AttributeError:
                if value == v:
                    return i

    def append(self, value, right=True):
        v = self.to_redis(value)

        if right:
            return self.redis.rpush(self.key, v)
        else:
            return self.redis.lpush(self.key, v)

    def extend(self, values):
        for value in values:
             v = self.to_redis(value)
             self.append(v)

    def rpush(self, value):
        """Redis RPUSH."""
        return self.append(value, right=True)

    def lpush(self, value):
        """Redis LPUSH."""
        return self.append(value, right=False)

    def lpop(self):
        """Redis LPOP."""
        return self.pop(right=False)

    def rpop(self):
        """Redis RPOP."""
        return self.pop(right=True)

    def pop(self, right=True):
        """Redis (R|L)POP."""

        if right:
            v = self.redis.lpop(self.key)
        else:
            v = self.redis.rpop(self.key)

        return self.to_python(v)

    def brpop(self, timeout=config.block_timeout):
        """Redis BRPOP."""
        return self.bpop(timeout, right=True)

    def blpop(self, timeout=config.block_timeout):
        """Redis BLPOP."""
        return self.bpop(timeout, right=False)

    def bpop(self, timeout=config.block_timeout, right=True):
        """Redis B(R|L)POP."""

        if right:
            v = self.redis.brpop(self.key, timeout)
        else:
            v = self.redis.blpop(self.key, timeout)

        try:
            return self.to_python(v[1])
        except TypeError:
            return None

    def find(self, *search):
        """FINDS ALL TEH THINGS."""

        for item in self._raw:
            for s in search:
                if callable(s):
                    if s(item):
                        yield item
                else:
                    if s in item:
                        yield self.to_python(item)

class SubList(ListMixin):
    """Lists within Redis values."""

    def __init__(self, l, writer):
        self.data = l
        self.writer = writer
        self.i = None

    def write(self):
        """Writes List to Redis."""
        if self.i is not None:
            self.writer(self.data, self.i)
        else:
            self.writer(self.data)

    def _get_element(self, i):
        return self.data[i]

    def _set_element(self, i, value):
        self.data[i] = value
        self.write()

    def __len__(self):
        return len(self.data)

    def _resize_region(self, start, end, new_size):

        self.data[start:end] = [None] * new_size
        self.write()

    def _constructor(self, iter):
        return SubList(iter, self.writer)

    def __iter__(self):
        for item in self.data:
            yield item

class SubDict(DictMixin):
    """Dicts within Redis values."""

    def __init__(self, d, writer):
        self.data = d
        self.writer = writer
        self.i = None

    def write(self):
        """Writes Dict to Redis."""
        if self.i is not None:
            self.writer(self.data, self.i)
        else:
            self.writer(self.data)

    def __getitem__(self, item):
        return self.data.get(item)

    def __setitem__(self, k, v):
        self.data[k] = v
        self.write()

    def __repr__(self):
        return repr(self.data)

    def keys(self):
        return self.data.keys()

<commit_msg>value -> data<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
redi.models
~~~~~~~~~~~

This module contains most of the functionality of redi.

"""

from UserDict import DictMixin

from . import config
from .ext import expand_key
from .utils import ListMixin, is_collection

from clint.textui import colored

class BaseRedis(object):
    """Base Redis interface object. Provides Redis instance and
    datatype mapping."""

    def __init__(self, redis=None):
        super(BaseRedis, self).__init__()

        if redis is None:
            self.redis = config.redis

        self.redis = redis

    @staticmethod
    def to_redis(o):
        """Converts Python datatypes to Redis values."""

        # don't serialize internal datatype classesf
        if isinstance(o, SubList) or isinstance(o, SubDict):
            o = o.data

        return config.encoder(o)

    def to_python(self, o):
        """Converts Redis values to Python datatypes."""
        try:
            v = config.decoder(o)

            if isinstance(v, dict):
                return SubDict(v, self.save)

            elif is_collection(v):
                return SubList(v, self.save)

            try:
                if not isinstance(v, float):
                    return int(v)
            except ValueError:
                pass

            try:
                if not isinstance(v, int):
                    return float(v)
            except ValueError:
                pass

            return v

        except (ValueError, TypeError):
            try:
                return unicode(o, config.str_codec)
            except (UnicodeDecodeError, TypeError):
                return o

    def save(self, *args):
        pass

class RedisKey(BaseRedis):
    """Contains methods that can be applied to any Redis key."""

    def __init__(self, key, redis=None):
        super(RedisKey, self).__init__(redis=redis)

        self.key = key

    def __repr__(self):
        return '<redis-key {0}>'.format(self.key)

    def delete(self):
        """Removes this key from Redis."""
        return self.redis.delete(self.key)

    def expire(self, s):
        """Expires this key from Redis in given seconds."""
        # TODO: Accept datetime for expire_at

        return self.redis.expire(self.key, s)

    def rename(self, new_name, safe=True):
        """Renames this key."""

        new_name = expand_key(new_name)

        if safe:
            if self.redis.renamenx(self.key, new_name):
                self.key = new_name
                return True
            return False
        else:
            self.redis.rename(self.key, new_name)
            self.key = new_name
            return True

    @property
    def children(self):
        """Lists all children of current key."""

        keys = self.redis.keys('{0}*'.format(self.key))

        for key in keys:
            yield key

class RedisValue(RedisKey):
    """Redis value of awesomeness."""

    def __init__(self, key, redis=None):
        super(RedisValue, self).__init__(key, redis=redis)

        self.key = key

    @property
    def _raw(self):
        return self.redis.get(self.key)

    def __repr__(self):
        return '<redis-value {0}>'.format(self.key)

    def save(self, value):
        v = self.to_redis(value)
        return self.redis.set(self.key, v)

    @property
    def data(self):
        v = self.redis.get(self.key)
        return self.to_python(v)

    @data.setter
    def data(self, value):
        self.save(value)

    @property
    def type(self):
        v = self.redis.get(self.key)
        return type(self.data)

class RedisList(RedisKey):
    """Redis list of awesomeness."""

    def __init__(self, key, redis=None):
        super(RedisList, self).__init__(key, redis=redis)
        # print r.db
        # print self.redis
        self.key = key

    def __repr__(self):
        return '<redis-list {0}>'.format(self.key)

    @property
    def _raw(self):
        for v in self.redis.lrange(self.key, 0, -1):
            yield v

    def save(self, value, i=None):
        """Save list."""

        self[i] = value

    def __getitem__(self, i):
        is_single = not isinstance(i, slice)

        if is_single:
            value = self.redis.lindex(self.key, i)
            values = self.to_python(value)
            try:
                values.i = i
            except AttributeError:
                pass

        else:
            start = 0 if i.start is None else i.start
            stop = -1 if i.stop is None else i.stop

            values = self.redis.lrange(self.key, start, stop)
            values = map(self.to_python, values)

            for i in range(start, start+len(values)):
                try:
                    values[i].i = i
                except AttributeError:
                    pass

        return values

    def __setitem__(self, i, value):
        v = self.to_redis(value)
        return self.redis.lset(self.key, i, v)

    def __delitem__(self, i):

        for value in self[i]:
            self.redis.lrem(self.key, value)

    def __iter__(self):
        for v in self[:]:
            yield v

    def __len__(self):
        return self.redis.llen(self.key)

    def __contains__(self, value):
        i = self.index(value)
        return (i is not None)

    def insert(self, index, value, before=True):

        refvalue = self[index]
        where = 'BEFORE' if before else 'AFTER'

        return self.redis.linsert(self.key, where, refvalue, value)

    def index(self, value):
        """Returns first found index of given value."""

        for i, v in enumerate(self):
            try:
                if value.__dict__ == v.__dict__:
                    return i
            except AttributeError:
                if value == v:
                    return i

    def append(self, value, right=True):
        v = self.to_redis(value)

        if right:
            return self.redis.rpush(self.key, v)
        else:
            return self.redis.lpush(self.key, v)

    def extend(self, values):
        for value in values:
             v = self.to_redis(value)
             self.append(v)

    def rpush(self, value):
        """Redis RPUSH."""
        return self.append(value, right=True)

    def lpush(self, value):
        """Redis LPUSH."""
        return self.append(value, right=False)

    def lpop(self):
        """Redis LPOP."""
        return self.pop(right=False)

    def rpop(self):
        """Redis RPOP."""
        return self.pop(right=True)

    def pop(self, right=True):
        """Redis (R|L)POP."""

        if right:
            v = self.redis.lpop(self.key)
        else:
            v = self.redis.rpop(self.key)

        return self.to_python(v)

    def brpop(self, timeout=config.block_timeout):
        """Redis BRPOP."""
        return self.bpop(timeout, right=True)

    def blpop(self, timeout=config.block_timeout):
        """Redis BLPOP."""
        return self.bpop(timeout, right=False)

    def bpop(self, timeout=config.block_timeout, right=True):
        """Redis B(R|L)POP."""

        if right:
            v = self.redis.brpop(self.key, timeout)
        else:
            v = self.redis.blpop(self.key, timeout)

        try:
            return self.to_python(v[1])
        except TypeError:
            return None

    def find(self, *search):
        """FINDS ALL TEH THINGS."""

        for item in self._raw:
            for s in search:
                if callable(s):
                    if s(item):
                        yield item
                else:
                    if s in item:
                        yield self.to_python(item)

class SubList(ListMixin):
    """Lists within Redis values."""

    def __init__(self, l, writer):
        self.data = l
        self.writer = writer
        self.i = None

    def write(self):
        """Writes List to Redis."""
        if self.i is not None:
            self.writer(self.data, self.i)
        else:
            self.writer(self.data)

    def _get_element(self, i):
        return self.data[i]

    def _set_element(self, i, value):
        self.data[i] = value
        self.write()

    def __len__(self):
        return len(self.data)

    def _resize_region(self, start, end, new_size):

        self.data[start:end] = [None] * new_size
        self.write()

    def _constructor(self, iter):
        return SubList(iter, self.writer)

    def __iter__(self):
        for item in self.data:
            yield item

class SubDict(DictMixin):
    """Dicts within Redis values."""

    def __init__(self, d, writer):
        self.data = d
        self.writer = writer
        self.i = None

    def write(self):
        """Writes Dict to Redis."""
        if self.i is not None:
            self.writer(self.data, self.i)
        else:
            self.writer(self.data)

    def __getitem__(self, item):
        return self.data.get(item)

    def __setitem__(self, k, v):
        self.data[k] = v
        self.write()

    def __repr__(self):
        return repr(self.data)

    def keys(self):
        return self.data.keys()

<|endoftext|>