├── .gitignore
├── .travis.yml
├── MANIFEST.in
├── README.rst
├── UNLICENSE
├── ipify
├── __init__.py
├── exceptions.py
├── ipify.py
└── settings.py
├── requirements.txt
├── setup.py
└── tests
├── test_ipify.py
└── test_settings.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .coverage
2 | .coveralls.yml
3 | *.egg-info
4 | build
5 | dist
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '2.7'
4 | - '3.2'
5 | - '3.3'
6 | - '3.4'
7 | - '3.5'
8 | - '3.6'
9 | - 'pypy'
10 | - 'pypy3'
11 | install:
12 | - pip install -e .
13 | # python-coveralls requires coverage 3.
14 | # See https://github.com/z4r/python-coveralls/pull/41
15 | - pip install coverage==3.7.1
16 | - pip install -r requirements.txt
17 | script:
18 | - python setup.py test
19 | after_success:
20 | - coveralls
21 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst UNLICENSE
2 | recursive-include tests *.py
3 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | python-ipify
2 | ============
3 |
4 | The official client library for `ipify `_: *A Simple IP
5 | Address API*.
6 |
7 | .. image:: https://img.shields.io/pypi/v/ipify.svg
8 | :alt: python-ipify Release
9 | :target: https://pypi.python.org/pypi/ipify
10 |
11 | .. image:: https://img.shields.io/pypi/dm/ipify.svg
12 | :alt: python-ipify Downloads
13 | :target: https://pypi.python.org/pypi/ipify
14 |
15 | .. image:: https://img.shields.io/travis/rdegges/python-ipify.svg
16 | :alt: python-ipify Build
17 | :target: https://travis-ci.org/rdegges/python-ipify
18 |
19 | .. image:: https://coveralls.io/repos/rdegges/python-ipify/badge.svg?branch=master
20 | :target: https://coveralls.io/r/rdegges/python-ipify?branch=master
21 |
22 |
23 | Meta
24 | ----
25 |
26 | - Author: Randall Degges
27 | - Email: r@rdegges.com
28 | - Site: http://www.rdegges.com
29 | - Status: maintained, active
30 |
31 |
32 | Purpose
33 | -------
34 |
35 | `ipify `_ is the best IP address lookup service on the
36 | internet. It's fast, simple, scalable, open source, and well-funded (*by me!*).
37 |
38 | In short: if you need a way to pragmatically get your public IP address, ipify
39 | is the best possible choice!
40 |
41 | This library will retrieve your public IP address from ipify's API service, and
42 | return it as a string. It can't get any simpler than that.
43 |
44 | This library also has some other nice features you might care about:
45 |
46 | - If a request fails for any reason, it is re-attempted 3 times using an
47 | exponential backoff algorithm for maximum effectiveness.
48 | - This library handles exceptions properly, and usage examples below show you
49 | how to deal with errors in a foolproof way.
50 | - This library only makes API requests over HTTPS.
51 |
52 |
53 | Installation
54 | ------------
55 |
56 | To install ``ipify``, simply run:
57 |
58 | .. code-block:: console
59 |
60 | $ pip install ipify
61 |
62 | This will install the latest version of the library automatically.
63 |
64 |
65 | Usage
66 | -----
67 |
68 | Using this library is very simple. Here's a simple example:
69 |
70 | .. code-block:: python
71 |
72 | >>> from ipify import get_ip
73 | >>> ip = get_ip()
74 | >>> ip
75 | u'96.41.136.144'
76 |
77 | Now, in regards to exception handling, there are several ways this can fail:
78 |
79 | - The ipify service is down (*not likely*), or:
80 | - Your machine is unable to get the request to ipify because of a network error
81 | of some sort (DNS, no internet, etc.).
82 |
83 | Here's how you can handle all of these edge cases:
84 |
85 | .. code-block:: python
86 |
87 | from ipify import get_ip
88 | from ipify.exceptions import ConnectionError, ServiceError
89 |
90 | try:
91 | ip = get_ip()
92 | except ConnectionError:
93 | # If you get here, it means you were unable to reach the ipify service,
94 | # most likely because of a network error on your end.
95 | except ServiceError:
96 | # If you get here, it means ipify is having issues, so the request
97 | # couldn't be completed :(
98 | except:
99 | # Something else happened (non-ipify related). Maybe you hit CTRL-C
100 | # while the program was running, the kernel is killing your process, or
101 | # something else all together.
102 |
103 | If you want to simplify the above error handling, you could also do the
104 | following (*it will catch any sort of ipify related errors regardless of what
105 | type they may be*):
106 |
107 | .. code-block:: python
108 |
109 | from ipify import get_ip
110 | from ipify.exceptions import IpifyException
111 |
112 | try:
113 | ip = get_ip()
114 | except IpifyException:
115 | # If you get here, then some ipify exception occurred.
116 | except:
117 | # If you get here, some non-ipify related exception occurred.
118 |
119 | One thing to keep in mind: regardless of how you decide to handle exceptions,
120 | the ipify library will retry any failed requests 3 times before ever raising
121 | exceptions -- so if you *do* need to handle exceptions, just remember that retry
122 | logic has already been attempted.
123 |
124 |
125 | Contributing
126 | ------------
127 |
128 | This project is only possible due to the amazing contributors who work on it!
129 |
130 | If you'd like to improve this library, please send me a pull request! I'm happy
131 | to review and merge pull requests.
132 |
133 | The standard contribution workflow should look something like this:
134 |
135 | - Fork this project on Github.
136 | - Make some changes in the master branch (*this project is simple, so no need to
137 | complicate things*).
138 | - Send a pull request when ready.
139 |
140 | Also, if you're making changes, please write tests for your changes -- this
141 | project has a full test suite you can easily modify / test.
142 |
143 | To run the test suite, you can use the following commands:
144 |
145 | .. code-block:: console
146 |
147 | $ pip install -e .
148 | $ pip install -r requirements.txt
149 | $ python setup.py test
150 |
151 |
152 | Change Log
153 | ----------
154 |
155 | All library changes, in descending order.
156 |
157 |
158 | Version 1.0.1
159 | *************
160 |
161 | **Not yet released.**
162 |
163 | - Improving test to actually validate IP addresses. Thanks to `@lethargilistic
164 | `_ for the pull request!
165 | - Fixing URLs in the README / comments to point to https URLs. Thanks to
166 | `@ktdreyer `_ for the pull request!
167 | - Fixing typo in the README. Thanks `@prologic `_
168 | for the find!
169 | - Adding a working test for exercising ``ServiceError`` exceptions. Improves
170 | test coverage a bit =)
171 | - Removing unnecessary assertions / tests.
172 | - Adding test to improve test coverage to 100% =)
173 | - Fixing minor style issues. I'm really obsessed with code style / quality,
174 | don't judge me!
175 | - Adding Python 3.5 / 3.6 support.
176 |
177 |
178 | Version 1.0.0
179 | *************
180 |
181 | **Released May 6, 2015.**
182 |
183 | - First release!
184 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/ipify/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | ipify
3 | ~~~~~
4 |
5 | The official client library for ipify: https://www.ipify.org - A Simple IP
6 | Address API.
7 |
8 | ipify will get your public IP address, and return it. No questions asked.
9 |
10 | ipify is a great choice because it's:
11 |
12 | - Open source.
13 | - Free to use as much as you want (*even if you want to do millions of
14 | requests per second*).
15 | - Fully distributed across Amazon's AWS cloud.
16 | - Got a rock-solid uptime record.
17 | - Personally funded by Randall Degges (http://www.rdegges.com), so it
18 | won't just *disappear* some day.
19 |
20 | For more information, visit the project website: https://www.ipify.org
21 |
22 | -Randall
23 | """
24 |
25 |
26 | __author__ = 'Randall Degges'
27 | __license__ = 'UNLICENSE'
28 | __version__ = '1.0.0'
29 |
30 |
31 | from .ipify import get_ip
32 |
--------------------------------------------------------------------------------
/ipify/exceptions.py:
--------------------------------------------------------------------------------
1 | """
2 | ipify.exceptions
3 | ~~~~~~~~~~~~~~~~
4 |
5 | This module contains all ipify exceptions.
6 | """
7 |
8 |
9 | class IpifyException(Exception):
10 | """
11 | There was an ambiguous exception that occurred while attempting to fetch
12 | your machine's public IP address from the ipify service.
13 | """
14 | pass
15 |
16 |
17 | class ServiceError(IpifyException):
18 | """
19 | The request failed because the ipify service is currently down or
20 | experiencing issues.
21 | """
22 | pass
23 |
24 |
25 | class ConnectionError(IpifyException):
26 | """
27 | The request failed because it wasn't able to reach the ipify service. This
28 | is most likely due to a networking error of some sort.
29 | """
30 | pass
31 |
--------------------------------------------------------------------------------
/ipify/ipify.py:
--------------------------------------------------------------------------------
1 | """
2 | ipify.ipify
3 | ~~~~~~~~~~~
4 |
5 | The module holds the main ipify library implementation.
6 | """
7 |
8 |
9 | from backoff import expo, on_exception
10 | from requests import get
11 | from requests.exceptions import RequestException
12 |
13 | from .exceptions import ConnectionError, ServiceError
14 | from .settings import API_URI, MAX_TRIES, USER_AGENT
15 |
16 |
17 | @on_exception(expo, RequestException, max_tries=MAX_TRIES)
18 | def _get_ip_resp():
19 | """
20 | Internal function which attempts to retrieve this machine's public IP
21 | address from the ipify service (https://www.ipify.org).
22 |
23 | :rtype: obj
24 | :returns: The response object from the HTTP request.
25 | :raises: RequestException if something bad happened and the request wasn't
26 | completed.
27 |
28 | .. note::
29 | If an error occurs when making the HTTP request, it will be retried
30 | using an exponential backoff algorithm. This is a safe way to retry
31 | failed requests without giving up.
32 | """
33 | return get(API_URI, headers={'user-agent': USER_AGENT})
34 |
35 |
36 | def get_ip():
37 | """
38 | Query the ipify service (https://www.ipify.org) to retrieve this machine's
39 | public IP address.
40 |
41 | :rtype: string
42 | :returns: The public IP address of this machine as a string.
43 | :raises: ConnectionError if the request couldn't reach the ipify service,
44 | or ServiceError if there was a problem getting the IP address from
45 | ipify's service.
46 | """
47 | try:
48 | resp = _get_ip_resp()
49 | except RequestException:
50 | raise ConnectionError("The request failed because it wasn't able to reach the ipify service. This is most likely due to a networking error of some sort.")
51 |
52 | if resp.status_code != 200:
53 | raise ServiceError('Received an invalid status code from ipify:' + str(resp.status_code) + '. The service might be experiencing issues.')
54 |
55 | return resp.text
56 |
--------------------------------------------------------------------------------
/ipify/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | ipify.settings
3 | ~~~~~~~~~~~~~~
4 |
5 | This module contains internal settings that make our ipify library simpler.
6 | """
7 |
8 |
9 | from platform import mac_ver, win32_ver, linux_distribution, system
10 | from sys import version_info as vi
11 |
12 | from . import __version__
13 |
14 |
15 | # This is the ipify service base URI. This is where all API requests go.
16 | API_URI = 'https://api.ipify.org'
17 |
18 | # The maximum amount of tries to attempt when making API calls.
19 | MAX_TRIES = 3
20 |
21 | # This dictionary is used to dynamically select the appropriate platform for
22 | # the user agent string.
23 | OS_VERSION_INFO = {
24 | 'Linux': '%s' % (linux_distribution()[0]),
25 | 'Windows': '%s' % (win32_ver()[0]),
26 | 'Darwin': '%s' % (mac_ver()[0]),
27 | }
28 |
29 | # The user-agent string is provided so that I can (eventually) keep track of
30 | # what libraries to support over time. EG: Maybe the service is used primarily
31 | # by Windows developers, and I should invest more time in improving those
32 | # integrations.
33 | USER_AGENT = 'python-ipify/%s python/%s %s/%s' % (
34 | __version__,
35 | '%s.%s.%s' % (vi.major, vi.minor, vi.micro),
36 | system(),
37 | OS_VERSION_INFO.get(system(), ''),
38 | )
39 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest>=2.7.0
2 | pytest-cov>=1.8.1
3 | python-coveralls>=2.5.0
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | setup.py
3 | ~~~~~~~~
4 |
5 | Packaging information and tools.
6 | """
7 |
8 |
9 | from os.path import abspath, dirname, join, normpath
10 | from subprocess import call
11 | from sys import exit
12 |
13 | from setuptools import Command, find_packages, setup
14 |
15 |
16 | class TestCommand(Command):
17 | """
18 | The ``python setup.py test`` command line invocation is powered by this
19 | helper class.
20 |
21 | This class will run ``py.test`` behind the scenes and handle all command
22 | line arguments for ``py.test`` as well.
23 | """
24 | description = 'run all tests'
25 | user_options = []
26 |
27 | def initialize_options(self):
28 | pass
29 |
30 | def finalize_options(self):
31 | pass
32 |
33 | def run(self):
34 | """Run the test suite."""
35 | exit(call(['py.test', '--cov-report', 'term-missing', '--cov', 'ipify']))
36 |
37 |
38 | setup(
39 |
40 | # Basic package information:
41 | name = 'ipify',
42 | version = '1.0.0',
43 | packages = find_packages(exclude=['tests']),
44 |
45 | # Packaging options:
46 | zip_safe = False,
47 | include_package_data = True,
48 |
49 | # Package dependencies:
50 | install_requires = [
51 | 'backoff>=1.0.7',
52 | 'requests>=2.7.0',
53 | ],
54 | tests_require = [
55 | 'pytest>=2.7.0',
56 | 'pytest-cov>=1.8.1',
57 | 'python-coveralls>=2.5.0',
58 | ],
59 |
60 | # Test harness:
61 | cmdclass = {
62 | 'test': TestCommand,
63 | },
64 |
65 | # Metadata for PyPI:
66 | author = 'Randall Degges',
67 | author_email = 'r@rdegges.com',
68 | license = 'UNLICENSE',
69 | url = 'https://github.com/rdegges/python-ipify',
70 | keywords = 'python api client ipify ip address public ipv4 ipv6 service',
71 | description = 'The official client library for ipify: A Simple IP Address API.',
72 | long_description = open(normpath(join(dirname(abspath(__file__)), 'README.rst'))).read(),
73 | classifiers = [
74 | 'Development Status :: 5 - Production/Stable',
75 | 'Environment :: Console',
76 | 'Intended Audience :: Developers',
77 | 'License :: Public Domain',
78 | 'Operating System :: OS Independent',
79 | 'Programming Language :: Python',
80 | 'Programming Language :: Python :: 2',
81 | 'Programming Language :: Python :: 2.7',
82 | 'Programming Language :: Python :: 3',
83 | 'Programming Language :: Python :: 3.2',
84 | 'Programming Language :: Python :: 3.3',
85 | 'Programming Language :: Python :: 3.4',
86 | 'Programming Language :: Python :: 3.5',
87 | 'Programming Language :: Python :: 3.6',
88 | 'Programming Language :: Python :: Implementation :: CPython',
89 | 'Programming Language :: Python :: Implementation :: PyPy',
90 | 'Topic :: Internet',
91 | 'Topic :: Software Development',
92 | 'Topic :: Software Development :: Libraries',
93 | 'Topic :: Software Development :: Libraries :: Python Modules',
94 | 'Topic :: Utilities',
95 | ],
96 |
97 | )
98 |
--------------------------------------------------------------------------------
/tests/test_ipify.py:
--------------------------------------------------------------------------------
1 | """
2 | tests.ipify.ipify
3 | ~~~~~~~~~~~~~~~~~
4 |
5 | All tests for our ipify.ipify module.
6 | """
7 |
8 |
9 | from socket import AF_INET, AF_INET6, inet_pton
10 | from unittest import TestCase
11 |
12 | from requests.models import Response
13 |
14 | from ipify.exceptions import ConnectionError, IpifyException, ServiceError
15 | from ipify.ipify import _get_ip_resp, get_ip
16 |
17 |
18 | class BaseTest(TestCase):
19 | """A base test class."""
20 |
21 | def setUp(self):
22 | import ipify
23 | self._api_uri = ipify.ipify.API_URI
24 |
25 | def tearDown(self):
26 | import ipify
27 | ipify.ipify.API_URI = self._api_uri
28 |
29 |
30 | class GetIpRespTest(BaseTest):
31 | """Tests for our helper function: ``_get_ip_resp``."""
32 |
33 | def test_returns_response(self):
34 | self.assertIsInstance(_get_ip_resp(), Response)
35 |
36 |
37 | class GetIpTest(BaseTest):
38 | """Tests for our ``get_ip`` function."""
39 |
40 | def test_raises_connection_error_on_connection_error(self):
41 | import ipify
42 |
43 | ipify.ipify.API_URI = 'https://api.asdgasggasgdasgdsasgdasdfadfsda.com'
44 | self.assertRaises(ConnectionError, get_ip)
45 |
46 | def test_raises_ipify_exception_on_error(self):
47 | import ipify
48 |
49 | ipify.ipify.API_URI = 'https://api.asdgasggasgdasgdsasgdasdfadfsds.com'
50 | self.assertRaises(IpifyException, get_ip)
51 |
52 | def test_raises_service_error_on_error(self):
53 | import ipify
54 |
55 | ipify.ipify.API_URI = 'https://api.ipify.org/woo'
56 | self.assertRaises(ServiceError, get_ip)
57 |
58 | def test_returns_ip_address(self):
59 | from ipify import get_ip
60 |
61 | def is_valid_ip(ip):
62 | # IPv4
63 | try:
64 | inet_pton(AF_INET, ip)
65 | return True
66 | except OSError:
67 | pass
68 |
69 | # IPv6
70 | try:
71 | inet_pton(AF_INET6, ip)
72 | return True
73 | except OSError:
74 | pass
75 |
76 | return False
77 |
78 | self.assertTrue(is_valid_ip(get_ip()))
79 |
--------------------------------------------------------------------------------
/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | """
2 | tests.ipify.settings
3 | ~~~~~~~~~~~~~~~~~~~~
4 |
5 | All tests for our ipify.settings module.
6 | """
7 |
8 |
9 | from unittest import TestCase
10 |
11 | from ipify import __version__
12 | from ipify.settings import USER_AGENT
13 |
14 |
15 | class SettingsTest(TestCase):
16 | """Tests for our settings module."""
17 |
18 | def test_user_agent_contains_library_version(self):
19 | self.assertTrue(__version__ in USER_AGENT)
20 |
21 | def test_user_agent_contains_python_version(self):
22 | self.assertTrue('python' in USER_AGENT)
23 |
--------------------------------------------------------------------------------