Repository: graycarl/hbkit

Issue:
Get current public IP
Sample:

```
$ hbkit ip get-public

202.111.111.111
```

Comments:

Patch:
diff --git a/hbkit/__init__.py b/hbkit/__init__.py
index 78596de..4d9613b 100644
--- a/hbkit/__init__.py
+++ b/hbkit/__init__.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 import click
-from . import core, random, short, watch, git, backup, pi, time, config
+from . import core, random, short, watch, git, backup, pi, time, config, ip
 
 __version__ = '0.6.0'
 
@@ -33,3 +33,4 @@ cli.add_command(backup.cli, 'backup')
 cli.add_command(pi.cli, 'pi')
 cli.add_command(time.cli, 'time')
 cli.add_command(config.cli, 'config')
+cli.add_command(ip.cli, 'ip')
diff --git a/hbkit/ip.py b/hbkit/ip.py
new file mode 100644
index 0000000..e3b846a
--- /dev/null
+++ b/hbkit/ip.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from builtins import *      # noqa
+import click
+import requests
+
+
+SERVICES = {
+    'httpbin': {
+        'url': 'https://httpbin.org/ip',
+        'response': lambda data: data['origin']
+    },
+    'ipify': {
+        'url': 'https://api.ipify.org',
+        'params': {
+            'format': 'json'
+        },
+        'response': lambda data: data['ip']
+    }
+}
+
+
+@click.group('ip')
+def cli():
+    """Tools about ip address."""
+
+
+@cli.command('get-public')
+@click.option('--timeout', default=5.0, help='Timeout for network requests.')
+def cli_get_public(timeout):
+    """Get current public IP."""
+    for name in ('ipify', 'httpbin'):
+        service = SERVICES[name]
+        try:
+            response = requests.get(service['url'],
+                                    params=service.get('params'),
+                                    timeout=timeout)
+            response.raise_for_status()
+            ip = service['response'](response.json())
+            break
+        except requests.exceptions.RequestException:
+            continue
+    else:
+        raise click.ClickException('Can not get public IP')
+    click.echo(ip)
diff --git a/hbkit/lib.py b/hbkit/lib.py
index bee5361..3327846 100644
--- a/hbkit/lib.py
+++ b/hbkit/lib.py
@@ -101,7 +101,9 @@ class ConfigManager(object):
     def save_to_file(self):
         try:
             configfile = open(self.path, 'w')
-        except FileNotFoundError:
+        # 暂时没法使用 Python3 的 FileNotFoundError，因为 Python2 没有这个定义
+        # 且 Python-Future 暂时没有对它进行兼容。
+        except IOError:
             os.makedirs(os.path.dirname(self.path))
             configfile = open(self.path, 'w')
         with configfile:


Test patch:
diff --git a/tests/test_ip.py b/tests/test_ip.py
new file mode 100644
index 0000000..8040ee2
--- /dev/null
+++ b/tests/test_ip.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from builtins import *      # noqa
+import requests
+from hbkit import ip
+
+
+class MockGet(object):
+
+    class FakeResponse(object):
+        def __init__(self, json):
+            self._json = json
+
+        def raise_for_status(self):
+            pass
+
+        def json(self):
+            return self._json
+
+    def __init__(self, responses):
+        self.responses = responses
+
+    def __call__(self, url, params, **kwargs):
+        resp = self.responses[url]
+        if isinstance(resp, Exception):
+            raise resp
+        return self.FakeResponse(resp)
+
+
+def test_get_public(runner, monkeypatch):
+    mock_get = MockGet({
+        'https://httpbin.org/ip': {
+            'origin': 'ip from httpbin',
+        },
+        'https://api.ipify.org': {
+            'ip': 'ip from ipify',
+        }
+    })
+    monkeypatch.setattr(requests, 'get', mock_get)
+    # normal case
+    result = runner.invoke(ip.cli_get_public).output.strip()
+    assert result == 'ip from ipify'
+
+    # ipify failed case
+    mock_get.responses['https://api.ipify.org'] = requests.Timeout()
+    result = runner.invoke(ip.cli_get_public).output.strip()
+    assert result == 'ip from httpbin'
+
+    # both failed case
+    mock_get.responses['https://httpbin.org/ip'] = requests.Timeout()
+    result = runner.invoke(ip.cli_get_public).output.strip()
+    assert 'Can not get public IP' in result