Skip to content

Commit 12a4b20

Browse files
authored
Merge pull request #23 from ShipChain/feature/url-shortener-client
Create UrlShortenerClient
2 parents f7c23c9 + 718745c commit 12a4b20

File tree

6 files changed

+282
-118
lines changed

6 files changed

+282
-118
lines changed

conf/test_settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,9 @@
6969

7070

7171
SIMPLE_JWT['VERIFYING_KEY'] = SIMPLE_JWT['PRIVATE_KEY'].public_key()
72+
73+
URL_SHORTENER_HOST = 'not-really-aws.com'
74+
URL_SHORTENER_URL = 'not-really-aws.com'
75+
76+
IOT_AWS_HOST = 'not-really-aws.com'
77+
IOT_GATEWAY_STAGE = 'test'

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "shipchain-common"
3-
version = "1.0.16"
3+
version = "1.0.17"
44
description = "A PyPI package containing shared code for ShipChain's Python/Django projects."
55

66
license = "Apache-2.0"

src/shipchain_common/aws.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
Copyright 2020 ShipChain, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
import json
18+
import logging
19+
20+
import requests
21+
from aws_requests_auth.boto_utils import BotoAWSRequestsAuth
22+
from django.conf import settings
23+
from influxdb_metrics.loader import log_metric, TimingMetric
24+
from rest_framework import status
25+
26+
from .exceptions import AWSIoTError
27+
from .utils import DecimalEncoder
28+
29+
LOG = logging.getLogger('python-common')
30+
31+
32+
class AWSClient:
33+
@property
34+
def url(self):
35+
raise NotImplementedError
36+
37+
@property
38+
def session(self):
39+
raise NotImplementedError
40+
41+
METHOD_POST = 'post'
42+
METHOD_PUT = 'put'
43+
METHOD_GET = 'get'
44+
METHOD_DELETE = 'delete'
45+
46+
RESPONSE_200_METHODS = [METHOD_PUT, METHOD_GET, METHOD_DELETE]
47+
48+
def _call(self, http_method, endpoint, payload=None, params=None):
49+
metric_name = self._get_generic_endpoint_for_metric(http_method, endpoint)
50+
calling_url = f'{self.url}/{endpoint}'
51+
52+
if payload:
53+
payload = json.dumps(payload, cls=DecimalEncoder)
54+
55+
try:
56+
57+
with TimingMetric('python_common_aws.call', tags={'method': metric_name}) as timer:
58+
59+
if http_method == self.METHOD_POST:
60+
response = self.session.post(calling_url, data=payload, params=params)
61+
response_json = response.json()
62+
63+
if response.status_code != status.HTTP_201_CREATED:
64+
self._process_error_object(metric_name, response, response_json)
65+
66+
elif http_method in self.RESPONSE_200_METHODS:
67+
response = getattr(self.session, http_method)(calling_url, data=payload, params=params)
68+
response_json = response.json()
69+
70+
if response.status_code != status.HTTP_200_OK:
71+
self._process_error_object(metric_name, response, response_json)
72+
73+
else:
74+
log_metric('python_common_aws.error', tags={'method': metric_name, 'code': 'InvalidHTTPMethod'})
75+
LOG.error('aws_client(%s) error: %s', metric_name, 'Invalid HTTP Method')
76+
raise AWSIoTError(f'Invalid HTTP Method {http_method}')
77+
78+
LOG.info('aws_client(%s) duration: %.3f', metric_name, timer.elapsed)
79+
80+
except requests.exceptions.ConnectionError:
81+
log_metric('python_common_aws.error', tags={'method': metric_name, 'code': 'ConnectionError'})
82+
raise AWSIoTError("Service temporarily unavailable, try again later", status.HTTP_503_SERVICE_UNAVAILABLE,
83+
'service_unavailable')
84+
85+
except Exception as exception:
86+
log_metric('python_common_aws.error', tags={'method': metric_name, 'code': 'exception'})
87+
raise AWSIoTError(str(exception))
88+
89+
return response_json
90+
91+
def _post(self, endpoint='', payload=None, query_params=None):
92+
return self._call(self.METHOD_POST, endpoint, payload, params=query_params)
93+
94+
def _put(self, endpoint='', payload=None, query_params=None):
95+
return self._call(self.METHOD_PUT, endpoint, payload, params=query_params)
96+
97+
def _get(self, endpoint='', query_params=None):
98+
return self._call(self.METHOD_GET, endpoint, params=query_params)
99+
100+
def _delete(self, endpoint='', query_params=None):
101+
return self._call(self.METHOD_DELETE, endpoint, params=query_params)
102+
103+
@staticmethod
104+
def _process_error_object(endpoint, response, response_json):
105+
error_code = response.status_code
106+
107+
if 'error' in response_json:
108+
message = response_json['error']
109+
if isinstance(message, dict):
110+
if 'code' in message:
111+
error_code = message['code']
112+
if 'message' in message:
113+
message = message['message']
114+
115+
elif 'message' in response_json:
116+
message = response_json['message']
117+
118+
else:
119+
message = response_json
120+
121+
log_metric('python_common_aws.error', tags={'method': endpoint, 'code': error_code})
122+
LOG.error('aws_client(%s) error: %s', endpoint, message)
123+
raise AWSIoTError(f'Error in AWS IoT Request: [{error_code}] {message}')
124+
125+
def _get_generic_endpoint_for_metric(self, http_method, endpoint):
126+
# This should be overwritten by each usage of this class for added clarity.
127+
return f'{http_method}::{endpoint}'
128+
129+
130+
class URLShortenerClient(AWSClient):
131+
url = settings.URL_SHORTENER_URL
132+
session = requests.session()
133+
134+
def __init__(self):
135+
aws_auth = BotoAWSRequestsAuth(
136+
aws_host=settings.URL_SHORTENER_HOST,
137+
aws_region='us-east-1',
138+
aws_service='execute-api'
139+
)
140+
141+
self.session.headers = {'content-type': 'application/json'}
142+
self.session.auth = aws_auth
143+
144+
def _get_generic_endpoint_for_metric(self, http_method, endpoint):
145+
return f'urlshortener::{http_method}::{endpoint}'

src/shipchain_common/iot.py

Lines changed: 6 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,21 @@
1414
limitations under the License.
1515
"""
1616

17-
import json
1817
import logging
1918
import re
2019

2120
import requests
2221
from aws_requests_auth.boto_utils import BotoAWSRequestsAuth
2322
from django.conf import settings
24-
from rest_framework import status
25-
from influxdb_metrics.loader import log_metric, TimingMetric
26-
27-
from .exceptions import AWSIoTError
28-
from .utils import DecimalEncoder
2923

24+
from .aws import AWSClient
3025

3126
LOG = logging.getLogger('python-common')
3227

3328

34-
class AWSIoTClient:
35-
36-
METHOD_POST = 'post'
37-
METHOD_PUT = 'put'
38-
METHOD_GET = 'get'
39-
METHOD_DELETE = 'delete'
40-
41-
RESPONSE_200_METHODS = [METHOD_PUT, METHOD_GET, METHOD_DELETE]
29+
class AWSIoTClient(AWSClient):
30+
url = f'https://{settings.IOT_AWS_HOST}/{settings.IOT_GATEWAY_STAGE}'
31+
session = requests.session()
4232

4333
def __init__(self):
4434
aws_auth = BotoAWSRequestsAuth(
@@ -47,92 +37,11 @@ def __init__(self):
4737
aws_service='execute-api'
4838
)
4939

50-
self.session = requests.session()
5140
self.session.headers = {'content-type': 'application/json'}
5241
self.session.auth = aws_auth
5342

54-
def _call(self, http_method, endpoint, payload=None, params=None):
55-
generic_endpoint = AWSIoTClient._get_generic_endpoint_for_metric(http_method, endpoint)
56-
57-
if payload:
58-
payload = json.dumps(payload, cls=DecimalEncoder)
59-
60-
url = f'https://{settings.IOT_AWS_HOST}/{settings.IOT_GATEWAY_STAGE}/{endpoint}'
61-
62-
try:
63-
64-
with TimingMetric('python_common_aws_iot.call', tags={'method': generic_endpoint}) as timer:
65-
66-
if http_method == AWSIoTClient.METHOD_POST:
67-
response = self.session.post(url, data=payload, params=params)
68-
response_json = response.json()
69-
70-
if response.status_code != status.HTTP_201_CREATED:
71-
self._process_error_object(generic_endpoint, response, response_json)
72-
73-
elif http_method in AWSIoTClient.RESPONSE_200_METHODS:
74-
response = getattr(self.session, http_method)(url, data=payload, params=params)
75-
response_json = response.json()
76-
77-
if response.status_code != status.HTTP_200_OK:
78-
self._process_error_object(generic_endpoint, response, response_json)
79-
80-
else:
81-
log_metric('python_common_aws_iot.error', tags={'method': generic_endpoint,
82-
'code': 'InvalidHTTPMethod'})
83-
LOG.error('aws_iot_client(%s) error: %s', generic_endpoint, 'Invalid HTTP Method')
84-
raise AWSIoTError(f'Invalid HTTP Method {http_method}')
85-
86-
LOG.info('aws_iot_client(%s) duration: %.3f', generic_endpoint, timer.elapsed)
87-
88-
except requests.exceptions.ConnectionError:
89-
log_metric('python_common_aws_iot.error', tags={'method': generic_endpoint, 'code': 'ConnectionError'})
90-
raise AWSIoTError("Service temporarily unavailable, try again later", status.HTTP_503_SERVICE_UNAVAILABLE,
91-
'service_unavailable')
92-
93-
except Exception as exception:
94-
log_metric('python_common_aws_iot.error', tags={'method': generic_endpoint, 'code': 'exception'})
95-
raise AWSIoTError(str(exception))
96-
97-
return response_json
98-
99-
def _post(self, endpoint, payload=None, query_params=None):
100-
return self._call(AWSIoTClient.METHOD_POST, endpoint, payload, params=query_params)
101-
102-
def _put(self, endpoint, payload=None, query_params=None):
103-
return self._call(AWSIoTClient.METHOD_PUT, endpoint, payload, params=query_params)
104-
105-
def _get(self, endpoint, query_params=None):
106-
return self._call(AWSIoTClient.METHOD_GET, endpoint, params=query_params)
107-
108-
def _delete(self, endpoint, query_params=None):
109-
return self._call(AWSIoTClient.METHOD_DELETE, endpoint, params=query_params)
110-
111-
@staticmethod
112-
def _get_generic_endpoint_for_metric(http_method, endpoint):
43+
def _get_generic_endpoint_for_metric(self, http_method, endpoint):
11344
generic_endpoint = re.sub(r'[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}',
11445
'<device_id>', endpoint, flags=re.IGNORECASE)
11546

116-
return f'{http_method}::{generic_endpoint}'
117-
118-
@staticmethod
119-
def _process_error_object(endpoint, response, response_json):
120-
error_code = response.status_code
121-
122-
if 'error' in response_json:
123-
message = response_json['error']
124-
if isinstance(message, dict):
125-
if 'code' in message:
126-
error_code = message['code']
127-
if 'message' in message:
128-
message = message['message']
129-
130-
elif 'message' in response_json:
131-
message = response_json['message']
132-
133-
else:
134-
message = response_json
135-
136-
log_metric('python_common_aws_iot.error', tags={'method': endpoint, 'code': error_code})
137-
LOG.error('aws_iot_client(%s) error: %s', endpoint, message)
138-
raise AWSIoTError(f'Error in AWS IoT Request: [{error_code}] {message}')
47+
return f'iot::{http_method}::{generic_endpoint}'

0 commit comments

Comments
 (0)