Skip to content

Commit 91c894a

Browse files
Added MarketplaceResource and its tests.
1 parent f9fc5dc commit 91c894a

File tree

7 files changed

+188
-5
lines changed

7 files changed

+188
-5
lines changed

connect/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,15 @@ class FileRetrievalError(Message):
145145
def __init__(self, message):
146146
# type: (str) -> None
147147
super(FileRetrievalError, self).__init__(message, 'fileretrieval')
148+
149+
150+
class UpdateResourceError(Message):
151+
def __init__(self, message):
152+
# type: (str) -> None
153+
super(UpdateResourceError, self).__init__(message, 'updateresource')
154+
155+
156+
class DeleteResourceError(Message):
157+
def __init__(self, message):
158+
# type: (str) -> None:
159+
super(DeleteResourceError, self).__init__(message, 'deleteresource')

connect/resources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from .directory import Directory
77
from .fulfillment_automation import FulfillmentAutomation
8+
from .marketplace import MarketplaceResource
89
from .template import TemplateResource
910
from .tier_config_automation import TierConfigAutomation
1011
from .usage_automation import UsageAutomation
@@ -17,6 +18,7 @@
1718
__all__ = [
1819
'Directory',
1920
'FulfillmentAutomation',
21+
'MarketplaceResource',
2022
'TemplateResource',
2123
'TierConfigAutomation',
2224
'UsageAutomation',

connect/resources/directory.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from connect.models.product import Product
1212
from connect.models.tier_config import TierConfig
1313
from connect.resources.base import ApiClient
14+
from connect.resources.marketplace import MarketplaceResource
1415
from connect.resources.tier_account import TierAccountResource
1516
from connect.rql import Query
1617

@@ -55,9 +56,7 @@ def list_marketplaces(self, filters=None):
5556
:return: List of marketplaces matching given filters.
5657
:rtype: list[Marketplace]
5758
"""
58-
query = self._get_filters_query(filters, False)
59-
text, code = ApiClient(self._config, 'marketplaces' + query.compile()).get()
60-
return Marketplace.deserialize(text)
59+
return MarketplaceResource(self._config).list(filters)
6160

6261
def get_marketplace(self, marketplace_id):
6362
""" Obtains Marketplace object given its ID.
@@ -66,8 +65,7 @@ def get_marketplace(self, marketplace_id):
6665
:return: The marketplace with the given id, or ``None`` if such marketplace does not exist.
6766
:rtype: Marketplace|None
6867
"""
69-
text, code = ApiClient(self._config, 'marketplaces/' + marketplace_id).get()
70-
return Marketplace.deserialize(text)
68+
return MarketplaceResource(self._config).get(marketplace_id)
7169

7270
def list_products(self, filters=None):
7371
""" List the products.

connect/resources/marketplace.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This file is part of the Ingram Micro Cloud Blue Connect SDK.
4+
# Copyright (c) 2019-2020 Ingram Micro. All Rights Reserved.
5+
6+
from requests import RequestException
7+
8+
from .base import BaseResource
9+
from connect.exceptions import DeleteResourceError, ServerError, UpdateResourceError
10+
from ..models import Marketplace
11+
12+
13+
class MarketplaceResource(BaseResource):
14+
""" Resource to work with :py:class:`connect.models.Marketplace` models.
15+
:param Config config: Config object or ``None`` to use environment config (default).
16+
"""
17+
DELETE_ERR = 'Error deleting marketplace {}: {}'
18+
UPLOAD_ERR = 'Error uploading icon for Marketplace {}: {}'
19+
RESPONSE_ERR = 'Unexpected server response, returned code {} -- Raw response: {}'
20+
resource = 'marketplaces'
21+
model_class = Marketplace
22+
23+
def __init__(self, config=None):
24+
super(MarketplaceResource, self).__init__(config)
25+
26+
def set_icon(self, id_, path):
27+
""" Sets or updates the Marketplace icon.
28+
29+
:param str id_: Id of the Marketplace.
30+
:param str path: Path to the icon file that will be sent to Connect.
31+
:return: Whether the icon was successfully uploaded.
32+
:rtype: bool
33+
"""
34+
icon = self._load_icon(id_, path)
35+
request_path, headers, multipart = self._setup_icon_request(id_, path, icon)
36+
self._post_icon_request(id_, request_path, headers, multipart)
37+
38+
def _load_icon(self, id_, path):
39+
# type: (str, str) -> bytes
40+
try:
41+
with open(path, 'rb') as f:
42+
return f.read()
43+
except IOError as ex:
44+
raise UpdateResourceError(self.UPLOAD_ERR.format(id_, ex))
45+
46+
def _setup_icon_request(self, id_, path, icon):
47+
# type: (str, str, bytes) -> (str, dict, dict)
48+
request_path = self._api.urljoin(id_, 'icon')
49+
headers = self._api.headers
50+
headers['Accept'] = 'application/json'
51+
del headers['Content-Type'] # This must NOT be set for multipart post requests
52+
multipart = {'icon': (path, icon)}
53+
self.logger.info('HTTP Request: {} - {} - {}'.format(request_path, headers, multipart))
54+
return request_path, headers, multipart
55+
56+
def _post_icon_request(self, id_, path, headers, multipart):
57+
# type: (str, str, dict, dict) -> None
58+
try:
59+
content, status = self._api.post(
60+
path=path,
61+
headers=headers,
62+
files=multipart)
63+
except (RequestException, ServerError) as ex:
64+
raise UpdateResourceError(self.UPLOAD_ERR.format(id_, ex))
65+
self._raise_if_invalid_status(200, status, content, UpdateResourceError)
66+
67+
def _raise_if_invalid_status(self, required, obtained, content, ex_class):
68+
# type: (int, int, str, type) -> None
69+
self.logger.info('HTTP Code: {}'.format(obtained))
70+
if obtained != required:
71+
msg = self.RESPONSE_ERR.format(obtained, content)
72+
self.logger.error(msg)
73+
raise ex_class(msg)
74+
75+
def delete(self, id_):
76+
""" Deletes a Marketplace.
77+
78+
:param id_: Id of the Marketplace.
79+
:return:
80+
"""
81+
try:
82+
content, status = self._api.delete(path=id_)
83+
except (RequestException, ServerError) as ex:
84+
raise DeleteResourceError(self.DELETE_ERR.format(id_, ex))
85+
self._raise_if_invalid_status(204, status, content, DeleteResourceError)

tests/data/icon.png

4.61 KB
Loading

tests/test_directory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def test_list_marketplaces(get_mock):
102102
get_mock.assert_called_with(
103103
url='http://localhost:8080/api/public/v1/marketplaces',
104104
headers={'Content-Type': 'application/json', 'Authorization': 'ApiKey XXXX:YYYYY'},
105+
params={'limit': 100},
105106
timeout=300)
106107

107108

tests/test_marketplace.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This file is part of the Ingram Micro Cloud Blue Connect SDK.
4+
# Copyright (c) 2019-2020 Ingram Micro. All Rights Reserved.
5+
6+
from mock import MagicMock, patch
7+
8+
import pytest
9+
from requests import RequestException
10+
11+
from .common import Response
12+
from connect.exceptions import DeleteResourceError, ServerError, UpdateResourceError
13+
from connect.models import ServerErrorResponse
14+
from connect.resources import MarketplaceResource
15+
16+
17+
ICON_FILE = 'tests/data/icon.png'
18+
19+
20+
@patch('requests.post')
21+
def test_set_icon_ok(post_mock):
22+
post_mock.return_value = Response(True, None, 200)
23+
with open(ICON_FILE, 'rb') as f:
24+
icon = f.read()
25+
26+
MarketplaceResource().set_icon('MP-XXX', ICON_FILE)
27+
28+
post_mock.assert_called_with(
29+
url='http://localhost:8080/api/public/v1/marketplaces/MP-XXX/icon',
30+
headers={'Accept': 'application/json', 'Authorization': 'ApiKey XXXX:YYYYY'},
31+
files={'icon': (ICON_FILE, icon)},
32+
timeout=300)
33+
34+
35+
def test_set_icon_bad_file():
36+
with pytest.raises(UpdateResourceError):
37+
MarketplaceResource().set_icon('MP-XXX', 'non_existing_icon.png')
38+
39+
40+
@patch('requests.post', MagicMock(side_effect=RequestException()))
41+
def test_set_icon_request_exception():
42+
with pytest.raises(UpdateResourceError):
43+
MarketplaceResource().set_icon('MP-XXX', ICON_FILE)
44+
45+
46+
@patch('requests.post', MagicMock(side_effect=ServerError(ServerErrorResponse())))
47+
def test_set_icon_server_error():
48+
with pytest.raises(UpdateResourceError):
49+
MarketplaceResource().set_icon('MP-XXX', ICON_FILE)
50+
51+
52+
@patch('requests.post', MagicMock(return_value=Response(False, '', 500)))
53+
def test_set_icon_status_500():
54+
with pytest.raises(UpdateResourceError):
55+
MarketplaceResource().set_icon('MP-XXX', ICON_FILE)
56+
57+
58+
@patch('requests.delete')
59+
def test_delete_ok(delete_mock):
60+
delete_mock.return_value = Response(True, None, 204)
61+
62+
MarketplaceResource().delete('MP-XXX')
63+
64+
delete_mock.assert_called_with(
65+
url='http://localhost:8080/api/public/v1/marketplaces/MP-XXX',
66+
headers={'Content-Type': 'application/json', 'Authorization': 'ApiKey XXXX:YYYYY'},
67+
timeout=300)
68+
69+
70+
@patch('requests.delete', MagicMock(side_effect=RequestException()))
71+
def test_delete_request_exception():
72+
with pytest.raises(DeleteResourceError):
73+
MarketplaceResource().delete('MP-XXX')
74+
75+
76+
@patch('requests.delete', MagicMock(side_effect=ServerError(ServerErrorResponse())))
77+
def test_delete_server_error():
78+
with pytest.raises(DeleteResourceError):
79+
MarketplaceResource().delete('MP-XXX')
80+
81+
82+
@patch('requests.delete', MagicMock(return_value=Response(False, '', 500)))
83+
def test_delete_status_500():
84+
with pytest.raises(DeleteResourceError):
85+
MarketplaceResource().delete('MP-XXX')

0 commit comments

Comments
 (0)