Skip to content

Commit 46d0db0

Browse files
committed
Added models for customs items and intl options.
* Added `ShipStationInternationalOptions` class. * Added `ShipStationCustomsItem` class. * Included method for adding customs item to intl options. * Included method for adding intl options to an order. * Added basic tests for functionality via tox. references natecox#2
1 parent 4bed470 commit 46d0db0

File tree

5 files changed

+217
-9
lines changed

5 files changed

+217
-9
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@
2323
install_requires=['requests>=2.6.0'],
2424
license='MIT',
2525
packages=['shipstation'],
26-
url='https://github.com/natecox/pyshipstation',
26+
url='https://github.com/natecox/pyshipstation'
2727
)

shipstation/api.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import requests
3+
from decimal import Decimal
34

45

56
class ShipStationBase(object):
@@ -22,6 +23,86 @@ def as_dict(self):
2223
return d
2324

2425

26+
class ShipStationCustomsItem(ShipStationBase):
27+
def __init__(self,
28+
description=None,
29+
quantity=1,
30+
value=Decimal('0'),
31+
harmonized_tariff_code=None,
32+
country_of_origin=None):
33+
self.description = description
34+
self.quantity = quantity
35+
self.value = value
36+
self.harmonized_tariff_code = harmonized_tariff_code
37+
self.country_of_origin = country_of_origin
38+
39+
if not self.description:
40+
raise AttributeError('description may not be empty')
41+
if not self.harmonized_tariff_code:
42+
raise AttributeError('harmonized_tariff_code may not be empty')
43+
if not self.country_of_origin:
44+
raise AttributeError('country_of_origin may not be empty')
45+
if len(self.country_of_origin) is not 2:
46+
raise AttributeError('country_of_origin must be two characters')
47+
if not isinstance(value, Decimal):
48+
raise AttributeError('value must be decimal')
49+
50+
51+
class ShipStationInternationalOptions(ShipStationBase):
52+
CONTENTS_VALUES = (
53+
'merchandise',
54+
'documents',
55+
'gift',
56+
'returned_goods',
57+
'sample'
58+
)
59+
60+
NON_DELIVERY_OPTIONS = (
61+
'return_to_sender',
62+
'treat_as_abandoned'
63+
)
64+
65+
def __init__(self, contents=None, non_delivery=None):
66+
self.customs_items = []
67+
self.set_contents(contents)
68+
self.set_non_delivery(non_delivery)
69+
70+
def set_contents(self, contents):
71+
if contents:
72+
if contents not in self.CONTENTS_VALUES:
73+
raise AttributeError('contents value not valid')
74+
self.contents = contents
75+
else:
76+
self.contents = None
77+
78+
def add_customs_item(self, customs_item):
79+
if customs_item:
80+
if not isinstance(customs_item, ShipStationCustomsItem):
81+
raise AttributeError('must be of type ShipStationCustomsItem')
82+
self.customs_items.append(customs_item)
83+
84+
def get_items(self):
85+
return self.customs_items
86+
87+
def get_items_as_dicts(self):
88+
return [x.as_dict() for x in self.customs_items]
89+
90+
def set_non_delivery(self, non_delivery):
91+
if non_delivery:
92+
if non_delivery not in self.NON_DELIVERY_OPTIONS:
93+
raise AttributeError('non_delivery value is not valid')
94+
self.non_delivery = non_delivery
95+
else:
96+
self.non_delivery = None
97+
98+
def as_dict(self):
99+
d = super(ShipStationInternationalOptions, self).as_dict()
100+
101+
d['customsItems'] = self.get_items_as_dicts()
102+
103+
return d
104+
105+
25106
class ShipStationWeight(ShipStationBase):
26107
def __init__(self, units=None, value=None):
27108
self.units = units
@@ -226,6 +307,20 @@ def get_items(self):
226307
def get_items_as_dicts(self):
227308
return [x.as_dict() for x in self.items]
228309

310+
def set_international_options(self, options):
311+
if not isinstance(options, ShipStationInternationalOptions):
312+
raise AttributeError(
313+
'options should be an instance of ' +
314+
'ShipStationInternationalOptions'
315+
)
316+
self.international_options = options
317+
318+
def get_international_options_as_dict(self):
319+
if self.international_options:
320+
return self.international_options.as_dict()
321+
else:
322+
return None
323+
229324
def as_dict(self):
230325
d = super(ShipStationOrder, self).as_dict()
231326

@@ -234,6 +329,7 @@ def as_dict(self):
234329
d['billTo'] = self.get_billing_address_as_dict()
235330
d['shipTo'] = self.get_shipping_address_as_dict()
236331
d['weight'] = self.get_weight()
332+
d['internationalOptions'] = self.get_international_options_as_dict()
237333

238334
return d
239335

tests.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

tests/test_customs_item.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import unittest
2+
from nose.tools import raises
3+
from shipstation.api import *
4+
from decimal import Decimal
5+
6+
7+
class ShipStationTests(unittest.TestCase):
8+
9+
def setUp(self):
10+
self.ss = ShipStation('123', '456')
11+
self.ss_order = ShipStationOrder()
12+
self.ss_intl = ShipStationInternationalOptions()
13+
14+
self.ss_customs_item1 = ShipStationCustomsItem(
15+
description='customs item',
16+
quantity=1,
17+
value=Decimal('10.00'),
18+
harmonized_tariff_code='code',
19+
country_of_origin='US'
20+
)
21+
self.ss_customs_item2 = ShipStationCustomsItem(
22+
description='customs item two',
23+
quantity=2,
24+
value=Decimal('20.00'),
25+
harmonized_tariff_code='code',
26+
country_of_origin='US'
27+
)
28+
29+
def tearDown(self):
30+
self.ss = None
31+
self.ss_order = None
32+
self.ss_intl = None
33+
self.ss_customs_item = None
34+
35+
def test_intl_options_accepts_customs_item(self):
36+
self.ss_intl.add_customs_item(self.ss_customs_item1)
37+
self.ss_intl.add_customs_item(self.ss_customs_item2)
38+
39+
expected = 2
40+
actual = len(self.ss_intl.as_dict()['customsItems'])
41+
42+
self.assertEqual(expected, actual)
43+
44+
@raises(AttributeError)
45+
def test_customs_item_must_have_description(self):
46+
ShipStationCustomsItem(
47+
description='',
48+
harmonized_tariff_code='test',
49+
country_of_origin='us'
50+
)
51+
52+
@raises(AttributeError)
53+
def test_customs_item_must_have_tariff_code(self):
54+
ShipStationCustomsItem(
55+
description='test',
56+
harmonized_tariff_code='',
57+
country_of_origin='us'
58+
)
59+
60+
@raises(AttributeError)
61+
def test_customs_item_must_have_country_code(self):
62+
ShipStationCustomsItem(
63+
description='test',
64+
harmonized_tariff_code='test',
65+
)
66+
67+
@raises(AttributeError)
68+
def test_customs_item_must_have_two_character_country_code(self):
69+
ShipStationCustomsItem(
70+
description='test',
71+
harmonized_tariff_code='test',
72+
country_of_origin='something_else'
73+
)

tests/test_international_options.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import unittest
2+
from nose.tools import raises
3+
from shipstation.api import *
4+
from decimal import Decimal
5+
6+
7+
class ShipStationTests(unittest.TestCase):
8+
9+
def setUp(self):
10+
self.ss = ShipStation('123', '456')
11+
self.ss_order = ShipStationOrder()
12+
self.ss_intl = ShipStationInternationalOptions()
13+
14+
self.ss_customs_item = ShipStationCustomsItem(
15+
description='customs item',
16+
quantity=1,
17+
value=Decimal('10.00'),
18+
harmonized_tariff_code='code',
19+
country_of_origin='US'
20+
)
21+
22+
def tearDown(self):
23+
self.ss = None
24+
self.ss_order = None
25+
self.ss_intl = None
26+
self.ss_customs_item = None
27+
28+
def test_order_accepts_international_options(self):
29+
self.ss_intl.add_customs_item(self.ss_customs_item)
30+
self.ss_intl.set_contents('documents')
31+
self.ss_intl.set_non_delivery('return_to_sender')
32+
33+
self.ss_order.set_international_options(self.ss_intl)
34+
35+
expected = self.ss_intl.as_dict()
36+
actual = self.ss_order.as_dict()['internationalOptions']
37+
38+
self.assertDictEqual(expected, actual)
39+
40+
@raises(AttributeError)
41+
def test_international_options_contents_must_be_valid(self):
42+
self.ss_intl.set_contents('something_else')
43+
44+
@raises(AttributeError)
45+
def test_international_options_non_delivery_must_be_valid(self):
46+
self.ss_intl.set_non_delivery('something_else')
47+

0 commit comments

Comments
 (0)