view globalneighbors/web.py @ 25:991bce6b6881 default tip

[knn] placeholder for planning session
author Jeff Hammel <k0scist@gmail.com>
date Sun, 17 Sep 2017 14:35:50 -0700
parents 6891c5523b69
children
line wrap: on
line source

#!/usr/bin/env python

"""
web handler for GlobalNeighbors
"""

# imports
import argparse
import json
import os
import sys
import time
from paste import httpserver
from paste.urlparser import StaticURLParser
from webob import Request, Response, exc
from wsgiref import simple_server
from .locations import locations
from .locations import city_dict
from .neighbors import read_neighbors_file
from .read import read_cities
from .read import read_city_list
from .schema import fields
from .schema import name
from .template import template_dir
from .template import TemplateLoader

here = os.path.dirname(os.path.abspath(__file__))
static_dir = os.path.join(here, 'static')


class PassthroughFileserver(object):

    """serve files if they exist"""


    def __init__(self, app, directory=static_dir):
        self.app = app
        self.directory = directory
        self.fileserver = StaticURLParser(self.directory)

    def __call__(self, environ, start_response):

        path = environ['PATH_INFO'].strip('/')

        if path and os.path.exists(os.path.join(self.directory, path)) and '..' not in path:

            return self.fileserver(environ, start_response)
        return self.app(environ, start_response)


def autocomplete(cities, startswith=None, limit=None):
    """autocomplete function for city names"""
    ### TODO:
    # - sort once, ahead of time
    # - return most populous cities

    if startswith:
        retval = []
        for i in cities:
            if i[name].startswith(startswith):
                retval.append({"name": i[name],
                               "country code": i["country code"],
                               "population": i['population'],
                               "geonameid": i['geonameid']})
    else:
        retval = [{"name": i[name],
                   "country code": i["country code"],
                   "population": i['population'],
                   "geonameid": i['geonameid']}
                   for i in cities]
    return sorted(retval,
                  key=lambda x: (x['name'],
                                 -x['population'])
                  )[:limit]


class Handler(object):
    """base class for HTTP handler"""

    def __call__(self, environ, start_response):
        request = Request(environ)
        method = getattr(self, request.method, None)
        response = None
        if method is not None:
            response = method(request)
        if response is None:
            response = exc.HTTPNotFound()
        return response(environ, start_response)


class CitiesHandler(Handler):
    """cities ReST API"""

    content_type = 'application/json'

    def __init__(self, locations, cities):
        self.locations = locations
        self._cities = cities


    def cities(self, startswith=None):
        """return list of cities"""
        return autocomplete(self._cities.values(),
                            startswith=startswith)

    def GET(self, request):
        return Response(content_type=self.content_type,
                        body=json.dumps(self.cities(
                            startswith=request.GET.get('term'))))

class NeighborsHandler(Handler):

    content_type = 'application/json'

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

    def GET(self, request):
        geoid = request.GET.get('geoid')
        neighbors = self.neighbors.get(geoid, [])
        return Response(content_type=self.content_type,
                        body=json.dumps(neighbors))


class GlobalHandler(Handler):
    """WSGI HTTP Handler"""

    content_type = 'text/html'

    def __init__(self,
                 datafile,
                 neighbors_file=None,
                 template_dir=template_dir):


        # parse data
        self.datafile = datafile
        self.cities = read_city_list(self.datafile,
                                     fields=fields)
        self.locations = locations(self.cities)
        if neighbors_file:
            self.neighbors = read_neighbors_file(neighbors_file)
        else:
            self.neighbors = None

        # get country codes
        self.country_codes = sorted(set([city['country code']
                                         for city in self.cities
                                         if city['country code']]))

        # convert cities to a dict for lookup
        self.cities = {city['geonameid'] : city
                       for city in self.cities}


        # declare handlers
        self.handlers = {'/cities':
                         CitiesHandler(self.locations,
                                       self.cities)}

        # template loader
        self.loader = TemplateLoader(template_dir)
        self.index = self.loader.load('index.html')
        self.citypage = self.loader.load('city.html')


    def GET(self, request):
        if request.path_info in ('', '/'):
            # Landing page
            body = self.index.render(n_cities=len(self.cities),
                                     country_codes=self.country_codes)
            return Response(content_type=self.content_type,
                            body=body)

        elif request.path_info in self.handlers:
            return request.get_response(self.handlers[request.path_info])
        else:
            try:
                geoid = int(request.path.strip('/'))
                city = self.cities.get(geoid)
                if not city:
                    return
                variables = dict(city=city, neighbors=None)
                if self.neighbors:
                    n_neighbors = request.GET.get('neighbors', 10)
                    try:
                        n_neighbors = int(n_neighbors)
                    except ValueError as e:
                        n_neighbors = 10
                    neighbors = self.neighbors.get(geoid, [])[:n_neighbors]
                    neighbors = [{'name': self.cities[geoid]['name'],
                                  'geoid': geoid,
                                  'distance': distance}
                                 for geoid, distance in neighbors]
                    variables['neighbors'] = neighbors

                return Response(content_type=self.content_type,
                                body=self.citypage.render(variables))
            except ValueError:
                pass



def main(args=sys.argv[1:]):
    """CLI"""

    # parse command line
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('cities',
                        help="cities1000 data file")
    parser.add_argument('-p', '--port', dest='port',
                        type=int, default=8080,
                        help="port to serve on")
    parser.add_argument('--hostname', dest='hostname',
                        default='localhost',
                        help="host name [DEFAULT: %(default)s]")
    parser.add_argument('--neighbors', dest='neighbors_file',
                        help="file for nearest neighbors")
    options = parser.parse_args(args)

    # instantiate WSGI handler
    app = GlobalHandler(datafile=options.cities,
                        neighbors_file=options.neighbors_file)

    # wrap it in a static file server
    app = PassthroughFileserver(app)

    print ("Serving on http://{hostname}:{port}/".format(**options.__dict__))
    try:
        httpserver.serve(app, host='0.0.0.0', port=options.port)
    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    main()