Repository: planetlabs/planet-client-python
Pull request #34 (2015-07-29 14:30:33)

add init command for storing API key
and remove old list-scene-types remnants


Patch:
diff --git a/planet/api/auth.py b/planet/api/auth.py
index 560d27b..d17087d 100644
--- a/planet/api/auth.py
+++ b/planet/api/auth.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 import os
+from .utils import read_planet_json
 
 ENV_KEY = 'PL_API_KEY'
 
@@ -23,4 +24,8 @@ class APIKey(object):
 
 
 def find_api_key():
-    return os.getenv(ENV_KEY)
+    api_key = os.getenv(ENV_KEY)
+    if api_key is None:
+        contents = read_planet_json()
+        api_key = contents.get('key', None)
+    return api_key
diff --git a/planet/api/client.py b/planet/api/client.py
index 7e56f4a..dad7594 100644
--- a/planet/api/client.py
+++ b/planet/api/client.py
@@ -12,8 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import base64
+import json
 from .dispatch import RequestsDispatcher
 from . import auth
+from .exceptions import InvalidIdentity
 from . import models
 
 
@@ -26,12 +29,16 @@ class Client(object):
         self.base_url = base_url
         self.dispatcher = RequestsDispatcher(workers)
 
-    def _request(self, path, body_type=models.JSON, params=None):
+    def _url(self, path):
         if path.startswith('http'):
             url = path
         else:
             url = self.base_url + path
-        return models.Request(url, self.auth, params, body_type)
+        return url
+
+    def _request(self, path, body_type=models.JSON, params=None, auth=None):
+        return models.Request(self._url(path), auth or self.auth, params,
+                              body_type)
 
     def _get(self, path, body_type=models.JSON, params=None, callback=None):
         request = self._request(path, body_type, params)
@@ -44,6 +51,26 @@ class Client(object):
         return [self._get(path, params=params, callback=callback)
                 for path in paths]
 
+    def login(self, identity, credentials):
+        result = self.dispatcher.session.post(self._url('auth/login'), {
+            'email': identity,
+            'password': credentials
+        }).result()
+        if result.status_code == 400:
+            # do our best to get something out to the user
+            msg = result.text
+            try:
+                msg = json.loads(result.text)['message']
+            finally:
+                raise InvalidIdentity(msg)
+        jwt = result.text
+        payload = jwt.split('.')[1]
+        rem = len(payload) % 4
+        if rem > 0:
+            payload += '=' * (4 - rem)
+        payload = base64.urlsafe_b64decode(payload.encode('utf-8'))
+        return json.loads(payload.decode('utf-8'))
+
     def get_scenes_list(self, scene_type='ortho', order_by=None, count=None,
                         intersects=None, **filters):
         params = {
diff --git a/planet/api/exceptions.py b/planet/api/exceptions.py
index 9729759..064a322 100644
--- a/planet/api/exceptions.py
+++ b/planet/api/exceptions.py
@@ -40,3 +40,8 @@ class OverQuota(APIException):
 
 class ServerError(APIException):
     pass
+
+
+class InvalidIdentity(APIException):
+    '''raised when logging in with identity'''
+    pass
diff --git a/planet/api/utils.py b/planet/api/utils.py
index 0eba0a6..ea90c2f 100644
--- a/planet/api/utils.py
+++ b/planet/api/utils.py
@@ -14,12 +14,32 @@
 
 from datetime import datetime
 from . import exceptions
+import json
 import os
 import re
 
 _ISO_FMT = '%Y-%m-%dT%H:%M:%S.%f+00:00'
 
 
+def _planet_json_file():
+    return os.path.join(os.path.expanduser('~'), '.planet.json')
+
+
+def read_planet_json():
+    fname = _planet_json_file()
+    contents = {}
+    if os.path.exists(fname):
+        with open(fname, 'r') as fp:
+            contents = json.loads(fp.read())
+    return contents
+
+
+def write_planet_json(contents):
+    fname = _planet_json_file()
+    with open(fname, 'w') as fp:
+        fp.write(json.dumps(contents))
+
+
 def check_status(response):
     '''check the status of the response and if needed raise an APIException'''
     status = response.status_code
diff --git a/planet/scripts/__init__.py b/planet/scripts/__init__.py
index 39acdc2..814d6e5 100644
--- a/planet/scripts/__init__.py
+++ b/planet/scripts/__init__.py
@@ -144,10 +144,13 @@ def help(context, command):
         click.echo(cli.get_help(context))
 
 
-@cli.command('list-scene-types')
-def list_scene_types():
-    '''List all scene types.'''
-    click.echo(call_and_wrap(client().list_scene_types).get_raw())
+@cli.command('init')
+@click.option('--email', default=None, prompt=True)
+@click.option('--password', default=None, prompt=True, hide_input=True)
+def init(email, password):
+    response = call_and_wrap(client().login, email, password)
+    planet.api.utils.write_planet_json({'key': response['api_key']})
+    click.echo('initialized')
 
 
 @pretty


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 6625500..4cb4700 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -70,13 +70,13 @@ def test_version_flag():
 
 def test_workers_flag():
     assert 'workers' not in scripts.client_params
-    runner.invoke(scripts.cli, ['--workers', '19', 'list-scene-types'])
+    runner.invoke(scripts.cli, ['--workers', '19', 'search'])
     assert 'workers' in scripts.client_params
     assert scripts.client_params['workers'] == 19
 
 
 def test_api_key_flag():
-    runner.invoke(scripts.cli, ['-k', 'shazbot', 'list-scene-types'])
+    runner.invoke(scripts.cli, ['-k', 'shazbot', 'search'])
     assert 'api_key' in scripts.client_params
     assert scripts.client_params['api_key'] == 'shazbot'
 
@@ -141,3 +141,22 @@ def test_download():
 
     result = runner.invoke(scripts.cli, ['download', '20150615_190229_0905'])
     assert result.exit_code == 0
+
+
+def test_init():
+    # monkey patch the storage file
+    test_file = '.test_planet_json'
+    api.utils._planet_json_file = lambda: test_file
+    client.login.return_value = {
+        'api_key': 'SECRIT'
+    }
+    try:
+        result = runner.invoke(scripts.cli, ['init',
+                                             '--email', 'bil@ly',
+                                             '--password', 'secret'])
+        assert result.exit_code == 0
+        with open(test_file) as fp:
+            data = json.loads(fp.read())
+        assert data['key'] == 'SECRIT'
+    finally:
+        os.unlink(test_file)
diff --git a/tests/test_mod.py b/tests/test_mod.py
index 2ad8911..3e69d3e 100644
--- a/tests/test_mod.py
+++ b/tests/test_mod.py
@@ -17,6 +17,8 @@ a request is made to the expected URL and the response is as provided. Unless
 specifically needed (e.g., JSON format), the response content should not
 matter'''
 
+import base64
+import json
 import os
 
 from planet import api
@@ -26,6 +28,8 @@ import requests_mock
 # have to clear in case key is picked up via env
 if api.auth.ENV_KEY in os.environ:
     os.environ.pop(api.auth.ENV_KEY)
+# and monkey patch the dot file to avoid picking an existing one up
+api.utils._planet_json_file = lambda: '.whyyounameafilelikethis123'
 
 
 @pytest.fixture()
@@ -85,3 +89,29 @@ def test_fetch_scene_info_scene_id(client):
         uri = os.path.join(client.base_url, 'scenes/ortho/x22')
         m.get(uri, text='bananas', status_code=200)
         client.get_scene_metadata('x22').get_raw() == 'bananas'
+
+
+def test_login(client):
+    '''Verify login functionality'''
+    with requests_mock.Mocker() as m:
+        uri = os.path.join(client.base_url, 'auth/login')
+        response = json.dumps({'api_key': 'foobar'}).encode('utf-8')
+        b64 = base64.urlsafe_b64encode(response)
+        response = 'whatever.%s.whatever' % b64.decode('utf-8')
+        m.post(uri, text=response, status_code=200)
+        resp = client.login('jimmy', 'crackcorn')
+        assert resp['api_key'] == 'foobar'
+
+
+def test_login_failure(client):
+    '''Verify login functionality'''
+    with requests_mock.Mocker() as m:
+        uri = os.path.join(client.base_url, 'auth/login')
+        response = json.dumps({'message': 'invalid'})
+        m.post(uri, text=response, status_code=400)
+        try:
+            client.login('jimmy', 'crackcorn')
+        except api.exceptions.InvalidIdentity as ex:
+            assert str(ex) == 'invalid'
+        else:
+            assert False