def get_records(self, fields=None, max_records=None, view=None, formula=None, sort=None): """ `Args:` fields: str or lst Only return specified column or list of columns. The column name is case sensitive max_records: int The maximum total number of records that will be returned. view: str If set, only the records in that view will be returned. The records will be sorted according to the order of the view. formula: str The formula will be evaluated for each record, and if the result is not 0, false, "", NaN, [], or #Error! the record will be included in the response. If combined with view, only records in that view which satisfy the formula will be returned. For example, to only include records where ``COLUMN_A`` isn't empty, pass in: ``"NOT({COLUMN_A}='')"`` For more information see `Airtable Docs on formulas. <https://airtable.com/api>`_ Usage - Text Column is not empty: ``airtable.get_all(formula="NOT({COLUMN_A}='')")`` Usage - Text Column contains: ``airtable.get_all(formula="FIND('SomeSubText', {COLUMN_STR})=1")`` sort: str or lst Specifies how the records will be ordered. If you set the view parameter, the returned records in that view will be sorted by these fields. If sorting by multiple columns, column names can be passed as a list. Sorting Direction is ascending by default, but can be reversed by prefixing the column name with a minus sign -. Example usage: ``airtable.get_records(sort=['ColumnA', '-ColumnB'])`` `Returns:` Parsons Table See :ref:`parsons-table` for output options. """ # Raises an error if sort is None type. Thus, only adding if populated. kwargs = {'fields': fields, 'max_records': max_records, 'view': view, 'formula': formula} if sort: kwargs['sort'] = sort tbl = Table(self.at.get_all(**kwargs)) # If the results are empty, then return an empty table. if 'fields' not in tbl.columns: return Table([[]]) return tbl.unpack_dict(column='fields', prepend=False)
def convert_to_table(self, data): # Internal method to create a Parsons table from a data element. table = None if type(data) is list: table = Table(data) else: table = Table([data]) return table
def test_create_leads(self, m): m.post(HUSTLE_URI + 'groups/cMCH0hxwGt/leads', json=expected_json.leads_tbl_01) tbl = Table([['phone_number', 'ln', 'first_name'], ['4435705355', 'Warren', 'Elizabeth'], ['5126993336', 'Obama', 'Barack']]) ids = self.hustle.create_leads(tbl, group_id='cMCH0hxwGt') assert_matching_tables(ids, Table(expected_json.leads['items']))
def test_get_leads(self, m): # By Organization m.get(HUSTLE_URI + 'organizations/cMCH0hxwGt/leads', json=expected_json.leads) leads = self.hustle.get_leads(organization_id='cMCH0hxwGt') assert_matching_tables(leads, Table(expected_json.leads['items'])) # By Group ID m.get(HUSTLE_URI + 'groups/cMCH0hxwGt/leads', json=expected_json.leads) leads = self.hustle.get_leads(group_id='cMCH0hxwGt') assert_matching_tables(leads, Table(expected_json.leads['items']))
def test_get_poll_locations(self, m): m.get(self.gc.uri + 'voterinfo', json=voterinfo_resp) expected_tbl = Table(polling_data) address_tbl = Table([['address'], ['900 N Washtenaw, Chicago, IL 60622'], ['900 N Washtenaw, Chicago, IL 60622']]) tbl = self.gc.get_polling_locations(2000, address_tbl) assert_matching_tables(tbl, expected_tbl)
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 test_geocode_address_batch(self): batch = [['id', 'street', 'city', 'state', 'zip'], ['1', '908 N Washtenaw', 'Chicago', 'IL', '60622'], ['2', '1405 Wilshire Blvd', 'Austin', 'TX', '78722'], ['3', '908 N Washtenaw', 'Chicago', 'IL', '60622'], ['4', '1405 Wilshire Blvd', 'Austin', 'TX', '78722'], ['5', '908 N Washtenaw', 'Chicago', 'IL', '60622']] tbl = Table(batch) self.cg.cg.addressbatch = mock.MagicMock(return_value=batch_resp) geo = self.cg.geocode_address_batch(tbl) assert_matching_tables(geo, Table(petl.fromdicts(batch_resp)))
def get_survey_responses(self, survey_id, page=None): """ Get the responses for a given survey. `Args:` survey_id: string The id of survey for which to retrieve the responses. page : int Retrieve a specific page of responses. If not given, then all pages are retrieved. `Returns:` Table Class """ r = self._client.api.surveyresponse.list(survey_id, page) logger.info(f"{survey_id}: {r['total_count']} responses.") data = r['data'] if not page: while r['page'] < r['total_pages']: r = self._client.api.surveyresponse.list(survey_id, page=(r['page']+1)) data.extend(r['data']) tbl = Table(data).add_column('survey_id', survey_id, index=1) logger.info(f"Found #{tbl.num_rows} responses.") return tbl
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 test_get_elections(self, m): m.get(self.gc.uri + 'elections', json=elections_resp) expected_tbl = Table(elections_resp['elections']) assert_matching_tables(self.gc.get_elections(), expected_tbl)
def test_list_files_by_id(self) -> None: # Count on environment variables being set box = Box() subfolder = box.create_folder_by_id( folder_name='id_subfolder', parent_folder_id=self.temp_folder_id) # Create a couple of files in the temp folder table = Table([['phone_number', 'last_name', 'first_name'], ['4435705355', 'Warren', 'Elizabeth'], ['5126993336', 'Obama', 'Barack']]) box.upload_table_to_folder_id(table, 'temp1', folder_id=subfolder) box.upload_table_to_folder_id(table, 'temp2', folder_id=subfolder) box.create_folder_by_id(folder_name='temp_folder1', parent_folder_id=subfolder) box.create_folder_by_id(folder_name='temp_folder2', parent_folder_id=subfolder) file_list = box.list_files_by_id(folder_id=subfolder) self.assertEqual(['temp1', 'temp2'], file_list['name']) # Check that if we delete a file, it's no longer there for box_file in file_list: if box_file['name'] == 'temp1': box.delete_file_by_id(box_file['id']) break file_list = box.list_files_by_id(folder_id=subfolder) self.assertEqual(['temp2'], file_list['name']) folder_list = box.list_folders_by_id(folder_id=subfolder)['name'] self.assertEqual(['temp_folder1', 'temp_folder2'], folder_list)
def get_constituents(self, page_number=1, page_size=50, order_by=None, order_direction=None, last_modified=None): """ `Args:` page_number: int Number of the page to fetch page_size: int Number of records per page (maximum allowed is 50) order_by: str Sorts by ``Id``, ``CreatedDate``, or ``LastModifiedDate`` (default ``Id``). order_direction: str Sorts the order_by in ``Asc`` or ``Desc`` order. last_modified: str Filters to constituents last modified after the specified date (ISO-8601 format). `Returns:` A Table of the entries. """ params = self._base_pagination_params(page_number, page_size) params.update(self._base_ordering_params(order_by, order_direction)) if last_modified: params["lastModified"] = last_modified response = self._base_get('constituents', params=params) return Table(response['Results'])
def test_get_leaderboard(self, m): m.get(self.ct.uri + '/leaderboard', json=expected_leaderboard) leaderboard = self.ct.get_leaderboard() exp_tbl = self.ct.unpack( Table(expected_leaderboard['result']['accountStatistics'])) assert_matching_tables(leaderboard, exp_tbl)
def get_leaderboard(self, start_date=None, end_date=None, list_ids=None, account_ids=None): """ Return advocates (person records). `Args:` start_date: str Filter to the earliest date at which a post could be posted. The time is formatted as UTC (e.g. ``yyyy-mm-ddThh:mm:ss``). end_date: str Filter to the latest date at which a post could be posted. The time is formatted as UTC (e.g. ``yyyy-mm-ddThh:mm:ss``). list_ids: list Filter to the ids of lists or saved searches to retrieve. account_ids: list A list of CrowdTangle accountIds to retrieve leaderboard data for. This and ``list_id`` are mutually exclusive; if both are sent, the ``account_ids`` value will be used. `Returns:` Parsons Table See :ref:`parsons-table` for output options. """ args = {'startDate': start_date, 'endDate': end_date, 'listIds': self.list_to_string(list_ids), 'accountIds': self.list_to_string(account_ids)} pt = Table(self.base_request('leaderboard', args=args)) logger.info(f'Retrieved {pt.num_rows} records from the leaderbooard.') self.unpack(pt) return pt
def _table_convert(self, obj): tbl = Table([x.__dict__['_properties'] for x in obj]) if 'subresource_uris' in tbl.columns and 'uri' in tbl.columns: tbl.remove_column('subresource_uris', 'uri') return tbl
def get_leads(self, organization_id=None, group_id=None): """ Get leads metadata. One of ``organization_id`` and ``group_id`` must be passed as an argument. If both are passed, an error will be raised. `Args:` organization_id: str The organization id. group_id: str The group id. `Returns:` Parsons Table See :ref:`parsons-table` for output options. """ if organization_id is None and group_id is None: raise ValueError('Either organization_id or group_id required.') if organization_id is not None and group_id is not None: raise ValueError( 'Only one of organization_id and group_id may be populated.') if organization_id: endpoint = f'organizations/{organization_id}/leads' logger.info(f'Retrieving {organization_id} organization leads.') if group_id: endpoint = f'groups/{group_id}/leads' logger.info(f'Retrieving {group_id} group leads.') tbl = Table(self._request(endpoint)) logger.info(f'Got {tbl.num_rows} leads.') return tbl
def test_upload_file(self) -> None: # Count on environment variables being set box = Box() table = Table([['phone_number', 'last_name', 'first_name'], ['4435705355', 'Warren', 'Elizabeth'], ['5126993336', 'Obama', 'Barack']]) box_file = box.upload_table_to_folder_id(table, 'phone_numbers', folder_id=self.temp_folder_id) new_table = box.get_table_by_file_id(box_file.id) # Check that what we saved is equal to what we got back self.assertEqual(str(table), str(new_table)) # Check that things also work in JSON box_file = box.upload_table_to_folder_id(table, 'phone_numbers_json', folder_id=self.temp_folder_id, format='json') new_table = box.get_table_by_file_id(box_file.id, format='json') # Check that what we saved is equal to what we got back self.assertEqual(str(table), str(new_table)) # Now check the same thing with paths instead of file_id path_filename = 'path_phone_numbers' box_file = box.upload_table(table, f'{self.temp_folder_name}/{path_filename}') new_table = box.get_table(path=f'{self.temp_folder_name}/{path_filename}') # Check that we throw an exception with bad formats with self.assertRaises(ValueError): box.upload_table_to_folder_id(table, 'phone_numbers', format='illegal_format') with self.assertRaises(ValueError): box.get_table_by_file_id(box_file.id, format='illegal_format')
def get_surveys(self, page=None): """ Get a table of lists under the account. `Args:` page : int Retrieve a specific page of responses. If not given, then all pages are retrieved. `Returns:` Table Class """ r = self._client.api.survey.list(page) data = r['data'] if not page: while r['page'] < r['total_pages']: r = self._client.api.survey.list(page=(r['page']+1)) data.extend(r['data']) tbl = Table(data).remove_column('links') tbl.unpack_dict('statistics', prepend=False) logger.info(f"Found {tbl.num_rows} surveys.") return tbl
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
def test_get_url(self): file_name = 'delete_me.csv' input_tbl = Table([['a'], ['1']]) self.cloud.upload_table(input_tbl, TEMP_BUCKET_NAME, file_name) url = self.cloud.get_url(TEMP_BUCKET_NAME, file_name) download_tbl = Table.from_csv(url) assert_matching_tables(input_tbl, download_tbl)
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()
def test_get_companies(self, m): processed_companies = Table([ {'id': 35015567, 'name': 'Company One', 'assignee_id': None, 'contact_type_id': 547508, 'details': None, 'email_domain': '*****@*****.**', 'tags': [], 'interaction_count': 1, 'date_created': 1558441519, 'date_modified': 1558441535, 'address_city': 'CityA', 'address_country': None, 'address_postal_code': '12345', 'address_state': 'New York', 'address_street': None}, {'id': 35026533, 'name': 'Company Two', 'assignee_id': None, 'contact_type_id': 547508, 'details': None, 'email_domain': '*****@*****.**', 'tags': [], 'interaction_count': 1, 'date_created': 1558452953, 'date_modified': 1558452967, 'address_city': 'CityB', 'address_country': None, 'address_postal_code': '23451', 'address_state': 'New York', 'address_street': None}, {'id': 35014973, 'name': 'Company Three', 'assignee_id': None, 'contact_type_id': 547508, 'details': None, 'email_domain': None, 'tags': [], 'interaction_count': 1, 'date_created': 1558434147, 'date_modified': 1558458137, 'address_city': None, 'address_country': None, 'address_postal_code': '34512', 'address_state': 'Alabama', 'address_street': None}, {'id': 35029116, 'name': 'Company Four', 'assignee_id': None, 'contact_type_id': 547508, 'details': None, 'email_domain': '*****@*****.**', 'tags': [], 'interaction_count': 0, 'date_created': 1558461301, 'date_modified': 1558461301, 'address_city': 'CityD ', 'address_country': None, 'address_postal_code': '45123', 'address_state': 'California', 'address_street': None}, {'id': 35082308, 'name': 'Company Five', 'assignee_id': None, 'contact_type_id': 547508, 'details': None, 'email_domain': '*****@*****.**', 'tags': [], 'interaction_count': 1, 'date_created': 1558639445, 'date_modified': 1558639459, 'address_city': 'CityE', 'address_country': None, 'address_postal_code': '51234', 'address_state': 'Arizona', 'address_street': None} ]) processed_companies_phones = Table([ {'id': 35082308, 'phone_numbers_category': 'work', 'phone_numbers_number': '123-555-9876'} ]) m.post(self.cp.uri + '/companies/search', json=self.paginate_callback, headers={"filename": "companies_search.json"}) processed_blob = self.cp.get_companies() blob_companies = [f for f in processed_blob if f['name'] == "companies"][0]['tbl'] blob_companies_phones = [f for f in processed_blob if f['name'] == "companies_phone_numbers"][0]['tbl'] assert_matching_tables(processed_companies, blob_companies) assert_matching_tables(processed_companies_phones, blob_companies_phones)
def test_create_leads(self, m): m.post(HUSTLE_URI + 'groups/cMCH0hxwGt/leads', json=expected_json.leads_tbl_01) tbl = Table([['phone_number', 'ln', 'first_name', 'address'], ['4435705355', 'Johnson', 'Lyndon', '123 Main Street'], ['4435705354', 'Richards', 'Ann', '124 Main Street']]) self.hustle.create_leads(tbl, group_id='cMCH0hxwGt')
def test_get_poll_location(self, m): m.get(self.gc.uri + 'voterinfo', json=voterinfo_resp) expected_tbl = Table(voterinfo_resp['pollingLocations']) tbl = self.gc.get_polling_location(2000, '900 N Washtenaw, Chicago, IL 60622') assert_matching_tables(tbl, expected_tbl)
def table_convert(self, obj): # Internal method to create a Parsons table from a Twilio object. tbl = Table([x.__dict__['_properties'] for x in obj]) if 'subresource_uris' in tbl.columns and 'uri' in tbl.columns: tbl.remove_column('subresource_uris', 'uri') return tbl
def test_insert_records(self, m): m.post(self.base_uri, json=insert_responses) tbl = Table([{'Name': 'Another row!'}, {'Name': 'Another!'}]) resp = self.at.insert_records(tbl) # Assert that row count is expected self.assertEqual(len(resp), 2)
def default_table(self): return Table([ { 'num': 1, 'ltr': 'a' }, { 'num': 2, 'ltr': 'b' }, ])
def get_elections(self): """ Get a collection of information about elections and voter information. `Returns:` Parsons Table See :ref:`parsons-table` for output options. """ url = self.uri + 'elections' return Table((self.request(url))['elections'])
def get_organizations(self): """ Get organizations. `Returns:` Parsons Table See :ref:`parsons-table` for output options. """ tbl = Table(self._request('organizations')) logger.info(f'Got {tbl.num_rows} organizations.') return tbl
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