Skip to content
This repository was archived by the owner on Jul 24, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The following code gets list of supported couriers
aftership.api_key = 'YOUR_API_KEY_FROM_AFTERSHIP'
couriers = aftership.courier.list_couriers()

You can also set API key via setting :code:`AFTERSHIP_API_KEY` environment varaible.
You can also set API key via setting :code:`AFTERSHIP_API_KEY` environment variable.

.. code-block:: bash

Expand Down
2 changes: 2 additions & 0 deletions aftership/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
__version__ = '1.3.0'

api_key = None

api_secret = None
8 changes: 7 additions & 1 deletion aftership/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
API_KEY_FILED_NAME = 'aftership-api-key'
AFTERSHIP_API_KEY = 'aftership-api-key'
AS_API_KEY = 'as-api-key'
AS_SIGNATURE_HMAC_SHA256 = 'as-signature-hmac-sha256'

CONTENT_TYPE = "application/json"

API_VERSION = "v4"
API_ENDPOINT = "https://api.aftership.com/v4/"

SIGNATURE_AES_HMAC_SHA256 = "AES-HMAC-SHA256"
6 changes: 6 additions & 0 deletions aftership/courier.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .request import make_request
from .response import process_response
import json

__all__ = ['list_couriers', 'list_all_couriers', 'detect_courier']

Expand All @@ -24,3 +25,8 @@ def detect_courier(tracking, **kwargs):
"""
response = make_request('POST', 'couriers/detect', json=dict(tracking=tracking), **kwargs)
return process_response(response)


def post_orders(order, **kwargs):
response = make_request('POST', 'commerce/v1/orders', json=dict(order=json.loads(order)), **kwargs)
return process_response(response)
18 changes: 18 additions & 0 deletions aftership/hmac/hmac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# coding=utf-8

import os
import hashlib
import base64
import hmac


class Hmac():
def __init__(self, api_secret):
self.api_secret = api_secret

def hmac_signature(self, sign_string: str) -> str:
if self.api_secret is None:
self.api_secret = os.getenv('AS_API_SECRET')
signature_str = hmac.new(bytes(self.api_secret.encode()), msg=bytes(
sign_string.encode()), digestmod=hashlib.sha256).digest()
return base64.b64encode(signature_str).decode()
73 changes: 68 additions & 5 deletions aftership/request.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import requests

from urllib.parse import urljoin
from urllib.parse import urlparse
from aftership.hmac.hmac import Hmac

import requests
from aftership.signstring.signstring import SignString

from .const import API_KEY_FILED_NAME, API_ENDPOINT
from .util import get_api_key
from .const import AFTERSHIP_API_KEY, API_ENDPOINT, AS_SIGNATURE_HMAC_SHA256
from .const import AS_API_KEY, CONTENT_TYPE, SIGNATURE_AES_HMAC_SHA256
from .util import get_aftership_api_key, get_as_api_key, get_as_api_secret


def build_request_url(path):
Expand All @@ -12,9 +17,67 @@ def build_request_url(path):

def make_request(method, path, **kwargs):
url = build_request_url(path)
res = urlparse(url)
params = kwargs.get('params', None)
path = res.path

params_str = ""
if params:
params_str = '&'.join([str(key)+'='+str(value) for key, value in params.items()])

if not path.startswith("/"):
path = '/' + path

if len(params_str) > 0:
path = '{}?{}'.format(path, params_str)

signature_type = kwargs.pop('signature_type', None)
if signature_type is None:
return request_with_token(method, url, **kwargs)

body = kwargs.get('json', None)
content_type = None
if (method == "POST" or method == "PUT" or method == "PATCH") and body is not None:
content_type = CONTENT_TYPE

# if using SignString, you must use AS_API_KEY header
if signature_type == SIGNATURE_AES_HMAC_SHA256:
return request_with_aes_hmac256_signature(method, url, path, content_type, **kwargs)

return None


def request_with_token(method, url, **kwargs):
headers = kwargs.pop('headers', dict())
if headers.get(API_KEY_FILED_NAME) is None:
headers[API_KEY_FILED_NAME] = get_api_key()
if headers.get(AFTERSHIP_API_KEY) is None and headers.get(AS_API_KEY) is None:
headers[AFTERSHIP_API_KEY] = get_aftership_api_key()

kwargs['headers'] = headers
return requests.request(method, url, **kwargs)


def request_with_aes_hmac256_signature(method, url, path, content_type, **kwargs):
headers = kwargs.pop('headers', dict())
if headers.get(AS_API_KEY, None) is None:
headers[AS_API_KEY] = get_as_api_key()
if headers.get(AS_API_KEY, None) is None:
return

body = kwargs.get('json', None)
date, sign_string = gen_sign_string(method, path, body, headers, content_type)

hmac = Hmac(get_as_api_secret())
hmac_signature = hmac.hmac_signature(sign_string)
headers[AS_SIGNATURE_HMAC_SHA256] = hmac_signature
headers['Date'] = date

if content_type is not None:
headers["Content-Type"] = content_type

kwargs['headers'] = headers
return requests.request(method, url, **kwargs)


def gen_sign_string(method, path, body, headers, content_type):
s = SignString(headers[AS_API_KEY])
return s.gen_sign_string(method, path, body, headers, content_type)
69 changes: 69 additions & 0 deletions aftership/signstring/signstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# coding=utf-8

import hashlib
import urllib
import time
import json
from urllib import parse
from typing import Union, Text, Dict

BODY = Union[Text, Dict]


class SignString():
def __init__(self, api_secret: str):
self.api_secret = api_secret

def _gen_sign_string(self, method: str, body: BODY, content_type: str, date: str, canonicalized_as_headers: str,
canonicalized_resource: str) -> str:
result = ''
result += method + '\n'
if body:
if isinstance(body, dict):
body = json.dumps(body)
body = hashlib.md5(body.encode()).hexdigest().upper()
else:
body = ''
content_type = ''

result += body + '\n'
result += content_type + '\n'
result += date + '\n'
result += canonicalized_as_headers + '\n'
result += canonicalized_resource

return result

def _get_canonicalized_as_headers(self, headers: dict) -> str:
new_header = {}
for k, v in headers.items():
new_key = k.lower()
new_value = v.strip()
new_header.update({new_key: new_value})

new_header = dict(sorted(new_header.items()))

result = '\n'.join([k + ':' + v for k, v in new_header.items()])
return result

def _get_canonicalized_resource(self, raw_url: str) -> str:
url_parse_result = parse.urlsplit(raw_url)
path = url_parse_result.path
query = urllib.parse.urlencode(sorted(dict(parse.parse_qsl(url_parse_result.query)).items()))
if query:
path = path + '?' + query
return path

def gen_sign_string(self, method: str, uri: str, body: str, as_header: dict, content_type: str) -> tuple:
# if self.api_secret is None:
# self.api_secret = os.getenv('AS_API_SECRET')

canonicalized_as_headers = self._get_canonicalized_as_headers(as_header)
canonicalized_resource = self._get_canonicalized_resource(uri)

date = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime())
sign_string = self._gen_sign_string(method=method, body=body, content_type=content_type, date=date,
canonicalized_as_headers=canonicalized_as_headers,
canonicalized_resource=canonicalized_resource)

return date, sign_string
16 changes: 15 additions & 1 deletion aftership/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,22 @@ def _build_tracking_url(tracking_id, slug, tracking_number):
return url


def get_api_key():
def get_aftership_api_key():
"""Get AfterShip API key"""
if aftership.api_key is not None:
return aftership.api_key
return os.getenv('AFTERSHIP_API_KEY')


def get_as_api_key():
"""Get AS API key"""
if aftership.api_key is not None:
return aftership.api_key
return os.getenv('AS_API_KEY')


def get_as_api_secret():
"""Get AfterShip API secret"""
if aftership.api_secret is not None:
return aftership.api_secret
return os.getenv('AS_API_SECRET')
21 changes: 21 additions & 0 deletions examples/courier_example_aes_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import aftership

aftership.api_key = 'asak_61f86xxx'
aftership.api_secret = 'assk_e8b4bxxx'


def get_enabled_courier_names():
result = aftership.courier.list_couriers(signature_type="AES")
courier_list = [courier['name'] for courier in result['couriers']]
return courier_list


def get_supported_courier_names():
result = aftership.courier.list_all_couriers(signature_type="AES")
courier_list = [courier['name'] for courier in result['couriers']]
return courier_list


if __name__ == '__main__':
enabled_couriers = get_enabled_courier_names(signature_type="AES")
print(enabled_couriers)
3 changes: 2 additions & 1 deletion examples/estimated_delivery_date_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def batch_predict_estimated_delivery_date():
)
return result['estimated_delivery_dates']


if __name__ == '__main__':
list = batch_predict_estimated_delivery_date()
print(list)
print(list)
8 changes: 8 additions & 0 deletions examples/notification_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@ def add_notification(tracking_id, emails=None, smses=None, webhook=None):
raise ValueError('You must specify one of emails, smses or webhook')

aftership.notification.add_notification(tracking_id=tracking_id, notification=update_params)

## using HMAC-SHA256 signature
aftership.notification.add_notification(tracking_id=tracking_id, notification=update_params, signature_type=const.SIGNATURE_AES_HMAC_SHA256)




def list_notifications(tracking_id):
notification = aftership.notification.list_notifications(tracking_id=tracking_id)

## using HMAC-SHA256 signature
notification = aftership.notification.list_notifications(tracking_id=tracking_id, signature_type=const.SIGNATURE_AES_HMAC_SHA256)
return notification


Expand Down
6 changes: 6 additions & 0 deletions examples/tracking_example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import aftership
from aftership import const


# aftership.api_key = 'PUT_YOUR_AFTERSHIP_KEY_HERE'
Expand All @@ -14,6 +15,11 @@ def create_tracking(slug, tracking_number):

def update_tracking(tracking_id, **values):
aftership.tracking.update_tracking(tracking_id=tracking_id, tracking=values)

# using HMAC-SHA256 signature
aftership.tracking.update_tracking(tracking_id=tracking_id, tracking=values,
signature_type=const.SIGNATURE_AES_HMAC_SHA256)

return True


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ packages = [

[tool.poetry.dependencies]
python = "^3.6"
requests = "^2.0.0'"
requests = "^2.0.0"

[tool.poetry.dev-dependencies]
pytest = "^6.0.1"
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def vcr_config():
return {
'cassette_library_dir': 'tests/fixtures/cassettes',
'serializer': 'yaml',
'filter_headers': [('aftership-api-key', 'YOUR_API_KEY_IS_HERE')],
'filter_headers': [('aftership-api-key', 'YOUR_API_KEY_IS_HERE'),('as-api-key', 'YOUR_AS_API_KEY'), ('as-api-ssecret', 'YOUR_AS_API_SECRET')],
'record_mode': 'none',
'match_on': ['uri', 'method', 'query', 'body'],
}
Loading