Write own usps library

This commit is contained in:
Nathan Chapman 2023-11-16 19:30:54 -07:00
parent 7d0606143d
commit 4b31688c25
9 changed files with 804 additions and 713 deletions

View File

@ -56,7 +56,7 @@ def get_shipping_cost(total_weight, postal_code):
str(total_weight.lb), container, str(postal_code) str(total_weight.lb), container, str(postal_code)
) )
usps = USPSApi(SiteSettings.load().usps_user_id, test=settings.DEBUG) usps = USPSApi(SiteSettings.load().usps_user_id)
try: try:
validation = usps.get_rate(usps_rate_request) validation = usps.get_rate(usps_rate_request)

View File

@ -1,41 +0,0 @@
import json
import requests
import xmltodict
from lxml import etree
from usps import USPSApi as USPSApiBase
from django.conf import settings
from . import ShippingContainer
class USPSApi(USPSApiBase):
urls = {
'tracking': 'TrackV2{test}&XML={xml}',
'label': 'eVS{test}&XML={xml}',
'validate': 'Verify&XML={xml}',
'rate': 'RateV4&XML={xml}',
}
def get_rate(self, *args, **kwargs):
return Rate(self, *args, **kwargs)
class Rate:
def __init__(self, usps, request, **kwargs):
xml = etree.Element('RateV4Request', {'USERID': usps.api_user_id})
etree.SubElement(xml, 'Revision').text = '2'
package = etree.SubElement(xml, 'Package', {'ID': '0'})
etree.SubElement(package, 'Service').text = request['service']
etree.SubElement(package, 'ZipOrigination').text = request['zip_origination']
etree.SubElement(package, 'ZipDestination').text = request['zip_destination']
etree.SubElement(package, 'Pounds').text = request['pounds']
etree.SubElement(package, 'Ounces').text = request['ounces']
etree.SubElement(package, 'Container').text = request['container']
etree.SubElement(package, 'Width').text = request['width']
etree.SubElement(package, 'Length').text = request['length']
etree.SubElement(package, 'Height').text = request['height']
etree.SubElement(package, 'Girth').text = request['girth']
etree.SubElement(package, 'Machinable').text = request['machinable']
self.result = usps.send_request('rate', xml)

2
core/usps/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .address import Address
from .usps import USPSApi, USPSApiError

56
core/usps/address.py Normal file
View File

@ -0,0 +1,56 @@
from lxml import etree
class Address(object):
def __init__(
self,
name,
address_1,
city,
state,
zipcode,
zipcode_ext='',
company='',
address_2='',
phone=''
):
self.name = name
self.company = company
self.address_1 = address_1
self.address_2 = address_2
self.city = city
self.state = state
self.zipcode = zipcode
self.zipcode_ext = zipcode_ext
self.phone = phone
def add_to_xml(self, root, prefix='To', validate=False):
if not validate:
name = etree.SubElement(root, prefix + 'Name')
name.text = self.name
company = etree.SubElement(root, prefix + 'Firm' + ('Name' if validate else ''))
company.text = self.company
address_1 = etree.SubElement(root, prefix + 'Address1')
address_1.text = self.address_1
address_2 = etree.SubElement(root, prefix + 'Address2')
address_2.text = self.address_2 or '-'
city = etree.SubElement(root, prefix + 'City')
city.text = self.city
state = etree.SubElement(root, prefix + 'State')
state.text = self.state
zipcode = etree.SubElement(root, prefix + 'Zip5')
zipcode.text = self.zipcode
zipcode_ext = etree.SubElement(root, prefix + 'Zip4')
zipcode_ext.text = self.zipcode_ext
if not validate:
phone = etree.SubElement(root, prefix + 'Phone')
phone.text = self.phone

80
core/usps/usps.py Normal file
View File

@ -0,0 +1,80 @@
import logging
import json
import requests
import xmltodict
from lxml import etree
from django.conf import settings
from core import ShippingContainer
logger = logging.getLogger(__name__)
class USPSApiError(Exception):
pass
class USPSApi:
BASE_URL = 'https://secure.shippingapis.com/ShippingAPI.dll?API='
urls = {
'validate': 'Verify&XML={xml}',
'rate': 'RateV4&XML={xml}'
}
def __init__(self, api_user_id, test=False):
self.api_user_id = api_user_id
self.test = test
def get_url(self, action, xml):
return self.BASE_URL + self.urls[action].format(
**{'test': 'Certify' if self.test else '', 'xml': xml}
)
def send_request(self, action, xml):
# The USPS developer guide says "ISO-8859-1 encoding is the expected character set for the request."
# (see https://www.usps.com/business/web-tools-apis/general-api-developer-guide.htm)
xml = etree.tostring(xml, encoding='iso-8859-1', pretty_print=self.test).decode()
url = self.get_url(action, xml)
xml_response = requests.get(url).content
response = json.loads(json.dumps(xmltodict.parse(xml_response)))
if 'Error' in response:
raise USPSApiError(response['Error']['Description'])
return response
def validate_address(self, *args, **kwargs):
return AddressValidate(self, *args, **kwargs)
def get_rate(self, *args, **kwargs):
return Rate(self, *args, **kwargs)
class AddressValidate:
def __init__(self, usps, address):
xml = etree.Element('AddressValidateRequest', {'USERID': usps.api_user_id})
_address = etree.SubElement(xml, 'Address', {'ID': '0'})
address.add_to_xml(_address, prefix='', validate=True)
self.result = usps.send_request('validate', xml)
class Rate:
def __init__(self, usps, request, **kwargs):
xml = etree.Element('RateV4Request', {'USERID': usps.api_user_id})
etree.SubElement(xml, 'Revision').text = '2'
package = etree.SubElement(xml, 'Package', {'ID': '0'})
etree.SubElement(package, 'Service').text = request['service']
etree.SubElement(package, 'ZipOrigination').text = request['zip_origination']
etree.SubElement(package, 'ZipDestination').text = request['zip_destination']
etree.SubElement(package, 'Pounds').text = request['pounds']
etree.SubElement(package, 'Ounces').text = request['ounces']
etree.SubElement(package, 'Container').text = request['container']
etree.SubElement(package, 'Width').text = request['width']
etree.SubElement(package, 'Length').text = request['length']
etree.SubElement(package, 'Height').text = request['height']
etree.SubElement(package, 'Girth').text = request['girth']
etree.SubElement(package, 'Machinable').text = request['machinable']
self.result = usps.send_request('rate', xml)

1327
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,6 @@ django-templated-email = "^3.0.1"
paypal-checkout-serversdk = "^1.0.1" paypal-checkout-serversdk = "^1.0.1"
pillow = "^9.4.0" pillow = "^9.4.0"
redis = "^4.4.0" redis = "^4.4.0"
usps-api = "^0.5"
psycopg2-binary = "^2.9.5" psycopg2-binary = "^2.9.5"
gunicorn = "^20.1.0" gunicorn = "^20.1.0"
sentry-sdk = "^1.12.1" sentry-sdk = "^1.12.1"
@ -31,6 +30,8 @@ environs = "^9.5.0"
python-dotenv = "^0.21.0" python-dotenv = "^0.21.0"
py-moneyed = "^3.0" py-moneyed = "^3.0"
django-simple-captcha = "^0.5.17" django-simple-captcha = "^0.5.17"
lxml = "^4.9.3"
xmltodict = "^0.13.0"
[build-system] [build-system]

View File

@ -265,7 +265,7 @@ class Cart:
str(self.session.get('shipping_address')['postal_code']) str(self.session.get('shipping_address')['postal_code'])
) )
usps = USPSApi(self.site_settings.usps_user_id, test=settings.DEBUG) usps = USPSApi(self.site_settings.usps_user_id)
try: try:
validation = usps.get_rate(usps_rate_request) validation = usps.get_rate(usps_rate_request)

View File

@ -8,7 +8,7 @@ from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from localflavor.us.us_states import USPS_CHOICES from localflavor.us.us_states import USPS_CHOICES
from usps import USPSApi, Address from core.usps import USPSApi, Address
from django_measurement.forms import MeasurementField from django_measurement.forms import MeasurementField
from core.models import ( from core.models import (
@ -96,7 +96,7 @@ class AddressForm(forms.Form):
state=quote(cleaned_data.get('state')), state=quote(cleaned_data.get('state')),
zipcode=quote(cleaned_data.get('postal_code')) zipcode=quote(cleaned_data.get('postal_code'))
) )
usps = USPSApi(SiteSettings.load().usps_user_id, test=True) usps = USPSApi(SiteSettings.load().usps_user_id)
try: try:
validation = usps.validate_address(address) validation = usps.validate_address(address)