How to unsubscribe / delete list members using Mailchimp3 in Python? - python

I am using mailchimp3 in Python. I managed to make batch insertion of users using this function:
client = MailChimp(USERNAME, APIKEY)
def fill_list(list_id, subscribers_data):
data = {'operations': create_subscriptions_data(list_id, subscribers_data)}
client.batches.create(data)
def create_subscriptions_data(list_id, users_data):
return [{'method': 'PUT',
'path': 'lists/{}/members/{}'.format(list_id, str(md5(user['email_address'].encode('utf-8')))),
'body': json.dumps(user)} for user in users_data]
Here is how one user dict looks like:
{"email_address": "user#somemail.com", "status": "subscribed"}
Then I wanted to use similar method to unsubscribe list of users. To achieve that I tried to use the same logic, just to change the user objects. Now, I used this user format:
{"email_address": "user#somemail.com", "status": "unsubscribed"}
It doesn't update the subscribe status. When I deleted all users manually (using the web interface) and tried the same command I successfully created users with "status": "unsubscribed". I am wondering why this approach can't change the status? I tried also using POST instead of PUT, but it didn't work. Any idea what can be the issue?
I used this reference https://devs.mailchimp.com/blog/batch-operations-and-put-in-api-v3-0/ and it mentions that this approach should work fine for updates as well.
Thank you in advance!

The only way to unsubscribe an already subscribed user will be to update with a list id and an MD5 hash of the lowercase version of the list member’s email address.
client.lists.members.update('LIST_ID', 'MD5 HASH', {"status":
"unsubscribed"})

Actually, I was using some wrong functions, so here is the fixed code. I also had some problems with the size of the batches. The maximum batch size is 500, so I did some splits of the data across several batches.
Here is a simple code how the insertion should be done:
client = MailChimp(USERNAME, APIKEY)
def _update_list(list_id: str, members_data: list):
client.lists.update_members(list_id, {'members': members_data, 'update_existing': True})
Each member in members_data has data like this:
mailchimp_user = {
'email_address': user.email,
'status': user.subscription_status,
'merge_fields': {
'FNAME': user.first_name,
'LNAME': user.last_name
},
'interests': {}
}
And here comes the most important function:
def fill_in_multiple_batches(list_id, mailchimp_members):
step_size = 400
for i in range(0, len(mailchimp_members), step_size):
batch_start_idx = i
batch_end_idx = min(i + step_size, len(mailchimp_members))
this_batch_of_members = mailchimp_members[batch_start_idx:batch_end_idx]
client.lists.update_members(list_id, {'members': members_data, 'update_existing': True})
After that, in the main of the script:
if __name__ == '__main__':
fill_in_multiple_batches('your_list_id', your_data_list)

Related

Why does stripe.Account.retrieve("") return an account, and what account is it?

I'm attempting to make a utility function to check if a user has submitted their onboarding details.
Ex:
#property
def is_onboarded(self):
stripe = stripe_config()
a = stripe.Account.retrieve(self.stripe_user_id)
return a.details_submitted
If self.stripe_user_id is an empty string, it returns some account for an ID I can't track down (don't see it in the online Connect interface).
Ex:
>>> stripe.Account.retrieve('')
<Account account id=acct_abc123abc1235N at 0x264b6b6cd00> JSON: {
"capabilities": {
"acss_debit_payments": "active",
...
My simple question is, why? And, what account is it returning?
It returns the platform account (ie. your account; the one you're making the request with).

Update a subscriber custom field using campaign monitor API and python

I've tried getting this to work every which way I can. I've poured through the campaign monitor API docs and search every possible article on the internet I could find. I didn't find one single example of code to update a subscriber record. I can get the record. I just can't update it and I am hoping someone can help me figure out what I am doing wrong. Thank you!
from createsend import *
def GetSubscribers(token, refresh_token, list_id):
auth = {
'access_token': token,
'refresh_token': refresh_token}
user_email = 'email#example.com'
subscriber = Subscriber(auth, list_id, user_email).get()
for i in subscriber.CustomFields:
for n in i.__dict__.keys():
print(i.__dict__[n])
if i.__dict__[n] == "campaign_code":
print(n, i.__dict__[n])
print(i.__dict__["Value"])
campaign_code = i.__dict__["Value"]
new_campaign_code = campaign_code + "_REPLIED"
print(new_campaign_code)
custom_fields = [
{"Key": i.__dict__[n], "Value": new_campaign_code}]
Subscriber.update(user_email, "Subscriber",
custom_fields, True, True, "Yes")
params = {"email": self.email_address}
AttributeError: 'str' object has no attribute 'email_address'
I figured it out. This is the correct structure to update a subscriber using the createsend python wrapper:
custom_fields = [{"Key": key, "Value": value}]
subscriber = Subscriber(auth, list_id, user_email).update(
new_user_email, "Subscriber", custom_fields, True, "Yes")
This one took a lot of time for me to figure out. I hope this post will help save some time for anyone trying to update subscribers in campaign monitor.

Jira API Assignee not populating with "name" anymore, instead needs "accoundId"

I'm working with Python 3.x
Previously, I had a function to create tickets that looks like this
def jira_incident(jira_subject, jira_description):
user = "username"
apikey = 'apikey'
server = 'https://serverName.atlassian.net'
options = {
'server': server,
'verify': False
}
issue_dict = {
'project': {'key': 'project_name'},
'summary': str(jira_subject),
'description': str(jira_description),
'issuetype': {'name': 'Incident'},
'assignee': {'name': my_username},
'priority': {'name': 'Low'},
'customfield_10125':
{'value': 'Application Ops'}
}
jira = JIRA(options, basic_auth=(user, apikey))
new_issue = jira.create_issue(fields=issue_dict)
return new_issue
my_username is a global variable that's used for other things as well.
Anyway, the assignee is no longer working as of about 2 days ago. I did some googling and found that it now needs the accountId instead of the name, I can get this via the web UI by leaving a comment as #'ing someone in a comment. As a temporary solution I've populated a dictionary to reference (and that works), however I'd like to make this more dynamic for future proofing the script.
'assignee': {'accountId': jira_dict[my_username]},
I can't seem to really find any documentation on looking up the accountId from the name, and I figured I'd go ahead and ask the community to see if anyone else has run into/solved this issue.
I was thinking about just writing a new function that performs this query for me, then returns the accountId.
EDIT
I did find this:
import requests
from requests.auth import HTTPBasicAuth
import json
url = "/rest/api/3/user/bulk/migration"
auth = HTTPBasicAuth("email#example.com", "<api_token>")
headers = {
"Accept": "application/json"
}
response = requests.request(
"GET",
url,
headers=headers,
auth=auth
)
print(json.dumps(json.loads(response.text), sort_keys=True, indent=4, separators=(",", ": ")))
However it 404's on me, I add the server address to the beginning of the url, and replace user, with the username in question.
Okay, I found a solution, it's not an elegant solution, but it does exactly what I need it to. So here is the new function:
def jira_account_id_from_username(username):
r = requests.get('https://serverName.atlassian.net/rest/api/3/user?username=' + username, auth=("username",api_key), verify=False)
value = re.search('"accountId":"(.*?)",', str(r.text)).group(1)
return value
I strongly encourage you to not rely on the username anymore. The endpoint you are using is deprecated, see also https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-user-privacy-api-migration-guide/.
The "new" or probably better way is to use the /user/search endpoint as described here: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-user-search-get There you can define a query that is matching against certain properties of a user (displayName or emailAddress), or search for the accountId if you already have it. Therefore, if you are linking users from the cloud to some other "user directory" (or just a scripts where you have stored some usernames), replace it by using either email address or accountId so you can properly link your users.

Reading a single user entry from REST GET if one can only GET a full user list

I use software that only lets me fetch information on all users, while my project requires me to get only one specific user. I only need his ID and I'm going to find it out by his e-mail (because it's unique).
The program creates an user (via POST) and stores his data (like email) in variables, then reads a provided list of devices that will be assigned to said user. To do this, the program has to:
Fetch all users (why software author didn't allow to fetch a single user is beyond me)
Filter users so it finds my newly created user <- here's my issue
Fetch all devices...
Filter devices...
Finally create a permission relationship between found user and device IDs
This is what I came up with so far:
inputText = self.imeiInput.GetValue().splitlines() # reads input and creates a list
url = "https://example.com/api/users/"
userList = requests.get(url, auth=(login, password)).content
foundUser = re.findall(givenMail, str(userList)) # givenMail provided by function argument
print(userList)
print(foundUser) # prints only searched mail
for imei in inputText:
self.TrackerAssign(imei, foundUser) # do your stuff
But it only confirms that there indeed is my email in the userbase, there's no other info (like ID I'm interested in). Here's a sample userList output (except that originally it's all in one line):
b'[{
"id":1,
"attributes": {
...
},
"name":"username",
"login":"",
"email":"test#example.com",
"phone":"999999999",...
},
{
"id":2,
"attributes": {
...
},
"name":"username2",
"login":"",
"email":"test2#exmaple.com",
"phone":"888888888",...
},
...]'
Then there's also a question how to only read the desired ID. Sorry if this is confusing, I barely know what I'm doing myself.
From your example it seems like you get a JSON response from the endpoint. This is great, because this JSON can be parsed to a list/dictionary!
Your approach can look something like this. In broad terms, this is a possible strategy:
Get all users from the users endpoint.
Parse the response JSON to a list of dictionaries.
Loop over all users, breaking the loop when you find your user.
Do something with the ID of the user that you found in your search.
response = requests.get(url, auth=(login, password)) # receive a Response instance
user_list = response.json() # parse the body to a dictionary
for user in user_list:
# Compare the email of this user with the target, using get to catch users with no email specified.
if user.get("email") == given_mail:
desired_user = user
break # if we find the correct user, we exit the loop
else:
# If we never find the desired user, we raise an exception.
raise ValueError("There is no user with email %s", given_email")
print(f"The ID of the user I am looking for is {desired_user["id"]}.")

Do I need to use mocks?

I have the function which handles a specific bot event from Slack. Generally speaking, user clicks a button then my server receives and handles payload of this button.
The question is how should I test it? Do I need to mock make_admin and build_admins_message and check that they were called or I need to test real implementations? For example, I can retrieve the user from the database and check that it is actually an admin and also check that build_admins_message returns a dictionary that I expect to receive.
#slack_interactions.on('admin_add')
def handle_admin_add(payload):
team_id = payload['team']['id']
user_id = payload['user']['id']
action_value = payload['actions'][0]['selected_options'][0]['value']
user = SlackUser.objects.find_by_ids(team_id, action_value)
if user and not user.is_bot:
user.make_admin()
return build_admins_message(team_id, user_id)
Currently my tests look like this:
class TestAdminAddHandler(TestCase):
def setUp(self):
team = SlackTeam.objects.create(team_id='TEAMID')
SlackUser.objects.create(team=team, user_id='USERID')
SlackUser.objects.create(team=team, user_id='BOTID', is_bot=True)
SlackUser.objects.create(
team=team, user_id='ADMINID', is_bot_admin=True)
def tearDown(self):
SlackUser.objects.all().delete()
SlackTeam.objects.all().delete()
def test_wrong_callback(self):
payload = {'callback_id': 'wrong_callback'}
message = handle_admin_add(payload)
self.assertIsNone(message)
def test_has_no_user(self):
payload = {
'callback_id': 'admin_add',
'team': {'id': 'TEAMID'},
'user': {'id': 'ADMINID'},
'actions': [{
'selected_options': [{'value': 'BADID'}]
}]
}
message = handle_admin_add(payload)
user = SlackUser.objects.get(user_id='USERID')
self.assertFalse(user.is_bot_admin)
for att in message['attachments']:
self.assertNotIn('BADID', att.get('title', ''))
def test_user_is_bot(self):
payload = {
'callback_id': 'admin_add',
'team': {'id': 'TEAMID'},
'user': {'id': 'ADMINID'},
'actions': [{
'selected_options': [{'value': 'BOTID'}]
}]
}
message = handle_admin_add(payload)
user = SlackUser.objects.get(user_id='BOTID')
self.assertFalse(user.is_bot_admin)
for att in message['attachments']:
self.assertNotIn('BOTID', att.get('title', ''))
def test_add_admin(self):
payload = {
'callback_id': 'admin_add',
'team': {'id': 'TEAMID'},
'user': {'id': 'ADMINID'},
'actions': [{
'selected_options': [{'value': 'USERID'}]
}]
}
message = handle_admin_add(payload)
user = SlackUser.objects.filter(user_id='USERID').first()
self.assertTrue(user.is_bot_admin)
user_in_list = False
for att in message['attachments']:
if 'USERID' in att.get('title', ''):
user_in_list = True
self.assertTrue(user_in_list)
The problem here is two-fold. First, you have to verify that your code would work correctly with a functioning Slack server - as you have deduced, mocks would be a good way to do this, because unit tests should be completely standalone. You can also write mocks to emulate the behaviour of an incorrectly-functioning server.
However, this leaves open the possibility that your mocks don't correctly emulate the behaviour of the Slack server, and therefore your code doesn't work in real life even though it passes unit testing. In order to do that you will need integration tests to verify (as your current test class appears to) that the code works correctly against a Slack server.
When creating mock objects you can even go as far as capturing network traffic from successful transactions and then using that content to generate mock responses by patching lower-level components to generate the appropriate network-level responses to avoid interaction with the server. Since you don't often have the ability to modify production servers for testing convenience, mocking is often the simplest way to verify correct handling of unusual server responses. It all depends how far you want to go.
Unit tests should validate the function of a single component, and should not rely on any external services. Integration tests verify that the code functions correctly in conjunction with other components, and will normally be performed only when the individual components' integrity has been verified.
Testing is a very large subject, so I hope this answers your question.

Categories