Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to ActionKit, S3, and NGPVan #775

Merged
merged 64 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
c75d3ef
Create Google Admin connector
crayolakat Mar 22, 2022
0fa5763
Merge branch 'master' into moveon
crayolakat Mar 22, 2022
825f489
Update setup_google_application_credentials to support dict parameter
technicalex Apr 1, 2022
8584408
Merge pull request #9 from MoveOnOrg/alex-support-dict-param
technicalex Apr 5, 2022
2e2047d
Update authorization library for Google Sheets to support account del…
sjwmoveon Apr 8, 2022
ad77645
Typo fix
sjwmoveon Apr 8, 2022
5633286
Merge pull request #10 from MoveOnOrg/gsheets
sjwmoveon Apr 8, 2022
29cf819
Merge pull request #11 from MoveOnOrg/gsheets
sjwmoveon Apr 11, 2022
1d292b9
Don't auto-grab session token if it's not passed in
sjwmoveon Aug 5, 2022
9e493c3
Merge pull request #17 from MoveOnOrg/sjwmoveon-patch-1
sjwmoveon Aug 5, 2022
7e3f053
Add new general object search methods to ActionKit
sjwmoveon Aug 12, 2022
04bd69d
Remove unused import
sjwmoveon Aug 16, 2022
99cb2f6
Add subscription search to Braintree
sjwmoveon Aug 24, 2022
8a6ca72
Merge branch 'moveon' of https://github.com/MoveOnOrg/parsons into mo…
sjwmoveon Aug 24, 2022
b4a8525
Merge branch 'moveon' into braintree_subs
sjwmoveon Aug 24, 2022
24ea3cb
Merge pull request #19 from MoveOnOrg/braintree_subs
sjwmoveon Aug 26, 2022
5d4e07c
Merge pull request #20 from MoveOnOrg/ak_search
sjwmoveon Sep 6, 2022
170b99c
Define get_aliases
crayolakat Sep 16, 2022
fe3e35b
Fix tests
crayolakat Sep 16, 2022
6e108a0
Merge branch 'main' into moveon_update
crayolakat Sep 16, 2022
698697c
Merge branch 'main' into moveon_update
crayolakat Sep 27, 2022
c705352
Merge pull request #21 from MoveOnOrg/moveon_update
crayolakat Sep 27, 2022
da7cc54
Add use_env_token option to Redshift
sjwmoveon Sep 27, 2022
ca35938
Add os import
crayolakat Sep 27, 2022
63fe054
Adhere to flake8 linting rules
crayolakat Sep 27, 2022
4b9de69
Fix typo
crayolakat Sep 27, 2022
cf4c877
Merge pull request #23 from MoveOnOrg/moveon_update
crayolakat Sep 27, 2022
30a9a2e
Merge pull request #22 from MoveOnOrg/envfix
sjwmoveon Sep 28, 2022
da66ac7
Add use_env_token options
sjwmoveon Oct 6, 2022
3305ff0
S3 env token options for distributed tasks
sjwmoveon Oct 6, 2022
7b8bff2
use_env_token is positional
sjwmoveon Oct 6, 2022
457bb35
Add use_env_token to Redshift constructor
sjwmoveon Oct 6, 2022
6e5325c
Actually set use_env_token
sjwmoveon Oct 6, 2022
77bd5d7
Merge pull request #24 from sjwmoveon/patch-4
sjwmoveon Oct 6, 2022
e211068
Add method to update saved payment token
sjwmoveon Oct 7, 2022
5e319d1
Merge pull request #25 from MoveOnOrg/ak_token
sjwmoveon Oct 11, 2022
8bff543
Merge branch 'moveon' into kathy_fix_tests
crayolakat Oct 18, 2022
0826bf6
Merge pull request #26 from MoveOnOrg/kathy_fix_tests
crayolakat Oct 18, 2022
183c4eb
Return HTTP status code for error tracking
sjwmoveon Oct 19, 2022
d943f16
Merge pull request #27 from MoveOnOrg/aktok
sjwmoveon Oct 19, 2022
ce57b83
Merge branch 'moveon' into kathy_get_aliases
crayolakat Oct 20, 2022
90070c9
Add get_aliases function to Google Admin
crayolakat Oct 20, 2022
3147663
Fix tests
crayolakat Oct 20, 2022
6b61ade
Merge pull request #28 from MoveOnOrg/kathy_get_aliases
crayolakat Oct 22, 2022
90b1bf4
Add method to cancel recurring orders
sjwmoveon Oct 26, 2022
a0585b9
Add contact notes API for NGP VAN connector
sjwmoveon Oct 26, 2022
885c3fc
Fix API URL
sjwmoveon Oct 26, 2022
2a5c3c3
Merge pull request #29 from MoveOnOrg/recur
sjwmoveon Oct 27, 2022
5b6c020
Return status code for recurring order cancel
sjwmoveon Oct 28, 2022
1737259
Fix typo
sjwmoveon Oct 28, 2022
1bc988b
Merge pull request #31 from MoveOnOrg/ordcan
sjwmoveon Oct 31, 2022
7e67048
Merge pull request #30 from MoveOnOrg/contactnotes
sjwmoveon Oct 31, 2022
b0bd5cb
Merge branch 'moveon' into kathy_update
crayolakat Nov 21, 2022
45c7cd2
Merge pull request #32 from MoveOnOrg/kathy_update
crayolakat Nov 22, 2022
6f487fb
Lint
crayolakat Nov 22, 2022
368eb6c
Merge pull request #33 from MoveOnOrg/kathy_lint
crayolakat Nov 22, 2022
c6c9e9d
Lint
crayolakat Nov 22, 2022
0094031
Merge pull request #34 from MoveOnOrg/kathy_lint
crayolakat Nov 22, 2022
90e6f09
Add create_transaction method
crayolakat Nov 29, 2022
ca5509a
Merge pull request #35 from MoveOnOrg/kathy_create_transaction_clean
crayolakat Nov 29, 2022
1b87ed0
Merge branch 'main' into kathy_update
crayolakat Dec 2, 2022
49b31df
Merge pull request #36 from MoveOnOrg/kathy_update
crayolakat Dec 5, 2022
84c23a5
Add tests for ActionKit and Van
crayolakat Dec 5, 2022
df5e330
Merge pull request #37 from MoveOnOrg/kathy_add_tests
crayolakat Dec 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions parsons/action_kit/action_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,41 @@ def update_order(self, order_id, **kwargs):
data=json.dumps(kwargs))
logger.info(f'{resp.status_code}: {order_id}')

def cancel_orderrecurring(self, recurring_id):
"""
Cancel a recurring order.

`Args:`
recurring_id: int
The id of the recurring order to update (NOT the order_id)
`Returns:`
``None``
"""

resp = self.conn.post(self._base_endpoint('orderrecurring', str(recurring_id)+'/cancel'))
logger.info(f'{resp.status_code}: {recurring_id}')
return resp

def update_paymenttoken(self, paymenttoken_id, **kwargs):
"""
Update a saved payment token.

`Args:`
paymenttoken_id: int
The id of the payment token to update
**kwargs:
Optional arguments and fields to pass to the client. A full list can be found
in the `ActionKit API Documentation <https://roboticdogs.actionkit.com/docs/\
manual/api/rest/actionprocessing.html>`_.
`Returns:`
``HTTP response``
"""

resp = self.conn.patch(self._base_endpoint('paymenttoken', paymenttoken_id),
data=json.dumps(kwargs))
logger.info(f'{resp.status_code}: {paymenttoken_id}')
return resp

def get_page_followup(self, page_followup_id):
"""
Get a page followup.
Expand Down Expand Up @@ -784,6 +819,21 @@ def update_survey_question(self, survey_question_id, **kwargs):
data=json.dumps(kwargs))
logger.info(f'{resp.status_code}: {survey_question_id}')

def create_transaction(self, **kwargs):
"""
Create a transaction.

`Args:`
**kwargs:
Optional arguments and fields to pass to the client.
`Returns:`
Transaction json object
"""

return self._base_post(
endpoint='transaction', exception_message='Could not create transaction', **kwargs
)

def update_transaction(self, transaction_id, **kwargs):
"""
Update a transaction.
Expand Down
27 changes: 18 additions & 9 deletions parsons/aws/lambda_distribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class S3Storage:
inside this file rather than s3.py
"""

def __init__(self):
self.s3 = S3()
def __init__(self, use_env_token=True):
self.s3 = S3(use_env_token=use_env_token)

def put_object(self, bucket, key, object_bytes, **kwargs):
return self.s3.client.put_object(Bucket=bucket, Key=key, Body=object_bytes, **kwargs)
Expand Down Expand Up @@ -65,7 +65,8 @@ def distribute_task_csv(csv_bytes_utf8, func_to_run, bucket,
func_class_kwargs=None,
catch=False,
group_count=100,
storage='s3'):
storage='s3',
use_s3_env_token=True):
"""
The same as distribute_task, but instead of a table, the
first argument is bytes of a csv encoded into utf8.
Expand Down Expand Up @@ -98,7 +99,9 @@ def distribute_task_csv(csv_bytes_utf8, func_to_run, bucket,

response = None
if storage == 's3':
response = S3Storage().put_object(bucket, storagekey, csv_bytes_utf8)
response = S3Storage(use_env_token=use_s3_env_token).put_object(
bucket, storagekey, csv_bytes_utf8
)
else:
response = FAKE_STORAGE.put_object(bucket, storagekey, csv_bytes_utf8)

Expand All @@ -107,7 +110,7 @@ def distribute_task_csv(csv_bytes_utf8, func_to_run, bucket,
maybe_async_run(
process_task_portion,
[bucket, storagekey, grp[0], grp[1], func_name, header,
storage, func_kwargs, catch, func_class_kwargs],
storage, func_kwargs, catch, func_class_kwargs, use_s3_env_token],
# if we are using local storage, then it must be run locally, as well
# (good for testing/debugging)
remote_aws_lambda_function_name='FORCE_LOCAL' if storage == 'local' else None
Expand All @@ -125,7 +128,8 @@ def distribute_task(table, func_to_run,
func_class_kwargs=None,
catch=False,
group_count=100,
storage='s3'):
storage='s3',
use_s3_env_token=True):
"""
Distribute processing rows in a table across multiple AWS Lambda invocations.

Expand Down Expand Up @@ -165,6 +169,8 @@ def distribute_task(table, func_to_run,
storage: str
Debugging option: Defaults to "s3". To test distribution locally without s3,
set to "local".
use_s3_env_token: str
If storage is set to "s3", sets the use_env_token parameter on the S3 storage.
`Returns:`
Debug information -- do not rely on the output, as it will change
depending on how this method is invoked.
Expand All @@ -184,19 +190,22 @@ def distribute_task(table, func_to_run,
func_class_kwargs=func_class_kwargs,
catch=catch,
group_count=group_count,
storage=storage)
storage=storage,
use_s3_env_token=use_s3_env_token)


def process_task_portion(bucket, storagekey, rangestart, rangeend, func_name, header,
storage='s3', func_kwargs=None, catch=False,
func_class_kwargs=None):
func_class_kwargs=None, use_s3_env_token=True):
global FAKE_STORAGE

logger.debug(f'process_task_portion func_name {func_name}, '
f'storagekey {storagekey}, byterange {rangestart}-{rangeend}')
func = import_and_get_task(func_name, func_class_kwargs)
if storage == 's3':
filedata = S3Storage().get_range(bucket, storagekey, rangestart, rangeend)
filedata = S3Storage(use_env_token=use_s3_env_token).get_range(
bucket, storagekey, rangestart, rangeend
)
else:
filedata = FAKE_STORAGE.get_range(bucket, storagekey, rangestart, rangeend)

Expand Down
14 changes: 11 additions & 3 deletions parsons/databases/redshift/redshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,16 @@ class Redshift(RedshiftCreateTable, RedshiftCopyTable, RedshiftTableUtilities, R
iam_role: str
AWS IAM Role ARN string -- an optional, different way for credentials to
be provided in the Redshift copy command that does not require an access key.
use_env_token: bool
Controls use of the ``AWS_SESSION_TOKEN`` environment variable for S3. Defaults
to ``True``. Set to ``False`` in order to ignore the ``AWS_SESSION_TOKEN`` environment
variable even if the ``aws_session_token`` argument was not passed in.
"""

def __init__(self, username=None, password=None, host=None, db=None, port=None,
timeout=10, s3_temp_bucket=None,
aws_access_key_id=None, aws_secret_access_key=None, iam_role=None):
aws_access_key_id=None, aws_secret_access_key=None, iam_role=None,
use_env_token=True):
super().__init__()

try:
Expand All @@ -85,6 +90,7 @@ def __init__(self, username=None, password=None, host=None, db=None, port=None,
split_temp_bucket_name = self.s3_temp_bucket.split('/', 1)
self.s3_temp_bucket = split_temp_bucket_name[0]
self.s3_temp_bucket_prefix = split_temp_bucket_name[1]
self.use_env_token = use_env_token
# We don't check/load the environment variables for aws_* here
# because the logic in S3() and rs_copy_table.py does already.
self.aws_access_key_id = aws_access_key_id
Expand Down Expand Up @@ -353,7 +359,8 @@ def copy_s3(self, table_name, bucket, key, manifest=False, data_type='csv',
# Grab the object from s3
from parsons.aws.s3 import S3
s3 = S3(aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key)
aws_secret_access_key=aws_secret_access_key,
use_env_token=self.use_env_token)

local_path = s3.get_file(bucket, key)
if data_type == 'csv':
Expand Down Expand Up @@ -712,7 +719,8 @@ def generate_manifest(self, buckets, aws_access_key_id=None, aws_secret_access_k

from parsons.aws import S3
s3 = S3(aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key)
aws_secret_access_key=aws_secret_access_key,
use_env_token=self.use_env_token)

# Deal with a single bucket being passed, rather than list.
if isinstance(buckets, str):
Expand Down
9 changes: 4 additions & 5 deletions parsons/databases/redshift/rs_copy_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ class RedshiftCopyTable(object):
aws_secret_access_key = None
iam_role = None

def __init__(self):

pass
def __init__(self, use_env_token=True):
self.use_env_token = use_env_token

def copy_statement(self, table_name, bucket, key, manifest=False,
data_type='csv', csv_delimiter=',', max_errors=0,
Expand Down Expand Up @@ -113,7 +112,7 @@ def get_creds(self, aws_access_key_id, aws_secret_access_key):

else:

s3 = S3()
s3 = S3(use_env_token=self.use_env_token)
creds = s3.aws.session.get_credentials()
aws_access_key_id = creds.access_key
aws_secret_access_key = creds.secret_key
Expand All @@ -135,7 +134,7 @@ def temp_s3_copy(self, tbl, aws_access_key_id=None, aws_secret_access_key=None,
aws_secret_access_key = aws_secret_access_key or self.aws_secret_access_key

self.s3 = S3(aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key)
aws_secret_access_key=aws_secret_access_key, use_env_token=self.use_env_token)

hashed_name = hash(time.time())
key = f"{S3_TEMP_KEY_PREFIX}/{hashed_name}.csv.gz"
Expand Down
55 changes: 55 additions & 0 deletions parsons/ngpvan/contact_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""NGPVAN Contact Notes Endpoints"""

from parsons.etl.table import Table
import logging

logger = logging.getLogger(__name__)


class ContactNotes(object):

def __init__(self, van_connection):
self.connection = van_connection

def get_contact_notes(self, van_id):
"""
Get custom fields.

`Args:`
van_id : str
VAN ID for the person to get notes for.
`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table(self.connection.get_request(f'people/{van_id}/notes'))
logger.info(f'Found {tbl.num_rows} custom fields.')
return tbl

def create_contact_note(self, van_id, text, is_view_restricted, note_category_id=None):
"""
Create a contact note

`Args:`
van_id: str
VAN ID for the person this note will be applied to.
text: str
The content of the note.
is_view_restricted: bool
Set to true if the note should be restricted only to certain users within
the current context; set to false if the note may be viewed by any user
in the current context.
note_category_id: int
Optional; if set, the note category for this note.
`Returns:`
int
The note ID.
"""
note = {'text': text, 'isViewRestricted': is_view_restricted}
if note_category_id is not None:
note['category'] = {'noteCategoryId': note_category_id}

r = self.connection.post_request(f'people/{van_id}/notes', json=note)
logger.info(f'Contact note {r} created.')
return r
3 changes: 2 additions & 1 deletion parsons/ngpvan/van.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from parsons.ngpvan.locations import Locations
from parsons.ngpvan.bulk_import import BulkImport
from parsons.ngpvan.changed_entities import ChangedEntities
from parsons.ngpvan.contact_notes import ContactNotes
from parsons.ngpvan.custom_fields import CustomFields
from parsons.ngpvan.targets import Targets

Expand All @@ -21,7 +22,7 @@

class VAN(People, Events, SavedLists, Folders, ExportJobs, ActivistCodes, CanvassResponses,
SurveyQuestions, Codes, Scores, FileLoadingJobs, SupporterGroups, Signups, Locations,
BulkImport, ChangedEntities, CustomFields, Targets):
BulkImport, ChangedEntities, ContactNotes, CustomFields, Targets):
"""
Returns the VAN class

Expand Down
23 changes: 23 additions & 0 deletions test/test_action_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,29 @@ def test_update_survey_question(self):
data=json.dumps({'question_html': 'test'})
)

def test_create_transaction(self):
# Test create transaction

# Mock resp and status code
resp_mock = mock.MagicMock()
type(resp_mock.post()).status_code = mock.PropertyMock(return_value=201)
self.actionkit.conn = resp_mock

self.actionkit.create_transaction(
account='Account', amount=1, amount_converted=1, currency='USD', failure_code='',
failure_description='', failure_message='', order='/rest/v1/order/1/',
status='completed', success=True, test_mode=False, trans_id='abc123', type='sale'
)
self.actionkit.conn.post.assert_called_with(
'https://domain.actionkit.com/rest/v1/transaction/',
data=json.dumps({
'account': 'Account', 'amount': 1, 'amount_converted': 1, 'currency': 'USD',
'failure_code': '', 'failure_description': '', 'failure_message': '',
'order': '/rest/v1/order/1/', 'status': 'completed', 'success': True,
'test_mode': False, 'trans_id': 'abc123', 'type': 'sale'
})
)

def test_update_transaction(self):
# Test update transaction

Expand Down