Source code for send_the_raven.validator
from .address import Addresses, Address, DEFAULT_ADDRESS_MAPPING
from .utils import split_into_n_elements
from xmltodict import parse, unparse
from urllib.parse import quote_plus
from aiohttp import ClientSession
from aiohttp.connector import TCPConnector
import asyncio
from typing import Iterable, Any
XML_DOC = """<?xml version="1.0"?>
<AddressValidateRequest USERID="{}">
<Revision>1</Revision>
</AddressValidateRequest>"""
def _prepare_xml(current_addresses: list[Address], usps_id: str) -> str:
"""
Prepare XML based on USPS Standard
https://www.usps.com/business/web-tools-apis/address-information-api.pdf
Up to 5 addresses per request.
Args:
current_addresses (list[Address]): Addresses to validate
returns:
string: not URLencoded XML string
"""
xml = parse(XML_DOC)
xml["AddressValidateRequest"]["@USERID"] = usps_id
xml["AddressValidateRequest"]["Address"] = []
for address in current_addresses[:5]:
xml["AddressValidateRequest"]["Address"].append(
{
"@ID": address.id,
"Address1": address.address_line_2,
"Address2": address.street,
"City": address.city,
"State": address.state,
"Zip5": address.zip_code,
"Zip4": None,
}
)
return unparse(xml)
[docs]class Validator(Addresses):
"""
Class to validate addresses to `USPS database <https://www.usps.com/business/web-tools-apis/documentation-updates.htm>`_.
Args:
data (Addresses): Addresses to validate.
usps_id (str): USPS ID to use for validation. Get it `here <https://registration.shippingapis.com/>`_.
request_limit (int): Maximum number of requests per second.
Attributes:
data (list[Address]): full address.
results (list[Addresses]): validated addresses.
errors (list[tuple[Address, Exception]]): addresses with errors.
Example:
>>> addresses = [
{"street": "123 Main St", "city": "Anytown", "state": "CA", "zip": "12345"},
{"street": "456 Oak Rd", "city": "Forest", "state": "VT", "zip": "67890"}
]
>>> validator = Validator(addresses, usps_id="MY_ID")
>>> valid_addresses = validator()
>>> for addr in valid_addresses:
>>> print(addr.street, addr.city, addr.state, addr.zip_code)
"""
[docs] def __init__(
self,
addresses: Iterable[Any],
usps_id: str,
request_limit: int = 10,
field_mapping: dict[str, str] = DEFAULT_ADDRESS_MAPPING,
):
super().__init__(addresses, field_mapping)
self.usps_id = usps_id
self.request_limit = request_limit
[docs] async def validate(self, data: list[Address], client: ClientSession):
"""
Request validation data to USPS database
Args:
data (list[Address]): Addresses to validate.
client (aiohttp.ClientSession): Initialized aiohttp ClientSession
"""
xml_string = _prepare_xml(data, self.usps_id)
url = f"https://secure.shippingapis.com/ShippingAPI.dll?API=Verify&XML={quote_plus(xml_string)}"
try:
res = await client.get(url)
result = parse(await res.text())
for address in result["AddressValidateResponse"]["Address"]:
if "Error" in address:
current_error_address = Address(id=address["@ID"])
self.errors.append(
(
current_error_address,
Exception(address["Error"]["Description"]),
)
)
self.results.append(current_error_address)
else:
self.results.append(
Address(
street=address["Address2"],
address_line_2=address["Address1"]
if "Address1" in address
else None,
city=address["City"],
state=address["State"],
zip_code=address["Zip4"],
id=address["@ID"],
)
)
except Exception as e:
for address in data:
self.errors.append((address, e))
self.results.append(address)
[docs] async def start_validation(self):
"""
Initialize aiohttp TCPConnector with limit and ClientSession.
Start validation in asyncio.
"""
connector = TCPConnector(limit_per_host=self.request_limit or 10)
async with ClientSession(connector=connector) as session:
await asyncio.gather(
*[
self.validate(current_addresses, session)
for current_addresses in split_into_n_elements(self.addresses)
]
)
[docs] def __call__(self):
"""
Start validation process. Will return after all async process
are completed.
returns:
Addresses: validated addresses.
"""
self.results = []
self.errors = []
try:
asyncio.run(self.start_validation())
except RuntimeError:
asyncio.create_task(self.start_validation())
self.addresses = self.results
return self.addresses