├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ip2geotools ├── __init__.py ├── __main__.py ├── cli.py ├── databases │ ├── __init__.py │ ├── commercial.py │ ├── interfaces.py │ └── noncommercial.py ├── errors.py └── models.py ├── requirements.txt ├── setup.py └── tests └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Visual Studio Code 107 | .vscode/ 108 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.1.6 - 24-Aug-2021 2 | ------------------- 3 | 4 | * Fix when some geolocation information is not provided by database provider 5 | 6 | 0.1.5 - 15-Apr-2019 7 | ------------------- 8 | 9 | * Fix ``ip2geotools.databases.commercial.NeustarWeb`` because of new URL 10 | * Updated ``requirements.txt`` 11 | 12 | 0.1.4 - 20-Feb-2019 13 | ------------------- 14 | 15 | * Fix ``ip2geotools.databases.commercial.Ip2LocationWeb`` by using ``selenium`` with Firefox because of new webpage layout 16 | * Better exception handling in ``ip2geotools.databases.noncommercial.Ipstack`` 17 | 18 | 0.1.3 - 27-Nov-2018 19 | ------------------- 20 | 21 | * New ``ip2geotools.databases.commercial.Ipdata`` requested by Jonathan Kosgei from ipdata.co 22 | * New ``ip2geotools.databases.noncommercial.Ipstack`` as a replacement for ``ip2geotools.databases.noncommercial.Freegeoip`` 23 | * Fix ``ip2geotools.databases.commercial.DbIpWeb`` because of new webpage layout 24 | * Fix ``ip2geotools.databases.commercial.Ip2LocationWeb`` because of new webpage layout 25 | * Fix ``ip2geotools.databases.commercial.NeustarWeb`` because of new URL 26 | * Default free api key in ``ip2geotools.databases.noncommercial.DbIpCity`` 27 | * ``ip2geotools.databases.noncommercial.Freegeoip`` is deprecated! 28 | 29 | 0.1.2 - 30-Nov-2017 30 | ------------------- 31 | 32 | * Fix ``ip2geotools.databases.commercial.DbIpWeb`` because of new webpage layout 33 | * "IP address not found" error can be recognized in ``ip2geotools.databases.commercial.SkyhookContextAcceleratorIp`` 34 | * Custom exceptions can be formatted in ``ip2geotools.errors`` 35 | 36 | 0.1.1 - 01-Nov-2017 37 | ------------------- 38 | 39 | * Fix installation from PyPi using ``pip`` 40 | 41 | 0.1 - 01-Nov-2017 42 | ----------------- 43 | 44 | * First release 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tomas Caha, tomas-net at seznam dot cz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE CHANGELOG.rst requirements.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | ip2geotools 3 | =========== 4 | 5 | Description 6 | ----------- 7 | 8 | ``ip2geotools`` is a simple tool for getting geolocation information on given IP address from various geolocation databases. This package provides an API for several geolocation databases. 9 | 10 | Installation 11 | ------------ 12 | 13 | To install the ``ip2geotools`` module, type: 14 | 15 | .. code-block:: bash 16 | 17 | $ pip install ip2geotools 18 | 19 | Basic usage 20 | ----------- 21 | 22 | .. code-block:: pycon 23 | 24 | >>> from ip2geotools.databases.noncommercial import DbIpCity 25 | >>> response = DbIpCity.get('147.229.2.90', api_key='free') 26 | >>> response.ip_address 27 | '147.229.2.90' 28 | >>> response.city 29 | 'Brno (Brno střed)' 30 | >>> response.region 31 | 'South Moravian' 32 | >>> response.country 33 | 'CZ' 34 | >>> response.latitude 35 | 49.1926824 36 | >>> response.longitude 37 | 16.6182105 38 | >>> response.to_json() 39 | '{"ip_address": "147.229.2.90", "city": "Brno (Brno střed)", "region": "South Moravian", "country": "CZ", "latitude": 49.1926824, "longitude": 16.6182105}' 40 | >>> response.to_xml() 41 | '147.229.2.90Brno (Brno střed)South MoravianCZ49.192682416.6182105' 42 | >>> response.to_csv(',') 43 | '147.229.2.90,Brno (Brno střed),South Moravian,CZ,49.1926824,16.6182105' 44 | 45 | Command-line usage 46 | ------------------ 47 | 48 | When installed, you can invoke ``ip2geotools`` from the command-line: 49 | 50 | .. code:: bash 51 | 52 | ip2geotools [-h] -d {dbipcity,hostip,freegeoip,ipstack,maxmindgeolite2city,ip2location,dbipweb,maxmindgeoip2city,ip2locationweb,neustarweb,geobytescitydetails,skyhookcontextacceleratorip,ipinfo,eurek,ipdata} 53 | [--api_key API_KEY] [--db_path DB_PATH] [-u USERNAME] 54 | [-p PASSWORD] [-f {json,xml,csv-space,csv-tab,inline}] [-v] 55 | IP_ADDRESS 56 | 57 | Where: 58 | 59 | * ``ip2geotools``: is the script when installed in your environment, in development you could use ``python -m ip2geotools`` instead 60 | 61 | * ``IP_ADDRESS``: IP address to be checked 62 | 63 | * ``-h``, ``--help``: show help message and exit 64 | 65 | * ``-d {dbipcity,hostip,...,ipdata}``: geolocation database to be used (case insesitive) 66 | 67 | * ``--api_key API_KEY``: API key for given geolocation database (if needed) 68 | 69 | * ``--db_path DB_PATH``: path to geolocation database file (if needed) 70 | 71 | * ``-u USERNAME``, ``--username USERNAME``: username for accessing given geolocation database (if needed) 72 | 73 | * ``-p PASSWORD``, ``--password PASSWORD``: password for accessing given geolocation database (if needed) 74 | 75 | * ``-f {json,xml,csv-space,csv-tab,inline}``, ``--format {json,xml,csv-space,csv-tab,inline}``: output data format 76 | 77 | * ``-v``, ``--version``: show program's version number and exit 78 | 79 | Examples: 80 | 81 | .. code:: bash 82 | 83 | $ ip2geotools 147.229.2.90 -d dbipcity -f json 84 | {"ip_address": "147.229.2.90", "city": "Brno (Brno střed)", "region": "South Moravian", "country": "CZ", "latitude": 49.1926824, "longitude": 16.6182105} 85 | 86 | Models 87 | ------ 88 | 89 | This module contains models for the data returned by geolocation databases 90 | and these models are also used for comparison of given and provided data. 91 | 92 | ``ip2geotools.models.IpLocation`` 93 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 94 | Model for storing location of given IP address. 95 | 96 | Attributes: 97 | 98 | * ``ip_address``: IP address 99 | * ``city``: city where IP address is located 100 | * ``region``: region where IP address is located 101 | * ``country``: country where IP address is located (two letters country code) 102 | * ``latitude``: latitude where IP address is located 103 | * ``longitude``: longitude where IP address is located 104 | 105 | Methods: 106 | 107 | * ``to_json``: returns model data in JSON format 108 | * ``to_xml``: returns model data in XML format (root element: ``ip_location``) 109 | * ``to_csv``: returns model data in CSV format separated by given delimiter 110 | * ``__str__``: internal string representation of model, every single information on new line 111 | 112 | Exceptions 113 | ---------- 114 | 115 | This module provides special exceptions used when accessing data from 116 | third-party geolocation databases. 117 | 118 | * ``ip2geotools.errors.LocationError``: a generic location error 119 | * ``ip2geotools.errors.IpAddressNotFoundError``: the IP address was not found 120 | * ``ip2geotools.errors.PermissionRequiredError``: problem with authentication or authorization of the request; check your permission for accessing the service 121 | * ``ip2geotools.errors.InvalidRequestError``: invalid request 122 | * ``ip2geotools.errors.InvalidResponseError``: invalid response 123 | * ``ip2geotools.errors.ServiceError``: response from geolocation database is invalid (not accessible, etc.) 124 | * ``ip2geotools.errors.LimitExceededError``: limits of geolocation database have been reached 125 | 126 | Databases 127 | --------- 128 | 129 | Following classes access many different noncommercial and commercial geolocation databases using defined interface. 130 | 131 | ``ip2geotools.databases.interfaces`` 132 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 133 | 134 | * ``IGeoIpDatabase``: interface for unified access to the data provided by various geolocation databases 135 | 136 | ``ip2geotools.databases.noncommercial`` 137 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 138 | 139 | * ``DbIpCity``: https://db-ip.com/api/ 140 | * ``HostIP``: http://hostip.info/ 141 | * ``Freegeoip``: http://freegeoip.net/ **Database is deprecated!** 142 | * ``Ipstack``: https://ipstack.com/ 143 | * ``MaxMindGeoLite2City``: https://dev.maxmind.com/geoip/geoip2/geolite2/ 144 | * ``Ip2Location``: https://lite.ip2location.com/database/ip-country-region-city-latitude-longitude 145 | 146 | ``ip2geotools.databases.commercial`` 147 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 148 | * ``DbIpWeb``: https://db-ip.com/ 149 | * ``MaxMindGeoIp2City``: https://www.maxmind.com/ 150 | * ``Ip2LocationWeb``: https://www.ip2location.com/ 151 | * ``NeustarWeb``: https://www.neustar.biz/resources/tools/ip-geolocation-lookup-tool/ 152 | * ``GeobytesCityDetails``: http://geobytes.com/get-city-details-api/ 153 | * ``SkyhookContextAcceleratorIp``: http://www.skyhookwireless.com/ 154 | * ``IpInfo``: https://ipinfo.io/ 155 | * ``Eurek``: https://www.eurekapi.com/ 156 | * ``Ipdata``: https://ipdata.co/ 157 | 158 | Requirements 159 | ------------ 160 | 161 | This code requires Python 3.3+ and several other packages listed in ``requirements.txt``. 162 | 163 | Support 164 | ------- 165 | 166 | Please report all issues with this code using the `GitHub issue tracker 167 | `_ 168 | 169 | License 170 | ------- 171 | 172 | ``ip2geotools`` is released under the MIT License. See the bundled `LICENSE`_ file for details. 173 | 174 | Author 175 | ------ 176 | 177 | ``ip2geotools`` was written by Tomas Caha / at `FEEC `_ `BUT `_. -------------------------------------------------------------------------------- /ip2geotools/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | 4 | __title__ = 'ip2geotools' 5 | __description__ = 'Simple tool for getting geolocation information on ' + \ 6 | 'given IP address from various geolocation databases.' 7 | __version__ = '0.1.6' 8 | __author__ = 'Tomas Caha' 9 | __author_email__ = 'tomas-net@seznam.cz' 10 | __url__ = 'https://github.com/tomas-net/ip2geotools' 11 | __license__ = 'MIT License' 12 | __copyright__ = 'Copyright (c) 2021 Tomas Caha' 13 | -------------------------------------------------------------------------------- /ip2geotools/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | 4 | if __name__ == '__main__': 5 | from ip2geotools.cli import execute_from_command_line 6 | execute_from_command_line() 7 | -------------------------------------------------------------------------------- /ip2geotools/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Cli 4 | ====== 5 | 6 | This module and its class `Command` and function `execute_from_command_line` 7 | handle running basic functions of `ip2geotools` from command line interface. 8 | 9 | """ 10 | # pylint: disable=invalid-name 11 | 12 | from __future__ import print_function 13 | import argparse 14 | import codecs 15 | import os 16 | import sys 17 | import json 18 | import dicttoxml 19 | 20 | import ip2geotools 21 | from ip2geotools.databases.noncommercial import DbIpCity, \ 22 | HostIP, \ 23 | Freegeoip, \ 24 | Ipstack, \ 25 | MaxMindGeoLite2City, \ 26 | Ip2Location 27 | from ip2geotools.databases.commercial import DbIpWeb, \ 28 | MaxMindGeoIp2City, \ 29 | Ip2LocationWeb, \ 30 | NeustarWeb, \ 31 | GeobytesCityDetails, \ 32 | SkyhookContextAcceleratorIp, \ 33 | IpInfo, \ 34 | Eurek, \ 35 | Ipdata 36 | from ip2geotools.models import IpLocation 37 | from ip2geotools.errors import LocationError 38 | 39 | 40 | class Command(object): 41 | """ 42 | Class for running ip2geotools from cli. 43 | 44 | """ 45 | 46 | def __init__(self, argv=None): 47 | self.argv = argv or sys.argv[:] 48 | self.prog_name = os.path.basename(self.argv[0]) 49 | 50 | def execute(self): 51 | """ 52 | Given the command-line arguments, this creates a parser appropriate 53 | to that command and runs it. 54 | 55 | """ 56 | 57 | # args parser 58 | parser = argparse.ArgumentParser( 59 | prog=self.prog_name, 60 | description='{0} version {1}'.format(self.prog_name, ip2geotools.__version__) + \ 61 | '\n\n{0}'.format(ip2geotools.__description__), 62 | epilog=('\n\nexample:' + \ 63 | '\n get information on 147.229.2.90 from DB-IP API in JSON format' + \ 64 | '\n {prog_name} 147.229.2.90 -d dbipcity -f json' + \ 65 | '\n\nauthor:' + \ 66 | '\n {prog_name} was written by {author} <{author_email}> / ' + \ 67 | ' at FEEC BUT').format( 68 | prog_name=self.prog_name, 69 | author=ip2geotools.__author__, 70 | author_email=ip2geotools.__author_email__), 71 | formatter_class=argparse.RawDescriptionHelpFormatter, 72 | add_help=True) 73 | 74 | parser.add_argument('IP_ADDRESS', 75 | help='IP address to be checked') 76 | 77 | parser.add_argument('-d', '--database', 78 | help='geolocation database to be used (case insesitive)', 79 | dest='database', 80 | required=True, 81 | type=str.lower, 82 | choices=[ 83 | #'all', 84 | 85 | # noncommercial 86 | 'dbipcity', 87 | 'hostip', 88 | 'freegeoip', 89 | 'ipstack', 90 | 'maxmindgeolite2city', 91 | 'ip2location', 92 | 93 | # commercial 94 | 'dbipweb', 95 | 'maxmindgeoip2city', 96 | 'ip2locationweb', 97 | 'neustarweb', 98 | 'geobytescitydetails', 99 | 'skyhookcontextacceleratorip', 100 | 'ipinfo', 101 | 'eurek', 102 | 'ipdata', 103 | ]) 104 | 105 | parser.add_argument('--api_key', 106 | help='API key for given geolocation database (if needed)', 107 | dest='api_key') 108 | 109 | parser.add_argument('--db_path', 110 | help='path to geolocation database file (if needed)', 111 | dest='db_path') 112 | 113 | parser.add_argument('-u', '--username', 114 | help='username for accessing given geolocation database (if needed)', 115 | dest='username') 116 | 117 | parser.add_argument('-p', '--password', 118 | help='password for accessing given geolocation database (if needed)', 119 | dest='password') 120 | 121 | parser.add_argument('-f', '--format', 122 | help='output data format', 123 | dest='format', 124 | required=False, 125 | default='inline', 126 | type=str.lower, 127 | choices=[ 128 | 'json', 129 | 'xml', 130 | 'csv-space', 131 | 'csv-tab', 132 | 'inline' 133 | ]) 134 | 135 | parser.add_argument('-v', '--version', 136 | action='version', 137 | version='%(prog)s {0}'.format(ip2geotools.__version__)) 138 | 139 | # parse cli arguments 140 | arguments = parser.parse_args(self.argv[1:]) 141 | 142 | # process requests 143 | ip_location = IpLocation('0.0.0.0') 144 | 145 | try: 146 | # noncommercial databases 147 | if arguments.database == 'dbipcity': 148 | if arguments.api_key: 149 | ip_location = DbIpCity.get(arguments.IP_ADDRESS, 150 | api_key=arguments.api_key) 151 | else: 152 | ip_location = DbIpCity.get(arguments.IP_ADDRESS) 153 | elif arguments.database == 'hostip': 154 | ip_location = HostIP.get(arguments.IP_ADDRESS) 155 | elif arguments.database == 'freegeoip': 156 | ip_location = Freegeoip.get(arguments.IP_ADDRESS) 157 | elif arguments.database == 'ipstack': 158 | ip_location = Ipstack.get(arguments.IP_ADDRESS, 159 | api_key=arguments.api_key) 160 | elif arguments.database == 'maxmindgeolite2city': 161 | ip_location = MaxMindGeoLite2City.get(arguments.IP_ADDRESS, 162 | db_path=arguments.db_path) 163 | elif arguments.database == 'ip2location': 164 | ip_location = Ip2Location.get(arguments.IP_ADDRESS, 165 | db_path=arguments.db_path) 166 | 167 | # commercial databases 168 | elif arguments.database == 'dbipweb': 169 | ip_location = DbIpWeb.get(arguments.IP_ADDRESS) 170 | elif arguments.database == 'maxmindgeoip2city': 171 | ip_location = MaxMindGeoIp2City.get(arguments.IP_ADDRESS) 172 | elif arguments.database == 'ip2locationweb': 173 | ip_location = Ip2LocationWeb.get(arguments.IP_ADDRESS) 174 | elif arguments.database == 'neustarweb': 175 | ip_location = NeustarWeb.get(arguments.IP_ADDRESS) 176 | elif arguments.database == 'geobytescitydetails': 177 | ip_location = GeobytesCityDetails.get(arguments.IP_ADDRESS) 178 | elif arguments.database == 'skyhookcontextacceleratorip': 179 | ip_location = SkyhookContextAcceleratorIp.get(arguments.IP_ADDRESS, 180 | username=arguments.username, 181 | password=arguments.password) 182 | elif arguments.database == 'ipinfo': 183 | ip_location = IpInfo.get(arguments.IP_ADDRESS) 184 | elif arguments.database == 'eurek': 185 | ip_location = Eurek.get(arguments.IP_ADDRESS, 186 | api_key=arguments.api_key) 187 | elif arguments.database == 'ipdata': 188 | if arguments.api_key: 189 | ip_location = Ipdata.get(arguments.IP_ADDRESS, 190 | api_key=arguments.api_key) 191 | else: 192 | ip_location = Ipdata.get(arguments.IP_ADDRESS) 193 | 194 | # print formatted output 195 | if arguments.format == 'json': 196 | print(ip_location.to_json()) 197 | elif arguments.format == 'xml': 198 | print(ip_location.to_xml()) 199 | elif arguments.format == 'csv-space': 200 | print(ip_location.to_csv(' ')) 201 | elif arguments.format == 'csv-tab': 202 | print(ip_location.to_csv('\t')) 203 | elif arguments.format == 'inline': 204 | print(ip_location) 205 | except LocationError as e: 206 | # print formatted output 207 | if arguments.format == 'json': 208 | print(e.to_json()) 209 | elif arguments.format == 'xml': 210 | print(e.to_xml()) 211 | elif arguments.format == 'csv-space': 212 | print(e.to_csv(' ')) 213 | elif arguments.format == 'csv-tab': 214 | print(e.to_csv('\t')) 215 | elif arguments.format == 'inline': 216 | print('%s: %s' % (type(e).__name__, e.__str__())) 217 | 218 | 219 | def execute_from_command_line(argv=None): 220 | """ 221 | A simple method that runs a Command. 222 | 223 | """ 224 | 225 | if sys.stdout.encoding is None: 226 | sys.stdout = codecs.getwriter('utf8')(sys.stdout) 227 | 228 | if sys.version_info.major != 3: 229 | print('Error: This script is intended to be run with Python 3', file=sys.stderr) 230 | sys.exit(1) 231 | 232 | command = Command(argv) 233 | command.execute() 234 | -------------------------------------------------------------------------------- /ip2geotools/databases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomas-net/ip2geotools/9b94b4ff175f2ae5db4dea3b394cc66f3fd10208/ip2geotools/databases/__init__.py -------------------------------------------------------------------------------- /ip2geotools/databases/commercial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Commercial geolocation databases 4 | =================================== 5 | 6 | These classes access many different commercial geolocation databases. 7 | 8 | """ 9 | # pylint: disable=line-too-long,invalid-name,W0702 10 | from __future__ import absolute_import 11 | import json 12 | from urllib.parse import quote 13 | import re 14 | import requests 15 | from requests.auth import HTTPBasicAuth 16 | import pyquery 17 | from selenium import webdriver # selenium for Ip2LocationWeb 18 | from selenium.webdriver.firefox.options import Options 19 | from selenium.webdriver.common.by import By 20 | from selenium.webdriver.support.ui import WebDriverWait 21 | from selenium.webdriver.support import expected_conditions as EC 22 | 23 | from ip2geotools.databases.interfaces import IGeoIpDatabase 24 | from ip2geotools.models import IpLocation 25 | from ip2geotools.errors import IpAddressNotFoundError, PermissionRequiredError, \ 26 | InvalidRequestError, InvalidResponseError, \ 27 | ServiceError, LimitExceededError 28 | 29 | 30 | class DbIpWeb(IGeoIpDatabase): 31 | """ 32 | Class for accessing geolocation data provided by searching directly 33 | on https://db-ip.com/. 34 | 35 | """ 36 | 37 | @staticmethod 38 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 39 | # process request 40 | try: 41 | request = requests.post('https://db-ip.com/', 42 | headers={'User-Agent': 'Mozilla/5.0'}, 43 | data=[('address', ip_address)], 44 | timeout=62) 45 | except: 46 | raise ServiceError() 47 | 48 | # check for HTTP errors 49 | if request.status_code != 200: 50 | raise ServiceError() 51 | 52 | # check for errors 53 | if b'you have exceeded the daily query limit' in request.content.lower(): 54 | raise LimitExceededError() 55 | 56 | # parse content 57 | try: 58 | content = request.content.decode('utf-8') 59 | pq = pyquery.PyQuery(content) 60 | parsed_ip = pq('html > body div.container > h1') \ 61 | .remove('span') \ 62 | .text() \ 63 | .strip() 64 | parsed_country = pq('html > body > div.container table tr:contains("Country") td') \ 65 | .text() \ 66 | .strip() 67 | parsed_region = pq('html > body > div.container table tr:contains("State / Region") td') \ 68 | .text() \ 69 | .strip() 70 | parsed_city = pq('html > body > div.container table tr:contains("City") td') \ 71 | .text() \ 72 | .strip() 73 | parsed_coords = pq('html > body > div.container table tr:contains("Coordinates") td') \ 74 | .text() \ 75 | .strip() \ 76 | .split(',') 77 | except: 78 | raise InvalidResponseError() 79 | 80 | # check for errors 81 | if ip_address != parsed_ip: 82 | raise IpAddressNotFoundError(ip_address) 83 | 84 | # prepare return value 85 | ip_location = IpLocation(ip_address) 86 | 87 | # format data 88 | try: 89 | ip_location.country = parsed_country 90 | ip_location.region = parsed_region 91 | ip_location.city = parsed_city 92 | ip_location.latitude = float(parsed_coords[0].strip()) 93 | ip_location.longitude = float(parsed_coords[1].strip()) 94 | except: 95 | ip_location.country = None 96 | ip_location.region = None 97 | ip_location.city = None 98 | ip_location.latitude = None 99 | ip_location.longitude = None 100 | 101 | return ip_location 102 | 103 | 104 | class MaxMindGeoIp2City(IGeoIpDatabase): 105 | """ 106 | Class for accessing geolocation data provided by GeoIP2 database 107 | created by MaxMind, available from https://www.maxmind.com/. 108 | 109 | """ 110 | 111 | @staticmethod 112 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 113 | # process request 114 | try: 115 | # optional auth for increasing amount of queries per day 116 | if username != None and password != None: 117 | auth = HTTPBasicAuth(username, password) 118 | else: 119 | auth = None 120 | 121 | request = requests.get('https://www.maxmind.com/geoip/v2.1/city/' 122 | + quote(ip_address) 123 | + ('?demo=1' if auth == None else ''), 124 | auth=auth, 125 | timeout=62) 126 | except: 127 | raise ServiceError() 128 | 129 | # parse content 130 | try: 131 | content = request.content.decode('utf-8') 132 | content = json.loads(content) 133 | except: 134 | raise InvalidResponseError() 135 | 136 | # check for HTTP errors 137 | if request.status_code != 200: 138 | if request.status_code == 400: 139 | raise InvalidRequestError(content['code']) 140 | elif request.status_code == 401: 141 | raise PermissionRequiredError(content['code']) 142 | elif request.status_code == 402: 143 | raise LimitExceededError(content['code']) 144 | elif request.status_code == 403: 145 | raise PermissionRequiredError(content['code']) 146 | elif request.status_code == 404: 147 | raise IpAddressNotFoundError(ip_address) 148 | elif request.status_code == 500: 149 | raise InvalidRequestError() 150 | else: 151 | raise ServiceError() 152 | 153 | # prepare return value 154 | ip_location = IpLocation(ip_address) 155 | 156 | # format data 157 | if content.get('country'): 158 | ip_location.country = content['country'].get('iso_code') 159 | else: 160 | ip_location.country = None 161 | 162 | if content.get('subdivisions'): 163 | if content['subdivisions'][0].get('names'): 164 | ip_location.region = content['subdivisions'][0]['names'].get('en') 165 | else: 166 | ip_location.region = None 167 | else: 168 | ip_location.region = None 169 | 170 | if content.get('city'): 171 | if content['city'].get('names'): 172 | ip_location.city = content['city']['names'].get('en') 173 | else: 174 | ip_location.city = None 175 | else: 176 | ip_location.city = None 177 | 178 | if content.get('location'): 179 | ip_location.latitude = float(content['location']['latitude']) 180 | ip_location.longitude = float(content['location']['longitude']) 181 | else: 182 | ip_location.latitude = None 183 | ip_location.longitude = None 184 | 185 | return ip_location 186 | 187 | 188 | class Ip2LocationWeb(IGeoIpDatabase): 189 | """ 190 | Class for accessing geolocation data provided by searching directly 191 | on https://www.ip2location.com/. 192 | 193 | """ 194 | 195 | @staticmethod 196 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 197 | # initiate headless Firefox using selenium to pass through Google reCAPTCHA 198 | options = Options() 199 | options.headless = True 200 | browser = webdriver.Firefox(options=options) 201 | 202 | try: 203 | browser.get('http://www.ip2location.com/demo/' + ip_address) 204 | element = WebDriverWait(browser, 30).until( 205 | EC.presence_of_element_located((By.NAME, 'ipAddress')) 206 | ) 207 | 208 | if not element: 209 | raise Exception 210 | except: 211 | raise ServiceError() 212 | 213 | # parse current limit 214 | current_limit = 0 215 | body = browser.find_element_by_tag_name('body').text 216 | 217 | try: 218 | limit = re.search(r'You still have.*?([\d]{1,2})/50.* query limit', 219 | body, 220 | re.DOTALL) 221 | 222 | if limit != None: 223 | current_limit = int(limit.group(1)) 224 | except: 225 | raise InvalidResponseError() 226 | 227 | # check if limit is exceeded 228 | if current_limit == 0: 229 | raise LimitExceededError() 230 | 231 | # parse content 232 | try: 233 | table = browser.find_element_by_xpath('//table[contains(.,"Permalink")]') 234 | 235 | parsed_ip = table.find_element_by_xpath('//tr[contains(.,"IP Address")]/td').text.strip() 236 | parsed_country = [class_name.replace('flag-icon-', '').upper() for class_name in table.find_element_by_class_name('flag-icon').get_attribute('class').split(' ') if class_name.startswith('flag-icon-')][0] 237 | parsed_region = table.find_element_by_xpath('//tr[contains(.,"Region")]/td').text.strip() 238 | parsed_city = table.find_element_by_xpath('//tr[contains(.,"City")]/td').text.strip() 239 | parsed_coords = table.find_element_by_xpath('//tr[contains(.,"Coordinates of City")]/td').text.strip() 240 | except: 241 | raise InvalidResponseError() 242 | 243 | # exit headless firefox 244 | browser.quit() 245 | 246 | # check for errors 247 | if ip_address != parsed_ip: 248 | raise IpAddressNotFoundError(ip_address) 249 | 250 | # prepare return value 251 | ip_location = IpLocation(ip_address) 252 | 253 | # format data 254 | try: 255 | ip_location.country = parsed_country 256 | ip_location.region = parsed_region 257 | ip_location.city = parsed_city 258 | 259 | parsed_coords = parsed_coords.split('(')[0].split(',') 260 | ip_location.latitude = float(parsed_coords[0].strip()) 261 | ip_location.longitude = float(parsed_coords[1].strip()) 262 | except: 263 | ip_location.country = None 264 | ip_location.region = None 265 | ip_location.city = None 266 | ip_location.latitude = None 267 | ip_location.longitude = None 268 | 269 | return ip_location 270 | 271 | 272 | class NeustarWeb(IGeoIpDatabase): 273 | """ 274 | Class for accessing geolocation data provided by searching directly 275 | on https://www.home.neustar/resources/tools/ip-geolocation-lookup-tool/. 276 | 277 | """ 278 | 279 | @staticmethod 280 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 281 | # process request 282 | try: 283 | request = requests.post('https://www.home.neustar/resources/tools/ip-geolocation-lookup-tool', 284 | headers={'User-Agent': 'Mozilla/5.0'}, 285 | data=[('ip', ip_address)], 286 | timeout=62) 287 | except: 288 | raise ServiceError() 289 | 290 | # check for HTTP errors 291 | if request.status_code != 200: 292 | raise ServiceError() 293 | 294 | # check for errors 295 | if b'rate limit exceeded' in request.content.lower(): 296 | raise LimitExceededError() 297 | 298 | # parse content 299 | try: 300 | content = request.content.decode('utf-8') 301 | pq = pyquery.PyQuery(content) 302 | parsed_ip = pq('html > body > section.full.resource article h2 > strong') \ 303 | .text() \ 304 | .strip() 305 | parsed_country = pq('html > body > section.full.resource article div.data >table:first tr:contains("Country Code:") td:not(.item)') \ 306 | .text() \ 307 | .strip() \ 308 | .upper() 309 | parsed_region = pq('html > body > section.full.resource article div.data >table:first tr:contains("Region:") td:not(.item)') \ 310 | .text() \ 311 | .strip() \ 312 | .title() 313 | parsed_state = pq('html > body > section.full.resource article div.data >table:first tr:contains("State:") td:not(.item)') \ 314 | .text() \ 315 | .strip() \ 316 | .title() 317 | parsed_city = pq('html > body > section.full.resource article div.data >table:first tr:contains("City:") td:not(.item)') \ 318 | .text() \ 319 | .strip() \ 320 | .title() 321 | parsed_latitude = pq('html > body > section.full.resource article div.data >table:first tr:contains("Latitude:") td:not(.item)') \ 322 | .text() \ 323 | .strip() 324 | parsed_longitude = pq('html > body > section.full.resource article div.data >table:first tr:contains("Longitude:") td:not(.item)') \ 325 | .text() \ 326 | .strip() 327 | except: 328 | raise InvalidResponseError() 329 | 330 | # check for errors 331 | if ip_address != parsed_ip: 332 | raise IpAddressNotFoundError(ip_address) 333 | 334 | # prepare return value 335 | ip_location = IpLocation(ip_address) 336 | 337 | # format data 338 | try: 339 | ip_location.country = parsed_country 340 | 341 | if parsed_region is None: 342 | ip_location.region = parsed_region 343 | else: 344 | ip_location.region = parsed_state 345 | 346 | ip_location.city = parsed_city 347 | ip_location.latitude = float(parsed_latitude) 348 | ip_location.longitude = float(parsed_longitude) 349 | except: 350 | ip_location.country = None 351 | ip_location.region = None 352 | ip_location.city = None 353 | ip_location.latitude = None 354 | ip_location.longitude = None 355 | 356 | return ip_location 357 | 358 | 359 | class GeobytesCityDetails(IGeoIpDatabase): 360 | """ 361 | Class for accessing geolocation data provided by 362 | http://geobytes.com/get-city-details-api/. 363 | 364 | """ 365 | 366 | @staticmethod 367 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 368 | # process request 369 | try: 370 | request = requests.get('http://getcitydetails.geobytes.com/GetCityDetails?fqcn=' 371 | + quote(ip_address), 372 | timeout=62) 373 | except: 374 | raise ServiceError() 375 | 376 | # check for HTTP errors 377 | if request.status_code != 200: 378 | raise ServiceError() 379 | 380 | # parse content 381 | try: 382 | content = request.content.decode('latin-1') 383 | content = json.loads(content) 384 | except: 385 | raise InvalidResponseError() 386 | 387 | # prepare return value 388 | ip_location = IpLocation(ip_address) 389 | 390 | # format data 391 | ip_location.country = content.get('geobytesinternet') 392 | ip_location.region = content.get('geobytesregion') 393 | ip_location.city = content.get('geobytescity') 394 | 395 | if content.get('geobyteslatitude') and content.get('geobyteslongitude'): 396 | ip_location.latitude = float(content['geobyteslatitude']) 397 | ip_location.longitude = float(content['geobyteslongitude']) 398 | else: 399 | ip_location.latitude = None 400 | ip_location.longitude = None 401 | 402 | return ip_location 403 | 404 | 405 | class SkyhookContextAcceleratorIp(IGeoIpDatabase): 406 | """ 407 | Class for accessing geolocation data provided by http://www.skyhookwireless.com/. 408 | 409 | """ 410 | 411 | @staticmethod 412 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 413 | # process request 414 | try: 415 | request = requests.get('https://context.skyhookwireless.com/accelerator/ip?' 416 | + 'ip=' + quote(ip_address) 417 | + '&user=' + quote(username) 418 | + '&key=' + quote(password) 419 | + '&version=2.0', 420 | timeout=62) 421 | except: 422 | raise ServiceError() 423 | 424 | # check for HTTP errors 425 | if request.status_code != 200: 426 | if request.status_code == 400: 427 | raise InvalidRequestError() 428 | elif request.status_code == 401: 429 | raise PermissionRequiredError(ip_address) 430 | else: 431 | raise ServiceError() 432 | 433 | # content decode 434 | try: 435 | content = request.content.decode('utf-8') 436 | except: 437 | raise InvalidResponseError() 438 | 439 | # check for IP address not found error 440 | if content == '{"data":{"ip":"' + ip_address + '"}}': 441 | raise IpAddressNotFoundError(ip_address) 442 | 443 | # parse content 444 | try: 445 | content = json.loads(content) 446 | except: 447 | raise InvalidResponseError() 448 | 449 | # prepare return value 450 | ip_location = IpLocation(ip_address) 451 | 452 | # format data 453 | if content.get('data'): 454 | if content['data'].get('civic'): 455 | ip_location.country = content['data']['civic'].get('countryIso') 456 | ip_location.region = content['data']['civic'].get('state') 457 | ip_location.city = content['data']['civic'].get('city') 458 | else: 459 | ip_location.country = None 460 | ip_location.region = None 461 | ip_location.city = None 462 | 463 | if content['data'].get('location'): 464 | if content['data']['location'].get('latitude') \ 465 | and content['data']['location'].get('longitude'): 466 | ip_location.latitude = content['data']['location']['latitude'] 467 | ip_location.longitude = content['data']['location']['longitude'] 468 | else: 469 | ip_location.latitude = None 470 | ip_location.longitude = None 471 | else: 472 | ip_location.latitude = None 473 | ip_location.longitude = None 474 | else: 475 | ip_location.country = None 476 | ip_location.region = None 477 | ip_location.city = None 478 | ip_location.latitude = None 479 | ip_location.longitude = None 480 | 481 | return ip_location 482 | 483 | 484 | class IpInfo(IGeoIpDatabase): 485 | """ 486 | Class for accessing geolocation data provided by https://ipinfo.io/. 487 | 488 | """ 489 | 490 | @staticmethod 491 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 492 | # process request 493 | try: 494 | request = requests.get('https://ipinfo.io/' + quote(ip_address) + '/geo/', 495 | timeout=62) 496 | except: 497 | raise ServiceError() 498 | 499 | # check for HTTP errors 500 | if request.status_code != 200: 501 | if request.status_code == 404: 502 | raise IpAddressNotFoundError(ip_address) 503 | elif request.status_code == 429: 504 | raise LimitExceededError() 505 | elif request.status_code == 500: 506 | raise InvalidRequestError() 507 | else: 508 | raise ServiceError() 509 | 510 | # parse content 511 | try: 512 | content = request.content.decode('utf-8') 513 | content = json.loads(content) 514 | except: 515 | raise InvalidResponseError() 516 | 517 | # prepare return value 518 | ip_location = IpLocation(ip_address) 519 | 520 | # format data 521 | ip_location.country = content.get('country') 522 | ip_location.region = content.get('region') 523 | ip_location.city = content.get('city') 524 | 525 | if content.get('loc'): 526 | location = content['loc'].split(',') 527 | ip_location.latitude = float(location[0]) 528 | ip_location.longitude = float(location[1]) 529 | else: 530 | ip_location.latitude = None 531 | ip_location.longitude = None 532 | 533 | return ip_location 534 | 535 | 536 | class Eurek(IGeoIpDatabase): 537 | """ 538 | Class for accessing geolocation data provided by https://www.eurekapi.com/. 539 | 540 | """ 541 | 542 | @staticmethod 543 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 544 | # process request 545 | try: 546 | request = requests.get('https://https-api.eurekapi.com/iplocation/v1.8/locateip?' 547 | + 'ip=' + quote(ip_address) 548 | + '&key=' + quote(api_key) 549 | + '&format=JSON', 550 | timeout=62) 551 | except: 552 | raise ServiceError() 553 | 554 | # check for HTTP errors 555 | if request.status_code != 200: 556 | if request.status_code == 429: 557 | raise LimitExceededError() 558 | elif request.status_code == 500: 559 | raise InvalidRequestError() 560 | else: 561 | raise ServiceError() 562 | 563 | # parse content 564 | try: 565 | content = request.content.decode('utf-8') 566 | content = json.loads(content) 567 | except: 568 | raise InvalidResponseError() 569 | 570 | # prepare return value 571 | ip_location = IpLocation(ip_address) 572 | 573 | # check for errors 574 | if content['query_status']['query_status_code'] != 'OK': 575 | error_status = content['query_status']['query_status_code'] 576 | error_status_desc = content['query_status']['query_status_description'] 577 | 578 | if error_status == 'MISSING_SERVICE_ACCESS_KEY' \ 579 | or error_status == 'INVALID_SERVICE_ACCESS_KEY' \ 580 | or error_status == 'FREE_TRIAL_LICENSE_EXPIRED' \ 581 | or error_status == 'SUBSCRIPTION_EXPIRED': 582 | raise PermissionRequiredError(error_status_desc) 583 | elif error_status == 'MISSING_IP_ADDRESS' \ 584 | or error_status == 'INVALID_IP_ADDRESS': 585 | raise IpAddressNotFoundError(ip_address) 586 | else: 587 | ip_location.country = None 588 | ip_location.region = None 589 | ip_location.city = None 590 | ip_location.latitude = None 591 | ip_location.longitude = None 592 | return ip_location 593 | 594 | # format data 595 | if content.get('geolocation_data'): 596 | ip_location.country = content['geolocation_data'].get('country_code_iso3166alpha2') 597 | ip_location.region = content['geolocation_data'].get('region_name') 598 | ip_location.city = content['geolocation_data'].get('city') 599 | 600 | if content['geolocation_data'].get('latitude') \ 601 | and content['geolocation_data'].get('longitude'): 602 | ip_location.latitude = float(content['geolocation_data']['latitude']) 603 | ip_location.longitude = float(content['geolocation_data']['longitude']) 604 | else: 605 | ip_location.latitude = None 606 | ip_location.longitude = None 607 | else: 608 | ip_location.country = None 609 | ip_location.region = None 610 | ip_location.city = None 611 | ip_location.latitude = None 612 | ip_location.longitude = None 613 | 614 | return ip_location 615 | 616 | 617 | class Ipdata(IGeoIpDatabase): 618 | """ 619 | Class for accessing geolocation data provided by https://ipdata.co/. 620 | 621 | """ 622 | 623 | @staticmethod 624 | def get(ip_address, api_key='test', db_path=None, username=None, password=None): 625 | # process request 626 | try: 627 | request = requests.get('https://api.ipdata.co/' + quote(ip_address) 628 | + '?api-key=' + quote(api_key), 629 | timeout=62) 630 | except: 631 | raise ServiceError() 632 | 633 | # check for HTTP errors 634 | if request.status_code != 200 and request.status_code != 400: 635 | if request.status_code == 401: 636 | raise PermissionRequiredError() 637 | elif request.status_code == 403: 638 | raise LimitExceededError() 639 | else: 640 | raise ServiceError() 641 | 642 | # parse content 643 | try: 644 | content = request.content.decode('utf-8') 645 | content = json.loads(content) 646 | except: 647 | raise InvalidResponseError() 648 | 649 | # check for errors 650 | if content.get('message'): 651 | if 'private IP address' in content['message']: 652 | raise IpAddressNotFoundError(ip_address) 653 | else: 654 | raise InvalidRequestError() 655 | 656 | # prepare return value 657 | ip_location = IpLocation(ip_address) 658 | 659 | # format data 660 | if content['country_code'] == '': 661 | ip_location.country = None 662 | else: 663 | ip_location.country = content['country_code'] 664 | 665 | if content['region'] == '': 666 | ip_location.region = None 667 | else: 668 | ip_location.region = content['region'] 669 | 670 | if content['city'] == '': 671 | ip_location.city = None 672 | else: 673 | ip_location.city = content['city'] 674 | 675 | if content['latitude'] != '-' and content['longitude'] != '-': 676 | ip_location.latitude = float(content['latitude']) 677 | ip_location.longitude = float(content['longitude']) 678 | else: 679 | ip_location.latitude = None 680 | ip_location.longitude = None 681 | 682 | return ip_location 683 | 684 | -------------------------------------------------------------------------------- /ip2geotools/databases/interfaces.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Interfaces 4 | ========== 5 | 6 | These classes provide interfaces for unifying access to the data provided 7 | by geolocation databases. 8 | 9 | """ 10 | from abc import ABCMeta, abstractmethod 11 | 12 | 13 | class IGeoIpDatabase: 14 | """ 15 | Interface for unified access to the data provided by geolocation databases. 16 | 17 | """ 18 | 19 | __metaclass__ = ABCMeta 20 | 21 | @staticmethod 22 | @abstractmethod 23 | def get(ip_address, api_key, db_path, username, password): 24 | """ 25 | Method for getting location of given IP address. 26 | 27 | """ 28 | 29 | raise NotImplementedError 30 | 31 | -------------------------------------------------------------------------------- /ip2geotools/databases/noncommercial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Noncommercial geolocation databases 4 | =================================== 5 | 6 | These classes access many different free geolocation databases. 7 | 8 | """ 9 | # pylint: disable=no-member 10 | from __future__ import absolute_import 11 | import json 12 | from urllib.parse import quote 13 | import requests 14 | import geocoder 15 | import geoip2.database 16 | import IP2Location 17 | 18 | from ip2geotools.databases.interfaces import IGeoIpDatabase 19 | from ip2geotools.models import IpLocation 20 | from ip2geotools.errors import IpAddressNotFoundError, PermissionRequiredError, \ 21 | InvalidRequestError, InvalidResponseError, ServiceError, \ 22 | LimitExceededError 23 | 24 | 25 | class DbIpCity(IGeoIpDatabase): 26 | """ 27 | Class for accessing geolocation data provided by https://db-ip.com/api/. 28 | 29 | """ 30 | 31 | @staticmethod 32 | def get(ip_address, api_key='free', db_path=None, username=None, password=None): 33 | # process request 34 | try: 35 | request = requests.get('http://api.db-ip.com/v2/' 36 | + quote(api_key) 37 | + '/' + quote(ip_address), 38 | timeout=62) 39 | except: 40 | raise ServiceError() 41 | 42 | # check for HTTP errors 43 | if request.status_code != 200: 44 | raise ServiceError() 45 | 46 | # parse content 47 | try: 48 | content = request.content.decode('utf-8') 49 | content = json.loads(content) 50 | except: 51 | raise InvalidResponseError() 52 | 53 | # check for errors 54 | if content.get('error'): 55 | if content['error'] == 'invalid address': 56 | raise IpAddressNotFoundError(ip_address) 57 | elif content['error'] == 'invalid API key': 58 | raise PermissionRequiredError() 59 | else: 60 | raise InvalidRequestError() 61 | 62 | # prepare return value 63 | ip_location = IpLocation(ip_address) 64 | 65 | # format data 66 | ip_location.country = content.get('countryCode') 67 | ip_location.region = content.get('stateProv') 68 | ip_location.city = content.get('city') 69 | 70 | # get lat/lon from OSM 71 | osm = geocoder.osm(content.get('city', '') + ', ' 72 | + content.get('stateProv', '') + ' ' 73 | + content.get('countryCode', ''), 74 | timeout=62) 75 | 76 | if osm.ok: 77 | osm = osm.json 78 | ip_location.latitude = float(osm['lat']) 79 | ip_location.longitude = float(osm['lng']) 80 | else: 81 | osm = geocoder.osm(content.get('city', '') + ', ' + content.get('countryCode', ''), timeout=62) 82 | 83 | if osm.ok: 84 | osm = osm.json 85 | ip_location.latitude = float(osm['lat']) 86 | ip_location.longitude = float(osm['lng']) 87 | else: 88 | ip_location.latitude = None 89 | ip_location.longitude = None 90 | 91 | return ip_location 92 | 93 | 94 | class HostIP(IGeoIpDatabase): 95 | """ 96 | Class for accessing geolocation data provided by http://hostip.info/. 97 | 98 | """ 99 | 100 | @staticmethod 101 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 102 | # process request 103 | try: 104 | request = requests.get('http://api.hostip.info/get_json.php?position=true&ip=' 105 | + quote(ip_address), 106 | timeout=62) 107 | except: 108 | raise ServiceError() 109 | 110 | # check for HTTP errors 111 | if request.status_code != 200: 112 | if request.status_code == 404: 113 | raise IpAddressNotFoundError(ip_address) 114 | elif request.status_code == 500: 115 | raise InvalidRequestError() 116 | else: 117 | raise ServiceError() 118 | 119 | # parse content 120 | try: 121 | content = request.content.decode('utf-8') 122 | content = json.loads(content) 123 | except: 124 | raise InvalidResponseError() 125 | 126 | # prepare return value 127 | ip_location = IpLocation(ip_address) 128 | 129 | # format data 130 | if content.get('country_code'): 131 | if content['country_code'] == 'XX': 132 | ip_location.country = None 133 | else: 134 | ip_location.country = content['country_code'] 135 | else: 136 | ip_location.country = None 137 | 138 | ip_location.region = None 139 | 140 | if content.get('city'): 141 | if content['city'] == '(Unknown City?)' \ 142 | or content['city'] == '(Unknown city)' \ 143 | or content['city'] == '(Private Address)': 144 | ip_location.city = None 145 | else: 146 | ip_location.city = content['city'] 147 | else: 148 | ip_location.city = None 149 | 150 | if content.get('lat') and content.get('lng'): 151 | ip_location.latitude = float(content['lat']) 152 | ip_location.longitude = float(content['lng']) 153 | else: 154 | ip_location.latitude = None 155 | ip_location.longitude = None 156 | 157 | return ip_location 158 | 159 | 160 | class Freegeoip(IGeoIpDatabase): 161 | """ 162 | Class for accessing geolocation data provided by http://freegeoip.net/. 163 | Freegeoip database is deprecated! 164 | 165 | """ 166 | 167 | @staticmethod 168 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 169 | """ 170 | # process request 171 | try: 172 | request = requests.get('http://freegeoip.net/json/' + quote(ip_address), 173 | timeout=62) 174 | except: 175 | raise ServiceError() 176 | 177 | # check for HTTP errors 178 | if request.status_code != 200: 179 | if request.status_code == 404: 180 | raise IpAddressNotFoundError(ip_address) 181 | elif request.status_code == 500: 182 | raise InvalidRequestError() 183 | else: 184 | raise ServiceError() 185 | 186 | # parse content 187 | try: 188 | content = request.content.decode('utf-8') 189 | content = json.loads(content) 190 | except: 191 | raise InvalidResponseError() 192 | 193 | # prepare return value 194 | ip_location = IpLocation(ip_address) 195 | 196 | # format data 197 | if content['country_code'] == '': 198 | ip_location.country = None 199 | else: 200 | ip_location.country = content['country_code'] 201 | 202 | if content['region_name'] == '': 203 | ip_location.region = None 204 | else: 205 | ip_location.region = content['region_name'] 206 | 207 | if content['city'] == '': 208 | ip_location.city = None 209 | else: 210 | ip_location.city = content['city'] 211 | 212 | if content['latitude'] != '-' and content['longitude'] != '-': 213 | ip_location.latitude = float(content['latitude']) 214 | ip_location.longitude = float(content['longitude']) 215 | else: 216 | ip_location.latitude = None 217 | ip_location.longitude = None 218 | 219 | return ip_location 220 | 221 | """ 222 | raise ServiceError('Freegeoip database is deprecated!') 223 | 224 | 225 | class Ipstack(IGeoIpDatabase): 226 | """ 227 | Class for accessing geolocation data provided by http://ipstack.com/. 228 | 229 | """ 230 | 231 | @staticmethod 232 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 233 | # process request 234 | try: 235 | request = requests.get('http://api.ipstack.com/' + quote(ip_address) 236 | + '?access_key=' + quote(api_key), 237 | timeout=62) 238 | except: 239 | raise ServiceError() 240 | 241 | # check for HTTP errors 242 | if request.status_code != 200: 243 | raise ServiceError() 244 | 245 | # parse content 246 | try: 247 | content = request.content.decode('utf-8') 248 | content = json.loads(content) 249 | except: 250 | raise InvalidResponseError() 251 | 252 | # check for errors 253 | if content.get('error'): 254 | if content['error']['code'] == 101 \ 255 | or content['error']['code'] == 102 \ 256 | or content['error']['code'] == 105: 257 | raise PermissionRequiredError() 258 | elif content['error']['code'] == 104: 259 | raise LimitExceededError() 260 | else: 261 | raise InvalidRequestError() 262 | 263 | # prepare return value 264 | ip_location = IpLocation(ip_address) 265 | 266 | # format data 267 | ip_location.country = content.get('country_code') 268 | ip_location.region = content.get('region_name') 269 | ip_location.city = content.get('city') 270 | 271 | if content.get('latitude') and content.get('longitude'): 272 | if content['latitude'] != '-' and content['longitude'] != '-': 273 | ip_location.latitude = float(content['latitude']) 274 | ip_location.longitude = float(content['longitude']) 275 | else: 276 | ip_location.latitude = None 277 | ip_location.longitude = None 278 | else: 279 | ip_location.latitude = None 280 | ip_location.longitude = None 281 | 282 | return ip_location 283 | 284 | 285 | class MaxMindGeoLite2City(IGeoIpDatabase): 286 | """ 287 | Class for accessing geolocation data provided by GeoLite2 database 288 | created by MaxMind, available from https://www.maxmind.com/. 289 | Downloadable from https://dev.maxmind.com/geoip/geoip2/geolite2/. 290 | 291 | """ 292 | 293 | @staticmethod 294 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 295 | # process request 296 | try: 297 | request = geoip2.database.Reader(db_path) 298 | except: 299 | raise ServiceError() 300 | 301 | # content 302 | try: 303 | res = request.city(ip_address) 304 | except TypeError: 305 | raise InvalidRequestError() 306 | except geoip2.errors.AddressNotFoundError: 307 | raise IpAddressNotFoundError(ip_address) 308 | 309 | # prepare return value 310 | ip_location = IpLocation(ip_address) 311 | 312 | # format data 313 | if res.country: 314 | ip_location.country = res.country.iso_code 315 | else: 316 | ip_location.country = None 317 | 318 | if res.subdivisions: 319 | ip_location.region = res.subdivisions[0].names['en'] 320 | else: 321 | ip_location.region = None 322 | 323 | if res.city.names: 324 | ip_location.city = res.city.names['en'] 325 | else: 326 | ip_location.city = None 327 | 328 | if res.location: 329 | ip_location.latitude = float(res.location.latitude) 330 | ip_location.longitude = float(res.location.longitude) 331 | else: 332 | ip_location.latitude = None 333 | ip_location.longitude = None 334 | 335 | return ip_location 336 | 337 | 338 | class Ip2Location(IGeoIpDatabase): 339 | """ 340 | Class for accessing geolocation data provided by IP2Location, 341 | available from https://www.ip2location.com/. 342 | Downloadable from http://lite.ip2location.com/database/ip-country-region-city-latitude-longitude. 343 | 344 | """ 345 | 346 | @staticmethod 347 | def get(ip_address, api_key=None, db_path=None, username=None, password=None): 348 | # process request 349 | try: 350 | ip2loc = IP2Location.IP2Location() 351 | ip2loc.open(db_path) 352 | except: 353 | raise ServiceError() 354 | 355 | # content 356 | res = ip2loc.get_all(ip_address) 357 | 358 | if res is None: 359 | raise IpAddressNotFoundError(ip_address) 360 | 361 | # prepare return value 362 | ip_location = IpLocation(ip_address) 363 | 364 | # format data 365 | if res.country_short != ' ' \ 366 | or res.country_short == 'N/A' \ 367 | or res.country_short == '??': 368 | ip_location.country = res.country_short.decode('utf-8') 369 | else: 370 | ip_location.country = None 371 | 372 | if res.region != ' ' \ 373 | or res.region == 'N/A': 374 | ip_location.region = res.region.decode('utf-8') 375 | else: 376 | ip_location.region = None 377 | 378 | if res.city != ' ' \ 379 | or res.city == 'N/A': 380 | ip_location.city = res.city.decode('utf-8') 381 | else: 382 | ip_location.city = None 383 | 384 | if res.latitude != ' ' \ 385 | or res.latitude == 'N/A': 386 | ip_location.latitude = float(res.latitude) 387 | else: 388 | ip_location.latitude = None 389 | 390 | if res.longitude != ' ' \ 391 | or res.longitude == 'N/A': 392 | ip_location.longitude = float(res.longitude) 393 | else: 394 | ip_location.longitude = None 395 | 396 | return ip_location 397 | 398 | -------------------------------------------------------------------------------- /ip2geotools/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Errors 4 | ====== 5 | 6 | These classes provide special exceptions used when accessing data from 7 | third-party geolocation databases. 8 | 9 | """ 10 | # pylint: disable=missing-docstring 11 | 12 | import json 13 | import dicttoxml 14 | 15 | 16 | class LocationError(RuntimeError): 17 | """ 18 | This class represents a generic location error. It extends 19 | :py:exc:`RuntimeError` and does not add any additional attributes. 20 | 21 | """ 22 | 23 | def to_json(self): 24 | return json.dumps( 25 | { 26 | 'error_type': type(self).__name__, 27 | 'error_message': self.__str__() 28 | }) 29 | 30 | def to_xml(self): 31 | return dicttoxml.dicttoxml( 32 | { 33 | 'error_type': type(self).__name__, 34 | 'error_message': self.__str__() 35 | }, 36 | custom_root='ip_location_error', 37 | attr_type=False).decode() 38 | 39 | def to_csv(self, delimiter): 40 | return '%s%s%s' % (type(self).__name__, delimiter, self.__str__()) 41 | 42 | 43 | class IpAddressNotFoundError(LocationError): 44 | """ 45 | The IP address was not found. 46 | 47 | """ 48 | 49 | pass 50 | 51 | 52 | class PermissionRequiredError(LocationError): 53 | """ 54 | Problem with authentication or authorization of the request. 55 | Check your permission for accessing the service. 56 | 57 | """ 58 | 59 | pass 60 | 61 | 62 | class InvalidRequestError(LocationError): 63 | """ 64 | Invalid request. 65 | 66 | """ 67 | 68 | pass 69 | 70 | 71 | class InvalidResponseError(LocationError): 72 | """ 73 | Invalid response. 74 | 75 | """ 76 | 77 | pass 78 | 79 | 80 | class ServiceError(LocationError): 81 | """ 82 | Response from geolocation database is invalid (not accessible, etc.) 83 | 84 | """ 85 | 86 | pass 87 | 88 | 89 | class LimitExceededError(LocationError): 90 | """ 91 | Limits of geolocation database have been reached. 92 | 93 | """ 94 | 95 | pass 96 | -------------------------------------------------------------------------------- /ip2geotools/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Models 4 | ====== 5 | 6 | These classes provide models for the data returned by geolocation databases 7 | and these models are also used for comparison of given and provided data. 8 | 9 | """ 10 | # pylint: disable=missing-docstring 11 | 12 | import json 13 | import dicttoxml 14 | 15 | 16 | class IpLocation(object): 17 | """ 18 | Model for storing location of given IP address. 19 | 20 | This class provides the following attributes: 21 | 22 | .. attribute:: ip_address 23 | 24 | IP address. 25 | 26 | .. attribute:: city 27 | 28 | City where IP address is located. 29 | 30 | .. attribute:: region 31 | 32 | Region where IP address is located. 33 | 34 | .. attribute:: country 35 | 36 | Country where IP address is located (two letters country code). 37 | 38 | .. attribute:: latitude 39 | 40 | Latitude where IP address is located. 41 | 42 | .. attribute:: longitude 43 | 44 | Longitude where IP address is located. 45 | 46 | """ 47 | 48 | _ip_address = None 49 | _city = None 50 | _region = None 51 | _country = None 52 | _latitude = None 53 | _longitude = None 54 | 55 | def __init__(self, ip_address, city=None, region=None, country=None, 56 | latitude=None, longitude=None): 57 | self.ip_address = ip_address 58 | self.city = city 59 | self.region = region 60 | self.country = country 61 | self.latitude = latitude 62 | self.longitude = longitude 63 | 64 | @property 65 | def ip_address(self): 66 | return self._ip_address 67 | 68 | @ip_address.setter 69 | def ip_address(self, value): 70 | self._ip_address = value 71 | 72 | @property 73 | def city(self): 74 | return self._city 75 | 76 | @city.setter 77 | def city(self, value): 78 | self._city = value 79 | 80 | @property 81 | def region(self): 82 | return self._region 83 | 84 | @region.setter 85 | def region(self, value): 86 | self._region = value 87 | 88 | @property 89 | def country(self): 90 | return self._country 91 | 92 | @country.setter 93 | def country(self, value): 94 | self._country = value 95 | 96 | @property 97 | def latitude(self): 98 | return self._latitude 99 | 100 | @latitude.setter 101 | def latitude(self, value): 102 | self._latitude = value 103 | 104 | @property 105 | def longitude(self): 106 | return self._longitude 107 | 108 | @longitude.setter 109 | def longitude(self, value): 110 | self._longitude = value 111 | 112 | def to_json(self): 113 | return json.dumps(self.__dict__).replace('"_', '"') 114 | 115 | def to_xml(self): 116 | return dicttoxml.dicttoxml(self.__dict__, 117 | custom_root='ip_location', 118 | attr_type=False).decode().replace('<_', '<').replace('=2.1.0 2 | autopep8>=1.4.3 3 | bleach>=3.0.2 4 | certifi>=2018.10.15 5 | chardet>=3.0.4 6 | Click>=7.0 7 | cssselect>=1.0.3 8 | decorator>=4.3.0 9 | dicttoxml>=1.7.4 10 | docutils>=0.14 11 | future>=0.17.1 12 | geocoder>=1.38.1 13 | geoip2>=2.9.0 14 | idna>=2.7 15 | IP2Location>=8.0.3 16 | isort>=4.3.4 17 | lazy-object-proxy>=1.3.1 18 | lxml>=4.2.5 19 | maxminddb>=1.4.1 20 | mccabe>=0.6.1 21 | packaging>=18.0 22 | pip-review>=1.0 23 | pkginfo>=1.4.2 24 | pycodestyle>=2.4.0 25 | Pygments>=2.3.0 26 | pylint>=2.2.0 27 | pyparsing>=2.3.0 28 | pyquery>=1.4.0 29 | ratelim>=0.1.6 30 | readme-renderer>=24.0 31 | requests>=2.20.1 32 | requests-toolbelt>=0.8.0 33 | selenium>=3.141.0 34 | six>=1.11.0 35 | tqdm>=4.28.1 36 | twine>=1.12.1 37 | typed-ast>=1.1.0 38 | typing>=3.6.6 39 | urllib3>=1.24.1 40 | webencodings>=0.5.1 41 | wrapt>=1.10.11 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import io 5 | import os 6 | from setuptools import setup, find_packages 7 | import ip2geotools 8 | 9 | 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 13 | readme = '\n' + f.read() 14 | 15 | with io.open(os.path.join(here, 'LICENSE'), encoding='utf-8') as f: 16 | license = '\n' + f.read() 17 | 18 | with io.open(os.path.join(here, 'requirements.txt'), encoding='utf-8') as f: 19 | requirements = [i.strip() for i in f.readlines()] 20 | 21 | setup( 22 | name='ip2geotools', 23 | version=ip2geotools.__version__, 24 | description=ip2geotools.__description__, 25 | long_description=readme, 26 | author=ip2geotools.__author__, 27 | author_email=ip2geotools.__author_email__, 28 | url=ip2geotools.__url__, 29 | download_url=ip2geotools.__url__ + '/archive/' + ip2geotools.__version__ + '.tar.gz', 30 | packages=find_packages(exclude=['docs', 'tests', 'tests.*']), 31 | package_data={'': ['LICENSE']}, 32 | package_dir={'ip2geotools': 'ip2geotools'}, 33 | install_requires=requirements, 34 | include_package_data=True, 35 | test_suite="tests", 36 | license=ip2geotools.__license__, 37 | classifiers=[ 38 | 'Development Status :: 5 - Production/Stable', 39 | 'Environment :: Console', 40 | 'Intended Audience :: Developers', 41 | 'Intended Audience :: System Administrators', 42 | 'Intended Audience :: Telecommunications Industry', 43 | 'License :: OSI Approved :: MIT License', 44 | 'Natural Language :: English', 45 | 'Operating System :: POSIX :: Linux', 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 3.3', 48 | 'Programming Language :: Python :: 3.4', 49 | 'Programming Language :: Python :: 3.5', 50 | 'Programming Language :: Python :: 3.6', 51 | 'Programming Language :: Python :: 3 :: Only', 52 | 'Programming Language :: Python :: Implementation :: CPython', 53 | 'Programming Language :: Python :: Implementation :: PyPy', 54 | 'Topic :: Internet', 55 | 'Topic :: Scientific/Engineering :: Information Analysis', 56 | 'Topic :: Utilities', 57 | ], 58 | entry_points={ 59 | 'console_scripts': ['ip2geotools=ip2geotools.cli:execute_from_command_line'], 60 | }, 61 | ) 62 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomas-net/ip2geotools/9b94b4ff175f2ae5db4dea3b394cc66f3fd10208/tests/__init__.py --------------------------------------------------------------------------------