diff --git a/dpm/client/__init__.py b/dpm/client/__init__.py new file mode 100644 index 0000000..91b9244 --- /dev/null +++ b/dpm/client/__init__.py @@ -0,0 +1 @@ +from .validate import validate diff --git a/dpm/client/validate.py b/dpm/client/validate.py new file mode 100644 index 0000000..12c5d2e --- /dev/null +++ b/dpm/client/validate.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals + +import datapackage +import sys +from os.path import exists + + +def validate(): + """ + Validate datapackage in the current dir. Print validation errors if found and + then exit. Return datapackage if valid. + + :return: + DataPackage -- valid DataPackage instance + + """ + if not exists('datapackage.json'): + print('Current directory is not a datapackage: datapackage.json not found.') + sys.exit(1) + + try: + dp = datapackage.DataPackage('datapackage.json') + except: + print('datapackage.json is malformed') + sys.exit(1) + + try: + dp.validate() + except datapackage.exceptions.ValidationError: + for error in dp.iter_errors(): + # TODO: printing error looks very noisy on output, maybe try make it look nice. + print(error) + sys.exit(1) + + return dp diff --git a/dpm/main.py b/dpm/main.py index f25bf39..65e7f0d 100755 --- a/dpm/main.py +++ b/dpm/main.py @@ -25,6 +25,7 @@ from requests.exceptions import ConnectionError from .runtime import credsfile, configfile from . import __version__ +from . import client # Disable click warning. We are trying to be python3-compatible @@ -72,47 +73,31 @@ def validate(): """ Validate datapackage in the current dir. Print validation errors if found. """ - if not exists('datapackage.json'): - print('Current directory is not a datapackage: datapackage.json not found.') - sys.exit(1) - - try: - dp = datapackage.DataPackage('datapackage.json') - except: - print('datapackage.json is malformed') - sys.exit(1) - - try: - dp.validate() - except datapackage.exceptions.ValidationError: - for error in dp.iter_errors(): - # TODO: printing error looks very noisy on output, maybe try make it look nice. - print(error) - sys.exit(1) - + client.validate() print('datapackage.json is valid') - return dp @cli.command() @click.option('--username') @click.option('--password') @click.option('--server') +@click.option('--publisher', prompt=True) @click.pass_context -def publish(ctx, username, password, server): +def publish(ctx, username, password, server, publisher): """ Publish datapackage to the registry server. """ + dp = client.validate() + if not (username or password): print('Please launch `dpm configure` first.') sys.exit(1) - dp = ctx.invoke(validate) #credentials = get_credentials() # TODO try: - response = requests.post( - '%s/api/v1/package' % server, - json=dp.to_dict(), + response = requests.put( + '%s/api/package/%s/%s' % (server, publisher, dp.descriptor['name']), + json=dp.descriptor, allow_redirects=True) except (OSError, IOError, ConnectionError) as e: # NOTE: This handling currently does not distinguish various local @@ -134,10 +119,10 @@ def publish(ctx, username, password, server): print('Original error was: %s' % repr(e)) print('Invalid JSON response from server') sys.exit(1) - if jsonresponse: - if jsonresponse.get('error_code') == 'DP_INVALID': - print('datapackage.json is invalid') - sys.exit(1) + + if response.status_code == 400: + print(jsonresponse['message']) + sys.exit(1) print(response.status_code) print('publish ok') diff --git a/tests/base.py b/tests/base.py index c623be9..3d8db00 100644 --- a/tests/base.py +++ b/tests/base.py @@ -50,7 +50,7 @@ def _post_teardown(self): class BaseCliTestCase(SimpleTestCase): mock_requests = True # Flag if the testcase should mock out requests library. - isolate = True # Falg if the test should run in isolated environment. + isolate = True # Flag if the test should run in isolated environment. def _pre_setup(self): # Use Mocket to prevent any real network access from tests @@ -61,22 +61,19 @@ def _pre_setup(self): if self.mock_requests: responses.start() - # Start with empty config by default - #patch('dpm.main.ConfigObj', lambda *a: {}).start() + # Start with default config self.config = { 'username': 'user', 'pasword': 'password', } patch('dpm.main.ConfigObj', lambda *a: self.config).start() - #self.config.update( - #server_url='https://example.com' - #) self.runner = CliRunner() def _post_teardown(self): """ Disable all mocks """ if self.mock_requests: + responses.reset() responses.stop() # TODO: Mocket.disable() sometimes makes tests hang. #Mocket.disable() diff --git a/tests/test_publish_connerror.py b/tests/test_publish_connerror.py index 263be0e..6c44a6c 100644 --- a/tests/test_publish_connerror.py +++ b/tests/test_publish_connerror.py @@ -26,8 +26,7 @@ def setUp(self): { "name": "some-resource", "path": "./data/some_data.csv", } ] }) - patch('dpm.main.datapackage', DataPackage=lambda *a: valid_dp).start() - patch('dpm.main.exists', lambda *a: True).start() + patch('dpm.main.client', validate=lambda *a: valid_dp).start() # AND valid credentials patch('dpm.main.get_credentials', lambda *a: 'fake creds').start() @@ -36,7 +35,7 @@ def test_connerror_oserror(self): # GIVEN socket that throws OSError with patch("socket.socket.connect", side_effect=OSError) as mocksock: # WHEN dpm publish is invoked - result = self.invoke(cli, ['publish']) + result = self.invoke(cli, ['publish', '--publisher', 'testpub']) # THEN socket.connect should be called once with server address mocksock.assert_called_once_with(('example.com', 443)) @@ -49,7 +48,7 @@ def test_connerror_ioerror(self): # GIVEN socket that throws IOError with patch("socket.socket.connect", side_effect=IOError) as mocksock: # WHEN dpm publish is invoked - result = self.invoke(cli, ['publish']) + result = self.invoke(cli, ['publish', '--publisher', 'testpub']) # THEN socket.connect should be called once with server address mocksock.assert_called_once_with(('example.com', 443)) @@ -63,7 +62,7 @@ def test_connerror_typeerror(self): with patch("socket.socket.connect", side_effect=TypeError) as mocksock: # WHEN dpm publish is invoked try: - result = self.invoke(cli, ['publish']) + result = self.invoke(cli, ['publish', '--publisher', 'testpub']) except Exception as e: result = e diff --git a/tests/test_publish_invalid.py b/tests/test_publish_invalid.py index 3a469bf..6736d5b 100644 --- a/tests/test_publish_invalid.py +++ b/tests/test_publish_invalid.py @@ -21,13 +21,12 @@ class PublishInvalidTest(BaseCliTestCase): def setUp(self): # GIVEN datapackage that can be treated as valid by the dpm valid_dp = datapackage.DataPackage({ - "name": "some-name", + "name": "some-datapackage", "resources": [ { "name": "some-resource", "path": "./data/some_data.csv", } ] }) - patch('dpm.main.datapackage', DataPackage=lambda *a: valid_dp).start() - patch('dpm.main.exists', lambda *a: True).start() + patch('dpm.main.client', validate=lambda *a: valid_dp).start() # AND valid credentials patch('dpm.main.get_credentials', lambda *a: 'fake creds').start() @@ -35,14 +34,14 @@ def setUp(self): def test_publish_invalid(self): # GIVEN the server that rejects datapackage as invalid responses.add( - responses.POST, 'https://example.com/api/v1/package', - json={'error': 'invalid datapackage json', 'error_code': 'DP_INVALID'}, + responses.PUT, 'https://example.com/api/package/testpub/some-datapackage', + json={'message': 'invalid datapackage json'}, status=400) # WHEN `dpm publish` is invoked - result = self.invoke(cli, ['publish']) + result = self.invoke(cli, ['publish', '--publisher', 'testpub']) # THEN exit code should be 1 self.assertEqual(result.exit_code, 1) # AND 'datapackage.json is invalid' should be printed to stdout - self.assertTrue('datapackage.json is invalid' in result.output) + self.assertTrue('invalid datapackage json' in result.output) diff --git a/tests/test_publish_success.py b/tests/test_publish_success.py new file mode 100644 index 0000000..14f2be9 --- /dev/null +++ b/tests/test_publish_success.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import +from __future__ import unicode_literals + +import datapackage +import responses +from mock import patch + +from dpm.main import cli +from .base import BaseCliTestCase + + +class PublishSuccessTest(BaseCliTestCase): + """ + When user publishes valid datapackage, and server accepts it, dpm should + report sucess. + """ + + def setUp(self): + # GIVEN datapackage that can be treated as valid by the dpm + valid_dp = datapackage.DataPackage({ + "name": "some-datapackage", + "resources": [ + { "name": "some-resource", "path": "./data/some_data.csv", } + ] + }) + patch('dpm.main.client', validate=lambda *a: valid_dp).start() + + # AND valid credentials + patch('dpm.main.get_credentials', lambda *a: 'fake creds').start() + + def test_publish_success(self): + # GIVEN the server that accepts datapackage + responses.add( + responses.PUT, 'https://example.com/api/package/testpub/some-datapackage', + json={'message': 'OK'}, + status=200) + + # WHEN `dpm publish` is invoked + result = self.invoke(cli, ['publish', '--publisher', 'testpub']) + + # THEN exit code should be 0 + self.assertEqual(result.exit_code, 0) + # AND 'publish OK' should be printed to stdout + self.assertTrue('publish ok' in result.output)