Source code for parsons.targetsmart.targetsmart_api

import requests
import os
import petl
from parsons.etl.table import Table


class TargetSmartConnector(object):

    def __init__(self, api_key=None, uri='https://api.targetsmart.com/'):

        if api_key is None:

            try:
                api_key = os.environ['TS_API_KEY']
            except KeyError:
                raise KeyError('No TargetSmart API key found. Please store'
                               ' in environment variable or pass as an'
                               'argument.')

        self.uri = uri
        self.api_key = api_key
        self.headers = {'x-api-key': self.api_key}

    def request(self, url, args=None, raw=False):

        r = requests.get(url, headers=self.headers, params=args)

        # This allows me to deal with data that needs to be munged.
        if raw:

            return r.json()

        return Table(r.json()['output'])


class Person(object):

    def __init__(self):

        return None

    def data_enhance(self, search_id, search_id_type='voterbase', state=None):
        """
        Searches for a record based on an id or phone or email address

        `Args:`
            search_id: str
                The primary key or email address or phone number
            search_id_type: str
                One of ``voterbase``, ``exacttrack``, ``abilitec_consumer_link``, ``phone``,
                ``email``, ``smartvan``, ``votebuilder``, ``voter``, ``household``.
            state: str
                Two character state code. Required if ``search_id_type`` of ``smartvan``,
                ``votebuilder`` or ``voter``.
        `Returns`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        if search_id_type in ['smartvan', 'votebuilder', 'voter'] and state is None:

            raise KeyError("Search ID type '{}' requires state kwarg".format(search_id_type))

        if search_id_type not in ('voterbase', 'exacttrack', 'abilitec_consumer_link', 'phone',
                                  'email', 'smartvan', 'votebuilder', 'voter', 'household'):

            raise ValueError('Search_id_type is not valid')

        url = self.connection.uri + 'person/data-enhance'

        args = {'search_id': search_id,
                'search_id_type': search_id_type,
                'state': state
                }

        return self.connection.request(url, args=args)

    def radius_search(self, first_name, last_name, middle_name=None, name_suffix=None,
                      latitude=None, longitude=None, address=None, address_type='reg',
                      radius_size=10, radius_unit='miles', max_results=10, gender='a',
                      age_min=None, age_max=None, composite_score_min=1, composite_score_max=100,
                      last_name_exact=True, last_name_is_prefix=False, last_name_prefix_length=10):
        """
        Search for a person based on a specified radius

        `Args`:
            first_name: str
                One or more alpha characters
            last_name: str
                One or more alpha characters
            middle_name: str
                One or more alpha characters
            name_suffix: str
                One or more alpha characters
            latitude: float
                Floating point number (e.g. 33.738987255507)
            longitude: float
                Floating point number (e.g. -116.40833849559)
            address: str
                Any geocode-able address
            address_type: str
                ``reg`` for registration (default) or ``tsmart`` for TargetSmart
            radius_unit: str
                One of ``meters``, ``feet``, ``miles`` (default), or ``kilometers``.
            max_results: int
                Default of ``10``. An integer in range [0 - 100]
            gender: str
                Default of ``a``. One of ``m``, ``f``, ``u``, ``a``.
            age_min: int
                A positive integer
            age_max: int
                A positive integer
            composite_score_min: int
                An integer in range [1 - 100]. Filter out results with composite score
                less than this value.
            composite_score_max: int
                An integer in range [1 - 100]. Filter out results with composite score
                greater than this value.
            last_name_exact: boolean
                By default, the full last name is used for finding matches if the length of the
                last name is not longer than 10 characters. As an example, “anders” is less likely
                to match to “anderson” with this enabled. Disable this option if you are using
                either ``last_name_is_prefix`` or ``last_name_prefix_length``.
            last_name_is_prefix: boolean
                By default, the full last name is used for finding matches. Enable this parameter
                if your search last name is truncated. This can be common for some client
                applications that for various reasons do not have full last names. Use this
                parameter along with ``last_name_prefix_length`` to configure the length of the last
                name prefix. This parameter is ignored if ``last_name_exact`` is enabled.
            last_name_prefix_length: int
                By default, up to the first 10 characters of the search last name are used for
                finding relative matches. This value must be between 3 and 10. This parameter is
                ignored if last_name_exact is enabled.
        `Returns`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        if (latitude is None or longitude is None) and address is None:
            raise ValueError('Lat/Long or Address required')

        # Convert booleans
        for a in [last_name_exact, last_name_is_prefix]:
            a = str(a)

        url = self.connection.uri + 'person/radius-search'

        args = {'first_name': first_name,
                'last_name': last_name,
                'middle_name': middle_name,
                'name_suffix': name_suffix,
                'latitude': latitude,
                'longitude': longitude,
                'address': address,
                'radius_size': radius_size,
                'radius_unit': radius_unit,
                'max_results': max_results,
                'gender': gender,
                'age_min': age_min,
                'age_max': age_max,
                'composite_score_min': composite_score_min,
                'composite_score_max': composite_score_max,
                'last_name_exact': last_name_exact,
                'last_name_is_prefix': last_name_is_prefix,
                'last_name_prefix_length': last_name_prefix_length
                }

        r = self.connection.request(url, args=args, raw=True)
        return Table([itm for itm in r['output']]).unpack_dict('data_fields', prepend=False)

    def phone(self, table):
        """
        Match based on a list of 500 phones numbers. Table
        can contain up to 500 phone numbers to match

        `Args:`
            table: parsons table
                See :ref:`parsons-table`. One row per phone number,
                up to 500 phone numbers.
        `Returns:`
            See :ref:`parsons-table` for output options.
        """

        url = self.connection.uri + 'person/phone-search'

        args = {'phones': list(petl.values(table.table, 0))}

        return Table(self.connection.request(url, args=args, raw=True)['result'])


class Service(object):

    def __init__(self):

        return None

    def district(self, search_type='zip', address=None, zip5=None, zip4=None, state=None,
                 latitude=None, longitude=None):
        """
        Return district information based on a geographic point. The method allows you to
        search based on the following:

        .. list-table::
            :widths: 30 30 30
            :header-rows: 1

            * - Search Type
              - Search Type Name
              - Required kwarg(s)
            * - Zip Code
              - ``zip``
              - ``zip5``, ``zip4``
            * - Address
              - ``address``
              - ``address``
            * - Point
              - point
              - ``latitude``, ``longitude``

        `Args`:
            search_type: str
                The type of district search to perform. One of ``zip``, ``address``
                or ``point``.
            address: str
                An uparsed full address
            zip5: str
                The USPS Zip5 code
            zip4: str
                The USPS Zip4 code
            state: str
                The two character state code
            latitude: float or str
                Valid latitude floating point
            lontitude: float or str
                Valid longitude floating point
        `Returns`:
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        if search_type == 'zip' and None in [zip5, zip4]:
            raise ValueError("Search type 'zip' requires 'zip5' and 'zip4' arguments")

        elif search_type == 'point' and None in [latitude, longitude]:
            raise ValueError("Search type 'point' requires 'latitude' and 'longitude' arguments")

        elif search_type == 'address' and None in [address]:
            raise ValueError("Search type 'address' requires 'address' argument")

        elif search_type not in ['zip', 'point', 'address']:
            raise KeyError("Invalid 'search_type' provided. ")

        else:
            pass

        url = self.connection.uri + 'service/district'

        args = {'search_type': search_type,
                'address': address,
                'zip5': zip5,
                'zip4': zip4,
                'state': state,
                'latitude': latitude,
                'longitude': longitude
                }

        return Table([self.connection.request(url, args=args, raw=True)['match_data']])


class Voter(object):

    def __init__(self, connection):

        self.connection = connection

    def voter_registration_check(self, first_name=None, last_name=None,
                                 state=None, street_number=None,
                                 street_name=None, city=None, zip_code=None,
                                 age=None, dob=None, phone=None, email=None,
                                 unparsed_full_address=None,
                                 obj_type="dict"):
        """
        Searches for a registered individual, returns matches.

        A search must include the at minimum first name, last name and state.

        `Args:`
            first_name: str
                Required; One or more alpha characters. Trailing wildcard allowed
            last_name: str
                Required; One or more alpha characters. Trailing wildcard allowed
            state: str
                Required; Two character state code (e.g. ``NY``)
            street_number: str
                Optional; One or more alpha characters. Trailing wildcard allowed
            street_name: str
                Optional; One or more alpha characters. Trailing wildcard allowed
            city: str
                Optional; The person's home city
            zip_code: str
                Optional; Numeric characters. Trailing wildcard allowed
            age; int
                Optional; One or more integers. Trailing wildcard allowed
            dob; str
                Numeric characters in YYYYMMDD format. Trailing wildcard allowed
            phone; str
                Integer followed by 0 or more * or integers
            email: str
                Alphanumeric character followed by 0 or more * or legal characters
                (alphanumeric, @, -, .)
            unparsed_full_address: str
                One or more alphanumeric characters. No wildcards.
        `Returns`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        url = self.connection.uri + 'voter/voter-registration-check'

        if None in [first_name, last_name, state]:
            raise ValueError("""Function must include at least first_name,
                             last_name, and state.""")

        args = {'first_name': first_name,
                'last_name': last_name,
                'state': state,
                'street_number': street_number,
                'street_name': street_name,
                'city': city,
                'zip_code': zip_code,
                'age': age,
                'dob': dob,
                'phone': phone,
                'email': email,
                'unparsed_full_address': unparsed_full_address
                }

        return self.connection.request(url, args=args, raw=True)


[docs]class TargetSmartAPI(Voter, Person, Service): def __init__(self, api_key=None): self.connection = TargetSmartConnector(api_key=api_key)