├── .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
--------------------------------------------------------------------------------