Ticket #16553: 16553.1.diff

File 16553.1.diff, 43.9 KB (added by jbronn, 13 years ago)

Refactor of GeoIP module.

  • new file django/contrib/gis/geoip/__init__.py

    diff -r a21c8b554d43 django/contrib/gis/geoip/__init__.py
    - +  
     1"""
     2 This module houses the GeoIP object, a ctypes wrapper for the MaxMind GeoIP(R)
     3 C API (http://www.maxmind.com/app/c).  This is an alternative to the GPL
     4 licensed Python GeoIP interface provided by MaxMind.
     5
     6 GeoIP(R) is a registered trademark of MaxMind, LLC of Boston, Massachusetts.
     7
     8 For IP-based geolocation, this module requires the GeoLite Country and City
     9 datasets, in binary format (CSV will not work!).  The datasets may be
     10 downloaded from MaxMind at http://www.maxmind.com/download/geoip/database/.
     11 Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory
     12 corresponding to settings.GEOIP_PATH.
     13"""
     14try:
     15    from django.contrib.gis.geoip.base import GeoIP, GeoIPException
     16    HAS_GEOIP = True
     17except:
     18    HAS_GEOIP = False
  • new file django/contrib/gis/geoip/base.py

    diff -r a21c8b554d43 django/contrib/gis/geoip/base.py
    - +  
     1import os
     2import re
     3from ctypes import c_char_p
     4
     5from django.core.validators import ipv4_re
     6from django.contrib.gis.geoip.libgeoip import GEOIP_SETTINGS
     7from django.contrib.gis.geoip.prototypes import (
     8    GeoIPRecord, GeoIPTag, GeoIP_open, GeoIP_delete, GeoIP_database_info,
     9    GeoIP_lib_version, GeoIP_record_by_addr, GeoIP_record_by_name,
     10    GeoIP_country_code_by_addr, GeoIP_country_code_by_name,
     11    GeoIP_country_name_by_addr, GeoIP_country_name_by_name)
     12
     13# Regular expressions for recognizing the GeoIP free database editions.
     14free_regex = re.compile(r'^GEO-\d{3}FREE')
     15lite_regex = re.compile(r'^GEO-\d{3}LITE')
     16
     17#### GeoIP classes ####
     18class GeoIPException(Exception): pass
     19
     20class GeoIP(object):
     21    # The flags for GeoIP memory caching.
     22    # GEOIP_STANDARD - read database from filesystem, uses least memory.
     23    #
     24    # GEOIP_MEMORY_CACHE - load database into memory, faster performance
     25    #        but uses more memory
     26    #
     27    # GEOIP_CHECK_CACHE - check for updated database.  If database has been
     28    #        updated, reload filehandle and/or memory cache.  This option
     29    #        is not thread safe.
     30    #
     31    # GEOIP_INDEX_CACHE - just cache the most frequently accessed index
     32    #        portion of the database, resulting in faster lookups than
     33    #        GEOIP_STANDARD, but less memory usage than GEOIP_MEMORY_CACHE -
     34    #        useful for larger databases such as GeoIP Organization and
     35    #        GeoIP City.  Note, for GeoIP Country, Region and Netspeed
     36    #        databases, GEOIP_INDEX_CACHE is equivalent to GEOIP_MEMORY_CACHE
     37    #
     38    # GEOIP_MMAP_CACHE - load database into mmap shared memory ( not available
     39    #       on Windows).
     40    GEOIP_STANDARD     = 0
     41    GEOIP_MEMORY_CACHE = 1
     42    GEOIP_CHECK_CACHE  = 2
     43    GEOIP_INDEX_CACHE  = 4
     44    GEOIP_MMAP_CACHE   = 8
     45    cache_options = dict((opt, None) for opt in (0, 1, 2, 4, 8))
     46
     47    # Paths to the city & country binary databases.
     48    _city_file = ''
     49    _country_file = ''
     50
     51    # Initially, pointers to GeoIP file references are NULL.
     52    _city = None
     53    _country = None
     54
     55    def __init__(self, path=None, cache=0, country=None, city=None):
     56        """
     57        Initializes the GeoIP object, no parameters are required to use default
     58        settings.  Keyword arguments may be passed in to customize the locations
     59        of the GeoIP data sets.
     60
     61        * path: Base directory to where GeoIP data is located or the full path
     62            to where the city or country data files (*.dat) are located.
     63            Assumes that both the city and country data sets are located in
     64            this directory; overrides the GEOIP_PATH settings attribute.
     65
     66        * cache: The cache settings when opening up the GeoIP datasets,
     67            and may be an integer in (0, 1, 2, 4, 8) corresponding to
     68            the GEOIP_STANDARD, GEOIP_MEMORY_CACHE, GEOIP_CHECK_CACHE,
     69            GEOIP_INDEX_CACHE, and GEOIP_MMAP_CACHE, `GeoIPOptions` C API
     70            settings,  respectively.  Defaults to 0, meaning that the data is read
     71            from the disk.
     72
     73        * country: The name of the GeoIP country data file.  Defaults to
     74            'GeoIP.dat'; overrides the GEOIP_COUNTRY settings attribute.
     75
     76        * city: The name of the GeoIP city data file.  Defaults to
     77            'GeoLiteCity.dat'; overrides the GEOIP_CITY settings attribute.
     78        """
     79        # Checking the given cache option.
     80        if cache in self.cache_options:
     81            self._cache = cache
     82        else:
     83            raise GeoIPException('Invalid GeoIP caching option: %s' % cache)
     84
     85        # Getting the GeoIP data path.
     86        if not path:
     87            path = GEOIP_SETTINGS.get('GEOIP_PATH', None)
     88            if not path: raise GeoIPException('GeoIP path must be provided via parameter or the GEOIP_PATH setting.')
     89        if not isinstance(path, basestring):
     90            raise TypeError('Invalid path type: %s' % type(path).__name__)
     91
     92        if os.path.isdir(path):
     93            # Constructing the GeoIP database filenames using the settings
     94            # dictionary.  If the database files for the GeoLite country
     95            # and/or city datasets exist, then try and open them.
     96            country_db = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.dat'))
     97            if os.path.isfile(country_db):
     98                self._country = GeoIP_open(country_db, cache)
     99                self._country_file = country_db
     100
     101            city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat'))
     102            if os.path.isfile(city_db):
     103                self._city = GeoIP_open(city_db, cache)
     104                self._city_file = city_db
     105        elif os.path.isfile(path):
     106            # Otherwise, some detective work will be needed to figure
     107            # out whether the given database path is for the GeoIP country
     108            # or city databases.
     109            ptr = GeoIP_open(path, cache)
     110            info = GeoIP_database_info(ptr)
     111            if lite_regex.match(info):
     112                # GeoLite City database detected.
     113                self._city = ptr
     114                self._city_file = path
     115            elif free_regex.match(info):
     116                # GeoIP Country database detected.
     117                self._country = ptr
     118                self._country_file = path
     119            else:
     120                raise GeoIPException('Unable to recognize database edition: %s' % info)
     121        else:
     122            raise GeoIPException('GeoIP path must be a valid file or directory.')
     123
     124    def __del__(self):
     125        # Cleaning any GeoIP file handles lying around.
     126        if self._country: GeoIP_delete(self._country)
     127        if self._city: GeoIP_delete(self._city)
     128
     129    def _check_query(self, query, country=False, city=False, city_or_country=False):
     130        "Helper routine for checking the query and database availability."
     131        # Making sure a string was passed in for the query.
     132        if not isinstance(query, basestring):
     133            raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__)
     134
     135        # Extra checks for the existence of country and city databases.
     136        if city_or_country and not (self._country or self._city):
     137            raise GeoIPException('Invalid GeoIP country and city data files.')
     138        elif country and not self._country:
     139            raise GeoIPException('Invalid GeoIP country data file: %s' % self._country_file)
     140        elif city and not self._city:
     141            raise GeoIPException('Invalid GeoIP city data file: %s' % self._city_file)
     142
     143    def city(self, query):
     144        """
     145        Returns a dictionary of city information for the given IP address or
     146        Fully Qualified Domain Name (FQDN).  Some information in the dictionary
     147        may be undefined (None).
     148        """
     149        self._check_query(query, city=True)
     150        if ipv4_re.match(query):
     151            # If an IP address was passed in
     152            return GeoIP_record_by_addr(self._city, c_char_p(query))
     153        else:
     154            # If a FQDN was passed in.
     155            return GeoIP_record_by_name(self._city, c_char_p(query))
     156
     157    def country_code(self, query):
     158        "Returns the country code for the given IP Address or FQDN."
     159        self._check_query(query, city_or_country=True)
     160        if self._country:
     161            if ipv4_re.match(query):
     162                return GeoIP_country_code_by_addr(self._country, query)
     163            else:
     164                return GeoIP_country_code_by_name(self._country, query)
     165        else:
     166            return self.city(query)['country_code']
     167
     168    def country_name(self, query):
     169        "Returns the country name for the given IP Address or FQDN."
     170        self._check_query(query, city_or_country=True)
     171        if self._country:
     172            if ipv4_re.match(query):
     173                return GeoIP_country_name_by_addr(self._country, query)
     174            else:
     175                return GeoIP_country_name_by_name(self._country, query)
     176        else:
     177            return self.city(query)['country_name']
     178
     179    def country(self, query):
     180        """
     181        Returns a dictonary with with the country code and name when given an
     182        IP address or a Fully Qualified Domain Name (FQDN).  For example, both
     183        '24.124.1.80' and 'djangoproject.com' are valid parameters.
     184        """
     185        # Returning the country code and name
     186        return {'country_code' : self.country_code(query),
     187                'country_name' : self.country_name(query),
     188                }
     189
     190    #### Coordinate retrieval routines ####
     191    def coords(self, query, ordering=('longitude', 'latitude')):
     192        cdict = self.city(query)
     193        if cdict is None: return None
     194        else: return tuple(cdict[o] for o in ordering)
     195
     196    def lon_lat(self, query):
     197        "Returns a tuple of the (longitude, latitude) for the given query."
     198        return self.coords(query)
     199
     200    def lat_lon(self, query):
     201        "Returns a tuple of the (latitude, longitude) for the given query."
     202        return self.coords(query, ('latitude', 'longitude'))
     203
     204    def geos(self, query):
     205        "Returns a GEOS Point object for the given query."
     206        ll = self.lon_lat(query)
     207        if ll:
     208            from django.contrib.gis.geos import Point
     209            return Point(ll, srid=4326)
     210        else:
     211            return None
     212
     213    #### GeoIP Database Information Routines ####
     214    @property
     215    def country_info(self):
     216        "Returns information about the GeoIP country database."
     217        if self._country is None:
     218            ci = 'No GeoIP Country data in "%s"' % self._country_file
     219        else:
     220            ci = GeoIP_database_info(self._country)
     221        return ci
     222
     223    @property
     224    def city_info(self):
     225        "Retuns information about the GeoIP city database."
     226        if self._city is None:
     227            ci = 'No GeoIP City data in "%s"' % self._city_file
     228        else:
     229            ci = GeoIP_database_info(self._city)
     230        return ci
     231
     232    @property
     233    def info(self):
     234        "Returns information about the GeoIP library and databases in use."
     235        info = ''
     236        if GeoIP_lib_version:
     237            info += 'GeoIP Library:\n\t%s\n' % GeoIP_lib_version()
     238        return info + 'Country:\n\t%s\nCity:\n\t%s' % (self.country_info, self.city_info)
     239
     240    #### Methods for compatibility w/the GeoIP-Python API. ####
     241    @classmethod
     242    def open(cls, full_path, cache):
     243        return GeoIP(full_path, cache)
     244
     245    def _rec_by_arg(self, arg):
     246        if self._city:
     247            return self.city(arg)
     248        else:
     249            return self.country(arg)
     250    region_by_addr = city
     251    region_by_name = city
     252    record_by_addr = _rec_by_arg
     253    record_by_name = _rec_by_arg
     254    country_code_by_addr = country_code
     255    country_code_by_name = country_code
     256    country_name_by_addr = country_name
     257    country_name_by_name = country_name
  • new file django/contrib/gis/geoip/libgeoip.py

    diff -r a21c8b554d43 django/contrib/gis/geoip/libgeoip.py
    - +  
     1import os
     2from ctypes import CDLL
     3from ctypes.util import find_library
     4from django.conf import settings
     5
     6# Creating the settings dictionary with any settings, if needed.
     7GEOIP_SETTINGS = dict((key, getattr(settings, key))
     8                      for key in ('GEOIP_PATH', 'GEOIP_LIBRARY_PATH', 'GEOIP_COUNTRY', 'GEOIP_CITY')
     9                      if hasattr(settings, key))
     10lib_path = GEOIP_SETTINGS.get('GEOIP_LIBRARY_PATH', None)
     11
     12# The shared library for the GeoIP C API.  May be downloaded
     13#  from http://www.maxmind.com/download/geoip/api/c/
     14if lib_path:
     15    lib_name = None
     16else:
     17    # TODO: Is this really the library name for Windows?
     18    lib_name = 'GeoIP'
     19
     20# Getting the path to the GeoIP library.
     21if lib_name: lib_path = find_library(lib_name)
     22if lib_path is None: raise GeoIPException('Could not find the GeoIP library (tried "%s"). '
     23                                          'Try setting GEOIP_LIBRARY_PATH in your settings.' % lib_name)
     24lgeoip = CDLL(lib_path)
     25
     26# Getting the C `free` for the platform.
     27if os.name == 'nt':
     28    libc = CDLL('msvcrt')
     29else:
     30    libc = CDLL(None)
     31free = libc.free
  • new file django/contrib/gis/geoip/prototypes.py

    diff -r a21c8b554d43 django/contrib/gis/geoip/prototypes.py
    - +  
     1from ctypes import c_char_p, c_float, c_int, string_at, Structure, POINTER
     2from django.contrib.gis.geoip.libgeoip import lgeoip, free
     3
     4#### GeoIP C Structure definitions ####
     5
     6class GeoIPRecord(Structure):
     7    _fields_ = [('country_code', c_char_p),
     8                ('country_code3', c_char_p),
     9                ('country_name', c_char_p),
     10                ('region', c_char_p),
     11                ('city', c_char_p),
     12                ('postal_code', c_char_p),
     13                ('latitude', c_float),
     14                ('longitude', c_float),
     15                # TODO: In 1.4.6 this changed from `int dma_code;` to
     16                # `union {int metro_code; int dma_code;};`.  Change
     17                # to a `ctypes.Union` in to accomodate in future when
     18                # pre-1.4.6 versions are no longer distributed.
     19                ('dma_code', c_int),
     20                ('area_code', c_int),
     21                ('charset', c_int),
     22                ('continent_code', c_char_p),
     23                ]
     24geoip_char_fields = [name for name, ctype in GeoIPRecord._fields_ if ctype is c_char_p]
     25geoip_encodings = { 0: 'iso-8859-1',
     26                    1: 'utf8',
     27                    }
     28
     29class GeoIPTag(Structure): pass
     30
     31RECTYPE = POINTER(GeoIPRecord)
     32DBTYPE = POINTER(GeoIPTag)
     33
     34#### ctypes function prototypes ####
     35
     36# GeoIP_lib_version appeared in version 1.4.7.
     37if hasattr(lgeoip, 'GeoIP_lib_version'):
     38    GeoIP_lib_version = lgeoip.GeoIP_lib_version
     39    GeoIP_lib_version.argtypes = None
     40    GeoIP_lib_version.restype = c_char_p
     41else:
     42    GeoIP_lib_version = None
     43
     44# For freeing memory allocated within a record
     45GeoIPRecord_delete = lgeoip.GeoIPRecord_delete
     46GeoIPRecord_delete.argtypes = [RECTYPE]
     47GeoIPRecord_delete.restype = None
     48
     49# For retrieving records by name or address.
     50def check_record(result, func, cargs):
     51    if bool(result):
     52        # Checking the pointer to the C structure, if valid pull out elements
     53        # into a dicionary.
     54        rec = result.contents
     55        record = dict((fld, getattr(rec, fld)) for fld, ctype in rec._fields_)
     56
     57        # Now converting the strings to unicode using the proper encoding.
     58        encoding = geoip_encodings[record['charset']]
     59        for char_field in geoip_char_fields:
     60            if record[char_field]:
     61                record[char_field] = record[char_field].decode(encoding)
     62
     63        # Free the memory allocated for the struct & return.
     64        GeoIPRecord_delete(result)
     65        return record
     66    else:
     67        return None
     68
     69def record_output(func):
     70    func.argtypes = [DBTYPE, c_char_p]
     71    func.restype = RECTYPE
     72    func.errcheck = check_record
     73    return func
     74GeoIP_record_by_addr = record_output(lgeoip.GeoIP_record_by_addr)
     75GeoIP_record_by_name = record_output(lgeoip.GeoIP_record_by_name)
     76
     77
     78# For opening & closing GeoIP database files.
     79GeoIP_open = lgeoip.GeoIP_open
     80GeoIP_open.restype = DBTYPE
     81GeoIP_delete = lgeoip.GeoIP_delete
     82GeoIP_delete.argtypes = [DBTYPE]
     83GeoIP_delete.restype = None
     84
     85# This is so the string pointer can be freed within Python.
     86class geoip_char_p(c_char_p):
     87    pass
     88
     89def check_string(result, func, cargs):
     90    if result:
     91        s = string_at(result)
     92        free(result)
     93    else:
     94        s = ''
     95    return s
     96
     97GeoIP_database_info = lgeoip.GeoIP_database_info
     98GeoIP_database_info.restype = geoip_char_p
     99GeoIP_database_info.errcheck = check_string
     100
     101# String output routines.
     102def string_output(func):
     103    func.restype = c_char_p
     104    return func
     105
     106GeoIP_country_code_by_addr = string_output(lgeoip.GeoIP_country_code_by_addr)
     107GeoIP_country_code_by_name = string_output(lgeoip.GeoIP_country_code_by_name)
     108GeoIP_country_name_by_addr = string_output(lgeoip.GeoIP_country_name_by_addr)
     109GeoIP_country_name_by_name = string_output(lgeoip.GeoIP_country_name_by_name)
  • new file django/contrib/gis/geoip/tests.py

    diff -r a21c8b554d43 django/contrib/gis/geoip/tests.py
    - +  
     1import os
     2from django.conf import settings
     3from django.contrib.gis.geos import GEOSGeometry
     4from django.contrib.gis.geoip import GeoIP, GeoIPException
     5from django.utils import unittest
     6
     7# Note: Requires use of both the GeoIP country and city datasets.
     8# The GEOIP_DATA path should be the only setting set (the directory
     9# should contain links or the actual database files 'GeoIP.dat' and
     10# 'GeoLiteCity.dat'.
     11class GeoIPTest(unittest.TestCase):
     12
     13    def test01_init(self):
     14        "Testing GeoIP initialization."
     15        g1 = GeoIP() # Everything inferred from GeoIP path
     16        path = settings.GEOIP_PATH
     17        g2 = GeoIP(path, 0) # Passing in data path explicitly.
     18        g3 = GeoIP.open(path, 0) # MaxMind Python API syntax.
     19
     20        for g in (g1, g2, g3):
     21            self.assertEqual(True, bool(g._country))
     22            self.assertEqual(True, bool(g._city))
     23
     24        # Only passing in the location of one database.
     25        city = os.path.join(path, 'GeoLiteCity.dat')
     26        cntry = os.path.join(path, 'GeoIP.dat')
     27        g4 = GeoIP(city, country='')
     28        self.assertEqual(None, g4._country)
     29        g5 = GeoIP(cntry, city='')
     30        self.assertEqual(None, g5._city)
     31
     32        # Improper parameters.
     33        bad_params = (23, 'foo', 15.23)
     34        for bad in bad_params:
     35            self.assertRaises(GeoIPException, GeoIP, cache=bad)
     36            if isinstance(bad, basestring):
     37                e = GeoIPException
     38            else:
     39                e = TypeError
     40            self.assertRaises(e, GeoIP, bad, 0)
     41
     42    def test02_bad_query(self):
     43        "Testing GeoIP query parameter checking."
     44        cntry_g = GeoIP(city='<foo>')
     45        # No city database available, these calls should fail.
     46        self.assertRaises(GeoIPException, cntry_g.city, 'google.com')
     47        self.assertRaises(GeoIPException, cntry_g.coords, 'yahoo.com')
     48
     49        # Non-string query should raise TypeError
     50        self.assertRaises(TypeError, cntry_g.country_code, 17)
     51        self.assertRaises(TypeError, cntry_g.country_name, GeoIP)
     52
     53    def test03_country(self):
     54        "Testing GeoIP country querying methods."
     55        g = GeoIP(city='<foo>')
     56
     57        fqdn = 'www.google.com'
     58        addr = '12.215.42.19'
     59
     60        for query in (fqdn, addr):
     61            for func in (g.country_code, g.country_code_by_addr, g.country_code_by_name):
     62                self.assertEqual('US', func(query))
     63            for func in (g.country_name, g.country_name_by_addr, g.country_name_by_name):
     64                self.assertEqual('United States', func(query))
     65            self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'},
     66                             g.country(query))
     67
     68    def test04_city(self):
     69        "Testing GeoIP city querying methods."
     70        g = GeoIP(country='<foo>')
     71
     72        addr = '128.249.1.1'
     73        fqdn = 'tmc.edu'
     74        for query in (fqdn, addr):
     75            # Country queries should still work.
     76            for func in (g.country_code, g.country_code_by_addr, g.country_code_by_name):
     77                self.assertEqual('US', func(query))
     78            for func in (g.country_name, g.country_name_by_addr, g.country_name_by_name):
     79                self.assertEqual('United States', func(query))
     80            self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'},
     81                             g.country(query))
     82
     83            # City information dictionary.
     84            d = g.city(query)
     85            self.assertEqual('USA', d['country_code3'])
     86            self.assertEqual('Houston', d['city'])
     87            self.assertEqual('TX', d['region'])
     88            self.assertEqual(713, d['area_code'])
     89            geom = g.geos(query)
     90            self.failIf(not isinstance(geom, GEOSGeometry))
     91            lon, lat = (-95.4010, 29.7079)
     92            lat_lon = g.lat_lon(query)
     93            lat_lon = (lat_lon[1], lat_lon[0])
     94            for tup in (geom.tuple, g.coords(query), g.lon_lat(query), lat_lon):
     95                self.assertAlmostEqual(lon, tup[0], 4)
     96                self.assertAlmostEqual(lat, tup[1], 4)
     97
     98    def test05_unicode(self):
     99        "Testing that GeoIP strings are properly encoded, see #16553."
     100        g = GeoIP()
     101        d = g.city('62.224.93.23')
     102        self.assertEqual(u'Sch\xf6mberg', d['city'])
     103
     104
     105def suite():
     106    s = unittest.TestSuite()
     107    s.addTest(unittest.makeSuite(GeoIPTest))
     108    return s
     109
     110def run(verbosity=1):
     111    unittest.TextTestRunner(verbosity=verbosity).run(suite())
  • django/contrib/gis/tests/__init__.py

    diff -r a21c8b554d43 django/contrib/gis/tests/__init__.py
    a b  
    7575        sys.stderr.write('GDAL not available - no tests requiring GDAL will be run.\n')
    7676
    7777    # Add GeoIP tests to the suite, if the library and data is available.
    78     from django.contrib.gis.utils import HAS_GEOIP
     78    from django.contrib.gis.geoip import HAS_GEOIP
    7979    if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'):
    80         from django.contrib.gis.tests import test_geoip
    81         suite.addTest(test_geoip.suite())
     80        from django.contrib.gis.geoip import tests as geoip_tests
     81        suite.addTest(geoip_tests.suite())
    8282
    8383    # Finally, adding the suites for each of the GeoDjango test apps.
    8484    if apps:
  • deleted file django/contrib/gis/tests/test_geoip.py

    diff -r a21c8b554d43 django/contrib/gis/tests/test_geoip.py
    + -  
    1 import os
    2 import unittest
    3 from django.db import settings
    4 from django.contrib.gis.geos import GEOSGeometry
    5 from django.contrib.gis.utils import GeoIP, GeoIPException
    6 
    7 # Note: Requires use of both the GeoIP country and city datasets.
    8 # The GEOIP_DATA path should be the only setting set (the directory
    9 # should contain links or the actual database files 'GeoIP.dat' and
    10 # 'GeoLiteCity.dat'.
    11 class GeoIPTest(unittest.TestCase):
    12 
    13     def test01_init(self):
    14         "Testing GeoIP initialization."
    15         g1 = GeoIP() # Everything inferred from GeoIP path
    16         path = settings.GEOIP_PATH
    17         g2 = GeoIP(path, 0) # Passing in data path explicitly.
    18         g3 = GeoIP.open(path, 0) # MaxMind Python API syntax.
    19 
    20         for g in (g1, g2, g3):
    21             self.assertEqual(True, bool(g._country))
    22             self.assertEqual(True, bool(g._city))
    23 
    24         # Only passing in the location of one database.
    25         city = os.path.join(path, 'GeoLiteCity.dat')
    26         cntry = os.path.join(path, 'GeoIP.dat')
    27         g4 = GeoIP(city, country='')
    28         self.assertEqual(None, g4._country)
    29         g5 = GeoIP(cntry, city='')
    30         self.assertEqual(None, g5._city)
    31 
    32         # Improper parameters.
    33         bad_params = (23, 'foo', 15.23)
    34         for bad in bad_params:
    35             self.assertRaises(GeoIPException, GeoIP, cache=bad)
    36             if isinstance(bad, basestring):
    37                 e = GeoIPException
    38             else:
    39                 e = TypeError
    40             self.assertRaises(e, GeoIP, bad, 0)
    41 
    42     def test02_bad_query(self):
    43         "Testing GeoIP query parameter checking."
    44         cntry_g = GeoIP(city='<foo>')
    45         # No city database available, these calls should fail.
    46         self.assertRaises(GeoIPException, cntry_g.city, 'google.com')
    47         self.assertRaises(GeoIPException, cntry_g.coords, 'yahoo.com')
    48 
    49         # Non-string query should raise TypeError
    50         self.assertRaises(TypeError, cntry_g.country_code, 17)
    51         self.assertRaises(TypeError, cntry_g.country_name, GeoIP)
    52 
    53     def test03_country(self):
    54         "Testing GeoIP country querying methods."
    55         g = GeoIP(city='<foo>')
    56 
    57         fqdn = 'www.google.com'
    58         addr = '12.215.42.19'
    59 
    60         for query in (fqdn, addr):
    61             for func in (g.country_code, g.country_code_by_addr, g.country_code_by_name):
    62                 self.assertEqual('US', func(query))
    63             for func in (g.country_name, g.country_name_by_addr, g.country_name_by_name):
    64                 self.assertEqual('United States', func(query))
    65             self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'},
    66                              g.country(query))
    67 
    68     def test04_city(self):
    69         "Testing GeoIP city querying methods."
    70         g = GeoIP(country='<foo>')
    71 
    72         addr = '130.80.29.3'
    73         fqdn = 'chron.com'
    74         for query in (fqdn, addr):
    75             # Country queries should still work.
    76             for func in (g.country_code, g.country_code_by_addr, g.country_code_by_name):
    77                 self.assertEqual('US', func(query))
    78             for func in (g.country_name, g.country_name_by_addr, g.country_name_by_name):
    79                 self.assertEqual('United States', func(query))
    80             self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'},
    81                              g.country(query))
    82 
    83             # City information dictionary.
    84             d = g.city(query)
    85             self.assertEqual('USA', d['country_code3'])
    86             self.assertEqual('Houston', d['city'])
    87             self.assertEqual('TX', d['region'])
    88             self.assertEqual(713, d['area_code'])
    89             geom = g.geos(query)
    90             self.failIf(not isinstance(geom, GEOSGeometry))
    91             lon, lat = (-95.3670, 29.7523)
    92             lat_lon = g.lat_lon(query)
    93             lat_lon = (lat_lon[1], lat_lon[0])
    94             for tup in (geom.tuple, g.coords(query), g.lon_lat(query), lat_lon):
    95                 self.assertAlmostEqual(lon, tup[0], 4)
    96                 self.assertAlmostEqual(lat, tup[1], 4)
    97 
    98 def suite():
    99     s = unittest.TestSuite()
    100     s.addTest(unittest.makeSuite(GeoIPTest))
    101     return s
    102 
    103 def run(verbosity=2):
    104     unittest.TextTestRunner(verbosity=verbosity).run(suite())
  • django/contrib/gis/utils/__init__.py

    diff -r a21c8b554d43 django/contrib/gis/utils/__init__.py
    a b  
    1515        pass
    1616   
    1717# Attempting to import the GeoIP class.
    18 try:
    19     from django.contrib.gis.utils.geoip import GeoIP, GeoIPException
    20     HAS_GEOIP = True
    21 except:
    22     HAS_GEOIP = False
     18from django.contrib.gis import geoip
     19HAS_GEOIP = geoip.HAS_GEOIP
     20if HAS_GEOIP:
     21    GeoIP = geoip.GeoIP
     22    GeoIPException = geoip.GeoIPException
    2323
    2424from django.contrib.gis.utils.wkt import precision_wkt
    2525
  • deleted file django/contrib/gis/utils/geoip.py

    diff -r a21c8b554d43 django/contrib/gis/utils/geoip.py
    + -  
    1 """
    2  This module houses the GeoIP object, a ctypes wrapper for the MaxMind GeoIP(R)
    3  C API (http://www.maxmind.com/app/c).  This is an alternative to the GPL
    4  licensed Python GeoIP interface provided by MaxMind.
    5 
    6  GeoIP(R) is a registered trademark of MaxMind, LLC of Boston, Massachusetts.
    7 
    8  For IP-based geolocation, this module requires the GeoLite Country and City
    9  datasets, in binary format (CSV will not work!).  The datasets may be
    10  downloaded from MaxMind at http://www.maxmind.com/download/geoip/database/.
    11  Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory
    12  corresponding to settings.GEOIP_PATH.  See the GeoIP docstring and examples
    13  below for more details.
    14 
    15  TODO: Verify compatibility with Windows.
    16 
    17  Example:
    18 
    19  >>> from django.contrib.gis.utils import GeoIP
    20  >>> g = GeoIP()
    21  >>> g.country('google.com')
    22  {'country_code': 'US', 'country_name': 'United States'}
    23  >>> g.city('72.14.207.99')
    24  {'area_code': 650,
    25  'city': 'Mountain View',
    26  'country_code': 'US',
    27  'country_code3': 'USA',
    28  'country_name': 'United States',
    29  'dma_code': 807,
    30  'latitude': 37.419200897216797,
    31  'longitude': -122.05740356445312,
    32  'postal_code': '94043',
    33  'region': 'CA'}
    34  >>> g.lat_lon('salon.com')
    35  (37.789798736572266, -122.39420318603516)
    36  >>> g.lon_lat('uh.edu')
    37  (-95.415199279785156, 29.77549934387207)
    38  >>> g.geos('24.124.1.80').wkt
    39  'POINT (-95.2087020874023438 39.0392990112304688)'
    40 """
    41 import os
    42 import re
    43 from ctypes import c_char_p, c_float, c_int, Structure, CDLL, POINTER
    44 from ctypes.util import find_library
    45 from django.conf import settings
    46 if not settings.configured: settings.configure()
    47 
    48 # Creating the settings dictionary with any settings, if needed.
    49 GEOIP_SETTINGS = dict((key, getattr(settings, key))
    50                       for key in ('GEOIP_PATH', 'GEOIP_LIBRARY_PATH', 'GEOIP_COUNTRY', 'GEOIP_CITY')
    51                       if hasattr(settings, key))
    52 lib_path = GEOIP_SETTINGS.get('GEOIP_LIBRARY_PATH', None)
    53 
    54 # GeoIP Exception class.
    55 class GeoIPException(Exception): pass
    56 
    57 # The shared library for the GeoIP C API.  May be downloaded
    58 #  from http://www.maxmind.com/download/geoip/api/c/
    59 if lib_path:
    60     lib_name = None
    61 else:
    62     # TODO: Is this really the library name for Windows?
    63     lib_name = 'GeoIP'
    64 
    65 # Getting the path to the GeoIP library.
    66 if lib_name: lib_path = find_library(lib_name)
    67 if lib_path is None: raise GeoIPException('Could not find the GeoIP library (tried "%s"). '
    68                                           'Try setting GEOIP_LIBRARY_PATH in your settings.' % lib_name)
    69 lgeoip = CDLL(lib_path)
    70 
    71 # Regular expressions for recognizing IP addresses and the GeoIP
    72 # free database editions.
    73 ipregex = re.compile(r'^(?P<w>\d\d?\d?)\.(?P<x>\d\d?\d?)\.(?P<y>\d\d?\d?)\.(?P<z>\d\d?\d?)$')
    74 free_regex = re.compile(r'^GEO-\d{3}FREE')
    75 lite_regex = re.compile(r'^GEO-\d{3}LITE')
    76 
    77 #### GeoIP C Structure definitions ####
    78 class GeoIPRecord(Structure):
    79     _fields_ = [('country_code', c_char_p),
    80                 ('country_code3', c_char_p),
    81                 ('country_name', c_char_p),
    82                 ('region', c_char_p),
    83                 ('city', c_char_p),
    84                 ('postal_code', c_char_p),
    85                 ('latitude', c_float),
    86                 ('longitude', c_float),
    87                 # TODO: In 1.4.6 this changed from `int dma_code;` to
    88                 # `union {int metro_code; int dma_code;};`.  Change
    89                 # to a `ctypes.Union` in to accomodate in future when
    90                 # pre-1.4.6 versions are no longer distributed.
    91                 ('dma_code', c_int),
    92                 ('area_code', c_int),
    93                 # TODO: The following structure fields were added in 1.4.3 --
    94                 # uncomment these fields when sure previous versions are no
    95                 # longer distributed by package maintainers.
    96                 #('charset', c_int),
    97                 #('continent_code', c_char_p),
    98                 ]
    99 class GeoIPTag(Structure): pass
    100 
    101 #### ctypes function prototypes ####
    102 RECTYPE = POINTER(GeoIPRecord)
    103 DBTYPE = POINTER(GeoIPTag)
    104 
    105 # For retrieving records by name or address.
    106 def record_output(func):
    107     func.restype = RECTYPE
    108     return func
    109 rec_by_addr = record_output(lgeoip.GeoIP_record_by_addr)
    110 rec_by_name = record_output(lgeoip.GeoIP_record_by_name)
    111 
    112 # For opening & closing GeoIP database files.
    113 geoip_open = lgeoip.GeoIP_open
    114 geoip_open.restype = DBTYPE
    115 geoip_close = lgeoip.GeoIP_delete
    116 geoip_close.argtypes = [DBTYPE]
    117 geoip_close.restype = None
    118 
    119 # String output routines.
    120 def string_output(func):
    121     func.restype = c_char_p
    122     return func
    123 geoip_dbinfo = string_output(lgeoip.GeoIP_database_info)
    124 cntry_code_by_addr = string_output(lgeoip.GeoIP_country_code_by_addr)
    125 cntry_code_by_name = string_output(lgeoip.GeoIP_country_code_by_name)
    126 cntry_name_by_addr = string_output(lgeoip.GeoIP_country_name_by_addr)
    127 cntry_name_by_name = string_output(lgeoip.GeoIP_country_name_by_name)
    128 
    129 #### GeoIP class ####
    130 class GeoIP(object):
    131     # The flags for GeoIP memory caching.
    132     # GEOIP_STANDARD - read database from filesystem, uses least memory.
    133     #
    134     # GEOIP_MEMORY_CACHE - load database into memory, faster performance
    135     #        but uses more memory
    136     #
    137     # GEOIP_CHECK_CACHE - check for updated database.  If database has been updated,
    138     #        reload filehandle and/or memory cache.
    139     #
    140     # GEOIP_INDEX_CACHE - just cache
    141     #        the most frequently accessed index portion of the database, resulting
    142     #        in faster lookups than GEOIP_STANDARD, but less memory usage than
    143     #        GEOIP_MEMORY_CACHE - useful for larger databases such as
    144     #        GeoIP Organization and GeoIP City.  Note, for GeoIP Country, Region
    145     #        and Netspeed databases, GEOIP_INDEX_CACHE is equivalent to GEOIP_MEMORY_CACHE
    146     #
    147     GEOIP_STANDARD = 0
    148     GEOIP_MEMORY_CACHE = 1
    149     GEOIP_CHECK_CACHE = 2
    150     GEOIP_INDEX_CACHE = 4
    151     cache_options = dict((opt, None) for opt in (0, 1, 2, 4))
    152     _city_file = ''
    153     _country_file = ''
    154 
    155     # Initially, pointers to GeoIP file references are NULL.
    156     _city = None
    157     _country = None
    158 
    159     def __init__(self, path=None, cache=0, country=None, city=None):
    160         """
    161         Initializes the GeoIP object, no parameters are required to use default
    162         settings.  Keyword arguments may be passed in to customize the locations
    163         of the GeoIP data sets.
    164 
    165         * path: Base directory to where GeoIP data is located or the full path
    166             to where the city or country data files (*.dat) are located.
    167             Assumes that both the city and country data sets are located in
    168             this directory; overrides the GEOIP_PATH settings attribute.
    169 
    170         * cache: The cache settings when opening up the GeoIP datasets,
    171             and may be an integer in (0, 1, 2, 4) corresponding to
    172             the GEOIP_STANDARD, GEOIP_MEMORY_CACHE, GEOIP_CHECK_CACHE,
    173             and GEOIP_INDEX_CACHE `GeoIPOptions` C API settings,
    174             respectively.  Defaults to 0, meaning that the data is read
    175             from the disk.
    176 
    177         * country: The name of the GeoIP country data file.  Defaults to
    178             'GeoIP.dat'; overrides the GEOIP_COUNTRY settings attribute.
    179 
    180         * city: The name of the GeoIP city data file.  Defaults to
    181             'GeoLiteCity.dat'; overrides the GEOIP_CITY settings attribute.
    182         """
    183         # Checking the given cache option.
    184         if cache in self.cache_options:
    185             self._cache = self.cache_options[cache]
    186         else:
    187             raise GeoIPException('Invalid caching option: %s' % cache)
    188 
    189         # Getting the GeoIP data path.
    190         if not path:
    191             path = GEOIP_SETTINGS.get('GEOIP_PATH', None)
    192             if not path: raise GeoIPException('GeoIP path must be provided via parameter or the GEOIP_PATH setting.')
    193         if not isinstance(path, basestring):
    194             raise TypeError('Invalid path type: %s' % type(path).__name__)
    195 
    196         if os.path.isdir(path):
    197             # Constructing the GeoIP database filenames using the settings
    198             # dictionary.  If the database files for the GeoLite country
    199             # and/or city datasets exist, then try and open them.
    200             country_db = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.dat'))
    201             if os.path.isfile(country_db):
    202                 self._country = geoip_open(country_db, cache)
    203                 self._country_file = country_db
    204 
    205             city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat'))
    206             if os.path.isfile(city_db):
    207                 self._city = geoip_open(city_db, cache)
    208                 self._city_file = city_db
    209         elif os.path.isfile(path):
    210             # Otherwise, some detective work will be needed to figure
    211             # out whether the given database path is for the GeoIP country
    212             # or city databases.
    213             ptr = geoip_open(path, cache)
    214             info = geoip_dbinfo(ptr)
    215             if lite_regex.match(info):
    216                 # GeoLite City database detected.
    217                 self._city = ptr
    218                 self._city_file = path
    219             elif free_regex.match(info):
    220                 # GeoIP Country database detected.
    221                 self._country = ptr
    222                 self._country_file = path
    223             else:
    224                 raise GeoIPException('Unable to recognize database edition: %s' % info)
    225         else:
    226             raise GeoIPException('GeoIP path must be a valid file or directory.')
    227 
    228     def __del__(self):
    229         # Cleaning any GeoIP file handles lying around.
    230         if self._country: geoip_close(self._country)
    231         if self._city: geoip_close(self._city)
    232 
    233     def _check_query(self, query, country=False, city=False, city_or_country=False):
    234         "Helper routine for checking the query and database availability."
    235         # Making sure a string was passed in for the query.
    236         if not isinstance(query, basestring):
    237             raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__)
    238 
    239         # Extra checks for the existence of country and city databases.
    240         if city_or_country and not (self._country or self._city):
    241             raise GeoIPException('Invalid GeoIP country and city data files.')
    242         elif country and not self._country:
    243             raise GeoIPException('Invalid GeoIP country data file: %s' % self._country_file)
    244         elif city and not self._city:
    245             raise GeoIPException('Invalid GeoIP city data file: %s' % self._city_file)
    246 
    247     def city(self, query):
    248         """
    249         Returns a dictionary of city information for the given IP address or
    250         Fully Qualified Domain Name (FQDN).  Some information in the dictionary
    251         may be undefined (None).
    252         """
    253         self._check_query(query, city=True)
    254         if ipregex.match(query):
    255             # If an IP address was passed in
    256             ptr = rec_by_addr(self._city, c_char_p(query))
    257         else:
    258             # If a FQDN was passed in.
    259             ptr = rec_by_name(self._city, c_char_p(query))
    260 
    261         # Checking the pointer to the C structure, if valid pull out elements
    262         # into a dicionary and return.
    263         if bool(ptr):
    264             record = ptr.contents
    265             return dict((tup[0], getattr(record, tup[0])) for tup in record._fields_)
    266         else:
    267             return None
    268 
    269     def country_code(self, query):
    270         "Returns the country code for the given IP Address or FQDN."
    271         self._check_query(query, city_or_country=True)
    272         if self._country:
    273             if ipregex.match(query): return cntry_code_by_addr(self._country, query)
    274             else: return cntry_code_by_name(self._country, query)
    275         else:
    276             return self.city(query)['country_code']
    277 
    278     def country_name(self, query):
    279         "Returns the country name for the given IP Address or FQDN."
    280         self._check_query(query, city_or_country=True)
    281         if self._country:
    282             if ipregex.match(query): return cntry_name_by_addr(self._country, query)
    283             else: return cntry_name_by_name(self._country, query)
    284         else:
    285             return self.city(query)['country_name']
    286 
    287     def country(self, query):
    288         """
    289         Returns a dictonary with with the country code and name when given an
    290         IP address or a Fully Qualified Domain Name (FQDN).  For example, both
    291         '24.124.1.80' and 'djangoproject.com' are valid parameters.
    292         """
    293         # Returning the country code and name
    294         return {'country_code' : self.country_code(query),
    295                 'country_name' : self.country_name(query),
    296                 }
    297 
    298     #### Coordinate retrieval routines ####
    299     def coords(self, query, ordering=('longitude', 'latitude')):
    300         cdict = self.city(query)
    301         if cdict is None: return None
    302         else: return tuple(cdict[o] for o in ordering)
    303 
    304     def lon_lat(self, query):
    305         "Returns a tuple of the (longitude, latitude) for the given query."
    306         return self.coords(query)
    307 
    308     def lat_lon(self, query):
    309         "Returns a tuple of the (latitude, longitude) for the given query."
    310         return self.coords(query, ('latitude', 'longitude'))
    311 
    312     def geos(self, query):
    313         "Returns a GEOS Point object for the given query."
    314         ll = self.lon_lat(query)
    315         if ll:
    316             from django.contrib.gis.geos import Point
    317             return Point(ll, srid=4326)
    318         else:
    319             return None
    320 
    321     #### GeoIP Database Information Routines ####
    322     def country_info(self):
    323         "Returns information about the GeoIP country database."
    324         if self._country is None:
    325             ci = 'No GeoIP Country data in "%s"' % self._country_file
    326         else:
    327             ci = geoip_dbinfo(self._country)
    328         return ci
    329     country_info = property(country_info)
    330 
    331     def city_info(self):
    332         "Retuns information about the GeoIP city database."
    333         if self._city is None:
    334             ci = 'No GeoIP City data in "%s"' % self._city_file
    335         else:
    336             ci = geoip_dbinfo(self._city)
    337         return ci
    338     city_info = property(city_info)
    339 
    340     def info(self):
    341         "Returns information about all GeoIP databases in use."
    342         return 'Country:\n\t%s\nCity:\n\t%s' % (self.country_info, self.city_info)
    343     info = property(info)
    344 
    345     #### Methods for compatibility w/the GeoIP-Python API. ####
    346     @classmethod
    347     def open(cls, full_path, cache):
    348         return GeoIP(full_path, cache)
    349 
    350     def _rec_by_arg(self, arg):
    351         if self._city:
    352             return self.city(arg)
    353         else:
    354             return self.country(arg)
    355     region_by_addr = city
    356     region_by_name = city
    357     record_by_addr = _rec_by_arg
    358     record_by_name = _rec_by_arg
    359     country_code_by_addr = country_code
    360     country_code_by_name = country_code
    361     country_name_by_addr = country_name
    362     country_name_by_name = country_name
  • docs/ref/contrib/gis/geoip.txt

    diff -r a21c8b554d43 docs/ref/contrib/gis/geoip.txt
    a b  
    44Geolocation with GeoIP
    55======================
    66
    7 .. module:: django.contrib.gis.utils.geoip
     7.. module:: django.contrib.gis.geoip
    88   :synopsis: High-level Python interface for MaxMind's GeoIP C library.
    99
    10 .. currentmodule:: django.contrib.gis.utils
    11 
    1210The :class:`GeoIP` object is a ctypes wrapper for the
    1311`MaxMind GeoIP C API`__. [#]_  This interface is a BSD-licensed alternative
    1412to the GPL-licensed `Python GeoIP`__ interface provided by MaxMind.
     
    136134
    137135All the following querying routines may take either a string IP address
    138136or a fully qualified domain name (FQDN).  For example, both
    139 ``'24.124.1.80'`` and ``'djangoproject.com'`` would be valid query
     137``'205.186.163.125'`` and ``'djangoproject.com'`` would be valid query
    140138parameters.
    141139
    142140.. method:: GeoIP.city(query)
     
    144142Returns a dictionary of city information for the given query.  Some
    145143of the values in the dictionary may be undefined (``None``).
    146144
    147 .. method:: GeoIPcountry(query)
     145.. method:: GeoIP.country(query)
    148146
    149147Returns a dictionary with the country code and country for the given
    150148query.
Back to Top