<commit_before>import logging
import struct
import copy

# ZigBee Profile IDs
ZDP_PROFILE_ID     = b'\x00\x00'  # Zigbee Device Profile
HA_PROFILE_ID      = b'\x01\x04'  # HA Device Profile
LL_PROFILE_ID      = b'\xc0\x5e'  # Light Link Profile
ALERTME_PROFILE_ID = b'\xc2\x16'  # AlertMe Private Profile

# ZigBee Addressing
BROADCAST_LONG = b'\x00\x00\x00\x00\x00\x00\xff\xff'
BROADCAST_SHORT = b'\xff\xfe'

messages = {
    'routing_table_request': {
        'name': 'Management Routing Table Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x00\x32',
            'profile': ZDP_PROFILE_ID,
            'data': '\x12\x01'
        }
    },
    'permit_join_request': {
        'name': 'Management Permit Join Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x00\x36',
            'profile': ZDP_PROFILE_ID,
            'data': '\xff\x00'
        }
    },
    'active_endpoint_request': {
        'name': 'Active Endpoints Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x00\x05',
            'profile': ZDP_PROFILE_ID,
            'data': b'\x00\x00'
        }
    },
    'match_descriptor_response': {
        'name': 'Match Descriptor Response',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x80\x06',
            'profile': ZDP_PROFILE_ID,
            'data': b'\x00\x00\x00\x00\x01\x02'
        }
    },
    'version_info_request': {
        'name': 'Version Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf6',
            'profile': ALERTME_PROFILE_ID,
            'data': b'\x11\x00\xfc'
        }
    },
    'version_info_update': {
        'name': 'Version Responce',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf6',
            'profile': ALERTME_PROFILE_ID,
            'data': lambda params: generate_version_info_update(params)
        }
    },
    'switch_state_request': {
        'name': 'Switch State Change',
        'frame': {
            'profile': ALERTME_PROFILE_ID,
            'cluster': b'\x00\xee',
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'data': lambda params: generate_switch_state_request(params)
        }
    },
    'switch_state_update': {
        'name': 'Switch State Update',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xee',
            'profile': ALERTME_PROFILE_ID,
            'data': lambda params: generate_switch_state_update(params)
        }
    },
    'power_demand_update': {
        'name': 'Current Power Demand',
        'frame': {
            'profile': ALERTME_PROFILE_ID,
            'cluster': b'\x00\xef',
            'src_endpoint': b'\x02',
            'dest_endpoint': b'\x02',
            'data': lambda params: generate_power_demand_update(params)
        }
    },
    'mode_change_request': {
       'name': 'Normal Mode',
       'frame': {
           'profile': ALERTME_PROFILE_ID,
           'cluster': b'\x00\xf0',
           'src_endpoint': b'\x00',
           'dest_endpoint': b'\x02',
           'data': lambda params: generate_mode_change_request(params)
       }
    },
    'range_info': {
        'name': 'Range Info',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf6',
            'profile': ALERTME_PROFILE_ID,
            'data': lambda params: generate_range_update(params)
        }
    },
    'missing_link': {
        'name': 'Missing Link',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf0',
            'profile': ALERTME_PROFILE_ID,
            'data': b'\x11\x39\xfd'
        }
    },
    'security_init': {
        'name': 'Security Initialization',
        'frame': {
            'profile': ALERTME_PROFILE_ID,
            'cluster': b'\x05\x00',
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'data': b'\x11\x80\x00\x00\x05'
        }
    }
}

def get_message(message_id, params=None):
    """
    Get message

    :param message_id: Message ID
    :param params: Optional
    :return:
    """
    if message_id in messages.keys():
        # Make a copy of the message
        message = copy.deepcopy(messages[message_id])
        data = message['frame']['data']

        # If data is a lambda then call it and replace with return value
        if callable(data):
            message['frame']['data'] = data(params)

        # Return processed message
        return message['frame']

    else:
        raise Exception('Message does not exist')

def list_messages():
    """
    List messages

    :return:
    """
    actions = {}
    for id, message in messages.items():
        actions[id] = message['name']
    return actions

def parse_version_info_update(data):
    """
    Process message, parse for version information:
    Type, Version, Manufacturer and Manufacturer Date

    :param data: Message data
    :return: Parameter dictionary of version info
    """
    # The version string is variable length. We therefore have to calculate the
    # length of the string which we then use in the unpack
    l = len(data) - 22
    values = dict(zip(
        ('cluster_cmd', 'hw_version', 'string'),
        struct.unpack('< 2x s H 17x %ds' % l, data)
    ))

    # Break down the version string into its component parts
    ret = {}
    ret['Version'] = values['hw_version']
    ret['String']  = str(values['string'].decode()) \
        .replace('\t', '\n') \
        .replace('\r', '\n') \
        .replace('\x0e', '\n') \
        .replace('\x0b', '\n') \
        .replace('\x06', '\n') \
        .replace('\x04', '\n') \
        .replace('\x12', '\n')

    ret['Manufacturer']    = ret['String'].split('\n')[0]
    ret['Type']            = ret['String'].split('\n')[1]
    ret['ManufactureDate'] = ret['String'].split('\n')[2]
    del ret['String']

    return ret

def generate_version_info_update(params):
    """
    Generate type message

    :param params: Parameter dictionary of version info
    :return: Message data
    """
    checksum = b'\tq'
    cluster_cmd = b'\xfe'
    payload = struct.pack('H', params['Version']) \
              + b'\xf8\xb9\xbb\x03\x00o\r\x009\x10\x07\x00\x00)\x00\x01\x0b' \
              + params['Manufacturer'] \
              + '\n' + params['Type'] \
              + '\n' + params['ManufactureDate']
    data = checksum + cluster_cmd + payload

    return data

def parse_range_info_update(data):
    """
    Process message, parse for RSSI range test value

    :param data: Message data
    :return: Parameter dictionary of RSSI value
    """
    values = dict(zip(
        ('cluster_cmd', 'RSSI'),
        struct.unpack('< 2x s B 1x', data)
    ))
    rssi = values['RSSI']
    return {'RSSI' : rssi}

def generate_range_update(params):
    """
    Generate range message

    :param params: Parameter dictionary of RSSI value
    :return: Message data
    """
    checksum = b'\t+'
    cluster_cmd = b'\xfd'
    payload = struct.pack('B 1x', params['RSSI'])
    data = checksum + cluster_cmd + payload

    return data

def parse_power_demand(data):
    """
    Process message, parse for power demand value.

    :param data: Message data
    :return: Parameter dictionary of power demand value
    """
    values = dict(zip(
        ('cluster_cmd', 'power_demand'),
        struct.unpack('< 2x s H', data)
    ))

    return {'PowerDemand': values['power_demand']}

def generate_power_demand_update(params):
    """
    Generate Power Demand Update message data

    :param params: Parameter dictionary of power demand value
    :return: Message data
    """
    checksum = b'\tj'
    cluster_cmd = b'\x81'
    payload = struct.pack('H', params['PowerDemand'])
    data = checksum + cluster_cmd + payload
    return data

def generate_mode_change_request(params):
    """
    Generate Mode Change Request
    Available Modes: 'Normal', 'RangeTest', 'Locked', 'Silent'

    :param params: Parameter dictionary of requested mode
    :return: Message data
    """
    mode = params['Mode']
    data = b''
    if mode == 'Normal':
        data = b'\x11\x00\xfa\x00\x01'
    elif mode == 'RangeTest':
        data = b'\x11\x00\xfa\x01\x01'
    elif mode == 'Locked':
        data = b'\x11\x00\xfa\x02\x01'
    elif mode == 'Silent':
        data = b'\x11\x00\xfa\x03\x01'
    else:
        logging.error('Invalid mode request %s', mode)

    return data

def parse_power_consumption(data):
    """
    Process message, parse for power consumption value.

    :param data: Message data
    :return: Parameter dictionary of usage stats
    """
    ret = {}
    values = dict(zip(
        ('cluster_cmd', 'powerConsumption', 'upTime'),
        struct.unpack('< 2x s I I 1x', data)
    ))
    ret['PowerConsumption'] = values['powerConsumption']
    ret['UpTime'] = values['upTime']

    return ret

def parse_switch_state_request(data):
    """
    Process message, parse for relay state change request.
    This message is sent from the hub to the smartplug requesting state change.

    :param data: Message data
    :return: Parameter dictionary of relay state
    """
    # Parse Switch State Request
    if data == b'\x11\x00\x02\x01\x01':
        return 1
    elif data == b'\x11\x00\x02\x00\x01':
        return 0
    else:
        logging.error('Unknown State Request')

def parse_switch_state_update(data):
    """
    Process message, parse for switch status.
    This message is sent from the smartplug to the hub advertising current state.

    :param data: Message data
    :return: Parameter dictionary of switch status
    """
    values = struct.unpack('< 2x b b b', data)
    if values[2] & 0x01:
        return {'State': 1}
    else:
        return {'State': 0}

def generate_switch_state_request(params):
    """
    Generate Switch State Change request data.
    This message is sent from the hub to the smartplug requesting state change.

    :param params: Parameter dictionary of relay state
    :return: Message data
    """
    data = b'\x11\x00\x01\x01'
    if 'State' in params.keys():
        if params['State'] == 1:
            # On
            data = b'\x11\x00\x02\x01\x01'
        elif params['State'] == 0:
            # Cff
            data = b'\x11\x00\x02\x00\x01'
        elif params['State'] == '':
            # Check
            data = b'\x11\x00\x01\x01'

    return data

def generate_switch_state_update(params):
    """
    Generate Switch State update message data.
    This message is sent from the smartplug to the hub advertising current state.

    :param params: Parameter dictionary of relay state
    :return: Message data
    """
    checksum = b'\th'
    cluster_cmd = b'\x80'
    payload = b'\x07\x01' if params['State'] else b'\x06\x00'
    data = checksum + cluster_cmd + payload
    return data

def parse_tamper_state(data):
    """
    Process message, parse for Tamper Switch State Change

    :param data: Message data
    :return: Parameter dictionary of tamper status
    """
    ret = {}
    if ord(data[3]) == 0x02:
        ret['TamperSwitch'] = 'OPEN'
    else:
        ret['TamperSwitch'] = 'CLOSED'

    return ret

def parse_button_press(data):
    """
    Process message, parse for button press status

    :param data: Message data
    :return: Parameter dictionary of button status
    """
    ret = {}
    if data[2] == b'\x00':
        ret['State'] = 0
    elif data[2] == b'\x01':
        ret['State'] = 1

    ret['Counter'] = struct.unpack('<H', data[5:7])[0]

    return ret

def parse_security_state(data):
    """
    Process message, parse for security state

    :param data: Message data
    :return: Parameter dictionary of security state
    """
    ret = {}
    # The switch state is in byte [3] and is a bitfield
    # bit 0 is the magnetic reed switch state
    # bit 3 is the tamper switch state
    state = ord(data[3])
    if (state & 0x01):
        ret['ReedSwitch']  = 'OPEN'
    else:
        ret['ReedSwitch']  = 'CLOSED'

    if (state & 0x04):
        ret['TamperSwitch'] = 'CLOSED'
    else:
        ret['TamperSwitch'] = 'OPEN'

    return ret

def parse_status_update(data):
    """
    Process message, parse for status update

    :param data: Message data
    :return: Parameter dictionary of state
    """
    ret = {}
    status = data[3]
    if status == b'\x1b':
        # Power Clamp
        # Unknown
        pass

    elif status == b'\x1c':
        # Power Switch
        # Unknown
        pass

    elif status == b'\x1d':
        # Key Fob
        ret['TempFahrenheit'] = float(struct.unpack("<h", data[8:10])[0]) / 100.0 * 1.8 + 32
        ret['Counter'] = struct.unpack('<I', data[4:8])[0]

    elif status == b'\x1e' or status == b'\x1f':
        # Door Sensor
        ret['TempFahrenheit'] = float(struct.unpack("<h", data[8:10])[0]) / 100.0 * 1.8 + 32
        if ord(data[-1]) & 0x01 == 1:
            ret['ReedSwitch']  = 'OPEN'
        else:
            ret['ReedSwitch']  = 'CLOSED'

        if ord(data[-1]) & 0x02 == 0:
            ret['TamperSwitch'] = 'OPEN'
        else:
            ret['TamperSwitch'] = 'CLOSED'

    else:
        logging.error('Unrecognised Device Status %r  %r', status, data)

    return ret
<commit_msg>Update messages.py<commit_after>import logging
import struct
import copy

# ZigBee Profile IDs
ZDP_PROFILE_ID     = b'\x00\x00'  # Zigbee Device Profile
HA_PROFILE_ID      = b'\x01\x04'  # HA Device Profile
LL_PROFILE_ID      = b'\xc0\x5e'  # Light Link Profile
ALERTME_PROFILE_ID = b'\xc2\x16'  # AlertMe Private Profile

# ZigBee Addressing
BROADCAST_LONG = b'\x00\x00\x00\x00\x00\x00\xff\xff'
BROADCAST_SHORT = b'\xff\xfe'

messages = {
    'routing_table_request': {
        'name': 'Management Routing Table Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x00\x32',
            'profile': ZDP_PROFILE_ID,
            'data': '\x12\x01'
        }
    },
    'permit_join_request': {
        'name': 'Management Permit Join Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x00\x36',
            'profile': ZDP_PROFILE_ID,
            'data': '\xff\x00'
        }
    },
    'active_endpoint_request': {
        'name': 'Active Endpoints Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x00\x05',
            'profile': ZDP_PROFILE_ID,
            'data': b'\x00\x00'
        }
    },
    'match_descriptor_response': {
        'name': 'Match Descriptor Response',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x00',
            'cluster': b'\x80\x06',
            'profile': ZDP_PROFILE_ID,
            'data': b'\x00\x00\x00\x00\x01\x02'
        }
    },
    'version_info_request': {
        'name': 'Version Request',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf6',
            'profile': ALERTME_PROFILE_ID,
            'data': b'\x11\x00\xfc'
        }
    },
    'version_info_update': {
        'name': 'Version Responce',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf6',
            'profile': ALERTME_PROFILE_ID,
            'data': lambda params: generate_version_info_update(params)
        }
    },
    'switch_state_request': {
        'name': 'Switch State Change',
        'frame': {
            'profile': ALERTME_PROFILE_ID,
            'cluster': b'\x00\xee',
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'data': lambda params: generate_switch_state_request(params)
        }
    },
    'switch_state_update': {
        'name': 'Switch State Update',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xee',
            'profile': ALERTME_PROFILE_ID,
            'data': lambda params: generate_switch_state_update(params)
        }
    },
    'power_demand_update': {
        'name': 'Current Power Demand',
        'frame': {
            'profile': ALERTME_PROFILE_ID,
            'cluster': b'\x00\xef',
            'src_endpoint': b'\x02',
            'dest_endpoint': b'\x02',
            'data': lambda params: generate_power_demand_update(params)
        }
    },
    'mode_change_request': {
       'name': 'Normal Mode',
       'frame': {
           'profile': ALERTME_PROFILE_ID,
           'cluster': b'\x00\xf0',
           'src_endpoint': b'\x00',
           'dest_endpoint': b'\x02',
           'data': lambda params: generate_mode_change_request(params)
       }
    },
    'range_info': {
        'name': 'Range Info',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf6',
            'profile': ALERTME_PROFILE_ID,
            'data': lambda params: generate_range_update(params)
        }
    },
    'missing_link': {
        'name': 'Missing Link',
        'frame': {
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'cluster': b'\x00\xf0',
            'profile': ALERTME_PROFILE_ID,
            'data': lambda params: generate_missing_link(params)
        }
    },
    'security_init': {
        'name': 'Security Initialization',
        'frame': {
            'profile': ALERTME_PROFILE_ID,
            'cluster': b'\x05\x00',
            'src_endpoint': b'\x00',
            'dest_endpoint': b'\x02',
            'data': lambda params: generate_security_init(params)
        }
    }
}

def get_message(message_id, params=None):
    """
    Get message

    :param message_id: Message ID
    :param params: Optional
    :return:
    """
    if message_id in messages.keys():
        # Make a copy of the message
        message = copy.deepcopy(messages[message_id])
        data = message['frame']['data']

        # If data is a lambda then call it and replace with return value
        if callable(data):
            message['frame']['data'] = data(params)

        # Return processed message
        return message['frame']

    else:
        raise Exception('Message does not exist')

def list_messages():
    """
    List messages

    :return:
    """
    actions = {}
    for id, message in messages.items():
        actions[id] = message['name']
    return actions

def parse_version_info_update(data):
    """
    Process message, parse for version information:
    Type, Version, Manufacturer and Manufacturer Date

    :param data: Message data
    :return: Parameter dictionary of version info
    """
    # The version string is variable length. We therefore have to calculate the
    # length of the string which we then use in the unpack
    l = len(data) - 22
    values = dict(zip(
        ('cluster_cmd', 'hw_version', 'string'),
        struct.unpack('< 2x s H 17x %ds' % l, data)
    ))

    # Break down the version string into its component parts
    ret = {}
    ret['Version'] = values['hw_version']
    ret['String']  = str(values['string'].decode()) \
        .replace('\t', '\n') \
        .replace('\r', '\n') \
        .replace('\x0e', '\n') \
        .replace('\x0b', '\n') \
        .replace('\x06', '\n') \
        .replace('\x04', '\n') \
        .replace('\x12', '\n')

    ret['Manufacturer']    = ret['String'].split('\n')[0]
    ret['Type']            = ret['String'].split('\n')[1]
    ret['ManufactureDate'] = ret['String'].split('\n')[2]
    del ret['String']

    return ret

def generate_version_info_update(params):
    """
    Generate type message

    :param params: Parameter dictionary of version info
    :return: Message data
    """
    checksum = b'\tq'
    cluster_cmd = b'\xfe'
    payload = struct.pack('H', params['Version']) \
              + b'\xf8\xb9\xbb\x03\x00o\r\x009\x10\x07\x00\x00)\x00\x01\x0b' \
              + params['Manufacturer'] \
              + '\n' + params['Type'] \
              + '\n' + params['ManufactureDate']
    data = checksum + cluster_cmd + payload

    return data

def parse_range_info_update(data):
    """
    Process message, parse for RSSI range test value

    :param data: Message data
    :return: Parameter dictionary of RSSI value
    """
    values = dict(zip(
        ('cluster_cmd', 'RSSI'),
        struct.unpack('< 2x s B 1x', data)
    ))
    rssi = values['RSSI']
    return {'RSSI' : rssi}

def generate_range_update(params):
    """
    Generate range message

    :param params: Parameter dictionary of RSSI value
    :return: Message data
    """
    checksum = b'\t+'
    cluster_cmd = b'\xfd'
    payload = struct.pack('B 1x', params['RSSI'])
    data = checksum + cluster_cmd + payload

    return data

def parse_power_demand(data):
    """
    Process message, parse for power demand value.

    :param data: Message data
    :return: Parameter dictionary of power demand value
    """
    values = dict(zip(
        ('cluster_cmd', 'power_demand'),
        struct.unpack('< 2x s H', data)
    ))

    return {'PowerDemand': values['power_demand']}

def generate_power_demand_update(params):
    """
    Generate Power Demand Update message data

    :param params: Parameter dictionary of power demand value
    :return: Message data
    """
    checksum = b'\tj'
    cluster_cmd = b'\x81'
    payload = struct.pack('H', params['PowerDemand'])
    data = checksum + cluster_cmd + payload
    return data

def generate_mode_change_request(params):
    """
    Generate Mode Change Request
    Available Modes: 'Normal', 'RangeTest', 'Locked', 'Silent'

    :param params: Parameter dictionary of requested mode
    :return: Message data
    """
    mode = params['Mode']
    data = b''
    if mode == 'Normal':
        data = b'\x11\x00\xfa\x00\x01'
    elif mode == 'RangeTest':
        data = b'\x11\x00\xfa\x01\x01'
    elif mode == 'Locked':
        data = b'\x11\x00\xfa\x02\x01'
    elif mode == 'Silent':
        data = b'\x11\x00\xfa\x03\x01'
    else:
        logging.error('Invalid mode request %s', mode)

    return data

def parse_power_consumption(data):
    """
    Process message, parse for power consumption value.

    :param data: Message data
    :return: Parameter dictionary of usage stats
    """
    ret = {}
    values = dict(zip(
        ('cluster_cmd', 'powerConsumption', 'upTime'),
        struct.unpack('< 2x s I I 1x', data)
    ))
    ret['PowerConsumption'] = values['powerConsumption']
    ret['UpTime'] = values['upTime']

    return ret

def parse_switch_state_request(data):
    """
    Process message, parse for relay state change request.
    This message is sent from the hub to the smartplug requesting state change.

    :param data: Message data
    :return: Parameter dictionary of relay state
    """
    # Parse Switch State Request
    if data == b'\x11\x00\x02\x01\x01':
        return 1
    elif data == b'\x11\x00\x02\x00\x01':
        return 0
    else:
        logging.error('Unknown State Request')

def parse_switch_state_update(data):
    """
    Process message, parse for switch status.
    This message is sent from the smartplug to the hub advertising current state.

    :param data: Message data
    :return: Parameter dictionary of switch status
    """
    values = struct.unpack('< 2x b b b', data)
    if values[2] & 0x01:
        return {'State': 1}
    else:
        return {'State': 0}

def generate_switch_state_request(params):
    """
    Generate Switch State Change request data.
    This message is sent from the hub to the smartplug requesting state change.

    :param params: Parameter dictionary of relay state
    :return: Message data
    """
    data = b'\x11\x00\x01\x01'
    if 'State' in params.keys():
        if params['State'] == 1:
            # On
            data = b'\x11\x00\x02\x01\x01'
        elif params['State'] == 0:
            # Cff
            data = b'\x11\x00\x02\x00\x01'
        elif params['State'] == '':
            # Check
            data = b'\x11\x00\x01\x01'

    return data

def generate_switch_state_update(params):
    """
    Generate Switch State update message data.
    This message is sent from the smartplug to the hub advertising current state.

    :param params: Parameter dictionary of relay state
    :return: Message data
    """
    checksum = b'\th'
    cluster_cmd = b'\x80'
    payload = b'\x07\x01' if params['State'] else b'\x06\x00'
    data = checksum + cluster_cmd + payload
    return data

def parse_tamper_state(data):
    """
    Process message, parse for Tamper Switch State Change

    :param data: Message data
    :return: Parameter dictionary of tamper status
    """
    ret = {}
    if ord(data[3]) == 0x02:
        ret['TamperSwitch'] = 'OPEN'
    else:
        ret['TamperSwitch'] = 'CLOSED'

    return ret

def parse_button_press(data):
    """
    Process message, parse for button press status

    :param data: Message data
    :return: Parameter dictionary of button status
    """
    ret = {}
    if data[2] == b'\x00':
        ret['State'] = 0
    elif data[2] == b'\x01':
        ret['State'] = 1

    ret['Counter'] = struct.unpack('<H', data[5:7])[0]

    return ret

def parse_security_state(data):
    """
    Process message, parse for security state

    :param data: Message data
    :return: Parameter dictionary of security state
    """
    ret = {}
    # The switch state is in byte [3] and is a bitfield
    # bit 0 is the magnetic reed switch state
    # bit 3 is the tamper switch state
    state = ord(data[3])
    if (state & 0x01):
        ret['ReedSwitch']  = 'OPEN'
    else:
        ret['ReedSwitch']  = 'CLOSED'

    if (state & 0x04):
        ret['TamperSwitch'] = 'CLOSED'
    else:
        ret['TamperSwitch'] = 'OPEN'

    return ret

def parse_status_update(data):
    """
    Process message, parse for status update

    :param data: Message data
    :return: Parameter dictionary of state
    """
    ret = {}
    status = data[3]
    if status == b'\x1b':
        # Power Clamp
        # Unknown
        pass

    elif status == b'\x1c':
        # Power Switch
        # Unknown
        pass

    elif status == b'\x1d':
        # Key Fob
        ret['TempFahrenheit'] = float(struct.unpack("<h", data[8:10])[0]) / 100.0 * 1.8 + 32
        ret['Counter'] = struct.unpack('<I', data[4:8])[0]

    elif status == b'\x1e' or status == b'\x1f':
        # Door Sensor
        ret['TempFahrenheit'] = float(struct.unpack("<h", data[8:10])[0]) / 100.0 * 1.8 + 32
        if ord(data[-1]) & 0x01 == 1:
            ret['ReedSwitch']  = 'OPEN'
        else:
            ret['ReedSwitch']  = 'CLOSED'

        if ord(data[-1]) & 0x02 == 0:
            ret['TamperSwitch'] = 'OPEN'
        else:
            ret['TamperSwitch'] = 'CLOSED'

    else:
        logging.error('Unrecognised Device Status %r  %r', status, data)

    return ret

def generate_missing_link(params):
    """
    Generate Missing Link. Not sure what this is yet?

    :param params: Parameter dictionary (none required?)
    :return: Message data
    """
    data = b'\x11\x39\xfd'
    return data

def generate_security_init(params):
    """
    Generate Security Init

    :param params: Parameter dictionary (none required?)
    :return: Message data
    """
    checksum = b'\x11\x80\x00\x00\x05'
    return data
<|endoftext|>


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

"""
U{WSGI<http://wsgi.org>} Server implementation.
"""

# Heavily borrowed from Flashticle http://osflash.org/flashticle

import sys, traceback
from types import ClassType

from pyamf import remoting, gateway
from pyamf.util import BufferedByteStream, hexdump

__all__ = ['Gateway']

class Gateway(object):
    request_number = 0

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

    def get_processor(self, request):
        if 'DescribeService' in request.headers:
            return NotImplementedError

        return self.process_message

    def get_target(self, target):
        try:
            obj = self.services[target]
            meth = None
        except KeyError:
            name, meth = target.encode('utf8').replace('/', '.').rsplit('.', 1)
            obj = self.services[name]

        if isinstance(obj, (type, ClassType)):
            obj = obj()

            return getattr(obj, meth)
        elif callable(obj):
            return obj

        raise ValueError("Unknown")

    def get_error_response(self, (cls, e, tb)):
        details = traceback.format_exception(cls, e, tb)

        return dict(
            code='SERVER.PROCESSING',
            level='Error',
            description='%s: %s' % (cls.__name__, e),
            type=cls.__name__,
            details=''.join(details),
        )

    def process_message(self, message):
        func = self.get_target(message.target)

        try:
            message.body = func(*message.body)
            message.status = remoting.STATUS_OK
        except (SystemExit, KeyboardInterrupt):
            raise
        except:
            message.body = self.get_error_response(sys.exc_info())
            message.status = remoting.STATUS_ERROR

    def get_request_body(self, environ):
        return environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))

    def __call__(self, environ, start_response):
        self.request_number += 1

        body = self.get_request_body(environ)
        x = open('request_' + str(self.request_number), 'wb')
        x.write(body)
        envelope = remoting.decode(body)
        processor = self.get_processor(envelope)

        for message in envelope:
            processor(message)

        stream = remoting.encode(envelope)

        start_response('200 OK', [
            ('Content-Type', remoting.CONTENT_TYPE),
            ('Content-Length', str(stream.tell())),
        ])
        x.write('=' * 80)
        x.write(stream.getvalue())
        x.close()

        return [stream.getvalue()]
<commit_msg>Whoops - added debug cruft, removed<commit_after># -*- encoding: utf8 -*-
#
# Copyright (c) 2007 The PyAMF Project. All rights reserved.
#
# Nick Joyce
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
U{WSGI<http://wsgi.org>} Server implementation.
"""

# Heavily borrowed from Flashticle http://osflash.org/flashticle

import sys, traceback
from types import ClassType

from pyamf import remoting, gateway
from pyamf.util import BufferedByteStream, hexdump

__all__ = ['Gateway']

class Gateway(object):
    request_number = 0

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

    def get_processor(self, request):
        if 'DescribeService' in request.headers:
            return NotImplementedError

        return self.process_message

    def get_target(self, target):
        try:
            obj = self.services[target]
            meth = None
        except KeyError:
            name, meth = target.encode('utf8').replace('/', '.').rsplit('.', 1)
            obj = self.services[name]

        if isinstance(obj, (type, ClassType)):
            obj = obj()

            return getattr(obj, meth)
        elif callable(obj):
            return obj

        raise ValueError("Unknown")

    def get_error_response(self, (cls, e, tb)):
        details = traceback.format_exception(cls, e, tb)

        return dict(
            code='SERVER.PROCESSING',
            level='Error',
            description='%s: %s' % (cls.__name__, e),
            type=cls.__name__,
            details=''.join(details),
        )

    def process_message(self, message):
        func = self.get_target(message.target)

        try:
            message.body = func(*message.body)
            message.status = remoting.STATUS_OK
        except (SystemExit, KeyboardInterrupt):
            raise
        except:
            message.body = self.get_error_response(sys.exc_info())
            message.status = remoting.STATUS_ERROR

    def get_request_body(self, environ):
        return environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))

    def __call__(self, environ, start_response):
        self.request_number += 1

        body = self.get_request_body(environ)
        #x = open('request_' + str(self.request_number), 'wb')
        #x.write(body)
        envelope = remoting.decode(body)
        processor = self.get_processor(envelope)

        for message in envelope:
            processor(message)

        stream = remoting.encode(envelope)

        start_response('200 OK', [
            ('Content-Type', remoting.CONTENT_TYPE),
            ('Content-Length', str(stream.tell())),
        ])
        #x.write('=' * 80)
        #x.write(stream.getvalue())
        #x.close()

        return [stream.getvalue()]
<|endoftext|>


<commit_before>import sys, pygame, math

class Ball():
    def __init__(self, images, speed, pos=[0,0]):
        self.speedx = speed[0]
        self.speedy = speed[1]
        self.speed = [self.speedx, self.speedy]

        self.images = []
        for image in images:
            self.images += [pygame.image.load(image)]
        self.image = self.images[0]
        self.rect = self.image.get_rect()
        self.radius = self.rect.width/2 - 2

        self.frame = 0
        self.maxFrame = len(self.images)-1
        self.timer = 0
        self.timerMax = .25* 60

        self.didBounceX = False
        self.didBounceY = False

        self.rect = self.rect.move(pos)

        self.living = True

    def die(self):
        self.living = False

    def update(self, size):
        self.move()
        self.animate()
        self.collideScreen(size)

    def animate(self):
        if self.timer < self.timerMax:
            self.timer += 1
        else:
            self.timer = 0
            if self.frame < self.maxFrame:
                self.frame += 1
            else:
                self.frame = 0
            self.image = self.images[self.frame]

    def move(self):
        self.speed = [self.speedx, self.speedy]
        self.rect = self.rect.move(self.speed)
        self.didBounceX = False
        self.didBounceY = False

    def collideScreen(self, size):
        width = size[0]
        height = size[1]

        if not self.didBounceX:
            if self.rect.left < 0 or self.rect.right > width:
                self.speedx = -self.speedx
                self.didBounceX = True
                self.move()
        if not self.didBounceY:
            if self.rect.top < 0 or self.rect.bottom > height:
                self.speedy = -self.speedy
                selfdidBounceY = True
                self.move()

    def collideBall(self, other):
        if self.rect.right > other.rect.left and self.rect.left < other.rect.right:
            if self.rect.bottom > other.rect.top and self.rect.top < other.rect.bottom:
                if self.radius + other.radius > self.distanceTo(other.rect.center):
                    if not self.didBounceX:
                        if ((self.rect.center[0] < other.rect.center[0] and self.speedx > 0) or
                            (self.rect.center[0] > other.rect.center[0] and self.speedx < 0)):
                            self.speedx = -self.speedx
                            self.didBounceX = True
                            self.move()
                    if not self.didBounceY:
                        if ((self.rect.center[1] < other.rect.center[1] and self.speedy > 0) or
                            (self.rect.center[1] > other.rect.center[1] and self.speedy < 0)):
                            self.speedy = -self.speedy
                            self.didBounceY = True
                            self.move()
                    return True
        return False

    def distanceTo(self, pt):
        x1 = self.rect.center[0]
        y1 = self.rect.center[1]
        x2 = pt[0]
        y2 = pt[1]
        return math.sqrt((x1-x2)**2+(y1-y2)**2)

<commit_msg>added mass attribute<commit_after>import sys, pygame, math

class Ball():
    def __init__(self, images, speed, pos=[0,0]):
        self.speedx = speed[0]
        self.speedy = speed[1]
        self.speed = [self.speedx, self.speedy]

        self.images = []
        for image in images:
            self.images += [pygame.image.load(image)]
        self.image = self.images[0]
        self.rect = self.image.get_rect()
        self.radius = self.rect.width/2 - 2

        self.frame = 0
        self.maxFrame = len(self.images)-1
        self.timer = 0
        self.timerMax = .25* 60

        self.didBounceX = False
        self.didBounceY = False

        self.rect = self.rect.move(pos)

        self.living = True

        self.mass = 0

    def die(self):
        self.living = False

    def update(self, size):
        self.move()
        self.animate()
        self.collideScreen(size)

    def animate(self):
        if self.timer < self.timerMax:
            self.timer += 1
        else:
            self.timer = 0
            if self.frame < self.maxFrame:
                self.frame += 1
            else:
                self.frame = 0
            self.image = self.images[self.frame]

    def move(self):
        self.speed = [self.speedx, self.speedy]
        self.rect = self.rect.move(self.speed)
        self.didBounceX = False
        self.didBounceY = False

    def collideScreen(self, size):
        width = size[0]
        height = size[1]

        if not self.didBounceX:
            if self.rect.left < 0 or self.rect.right > width:
                self.speedx = -self.speedx
                self.didBounceX = True
                self.move()
        if not self.didBounceY:
            if self.rect.top < 0 or self.rect.bottom > height:
                self.speedy = -self.speedy
                selfdidBounceY = True
                self.move()

    def collideBall(self, other):
        if self.rect.right > other.rect.left and self.rect.left < other.rect.right:
            if self.rect.bottom > other.rect.top and self.rect.top < other.rect.bottom:
                if self.radius + other.radius > self.distanceTo(other.rect.center):
                    if not self.didBounceX:
                        if ((self.rect.center[0] < other.rect.center[0] and self.speedx > 0) or
                            (self.rect.center[0] > other.rect.center[0] and self.speedx < 0)):
                            self.speedx = -self.speedx
                            self.didBounceX = True
                            self.move()
                    if not self.didBounceY:
                        if ((self.rect.center[1] < other.rect.center[1] and self.speedy > 0) or
                            (self.rect.center[1] > other.rect.center[1] and self.speedy < 0)):
                            self.speedy = -self.speedy
                            self.didBounceY = True
                            self.move()
                    return True
        return False

    def distanceTo(self, pt):
        x1 = self.rect.center[0]
        y1 = self.rect.center[1]
        x2 = pt[0]
        y2 = pt[1]
        return math.sqrt((x1-x2)**2+(y1-y2)**2)

<|endoftext|>


<commit_before>def update_read_tracking(topic, user):
    tracking = user.readtracking

    #if last_read > last_read - don't check topics
    if tracking.last_read and tracking.last_read > (topic.last_post.updated or
                                                    topic.last_post.created):
        return

    if isinstance(tracking.topics, dict):
        #clear topics if len > 5Kb and set last_read to current time
        if len(tracking.topics) > 5120:
            tracking.topics = None
            tracking.last_read = datetime.now()
            tracking.save()
        #update topics if exist new post or does't exist in dict
        if topic.last_post.pk > tracking.topics.get(str(topic.pk), 0):
            tracking.topics.setdefault(str(topic.pk), topic.last_post.pk)
            tracking.save()
    else:
        #initialize topic tracking dict
        tracking.topics = {topic.pk: topic.last_post.pk}
        tracking.save()
<commit_msg>Fix bug in read tracking system<commit_after>def update_read_tracking(topic, user):
    tracking = user.readtracking

    #if last_read > last_read - don't check topics
    if tracking.last_read and tracking.last_read > (topic.last_post.updated or
                                                    topic.last_post.created):
        return

    if isinstance(tracking.topics, dict):
        #clear topics if len > 5Kb and set last_read to current time
        if len(tracking.topics) > 5120:
            tracking.topics = None
            tracking.last_read = datetime.now()
            tracking.save()
        #update topics if new post exists or cache entry is empty
        if topic.last_post.pk > tracking.topics.get(str(topic.pk), 0):
            tracking.topics[str(topic.pk)] = topic.last_post.pk
            tracking.save()
    else:
        #initialize topic tracking dict
        tracking.topics = {topic.pk: topic.last_post.pk}
        tracking.save()
<|endoftext|>


<commit_before># Copyright (C) 2006, 2007, 2008  Andrey Golovizin
#
# This file is part of pybtex.
#
# pybtex is free software; you can redistribute it and/or modify
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# pybtex is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pybtex; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
# USA

"""name formatting styles
"""
from pybtex.richtext import Symbol, Text, nbsp
from pybtex.style.template import join, together, node, _format_list
from pybtex.bibtex.name import tie_or_space

@node
def name_part(children, data, before='', tie=False):
    parts = together [children].format_data(data)
    if not parts:
        return Text()
    if tie:
        return Text(before, parts, tie_or_space(parts, nbsp, ' '))
    else:
        return Text(before, parts)

def plain(person, abbr=False):
    r"""
    >>> from pybtex.core import Person
    >>> name = Person(string=r"Charles Louis Xavier Joseph de la Vall{\'e}e Poussin")
    >>> print plain(name).format().plaintext()
    de<nbsp>la Vall{\'e}e<nbsp>Poussin, Charles Louis Xavier<nbsp>Joseph
    >>> print plain(name, abbr=True).format().plaintext()
    de<nbsp>la Vall{\'e}e<nbsp>Poussin, C.<nbsp>L. X.<nbsp>J.
    >>> name = Person(first='First', last='Last', middle='Middle')
    >>> print plain(name).format().plaintext()
    Last, First<nbsp>Middle
    >>> print plain(name, abbr=True).format().plaintext()
    Last, F.<nbsp>M.

    """
    return join [
        name_part(tie=True) [person.prelast()],
        name_part [person.last()],
        name_part(before=', ') [person.lineage()],
        name_part(before=', ') [person.first(abbr) + person.middle(abbr)],
    ]
<commit_msg>Rename names.plain to last_first, add a real plain style ({ff~}{vv~}{ll}{, jj}).<commit_after># Copyright (C) 2006, 2007, 2008  Andrey Golovizin
#
# This file is part of pybtex.
#
# pybtex is free software; you can redistribute it and/or modify
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# pybtex is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pybtex; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
# USA

"""name formatting styles
"""
from pybtex.richtext import Symbol, Text, nbsp
from pybtex.style.template import join, together, node, _format_list
from pybtex.bibtex.name import tie_or_space

@node
def name_part(children, data, before='', tie=False):
    parts = together [children].format_data(data)
    if not parts:
        return Text()
    if tie:
        return Text(before, parts, tie_or_space(parts, nbsp, ' '))
    else:
        return Text(before, parts)

def plain(person, abbr=False):
    r"""
    {ff~}{vv~}{ll}{, jj}

    >>> from pybtex.core import Person
    >>> name = Person(string=r"Charles Louis Xavier Joseph de la Vall{\'e}e Poussin")
    >>> print plain(name).format().plaintext()
    Charles Louis Xavier<nbsp>Joseph de<nbsp>la Vall{\'e}e<nbsp>Poussin
    >>> print plain(name, abbr=True).format().plaintext()
    C.<nbsp>L. X.<nbsp>J. de<nbsp>la Vall{\'e}e<nbsp>Poussin

    >>> name = Person(first='First', last='Last', middle='Middle')
    >>> print plain(name).format().plaintext()
    First<nbsp>Middle Last
    >>> print plain(name, abbr=True).format().plaintext()
    F.<nbsp>M. Last
    """
    return join [
        name_part(tie=True) [person.first(abbr) + person.middle(abbr)],
        name_part(tie=True) [person.prelast()],
        name_part [person.last()],
        name_part(before=', ') [person.lineage()]
    ]

def last_first(person, abbr=False):
    r"""
    >>> from pybtex.core import Person
    >>> name = Person(string=r"Charles Louis Xavier Joseph de la Vall{\'e}e Poussin")
    >>> print last_first(name).format().plaintext()
    de<nbsp>la Vall{\'e}e<nbsp>Poussin, Charles Louis Xavier<nbsp>Joseph
    >>> print last_first(name, abbr=True).format().plaintext()
    de<nbsp>la Vall{\'e}e<nbsp>Poussin, C.<nbsp>L. X.<nbsp>J.

    >>> name = Person(first='First', last='Last', middle='Middle')
    >>> print last_first(name).format().plaintext()
    Last, First<nbsp>Middle
    >>> print last_first(name, abbr=True).format().plaintext()
    Last, F.<nbsp>M.

    """
    return join [
        name_part(tie=True) [person.prelast()],
        name_part [person.last()],
        name_part(before=', ') [person.lineage()],
        name_part(before=', ') [person.first(abbr) + person.middle(abbr)],
    ]
<|endoftext|>


<commit_before>import abc
import commands
import os.path as path
import tempfile
from pybug.io.base import Importer, find_alternative_files, \
    map_filepath_to_importer
from pybug.io.mesh.assimp import AIImporter
from pybug.io.exceptions import MeshImportError
from pybug.shape import TexturedTriMesh, TriMesh
from pyvrml import buildVRML97Parser
import pyvrml.vrml97.basenodes as basenodes
from scipy.spatial import Delaunay
import numpy as np

def process_with_meshlabserver(file_path, output_dir=None, script_path=None,
                               output_filetype=None, export_flags=None,
                               meshlab_command='meshlabserver'):
    r"""
    Interface to `meshlabserver` to perform prepossessing on meshes before
    import. Returns a path to the result of the meshlabserver call, ready for
    import as usual. **Requires Meshlab to be installed**.

    Parameters
    ----------
    file_path : string
        Absolute filepath to the mesh
    script_path : atring, optional
        If specified this script will be run on the input mesh.

        Default: ``None``
    output_dir : string, optional
        The output directory for the processed mesh.

        Default: The users tmp directory.
    output_filetype : string, optional
        The output filetype desired from meshlabserver. Takes the form of an
        extension, eg ``obj``.

        Default: The same as the input mesh
    export_flags : string, optional
        Flags passed to the ``-om`` parameter. Allows for choosing
        what aspects of the model will be exported (normals,
        texture coords etc)
    meshlab_command : string, optional
        The meshlabserver executable to run.

        Default: 'meshlabserver'

    Returns
    -------
    output_path : string
        The absolute filepath to the processed mesh.
    """
    if output_dir is None:
        output_dir = tempfile.gettempdir()
    filename = path.split(file_path)[-1]
    if output_filetype is not None:
        file_root = path.splitext(filename)[0]
        output_filename = file_root + '.' + output_filetype
    else:
        output_filename = filename
    output_path = path.join(output_dir, output_filename)
    command = (meshlab_command + ' -i ' + file_path + ' -o ' +
               output_path)
    if script_path is not None:
        command += ' -s ' + script_path
    if export_flags is not None:
        command += ' -om ' + export_flags
    commands.getoutput(command)
    return output_path

class MeshImporter(Importer):
    r"""
    Abstract base class for importing meshes. Searches in the directory
    specified by filepath for landmarks and textures with the same basename as
    the mesh. If found, they are automatically attached. If a texture is found
    then a :class:`pybug.shape.mesh.textured.TexturedTriMesh` is built, else a
    :class:`pybug.shape.mesh.base.Trimesh` is built.

    Parameters
    ----------
    filepath : string
        Absolute filepath of the mesh.
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(MeshImporter, self).__init__(filepath)
        self.meshes = []
        self.attempted_texture_search = False
        self.attempted_landmark_search = False

    def _build_texture_and_landmark_importers(self):
        r"""
        Search for a texture and landmark file in the same directory as the
        mesh. If they exist, create importers for them.
        """
        if self.texture_path is None or not path.exists(self.texture_path):
            self.texture_importer = None
        else:
            # This import is here to avoid circular dependencies
            from pybug.io.extensions import image_types
            self.texture_importer = map_filepath_to_importer(self.texture_path,
                                                             image_types)

        if self.landmark_path is None or not path.exists(self.landmark_path):
            self.landmark_importer = None
        else:
            # This import is here to avoid circular dependencies
            from pybug.io.extensions import mesh_landmark_types
            self.landmark_importer = map_filepath_to_importer(
                self.landmark_path, mesh_landmark_types)

    def _search_for_texture(self):
        r"""
        Tries to find a texture with the same name as the mesh.

        Returns
        --------
        relative_texture_path : string
            The relative path to the texture or ``None`` if one can't be found
        """
        # Stop searching every single time we access the property
        self.attempted_texture_search = True
        # This import is here to avoid circular dependencies
        from pybug.io.extensions import image_types
        try:
            return find_alternative_files('texture', self.filepath,
                                          image_types)
        except ImportError:
            return None

    def _search_for_landmarks(self):
        """
        Tries to find a landmark file with the same name as the mesh.

        Returns
        --------
        relative_landmark_path : string
            The relative path to the landmarks or ``None`` if none can be found
        """
        # Stop searching every single time we access the property
        self.attempted_landmark_search = True
        # This import is here to avoid circular dependencies
        from pybug.io.extensions import mesh_landmark_types
        try:
            return find_alternative_files('landmarks', self.filepath,
                                          mesh_landmark_types)
        except ImportError:
            return None

    @property
    def texture_path(self):
        """
        Get the absolute path to the texture. Returns None if one can't be
        found. Makes it's best effort to find an appropriate texture by
        searching for textures with the same name as the mesh. Will only
        search for the path the first time ``texture_path`` is invoked.

        Sets the ``self.relative_texture_path`` attribute.

        Returns
        -------
        texture_path : string
            Absolute filepath to the texture
        """
        # Avoid attribute not being set
        if not hasattr(self, 'relative_texture_path'):
            self.relative_texture_path = None

        # Try find a texture path if we can
        if self.relative_texture_path is None and \
                not self.attempted_texture_search:
            self.relative_texture_path = self._search_for_texture()

        try:
            return path.join(self.folder, self.relative_texture_path)
        except AttributeError:
            return None

    @property
    def landmark_path(self):
        """
        Get the absolute path to the landmarks. Returns None if none can be
        found. Makes it's best effort to find an appropriate landmark set by
        searching for landmarks with the same name as the mesh. Will only
        search for the path the first time ``landmark_path`` is invoked.

        Sets the ``self.relative_landmark_path`` attribute.

        Returns
        -------
        landmark_path : string
            Absolute filepath to the landmarks
        """
        # Avoid attribute not being set
        if not hasattr(self, 'relative_landmark_path'):
            self.relative_landmark_path = None

        # Try find a texture path if we can
        if self.relative_landmark_path is None and \
                not self.attempted_landmark_search:
            self.relative_landmark_path = self._search_for_landmarks()

        try:
            return path.join(self.folder, self.relative_landmark_path)
        except AttributeError:
            return None

    @abc.abstractmethod
    def _parse_format(self):
        r"""
        Abstract method that handles actually building a mesh. This involves
        reading the mesh from disk and doing any necessary parsing.

        Should set the ``self.meshes`` attribute. Each mesh in ``self.meshes``
        is expected to be an object with attributes:

        ======== ==========================
        name     type
        ======== ==========================
        points   double ndarray
        trilist  int ndarray
        tcoords  double ndarray (optional)
        ======== ==========================

        May also set the ``self.relative_texture_path`` if it is specified by
        the format.
        """
        pass

    def build(self):
        r"""
        Overrides the :meth:`build <pybug.io.base.Importer.build>` method.

        Parse the format as defined by :meth:`_parse_format` and then search
        for valid textures and landmarks that may have been defined by the
        format.

        Build the appropriate type of mesh defined by parsing the format. May
        or may not be textured.

        Returns
        -------
        meshes : list of :class:`pybug.shape.mesh.textured.TexturedTriMesh` or :class:`pybug.shape.mesh.base.Trimesh`
            If more than one mesh, returns a list of meshes. If only one
            mesh, returns the single mesh.
        """
        #
        self._parse_format()
        self._build_texture_and_landmark_importers()

        meshes = []
        for mesh in self.meshes:
            if self.texture_importer is not None:
                new_mesh = TexturedTriMesh(mesh.points.astype(np.float64),
                                           mesh.trilist,
                                           mesh.tcoords,
                                           self.texture_importer.build())
            else:
                new_mesh = TriMesh(mesh.points, mesh.trilist)

            if self.landmark_importer is not None:
                lmark_group = self.landmark_importer.build(
                    scale_factors=np.max(mesh.points))
                new_mesh.landmarks[lmark_group.group_label] = lmark_group

            meshes.append(new_mesh)
        if len(meshes) == 1:
            return meshes[0]
        else:
            return meshes

class AssimpImporter(AIImporter, MeshImporter):
    """
    Uses assimp to import meshes. The assimp importing is wrapped via cython,

    Parameters
    ----------
    filepath : string
        Absolute filepath of the mesh.
    """
    def __init__(self, filepath):
        super(AssimpImporter, self).__init__(filepath)

    def _parse_format(self):
        r"""
        Use assimp to build the mesh. Also, get the relative texture path.
        """
        self.build_scene()
        # Properties should have different names because of multiple
        # inheritance
        self.relative_texture_path = self.assimp_texture_path

class WRLImporter(MeshImporter):
    """
    Allows importing VRML meshes.
    Uses a fork of PyVRML97 to do (hopefully) more robust parsing of VRML
    files. It should be noted that, unfortunately, this is a lot slower than
    the C++-based assimp importer.

    VRML allows non-triangular polygons, whilst our importer pipeline doesn't.
    Therefore, any non-triangular polygons are dropped. VRML also allows
    separate texture coordinate indices, which we do not support. To have
    a better formed mesh, try exporting the WRL as OBJ from Meshlab.

    Parameters
    ----------
    filepath : string
        Absolute filepath of the mesh.
    """

    def __init__(self, filepath):
        # Setup class before super class call
        super(WRLImporter, self).__init__(filepath)

    def _parse_format(self):
        r"""
        Use pyVRML to parse the file and build a mesh object. A single mesh per
        file is assumed.

        Raises
        ------
        MeshImportError
            If no transform or shape is found in the scenegraph
        """
        with open(self.filepath) as f:
            self.text = f.read()

        parser = buildVRML97Parser()
        vrml_tuple = parser.parse(self.text)

        # Build expando object (dynamic object hack)
        self.mesh = lambda: 0

        # I assume these tuples are always built in this order
        scenegraph = vrml_tuple[1][1]
        transform = None
        for child in scenegraph.children:
            if type(child) is basenodes.Transform:
                transform = child

        if transform is None:
            raise MeshImportError('Unable to find transform in scenegraph')

        shape = None
        for child in transform.children:
            if type(child) is basenodes.Shape:
                shape = child

        if shape is None:
            raise MeshImportError('Unable to find shape in transform')

        self.mesh.points = shape.geometry.coord.point
        self.mesh.tcoords = shape.geometry.texCoord.point

        self.mesh.trilist = self._filter_non_triangular_polygons(
                shape.geometry.coordIndex)

        # See if we have a seperate texture index, if not just create an empty
        # array
        try:
            tex_trilist = self._filter_non_triangular_polygons(
                shape.geometry.texCoordIndex)
        except AttributeError:
            tex_trilist = np.array([-1])

        # Fix texture coordinates - we can only have one index so we choose
        # to use the triangle index
        if np.max(tex_trilist) > np.max(self.mesh.trilist):
            new_tcoords = np.zeros([self.mesh.points.shape[0], 2])
            new_tcoords[self.mesh.trilist] = self.mesh.tcoords[tex_trilist]
            self.mesh.tcoords = new_tcoords

        # Get the texture path - it's fine not to have one defined
        try:
            self.relative_texture_path = shape.appearance.texture.url[0]
        except (AttributeError, IndexError):
            self.relative_texture_path = None

        # Assumes a single mesh per file
        self.meshes = [self.mesh]

    def _filter_non_triangular_polygons(self, coord_list):
        # VRML allows arbitrary polygon coordinates, whilst we only support
        # triangles. They are delimited by -1, so we split on them and filter
        # out non-triangle polygons
        index_list = coord_list
        index_list = np.split(index_list, np.where(index_list == -1)[0])
        # The first polygon is missing the -1 at the beginning
        # Have to cast to int32 because that's the default, but on 64bit
        # machines a single number defaults to int64
        np.insert(index_list[0], 0, np.array([-1], dtype=np.int32))
        # Filter out those coordinates that are not triangles
        index_list = [i for i in index_list if len(i[1:]) == 3]
        # Convert to 2D array
        index_list = np.array(index_list)
        # Slice of -1 delimiters
        return index_list[:, 1:]

<commit_msg>HOTFIX: Loading VRML files from BU4D<commit_after>import abc
import commands
import os.path as path
import tempfile
from pybug.io.base import Importer, find_alternative_files, \
    map_filepath_to_importer
from pybug.io.mesh.assimp import AIImporter
from pybug.io.exceptions import MeshImportError
from pybug.shape import TexturedTriMesh, TriMesh
from pyvrml import buildVRML97Parser
import pyvrml.vrml97.basenodes as basenodes
from scipy.spatial import Delaunay
import numpy as np

def process_with_meshlabserver(file_path, output_dir=None, script_path=None,
                               output_filetype=None, export_flags=None,
                               meshlab_command='meshlabserver'):
    r"""
    Interface to `meshlabserver` to perform prepossessing on meshes before
    import. Returns a path to the result of the meshlabserver call, ready for
    import as usual. **Requires Meshlab to be installed**.

    Parameters
    ----------
    file_path : string
        Absolute filepath to the mesh
    script_path : atring, optional
        If specified this script will be run on the input mesh.

        Default: ``None``
    output_dir : string, optional
        The output directory for the processed mesh.

        Default: The users tmp directory.
    output_filetype : string, optional
        The output filetype desired from meshlabserver. Takes the form of an
        extension, eg ``obj``.

        Default: The same as the input mesh
    export_flags : string, optional
        Flags passed to the ``-om`` parameter. Allows for choosing
        what aspects of the model will be exported (normals,
        texture coords etc)
    meshlab_command : string, optional
        The meshlabserver executable to run.

        Default: 'meshlabserver'

    Returns
    -------
    output_path : string
        The absolute filepath to the processed mesh.
    """
    if output_dir is None:
        output_dir = tempfile.gettempdir()
    filename = path.split(file_path)[-1]
    if output_filetype is not None:
        file_root = path.splitext(filename)[0]
        output_filename = file_root + '.' + output_filetype
    else:
        output_filename = filename
    output_path = path.join(output_dir, output_filename)
    command = (meshlab_command + ' -i ' + file_path + ' -o ' +
               output_path)
    if script_path is not None:
        command += ' -s ' + script_path
    if export_flags is not None:
        command += ' -om ' + export_flags
    commands.getoutput(command)
    return output_path

class MeshImporter(Importer):
    r"""
    Abstract base class for importing meshes. Searches in the directory
    specified by filepath for landmarks and textures with the same basename as
    the mesh. If found, they are automatically attached. If a texture is found
    then a :class:`pybug.shape.mesh.textured.TexturedTriMesh` is built, else a
    :class:`pybug.shape.mesh.base.Trimesh` is built.

    Parameters
    ----------
    filepath : string
        Absolute filepath of the mesh.
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, filepath):
        super(MeshImporter, self).__init__(filepath)
        self.meshes = []
        self.attempted_texture_search = False
        self.attempted_landmark_search = False

    def _build_texture_and_landmark_importers(self):
        r"""
        Search for a texture and landmark file in the same directory as the
        mesh. If they exist, create importers for them.
        """
        if self.texture_path is None or not path.exists(self.texture_path):
            self.texture_importer = None
        else:
            # This import is here to avoid circular dependencies
            from pybug.io.extensions import image_types
            self.texture_importer = map_filepath_to_importer(self.texture_path,
                                                             image_types)

        if self.landmark_path is None or not path.exists(self.landmark_path):
            self.landmark_importer = None
        else:
            # This import is here to avoid circular dependencies
            from pybug.io.extensions import mesh_landmark_types
            self.landmark_importer = map_filepath_to_importer(
                self.landmark_path, mesh_landmark_types)

    def _search_for_texture(self):
        r"""
        Tries to find a texture with the same name as the mesh.

        Returns
        --------
        relative_texture_path : string
            The relative path to the texture or ``None`` if one can't be found
        """
        # Stop searching every single time we access the property
        self.attempted_texture_search = True
        # This import is here to avoid circular dependencies
        from pybug.io.extensions import image_types
        try:
            return find_alternative_files('texture', self.filepath,
                                          image_types)
        except ImportError:
            return None

    def _search_for_landmarks(self):
        """
        Tries to find a landmark file with the same name as the mesh.

        Returns
        --------
        relative_landmark_path : string
            The relative path to the landmarks or ``None`` if none can be found
        """
        # Stop searching every single time we access the property
        self.attempted_landmark_search = True
        # This import is here to avoid circular dependencies
        from pybug.io.extensions import mesh_landmark_types
        try:
            return find_alternative_files('landmarks', self.filepath,
                                          mesh_landmark_types)
        except ImportError:
            return None

    @property
    def texture_path(self):
        """
        Get the absolute path to the texture. Returns None if one can't be
        found. Makes it's best effort to find an appropriate texture by
        searching for textures with the same name as the mesh. Will only
        search for the path the first time ``texture_path`` is invoked.

        Sets the ``self.relative_texture_path`` attribute.

        Returns
        -------
        texture_path : string
            Absolute filepath to the texture
        """
        # Avoid attribute not being set
        if not hasattr(self, 'relative_texture_path'):
            self.relative_texture_path = None

        # Try find a texture path if we can
        if self.relative_texture_path is None and \
                not self.attempted_texture_search:
            self.relative_texture_path = self._search_for_texture()

        try:
            return path.join(self.folder, self.relative_texture_path)
        except AttributeError:
            return None

    @property
    def landmark_path(self):
        """
        Get the absolute path to the landmarks. Returns None if none can be
        found. Makes it's best effort to find an appropriate landmark set by
        searching for landmarks with the same name as the mesh. Will only
        search for the path the first time ``landmark_path`` is invoked.

        Sets the ``self.relative_landmark_path`` attribute.

        Returns
        -------
        landmark_path : string
            Absolute filepath to the landmarks
        """
        # Avoid attribute not being set
        if not hasattr(self, 'relative_landmark_path'):
            self.relative_landmark_path = None

        # Try find a texture path if we can
        if self.relative_landmark_path is None and \
                not self.attempted_landmark_search:
            self.relative_landmark_path = self._search_for_landmarks()

        try:
            return path.join(self.folder, self.relative_landmark_path)
        except AttributeError:
            return None

    @abc.abstractmethod
    def _parse_format(self):
        r"""
        Abstract method that handles actually building a mesh. This involves
        reading the mesh from disk and doing any necessary parsing.

        Should set the ``self.meshes`` attribute. Each mesh in ``self.meshes``
        is expected to be an object with attributes:

        ======== ==========================
        name     type
        ======== ==========================
        points   double ndarray
        trilist  int ndarray
        tcoords  double ndarray (optional)
        ======== ==========================

        May also set the ``self.relative_texture_path`` if it is specified by
        the format.
        """
        pass

    def build(self):
        r"""
        Overrides the :meth:`build <pybug.io.base.Importer.build>` method.

        Parse the format as defined by :meth:`_parse_format` and then search
        for valid textures and landmarks that may have been defined by the
        format.

        Build the appropriate type of mesh defined by parsing the format. May
        or may not be textured.

        Returns
        -------
        meshes : list of :class:`pybug.shape.mesh.textured.TexturedTriMesh` or :class:`pybug.shape.mesh.base.Trimesh`
            If more than one mesh, returns a list of meshes. If only one
            mesh, returns the single mesh.
        """
        #
        self._parse_format()
        self._build_texture_and_landmark_importers()

        meshes = []
        for mesh in self.meshes:
            if self.texture_importer is not None:
                new_mesh = TexturedTriMesh(mesh.points.astype(np.float64),
                                           mesh.trilist,
                                           mesh.tcoords,
                                           self.texture_importer.build())
            else:
                new_mesh = TriMesh(mesh.points, mesh.trilist)

            if self.landmark_importer is not None:
                lmark_group = self.landmark_importer.build(
                    scale_factors=np.max(mesh.points))
                new_mesh.landmarks[lmark_group.group_label] = lmark_group

            meshes.append(new_mesh)
        if len(meshes) == 1:
            return meshes[0]
        else:
            return meshes

class AssimpImporter(AIImporter, MeshImporter):
    """
    Uses assimp to import meshes. The assimp importing is wrapped via cython,

    Parameters
    ----------
    filepath : string
        Absolute filepath of the mesh.
    """
    def __init__(self, filepath):
        super(AssimpImporter, self).__init__(filepath)

    def _parse_format(self):
        r"""
        Use assimp to build the mesh. Also, get the relative texture path.
        """
        self.build_scene()
        # Properties should have different names because of multiple
        # inheritance
        self.relative_texture_path = self.assimp_texture_path

class WRLImporter(MeshImporter):
    """
    Allows importing VRML meshes.
    Uses a fork of PyVRML97 to do (hopefully) more robust parsing of VRML
    files. It should be noted that, unfortunately, this is a lot slower than
    the C++-based assimp importer.

    VRML allows non-triangular polygons, whilst our importer pipeline doesn't.
    Therefore, any non-triangular polygons are dropped. VRML also allows
    separate texture coordinate indices, which we do not support. To have
    a better formed mesh, try exporting the WRL as OBJ from Meshlab.

    Parameters
    ----------
    filepath : string
        Absolute filepath of the mesh.
    """

    def __init__(self, filepath):
        # Setup class before super class call
        super(WRLImporter, self).__init__(filepath)

    def _parse_format(self):
        r"""
        Use pyVRML to parse the file and build a mesh object. A single mesh per
        file is assumed.

        Raises
        ------
        MeshImportError
            If no transform or shape is found in the scenegraph
        """
        with open(self.filepath) as f:
            self.text = f.read()

        parser = buildVRML97Parser()
        vrml_tuple = parser.parse(self.text)

        # Build expando object (dynamic object hack)
        self.mesh = lambda: 0

        # I assume these tuples are always built in this order
        scenegraph = vrml_tuple[1][1]
        shape_container = None

        # Let's check if
        for child in scenegraph.children:
            if type(child) in [basenodes.Transform, basenodes.Group]:
                # Only fetch the first container (unknown what do do with more
                # than one container at this time)
                shape_container = child
                break

        if shape_container is None:
            raise MeshImportError('Unable to find shape container in '
                                  'scenegraph')

        shape = None
        for child in shape_container.children:
            if type(child) is basenodes.Shape:
                # Only fetch the first shape (unknown what do do with more
                # than one shape at this time)
                shape = child
                break

        if shape is None:
            raise MeshImportError('Unable to find shape in transform')

        self.mesh.points = shape.geometry.coord.point
        self.mesh.tcoords = shape.geometry.texCoord.point

        self.mesh.trilist = self._filter_non_triangular_polygons(
                shape.geometry.coordIndex)

        # See if we have a seperate texture index, if not just create an empty
        # array
        try:
            tex_trilist = self._filter_non_triangular_polygons(
                shape.geometry.texCoordIndex)
        except AttributeError:
            tex_trilist = np.array([-1])

        # Fix texture coordinates - we can only have one index so we choose
        # to use the triangle index
        if np.max(tex_trilist) > np.max(self.mesh.trilist):
            new_tcoords = np.zeros([self.mesh.points.shape[0], 2])
            new_tcoords[self.mesh.trilist] = self.mesh.tcoords[tex_trilist]
            self.mesh.tcoords = new_tcoords

        # Get the texture path - it's fine not to have one defined
        try:
            self.relative_texture_path = shape.appearance.texture.url[0]
        except (AttributeError, IndexError):
            self.relative_texture_path = None

        # Assumes a single mesh per file
        self.meshes = [self.mesh]

    def _filter_non_triangular_polygons(self, coord_list):
        # VRML allows arbitrary polygon coordinates, whilst we only support
        # triangles. They are delimited by -1, so we split on them and filter
        # out non-triangle polygons
        index_list = coord_list
        index_list = np.split(index_list, np.where(index_list == -1)[0])
        # The first polygon is missing the -1 at the beginning
        # Have to cast to int32 because that's the default, but on 64bit
        # machines a single number defaults to int64
        np.insert(index_list[0], 0, np.array([-1], dtype=np.int32))
        # Filter out those coordinates that are not triangles
        index_list = [i for i in index_list if len(i[1:]) == 3]
        # Convert to 2D array
        index_list = np.array(index_list)
        # Slice of -1 delimiters
        return index_list[:, 1:]

<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# Copyright (c) 2011 The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details.
"""
    pycroft.model.base
    ~~~~~~~~~~~~~~

    This module contains base stuff for all models.

    :copyright: (c) 2011 by AG DSN.
"""

from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base, declared_attr
from sqlalchemy import Integer, String, MetaData, ForeignKey, Boolean, DateTime
from sqlalchemy.orm import relation, backref
from sqlalchemy import Table, Column

_session = None

class _ModelMeta(DeclarativeMeta):
    """Metaclass for all mapped Database objects.
    """

    @property
    def q(cls):
        """This is a shortcut for easy querying of whole objects.

        With this metaclass shortcut you can query a Model with
        Model.q.filter(...) without using the verbose session stuff
        """
        global _session
        if _session is None:
            import pycroft.model.session
            _session = pycroft.model.session.session
        return _session.query(cls)

class _Base(object):
    """Baseclass for all database models.

    """

    @declared_attr
    def __tablename__(cls):
        """Autogenerate the tablename for the mapped objects.

        """
        return cls.__name__.lower()

ModelBase = declarative_base(cls=_Base, metaclass=_ModelMeta)

class Dormitory(ModelBase):
    id = Column(Integer, primary_key=True)
    number = Column(String(3), unique=True)
    street = Column(String(20))
    short_name = Column(String(5), unique=True)

class Room(ModelBase):
    id = Column(Integer, primary_key=True)
    number = Column(String(36))
    level = Column(Integer)
    inhabitable = Column(Boolean)
    dormitory_id = Column(Integer, ForeignKey("dormitory.id"))

    dormitory = relation("Dormitory", backref=backref("rooms",
                                                      order_by=number))

class User(ModelBase):
    id = Column(Integer, primary_key=True)
    name = Column(String(255))
    login = Column(String(40))
    registration_date = Column(DateTime)
    room_id = Column(Integer, ForeignKey("room.id"))

    room = relation("Room", backref=backref("users", order_by=id))

class Host(ModelBase):
    id = Column(Integer, primary_key=True)
    hostname = Column(String(255))
<commit_msg>ADD class LogEntry, Subnet, TrafficVolume, VLan ADD import BigInteger, Text, postgresql ADD id in class _Base REMOVE id in child classes of _Base REPLACE call relation with relationship (relationship() is historically known as relation() prior to version 0.6.)<commit_after># -*- coding: utf-8 -*-
# Copyright (c) 2011 The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details.
"""
    pycroft.model.base
    ~~~~~~~~~~~~~~

    This module contains base stuff for all models.

    :copyright: (c) 2011 by AG DSN.
"""
from sqlalchemy import ForeignKey, MetaData
from sqlalchemy import Table, Column
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base, declared_attr
from sqlalchemy.orm import relationship, backref
from sqlalchemy.types import BigInteger, Boolean, DateTime, Integer
from sqlalchemy.types import Text, String

_session = None

class _ModelMeta(DeclarativeMeta):
    """Metaclass for all mapped Database objects.
    """

    @property
    def q(cls):
        """This is a shortcut for easy querying of whole objects.

        With this metaclass shortcut you can query a Model with
        Model.q.filter(...) without using the verbose session stuff
        """
        global _session
        if _session is None:
            import pycroft.model.session
            _session = pycroft.model.session.session
        return _session.query(cls)

class _Base(object):
    """Baseclass for all database models.

    """
    id = Column(Integer, primary_key=True)

    @declared_attr
    def __tablename__(cls):
        """Autogenerate the tablename for the mapped objects.

        """
        return cls.__name__.lower()

ModelBase = declarative_base(cls=_Base, metaclass=_ModelMeta)

class Dormitory(ModelBase):
    number = Column(String(3), unique=True)
    street = Column(String(20))
    short_name = Column(String(5), unique=True)

    # one to one from Dormitory to VLan
    v_lan = relationship("VLan", uselist=False, backref="dormitory")

class Host(ModelBase):
    hostname = Column(String(255))

    # one to many from User to Host
    user_id = Column(Integer, ForeignKey("user.id"))

class LogEntry(ModelBase):
    # variably sized string
    message = Column(Text)
    # created
    timestamp = Column(DateTime)

    # many to one from User to LogEntry
    author_id = Column(Integer, ForeignKey("user.id"))

class Room(ModelBase):
    number = Column(String(36))
    level = Column(Integer)
    inhabitable = Column(Boolean)
    dormitory_id = Column(Integer, ForeignKey("dormitory.id"))

    dormitory = relationship("Dormitory", backref=backref("rooms",
                                                      order_by=number))

class Subnet(ModelBase):
    address = Column(String(48))
    #address = Column(postgresql.INET)

   # one to one from Subnet to VLan
    v_lan = relationship("VLan", uselist=False, backref="subnet")

class TrafficVolume(ModelBase):
    # 1 (true) for in, 0 (false) for out
    direction = Column(Boolean)
    # how many bytes
    size = Column(BigInteger)
    # when this was logged
    timestamp = Column(DateTime)

    # one to many from User to TrafficVolume
    user_id = Column(Integer, ForeignKey("user.id"))

class User(ModelBase):
    login = Column(String(40))
    name = Column(String(255))
    registration_date = Column(DateTime)
    room_id = Column(Integer, ForeignKey("room.id"))

    # one to many from User to Host
    hosts = relationship("Host")
    # one to many from User to LogEntry
    log_entries = relationship("LogEntry")
    #
    room = relationship("Room", backref=backref("users", order_by=id))
    # one to many from User to TrafficVolume
    traffic_volumes = relationship("TrafficVolume")

class VLan(ModelBase):
    name = Column(String(127))
    tag = Column(Integer)

    # one to one from Dormitory to VLan
    dormitory_id = Column(Integer, ForeignKey("dormitory.id"))
    # one to one from Subnet to VLan
    subnet_id = Column(Integer, ForeignKey("subnet.id"))
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# Copyright (c) 2015 The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details.
from sqlalchemy import Column, ForeignKey, event
from sqlalchemy.orm import backref, relationship, validates
from sqlalchemy.types import Integer, String
from pycroft.helpers.i18n import gettext
from pycroft.helpers.net import mac_regex

from pycroft.model.base import ModelBase
from pycroft.model.types import (
    IPAddress, MACAddress, InvalidMACAddressException)

class Host(ModelBase):
    discriminator = Column('type', String(50), nullable=False)
    __mapper_args__ = {'polymorphic_on': discriminator}

    # many to one from Host to User
    owner_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"),
                     nullable=True)

    # many to one from Host to Room
    room = relationship("Room", backref=backref("hosts"))
    room_id = Column(Integer, ForeignKey("room.id", ondelete="SET NULL"),
                     nullable=True)

class UserHost(Host):
    id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                primary_key=True)
    __mapper_args__ = {'polymorphic_identity': 'user_host'}

    desired_name = Column(String(63))
    owner = relationship("User", backref=backref(
        "user_hosts", cascade="all, delete-orphan"))

class ServerHost(Host):
    id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                primary_key=True)
    __mapper_args__ = {'polymorphic_identity': 'server_host'}

    name = Column(String(255))

    owner = relationship("User", backref=backref(
        "server_hosts", cascade="all, delete-orphan"))

class Switch(Host):
    __mapper_args__ = {'polymorphic_identity': 'switch'}
    id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                primary_key=True)

    name = Column(String(127), nullable=False)

    management_ip = Column(String(127), nullable=False)

    owner = relationship("User", backref=backref(
        "switches", cascade="all, delete-orphan"))

def _check_user_host_in_user_room(mapper, connection, userhost):
    if userhost.room is not userhost.owner.room:
        raise Exception("UserHost can only be in user's room")

event.listen(UserHost, "before_insert", _check_user_host_in_user_room)
event.listen(UserHost, "before_update", _check_user_host_in_user_room)

class MulticastFlagException(InvalidMACAddressException):
    pass

class TypeMismatch(Exception):
    pass

class Interface(ModelBase):
    """A logical network interface (hence the single MAC address), which means
    many net interfaces can be connected to the same switch port"""

    #foreign key discriminator
    discriminator = Column('type', String(50))
    __mapper_args__ = {'polymorphic_on': discriminator}

    mac = Column(MACAddress, nullable=False)

    host_id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                     nullable=False)

    @validates('mac')
    def validate_mac(self, _, mac_address):
        match = mac_regex.match(mac_address)
        if not match:
            raise InvalidMACAddressException("MAC address '"+mac_address+"' is not valid")
        if int(mac_address[0:2], base=16) & 1:
            raise MulticastFlagException()
        return mac_address

class UserInterface(Interface):
    id = Column(Integer, ForeignKey(Interface.id, ondelete="CASCADE"),
                primary_key=True)

    __mapper_args__ = {'polymorphic_identity': "user_interface"}

    host = relationship(UserHost,
                        backref=backref("user_interfaces",
                                        cascade="all, delete-orphan"))

class ServerInterface(Interface):
    id = Column(Integer, ForeignKey(Interface.id, ondelete="CASCADE"),
                primary_key=True)

    __mapper_args__ = {'polymorphic_identity': "server_interface"}

    host = relationship(ServerHost,
                        backref=backref("server_interfaces",
                                        cascade="all, delete-orphan"))

    #TODO switch_port_id nicht Nullable machen: CLash mit Importscript
    switch_port_id = Column(Integer, ForeignKey('switch_port.id'),
                            nullable=True)
    switch_port = relationship("SwitchPort")

class SwitchInterface(Interface):
    id = Column(Integer, ForeignKey(Interface.id, ondelete="CASCADE"),
                primary_key=True)

    __mapper_args__ = {'polymorphic_identity': "switch_interface"}

    host = relationship("Switch",
                        backref=backref("switch_interfaces",
                                        cascade="all, delete-orphan"))

class IP(ModelBase):
    address = Column(IPAddress, nullable=False, unique=True)
    interface_id = Column(Integer,
                           ForeignKey(Interface.id, ondelete="CASCADE"),
                           nullable=False)
    interface = relationship(Interface,
                              backref=backref("ips",
                                              cascade="all, delete-orphan"))

    host = relationship(Host, secondary=Interface.__table__,
                        backref=backref("ips", viewonly=True), viewonly=True)

    subnet_id = Column(Integer, ForeignKey("subnet.id", ondelete="CASCADE"),
                       nullable=False)
    subnet = relationship("Subnet",
                          backref=backref("ips", cascade="all, delete-orphan"),
                          lazy='joined')

    def _check_subnet_valid(self, address, subnet):
        if address is None or subnet is None:
            return
        if address not in subnet:
            message = gettext('IP address {} is not contained in its subnet {}'
                              .format(address, subnet))
            raise ValueError(message)

    @validates('subnet')
    def validate_subnet(self, _, value):
        if value is None:
            return value
        self._check_subnet_valid(self.address, value.address)
        return value

    @validates("address")
    def validate_address(self, _, value):
        if value is None or self.subnet is None:
            return value
        self._check_subnet_valid(value, self.subnet.address)
        return value

def _check_correct_ip_subnet(mapper, connection, target):
    if target.subnet is not None:
        target._check_subnet_valid(target.address, target.subnet.address)

event.listen(IP, "before_insert", _check_correct_ip_subnet)
event.listen(IP, "before_update", _check_correct_ip_subnet)
<commit_msg>Fix indentation errors from Interface rename<commit_after># -*- coding: utf-8 -*-
# Copyright (c) 2015 The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details.
from sqlalchemy import Column, ForeignKey, event
from sqlalchemy.orm import backref, relationship, validates
from sqlalchemy.types import Integer, String
from pycroft.helpers.i18n import gettext
from pycroft.helpers.net import mac_regex

from pycroft.model.base import ModelBase
from pycroft.model.types import (
    IPAddress, MACAddress, InvalidMACAddressException)

class Host(ModelBase):
    discriminator = Column('type', String(50), nullable=False)
    __mapper_args__ = {'polymorphic_on': discriminator}

    # many to one from Host to User
    owner_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"),
                     nullable=True)

    # many to one from Host to Room
    room = relationship("Room", backref=backref("hosts"))
    room_id = Column(Integer, ForeignKey("room.id", ondelete="SET NULL"),
                     nullable=True)

class UserHost(Host):
    id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                primary_key=True)
    __mapper_args__ = {'polymorphic_identity': 'user_host'}

    desired_name = Column(String(63))
    owner = relationship("User", backref=backref(
        "user_hosts", cascade="all, delete-orphan"))

class ServerHost(Host):
    id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                primary_key=True)
    __mapper_args__ = {'polymorphic_identity': 'server_host'}

    name = Column(String(255))

    owner = relationship("User", backref=backref(
        "server_hosts", cascade="all, delete-orphan"))

class Switch(Host):
    __mapper_args__ = {'polymorphic_identity': 'switch'}
    id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                primary_key=True)

    name = Column(String(127), nullable=False)

    management_ip = Column(String(127), nullable=False)

    owner = relationship("User", backref=backref(
        "switches", cascade="all, delete-orphan"))

def _check_user_host_in_user_room(mapper, connection, userhost):
    if userhost.room is not userhost.owner.room:
        raise Exception("UserHost can only be in user's room")

event.listen(UserHost, "before_insert", _check_user_host_in_user_room)
event.listen(UserHost, "before_update", _check_user_host_in_user_room)

class MulticastFlagException(InvalidMACAddressException):
    pass

class TypeMismatch(Exception):
    pass

class Interface(ModelBase):
    """A logical network interface (hence the single MAC address), which means
    many net interfaces can be connected to the same switch port"""

    #foreign key discriminator
    discriminator = Column('type', String(50))
    __mapper_args__ = {'polymorphic_on': discriminator}

    mac = Column(MACAddress, nullable=False)

    host_id = Column(Integer, ForeignKey(Host.id, ondelete="CASCADE"),
                     nullable=False)

    @validates('mac')
    def validate_mac(self, _, mac_address):
        match = mac_regex.match(mac_address)
        if not match:
            raise InvalidMACAddressException("MAC address '"+mac_address+"' is not valid")
        if int(mac_address[0:2], base=16) & 1:
            raise MulticastFlagException()
        return mac_address

class UserInterface(Interface):
    id = Column(Integer, ForeignKey(Interface.id, ondelete="CASCADE"),
                primary_key=True)

    __mapper_args__ = {'polymorphic_identity': "user_interface"}

    host = relationship(UserHost,
                        backref=backref("user_interfaces",
                                        cascade="all, delete-orphan"))

class ServerInterface(Interface):
    id = Column(Integer, ForeignKey(Interface.id, ondelete="CASCADE"),
                primary_key=True)

    __mapper_args__ = {'polymorphic_identity': "server_interface"}

    host = relationship(ServerHost,
                        backref=backref("server_interfaces",
                                        cascade="all, delete-orphan"))

    #TODO switch_port_id nicht Nullable machen: CLash mit Importscript
    switch_port_id = Column(Integer, ForeignKey('switch_port.id'),
                            nullable=True)
    switch_port = relationship("SwitchPort")

class SwitchInterface(Interface):
    id = Column(Integer, ForeignKey(Interface.id, ondelete="CASCADE"),
                primary_key=True)

    __mapper_args__ = {'polymorphic_identity': "switch_interface"}

    host = relationship("Switch",
                        backref=backref("switch_interfaces",
                                        cascade="all, delete-orphan"))

class IP(ModelBase):
    address = Column(IPAddress, nullable=False, unique=True)
    interface_id = Column(Integer,
                          ForeignKey(Interface.id, ondelete="CASCADE"),
                          nullable=False)
    interface = relationship(Interface,
                             backref=backref("ips",
                                             cascade="all, delete-orphan"))

    host = relationship(Host, secondary=Interface.__table__,
                        backref=backref("ips", viewonly=True), viewonly=True)

    subnet_id = Column(Integer, ForeignKey("subnet.id", ondelete="CASCADE"),
                       nullable=False)
    subnet = relationship("Subnet",
                          backref=backref("ips", cascade="all, delete-orphan"),
                          lazy='joined')

    def _check_subnet_valid(self, address, subnet):
        if address is None or subnet is None:
            return
        if address not in subnet:
            message = gettext('IP address {} is not contained in its subnet {}'
                              .format(address, subnet))
            raise ValueError(message)

    @validates('subnet')
    def validate_subnet(self, _, value):
        if value is None:
            return value
        self._check_subnet_valid(self.address, value.address)
        return value

    @validates("address")
    def validate_address(self, _, value):
        if value is None or self.subnet is None:
            return value
        self._check_subnet_valid(value, self.subnet.address)
        return value

def _check_correct_ip_subnet(mapper, connection, target):
    if target.subnet is not None:
        target._check_subnet_valid(target.address, target.subnet.address)

event.listen(IP, "before_insert", _check_correct_ip_subnet)
event.listen(IP, "before_update", _check_correct_ip_subnet)
<|endoftext|>