Executes most of the request.

        The parallelism parameter is useful to avoid swamping the API service with calls. Thus
        the entire set of requests won't be all made at once, but in chunked groups.

        :param endpoint: string indicating the URL component to call
        :param data: the JSON ready data to submit (list of dictionaries of addresses)
        :param parallelism: number of simultaneous requests to make.
        :return: a tuple of an AddressCollection and a dictionary of the response codes and the
                count for each.

def post(self, endpoint, data, parallelism=5):
        """
        Executes most of the request.

        The parallelism parameter is useful to avoid swamping the API service with calls. Thus
        the entire set of requests won't be all made at once, but in chunked groups.

        :param endpoint: string indicating the URL component to call
        :param data: the JSON ready data to submit (list of dictionaries of addresses)
        :param parallelism: number of simultaneous requests to make.
        :return: a tuple of an AddressCollection and a dictionary of the response codes and the
                count for each.
        """
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "x-standardize-only": "true" if self.standardize else "false",
            "x-include-invalid": "true" if self.invalid else "false",
            "x-accept-keypair": "true" if self.accept_keypair else "false",
        }
        if not self.logging:
            headers["x-suppress-logging"] = "false"

        params = {"auth-id": self.auth_id, "auth-token": self.auth_token}
        url = self.BASE_URL + endpoint

        rs = (
            grequests.post(
                url=url,
                data=json.dumps(stringify(data_chunk)),
                params=params,
                headers=headers,
            )
            for data_chunk in chunker(data, 100)
        )

        responses = grequests.imap(rs, size=parallelism)
        status_codes = {}
        addresses = AddressCollection([])
        for response in responses:
            if response.status_code not in status_codes.keys():
                status_codes[response.status_code] = 1
            else:
                status_codes[response.status_code] += 1

            if response.status_code == 200:
                addresses[0:0] = AddressCollection(
                    response.json()
                )  # Fast list insertion

            # If an auth error is raised, it's safe to say that this is
            # going to affect every request, so raise the exception immediately..
            elif response.status_code == 401:
                raise ERROR_CODES[401]

        # The return value or exception is simple if it is consistent.
        if len(status_codes.keys()) == 1:
            if 200 in status_codes:
                return addresses, status_codes

            else:
                raise ERROR_CODES.get(status_codes.keys()[0], SmartyStreetsError)

        # For any other mix not really sure of the best way to handle it. If it's a mix of 200
        # and error codes, then returning the resultant addresses and status code dictionary
        # seems pretty sensible. But if it's a mix of all error codes (could be a mix of payment
        # error, input error, potentially server error) this will probably require careful
        # checking in the code using this interface.
        return addresses, status_codes