Combining HTTP and JavaScript APIs with python on google appengine

Christian Harms's picture

In this part I will introduce the python implementation of the ip to geolocation script. It's more object oriented and hopefully better to read. In the first part of this article I willdescribe the solution to read http resources and parse the content. The second part is the same like the php version. As conclusion I will compare the results of all five APIs with the data from the cache.

python: asynchonous http requests on google appengine

First I want to build the same multi-url-fetch-function (like in php) but I am using the google appengine. There are no threads allowed and the urllib/httplib modules are masked with the urlfetch module from google. I chose the normal and easy urllib.open call because the google backend works fast. After this was done I found in the updated URLFetch-documentation (since June 18, 2009 or appengine version 1.2.3) the section that said: "To do asynchronous calls you have to use the special modul from the urlfetch modul". Have fun with the improved example.

  1. from google.appengine.api import urlfetch
  2.  
  3. class InfoItem(dict):
  4.     '''dict with start reading while __init__ the ipinfodb '''
  5.     def __init__(self, url):
  6.         self.rpc = urlfetch.create_rpc()
  7.         urlfetch.make_fetch_call(self.rpc, url)
  8.  
  9.     def ready(self):
  10.         '''Check if the async call is ready.
  11.        @return True - if got data after parsing
  12.        '''
  13.         try:
  14.             result = self.rpc.get_result()
  15.         except urlfetch.Error, ex:
  16.             logging.error("Error while fetch: %s" % ex)
  17.             return False
  18.  
  19.         if result.status_code != 200:
  20.             return False
  21.         return self.parse(result.content)
  22.     #ready
  23. #InfoItem

For easy access the result class is based on a python dict. To check if the api data is filled in the dict call the ready() function. You can build the instances of InfoItem, do something other and then ask the instances with the ready-function, if the data has arrived (if not it will wait). Accessing the values is easy because it's a dict.

Parsing XML data with python

xml should be parsed with the elementtree modul. Its very fast and simple to use. Using the InfoItem class there are two jobs: building the url to the api by simple adding the ip string and parsing the content.

  1. import xml.etree.ElementTree as etree
  2. from xml.parsers.expat import ExpatError
  3.  
  4. class IpInfoDbItem(InfoItem):
  5.     '''Simple parsing the content of the IpInfoDP-API'''
  6.     def __init__(self, ip):
  7.         '''Init with the IpInfoDb-url'''
  8.         super(IpInfoDbItem, self).__init__("http://ipinfodb.com/ip_query.php?ip="+ip)
  9.  
  10.     def parse(self, content):
  11.         '''Parse the IpInfoDb-XML and save the keys in the inner dict.
  12.        @return True - if parsing was successfull.
  13.        '''
  14.         try:
  15.             #etree needs a file-like-object instead a string!
  16.             t = etree.ElementTree().parse(StringIO.StringIO(content))
  17.             self.update({'name': 'ipinfodb',
  18.                          'country': t.find("CountryName").text or '',
  19.                          'city':    t.find("City").text or '',
  20.                          'lat':     float(t.find("Latitude").text),
  21.                          'long':    float(t.find("Longitude").text)})
  22.             return True
  23.         except (ExpatError, IOError), ex:
  24.             logging.warn("Nothing parsed: %s" % ex)
  25.         return False
  26.     #parse
  27. #IpInfoDbItem
  28.  
  29. #Test the code directly (if google modules are in the path)
  30. testing = IpInfoDbItem("127.0.0.1")
  31. if testing.ready():
  32.     print testing
  33. #  {'lat': 0.0, 'country': 'Reserved', 'name': 'ipinfodb', 'long': 0.0, 'city': None}

The example starts fetching the data from the IpInfoDb-API in the __init__ function, parses the xml und fills the values in the dict with self.update.

Parsing non-structured data with python

The same hint like in php - use regular expressing for matching the data!

  1. import re
  2.  
  3. class HostIpItem(InfoItem):
  4.     '''dict with reading while __init__ the hostip '''
  5.     def __init__(self, ip):
  6.         super(HostIpItem, self).__init__("http://api.hostip.info/get_html.php?position=true&ip="+ip)
  7.  
  8.     def parse(self, content):
  9.         '''Parse the HostIp-Text and save the keys in the inner dict.
  10.        @return True if parsing was successfull.
  11.        '''
  12.         match = re.search("Country:\s+(.*?)\(\w+\)\nCity:\s+(.*?)\nLatitude: (-*\d+\.\d+)\nLongitude: (-*\d+\.\d+)", content, re.S|re.I)
  13.         if match:
  14.             self.update( {'name': 'hostip',
  15.                           'country': match.group(1),
  16.                           'city': match.group(2),
  17.                           'long': float(match.group(4)),
  18.                           'lat': float(match.group(3))})
  19.             return True
  20.         return False
  21.     #parse
  22. #HostIpItem

Works like the xml example ...

Build a complete webapplication

To put this together you have to define a RequestHandler, who fetches the data and produces a javascript. In django style you need the following template, the values in {{ x }} will be replaced with a dict.

  1. var com = com||{};
  2. com.unitedCoders = com.unitedCoders||{};
  3. com.unitedCoders.geo = com.unitedCoders.geo||{};
  4. com.unitedCoders.geo.ll = {{ ll_json }} ;
  5.  
  6. {{ maxmind }}
  7. {{ wipmania }}
  8. {{ google }}
  9. document.write('<script type="text/javascript" src="http://pyUnitedCoders.appspot.com/geo_func.js"></script>');
  10.  
  11. com.unitedCoders.geo.staticMapUrl = function(x, y) {
  12.   var url = "http://maps.google.com/staticmap?key={{ google_key }}&size="+x+"x"+y+"&markers=";
  13.   var colors = ["blue","green","red","yellow","white", "black"];
  14.   for (var i=0; i<com.unitedCoders.geo.ll.length;i++) {
  15.       var s = com.unitedCoders.geo.ll[i];
  16.       url += s.lat+","+s.long+",mid"+colors[i]+(i+1)+"%7C";
  17.   };
  18.   url += this.getLat() + ","+this.getLong() + ",black";
  19.   return url;
  20. };

  1. from google.appengine.ext import webapp
  2. from google.appengine.ext.webapp.util import run_wsgi_app
  3. from google.appengine.ext.webapp import template
  4.  
  5. class GeoScript(webapp.RequestHandler):
  6.     def get(self):
  7.         '''Get the location infos for the calling ip (from api).'''
  8.  
  9.         self.response.headers['Content-Type'] = 'text/plain;charset=UTF-8'
  10.  
  11.         #result-dict and local location list
  12.         result = {}
  13.         ll = []
  14.  
  15.         #Start fetching API data
  16.         ipInfo = IpInfoDbItem(ip)
  17.         hostIp = HostIpItem(ip)
  18.  
  19.         #Add some more Javascrip APIs
  20.         scriptTemp = "document.write('<script type=\"text/javascript\" src=\"%s\"></script>');"
  21.         result['maxmind'] = scriptTemp % "http://j.maxmind.com/app/geoip.js"
  22.         result['wipmania'] = scriptTemp % "http://api.wipmania.com/wip.js"
  23.         if self.request.get("key"):
  24.             result['google_key'] = self.request.get("key")
  25.             result['google'] = scriptTemp % \
  26.                       ("http://www.google.com/jsapi?key=" + self.request.get("key"))
  27.  
  28.         #Get the fetched API Data
  29.         if ipInfo.ready():
  30.             ll.append(ipInfo)
  31.         if hostIp.ready():
  32.             ll.append(hostIp)
  33.  
  34.         result['ll_json'] = encoder.JSONEncoder().encode(result['ll'])
  35.  
  36.         #Put all together in the javascript template
  37.         path = os.path.join(os.path.dirname(__file__), 'geo.temp')
  38.         self.response.out.write(template.render(path, result))
  39.     #get
  40. #GeoScript
  41.  
  42. application = webapp.WSGIApplication([
  43.         ('/geo_data.js', GeoScript) ], debug=True)

For more information on how to start a python google appengine Webapplication start reading the fine google documentation!

conclusion

Don't mix too many languages - you will be confused! The parallel implementation of the server side script in php and python and using one version for the advanced functions in javascript will mix three script language! My first failures have been setting some semicolons in python or forgetting the block parentheses in javascript.

After deploying and watching a server side script with the dashboard of google's appengine you get all data: Logs and API-calls in detail, you can manage many different versions (default is one) or give deploy access to other google accounts:That's great!

What is the best service provider?

I have done some caching and checked all five API results. Here is the hit rate for the location of visitors of this blog and the distance to the center from the given locations (The center is the average of all long/lat values pairs with a given city value per ip).

 Service Provider   lat/long per ip   city per ip   distance to center 
maxmind 86%  85%  123 km
WIPmania 89%  0%  1059 km
google 48%  0%  197 km
IPinfoDB 98%  91%  168 km
hostip 35%  53%  404 km

HostIP and google do not offer location data for many visitors. IPInfoDB and MaxMind do not have the same positions (like suggested in the comments) for all IPs. At this time WIPMania mainly offers the center of the country. So the positions are not very accurate (in comparision to the calculated center).

How calculate the distance between lat/long values?

I found some nice functions in javascript (please don't add functions to the prototype to String and Integer!!!), in python the distance function looks like the following lines:

  1. def distance(lat1, long1, lat2, long2):
  2.     return 6378.7 * math.acos(math.sin(lat1/57.2958) * math.sin(lat2/57.2958) + math.cos(lat1/57.2958) * math.cos(lat2/57.2958) * math.cos(lon2/57.2958 - lon1/57.2958))
  3. #distance

Comments

Anonymous's picture

This article has been shared on favSHARE.net.

Combining HTTP and JavaScript APIs with php | united-coders.'s picture

[...] the python google appengine implementation [...]

Anonymous's picture

"c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))" can be simplified to "c = 2 * math.asin(math.sqrt(a))"

3 caching steps to boost your webservice by x10 | united-cod's picture

[...] the python google appengine implementation [...]