Source code for medicaid_utils.common_utils.usps_address

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""This script shows an example of using `requests` and the USPS Address
Information API. In order to use this, you must first register so you can
get your USERID.  Your ID must be in the environment variable `USPS_USERID`.
For information on the API see `here <https://www.usps.com/business/web-tools-apis/address-information-api.htm>_`
"""

# Imports #####################################################################
import os
import xml.dom.minidom
import xml.etree.ElementTree as xml_et
from dotenv import load_dotenv

import requests

load_dotenv()
# Globals #####################################################################
USPS_USERID = os.environ.get("USPS_USERID", "")


[docs] def dump_xml(raw_xml): """ Return a string representation of XML with proper intendation """ if isinstance(raw_xml, xml_et.Element): raw_xml = xml_et.tostring(raw_xml) return xml.dom.minidom.parseString(raw_xml).toprettyxml()
[docs] def get_text(root, xpath): """ Return the text of the XPath element, or None if the element was not found. """ element = root.find(xpath) if element is not None: return element.text return ""
[docs] class USPSAddress: """Representation of an United States Postal Service address.""" def __init__( self, name="", suite="", street="", city="", state="", zip5="", zip4="" ): self.name = name self.suite = suite # or APT self.street = street.title() self.city = city.title() self.state = state.upper() if len(state) == 2 else state.title() self.zip5 = zip5 self.zip4 = zip4 self._standardized = ""
[docs] def zipcode(self): """Returns the zipcode based on whether or not `zip4` is used.""" if self.zip4: return f"{self.zip5}-{self.zip4}" return self.zip5
[docs] def original(self): """Return the non-standardized address format""" lines = [ self.name, self.suite, self.street, f"{self.city}, {self.state} {self.zipcode()}", ] return "\n".join([x for x in lines if x])
[docs] def standardized(self): """Return the standardized address format""" if self._standardized: return self._standardized api = AddressStandardizationWebTool( self.street, self.city, self.state, self.name, self.suite, self.zip5, self.zip4, ) result, warning, error = api.get_standardized_address() lines = [] if not bool(error): lines = [result["name"]] if result["suite"]: lines.append(f"{result['street']} {result['suite']}") else: lines.append(result["street"]) if result["zip4"]: lines.append( f"{result['city']} {result['state']} {result['zip5']}-{result['zip4']}" ) else: lines.append( f"{result['city']} {result['state']} {result['zip5']}" ) self._standardized = " ".join([x for x in lines if x]) self._warning = warning self._error = error return self._standardized
[docs] class USPSShippingAPI: """ Representation of the USPS Shipping API https://www.usps.com/business/web-tools-apis/address-information-api.htm """ url = "https://production.shippingapis.com/ShippingAPI.dll" def __init__(self, api, userid=USPS_USERID): self.userid = userid self.api = api def _xml_payload(self): """ Child classes should override this to return a string appropriate for its API. """ raise NotImplementedError("Must override this function!")
[docs] def send_request(self): """ Send the request and return the XML response. """ response = requests.get( USPSShippingAPI.url, params={"API": self.api, "XML": self._xml_payload()}, timeout=30, ) root = xml_et.fromstring(response.content) error = "" if bool( (response.status_code != 200) or (root.tag.lower() == "error") or root.findall(".//Error") ): error = get_text(root, "./Address/Error/Description") # Check for any returned messages. There's shouldn't be any, so treat # it as a warning. warning = root.find(".//ReturnText") warning = warning.text if warning is not None else "" return root, warning, error
[docs] class AddressStandardizationWebTool(USPSShippingAPI): """ Object to get a standardized USPS Address. """ api = "Verify" def __init__( self, street, city, state, name=None, suite=None, zip5=None, zip4=None, userid=USPS_USERID, ): super().__init__( userid=userid, api=AddressStandardizationWebTool.api ) self.name = name self.suite = suite # or APT self.street = street self.city = city self.state = state self.zip5 = zip5 self.zip4 = zip4
[docs] def get_standardized_address(self): """ Returns a standardized format of the object's address. """ root, warning, error = self.send_request() standardized = {} if not bool(error): standardized = { "name": get_text(root, "./Address/FirmName"), "suite": get_text(root, "./Address/Address1"), "street": get_text(root, "./Address/Address2"), "city": get_text(root, "./Address/City"), "state": get_text(root, "./Address/State"), "zip5": get_text(root, "./Address/Zip5"), "zip4": get_text(root, "./Address/Zip4"), } return standardized, warning, error
def _xml_payload(self): """ Returns the appropriate XML format for an 'Address Standardization Web Tool' request. """ root = xml_et.Element("AddressValidateRequest") root.set("USERID", self.userid) include_opt = xml_et.Element("IncludeOptionalElements") include_opt.text = "false" root.append(include_opt) return_carrier_route = xml_et.Element("ReturnCarrierRoute") return_carrier_route.text = "false" root.append(return_carrier_route) address = xml_et.Element("Address") address.set("ID", "0") firm_element = xml_et.Element("FirmName") firm_element.text = self.name address.append(firm_element) address1_element = xml_et.Element("Address1") address1_element.text = self.suite address.append(address1_element) address2_element = xml_et.Element("Address2") address2_element.text = self.street address.append(address2_element) city_element = xml_et.Element("City") city_element.text = self.city address.append(city_element) state_element = xml_et.Element("State") state_element.text = self.state address.append(state_element) zip5_element = xml_et.Element("Zip5") zip5_element.text = self.zip5 address.append(zip5_element) zip4_element = xml_et.Element("Zip4") zip4_element.text = self.zip4 address.append(zip4_element) root.append(address) return xml_et.tostring(root)