From fd7f1586ef00944c72ff317a065d760ef16bca66 Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Tue, 20 Jan 2015 17:49:01 -0200 Subject: [PATCH 01/10] using requests lib --- .gitignore | 1 + noipy/dnsupdater.py | 59 +++++++++++++++++++++------------------------ noipy/main.py | 23 +++++++++--------- setup.py | 5 ++++ test/test_noipy.py | 30 +++++++++++------------ 5 files changed, 60 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 5c49a76..42c6e67 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build dist *egg-info +*egg/ *~ .DS_Store diff --git a/noipy/dnsupdater.py b/noipy/dnsupdater.py index 7af2217..51dd48f 100755 --- a/noipy/dnsupdater.py +++ b/noipy/dnsupdater.py @@ -7,13 +7,10 @@ from __future__ import print_function -try: - import urllib.request as urllib2 -except ImportError: - import urllib2 - import re +import requests + AVAILABLE_PLUGINS = { 'noip': 'NoipDnsUpdater', 'dyn': 'DynDnsUpdater', @@ -37,7 +34,7 @@ def __init__(self, auth, hostname, options=None): self._auth = auth self._hostname = hostname self._options = {} if options is None else options - self.last_status_code = '' + self.last_ddns_response = "" @property def auth(self): @@ -61,68 +58,68 @@ def update_dns(self, new_ip): """Call No-IP API based on dict login_info and return the status code. """ + headers = None if self.auth_type == 'T': api_call_url = self._get_base_url().format(hostname=self.hostname, token=self.auth.token, ip=new_ip) - request = urllib2.Request(api_call_url) else: api_call_url = self._get_base_url().format(hostname=self.hostname, ip=new_ip) - request = urllib2.Request(api_call_url) - request.add_header('Authorization', 'Basic %s' % - self.auth.base64key.decode('utf-8')) + headers = { + 'Authorization': 'Basic %s' % + self.auth.base64key.decode('utf-8'), + } + + r = requests.get(api_call_url, headers=headers) + self.last_ddns_response = str(r.text) - try: - response = urllib2.urlopen(request) - self.last_status_code = response.read().decode('utf-8') - except urllib2.HTTPError as e: - self.last_status_code = str(e.code) + return r.status_code, r.text @property def status_message(self): """Return friendly response from API based on response code. """ msg = None - if self.last_status_code in ['badauth', 'nochg', '401', '403']: + if self.last_ddns_response in ['badauth', 'nochg', '401', '403']: msg = "ERROR: Invalid username or password (%s)." % \ - self.last_status_code - elif 'good' in self.last_status_code \ - or 'nochg' in self.last_status_code: - ip = re.search(r'(\d{1,3}\.?){4}', self.last_status_code).group() - if 'good' in self.last_status_code: + self.last_ddns_response + elif 'good' in self.last_ddns_response \ + or 'nochg' in self.last_ddns_response: + ip = re.search(r'(\d{1,3}\.?){4}', self.last_ddns_response).group() + if 'good' in self.last_ddns_response: msg = "SUCCESS: DNS hostname IP (%s) successfully updated." % \ ip else: msg = "SUCCESS: IP address (%s) is up to date, nothing was " \ "changed. Additional 'nochg' updates may be considered" \ " abusive." % ip - elif self.last_status_code == '!donator': + elif self.last_ddns_response == '!donator': msg = "ERROR: Update request include a feature that is not " \ "available to informed user." - elif self.last_status_code == 'notfqdn': + elif self.last_ddns_response == 'notfqdn': msg = "ERROR: The hostname specified is not a fully-qualified " \ "domain name (not in the form hostname.dyndns.org or " \ "domain.com)." - elif self.last_status_code == 'nohost': + elif self.last_ddns_response == 'nohost': msg = "ERROR: Hostname specified does not exist in this user " \ "account." - elif self.last_status_code == 'numhost': + elif self.last_ddns_response == 'numhost': msg = "ERROR: Too many hosts (more than 20) specified in an " \ "update. Also returned if trying to update a round robin " \ "(which is not allowed)." - elif self.last_status_code == 'abuse': + elif self.last_ddns_response == 'abuse': msg = "ERROR: Username/hostname is blocked due to update abuse." - elif self.last_status_code == 'badagent': + elif self.last_ddns_response == 'badagent': msg = "ERROR: User agent not sent or HTTP method not permitted." - elif self.last_status_code == 'dnserr': + elif self.last_ddns_response == 'dnserr': msg = "ERROR: DNS error encountered." - elif self.last_status_code == '911': + elif self.last_ddns_response == '911': msg = "ERROR: Problem on server side. Retry update in a few " \ "minutes." - elif self.last_status_code == 'OK': + elif self.last_ddns_response == 'OK': msg = "SUCCESS: DNS hostname successfully updated." - elif self.last_status_code == 'KO': + elif self.last_ddns_response == 'KO': msg = "ERROR: Hostname and/or token incorrect." else: msg = "WARNING: Ooops! Something went wrong !!!" diff --git a/noipy/main.py b/noipy/main.py index 067b0fe..f8020d9 100644 --- a/noipy/main.py +++ b/noipy/main.py @@ -7,17 +7,14 @@ from __future__ import print_function -try: - import urllib.request as urllib -except ImportError: - import urllib - import argparse import sys import re import getpass import socket +import requests + try: from . import dnsupdater from . import authinfo @@ -51,11 +48,13 @@ def get_ip(): """Return the machine external IP. """ - - page = urllib.urlopen('http://checkip.dyndns.org') - content = page.read().decode('utf-8') - - return re.search(r'(\d{1,3}\.?){4}', content).group() + try: + r = requests.get("http://httpbin.org/ip") + return r.json()['origin'] if r.status_code == requests.codes.ok \ + else None + except requests.exceptions.ConnectionError as e: + print("Error getting IP address. %s" % e) + return None def get_dns_ip(dnsname): @@ -64,7 +63,7 @@ def get_dns_ip(dnsname): try: return socket.gethostbyname(dnsname) except socket.error: - return "" + return None def print_version(): @@ -136,7 +135,7 @@ def execute_update(args): if update_ddns and args.provider == 'generic': if args.url: if not URL_RE.match(args.url): - process_message = "Malformed url" + process_message = "Malformed URL." exec_result = EXECUTION_RESULT_NOK update_ddns = False else: diff --git a/setup.py b/setup.py index 1c689ac..9fd56c6 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,10 @@ from setuptools import setup, find_packages from noipy import __version__, __author__, __email__, __license__ +lib_dependencies = [ + 'requests>=2.0.0', +] + with open('README.rst') as f: readme = f.read() with open('CHANGELOG.rst') as f: @@ -23,6 +27,7 @@ author_email=__email__, url='https://github.com/povieira/noipy', packages=find_packages(), + install_requires=lib_dependencies, keywords=['no-ip', 'dyndns', 'duckdns', 'ddns', 'api'], classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/test/test_noipy.py b/test/test_noipy.py index 22f73f5..285550c 100644 --- a/test/test_noipy.py +++ b/test/test_noipy.py @@ -232,21 +232,21 @@ def test_dns_plugin_status_message(self): plugin = dnsupdater.DnsUpdaterPlugin(auth, hostname) # badauth code - plugin.last_status_code = 'badauth' + plugin.last_ddns_response = 'badauth' expected_message = "ERROR: Invalid username or password (%s)." \ - % plugin.last_status_code + % plugin.last_ddns_response self.assertTrue(plugin.status_message == expected_message, "Expected 'badauth' status code.") # good code - plugin.last_status_code = 'good 1.1.1.1' + plugin.last_ddns_response = 'good 1.1.1.1' expected_message = "SUCCESS: DNS hostname IP (1.1.1.1) successfully " \ "updated." self.assertTrue(plugin.status_message == expected_message, "Expected 'good <1.1.1.1>' status code.") # nochg code - plugin.last_status_code = 'nochg 1.1.1.1' + plugin.last_ddns_response = 'nochg 1.1.1.1' expected_message = "SUCCESS: IP address (1.1.1.1) is up to date, " \ "nothing was changed. Additional 'nochg' updates " \ "may be considered abusive." @@ -254,14 +254,14 @@ def test_dns_plugin_status_message(self): "Expected 'nochg <1.1.1.1>' status code.") # !donator code - plugin.last_status_code = '!donator' + plugin.last_ddns_response = '!donator' expected_message = "ERROR: Update request include a feature that is " \ "not available to informed user." self.assertTrue(plugin.status_message == expected_message, "Expected '!donator' status code.") # notfqdn code - plugin.last_status_code = 'notfqdn' + plugin.last_ddns_response = 'notfqdn' expected_message = "ERROR: The hostname specified is not a " \ "fully-qualified domain name (not in the form " \ "hostname.dyndns.org or domain.com)." @@ -269,14 +269,14 @@ def test_dns_plugin_status_message(self): "Expected 'notfqdn' status code.") # nohost code - plugin.last_status_code = 'nohost' + plugin.last_ddns_response = 'nohost' expected_message = "ERROR: Hostname specified does not exist in this" \ " user account." self.assertTrue(plugin.status_message == expected_message, "Expected 'nohost' status code.") # numhost code - plugin.last_status_code = 'numhost' + plugin.last_ddns_response = 'numhost' expected_message = "ERROR: Too many hosts (more than 20) specified " \ "in an update. Also returned if trying to update " \ "a round robin (which is not allowed)." @@ -284,46 +284,46 @@ def test_dns_plugin_status_message(self): "Expected 'numhost' status code.") # abuse code - plugin.last_status_code = 'abuse' + plugin.last_ddns_response = 'abuse' expected_message = "ERROR: Username/hostname is blocked due to " \ "update abuse." self.assertTrue(plugin.status_message == expected_message, "Expected 'abuse' status code.") # badagent code - plugin.last_status_code = 'badagent' + plugin.last_ddns_response = 'badagent' expected_message = "ERROR: User agent not sent or HTTP method not " \ "permitted." self.assertTrue(plugin.status_message == expected_message, "Expected 'badagent' status code.") # dnserr code - plugin.last_status_code = 'dnserr' + plugin.last_ddns_response = 'dnserr' expected_message = "ERROR: DNS error encountered." self.assertTrue(plugin.status_message == expected_message, "Expected 'dnserr' status code.") # 911 code - plugin.last_status_code = '911' + plugin.last_ddns_response = '911' expected_message = "ERROR: Problem on server side. Retry update in a" \ " few minutes." self.assertTrue(plugin.status_message == expected_message, "Expected '911' status code.") # OK code - plugin.last_status_code = 'OK' + plugin.last_ddns_response = 'OK' expected_message = "SUCCESS: DNS hostname successfully updated." self.assertTrue(plugin.status_message == expected_message, "Expected 'OK' status code.") # KO code - plugin.last_status_code = 'KO' + plugin.last_ddns_response = 'KO' expected_message = "ERROR: Hostname and/or token incorrect." self.assertTrue(plugin.status_message == expected_message, "Expected 'KO' status code.") # Unknown code - plugin.last_status_code = 'UNKNOWN_CODE' + plugin.last_ddns_response = 'UNKNOWN_CODE' expected_message = "WARNING: Ooops! Something went wrong !!!" self.assertTrue(plugin.status_message == expected_message, "Expected 'Ooops' warning message.") From 0aa9c47c001d305721ac59a8874f4b75dfec98af Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Tue, 20 Jan 2015 18:00:25 -0200 Subject: [PATCH 02/10] simplifying status_code check --- noipy/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/noipy/main.py b/noipy/main.py index f8020d9..812f3dd 100644 --- a/noipy/main.py +++ b/noipy/main.py @@ -50,8 +50,7 @@ def get_ip(): """ try: r = requests.get("http://httpbin.org/ip") - return r.json()['origin'] if r.status_code == requests.codes.ok \ - else None + return r.json()['origin'] if r.status_code == 200 else None except requests.exceptions.ConnectionError as e: print("Error getting IP address. %s" % e) return None From 3abe77cf44c43db28acaa11584faa08922d6faff Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Tue, 20 Jan 2015 19:00:04 -0200 Subject: [PATCH 03/10] test for versions 2.6 and 3.2 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 90b1c04..6e5d4b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,9 @@ before_install: language: python python: + - 2.6 - 2.7 + - 3.2 - 3.3 - 3.4 - pypy From 14d49fd401d5c8d49d4810cde8481eb91d0158a8 Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Tue, 20 Jan 2015 19:11:11 -0200 Subject: [PATCH 04/10] classifiers update --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 9fd56c6..aae3ed4 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,10 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: Implementation :: CPython', From 641f2095f8c4b3b383e74ff4deb45bd205116560 Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Fri, 6 Feb 2015 11:29:49 -0200 Subject: [PATCH 05/10] tox config --- .coveragerc | 13 +++++++++++++ .gitignore | 11 ++++++++--- .travis.yml | 25 ++++++++++--------------- setup.cfg | 7 ++++++- tox.ini | 17 +++++++++++++++++ 5 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 .coveragerc create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..3cd9d09 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +# .coveragerc to control coverage.py +[run] +branch = True + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore index 42c6e67..34ccbf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,19 @@ -*.pyc +*.py[co] +# packages build dist *egg-info -*egg/ +*egg +# tests/coverage +.tox +.coverage + +# IDE/SO files *~ .DS_Store .idea .project .pydevproject .settings - diff --git a/.travis.yml b/.travis.yml index 6e5d4b1..a4234f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,23 +7,18 @@ before_install: language: python -python: - - 2.6 - - 2.7 - - 3.2 - - 3.3 - - 3.4 - - pypy - - pypy3 - -matrix: - allow_failures: - - python: pypy - - python: pypy3 - fast_finish: true +env: + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py32 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=pypy + - TOXENV=pypy3 + - TOXENV=pep8 install: - - pip install coveralls + - pip install tox coveralls script: - coverage run --source=noipy setup.py test diff --git a/setup.cfg b/setup.cfg index 0a8df87..6b68236 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,7 @@ [wheel] -universal = 1 \ No newline at end of file +universal = 1 + +[check-manifest] +ignore = + .travis.yml + tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9d73702 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = py27,py33,py34,pypy,pypy3,pep8 + +[testenv] +deps = coverage + +commands = + python --version + coverage run --source=noipy setup.py test + coverage report -m + +[testenv:py33] +basepython = python3.4 + +[testenv:pep8] +deps = flake8 +commands = flake8 --statistics noipy/ test/ \ No newline at end of file From 08a2e5f4cc98cc3afdb2f842364e87cc3deb949e Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Fri, 6 Feb 2015 11:32:25 -0200 Subject: [PATCH 06/10] tox script --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a4234f2..a5235d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ install: - pip install tox coveralls script: - - coverage run --source=noipy setup.py test + - tox after_success: - coveralls From ffb9f75df84b85cc44603a50d242efa7852e6c30 Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Fri, 6 Feb 2015 16:42:12 -0200 Subject: [PATCH 07/10] argparse import for '< py27' and code simplification --- noipy/dnsupdater.py | 57 ++++++++++++++++------------------- noipy/main.py | 52 ++++++++------------------------ noipy/utils.py | 37 +++++++++++++++++++++++ setup.py | 72 +++++++++++++++++++++++++-------------------- test/test_noipy.py | 36 ++++++++++------------- tox.ini | 7 ++--- 6 files changed, 132 insertions(+), 129 deletions(-) create mode 100644 noipy/utils.py diff --git a/noipy/dnsupdater.py b/noipy/dnsupdater.py index 51dd48f..c77c2d0 100755 --- a/noipy/dnsupdater.py +++ b/noipy/dnsupdater.py @@ -21,6 +21,27 @@ DEFAULT_PLUGIN = 'generic' +error_messages = { + 'badauth': "ERROR: Invalid username or password (badauth).", + 'nochg': "ERROR: Invalid username or password (nochg).", + '401': "ERROR: Invalid username or password (401).", + '403': "ERROR: Invalid username or password (403).", + '!donator': "ERROR: Update request include a feature that is not " + "available to informed user.", + 'notfqdn': "ERROR: The hostname specified is not a fully-qualified domain" + " name (not in the form hostname.dyndns.org or domain.com).", + 'nohost': "ERROR: Hostname specified does not exist in this user account.", + 'numhost': "ERROR: Too many hosts (more than 20) specified in an update. " + "Also returned if trying to update a round robin (which is " + "not allowed).", + 'abuse': "ERROR: Username/hostname is blocked due to update abuse.", + 'badagent': "ERROR: User agent not sent or HTTP method not permitted.", + 'dnserr': "ERROR: DNS error encountered.", + '911': "ERROR: Problem on server side. Retry update in a few minutes.", + 'KO': "ERROR: Hostname and/or token incorrect.", +} + + class DnsUpdaterPlugin(object): """ Base class for any DDNS updater """ @@ -81,9 +102,10 @@ def status_message(self): """Return friendly response from API based on response code. """ msg = None - if self.last_ddns_response in ['badauth', 'nochg', '401', '403']: - msg = "ERROR: Invalid username or password (%s)." % \ - self.last_ddns_response + if self.last_ddns_response in error_messages.keys(): + msg = error_messages.get(self.last_ddns_response) + elif self.last_ddns_response == 'OK': + msg = "SUCCESS: DNS hostname successfully updated." elif 'good' in self.last_ddns_response \ or 'nochg' in self.last_ddns_response: ip = re.search(r'(\d{1,3}\.?){4}', self.last_ddns_response).group() @@ -94,35 +116,8 @@ def status_message(self): msg = "SUCCESS: IP address (%s) is up to date, nothing was " \ "changed. Additional 'nochg' updates may be considered" \ " abusive." % ip - elif self.last_ddns_response == '!donator': - msg = "ERROR: Update request include a feature that is not " \ - "available to informed user." - elif self.last_ddns_response == 'notfqdn': - msg = "ERROR: The hostname specified is not a fully-qualified " \ - "domain name (not in the form hostname.dyndns.org or " \ - "domain.com)." - elif self.last_ddns_response == 'nohost': - msg = "ERROR: Hostname specified does not exist in this user " \ - "account." - elif self.last_ddns_response == 'numhost': - msg = "ERROR: Too many hosts (more than 20) specified in an " \ - "update. Also returned if trying to update a round robin " \ - "(which is not allowed)." - elif self.last_ddns_response == 'abuse': - msg = "ERROR: Username/hostname is blocked due to update abuse." - elif self.last_ddns_response == 'badagent': - msg = "ERROR: User agent not sent or HTTP method not permitted." - elif self.last_ddns_response == 'dnserr': - msg = "ERROR: DNS error encountered." - elif self.last_ddns_response == '911': - msg = "ERROR: Problem on server side. Retry update in a few " \ - "minutes." - elif self.last_ddns_response == 'OK': - msg = "SUCCESS: DNS hostname successfully updated." - elif self.last_ddns_response == 'KO': - msg = "ERROR: Hostname and/or token incorrect." else: - msg = "WARNING: Ooops! Something went wrong !!!" + msg = "ERROR: Ooops! Something went wrong !!!" return msg diff --git a/noipy/main.py b/noipy/main.py index 812f3dd..adfc47f 100644 --- a/noipy/main.py +++ b/noipy/main.py @@ -8,12 +8,12 @@ from __future__ import print_function import argparse -import sys -import re import getpass -import socket +import re +import sys + +from noipy import utils -import requests try: from . import dnsupdater @@ -24,12 +24,6 @@ import authinfo __version__ = "0.TEST" -try: - # Python 3 capability - input = raw_input -except NameError: - pass - EXECUTION_RESULT_OK = 0 EXECUTION_RESULT_NOK = 1 @@ -45,30 +39,6 @@ r'(?:/?|[/?]\S+)$', re.IGNORECASE) -def get_ip(): - """Return the machine external IP. - """ - try: - r = requests.get("http://httpbin.org/ip") - return r.json()['origin'] if r.status_code == 200 else None - except requests.exceptions.ConnectionError as e: - print("Error getting IP address. %s" % e) - return None - - -def get_dns_ip(dnsname): - """Return the machine's current IP address in DNS. - """ - try: - return socket.gethostbyname(dnsname) - except socket.error: - return None - - -def print_version(): - print("== noipy DDNS updater tool v%s ==" % __version__) - - def execute_update(args): """Execute the update based on command line args and returns a tuple with Exit Code and the processing Status Massage @@ -81,7 +51,6 @@ def execute_update(args): exec_result = EXECUTION_RESULT_NOK update_ddns = False - auth = None if args.store: # --store argument if args.usertoken: if args.password: @@ -90,10 +59,10 @@ def execute_update(args): auth = authinfo.ApiAuth(args.usertoken) else: if provider_class.auth_type == 'T': - token = input("Paste your auth token: ") + token = utils.get_input("Paste your auth token: ") auth = authinfo.ApiAuth(usertoken=token) else: - username = input("Type your username: ") + username = utils.get_input("Type your username: ") password = getpass.getpass("Type your password: ") auth = authinfo.ApiAuth(usertoken=username, password=password) @@ -146,8 +115,11 @@ def execute_update(args): update_ddns = False if update_ddns: - ip_address = args.ip if args.ip else get_ip() - if ip_address == get_dns_ip(args.hostname): + ip_address = args.ip if args.ip else utils.get_ip() + if not ip_address: + process_message = "Unable to get IP address. Check connection." + exec_result = False + elif ip_address == utils.get_dns_ip(args.hostname): process_message = "No update required." else: updater = provider_class(auth, args.hostname, updater_options) @@ -189,7 +161,7 @@ def create_parser(): def main(): - print_version() + print("== noipy DDNS updater tool v%s ==" % __version__) parser = create_parser() args = parser.parse_args() diff --git a/noipy/utils.py b/noipy/utils.py new file mode 100644 index 0000000..2b9a61f --- /dev/null +++ b/noipy/utils.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# noipy.utils +# Copyright (c) 2013 Pablo O Vieira (povieira) +# See README.rst and LICENSE for details. + +import socket +import sys + +import requests + + +def get_input(message): + if sys.version_info[:2] < (3, 0): + return input(message) + else: + return raw_input(message) + + +def get_ip(): + """Return machine's external IP. + """ + try: + r = requests.get("http://httpbin.org/ip") + return r.json()['origin'] if r.status_code == 200 else None + except requests.exceptions.ConnectionError: + return None + + +def get_dns_ip(dnsname): + """Return machine's current IP address in DNS. + """ + try: + return socket.gethostbyname(dnsname) + except socket.error: + return None diff --git a/setup.py b/setup.py index aae3ed4..5f1ee61 100644 --- a/setup.py +++ b/setup.py @@ -5,54 +5,62 @@ # Copyright (c) 2013 Pablo O Vieira (povieira) # See README.rst and LICENSE for details. -from setuptools import setup, find_packages +from setuptools import setup + +import sys + from noipy import __version__, __author__, __email__, __license__ -lib_dependencies = [ - 'requests>=2.0.0', + +install_requires = [ + "requests>=2.0", ] -with open('README.rst') as f: +if sys.version_info[:2] < (2, 7): + install_requires.append("argparse") + + +with open("README.rst") as f: readme = f.read() -with open('CHANGELOG.rst') as f: +with open("CHANGELOG.rst") as f: changelog = f.read() setup( - name='noipy', + name="noipy", version=__version__, - description='Command line update for No-IP and Dyn DDNS Update API', - long_description=readme + '\n\n' + changelog, + description="Command line update for No-IP and Dyn DDNS Update API", + long_description=readme + "\n\n" + changelog, license=__license__, author=__author__, author_email=__email__, - url='https://github.com/povieira/noipy', - packages=find_packages(), - install_requires=lib_dependencies, - keywords=['no-ip', 'dyndns', 'duckdns', 'ddns', 'api'], + url="https://github.com/povieira/noipy", + packages=["noipy"], + install_requires=install_requires, + keywords=["no-ip", "dyndns", "duckdns", "ddns", "api"], classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Internet :: Name Service (DNS)', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet :: Name Service (DNS)", ], entry_points={ - 'console_scripts': [ - 'noipy = noipy.main:main', + "console_scripts": [ + "noipy = noipy.main:main", ], }, zip_safe=True, - test_suite='test.test_noipy' + test_suite="test.test_noipy" ) diff --git a/test/test_noipy.py b/test/test_noipy.py index 285550c..bb8f83e 100644 --- a/test/test_noipy.py +++ b/test/test_noipy.py @@ -13,13 +13,15 @@ from noipy import authinfo from noipy import dnsupdater from noipy import main +from noipy import utils + VALID_IP_REGEX = r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25' \ r'[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4]' \ r'[0-9]|25[0-5])$' -class SanityTest(unittest.TestCase): +class SanityTest(): def setUp(self): self.parser = main.create_parser() @@ -29,17 +31,18 @@ def tearDown(self): if os.path.exists(self.test_dir): shutil.rmtree(self.test_dir) - def test_sanity(self): - """Tests the sanity of the unit testing framework and if we can - import all we need to work + def test_get_ip(self): + ip = utils.get_ip() + + self.assertTrue(re.match(VALID_IP_REGEX, ip), 'get_ip() failed.') + + def test_get_dns_ip(self): + ip = utils.get_dns_ip('localhost') - * From https://github.com/rbanffy/testable_appengine (thanks, @rbanffy) - """ - self.assertTrue(True, "Oops! Sanity test failed! Did we take the" - " blue pill?") + self.assertTrue(ip == '127.0.0.1', 'get_dns_ip() failed.') -class PluginsTest(unittest.TestCase): +class PluginsTest(): def setUp(self): self.parser = main.create_parser() @@ -141,7 +144,7 @@ def test_auth_get_instance_token(self): self.assertEqual(auth1.token, auth2.token, 'ApiAuth.token fail.') def test_store_and_load_auth_info(self): - cmd_args = ['--store', '-u', 'username', '-p', 'password', + cmd_args = ['--store', '-u', "username", '-p', "password", '--provider', 'noip', '-c', self.test_dir, self.test_ip] # store @@ -153,6 +156,7 @@ def test_store_and_load_auth_info(self): self.assertTrue(status_message == "Auth info stored.", "Status message should be an 'Auth info stored.'") + # load cmd_args = ['--provider', 'noip', '-n', 'noipy.no-ip.org', '-c', self.test_dir, self.test_ip] @@ -163,7 +167,7 @@ def test_store_and_load_auth_info(self): "Error loading auth info") -class GeneralTest(unittest.TestCase): +class GeneralTest(): def setUp(self): self.parser = main.create_parser() @@ -186,16 +190,6 @@ def test_cmd_line_no_args(self): "Status message should start with 'Warning: The hostname to be " "updated must be provided.'") - def test_get_ip(self): - ip = main.get_ip() - - self.assertTrue(re.match(VALID_IP_REGEX, ip), 'get_ip() failed.') - - def test_get_dns_ip(self): - ip = main.get_dns_ip('localhost') - - self.assertTrue(ip == '127.0.0.1', 'get_dns_ip() failed.') - def test_unchanged_ip(self): cmd_args = ['-u', 'username', '-p', 'password', '--url', 'https://dynupdate.no-ip.com/nic/update', diff --git a/tox.ini b/tox.ini index 9d73702..53d0cc9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,14 @@ [tox] -envlist = py27,py33,py34,pypy,pypy3,pep8 +envlist = py26,py27,py33,py34,pypy,pypy3,pep8 [testenv] deps = coverage commands = python --version - coverage run --source=noipy setup.py test + coverage run --source noipy setup.py test coverage report -m -[testenv:py33] -basepython = python3.4 - [testenv:pep8] deps = flake8 commands = flake8 --statistics noipy/ test/ \ No newline at end of file From e50ad0fc3929891d53579333324c866ab95e1209 Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Fri, 6 Feb 2015 16:58:11 -0200 Subject: [PATCH 08/10] Oops.. :flushed: --- test/test_noipy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_noipy.py b/test/test_noipy.py index bb8f83e..2fd879b 100644 --- a/test/test_noipy.py +++ b/test/test_noipy.py @@ -21,7 +21,7 @@ r'[0-9]|25[0-5])$' -class SanityTest(): +class SanityTest(unittest.TestCase): def setUp(self): self.parser = main.create_parser() @@ -42,7 +42,7 @@ def test_get_dns_ip(self): self.assertTrue(ip == '127.0.0.1', 'get_dns_ip() failed.') -class PluginsTest(): +class PluginsTest(unittest.TestCase): def setUp(self): self.parser = main.create_parser() @@ -167,7 +167,7 @@ def test_store_and_load_auth_info(self): "Error loading auth info") -class GeneralTest(): +class GeneralTest(unittest.TestCase): def setUp(self): self.parser = main.create_parser() @@ -318,7 +318,7 @@ def test_dns_plugin_status_message(self): # Unknown code plugin.last_ddns_response = 'UNKNOWN_CODE' - expected_message = "WARNING: Ooops! Something went wrong !!!" + expected_message = "ERROR: Ooops! Something went wrong !!!" self.assertTrue(plugin.status_message == expected_message, "Expected 'Ooops' warning message.") From 1380b34b886fac3f8801daa4cddf4172a2e72e6b Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Sat, 25 Apr 2015 13:48:17 -0300 Subject: [PATCH 09/10] testcase for malformed URL --- test/test_noipy.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/test_noipy.py b/test/test_noipy.py index 2fd879b..82efb57 100644 --- a/test/test_noipy.py +++ b/test/test_noipy.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# tests +# test.test_noipy # Copyright (c) 2013 Pablo O Vieira (povieira) # See README.rst and LICENSE for details. @@ -116,6 +116,21 @@ def test_generic_plugin(self): self.assertTrue(status_message.startswith("ERROR:"), "Status message should be an 'ERROR'") + def test_generic_plugin_malformed_url(self): + cmd_args = ['-u', 'username', '-p', 'password', + '--url', 'abced', + '--provider', 'generic', + '-n', 'noipy.no-ip.org', self.test_ip] + + args = self.parser.parse_args(cmd_args) + result, status_message = main.execute_update(args) + + self.assertTrue(result == main.EXECUTION_RESULT_NOK, + "An error should be flagged when --provider is " + "'generic' and URL is malformed.") + self.assertTrue(status_message == "Malformed URL.", + "Status message should be an 'Malformed URL.'") + class AuthInfoTest(unittest.TestCase): From c3b727291c9eec11deb3d0a4b845f7fc762bdc0c Mon Sep 17 00:00:00 2001 From: Pablo O Vieira Date: Sat, 25 Apr 2015 13:53:06 -0300 Subject: [PATCH 10/10] Version 1.4.0 (using requests library) --- .gitignore | 5 +++-- .travis.yml | 6 +++++- CHANGELOG.rst | 6 ++++++ README.rst | 33 +++++++++++++++++++++++++++++++-- dev-requirements.txt | 3 +++ noipy/__init__.py | 2 +- noipy/authinfo.py | 8 ++++---- noipy/main.py | 6 +++--- noipy/utils.py | 2 +- setup.py | 3 +-- tox.ini | 6 +++++- 11 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 dev-requirements.txt diff --git a/.gitignore b/.gitignore index 34ccbf2..2ce8916 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,9 @@ # packages build dist -*egg-info -*egg +.eggs +*.egg-info +*.egg # tests/coverage .tox diff --git a/.travis.yml b/.travis.yml index a5235d6..1497645 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,8 +20,12 @@ env: install: - pip install tox coveralls -script: +script: - tox after_success: - coveralls + +notifications: + email: + on_success: never diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 010d6a7..875668a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,12 @@ Changelog ========= +1.4.0 (2015-04-25) +------------------ + +- Using the awesome `Requests HTTP library `_ +- Using `tox `_ in order to ease test against multiple Python versions + 1.3.1 (2014-12-19) ------------------ diff --git a/README.rst b/README.rst index 8088430..9993431 100644 --- a/README.rst +++ b/README.rst @@ -38,6 +38,8 @@ To install **noipy**, simply: $ pip install noipy +**Note**: **noipy** will also install the `Requests HTTP library `_ if you haven't yet. + Usage ----- @@ -124,15 +126,42 @@ If you have any enhancement suggestions or find a bug, please: #. Open an `issue `_ #. `Fork `_ the project -#. Do your magic (+ `PEP8 `_ + test) +#. Do your magic +#. Please, `PEP8 `_ and test your code #. Is everything working? Send a `pull request `_ Running tests ~~~~~~~~~~~~~ +First, install tests dependencies (`tox `_ +and `flake8 `_): + +.. code-block:: bash + + $ pip install -r dev-requirements.txt + + +To test against all supported Python versions (if you have them installed): + +.. code-block:: bash + + $ tox + + +Or you can to test against a specific version: + +.. code-block:: bash + + $ tox -e {version} + +Where ``{version}`` can be ``py26``, ``py27``, ``py33``, ``py34``, ``pypy`` and ``pypy3``. + + +Don't forget to run ``pep8``: + .. code-block:: bash - $ python setup.py test + $ tox -e pep8 Copyright & License diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..15490d9 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,3 @@ +flake8>=2.3 +tox>=1.8.1 +requests>=2.0 \ No newline at end of file diff --git a/noipy/__init__.py b/noipy/__init__.py index 7e7902a..b85627f 100644 --- a/noipy/__init__.py +++ b/noipy/__init__.py @@ -9,7 +9,7 @@ """ __title__ = "noipy" -__version_info__ = ('1', '3', '1') +__version_info__ = ('1', '4', '0') __version__ = ".".join(__version_info__) __author__ = "Pablo O Vieira" __email__ = "email@povieira.com" diff --git a/noipy/authinfo.py b/noipy/authinfo.py index ca9814b..d684ca4 100644 --- a/noipy/authinfo.py +++ b/noipy/authinfo.py @@ -11,7 +11,7 @@ import base64 NOIPY_CONFIG = ".noipy" -DEFAULT_CONFIG_LOCATION = os.path.expanduser("~") +DEFAULT_CONFIG_DIR = os.path.expanduser("~") class ApiAuth(object): @@ -50,7 +50,7 @@ def __eq__(self, other): return str(self) == str(other) -def store(auth, provider, config_location=DEFAULT_CONFIG_LOCATION): +def store(auth, provider, config_location=DEFAULT_CONFIG_DIR): """Store auth info in file for specified provider """ auth_file = None @@ -85,7 +85,7 @@ def store(auth, provider, config_location=DEFAULT_CONFIG_LOCATION): raise e -def load(provider, config_location=DEFAULT_CONFIG_LOCATION): +def load(provider, config_location=DEFAULT_CONFIG_DIR): """Load provider specific auth info from file """ auth = None @@ -105,7 +105,7 @@ def load(provider, config_location=DEFAULT_CONFIG_LOCATION): return auth -def exists(provider, config_location=DEFAULT_CONFIG_LOCATION): +def exists(provider, config_location=DEFAULT_CONFIG_DIR): """Check whether provider info is already stored """ config_dir = os.path.join(config_location, NOIPY_CONFIG) diff --git a/noipy/main.py b/noipy/main.py index adfc47f..75796dc 100644 --- a/noipy/main.py +++ b/noipy/main.py @@ -150,9 +150,9 @@ def create_parser(): "update the hostname if it is provided", action='store_true') parser.add_argument('-c', '--config', - help="path to noipy config location (default: %s)" % - authinfo.DEFAULT_CONFIG_LOCATION, - default=authinfo.DEFAULT_CONFIG_LOCATION) + help="noipy config directory (default: %s)" % + authinfo.DEFAULT_CONFIG_DIR, + default=authinfo.DEFAULT_CONFIG_DIR) parser.add_argument('ip', metavar='IP_ADDRESS', nargs='?', help="New host IP address. If not provided, current " "external IP address will be used.") diff --git a/noipy/utils.py b/noipy/utils.py index 2b9a61f..c4b2934 100644 --- a/noipy/utils.py +++ b/noipy/utils.py @@ -19,7 +19,7 @@ def get_input(message): def get_ip(): - """Return machine's external IP. + """Return machine's origin IP address. """ try: r = requests.get("http://httpbin.org/ip") diff --git a/setup.py b/setup.py index 5f1ee61..f447dde 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,6 @@ # See README.rst and LICENSE for details. from setuptools import setup - import sys from noipy import __version__, __author__, __email__, __license__ @@ -28,7 +27,7 @@ setup( name="noipy", version=__version__, - description="Command line update for No-IP and Dyn DDNS Update API", + description="Command line tool for DDNS IP address updating.", long_description=readme + "\n\n" + changelog, license=__license__, author=__author__, diff --git a/tox.ini b/tox.ini index 53d0cc9..11425b8 100644 --- a/tox.ini +++ b/tox.ini @@ -11,4 +11,8 @@ commands = [testenv:pep8] deps = flake8 -commands = flake8 --statistics noipy/ test/ \ No newline at end of file +commands = flake8 --statistics noipy/ test/ + +[flake8] +exclude = .tox,*.egg,build +select = E,W,F \ No newline at end of file