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