""" zipcodes ~~~~~~~~ No-SQLite U.S. zipcode validation Python package, ready for use in AWS Lambda :author: Sean Pianka :github: @seanpianka """ import os import re import sys import bz2 import json import warnings if sys.version_info >= (3, 0): bz2_open = bz2.open else: bz2_open = bz2.BZ2File __author__ = "Sean Pianka" __email__ = "pianka@eml.cc" __license__ = "MIT" __package__ = "zipcodes" __version__ = "1.1.0" _digits = re.compile(r"[^\d\-]") _valid_zipcode_length = 5 _zips_json = os.path.join(os.path.dirname(os.path.abspath(__file__)), "zips.json.bz2") with bz2_open(_zips_json, "rb") as f: _zips = json.loads(f.read().decode("ascii")) def _clean_zipcode(f): def decorator(zipcode, *args, **kwargs): if not zipcode or not isinstance(zipcode, str): raise TypeError("Invalid type, zipcode must be a string.") return f( _clean(zipcode, min(len(zipcode), _valid_zipcode_length)), *args, **kwargs ) return decorator @_clean_zipcode def matching(zipcode, zips=None): """ Retrieve zipcode dict for provided zipcode """ if zips is None: zips = _zips return [z for z in zips if z["zip_code"] == zipcode] @_clean_zipcode def is_valid(zipcode): warnings.warn("is_valid is deprecated; use is_real", warnings.DeprecationWarning) return is_real(zipcode) @_clean_zipcode def is_real(zipcode): """ Determine whether a given zip or zip+4 zipcode is real. """ return bool(matching(zipcode)) @_clean_zipcode def similar_to(partial_zipcode, zips=None): """ List of zipcode dicts where zipcode prefix matches `partial_zipcode` """ if zips is None: zips = _zips return [z for z in zips if z["zip_code"].startswith(partial_zipcode)] def filter_by(zips=None, **filters): """ Use `kwargs` to select for desired attributes from list of zipcode dicts """ if zips is None: zips = _zips return [ zip for zip in zips if all([key in zip and zip[key] == value for key, value in filters.items()]) ] def list_all(zips=None): """ Return a list containing all zip-code objects. """ if zips is None: zips = _zips return zips def _contains_nondigits(s): return bool(_digits.search(s)) def _clean(zipcode, valid_length=_valid_zipcode_length): """ Assumes zipcode is of type `str` """ zipcode = zipcode.split("-")[0] # Convert #####-#### to ##### if len(zipcode) != valid_length: raise ValueError( 'Invalid format, zipcode must be of the format: "#####" or "#####-####"' ) if _contains_nondigits(zipcode): raise ValueError('Invalid characters, zipcode may only contain digits and "-".') return zipcode