Beispiel #1
0
 def __init__(self, hostname=None, user_token=None):
     self.hostname = check_env.check('QUICKBASE_HOSTNAME', hostname)
     self.user_token = check_env.check('QUICKBASE_USER_TOKEN', user_token)
     self.api_hostname = 'https://api.quickbase.com/v1'
     self.client = APIConnector(self.api_hostname,
                                headers={'QB-Realm-Hostname': self.hostname,
                                         'AUTHORIZATION': f'QB-USER-TOKEN {self.user_token}'})
    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
Beispiel #3
0
    def __init__(self, partner_id=None, partner_api_key=None):
        self.partner_id = check_env.check('RTV_PARTNER_ID', partner_id)
        self.partner_api_key = check_env.check('RTV_PARTNER_API_KEY',
                                               partner_api_key)

        self.client = APIConnector('https://vr.rockthevote.com/api/v4',
                                   headers=REQUEST_HEADERS)
Beispiel #4
0
 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)
Beispiel #5
0
    def __init__(self, partner_id=None, partner_api_key=None, testing=False):
        self.partner_id = check_env.check('RTV_PARTNER_ID', partner_id)
        self.partner_api_key = check_env.check('RTV_PARTNER_API_KEY',
                                               partner_api_key)

        if testing:
            self.client = APIConnector(TESTING_URI, headers=REQUEST_HEADERS)
        else:
            self.client = APIConnector(PRODUCTION_URI, headers=REQUEST_HEADERS)
Beispiel #6
0
 def __init__(self, subdomain=None, password=None, api_key=None, api_version=None):
     self.subdomain = check_env.check('SHOPIFY_SUBDOMAIN', subdomain)
     self.password = check_env.check('SHOPIFY_PASSWORD', password)
     self.api_key = check_env.check('SHOPIFY_API_KEY', api_key)
     self.api_version = check_env.check('SHOPIFY_API_VERSION', api_version)
     self.client = APIConnector('https://%s.myshopify.com/admin/api/%s/' % (
         self.subdomain,
         self.api_version
     ), auth=(self.api_key, self.password))
Beispiel #7
0
 def __init__(self, user=None, password=None):
     self.user = check_env.check('BLUELINK_WEBHOOK_USER', user)
     self.password = check_env.check('BLUELINK_WEBHOOK_PASSWORD', password)
     self.headers = {
         "Content-Type": "application/json",
     }
     self.api_url = API_URL
     self.api = APIConnector(self.api_url,
                             auth=(self.user, self.password),
                             headers=self.headers)
Beispiel #8
0
 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)
Beispiel #9
0
    def __init__(self, app_id=None, app_key=None):
        # check first for CapitolCanary branded app key and ID
        cc_app_id = check_env.check('CAPITOLCANARY_APP_ID',
                                    None,
                                    optional=True)
        cc_app_key = check_env.check('CAPITOLCANARY_APP_KEY',
                                     None,
                                     optional=True)

        self.app_id = cc_app_id or check_env.check('PHONE2ACTION_APP_ID',
                                                   app_id)
        self.app_key = cc_app_key or check_env.check('PHONE2ACTION_APP_KEY',
                                                     app_key)
        self.auth = HTTPBasicAuth(self.app_id, self.app_key)
        self.client = APIConnector(CAPITOL_CANARY_URI, auth=self.auth)
Beispiel #10
0
    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')
Beispiel #11
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)
Beispiel #12
0
 def _conn(self):
     # Instantiate APIConnector with authentication credentials
     headers = {"accept": "application/json",
                "Content-Type": "application/json"}
     if self.api_key is not None:
         logger.info("Using API key authentication.")
         headers['X-API-KEY'] = f"{self.api_key}"
     elif (self.client_id is not None) & (self.client_secret is not None):
         logger.info('Using OAuth2 authentication.')
         self._generate_authorization_code()
         self._generate_access_token()
         headers['Authorization'] = f"Bearer {self.access_token}"
     else:
         raise Exception('Missing authorization credentials.')
     return APIConnector(uri=self.uri, headers=headers)
Beispiel #13
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()
Beispiel #14
0
 def _api(self):
     headers = {'HTTP-X-PARTNER-AUTH': self.site_name + ":" + self.api_key}
     return APIConnector(uri=self.uri, headers=headers)
Beispiel #15
0
 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))
Beispiel #16
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)
Beispiel #17
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
Beispiel #18
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)
Beispiel #19
0
    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)
Beispiel #20
0
class Freshdesk():
    """
    Instantiate FreshDesk Class

    `Args:`
        domain:
            The subdomain of the FreshDesk account. Not required if ``FRESHDESK_DOMAIN``
            env variable set.
        api_key:
            The FreshDesk provided application key. Not required if ``FRESHDESK_API_KEY``
            env variable set.
    `Returns:`
        FreshDesk Class
    """
    def __init__(self, domain, api_key):

        self.api_key = check_env.check('FRESHDESK_API_KEY', api_key)
        self.domain = check_env.check('FRESHDESK_DOMAIN', domain)
        self.uri = f'https://{self.domain}.freshdesk.com/api/v2/'
        self.client = APIConnector(self.uri, auth=(self.api_key, 'x'))

    def get_request(self, endpoint, params=None, **kwargs):
        # Internal method to make a get request.

        base_params = {'per_page': PAGE_SIZE}

        if params:
            base_params.update(params)

        r = self.client.request(self.uri + endpoint, 'GET', params=base_params)
        self.client.validate_response(r)
        data = r.json()

        # Paginate
        while 'link' in r.headers.keys():
            logger.info(f'Retrieving another page of {PAGE_SIZE} records.')
            url = re.search('<(.*)>', r.headers['link']).group(1)
            r = self.client.request(url, 'GET', params=params)
            self.client.validate_response(r)
            data.extend(r.json())

        return data

    def transform_table(self, tbl, expand_custom_fields=None):
        # Internal method to transform a table prior to returning
        if tbl.num_rows > 0:
            tbl.move_column('id', 0)
            tbl.sort()
            if expand_custom_fields:
                tbl.unpack_dict('custom_fields', prepend=False)

        return tbl

    def get_tickets(self,
                    ticket_type=None,
                    requester_id=None,
                    requester_email=None,
                    company_id=None,
                    updated_since='2016-01-01',
                    expand_custom_fields=False):
        """
        List tickets.

        .. warning::
            Deleted and Spam tickets are not included. However they can be pulled separately
            by utilizing the ``ticket_type`` parameter.

        .. warning::
            Freshdesk will return a maximum of 9,000 tickets. To access additional tickets,
            utilize the ``updated_since`` parameter.

        `Args:`
            ticket_type: str
                Filter by type of ticket to filter by. Valid fields include ``new_and_my_open``,
                ``watching``, ``spam`` and ``deleted``.
            requester_id: int
                Filter by requester id.
            requester_email: str
                Filter by requester email.
            company_id: int
                Filter by company_id.
            expand_custom_fields: boolean
                Expand nested custom fields to their own columns.
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        params = {
            'filter': ticket_type,
            'requester_id': requester_id,
            'requester_email': requester_email,
            'company_id': company_id,
            'updated_since': updated_since
        }

        tbl = Table(self.get_request('tickets', params=params))
        logger.info(f'Found {tbl.num_rows} tickets.')
        return self.transform_table(tbl, expand_custom_fields)

    def get_contacts(self,
                     email=None,
                     mobile=None,
                     phone=None,
                     company_id=None,
                     state=None,
                     updated_since=None,
                     expand_custom_fields=None):
        """
        Get contacts.

        `Args:`
            email: str
                Filter by email address.
            mobile: str
                Filter by mobile phone number.
            phone: str
                Filter by phone number.
            expand_custom_fields: boolean
                Expand nested custom fields to their own columns.
        """

        params = {
            'email': email,
            'mobile': mobile,
            'phone': phone,
            'company_id': company_id,
            'state': state,
            '_updated_since': updated_since
        }

        tbl = Table(self.get_request('contacts', params=params))
        logger.info(f'Found {tbl.num_rows} contacts.')
        return self.transform_table(tbl, expand_custom_fields)

    def get_companies(self, expand_custom_fields=False):
        """
        List companies.

        `Args:`
            expand_custom_fields: boolean
                Expand nested custom fields to their own columns.
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        tbl = Table(self.get_request('companies'))
        logger.info(f'Found {tbl.num_rows} companies.')
        return self.transform_table(tbl, expand_custom_fields)

    def get_agents(self, email=None, mobile=None, phone=None, state=None):
        """
        List agents.

        `Args:`
            email: str
                Filter by email address.
            mobile: str
                Filter by mobile phone number
            phone: str
                Filter by phone number
            state: str
                Filter by state
        `Returns:`
            Parsons Table
                See :ref:`parsons-table` for output options.
        """

        params = {
            'email': email,
            'mobile': mobile,
            'phone': phone,
            'state': state
        }

        tbl = Table(self.get_request('agents', params=params))
        logger.info(f'Found {tbl.num_rows} agents.')
        tbl = self.transform_table(tbl)
        tbl = tbl.unpack_dict('contact', prepend=False)
        tbl.remove_column(
            'signature')  # Removing since raw HTML might cause issues.

        return tbl
Beispiel #21
0
    def __init__(self, domain, api_key):

        self.api_key = check_env.check('FRESHDESK_API_KEY', api_key)
        self.domain = check_env.check('FRESHDESK_DOMAIN', domain)
        self.uri = f'https://{self.domain}.freshdesk.com/api/v2/'
        self.client = APIConnector(self.uri, auth=(self.api_key, 'x'))
Beispiel #22
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
Beispiel #23
0
class Bluelink:
    """
    Instantiate a Bluelink connector.
    Allows for a simple method of inserting person data to Bluelink via a webhook.
    # see: https://bluelinkdata.github.io/docs/BluelinkApiGuide#webhook

    `Args:`:
        user: str
            Bluelink webhook user name.
        password: str
            Bluelink webhook password.
    """
    def __init__(self, user=None, password=None):
        self.user = check_env.check('BLUELINK_WEBHOOK_USER', user)
        self.password = check_env.check('BLUELINK_WEBHOOK_PASSWORD', password)
        self.headers = {
            "Content-Type": "application/json",
        }
        self.api_url = API_URL
        self.api = APIConnector(self.api_url,
                                auth=(self.user, self.password),
                                headers=self.headers)

    def upsert_person(self, source, person=None):
        """
        Upsert a BluelinkPerson object into Bluelink.
        Rows will update, as opposed to being inserted, if an existing person record in
        Bluelink has a matching BluelinkIdentifier (same source and id) as the BluelinkPerson object
        passed into this function.

        `Args:`
            source: str
                String to identify that the data came from your system. For example,
                your company name.
            person: BluelinkPerson
                A BluelinkPerson object.
                Will be inserted to Bluelink, or updated if a matching record is found.
        `Returns:`
            int
            An http status code from the http post request to the Bluelink webhook.
        """
        data = {'source': source, 'person': person}
        jdata = json.dumps(
            data,
            default=lambda o:
            {k: v
             for k, v in o.__dict__.items() if v is not None})
        resp = self.api.post_request(url=self.api_url, data=jdata)
        return resp

    def bulk_upsert_person(self, source, tbl, row_to_person):
        """
        Upsert all rows into Bluelink, using the row_to_person function to
        transform rows to BluelinkPerson objects.

        `Args:`
            source: str
                String to identify that the data came from your system.
                For example, your company name.
            tbl: Table
                A parsons Table that represents people data.
            row_to_person: Callable[[dict],BluelinkPerson]
                A function that takes a dict representation of a row from the passed in tbl
                and returns a BluelinkPerson object.

        `Returns:`
            list[int]
            A list of https response status codes, one response for each row in the table.
        """
        people = BluelinkPerson.from_table(tbl, row_to_person)
        responses = []
        for person in people:
            response = self.upsert_person(source, person)
            responses.append(response)
        return responses
Beispiel #24
0
class RockTheVote:
    """
    `Args:`
        partner_id: str
            The RockTheVote partner ID for the RTV account
        partner_api_key: str
            The API Key for the partner
    `Returns`:
        RockTheVote class
    """
    def __init__(self, partner_id=None, partner_api_key=None):
        self.partner_id = check_env.check('RTV_PARTNER_ID', partner_id)
        self.partner_api_key = check_env.check('RTV_PARTNER_API_KEY',
                                               partner_api_key)

        self.client = APIConnector('https://vr.rockthevote.com/api/v4',
                                   headers=REQUEST_HEADERS)

    def create_registration_report(self, before=None, since=None):
        """
        Create a new registration report.

        `Args:`
            before: str
                Date before which to return registrations for
            since: str
                Date for filtering registrations
        `Returns:`
            int
                The ID of the created report.
        """
        report_url = f'{self.client.uri}/registrant_reports.json'
        # Create the report for the new data
        report_parameters = {
            'partner_id': self.partner_id,
            'partner_API_key': self.partner_api_key,
        }

        if since:
            since_date = parse_date(since)
            report_parameters['since'] = since_date.strftime(DATETIME_FORMAT)
        if before:
            before_date = parse_date(before)
            report_parameters['before'] = before_date.strftime(DATETIME_FORMAT)

        # The report parameters get passed into the request as JSON in the body
        # of the request.
        response = self.client.request(report_url,
                                       'post',
                                       json=report_parameters)
        if response.status_code != requests.codes.ok:
            raise RTVFailure("Couldn't create RTV registrations report")

        response_json = response.json()
        # The RTV API says the response should include the report_id, but I have not found
        # that to be the case
        report_id = response_json.get('report_id')
        if report_id:
            return report_id

        # If the response didn't include the report_id, then we will parse it out of the URL.
        status_url = response_json.get('status_url')
        url_match = STATUS_URL_PARSE_REGEX.search(status_url)
        if url_match:
            report_id = url_match.group(1)

        return report_id

    def get_registration_report(self,
                                report_id,
                                block=False,
                                poll_interval_seconds=60,
                                report_timeout_seconds=3600):
        """
        Get data from an existing registration report.

        `Args:`
            report_id: int
                The ID of the report to get data from
            block: bool
                Whether or not to block execution until the report is complete
            poll_interval_seconds: int
                If blocking, how long to pause between attempts to check if the report is done
            report_timeout_seconds: int
                If blocking, how long to wait for the report before timing out
        `Returns:`
            Parsons Table
                Parsons table with the report data.
        """
        credentials = {
            'partner_id': self.partner_id,
            'partner_API_key': self.partner_api_key,
        }
        status_url = f'{self.client.uri}/registrant_reports/{report_id}'
        download_url = None

        # Let's figure out at what time should we just give up because we waited
        # too long
        end_time = datetime.datetime.now() + datetime.timedelta(
            seconds=report_timeout_seconds)

        # If we have a download URL, we can move on and just download the
        # report. Otherwise, as long as we haven't run out of time, we will
        # check the status.
        while not download_url and datetime.datetime.now() < end_time:
            logger.debug(
                f'Registrations report not ready yet, sleeping %s seconds',
                poll_interval_seconds)

            # Check the status again via the status endpoint
            status_response = self.client.request(status_url,
                                                  'get',
                                                  params=credentials)

            # Check to make sure the call got a valid response
            if status_response.status_code == requests.codes.ok:
                status_json = status_response.json()

                # Grab the download_url from the response.
                download_url = status_json.get('download_url')

                if not download_url and not block:
                    return None
            else:
                raise RTVFailure("Couldn't get report status")

            if not download_url:
                # We just got the status, so we should wait a minute before
                # we check it again.
                time.sleep(poll_interval_seconds)

        # If we never got a valid download_url, then we timed out waiting for
        # the report to generate. We will log an error and exit.
        if not download_url:
            raise RTVFailure('Timed out waiting for report')

        # Download the report data
        download_response = self.client.request(download_url,
                                                'get',
                                                params=credentials)

        # Check to make sure the call got a valid response
        if download_response.status_code == requests.codes.ok:
            report_data = download_response.text

            # Load the report data into a Parsons Table
            table = Table.from_csv_string(report_data)

            # Transform the data from the report's CSV format to something more
            # Pythonic (snake case)
            normalized_column_names = [
                re.sub(r'\s', '_', name).lower() for name in table.columns
            ]
            normalized_column_names = [
                re.sub(r'[^A-Za-z\d_]', '', name)
                for name in normalized_column_names
            ]
            table.table = petl.setheader(table.table, normalized_column_names)
            return table
        else:
            raise RTVFailure('Unable to download report data')

    def run_registration_report(self,
                                before=None,
                                since=None,
                                poll_interval_seconds=60,
                                report_timeout_seconds=3600):
        """
        Run a new registration report.

        This method will block until the report has finished generating, or until the specified
        timeout is reached.

        `Args:`
            before: str
                Date before which to return registrations for
            since: str
                Date for filtering registrations
            poll_interval_seconds: int
                If blocking, how long to pause between attempts to check if the report is done
            report_timeout_seconds: int
                If blocking, how long to wait for the report before timing out
        `Returns:`
            int
                The ID of the created report.
        """
        report_id = self.create_registration_report(before=before, since=since)
        return self.get_registration_report(
            report_id,
            block=True,
            poll_interval_seconds=poll_interval_seconds,
            report_timeout_seconds=report_timeout_seconds)
Beispiel #25
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
Beispiel #26
0
class Shopify(object):
    """
    Instantiate the Shopify class

    `Args:`
        subdomain: str
            The Shopify subdomain (e.g. ``myorg`` for myorg.myshopify.com) Not required if
            ``SHOPIFY_SUBDOMAIN`` env variable set.
        password: str
            The Shopify account password. Not required if ``SHOPIFY_PASSWORD`` env
            variable set.
        api_key: str
            The Shopify account API key. Not required if ``SHOPIFY_API_KEY`` env variable
            set.
        api_version: str
            The Shopify API version. Not required if ``SHOPIFY_API_VERSION`` env variable
            set.
    `Returns:`
        Shopify Class
    """
    def __init__(self,
                 subdomain=None,
                 password=None,
                 api_key=None,
                 api_version=None):
        self.subdomain = check_env.check('SHOPIFY_SUBDOMAIN', subdomain)
        self.password = check_env.check('SHOPIFY_PASSWORD', password)
        self.api_key = check_env.check('SHOPIFY_API_KEY', api_key)
        self.api_version = check_env.check('SHOPIFY_API_VERSION', api_version)
        self.base_url = 'https://%s.myshopify.com/admin/api/%s/' % (
            self.subdomain, self.api_version)
        self.client = APIConnector(self.base_url,
                                   auth=(self.api_key, self.password))

    def get_count(self, query_date=None, since_id=None, table_name=None):
        """
        Get the count of rows in a table.

        `Args:`
            query_date: str
                Filter query by a date that rows were created. This filter is ignored if value
                is None.
            since_id: str
                Filter query by a minimum ID. This filter is ignored if value is None.
            table_name: str
                The name of the Shopify table to query.
        `Returns:`
            int
        """
        return self.client.request(
            self.get_query_url(query_date, since_id, table_name),
            'GET').json().get("count", 0)

    def get_orders(self, query_date=None, since_id=None, completed=True):
        """
        Get Shopify orders.

        `Args:`
            query_date: str
                Filter query by a date that rows were created. Format: yyyy-mm-dd. This filter
                is ignored if value is None.
            since_id: str
                Filter query by a minimum ID. This filter is ignored if value is None.
            completed: bool
                True if only getting completed orders, False otherwise.
        `Returns:`
            Table Class
        """
        orders = []

        def _append_orders(url):
            nonlocal orders

            if completed:
                url += '&financial_status=paid'

            res = self.client.request(url, 'GET')

            cur_orders = res.json().get("orders", [])

            # Flatten orders to non-complex types
            for order in cur_orders:
                keys_to_add = {}
                keys_to_delete = []

                for key1 in order:
                    if isinstance(order[key1], dict):
                        for key2 in order[key1]:
                            keys_to_add[key1 + '_' + key2] = order[key1][key2]
                        keys_to_delete.append(key1)
                    elif key1 == 'note_attributes':
                        for note in order[key1]:
                            keys_to_add[key1 + '_' +
                                        note['name']] = note['value']

                order.update(keys_to_add)
                for key in keys_to_delete:
                    del order[key]

            orders += cur_orders

            return res

        res = _append_orders(
            self.get_query_url(query_date, since_id, "orders", False))

        # Get next page
        while res.headers.get("Link"):
            link = re.split('; |, ', res.headers.get("Link"))
            if len(link) and link[len(link) - 1] == 'rel="next"':
                res = _append_orders(link[len(link) - 2][1:-1])
            else:
                break

        return Table(orders)

    def get_query_url(self,
                      query_date=None,
                      since_id=None,
                      table_name=None,
                      count=True):
        """
        Get the URL of a Shopify API request

        `Args:`
            query_date: str
                Filter query by a date that rows were created. Format: yyyy-mm-dd. This filter
                is ignored if value is None.
            since_id: str
                Filter query by a minimum ID. This filter is ignored if value is None.
            table_name: str
                The name of the Shopify table to query.
            count: bool
                True if refund should be included in Table, False otherwise.
        `Returns:`
            str
        """
        filters = 'limit=250&status=any'

        if count:
            table = table_name + '/count.json'
        else:
            table = table_name + '.json'

        if query_date:
            # Specific date if provided
            query_date = datetime.strptime(query_date, "%Y-%m-%d")
            max_date = query_date + timedelta(days=1)
            filters += '&created_at_min={}&created_at_max={}'.format(
                query_date.isoformat(), max_date.isoformat())
        elif since_id:
            # Since ID if provided
            filters += '&since_id=%s' % since_id

        return self.base_url + '%s?%s' % (table, filters)

    def graphql(self, query):
        """
        Make GraphQL request. Reference: https://shopify.dev/api/admin-graphql

        `Args:`
            query: str
                GraphQL query.
        `Returns:`
            dict
        """
        return self.client.request(self.base_url + 'graphql.json',
                                   'POST',
                                   json={
                                       "query": query
                                   }).json().get('data')

    @classmethod
    def load_to_table(cls,
                      subdomain=None,
                      password=None,
                      api_key=None,
                      api_version=None,
                      query_date=None,
                      since_id=None,
                      completed=True):
        """
        Fast classmethod so you can get the data all at once:
        tabledata = Shopify.load_to_table(subdomain='myorg', password='******',
                                         api_key='abc123', api_version='2020-10',
                                         query_date='2020-10-20', since_id='8414',
                                         completed=True)
        This instantiates the class and makes the appropriate query type to Shopify's orders
        table based on which arguments are supplied.

        `Args:`
            subdomain: str
                The Shopify subdomain (e.g. ``myorg`` for myorg.myshopify.com).
            password: str
                The Shopify account password.
            api_key: str
                The Shopify account API key.
            api_version: str
                The Shopify API version.
            query_date: str
                Filter query by a date that rows were created. Format: yyyy-mm-dd. This filter
                is ignored if value is None.
            since_id: str
                Filter query by a minimum ID. This filter is ignored if value is None.
            completed: bool
                True if only getting completed orders, False otherwise.
                value as value
        `Returns:`
            Table Class
        """
        return cls(subdomain, password, api_key,
                   api_version).get_orders(query_date, since_id, completed)
Beispiel #27
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
Beispiel #28
0
class RockTheVote:
    """
    `Args:`
        partner_id: str
            The RockTheVote partner ID for the RTV account
        partner_api_key: str
            The API Key for the partner
        testing: bool
            Whether or not to use the staging instance. Defaults to False.
    `Returns`:
        RockTheVote class
    """
    def __init__(self, partner_id=None, partner_api_key=None, testing=False):
        self.partner_id = check_env.check('RTV_PARTNER_ID', partner_id)
        self.partner_api_key = check_env.check('RTV_PARTNER_API_KEY',
                                               partner_api_key)

        if testing:
            self.client = APIConnector(TESTING_URI, headers=REQUEST_HEADERS)
        else:
            self.client = APIConnector(PRODUCTION_URI, headers=REQUEST_HEADERS)

    def create_registration_report(self,
                                   before=None,
                                   since=None,
                                   report_type=None):
        """
        Create a new registration report.

        `Args:`
            before: str
                Limit to registrations that were started before this date, in
                ISO format (e.g. 2020-01-01)
            since: str
                Limit to registrations that were started since this date, in
                ISO format (e.g. 2020-01-01)
            report_type: str
                The type of report to create. If left as None, it creates the default report. The
                ``extended`` report includes additional fields. Currently only accepts ``extended``.
        `Returns:`
            int
                The ID of the created report.
        """
        report_url = f'registrant_reports.json'
        # Create the report for the new data
        report_parameters = {
            'partner_id': self.partner_id,
            'partner_API_key': self.partner_api_key,
        }

        # Declare these here so the logging doesn't error out
        since_date = before_date = None

        if report_type:
            if report_type not in VALID_REPORT_TYPES:
                raise RTVFailure(
                    f"Invalid report type. Must be one of {VALID_REPORT_TYPES}"
                )
            report_parameters["report_type"] = report_type
        if since:
            since_date = parse_date(since).strftime(DATETIME_FORMAT)
            report_parameters['since'] = since_date
        if before:
            before_date = parse_date(before).strftime(DATETIME_FORMAT)
            report_parameters['before'] = before_date

        # The report parameters get passed into the request as JSON in the body
        # of the request.
        report_str = f"{report_type} report" if report_type else "report"
        logger.info(f"Creating {report_str} for {self.partner_id} "
                    f"for dates: {since_date} to {before_date}...")
        response = self.client.request(report_url,
                                       'post',
                                       json=report_parameters)
        if response.status_code != requests.codes.ok:
            raise RTVFailure("Couldn't create RTV registrations report")

        response_json = response.json()
        # The RTV API says the response should include the report_id, but I have not found
        # that to be the case
        report_id = response_json.get('report_id')
        if report_id:
            logger.info(f"Created report with id {report_id}.")
            return report_id

        # If the response didn't include the report_id, then we will parse it out of the URL.
        status_url = response_json.get('status_url')
        url_match = STATUS_URL_PARSE_REGEX.search(status_url)
        if url_match:
            report_id = url_match.group(1)

        logger.info(f"Created report with id {report_id}.")
        return report_id

    def get_registration_report(self,
                                report_id,
                                block=False,
                                poll_interval_seconds=60,
                                report_timeout_seconds=3600):
        """
        Get data from an existing registration report.

        `Args:`
            report_id: int
                The ID of the report to get data from
            block: bool
                Whether or not to block execution until the report is complete
            poll_interval_seconds: int
                If blocking, how long to pause between attempts to check if the report is done
            report_timeout_seconds: int
                If blocking, how long to wait for the report before timing out
        `Returns:`
            Parsons Table
                Parsons table with the report data.
        """
        logger.info(f"Getting report with id {report_id}...")
        credentials = {
            'partner_id': self.partner_id,
            'partner_API_key': self.partner_api_key,
        }
        status_url = f'registrant_reports/{report_id}'
        download_url = None

        # Let's figure out at what time should we just give up because we waited
        # too long
        end_time = datetime.datetime.now() + datetime.timedelta(
            seconds=report_timeout_seconds)

        # If we have a download URL, we can move on and just download the
        # report. Otherwise, as long as we haven't run out of time, we will
        # check the status.
        while not download_url and datetime.datetime.now() < end_time:
            logger.debug(
                f'Registrations report not ready yet, sleeping %s seconds',
                poll_interval_seconds)

            # Check the status again via the status endpoint
            status_response = self.client.request(status_url,
                                                  'get',
                                                  params=credentials)

            # Check to make sure the call got a valid response
            if status_response.status_code == requests.codes.ok:
                status_json = status_response.json()

                # Grab the download_url from the response.
                download_url = status_json.get('download_url')

                if not download_url and not block:
                    return None
            else:
                raise RTVFailure("Couldn't get report status")

            if not download_url:
                # We just got the status, so we should wait a minute before
                # we check it again.
                time.sleep(poll_interval_seconds)

        # If we never got a valid download_url, then we timed out waiting for
        # the report to generate. We will log an error and exit.
        if not download_url:
            raise RTVFailure('Timed out waiting for report')

        # Download the report data
        download_response = self.client.request(download_url,
                                                'get',
                                                params=credentials)

        # Check to make sure the call got a valid response
        if download_response.status_code == requests.codes.ok:
            report_data = download_response.text

            # Load the report data into a Parsons Table
            table = Table.from_csv_string(report_data)

            # Transform the data from the report's CSV format to something more
            # Pythonic (snake case)
            normalized_column_names = [
                re.sub(r'\s', '_', name).lower() for name in table.columns
            ]
            normalized_column_names = [
                re.sub(r'[^A-Za-z\d_]', '', name)
                for name in normalized_column_names
            ]
            table.table = petl.setheader(table.table, normalized_column_names)
            return table
        else:
            raise RTVFailure('Unable to download report data')

    def run_registration_report(self,
                                before=None,
                                since=None,
                                report_type=None,
                                poll_interval_seconds=60,
                                report_timeout_seconds=3600):
        """
        Run a new registration report.

        This method will block until the report has finished generating, or until the specified
        timeout is reached.

        `Args:`
            before: str
                Limit to registrations that were started before this date, in
                ISO format (e.g. 2020-01-01)
            since: str
                Limit to registrations that were started since this date, in
                ISO format (e.g. 2020-01-01)
            report_type: str
                The type of report to run. If left as None, it runs the default report. The
                ``extended`` report includes additional fields. Currently only accepts ``extended``.
            poll_interval_seconds: int
                If blocking, how long to pause between attempts to check if the report is done
            report_timeout_seconds: int
                If blocking, how long to wait for the report before timing out
        `Returns:`
            Parsons.Table
                The table with the report data.
        """
        report_str = f"{report_type} report" if report_type else "report"
        logger.info(f"Running {report_str} for {self.partner_id} "
                    f"for dates: {since} to {before}...")
        report_id = self.create_registration_report(before=before,
                                                    since=since,
                                                    report_type=report_type)
        return self.get_registration_report(
            report_id,
            block=True,
            poll_interval_seconds=poll_interval_seconds,
            report_timeout_seconds=report_timeout_seconds)
Beispiel #29
0
    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)
Beispiel #30
0
class Quickbase(object):
    """
    Instantiate the Quickbase class

    `Args:`
        hostname: str
            The URL for the homepage/login page of the organization's Quickbase
            instance (e.g. demo.quickbase.com).
        user_token: str
            The Quickbase account user token (API key). Not required if
            ``QUICKBASE_USER_TOKEN`` env variable is set.
    `Returns:`
        Quickbase Class
    """
    def __init__(self, hostname=None, user_token=None):
        self.hostname = check_env.check('QUICKBASE_HOSTNAME', hostname)
        self.user_token = check_env.check('QUICKBASE_USER_TOKEN', user_token)
        self.api_hostname = 'https://api.quickbase.com/v1'
        self.client = APIConnector(self.api_hostname,
                                   headers={'QB-Realm-Hostname': self.hostname,
                                            'AUTHORIZATION': f'QB-USER-TOKEN {self.user_token}'})

    def get_app_tables(self, app_id=None):
        """
        Query records in a Quickbase table. This follows the patterns laid out
        in Quickbase query documentaiton, located here:
        https://help.quickbase.com/api-guide/componentsquery.html

        `Args:`
            app_id: str
                Identifies which Quickbase app from which to fetch tables.
        `Returns:`
            Table Class
        """
        return Table(self.client.request(
            f'{self.api_hostname}/tables?appId={app_id}',
            'GET').json())

    def query_records(self, table_from=None):
        """
        Query records in a Quickbase table. This follows the patterns laid out
        in Quickbase query documentaiton, located here:
        https://help.quickbase.com/api-guide/componentsquery.html

        `Args:`
            from: str
                The ID of a Quickbase resource (i.e. a table) to query.
        `Returns:`
            Table Class
        """
        req_resp = \
            (self.client.request(f'{self.api_hostname}/records/query',
                                 'POST',
                                 json={"from": table_from}).json())

        resp_tbl = Table(req_resp['data'])
        cleaned_tbl = Table()

        for row in resp_tbl:
            row_dict = {}
            for column in resp_tbl.columns:
                row_dict[column] = row[column]['value']
            cleaned_tbl.concat(Table([row_dict]))

        column_resp = req_resp['fields']
        column_map = {}
        for entry in column_resp:
            column_map[str(entry['id'])] = entry['label'].lower().strip()

        for column in cleaned_tbl.columns:
            cleaned_tbl.rename_column(column, column_map[column])

        return cleaned_tbl