diff --git a/pillarsdk/nodes.py b/pillarsdk/nodes.py index 43cbdc2..a127c27 100755 --- a/pillarsdk/nodes.py +++ b/pillarsdk/nodes.py @@ -1,3 +1,7 @@ +import json + +import os.path + from .resource import List from .resource import Find from .resource import Create @@ -70,6 +74,49 @@ class Node(List, Find, Create, Post, Update, Delete, Replace): utils.convert_datetime(item) return cls.list_class(response) + @classmethod + def create_asset_from_file(cls, project_id, parent_node_id, asset_type, filename, + always_create_new_node=False, api=None): + """Uploads the file to the Cloud and creates an asset node.""" + + api = api or Api.Default() + + # Upload the file. + with open(filename, mode='rb') as infile: + file_upload_resp = api.post('storage/stream/%s' % project_id, + files={'file': infile}) + if file_upload_resp['status'] != 'ok': + raise ValueError('Received bad status %s from Pillar: %s' % + (file_upload_resp['status'], json.dumps(file_upload_resp))) + file_id = file_upload_resp['file_id'] + + # Create or update the node. + basic_properties = { + 'project': project_id, + 'node_type': 'asset', + 'parent': parent_node_id, + 'name': os.path.basename(filename) + } + + if not always_create_new_node: + # Try to find an existing one to see if there is anything to update. + existing_node = cls.find_first({'where': basic_properties}, api=api) + if existing_node: + # Just update the file ID and we're done. + existing_node.properties.content_type = asset_type + existing_node.properties.file = file_id + existing_node.update(api=api) + return existing_node + + basic_properties.update({ + 'properties': {'content_type': asset_type, + 'file': file_id}, + }) + node = cls(basic_properties) + node.create(api=api) + + return node + class NodeType(List, Find, Create, Post, Delete): """NodeType class wrapping the REST node_types endpoint diff --git a/requirements.txt b/requirements.txt index a22f1ed..e3e1cc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ +# Primary requirements pyOpenSSL==0.15.1 requests==2.9.1 -tox>=2.3.1 -wheel>=0.29.0 -coverage>=3.5 -PyTest -pytest-xdist -pytest-cov +# Development requirements +tox==2.3.1 +wheel==0.29.0 +coverage==4.0.3 +pytest==2.9.1 +pytest-xdist==1.14 +pytest-cov==2.2.1 +responses==0.5.1 diff --git a/tests/test_nodes.py b/tests/test_nodes.py new file mode 100644 index 0000000..b77acf6 --- /dev/null +++ b/tests/test_nodes.py @@ -0,0 +1,256 @@ +import unittest +import responses + +import pillarsdk +import pillarsdk.exceptions as sdk_exceptions + +mock = responses.RequestsMock(assert_all_requests_are_fired=True) + + +class AssetNodesTests(unittest.TestCase): + def setUp(self): + self.endpoint = 'http://localhost:12345' + self.api = pillarsdk.Api( + endpoint=self.endpoint, + username='', + password='', + token='jemoeder', + ) + self.project_id = 1234 + + @mock.activate + def test_create_asset_from_file__always_new_node(self): + parent_node_id = 24 * 'a' + + mock.add(responses.POST, + '%s/nodes' % self.endpoint, + json={ + '_id': 24 * 'b', + 'parent': parent_node_id, + 'name': 'test_nodes.py', + 'project': self.project_id, + }, + status=201) + + mock.add(responses.POST, + '%s/storage/stream/%s' % (self.endpoint, self.project_id), + json={ + 'status': 'ok', + 'file_id': 24 * 'c', + }, + status=201) + + node = pillarsdk.Node.create_asset_from_file( + project_id=self.project_id, + parent_node_id=parent_node_id, + asset_type='image', + filename=__file__, + always_create_new_node=True, + api=self.api) + + self.assertEqual(node['_id'], 24 * 'b') + self.assertEqual(node['parent'], parent_node_id) + self.assertEqual(node['name'], 'test_nodes.py') + self.assertEqual(node['node_type'], 'asset') + self.assertEqual(node['properties']['content_type'], 'image') + self.assertEqual(node['properties']['file'], 24 * 'c') + + @mock.activate + def test_create_asset_from_file__update_existing_node(self): + parent_node_id = 24 * 'a' + asset_node_id = 24 * 'b' + + # Uploading the file + mock.add(responses.POST, + '%s/storage/stream/%s' % (self.endpoint, self.project_id), + json={ + 'status': 'ok', + 'file_id': 24 * 'c', + }, + status=201) + + # Finding the existing node + mock.add(responses.GET, + '%s/nodes' % self.endpoint, + json={'_items': [{ + '_id': asset_node_id, + '_etag': 'awesome-etag', + 'name': 'test_nodes.py', + 'node_type': 'asset', + 'project': self.project_id, + 'parent': parent_node_id, + 'properties': { + 'content_type': 'video', + 'file': 24 * 'e', + }} + ] + }) + + # Updating the node + mock.add(responses.PUT, + '%s/nodes/%s' % (self.endpoint, asset_node_id), + json={'_created': 'Wed, 29 Jun 2016 14:45:35 GMT', + '_deleted': False, + '_etag': 'df983fb8834802be83f0f657201cbf7a3d177a9c', + '_id': asset_node_id, + '_status': 'OK', + '_updated': 'Tue, 05 Jul 2016 14:12:27 GMT'}, + status=200) + + node = pillarsdk.Node.create_asset_from_file( + project_id=self.project_id, + parent_node_id=parent_node_id, + asset_type='image', + filename=__file__, + api=self.api) + + self.assertEqual(node['_id'], asset_node_id) + self.assertEqual(node['parent'], parent_node_id) + self.assertEqual(node['name'], 'test_nodes.py') + self.assertEqual(node['node_type'], 'asset') + self.assertEqual(node['properties']['content_type'], 'image') + self.assertEqual(node['properties']['file'], 24 * 'c') + + @mock.activate + def test_create_asset_from_file__create_new_node(self): + parent_node_id = 24 * 'a' + asset_node_id = 24 * 'b' + + # Upload the file + mock.add(responses.POST, + '%s/storage/stream/%s' % (self.endpoint, self.project_id), + json={ + 'status': 'ok', + 'file_id': 24 * 'c', + }, + status=201) + + # Try to find whether the node exists (it doesn't). + mock.add(responses.GET, + '%s/nodes' % self.endpoint, + json={'_items': []}) + + # Create a new node + mock.add(responses.POST, + '%s/nodes' % self.endpoint, + json={ + '_id': asset_node_id, + 'parent': parent_node_id, + 'name': 'test_nodes.py', + 'project': self.project_id, + }, + status=201) + + node = pillarsdk.Node.create_asset_from_file( + project_id=self.project_id, + parent_node_id=parent_node_id, + asset_type='image', + filename=__file__, + api=self.api) + + self.assertEqual(node['_id'], asset_node_id) + self.assertEqual(node['parent'], parent_node_id) + self.assertEqual(node['name'], 'test_nodes.py') + self.assertEqual(node['node_type'], 'asset') + self.assertEqual(node['properties']['content_type'], 'image') + self.assertEqual(node['properties']['file'], 24 * 'c') + + @mock.activate + def test_create_asset_from_file__upload_fails(self): + parent_node_id = 24 * 'a' + + # Upload the file + mock.add(responses.POST, + '%s/storage/stream/%s' % (self.endpoint, self.project_id), + json={ + 'status': 'error', + 'error': 'Internal server error' + }, + status=500) + + self.assertRaises( + sdk_exceptions.ServerError, + pillarsdk.Node.create_asset_from_file, + project_id=self.project_id, + parent_node_id=parent_node_id, + asset_type='image', + filename=__file__, + api=self.api) + + @mock.activate + def test_create_asset_from_file__create_new_node_fails(self): + parent_node_id = 24 * 'a' + asset_node_id = 24 * 'b' + + # Upload the file + mock.add(responses.POST, + '%s/storage/stream/%s' % (self.endpoint, self.project_id), + json={ + 'status': 'ok', + 'file_id': 24 * 'c', + }, + status=201) + + # Try to find whether the node exists (it doesn't). + mock.add(responses.GET, + '%s/nodes' % self.endpoint, + json={'_items': []}) + + # Create a new node, which fails + mock.add(responses.POST, + '%s/nodes' % self.endpoint, + status=500) + + self.assertRaises( + sdk_exceptions.ServerError, + pillarsdk.Node.create_asset_from_file, + project_id=self.project_id, + parent_node_id=parent_node_id, + asset_type='image', + filename=__file__, + api=self.api) + + @mock.activate + def test_create_asset_from_file__update_existing_node_fails(self): + parent_node_id = 24 * 'a' + asset_node_id = 24 * 'b' + + # Uploading the file + mock.add(responses.POST, + '%s/storage/stream/%s' % (self.endpoint, self.project_id), + json={ + 'status': 'ok', + 'file_id': 24 * 'c', + }, + status=201) + + # Finding the existing node + mock.add(responses.GET, + '%s/nodes' % self.endpoint, + json={'_items': [{ + '_id': asset_node_id, + '_etag': 'awesome-etag', + 'name': 'test_nodes.py', + 'node_type': 'asset', + 'project': self.project_id, + 'parent': parent_node_id, + 'properties': { + 'content_type': 'video', + 'file': 24 * 'e', + }} + ] + }) + + # Updating the node fails + mock.add(responses.PUT, + '%s/nodes/%s' % (self.endpoint, asset_node_id), + status=500) + + self.assertRaises( + sdk_exceptions.ServerError, + pillarsdk.Node.create_asset_from_file, + project_id=self.project_id, + parent_node_id=parent_node_id, + asset_type='image', + filename=__file__, + api=self.api) diff --git a/tox.ini b/tox.ini index 59ac891..c9c38de 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ deps=coverage >=3.5 PyTest pytest-xdist pytest-cov + responses ; For now we skip doctests. ;[testenv:py35]