# coding: utf-8
#
# Copyright (c) 2013, CN-Software Ltd.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#     1. Redistributions of source code must retain the above copyright notice,
#        this list of conditions and the following disclaimer.
#
#     2. Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in the
#        documentation and/or other materials provided with the distribution.
#
#     3. Neither the name of CN-Software Ltd. nor the names of its contributors
#        may be used to endorse or promote products derived from this software
#        without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""
Geolocation by the IP-address.

Geobaza is the software for identifying geolocation by IP-address, developed and
supported by CN-Software Ltd. Taking a certain IP-address, Geobaza outputs its
local registration data—a country and a city with geographic coordinates.

Geobaza database optionally includes: object identifier ISO 3166, translations,
up-to-date information about population size and official languages.

Documentation: http://geobaza.ru/docs/python/v1/.

Detailed information and updates: http://geobaza.ru/.
"""

__copyright__ = "Copyright 2013, CN-Software Ltd."

__license__ = "New-style BSD"
__version__ = "1.0.4.13.01"
__email__ = "support@cn-software.com"
__status__ = "Production"

import os
import socket
import struct
import decimal
import xml.etree.cElementTree as etree
from time import time
from datetime import datetime as dt
import simplejson as json

from geobaza.utils import validate_ip, Bunch, to_str, prettify_xml, singleton_init

FILE_PATH = '%s/data/geobaza.dat' % os.path.normpath(os.path.dirname(__file__))

# Caching
NO_CACHE = 0
MEMORY_CACHE = 1

class GeobazaException(Exception):
    pass

class GeobazaIPException(GeobazaException):
    pass

class Headers(Bunch):
    pass

class Serializer(object):
    def to_xml(self):
        """Returns XML"""
        xml = self.as_xml()
        return prettify_xml(etree.tostring(xml))

    def to_json(self):
        """Returns JSON"""
        return json.dumps(self.as_dict())

    def to_pretty_xml(self):
        """Returns pretty printed XML"""
        xml = self.as_xml()
        return prettify_xml(etree.tostring(xml), indent=' ' * 4)

    def to_pretty_json(self):
        """Returns pretty printed JSON"""
        return json.dumps(self.as_dict(), sort_keys=True, indent=4)

class Binary(object):
    """Proxy for working with binary files"""

    def unpack(self, bytes, fmt):
        """Reads bytes and unpacks with struct.unpack_from using format 'fmt'"""
        data = self.read(bytes)
        return struct.unpack_from(fmt, data)[0]

class BinaryFile(Binary, file):
    __new__ = singleton_init

class BinaryBlob(Binary):
    __new__ = singleton_init

    def __init__(self, path):
        self.offset = 0

        f = open(path, 'rb')
        self.blob = f.read(os.path.getsize(f.name))

    def tell(self):
        return self.offset

    def seek(self, offset, whence=0):
        if whence == 1:
            self.offset += offset
        elif whence == 2:
            self.offset = len(self.blob) + offset
        else:
            self.offset = offset

    def read(self, bytes):
        offset = self.offset + bytes
        data = self.blob[self.offset:offset]
        self.offset = offset
        return data

class Language(object):
    def __init__(self, id, name):
        self.id = id
        self.name = name

    def __repr__(self):
        return '<Language: %s>' % self.name.encode('utf-8')

class Languages(object):
    _languages = {
        'aa': u'Afar',
        'ab': u'Abkhazian',
        'ae': u'Avestan',
        'af': u'Afrikaans',
        'ak': u'Akan',
        'am': u'Amharic',
        'an': u'Aragonese',
        'ar': u'Arabic',
        'as': u'Assamese',
        'av': u'Avaric',
        'ay': u'Aymara',
        'az': u'Azerbaijani',
        'ba': u'Bashkir',
        'be': u'Belarusian',
        'bg': u'Bulgarian',
        'bh': u'Bihari',
        'bi': u'Bislama',
        'bm': u'Bambara',
        'bn': u'Bengali',
        'bo': u'Tibetan',
        'br': u'Breton',
        'bs': u'Bosnian',
        'ca': u'Catalan; Valencian',
        'ce': u'Chechen',
        'ch': u'Chamorro',
        'co': u'Corsican',
        'cr': u'Cree',
        'cs': u'Czech',
        'cu': u'Church Slavic',
        'cv': u'Chuvash',
        'cy': u'Welsh',
        'da': u'Danish',
        'de': u'German',
        'dv': u'Divehi; Dhivehi; Maldivian',
        'dz': u'Dzongkha',
        'ee': u'Ewe',
        'el': u'Greek, Modern',
        'en': u'English',
        'eo': u'Esperanto',
        'es': u'Spanish; Castilian',
        'et': u'Estonian',
        'eu': u'Basque',
        'fa': u'Persian',
        'ff': u'Fulah',
        'fi': u'Finnish',
        'fj': u'Fijian',
        'fo': u'Faroese',
        'fr': u'French',
        'fy': u'Western Frisian',
        'ga': u'Irish',
        'gd': u'Gaelic; Scottish Gaelic',
        'gl': u'Galician',
        'gn': u'Guarani',
        'gu': u'Gujarati',
        'gv': u'Manx',
        'ha': u'Hausa',
        'he': u'Hebrew',
        'hi': u'Hindi',
        'ho': u'Hiri Motu',
        'hr': u'Croatian',
        'ht': u'Haitian; Haitian Creole',
        'hu': u'Hungarian',
        'hy': u'Armenian',
        'hz': u'Herero',
        'ia': u'Interlingua',
        'id': u'Indonesian',
        'ie': u'Interlingue; Occidental',
        'ig': u'Igbo',
        'ii': u'Sichuan Yi; Nuosu',
        'ik': u'Inupiaq',
        'io': u'Ido',
        'is': u'Icelandic',
        'it': u'Italian',
        'iu': u'Inuktitut',
        'ja': u'Japanese',
        'jv': u'Javanese',
        'ka': u'Georgian',
        'kg': u'Kongo',
        'ki': u'Kikuyu; Gikuyu',
        'kj': u'Kuanyama; Kwanyama',
        'kk': u'Kazakh',
        'kl': u'Kalaallisut; Greenlandic',
        'km': u'Central Khmer',
        'kn': u'Kannada',
        'ko': u'Korean',
        'kr': u'Kanuri',
        'ks': u'Kashmiri',
        'ku': u'Kurdish',
        'kv': u'Komi',
        'kw': u'Cornish',
        'ky': u'Kirghiz; Kyrgyz',
        'la': u'Latin',
        'lb': u'Luxembourgish; Letzeburgesch',
        'lg': u'Ganda',
        'li': u'Limburgan; Limburger; Limburgish',
        'ln': u'Lingala',
        'lo': u'Lao',
        'lt': u'Lithuanian',
        'lu': u'Luba-Katanga',
        'lv': u'Latvian',
        'mg': u'Malagasy',
        'mh': u'Marshallese',
        'mi': u'Maori',
        'mk': u'Macedonian',
        'ml': u'Malayalam',
        'mn': u'Mongolian',
        'mo': u'Moldavian; Moldovan',
        'mr': u'Marathi',
        'ms': u'Malay',
        'mt': u'Maltese',
        'my': u'Burmese',
        'na': u'Nauru',
        'nb': u'Bokmål, Norwegian; Norwegian Bokmål',
        'nd': u'Ndebele, North; North Ndebele',
        'ne': u'Nepali',
        'ng': u'Ndonga',
        'nl': u'Dutch (Flemish)',
        'nn': u'Norwegian Nynorsk; Nynorsk, Norwegian',
        'no': u'Norwegian',
        'nr': u'Ndebele, South; South Ndebele',
        'nv': u'Navajo; Navaho',
        'ny': u'Chichewa; Chewa; Nyanja',
        'oc': u'Occitan (post 1500)',
        'oj': u'Ojibwa',
        'om': u'Oromo',
        'or': u'Oriya',
        'os': u'Ossetian; Ossetic',
        'pa': u'Panjabi; Punjabi',
        'pi': u'Pali',
        'pl': u'Polish',
        'ps': u'Pushto; Pashto',
        'pt': u'Portuguese',
        'qu': u'Quechua',
        'rm': u'Romansh',
        'rn': u'Rundi',
        'ro': u'Romanian',
        'ru': u'Russian',
        'rw': u'Kinyarwanda',
        'sa': u'Sanskrit',
        'sc': u'Sardinian',
        'sd': u'Sindhi',
        'se': u'Northern Sami',
        'sg': u'Sango',
        'si': u'Sinhala; Sinhalese',
        'sk': u'Slovak',
        'sl': u'Slovenian',
        'sm': u'Samoan',
        'sn': u'Shona',
        'so': u'Somali',
        'sq': u'Albanian',
        'sr': u'Serbian',
        'ss': u'Swati',
        'st': u'Sotho, Southern',
        'su': u'Sundanese',
        'sv': u'Swedish',
        'sw': u'Swahili',
        'ta': u'Tamil',
        'te': u'Telugu',
        'tg': u'Tajik',
        'th': u'Thai',
        'ti': u'Tigrinya',
        'tk': u'Turkmen',
        'tl': u'Tagalog',
        'tn': u'Tswana',
        'to': u'Tonga (Tonga Islands)',
        'tr': u'Turkish',
        'ts': u'Tsonga',
        'tt': u'Tatar',
        'tw': u'Twi',
        'ty': u'Tahitian',
        'ug': u'Uighur; Uyghur',
        'uk': u'Ukrainian',
        'ur': u'Urdu',
        'uz': u'Uzbek',
        've': u'Venda',
        'vi': u'Vietnamese',
        'vo': u'Volapük',
        'wa': u'Walloon',
        'wo': u'Wolof',
        'xh': u'Xhosa',
        'yi': u'Yiddish',
        'yo': u'Yoruba',
        'za': u'Zhuang; Chuang',
        'zh': u'Chinese',
        'zu': u'Zulu'
    }

    @classmethod
    def list_languages(cls):
        for id, name in cls._languages.iteritems():
            print '%s, %s' % (id, name)

    @classmethod
    def get_language(cls, id):
        lang = cls._languages.get(id)
        if not lang:
            raise GeobazaException('Language with ISO ID %s does not exists!' % id)
        return Language(id, lang)

class LatLon(object):
    def __init__(self, lat=None, lon=None):
        self.latitude = lat
        self.longitude = lon

class Geography(Serializer):
    def __init__(self, **kwargs):
        for k, v in kwargs.iteritems():
            setattr(self, k, v)

    def as_xml(self):
        """Returns self as xml.etree.cElementTree.Element"""
        root = etree.Element('geography')
        center = etree.SubElement(root, 'center')
        center.set('latitude', to_str(self.center.latitude))
        center.set('longitude', to_str(self.center.longitude))
        return root

    def as_dict(self):
        """Returns self as python dictionary"""
        return {'center': {'latitude': self.center.latitude, 'longitude': self.center.longitude}}

class AbstractObject(object):
    pass

class GeobazaObject(AbstractObject, Serializer):
    COUNTRY = 'country'
    REGION = 'region'
    LOCALITY = 'locality'
    SPECIAL = 'special'

    def __init__(self, data):
        def geography(data):
            lat = data.get('lat', None)
            lon = data.get('lon', None)
            self.geography = Geography(center=LatLon(lat, lon))

        def population(data):
            try:
                self.population = int(data.get('population', None))
            except TypeError:
                self.population = None

        def translation(data):
            RAW_MAP = {
                'official': (0, 'name_official'),
                'alt': (1, 'name'),
            }

            self.translations = []
            for type, (priority, key) in RAW_MAP.iteritems():
                obj = Bunch(type=type)
                for lang, name in data.get(key).iteritems():
                    obj[lang.lower()] = name
                    self.translations.insert(priority, obj)

        def name():
            self.name = self.translations[0].en

        self._parent = None
        self.level = None
        self.child = None

        self.id = int(data['id'])
        self.level = int(data['level'])
        self.type = data['type']

        for attr in ['iso_id']:
            setattr(self, attr, data.get(attr))

        geography(data)
        population(data)
        translation(data)
        name()

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self.name.encode('utf-8'))

    @property
    def parent(self):
        if isinstance(self._parent, int):
            data = self.query.get_from(self._parent)

            if len(data) == 2:
                obj = data[0]
                parent = data[1]['parent']
            else:
                obj = data[0]
                parent = None
            self._parent = Geobaza.create_instance(obj, child=self, parent=parent)
            self._parent.query = self.query
        return self._parent

    def set_parent(self, parent):
        self._parent = parent

    def as_xml(self):
        """Returns self as xml.etree.cElementTree.Element"""
        object = etree.Element('object')
        object.set('type', self.type)
        object.set('id', to_str(self.id))
        if self.parent:
            object.set('parent', to_str(self.parent.id))
        if self.child:
            object.set('child', to_str(self.child.id))

        name = etree.SubElement(object, 'name')
        name.text = self.name
        iso_id = etree.SubElement(object, 'iso-id')
        iso_id.text = to_str(self.iso_id)

        object.append(self.geography.as_xml())
        population = etree.SubElement(object, 'population')
        population.text = to_str(self.population)
        translations = etree.SubElement(object, 'translations')
        for item in self.translations:
            group = etree.SubElement(translations, 'group')
            group.set('type', item.type)
            for key, value in item.iteritems():
                if key != 'type':
                    translation = etree.SubElement(group, 'item')
                    translation.set('language', key)
                    translation.text = value
        return object

    def as_dict(self, attrs=[]):
        """Returns self as python dictionary"""
        object = {'type': self.type, 'name': self.name, 'iso_id': to_str(self.iso_id), 'id': self.id}
        if self.parent:
            object['parent'] = self.parent.id
        if self.child:
            object['child'] = self.child.id

        object['geography'] = self.geography.as_dict()
        object['population'] = to_str(self.population)
        object['translations'] = self.translations
        return object

class Region(GeobazaObject):
    def __init__(self, data):
        super(Region, self).__init__(data)

        def lang(data):
            try:
                self.language = Languages.get_language(data.get('lang', None).lower())
            except (GeobazaException, AttributeError):
                self.language = None

        # Set language for region
        lang(data)

    def as_xml(self):
        xml = super(Region, self).as_xml()
        language = etree.SubElement(xml, 'language')
        if self.language:
            language.set('id', self.language.id)
            language.text = self.language.name
        return xml

    def as_dict(self):
        object = super(Region, self).as_dict()
        language = None
        if self.language:
            language = {'name': self.language.name, 'id': self.language.id}
        object['language'] = language
        return object

class Country(Region):
    def __init__(self, data):
        super(Country, self).__init__(data)

        def tld():
            if self.iso_id == 'GB':
                self.tld = 'uk'
            elif self.iso_id is None:
                self.tld = None
            else:
                self.tld = self.iso_id.lower()

        # Set TLD for country
        tld()

    def as_xml(self):
        xml = super(Country, self).as_xml()
        tld = etree.SubElement(xml, 'tld')
        tld.text = to_str(self.tld)
        return xml

    def as_dict(self):
        object = super(Region, self).as_dict()
        object['tld'] = self.tld
        return object

class Locality(GeobazaObject):
    pass

class SpecialRange(AbstractObject, Serializer):
    """
    Class for special IPv4 addresses ranges

    See: http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xml
    """
    def __init__(self, data):
        self.type = GeobazaObject.SPECIAL
        self.name = data.get('special', None)

    def __repr__(self):
        name = self.__class__.__name__
        if self.name:
            name = '<%s: %s>' % (name, self.name.encode('utf-8'))
        else:
            name = '<%s>' % name
        return name

    def as_xml(self):
        root = etree.Element('object')
        root.set('type', self.type)
        name = etree.SubElement(root, 'name')
        name.text = to_str(self.name)
        return root

    def as_dict(self):
        return {'type': self.type, 'name': to_str(self.name)}

class Geobaza(Serializer):
    REPR_OUTPUT_SIZE = 2

    def __len__(self):
        return len(self._list)

    def __getitem__(self, slc):
        if isinstance(slc, slice):
            return Geobaza.from_list(self._list[slc], **self._kwargs)
        else:
            return self._list[slc]

    def __setitem__(self, key, value):
        self._list[key] = value

    def __delitem__(self, key):
        del self._list[key]

    def __iter__(self):
        return iter(self._list)

    def __contains__(self, item):
        return item in self._list

    @property
    def is_special(self):
        if self._is_special is None:
            self._is_special = SpecialRange in [i.__class__ for i in self._list]
        return self._is_special

    def append(self, item):
        self._add_by_type(item)
        self._list.append(item)

    def extend(self, lst):
        for item in lst:
            self._add_by_type(item)
        self._list.extend(lst)

    def __init__(self, **kwargs):
        self._kwargs = kwargs
        self._list = []
        self._is_special = None

        self.country = None
        self._regions = []
        self._localities = []
        self.ip = kwargs['ip']
        self.query = kwargs['query']
        self.headers = kwargs['headers']

    @staticmethod
    def from_list(*items, **kwargs):
        geobaza = Geobaza(**kwargs)
        geobaza.extend(list(*items))
        return geobaza

    @staticmethod
    def from_dict(data, **kwargs):
        geobaza = Geobaza(**kwargs)

        parent = None
        data.reverse()

        for obj in data:
            if obj.has_key('special'):
                geobaza.append(SpecialRange(obj))
            elif obj.has_key('parent'):
                # If object only contains link
                parent = obj['parent']
            else:
                inst = Geobaza.create_instance(obj)
                # Set attrs
                inst.query = geobaza.query
                # Add to objects list
                geobaza.append(inst)
                # Parent-child relations
                inst.set_parent(parent)
                if isinstance(parent, AbstractObject):
                    parent.child = inst
                parent = inst
        return geobaza

    def __repr__(self):
        items = list(self[:self.REPR_OUTPUT_SIZE])
        lst_repr = ', '.join([repr(item) for item in items])
        diff = len(self) - self.REPR_OUTPUT_SIZE
        if diff > 0:
            lst_repr = '[%s, +%d objects]' % (lst_repr, diff)
        else:
            lst_repr = '[%s]' % lst_repr
        return '<Geobaza: %s>' % lst_repr

    @staticmethod
    def create_instance(obj, child=None, parent=None):
        type_map = {
            GeobazaObject.COUNTRY: Country,
            GeobazaObject.REGION: Region,
            GeobazaObject.LOCALITY: Locality
        }

        type = obj.get('type')
        ItemClass = type_map[type]
        item = ItemClass(obj)
        item.child = child
        if parent:
            item.set_parent(parent)
        return item

    def _add_by_type(self, obj):
        if obj.type == GeobazaObject.COUNTRY:
            self.country = obj
        elif obj.type == GeobazaObject.REGION:
            self._regions.append(obj)
        elif obj.type == GeobazaObject.LOCALITY:
            self._localities.append(obj)

    def name_path(self, delimiter=u', '):
        if not self.is_special:
            return delimiter.join([obj.name for obj in self])

    def name_list(self):
        if not self.is_special:
            return [obj.name for obj in self]

    def id_list(self):
        if not self.is_special:
            return [obj.id for obj in self]

    @property
    def regions(self):
        return Geobaza.from_list(self._regions, **self._kwargs)

    @property
    def localities(self):
        return Geobaza.from_list(self._localities, **self._kwargs)

    @property
    def first(self):
        try:
            return self[0]
        except IndexError:
            pass

    @property
    def last(self):
        return self[-1]

    def as_xml(self):
        # Headers
        root = etree.Element('geobaza')
        root.set('api-version', to_str(self.headers.api_version))
        root.set('release', self.headers.release)
        root.set('build-timestamp', to_str(self.headers.build_timestamp))
        root.set('build-date', to_str(self.headers.build_date))
        root.set('lite', to_str(self.headers.lite))
        root.set('query-timestamp', to_str(time()))
        root.set('ip', self.ip)
        root.set('is-special', to_str(self.is_special))

        # Objects
        objects = etree.SubElement(root, 'objects')
        for item in self:
            objects.append(item.as_xml())

        return root

    def as_dict(self):
        # Headers
        object = {
            'api_version': self.headers.api_version,
            'release': self.headers.release,
            'build_timestamp': self.headers.build_timestamp,
            'build_date': to_str(self.headers.build_date),
            'lite': to_str(self.headers.lite),
            'query_timestamp': time(),
            'ip': self.ip,
            'is_special': to_str(self.is_special),
        }

        # Objects
        object['objects'] = [item.as_dict() for item in self]

        return object

class GeobazaQuery(object):
    _level = []

    def __init__(self, path=FILE_PATH, cache=NO_CACHE):
        if cache == MEMORY_CACHE:
            self.f = BinaryBlob(path)
        else:
            self.f = BinaryFile(path, 'rb')

        # Signature
        if self.f.read(7) != 'GEOBAZA':
            raise GeobazaException('Invalid datafile signature!')

        # Headers
        headers = json.loads(self.f.read(self.f.unpack(2, '!H')))
        headers['build_date'] = dt.fromtimestamp(headers['build_timestamp'])
        headers['lite'] = headers.has_key('lite')
        headers['release'] = headers['release'].strip()
        self.headers = Headers(**headers)

        while True:
            data = self.f.unpack(1, 'B')
            hi = (data >> 4) & 0x0f
            self._level.append(hi)
            if hi == 0:
                break
            lo = data & 0x0f
            self._level.append(lo)
            if lo == 0:
                break

        self.offset = self.f.tell()

    def _ip2long(self, ip):
        try:
            return struct.unpack_from('!L', socket.inet_aton(ip))[0]
        except socket.error:
            raise GeobazaIPException('Given IP-adress is invalid!')

    def _get_json(self, offset, path=False):
        json_str = []
        while offset:
            length = self._get_length(offset)
            if length:
                obj_str = self.f.read(length)
                offset = self.f.unpack(4, '!L')
                json_str.append(obj_str.decode('utf-8'))
                if not path:
                    length = self._get_length(offset)
                    if length and offset > 0:
                        json_str.append('{"parent": %d}' % offset)
                    break
            else:
                return
        return u'[%s]' % u','.join(json_str)

    def _get_length(self, offset):
        position = offset & 0x7fffffff
        self.f.seek(position, 0)
        return self.f.unpack(2, '!H') & 0xffff

    def _get(self, ip, path=False):
        self._validate_ip(ip)
        offset = self.offset
        ip_int = self._ip2long(ip)
        shift = 32
        for i in xrange(len(self._level)):
            shift -= self._level[i]
            index = ((ip_int >> shift)) & ((1 << self._level[i]) - 1)
            tell = offset + index * 4
            self.f.seek(tell, 0)
            unpack = self.f.unpack(4, '!L')
            offset = unpack & 0xffffffff
            if offset & 0x80000000:
                return self._get_json(offset, path=path)

    def _validate_ip(self, ip):
        if not validate_ip(ip):
            raise GeobazaIPException('Given IP-adress is invalid!')

    def get(self, ip):
        json_str = self._get(ip)
        if json_str:
            geobaza = Geobaza.from_dict(json.loads(json_str), query=self, headers=self.headers, ip=ip)
            return geobaza.first

    def get_list(self, ip):
        json_str = self._get(ip)
        if json_str:
            return Geobaza.from_dict(json.loads(json_str), query=self, headers=self.headers, ip=ip)

    def get_path(self, ip):
        json_str = self._get(ip, path=True)
        if json_str:
            return Geobaza.from_dict(json.loads(json_str), query=self, headers=self.headers, ip=ip)

    def get_from(self, offset):
        json_str = self._get_json(offset)
        if json_str:
            return json.loads(json_str)

def lookup(ip):
    try:
        geobaza = GeobazaQuery()
        return geobaza.get_path(ip)
    except GeobazaException:
        pass

def lookup_xml(ip, pretty=False):
    geobaza = lookup(ip)
    if geobaza:
        if pretty:
            return geobaza.to_pretty_xml()
        return geobaza.to_xml()

def lookup_json(ip, pretty=False):
    geobaza = lookup(ip)
    if geobaza:
        if pretty:
            return geobaza.to_pretty_json()
        return geobaza.to_json()
