Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #73 from ryanbreen/external-config
Add ExternalConfigServer
- Loading branch information
Showing
1 changed file
with
138 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
""" ExternalConfigServer is a replacement for WSGITileServer that uses external | ||
configuration fetched via HTTP to service all config requests. | ||
Example usage, with gunicorn (http://gunicorn.org): | ||
gunicorn --bind localhost:8888 "TileStache.Goodies.ExternalConfigServer:WSGIServer(url)" | ||
""" | ||
|
||
from urllib import urlopen | ||
from sets import Set | ||
import logging | ||
|
||
try: | ||
from json import load as json_load | ||
except ImportError: | ||
from simplejson import load as json_load | ||
|
||
import TileStache | ||
|
||
class DynamicLayers: | ||
|
||
def __init__(self, config, url_root, cache_responses, dirpath): | ||
self.config = config | ||
self.url_root = url_root | ||
self.dirpath = dirpath | ||
self.cache_responses = cache_responses; | ||
self.seen_layers = {} | ||
self.lookup_failures = Set() | ||
|
||
def keys(self): | ||
return self.seen_layers.keys() | ||
|
||
def items(self): | ||
return self.seen_layers.items() | ||
|
||
def parse_layer(self, layer_json): | ||
layer_dict = json_load(layer_json) | ||
return TileStache.Config._parseConfigfileLayer(layer_dict, self.config, self.dirpath) | ||
|
||
def __contains__(self, key): | ||
# If caching is enabled and we've seen a request for this layer before, return True unless | ||
# the prior lookup failed to find this layer. | ||
if self.cache_responses: | ||
if key in self.seen_layers: | ||
return True | ||
elif key in self.lookup_failures: | ||
return False | ||
|
||
res = urlopen(self.url_root + "/layer/" + key) | ||
|
||
if self.cache_responses: | ||
if res.getcode() != 200: | ||
# Cache a failed lookup | ||
self.lookup_failures.add(key) | ||
else : | ||
# If lookup succeeded and we are caching, parse the layer now so that a subsequent | ||
# call to __getitem__ doesn't require a call to the config server. If we aren't | ||
# caching, we skip this step to avoid an unnecessary json parse. | ||
try: | ||
self.seen_layers[key] = self.parse_layer(res) | ||
except ValueError: | ||
# The JSON received by the config server was invalid. Treat this layer as a | ||
# failure. We don't want to raise ValueError from here because other parts | ||
# of TileStache are just expecting a boolean response from __contains__ | ||
logging.error("Invalid JSON response seen for %s", key) | ||
self.lookup_failures.add(key) | ||
return False | ||
|
||
if res.getcode() != 200: | ||
logging.info("Config response code %s for %s", res.getcode(), key) | ||
return res.getcode() == 200 | ||
|
||
def __getitem__(self, key): | ||
if self.cache_responses: | ||
if key in self.seen_layers: | ||
return self.seen_layers[key] | ||
elif key in self.lookup_failures: | ||
# If we are caching, raise KnownUnknown if we have previously failed to find this layer | ||
raise TileStache.KnownUnknown("Layer %s previously not found", key) | ||
|
||
logging.debug("Requesting layer %s", self.url_root + "/layer/" + key) | ||
res = urlopen(self.url_root + "/layer/" + key) | ||
if (res.getcode() != 200) : | ||
logging.info("Config response code %s for %s", res.getcode(), key) | ||
if (self.cache_responses) : | ||
self.lookup_failures.add(key) | ||
raise TileStache.KnownUnknown("Layer %s not found", key) | ||
|
||
try : | ||
layer = self.parse_layer(res) | ||
self.seen_layers[key] = layer | ||
return layer | ||
except ValueError: | ||
logging.error("Invalid JSON response seen for %s", key) | ||
if (self.cache_responses) : | ||
# If caching responses, cache this failure | ||
self.lookup_failures.add(key) | ||
# KnownUnknown seems like the appropriate thing to raise here since this is akin | ||
# to a missing configuration. | ||
raise TileStache.KnownUnknown("Failed to parse JSON configuration for %s", key) | ||
|
||
class ExternalConfiguration: | ||
|
||
def __init__(self, url_root, cache_dict, cache_responses, dirpath): | ||
self.cache = TileStache.Config._parseConfigfileCache(cache_dict, dirpath) | ||
self.dirpath = dirpath | ||
self.layers = DynamicLayers(self, url_root, cache_responses, dirpath) | ||
|
||
class WSGIServer (TileStache.WSGITileServer): | ||
|
||
""" | ||
Wrap WSGI application, passing it a custom configuration. | ||
The WSGI application is an instance of TileStache:WSGITileServer. | ||
This method is initiated with a url_root that contains the scheme, host, port | ||
and path that must prefix the API calls on our local server. Any valid http | ||
or https urls should work. | ||
The cache_responses parameter tells TileStache to cache all responses from | ||
the configuration server. | ||
""" | ||
|
||
def __init__(self, url_root, cache_responses=True, debug_level="DEBUG"): | ||
logging.basicConfig(level=debug_level) | ||
|
||
# Call API server at url to grab cache_dict | ||
cache_dict = json_load(urlopen(url_root + "/cache")) | ||
|
||
dirpath = '/tmp/stache' | ||
|
||
config = ExternalConfiguration(url_root, cache_dict, cache_responses, dirpath) | ||
|
||
TileStache.WSGITileServer.__init__(self, config, False) | ||
|
||
def __call__(self, environ, start_response): | ||
response = TileStache.WSGITileServer.__call__(self, environ, start_response) | ||
return response |