Ejemplo n.º 1
0
class VANConnector(object):

    def __init__(self, api_key=None, auth_name='default', db=None):

        self.api_key = check_env.check('VAN_API_KEY', api_key)

        if db == 'MyVoters':
            self.db_code = 0
        elif db in ['MyMembers', 'MyCampaign', 'EveryAction']:
            self.db_code = 1
        else:
            raise KeyError('Invalid database type specified. Pick one of:'
                           ' MyVoters, MyCampaign, MyMembers, EveryAction.')

        self.uri = URI
        self.db = db
        self.auth_name = auth_name
        self.auth = (self.auth_name, self.api_key + '|' + str(self.db_code))
        self.api = APIConnector(self.uri, auth=self.auth, data_key='items')

    def get_request(self, endpoint, **kwargs):

        r = self.api.get_request(self.uri + endpoint, **kwargs)
        data = self.api.data_parse(r)

        # Paginate
        while self.api.next_page_check_url(r):
            r = self.api.get_request(r[self.pagination_key], **kwargs)
            data.extend(self.api.data_parse(r))

        return data

    def post_request(self, endpoint, **kwargs):

        return self.api.post_request(self.uri + endpoint, **kwargs)

    def delete_request(self, endpoint, **kwargs):

        return self.api.delete_request(self.uri + endpoint, **kwargs)

    def patch_request(self, endpoint, **kwargs):

        return self.api.patch_request(self.uri + endpoint, **kwargs)

    def put_request(self, endpoint, **kwargs):

        return self.api.put_request(self.uri + endpoint, **kwargs)
Ejemplo n.º 2
0
class Zoom:
    """
    Instantiate the Zoom class.

    `Args:`
        api_key: str
            A valid Zoom api key. Not required if ``ZOOM_API_KEY`` env
            variable set.
        api_secret: str
            A valid Zoom api secret. Not required if ``ZOOM_API_SECRET`` env
            variable set.
    """

    def __init__(self, api_key=None, api_secret=None):

        self.api_key = check_env.check('ZOOM_API_KEY', api_key)
        self.api_secret = check_env.check('ZOOM_API_SECRET', api_secret)
        self.client = APIConnector(ZOOM_URI)

    def refresh_header_token(self):
        # Generate a token that is valid for 30 seconds and update header. Full documentation
        # on JWT generation using Zoom API: https://marketplace.zoom.us/docs/guides/auth/jwt

        payload = {"iss": self.api_key, "exp": int(datetime.datetime.now().timestamp() + 30)}
        token = jwt.encode(payload, self.api_secret, algorithm='HS256').decode("utf-8")
        self.client.headers = {'authorization': f"Bearer {token}",
                               'content-type': "application/json"}

    def _get_request(self, endpoint, data_key, params=None, **kwargs):
        # To Do: Consider increasing default page size.

        self.refresh_header_token()
        r = self.client.get_request(endpoint, params=params, **kwargs)
        self.client.data_key = data_key
        data = self.client.data_parse(r)

        if not params:
            params = {}

        # Return a dict or table if only one item.
        if 'page_number' not in r.keys():
            if isinstance(data, dict):
                return data
            if isinstance(data, list):
                return Table(data)

        # Else iterate through the pages and return a Table
        else:
            while r['page_number'] < r['page_count']:
                params['page_number'] = int(r['page_number']) + 1
                r = self.client.get_request(endpoint, params=params, **kwargs)
                data.extend(self.client.data_parse(r))
            return Table(data)

    def get_users(self, status='active', role_id=None):
        """
        Get users.

        `Args:`
            status: str
                Filter by the user status. Must be one of following: ``active``,
                ``inactive``, or ``pending``.
            role_id: str
                Filter by the user role.
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        if status not in ['active', 'inactive', 'pending']:
            raise ValueError('Invalid status type provided.')

        params = {'status': status,
                  'role_id': role_id}

        tbl = self._get_request('users', 'users', params=params)
        logger.info(f'Retrieved {tbl.num_rows} users.')
        return tbl

    def get_meetings(self, user_id, meeting_type='scheduled'):
        """
        Get meetings scheduled by a user.

        `Args:`
            user_id: str
                A user id or email address of the meeting host.
            meeting_type: str

                .. list-table::
                    :widths: 25 50
                    :header-rows: 1

                    * - Type
                      - Notes
                    * - ``scheduled``
                      - This includes all valid past meetings, live meetings and upcoming
                        scheduled meetings. It is the equivalent to the combined list of
                        "Previous Meetings" and "Upcoming Meetings" displayed in the user's
                        Meetings page.
                    * - ``live``
                      - All the ongoing meetings.
                    * - ``upcoming``
                      - All upcoming meetings including live meetings.
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        tbl = self._get_request(f'users/{user_id}/meetings', 'meetings')
        logger.info(f'Retrieved {tbl.num_rows} meetings.')
        return tbl

    def get_past_meeting(self, meeting_uuid):
        """
        Get metadata regarding a past meeting.

        `Args:`
            meeting_id: str
                The meeting id
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        tbl = self._get_request(f'past_meetings/{meeting_uuid}', None)
        logger.info(f'Retrieved meeting {meeting_uuid}.')
        return tbl

    def get_past_meeting_participants(self, meeting_id):
        """
        Get past meeting participants

        `Args:`
            meeting_id: str
                The meeting id
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        tbl = self._get_request(f'report/meetings/{meeting_id}/participants', 'participants')
        logger.info(f'Retrieved {tbl.num_rows} participants.')
        return tbl
Ejemplo n.º 3
0
class Mailchimp():
    """
    Instantiate Mailchimp Class

    `Args:`
        api_key:
            The Mailchimp-provided application key. Not required if
            ``MAILCHIMP_API_KEY`` env variable set.
    `Returns:`
        Mailchimp Class
    """
    def __init__(self, api_key=None):
        self.api_key = check_env.check('MAILCHIMP_API_KEY', api_key)
        self.domain = re.findall("(?<=-).+$", self.api_key)[0]
        self.uri = f'https://{self.domain}.api.mailchimp.com/3.0/'
        self.client = APIConnector(self.uri, auth=('x', self.api_key))

    def get_lists(self,
                  fields=None,
                  exclude_fields=None,
                  count=None,
                  offset=None,
                  before_date_created=None,
                  since_date_created=None,
                  before_campaign_last_sent=None,
                  since_campaign_last_sent=None,
                  email=None,
                  sort_field=None,
                  sort_dir=None):
        """
        Get a table of lists under the account based on query parameters. Note
        that argument descriptions here are sourced from Mailchimp's official
        API documentation.

        `Args:`
            fields: list of strings
                A comma-separated list of fields to return. Reference
                parameters of sub-objects with dot notation.
            exclude_fields: list of strings
                A comma-separated list of fields to exclude. Reference
                parameters of sub-objects with dot notation.
            count: int
                The number of records to return. Default value is 10. Maximum
                value is 1000.
            offset: int
                The number of records from a collection to skip. Iterating over
                large collections with this parameter can be slow. Default
                value is 0.
            before_date_created: string
                Restrict response to lists created before the set date. We
                recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00.
            since_date_created: string
                Restrict results to lists created after the set date. We
                recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00.
            before_campaign_last_sent: string
                Restrict results to lists created before the last campaign send
                date. We recommend ISO 8601 time format:
                2015-10-21T15:41:36+00:00.
            since_campaign_last_sent: string
                Restrict results to lists created after the last campaign send
                date. We recommend ISO 8601 time format:
                2015-10-21T15:41:36+00:00.
            email: string
                Restrict results to lists that include a specific subscriber's
                email address.
            sort_field: string, can only be 'date_created' or None
                Returns files sorted by the specified field.
            sort_dir: string, can only be 'ASC', 'DESC', or None
                Determines the order direction for sorted results.

        `Returns:`
            Table Class
        """
        params = {
            'fields': fields,
            'exclude_fields': exclude_fields,
            'count': count,
            'offset': offset,
            'before_date_created': before_date_created,
            'since_date_created': since_date_created,
            'before_campaign_last_sent': before_campaign_last_sent,
            'since_campaign_last_sent': since_campaign_last_sent,
            'email': email,
            'sort_field': sort_field,
            'sort_dir': sort_dir
        }

        response = self.client.get_request('lists', params=params)
        tbl = Table(response['lists'])
        logger.info(f'Found {tbl.num_rows} lists.')
        if tbl.num_rows > 0:
            return tbl
        else:
            return Table()

    def get_campaigns(self,
                      fields=None,
                      exclude_fields=None,
                      count=None,
                      offset=None,
                      type=None,
                      status=None,
                      before_send_time=None,
                      since_send_time=None,
                      before_create_time=None,
                      since_create_time=None,
                      list_id=None,
                      folder_id=None,
                      member_id=None,
                      sort_field=None,
                      sort_dir=None):
        """
        Get a table of campaigns under the account based on query parameters.
        Note that argument descriptions here are sourced from Mailchimp's
        official API documentation.

        `Args:`
            fields: list of strings
                A comma-separated list of fields to return. Reference
                parameters of sub-objects with dot notation.
            exclude_fields: list of strings
                A comma-separated list of fields to exclude. Reference
                parameters of sub-objects with dot notation.
            count: int
                The number of records to return. Default value is 10. Maximum
                value is 1000.
            offset: int
                The number of records from a collection to skip. Iterating over
                large collections with this parameter can be slow. Default
                value is 0.
            type: string, can only be 'regular', 'plaintext', 'absplit', 'rss',
            'variate', or None
                The campaign type.
            status: string, can only be 'save', 'paused', 'schedule',
            'sending', 'sent', or None
                The status of the campaign.
            before_send_time: string
                Restrict the response to campaigns sent before the set time. We
                recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00.
            since_send_time: string
                Restrict the response to campaigns sent after the set time. We
                recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00.
            before_create_time: string
                Restrict the response to campaigns created before the set time.
                We recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00.
            since_create_time: string
                Restrict the response to campaigns created after the set time.
                We recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00.
            list_id: string
                The unique id for the list.
            folder_id: string
                The unique folder id.
            member_id: string
                Retrieve campaigns sent to a particular list member. Member ID
                is The MD5 hash of the lowercase version of the list member’s
                email address.
            sort_field: string, can only be 'create_time', 'send_time', or None
                Returns files sorted by the specified field.
            sort_dir: string, can only be 'ASC', 'DESC', or None
                Determines the order direction for sorted results.

        `Returns:`
            Table Class
        """
        params = {
            'fields': fields,
            'exclude_fields': exclude_fields,
            'count': count,
            'offset': offset,
            'type': type,
            'status': status,
            'before_send_time': before_send_time,
            'since_send_time': since_send_time,
            'before_create_time': before_create_time,
            'since_create_time': since_create_time,
            'list_id': list_id,
            'folder_id': folder_id,
            'member_id': member_id,
            'sort_field': sort_field,
            'sort_dir': sort_dir
        }

        response = self.client.get_request('campaigns', params=params)
        tbl = Table(response['campaigns'])
        logger.info(f'Found {tbl.num_rows} campaigns.')
        if tbl.num_rows > 0:
            return tbl
        else:
            return Table()

    def get_members(self,
                    list_id,
                    fields=None,
                    exclude_fields=None,
                    count=None,
                    offset=None,
                    email_type=None,
                    status=None,
                    since_timestamp_opt=None,
                    before_timestamp_opt=None,
                    since_last_changed=None,
                    before_last_changed=None,
                    unique_email_id=None,
                    vip_only=False,
                    interest_category_id=None,
                    interest_ids=None,
                    interest_match=None,
                    sort_field=None,
                    sort_dir=None,
                    since_last_campaign=None,
                    unsubscribed_since=None):
        """
        Get a table of members in a list based on query parameters. Note that
        argument descriptions here are sourced from Mailchimp's official API
        documentation.

        `Args:`
            list_id: string
                The unique ID of the list to fetch members from.
            fields: list of strings
                A comma-separated list of fields to return. Reference
                parameters of sub-objects with dot notation.
            exclude_fields: list of fields as strings
                A comma-separated list of fields to exclude. Reference
                parameters of sub-objects with dot notation.
            count: int
                The number of records to return. Default value is 10. Maximum
                value is 1000.
            offset: int
                The number of records from a collection to skip. Iterating over
                large collections with this parameter can be slow. Default
                value is 0.
            email_type: string
                The email type.
            status: string, can only be 'subscribed', 'unsubscribed',
            'cleaned', 'pending', 'transactional', 'archived', or None
                The subscriber's status.
            since_timestamp_opt: string
                Restrict results to subscribers who opted-in after the set
                timeframe. We recommend ISO 8601 time format:
                2015-10-21T15:41:36+00:00.
            before_timestamp_opt: string
                Restrict results to subscribers who opted-in before the set
                timeframe. We recommend ISO 8601 time format:
                2015-10-21T15:41:36+00:00.
            since_last_changed: string
                Restrict results to subscribers whose information changed after
                the set timeframe. We recommend ISO 8601 time format:
                2015-10-21T15:41:36+00:00.
            before_last_changed: string
                Restrict results to subscribers whose information changed
                before the set timeframe. We recommend ISO 8601 time format:
                2015-10-21T15:41:36+00:00.
            unique_email_id: string
                A unique identifier for the email address across all Mailchimp
                lists. This parameter can be found in any links with Ecommerce
                Tracking enabled.
            vip_only: boolean
                A filter to return only the list's VIP members. Passing true
                will restrict results to VIP list members, passing false will
                return all list members.
            interest_category_id: string
                The unique id for the interest category.
            interest_ids: list of strings
                Used to filter list members by interests. Must be accompanied
                by interest_category_id and interest_match. The value must be a
                comma separated list of interest ids present for any supplied
                interest categories.
            interest_match: string, can only be 'any', 'all', 'none', or None
                Used to filter list members by interests. Must be accompanied
                by interest_category_id and interest_ids. "any" will match a
                member with any of the interest supplied, "all" will only match
                members with every interest supplied, and "none" will match
                members without any of the interest supplied.
            sort_field: string, can only be 'timestamp_opt',
            'timestamp_signup', 'last_changed', or None
                Returns files sorted by the specified field.
            sort_dir: string, can only be 'ASC', 'DESC', or None
                Determines the order direction for sorted results.
            since_last_campaign: string
                Filter subscribers by those
                subscribed/unsubscribed/pending/cleaned since last email
                campaign send. Member status is required to use this filter.
            unsubscribed_since: string
                Filter subscribers by those unsubscribed since a specific date.
                Using any status other than unsubscribed with this filter will
                result in an error.

        `Returns:`
            Table Class
        """
        params = {
            'fields': fields,
            'exclude_fields': exclude_fields,
            'count': count,
            'offset': offset,
            'email_type': email_type,
            'status': status,
            'since_timestamp_opt': since_timestamp_opt,
            'before_timestamp_opt': before_timestamp_opt,
            'since_last_changed': since_last_changed,
            'before_last_changed': before_last_changed,
            'unqiue_email_id': unique_email_id,
            'vip_only': vip_only,
            'interest_category_id': interest_category_id,
            'interest_ids': interest_ids,
            'interest_match': interest_match,
            'sort_field': sort_field,
            'sort_dir': sort_dir,
            'since_last_campaign': since_last_campaign,
            'unsubscribed_since': unsubscribed_since
        }

        response = self.client.get_request(f'lists/{list_id}/members',
                                           params=params)
        tbl = Table(response['members'])
        logger.info(f'Found {tbl.num_rows} members.')
        if tbl.num_rows > 0:
            return tbl
        else:
            return Table()

    def get_campaign_emails(self,
                            campaign_id,
                            fields=None,
                            exclude_fields=None,
                            count=None,
                            offset=None,
                            since=None):
        """
        Get a table of individual emails from a campaign based on query
        parameters. Note that argument descriptions here are sourced from
        Mailchimp's official API documentation.

        `Args:`
            campaign_id: string
                The unique ID of the campaign to fetch emails from.
            fields: list of strings
                A comma-separated list of fields to return. Reference
                parameters of sub-objects with dot notation.
            exclude_fields: list of strings
                A comma-separated list of fields to exclude. Reference
                parameters of sub-objects with dot notation.
            count: int
                The number of records to return. Default value is 10. Maximum
                value is 1000.
            offset: int
                The number of records from a collection to skip. Iterating over
                large collections with this parameter can be slow. Default
                value is 0.
            since: string
                Restrict results to email activity events that occur after a
                specific time. We recommend ISO 8601 time format:
                2015-10-21T15:41:36+00:00.

        `Returns:`
            Table Class
        """
        params = {
            'fields': fields,
            'exclude_fields': exclude_fields,
            'count': count,
            'offset': offset,
            'since': since
        }

        response = self.client.get_request(
            f'reports/{campaign_id}/email-activity', params=params)
        tbl = Table(response['emails'])
        if tbl.num_rows > 0:
            return tbl
        else:
            return Table()

    def get_unsubscribes(self,
                         campaign_id,
                         fields=None,
                         exclude_fields=None,
                         count=None,
                         offset=None):
        """
        Get a table of unsubscribes associated with a campaign based on query
        parameters. Note that argument descriptions here are sourced from
        Mailchimp's official API documentation.

        `Args:`
            campaign_id: string
                The unique ID of the campaign to fetch unsubscribes from.
            fields: list of strings
                A comma-separated list of fields to return. Reference
                parameters of sub-objects with dot notation.
            exclude_fields: list of strings
                A comma-separated list of fields to exclude. Reference
                parameters of sub-objects with dot notation.
            count: int
                The number of records to return. Default value is 10. Maximum
                value is 1000.
            offset: int
                The number of records from a collection to skip. Iterating over
                large collections with this parameter can be slow. Default
                value is 0.

        `Returns:`
            Table Class
        """
        params = {
            'fields': fields,
            'exclude_fields': exclude_fields,
            'count': count,
            'offset': offset
        }

        response = self.client.get_request(
            f'reports/{campaign_id}/unsubscribed', params=params)
        tbl = Table(response['unsubscribes'])
        logger.info(f'Found {tbl.num_rows} unsubscribes for {campaign_id}.')
        if tbl.num_rows > 0:
            return tbl
        else:
            return Table()
Ejemplo n.º 4
0
class ActionNetwork(object):
    """
    `Args:`
        api_token: str
            The OSDI API token
    """
    def __init__(self, api_token=None):
        self.api_token = check_env.check('AN_API_TOKEN', api_token)
        self.headers = {
            "Content-Type": "application/json",
            "OSDI-API-Token": self.api_token
        }
        self.api_url = API_URL
        self.api = APIConnector(self.api_url, headers=self.headers)

    def _get_page(self, object_name, page, per_page=25):
        # returns data from one page of results
        if per_page > 25:
            per_page = 25
            logger.info(
                "Action Network's API will not return more than 25 entries per page. \
            Changing per_page parameter to 25.")
        page_url = f"{object_name}?page={page}&per_page={per_page}"
        return self.api.get_request(url=page_url)

    def _get_entry_list(self, object_name, limit=None, per_page=25):
        # returns a list of entries for a given object, such as people, tags, or actions
        count = 0
        page = 1
        return_list = []
        while True:
            response = self._get_page(object_name, page, per_page)
            page = page + 1
            response_list = response['_embedded'][f"osdi:{object_name}"]
            if not response_list:
                return Table(return_list)
            return_list.extend(response_list)
            count = count + len(response_list)
            if limit:
                if count >= limit:
                    return Table(return_list[0:limit])

    def get_people(self, limit=None, per_page=25, page=None):
        """
        `Args:`
            limit:
                The number of entries to return. When None, returns all entries.
            per_page
                The number of entries per page to return. 25 maximum.
            page
                Which page of results to return
        `Returns:`
            A list of JSONs of people stored in Action Network.
        """
        if page:
            self._get_page("people", page, per_page)
        return self._get_entry_list("people", limit, per_page)

    def get_person(self, person_id):
        """
        `Args:`
            person_id:
                Id of the person.
        `Returns:`
            A  JSON of the entry. If the entry doesn't exist, Action Network returns
            ``{'error': 'Couldn't find person with id = <id>'}``.
        """
        return self.api.get_request(url=f"people/{person_id}")

    def add_person(self,
                   email_address=None,
                   given_name=None,
                   family_name=None,
                   tags=None,
                   languages_spoken=None,
                   postal_addresses=None,
                   mobile_number=None,
                   mobile_status='subscribed',
                   **kwargs):
        """
        `Args:`
            email_address:
                Either email_address or mobile_number are required. Can be any of the following
                    - a string with the person's email
                    - a list of strings with a person's emails
                    - a dictionary with the following fields
                        - email_address (REQUIRED)
                        - primary (OPTIONAL): Boolean indicating the user's primary email address
                        - status (OPTIONAL): can taken on any of these values
                            - "subscribed"
                            - "unsubscribed"
                            - "bouncing"
                            - "previous bounce"
                            - "spam complaint"
                            - "previous spam complaint"
            given_name:
                The person's given name
            family_name:
                The person's family name
            tags:
                Any tags to be applied to the person
            languages_spoken:
                Optional field. A list of strings of the languages spoken by the person
            postal_addresses:
                Optional field. A list of dictionaries.
                For details, see Action Network's documentation:
                https://actionnetwork.org/docs/v2/person_signup_helper
            mobile_number:
                Either email_address or mobile_number are required. Can be any of the following
                    - a string with the person's cell phone number
                    - an integer with the person's cell phone number
                    - a list of strings with the person's cell phone numbers
                    - a list of integers with the person's cell phone numbers
                    - a dictionary with the following fields
                        - number (REQUIRED)
                        - primary (OPTIONAL): Boolean indicating the user's primary mobile number
                        - status (OPTIONAL): can taken on any of these values
                            - "subscribed"
                            - "unsubscribed"
            mobile_status:
                'subscribed' or 'unsubscribed'
            **kwargs:
                Any additional fields to store about the person. Action Network allows
                any custom field.
        Adds a person to Action Network
        """
        email_addresses_field = None
        if type(email_address) == str:
            email_addresses_field = [{"address": email_address}]
        elif type(email_address) == list:
            if type(email_address[0]) == str:
                email_addresses_field = [{
                    "address": email
                } for email in email_address]
                email_addresses_field[0]['primary'] = True
            if type(email_address[0]) == dict:
                email_addresses_field = email_address

        mobile_numbers_field = None
        if type(mobile_number) == str:
            mobile_numbers_field = [{
                "number":
                re.sub('[^0-9]', "", mobile_number),
                "status":
                mobile_status
            }]
        elif type(mobile_number) == int:
            mobile_numbers_field = [{
                "number": str(mobile_number),
                "status": mobile_status
            }]
        elif type(mobile_number) == list:
            if len(mobile_number) > 1:
                raise (
                    'Action Network allows only 1 phone number per activist')
            if type(mobile_number[0]) == str:
                mobile_numbers_field = [{
                    "number": re.sub('[^0-9]', "", cell),
                    "status": mobile_status
                } for cell in mobile_number]
                mobile_numbers_field[0]['primary'] = True
            if type(mobile_number[0]) == int:
                mobile_numbers_field = [{
                    "number": cell,
                    "status": mobile_status
                } for cell in mobile_number]
                mobile_numbers_field[0]['primary'] = True
            if type(mobile_number[0]) == dict:
                mobile_numbers_field = mobile_number

        if not email_addresses_field and not mobile_numbers_field:
            raise (
                "Either email_address or mobile_number is required and can be formatted "
                "as a string, list of strings, a dictionary, a list of dictionaries, or "
                "(for mobile_number only) an integer or list of integers")

        data = {"person": {}}

        if email_addresses_field is not None:
            data["person"]["email_addresses"] = email_addresses_field
        if mobile_numbers_field is not None:
            data["person"]["phone_numbers"] = mobile_numbers_field
        if given_name is not None:
            data["person"]["given_name"] = given_name
        if family_name is not None:
            data["person"]["family_name"] = family_name
        if languages_spoken is not None:
            data["person"]["languages_spoken"] = languages_spoken
        if postal_addresses is not None:
            data["person"]["postal_address"] = postal_addresses
        if tags is not None:
            data["add_tags"] = tags
        data["person"]["custom_fields"] = {**kwargs}
        response = self.api.post_request(url=f"{self.api_url}/people",
                                         data=json.dumps(data))
        identifiers = response['identifiers']
        person_id = [
            entry_id.split(':')[1] for entry_id in identifiers
            if 'action_network:' in entry_id
        ][0]
        logger.info(f"Entry {person_id} successfully added to people.")
        return response

    def update_person(self, entry_id, **kwargs):
        """
        `Args:`
            entry_id:
                The person's Action Network id
            **kwargs:
                Fields to be updated. The possible fields are
                    email_address:
                        Can be any of the following
                            - a string with the person's email
                            - a dictionary with the following fields
                                - email_address (REQUIRED)
                                    - primary (OPTIONAL): Boolean indicating the user's
                                    primary email address
                                - status (OPTIONAL): can taken on any of these values
                                    - "subscribed"
                                    - "unsubscribed"
                                    - "bouncing"
                                    - "previous bounce"
                                    - "spam complaint"
                                    - "previous spam complaint"
                    given_name:
                        The person's given name
                    family_name:
                        The person's family name
                    tags:
                        Any tags to be applied to the person
                    languages_spoken:
                        Optional field. A list of strings of the languages spoken by the person
                    postal_addresses:
                        Optional field. A list of dictionaries.
                        For details, see Action Network's documentation:
                        https://actionnetwork.org/docs/v2/people#put
                    custom_fields:
                        A dictionary of any other fields to store about the person.
        Updates a person's data in Action Network
        """
        data = {**kwargs}
        response = self.api.put_request(
            url=f"{self.api_url}/people/{entry_id}",
            json=json.dumps(data),
            success_codes=[204, 201, 200])
        logger.info(f"Person {entry_id} successfully updated")
        return response

    def get_tags(self, limit=None, per_page=25, page=None):
        """
        `Args:`
            limit:
                The number of entries to return. When None, returns all entries.
            per_page
                The number of entries per page to return. 25 maximum.
            page
                Which page of results to return
        `Returns:`
            A list of JSONs of tags in Action Network.
        """
        if page:
            self.get_page("tags", page, per_page)
        return self._get_entry_list("tags", limit, per_page)

    def get_tag(self, tag_id):
        """
        `Args:`
            tag_id:
                Id of the tag.
        `Returns:`
            A  JSON of the entry. If the entry doesn't exist, Action Network returns
            "{'error': 'Couldn't find tag with id = <id>'}"
        """
        return self.api.get_request(url=f"tags/{tag_id}")

    def add_tag(self, name):
        """
        `Args:`
            name:
                The tag's name. This is the ONLY editable field
        Adds a tag to Action Network. Once created, tags CANNOT be edited or deleted.
        """
        data = {"name": name}
        response = self.api.post_request(url=f"{self.api_url}/tags",
                                         data=json.dumps(data))
        identifiers = response['identifiers']
        person_id = [
            entry_id.split(':')[1] for entry_id in identifiers
            if 'action_network:' in entry_id
        ][0]
        logger.info(f"Tag {person_id} successfully added to tags.")
        return response

    def create_event(self, title, start_date=None, location=None):
        """
        Create an event in Action Network

        `Args:`
            title: str
                The public title of the event
            start_date: str OR datetime
                OPTIONAL: The starting date & time. If a string, use format "YYYY-MM-DD HH:MM:SS"
                (hint: the default format you get when you use `str()` on a datetime)
            location: dict
                OPTIONAL: A dict of location details. Can include any combination of the types of
                values in the following example:
                .. code-block:: python

                    my_location = {
                        "venue": "White House",
                        "address_lines": [
                            "1600 Pennsylvania Ave"
                        ],
                        "locality": "Washington",
                        "region": "DC",
                        "postal_code": "20009",
                        "country": "US"
                    }

        `Returns:`
            Dict of Action Network Event data.
        """

        data = {"title": title}

        if start_date:
            start_date = str(start_date)
            data["start_date"] = start_date

        if isinstance(location, dict):
            data["location"] = location

        event_dict = self.api.post_request(url=f"{self.api_url}/events",
                                           data=json.dumps(data))

        an_event_id = event_dict["_links"]["self"]["href"].split('/')[-1]
        event_dict["event_id"] = an_event_id

        return event_dict
Ejemplo n.º 5
0
class VANConnector(object):

    def __init__(self, api_key=None, auth_name='default', db=None):

        self.api_key = check_env.check('VAN_API_KEY', api_key)

        if db == 'MyVoters':
            self.db_code = 0
        elif db in ['MyMembers', 'MyCampaign', 'EveryAction']:
            self.db_code = 1
        else:
            raise KeyError('Invalid database type specified. Pick one of:'
                           ' MyVoters, MyCampaign, MyMembers, EveryAction.')

        self.uri = URI
        self.db = db
        self.auth_name = auth_name
        self.pagination_key = 'nextPageLink'
        self.auth = (self.auth_name, self.api_key + '|' + str(self.db_code))
        self.api = APIConnector(self.uri, auth=self.auth, data_key='items',
                                pagination_key=self.pagination_key)

        # We will not create the SOAP client unless we need to as this triggers checking for
        # valid credentials. As not all API keys are provisioned for SOAP, this keeps it from
        # raising a permission exception when creating the class.
        self._soap_client = None

    @property
    def api_key_profile(self):
        """
        Returns the API key profile with includes permissions and other metadata.
        """

        return self.get_request('apiKeyProfiles')[0]

    @property
    def soap_client(self):

        if not self._soap_client:

            # Create the SOAP client
            soap_auth = {'Header': {'DatabaseMode': self.soap_client_db(), 'APIKey': self.api_key}}
            self._soap_client = Client(SOAP_URI, soapheaders=soap_auth)

        return self._soap_client

    def soap_client_db(self):
        """
        Parse the REST database name to the accepted SOAP format
        """

        if self.db == 'MyVoters':
            return 'MyVoterFile'
        if self.db == 'EveryAction':
            return 'MyCampaign'
        else:
            return self.db

    def get_request(self, endpoint, **kwargs):

        r = self.api.get_request(self.uri + endpoint, **kwargs)
        data = self.api.data_parse(r)

        # Paginate
        while isinstance(r, dict) and self.api.next_page_check_url(r):
            if endpoint == 'savedLists' and not r['items']:
                break
            r = self.api.get_request(r[self.pagination_key], **kwargs)
            data.extend(self.api.data_parse(r))

        return data

    def post_request(self, endpoint, **kwargs):

        return self.api.post_request(endpoint, **kwargs)

    def delete_request(self, endpoint, **kwargs):

        return self.api.delete_request(endpoint, **kwargs)

    def patch_request(self, endpoint, **kwargs):

        return self.api.patch_request(endpoint, **kwargs)

    def put_request(self, endpoint, **kwargs):

        return self.api.put_request(endpoint, **kwargs)
Ejemplo n.º 6
0
class Phone2Action(object):
    """
    Instantiate Phone2Action Class

    `Args:`
        app_id: str
            The Phone2Action provided application id. Not required if ``PHONE2ACTION_APP_ID``
            env variable set.
        app_key: str
            The Phone2Action provided application key. Not required if ``PHONE2ACTION_APP_KEY``
            env variable set.
    `Returns:`
        Phone2Action Class
    """
    def __init__(self, app_id=None, app_key=None):

        self.app_id = check_env.check('PHONE2ACTION_APP_ID', app_id)
        self.app_key = check_env.check('PHONE2ACTION_APP_KEY', app_key)
        self.auth = HTTPBasicAuth(self.app_id, self.app_key)
        self.client = APIConnector(PHONE2ACTION_URI, auth=self.auth)

    def _paginate_request(self, url, args=None, page=None):
        # Internal pagination method

        if page is not None:
            args['page'] = page

        r = self.client.get_request(url, params=args)

        json = r['data']

        if page is not None:
            return json

        # If count of items is less than the total allowed per page, paginate
        while r['pagination']['count'] == r['pagination']['per_page']:

            r = self.client.get_request(r['pagination']['next_url'], args)
            json.extend(r['data'])

        return json

    def get_advocates(self,
                      state=None,
                      campaign_id=None,
                      updated_since=None,
                      page=None):
        """
        Return advocates (person records).

        If no page is specified, the method will automatically paginate through the available
        advocates.

        `Args:`
            state: str
                Filter by US postal abbreviation for a state
                or territory e.g., "CA" "NY" or "DC"
            campaign_id: int
                Filter to specific campaign
            updated_since: str or int or datetime
                Fetch all advocates updated since the date provided; this can be a datetime
                object, a UNIX timestamp, or a date string (ex. '2014-01-05 23:59:43')
            page: int
                Page number of data to fetch; if this is specified, call will only return one
                page.
        `Returns:`
            A dict of parsons tables:
                * emails
                * phones
                * memberships
                * tags
                * ids
                * fields
                * advocates
        """

        # Convert the passed in updated_since into a Unix timestamp (which is what the API wants)
        updated_since = date_to_timestamp(updated_since)

        args = {
            'state': state,
            'campaignid': campaign_id,
            'updatedSince': updated_since
        }

        logger.info('Retrieving advocates...')
        json = self._paginate_request('advocates', args=args, page=page)

        return self._advocates_tables(Table(json))

    def _advocates_tables(self, tbl):
        # Convert the advocates nested table into multiple tables

        tbls = {
            'advocates': tbl,
            'emails': Table(),
            'phones': Table(),
            'memberships': Table(),
            'tags': Table(),
            'ids': Table(),
            'fields': Table(),
        }

        if not tbl:
            return tbls

        logger.info(f'Retrieved {tbl.num_rows} advocates...')

        # Unpack all of the single objects
        # The Phone2Action API docs says that created_at and updated_at are dictionaries, but
        # the data returned from the server is a ISO8601 timestamp. - EHS, 05/21/2020
        for c in ['address', 'districts']:
            tbl.unpack_dict(c)

        # Unpack all of the arrays
        child_tables = [child for child in tbls.keys() if child != 'advocates']
        for c in child_tables:
            tbls[c] = tbl.long_table(['id'],
                                     c,
                                     key_rename={'id': 'advocate_id'})

        return tbls

    def get_campaigns(self,
                      state=None,
                      zip=None,
                      include_generic=False,
                      include_private=False,
                      include_content=True):
        """
        Returns a list of campaigns

        `Args:`
            state: str
                Filter by US postal abbreviation for a state or territory e.g., "CA" "NY" or "DC"
            zip: int
                Filter by 5 digit zip code
            include_generic: boolean
                When filtering by state or ZIP code, include unrestricted campaigns
            include_private: boolean
                If true, will include private campaigns in results
            include_content: boolean
                If true, include campaign content fields, which may vary. This may cause
                sync errors.
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        args = {
            'state': state,
            'zip': zip,
            'includeGeneric': str(include_generic),
            'includePrivate': str(include_private)
        }

        tbl = Table(self.client.get_request('campaigns', params=args))
        if tbl:
            tbl.unpack_dict('updated_at')
            if include_content:
                tbl.unpack_dict('content')

        return tbl

    def create_advocate(self,
                        campaigns,
                        first_name=None,
                        last_name=None,
                        email=None,
                        phone=None,
                        address1=None,
                        address2=None,
                        city=None,
                        state=None,
                        zip5=None,
                        sms_optin=None,
                        email_optin=None,
                        sms_optout=None,
                        email_optout=None,
                        **kwargs):
        """
        Create an advocate.

        If you want to opt an advocate into or out of SMS / email campaigns, you must provide
        the email address or phone number (accordingly).

        The list of arguments only partially covers the fields that can be set on the advocate.
        For a complete list of fields that can be updated, see
        `the Phone2Action API documentation <https://docs.phone2action.com/#calls-create>`_.

        `Args:`
            campaigns: list
                The ID(s) of campaigns to add the advocate to
            first_name: str
                `Optional`; The first name of the advocate
            last_name: str
                `Optional`; The last name of the advocate
            email: str
                `Optional`; An email address to add for the advocate. One of ``email`` or ``phone``
                is required.
            phone: str
                `Optional`; An phone # to add for the advocate. One of ``email`` or ``phone`` is
                required.
            address1: str
                `Optional`; The first line of the advocates' address
            address2: str
                `Optional`; The second line of the advocates' address
            city: str
                `Optional`; The city of the advocates address
            state: str
                `Optional`; The state of the advocates address
            zip5: str
                `Optional`; The 5 digit Zip code of the advocate
            sms_optin: boolean
                `Optional`; Whether to opt the advocate into receiving text messages; an SMS
                confirmation text message will be sent. You must provide values for the ``phone``
                and ``campaigns`` arguments.
            email_optin: boolean
                `Optional`; Whether to opt the advocate into receiving emails. You must provide
                values for the ``email`` and ``campaigns`` arguments.
            sms_optout: boolean
                `Optional`; Whether to opt the advocate out of receiving text messages. You must
                provide values for the ``phone`` and ``campaigns`` arguments. Once an advocate is
                opted out, they cannot be opted back in.
            email_optout: boolean
                `Optional`; Whether to opt the advocate out of receiving emails. You must
                provide values for the ``email`` and ``campaigns`` arguments. Once an advocate is
                opted out, they cannot be opted back in.
            **kwargs:
                Additional fields on the advocate to update
        `Returns:`
            The int ID of the created advocate.
        """

        # Validate the passed in arguments

        if not campaigns:
            raise ValueError(
                'When creating an advocate, you must specify one or more campaigns.'
            )

        if not email and not phone:
            raise ValueError(
                'When creating an advocate, you must provide an email address or a phone number.'
            )

        if (sms_optin or sms_optout) and not phone:
            raise ValueError(
                'When opting an advocate in or out of SMS messages, you must specify a valid '
                'phone and one or more campaigns')

        if (email_optin or email_optout) and not email:
            raise ValueError(
                'When opting an advocate in or out of email messages, you must specify a valid '
                'email address and one or more campaigns')

        # Align our arguments with the expected parameters for the API
        payload = {
            'email': email,
            'phone': phone,
            'firstname': first_name,
            'lastname': last_name,
            'address1': address1,
            'address2': address2,
            'city': city,
            'state': state,
            'zip5': zip5,
            'smsOptin': 1 if sms_optin else None,
            'emailOptin': 1 if email_optin else None,
            'smsOptout': 1 if sms_optout else None,
            'emailOptout': 1 if email_optout else None,
        }

        # Clean up any keys that have a "None" value
        payload = {key: val for key, val in payload.items() if val is not None}

        # Merge in any kwargs
        payload.update(kwargs)

        # Turn into a list of items so we can append multiple campaigns
        campaign_keys = [('campaigns[]', val) for val in campaigns]
        data = [(key, value) for key, value in payload.items()] + campaign_keys

        # Call into the Phone2Action API
        response = self.client.post_request('advocates', data=data)
        return response['advocateid']

    def update_advocate(self,
                        advocate_id,
                        campaigns=None,
                        email=None,
                        phone=None,
                        sms_optin=None,
                        email_optin=None,
                        sms_optout=None,
                        email_optout=None,
                        **kwargs):
        """
        Update the fields of an advocate.

        If you want to opt an advocate into or out of SMS / email campaigns, you must provide
        the email address or phone number along with a list of campaigns.

        The list of arguments only partially covers the fields that can be updated on the advocate.
        For a complete list of fields that can be updated, see
        `the Phone2Action API documentation <https://docs.phone2action.com/#calls-create>`_.

        `Args:`
            advocate_id: integer
                The ID of the advocate being updates
            campaigns: list
                `Optional`; The ID(s) of campaigns to add the user to
            email: str
                `Optional`; An email address to add for the advocate (or to use when opting in/out)
            phone: str
                `Optional`; An phone # to add for the advocate (or to use when opting in/out)
            sms_optin: boolean
                `Optional`; Whether to opt the advocate into receiving text messages; an SMS
                confirmation text message will be sent. You must provide values for the ``phone``
                and ``campaigns`` arguments.
            email_optin: boolean
                `Optional`; Whether to opt the advocate into receiving emails. You must provide
                values for the ``email`` and ``campaigns`` arguments.
            sms_optout: boolean
                `Optional`; Whether to opt the advocate out of receiving text messages. You must
                provide values for the ``phone`` and ``campaigns`` arguments. Once an advocate is
                opted out, they cannot be opted back in.
            email_optout: boolean
                `Optional`; Whether to opt the advocate out of receiving emails. You must
                provide values for the ``email`` and ``campaigns`` arguments. Once an advocate is
                opted out, they cannot be opted back in.
            **kwargs:
                Additional fields on the advocate to update
        """

        # Validate the passed in arguments
        if (sms_optin or sms_optout) and not (phone and campaigns):
            raise ValueError(
                'When opting an advocate in or out of SMS messages, you must specify a valid '
                'phone and one or more campaigns')

        if (email_optin or email_optout) and not (email and campaigns):
            raise ValueError(
                'When opting an advocate in or out of email messages, you must specify a valid '
                'email address and one or more campaigns')

        # Align our arguments with the expected parameters for the API
        payload = {
            'advocateid': advocate_id,
            'campaigns': campaigns,
            'email': email,
            'phone': phone,
            'smsOptin': 1 if sms_optin else None,
            'emailOptin': 1 if email_optin else None,
            'smsOptout': 1 if sms_optout else None,
            'emailOptout': 1 if email_optout else None,
            # remap first_name / last_name to be consistent with updated_advocates
            'firstname': kwargs.pop('first_name', None),
            'lastname': kwargs.pop('last_name', None),
        }

        # Clean up any keys that have a "None" value
        payload = {key: val for key, val in payload.items() if val is not None}

        # Merge in any kwargs
        payload.update(kwargs)

        # Turn into a list of items so we can append multiple campaigns
        campaigns = campaigns or []
        campaign_keys = [('campaigns[]', val) for val in campaigns]
        data = [(key, value) for key, value in payload.items()] + campaign_keys

        # Call into the Phone2Action API
        self.client.post_request('advocates', data=data)
Ejemplo n.º 7
0
class ActBlue(object):
    """
    Instantiate class.

       `Args:`
            actblue_client_uuid: str
                The ActBlue provided Client UUID. Not required if ``ACTBLUE_CLIENT_UUID`` env
                variable set.
            actblue_client_secret: str
                The ActBlue provided Client Secret. Not required if ``ACTBLUE_CLIENT_SECRET`` env
                variable set.
            actblue_uri: str
                The URI to access the CSV API. Not required, default is
                https://secure.actblue.com/api/v1. You can set an ``ACTBLUE_URI`` env variable or
                use this URI parameter if a different endpoint is necessary - for example, when
                running this code in a test environment where you don't want to hit the actual API.

        For instructions on how to generate a Client UUID and Client Secret set,
        visit https://secure.actblue.com/docs/csv_api#authentication.
    """

    def __init__(self, actblue_client_uuid=None, actblue_client_secret=None, actblue_uri=None):
        self.actblue_client_uuid = check_env.check('ACTBLUE_CLIENT_UUID', actblue_client_uuid)
        self.actblue_client_secret = check_env.check('ACTBLUE_CLIENT_SECRET', actblue_client_secret)
        self.uri = check_env.check(
            'ACTBLUE_URI', actblue_uri, optional=True
        ) or ACTBLUE_API_ENDPOINT
        self.headers = {
            "accept": "application/json",
        }
        self.client = APIConnector(self.uri,
                                   auth=(self.actblue_client_uuid, self.actblue_client_secret),
                                   headers=self.headers)

    def post_request(self, csv_type=None, date_range_start=None, date_range_end=None):
        """
        POST request to ActBlue API to begin generating the CSV.

        `Args:`
            csv_type: str
                Type of CSV you are requesting.
                Options:
                    'paid_contributions': contains paid, non-refunded contributions to the entity
                    (campaign or organization) you created the credential for, during the specified
                    date range.

                    'refunded_contributions': contributions to your entity that were refunded,
                    during the specified date range.

                    'managed_form_contributions': contributions made through any form that is
                    managed by your entity, during the specified date range - including
                    contributions to other entities via that form if it is a tandem form.
            date_range_start: str
                Start of date range to withdraw contribution data (inclusive). Ex: '2020-01-01'
            date_range_end: str
                End of date range to withdraw contribution data (exclusive). Ex: '2020-02-01'

        `Returns:`
            Response of POST request; a successful response includes 'id', a unique identifier for
            the CSV being generated.
        """

        body = {
            "csv_type": csv_type,
            "date_range_start": date_range_start,
            "date_range_end": date_range_end
        }
        logger.info(f'Requesting {csv_type} from {date_range_start} up to {date_range_end}.')
        response = self.client.post_request(url="csvs", json=body)
        return response

    def get_download_url(self, csv_id=None):
        """
        GET request to retrieve download_url for generated CSV.

        `Args:`
            csv_id: str
                Unique identifier of the CSV you requested.

        `Returns:`
            While CSV is being generated, 'None' is returned. When CSV is ready, the method returns
            the download_url.
        """
        response = self.client.get_request(url=f"csvs/{csv_id}")

        return response['download_url']

    def poll_for_download_url(self, csv_id):
        """
        Poll the GET request method to check whether CSV generation has finished, signified by the
        presence of a download_url.

        `Args:`
            csv_id: str
                Unique identifier of the CSV you requested.

        `Returns:`
            Download URL from which you can download the generated CSV, valid for 10 minutes after
            retrieval. Null until CSV has finished generating. Keep this URL secure because until
            it expires, it could be used by anyone to download the CSV.
        """

        logger.info('Request received. Please wait while ActBlue generates this data.')
        download_url = None
        while download_url is None:
            download_url = self.get_download_url(csv_id)
            time.sleep(POLLING_DELAY)

        logger.info('Completed data generation.')
        logger.info('Beginning conversion to Parsons Table.')
        return download_url

    def get_contributions(self, csv_type, date_range_start, date_range_end):
        """
        Get specified contribution data from CSV API as Parsons table.

        `Args:`
            csv_type: str
                Type of CSV you are requesting.
                Options:
                    'paid_contributions': contains paid, non-refunded contributions to the entity
                    (campaign or organization) you created the credential for, during the specified
                    date range.

                    'refunded_contributions': contributions to your entity that were refunded,
                    during the specified date range.

                    'managed_form_contributions': contributions made through any form that is
                    managed by your entity, during the specified date range - including
                    contributions to other entities via that form if it is a tandem form.
            date_range_start: str
                Start of date range to withdraw contribution data (inclusive). Ex: '2020-01-01'
            date_range_end: str
                End of date range to withdraw contribution data (exclusive). Ex: '2020-02-01'

        `Returns:`
            Contents of the generated contribution CSV as a Parsons table.
        """

        post_request_response = self.post_request(csv_type, date_range_start, date_range_end)
        csv_id = post_request_response["id"]
        download_url = self.poll_for_download_url(csv_id)
        table = Table.from_csv(download_url)
        logger.info('Completed conversion to Parsons Table.')
        return table
Ejemplo n.º 8
0
class ActionNetwork(object):
    """
    `Args:`
        api_token: str
            The OSDI API token
        api_url:
            The end point url
    """
    def __init__(self, api_token=None, api_url=None):
        self.api_token = check_env.check('AN_API_TOKEN', api_token)
        self.headers = {
            "Content-Type": "application/json",
            "OSDI-API-Token": self.api_token
        }
        self.api_url = check_env.check('AN_API_URL', api_url)
        self.api = APIConnector(self.api_url, headers=self.headers)

    def _get_page(self, object_name, page, per_page=25):
        # returns data from one page of results
        if per_page > 25:
            per_page = 25
            logger.info("Action Network's API will not return more than 25 entries per page. \
            Changing per_page parameter to 25.")
        page_url = f"{object_name}?page={page}&per_page={per_page}"
        return self.api.get_request(url=page_url)

    def _get_entry_list(self, object_name, limit=None, per_page=25):
        # returns a list of entries for a given object, such as people, tags, or actions
        count = 0
        page = 1
        return_list = []
        while True:
            response = self._get_page(object_name, page, per_page)
            page = page + 1
            response_list = response['_embedded'][f"osdi:{object_name}"]
            if not response_list:
                return Table(return_list)
            return_list.extend(response_list)
            count = count + len(response_list)
            if limit:
                if count >= limit:
                    return Table(return_list[0:limit])

    def get_people(self, limit=None, per_page=25, page=None):
        """
        `Args:`
            limit:
                The number of entries to return. When None, returns all entries.
            per_page
                The number of entries per page to return. 25 maximum.
            page
                Which page of results to return
        `Returns:`
            A list of JSONs of people stored in Action Network.
        """
        if page:
            self._get_page("people", page, per_page)
        return self._get_entry_list("people", limit, per_page)

    def get_person(self, person_id):
        """
        `Args:`
            person_id:
                Id of the person.
        `Returns:`
            A  JSON of the entry. If the entry doesn't exist, Action Network returns
            ``{'error': 'Couldn't find person with id = <id>'}``.
        """
        return self.api.get_request(url=f"people/{person_id}")

    def add_person(self, email_address, given_name=None, family_name=None, tags=[],
                   languages_spoken=[], postal_addresses=[],
                   **kwargs):
        """
        `Args:`
            email_address:
                Can be any of the following
                    - a string with the person's email
                    - a dictionary with the following fields
                        - email_address (REQUIRED)
                        - primary (OPTIONAL): Boolean indicating the user's primary email address
                        - status (OPTIONAL): can taken on any of these values
                            - "subscribed"
                            - "unsubscribed"
                            - "bouncing"
                            - "previous bounce"
                            - "spam complaint"
                            - "previous spam complaint"
            given_name:
                The person's given name
            family_name:
                The person's family name
            tags:
                Any tags to be applied to the person
            languages_spoken:
                Optional field. A list of strings of the languages spoken by the person
            postal_addresses:
                Optional field. A list of dictionaries.
                For details, see Action Network's documentation:
                https://actionnetwork.org/docs/v2/people#put
            **kwargs:
                Any additional fields to store about the person. Action Network allows
                any custom field.
        Adds a person to Action Network
        """
        email_addresses_field = None
        if type(email_address) == str:
            email_addresses_field = [{"address": email_address}]
        elif type(email_address == list):
            if type(email_address[0]) == str:
                email_addresses_field = [{"address": email} for email in email_address]
                email_addresses_field[0]['primary'] = True
            if type(email_address[0]) == dict:
                email_addresses_field = email_address
        if not email_addresses_field:
            raise("email_address must be a string, list of strings, or list of dictionaries")
        data = {
            "person": {
                "email_addresses": email_addresses_field,
                "given_name": given_name,
                "family_name": family_name,
                "languages_spoken": languages_spoken,
                "postal_addresses": postal_addresses,
                "custom_fields": {**kwargs}
              },
            "add_tags": tags
        }
        response = self.api.post_request(url=f"{self.api_url}/people", data=json.dumps(data))
        identifiers = response['identifiers']
        person_id = [entry_id.split(':')[1]
                     for entry_id in identifiers if 'action_network:' in entry_id][0]
        logger.info(f"Entry {person_id} successfully added to people.")
        return response

    def update_person(self, entry_id, **kwargs):
        """
        `Args:`
            entry_id:
                The person's Action Network id
            **kwargs:
                Fields to be updated. The possible fields are
                    email_address:
                        Can be any of the following
                            - a string with the person's email
                            - a dictionary with the following fields
                                - email_address (REQUIRED)
                                    - primary (OPTIONAL): Boolean indicating the user's
                                    primary email address
                                - status (OPTIONAL): can taken on any of these values
                                    - "subscribed"
                                    - "unsubscribed"
                                    - "bouncing"
                                    - "previous bounce"
                                    - "spam complaint"
                                    - "previous spam complaint"
                    given_name:
                        The person's given name
                    family_name:
                        The person's family name
                    tags:
                        Any tags to be applied to the person
                    languages_spoken:
                        Optional field. A list of strings of the languages spoken by the person
                    postal_addresses:
                        Optional field. A list of dictionaries.
                        For details, see Action Network's documentation:
                        https://actionnetwork.org/docs/v2/people#put
                    custom_fields:
                        A dictionary of any other fields to store about the person.
        Updates a person's data in Action Network
        """
        data = {**kwargs}
        response = self.api.put_request(url=f"{self.api_url}/people/{entry_id}",
                                        json=json.dumps(data), success_codes=[204, 201, 200])
        logger.info(f"Person {entry_id} successfully updated")
        return response

    def get_tags(self, limit=None, per_page=25, page=None):
        """
        `Args:`
            limit:
                The number of entries to return. When None, returns all entries.
            per_page
                The number of entries per page to return. 25 maximum.
            page
                Which page of results to return
        `Returns:`
            A list of JSONs of tags in Action Network.
        """
        if page:
            self.get_page("tags", page, per_page)
        return self._get_entry_list("tags", limit, per_page)

    def get_tag(self, tag_id):
        """
        `Args:`
            tag_id:
                Id of the tag.
        `Returns:`
            A  JSON of the entry. If the entry doesn't exist, Action Network returns
            "{'error': 'Couldn't find tag with id = <id>'}"
        """
        return self.api.get_request(url=f"tags/{tag_id}")

    def add_tag(self, name):
        """
        `Args:`
            name:
                The tag's name. This is the ONLY editable field
        Adds a tag to Action Network. Once created, tags CANNOT be edited or deleted.
        """
        data = {
            "name": name
        }
        response = self.api.post_request(url=f"{self.api_url}/tags", data=json.dumps(data))
        identifiers = response['identifiers']
        person_id = [entry_id.split(':')[1]
                     for entry_id in identifiers if 'action_network:' in entry_id][0]
        logger.info(f"Tag {person_id} successfully added to tags.")
        return response