<commit_before>import logging

import gevent
from gevent import socket
from gevent.server import DatagramServer


log = logging.getLogger(__name__)


class UPnP(object):
    """
    Makes M-SEARCH requests, filters out non-WeMo responses, and calls a
    user-provided handler with the results.
    """

    def __init__(self, handler, mcast_ip="172.16.58.3", port=1900):
        self.ip = socket.gethostbyname(socket.gethostname())
        self.mcast_ip = mcast_ip
        self.port = port
        self.clients = {}
        self._handler = handler

    def _response_received(self, message, address):
        log.debug("Received a response from {0}:{1}".format(*address))
        if address[0] not in self.clients:
            lines = message.splitlines()
            lines.pop(0) # HTTP status
            headers = {}
            for line in lines:
                try:
                    header, value = line.split(":", 1)
                    headers[header.lower()] = value.strip()
                except ValueError:
                    continue
            if (headers.get('x-user-agent', None) == 'redsonic'):
                log.info("Found WeMo at {0}:{1}".format(*address))
                self.clients[address[0]] = headers
                gevent.spawn(self._handler, address, headers)

    @property
    def server(self):
        """
        UDP server to listen for responses.
        """
        server = getattr(self, "_server", None)
        if server is None:
            server = DatagramServer("{ip}:{port}".format(**self.__dict__),
                                    self._response_received)
            self._server = server
        return server

    def broadcast(self):
        """
        Send a multicast M-SEARCH request asking for devices to report in.
        """
        log.debug("Broadcasting M-SEARCH to %s:%s", self.mcast_ip, self.port)
        request = '\r\n'.join(("M-SEARCH * HTTP/1.1",
                               "HOST:{ip}:{port}",
                               "ST:upnp:rootdevice",
                               "MX:2",
                               'MAN:"ssdp:discover"',
                               "", "")).format(
            ip=self.mcast_ip, port=self.port)
        self.server.sendto(request, (self.mcast_ip, self.port))


def test():
    logging.basicConfig(level=logging.DEBUG)

    def handler(address, headers):
        print "I GOT ONE"
        print address, headers

    upnp = UPnP(handler)
    upnp.server.set_spawn(1)
    upnp.server.start()
    log.debug("Started server, listening for responses")
    with gevent.Timeout(2, KeyboardInterrupt) as timeout:
        while True:
            try:
                upnp.broadcast()
                gevent.sleep(2)
            except KeyboardInterrupt:
                break
    try:
        gevent.sleep(1000)
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    test()<commit_msg>Add ability to specify interface and port to bind the response server to, in pursuit of #4<commit_after>import logging

import gevent
from gevent import socket
from gevent.server import DatagramServer


log = logging.getLogger(__name__)


class UPnP(object):
    """
    Makes M-SEARCH requests, filters out non-WeMo responses, and calls a
    user-provided handler with the results.
    """

    def __init__(self, handler, mcast_ip='172.16.58.3', mcast_port=1900, bind=None):
        if bind is None:
            host = socket.gethostbyname(socket.gethostname())
            port = 54321
            bind = '{0}:{1}'.format(host, port)
        self.bind = bind
        self.mcast_ip = mcast_ip
        self.mcast_port = mcast_port
        self.clients = {}
        self._handler = handler

    def _response_received(self, message, address):
        log.debug("Received a response from {0}:{1}".format(*address))
        if address[0] not in self.clients:
            lines = message.splitlines()
            lines.pop(0) # HTTP status
            headers = {}
            for line in lines:
                try:
                    header, value = line.split(":", 1)
                    headers[header.lower()] = value.strip()
                except ValueError:
                    continue
            if headers.get('x-user-agent', None) == 'redsonic':
                log.info("Found WeMo at {0}:{1}".format(*address))
                self.clients[address[0]] = headers
                gevent.spawn(self._handler, address, headers)

    @property
    def server(self):
        """
        UDP server to listen for responses.
        """
        server = getattr(self, "_server", None)
        if server is None:
            server = DatagramServer(self.bind, self._response_received)
            self._server = server
        return server

    def broadcast(self):
        """
        Send a multicast M-SEARCH request asking for devices to report in.
        """
        log.debug("Broadcasting M-SEARCH to %s:%s", self.mcast_ip, self.mcast_port)
        request = '\r\n'.join(("M-SEARCH * HTTP/1.1",
                               "HOST:{bind}",
                               "ST:upnp:rootdevice",
                               "MX:2",
                               'MAN:"ssdp:discover"',
                               "", "")).format(bind=self.bind)
        self.server.sendto(request, (self.mcast_ip, self.mcast_port))


def test():
    logging.basicConfig(level=logging.DEBUG)

    def handler(address, headers):
        print "I GOT ONE"
        print address, headers

    upnp = UPnP(handler, bind='10.42.1.120:54321')
    upnp.server.set_spawn(1)
    upnp.server.start()
    log.debug("Started server, listening for responses")
    with gevent.Timeout(2, KeyboardInterrupt):
        while True:
            try:
                upnp.broadcast()
                gevent.sleep(2)
            except KeyboardInterrupt:
                break


if __name__ == "__main__":
    test()
{'fraction_non_alphanumeric': 0.11392832995267073, 'fraction_numerical': 0.013015551048005409, 'mean_word_length': 1.7714285714285714, 'pattern_counts': {'":': 4, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 3, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3575338', 'n_tokens_mistral': 1731, 'n_tokens_neox': 1660, 'n_words': 403}