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
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
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)
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)
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_request(self, endpoint, params=None, **kwargs): # Internal method to make a get request. r = self.client.request(self.uri + endpoint, 'GET', params=params) self.client.validate_response(r) data = r.json() return data 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.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.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.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.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.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()
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)