view globalneighbors/web.py @ 12:6d89ea94930b

add passthrough fileserver for autocomplete
author Jeff Hammel <k0scist@gmail.com>
date Sun, 25 Jun 2017 13:18:13 -0700
parents d1b99c695511
children 94af113e498a
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 .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(i[name])
        return sorted(retval)[:limit]
    else:
        return sorted([i[name] for i in cities])[: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 GlobalHandler(Handler):
    """WSGI HTTP Handler"""

    content_type = 'text/html'

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


        # parse data
        self.datafile = datafile
        self.cities = read_city_list(self.datafile,
                                     fields=fields)
        self.locations = locations(self.cities)

        # 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
            return Response(content_type=self.content_type,
                            body=self.index.render(n_cities=len(self.cities)))
        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
                return Response(content_type=self.content_type,
                                body=self.citypage.render(**city))
            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]")
    options = parser.parse_args(args)

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

    # 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()