Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
willyraedy committed Sep 22, 2023
2 parents 29e9f3e + 1edd75f commit f6ee530
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 453 deletions.
15 changes: 8 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
We're thrilled that you're thinking about contributing to Parsons! Welcome to our contributor community.

Here are some ways you can contribute:
You can find a detailed version of this guide [on our website](https://www.parsonsproject.org/pub/contributing-guide/).

* [submit an issue](#submitting-issues)
* [contribute code](#contributing-code-to-parsons)
* [contribute documentation](#documentation)
* [add sample code to our library of examples](#contributing-sample-code)
The best way to get involved is by joining our Slack. To join, email [email protected]. In addition to all the great discussions that happen on our Slack, we also have virtual events including trainings, pairing sessions, social hangouts, discussions, and more. Every other Thursday afternoon we host 🎉 Parsons Parties 🎉 on Zoom where we work on contributions together.

Every other Thursday afternoon we host 🎉 Parsons Parties 🎉 on Zoom where we work on contributions together. Reach out if you'd like to join - it's a great way to get involved.
You can contribute by:

## Submitting Issues
* [submitting issues](https://www.parsonsproject.org/pub/contributing-guide#submitting-issues)
* [contributing code](https://www.parsonsproject.org/pub/contributing-guide/)
* [updating our documentation](https://www.parsonsproject.org/pub/updating-documentation/)
* [teaching and mentoring](https://www.parsonsproject.org/pub/contributing-guide#teaching-and-mentoring)
* [helping "triage" issues and review pull requests](https://www.parsonsproject.org/pub/contributing-guide#maintainer-tasks)

We encourage folks to review existing issues before starting a new issue.

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.7
FROM --platform=linux/amd64 python:3.7

####################
## Selenium setup ##
Expand Down
399 changes: 1 addition & 398 deletions docs/build_a_connector.rst

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
Contributing to Parsons
=======================

.. include:: ../CONTRIBUTING.md
:parser: myst_parser.sphinx_

The contributing guide has been moved to the Parsons website! You can find it `here <https://www.parsonsproject.org/pub/contributing-guide/>`_.
22 changes: 14 additions & 8 deletions docs/zoom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@ Overview

`Zoom <https://zoom.us>`_ is a video conferencing platform. This connector supports
fetching users, fetching meetings, fetching metadata for past meetings, and fetching
participants of past meetings via the `Zoom API <https://marketplace.zoom.us/docs/api-reference/zoom-api/>`_.
participants of past meetings via the `Zoom API <https://developers.zoom.us/docs/api/>`_.

.. note::
Authentication
The ``Zoom`` class supports `JSON Web Token Authentication <https://marketplace.zoom.us/docs/guides/auth/jwt>`_.
You must `Create a JWT App <https://marketplace.zoom.us/docs/guides/build/jwt-app>`_ to obtain
an API Key and API Secret for authentication.
The ``Zoom`` class uses server-to-server `OAuth <https://developers.zoom.us/docs/internal-apps/s2s-oauth/>`
to authenticate queries to the Zoom API. You must create a server-to-server application in
`Zoom's app marketplace <https://marketplace.zoom.us/develop/create>` to obtain an
``account_id``, ``client_id``, and ``client_secret`` key. You will use this OAuth application to define your scopes,
which gives your ``Zoom`` connector read permission on endpoints of your choosing (`meetings`, `webinars`, etc.)

***********
Quick Start
***********

To instantiate the ``Zoom`` class, you can either store your Zoom API
key and secret as environmental variables (``ZOOM_API_KEY`` and ``ZOOM_API_SECRET``,
respectively) or pass them in as arguments:
To instantiate the ``Zoom`` class, you can either store your Zoom account ID, client ID, and client secret
as environmental variables (``ZOOM_ACCOUNT_ID``, ``ZOOM_CLIENT_ID``, ``ZOOM_CLIENT_SECRET``)
or pass them in as arguments.

.. code-block:: python
Expand All @@ -32,7 +34,11 @@ respectively) or pass them in as arguments:
zoom = Zoom()
# If providing authentication credentials via arguments
zoom = Zoom(api_key='my_api_key', api_secret='my_api_secret')
zoom = Zoom(
account_id="my_account_id",
client_id="my_client_id",
client_secret="my_client_secret"
)
# Get a table of host's meetings via their email or user id
meetings_tbl = zoom.get_meetings('[email protected]')
Expand Down
86 changes: 83 additions & 3 deletions parsons/action_builder/action_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,11 @@ def insert_entity_record(self, entity_type, data=None, campaign=None):
Dict containing Action Builder entity data.
""" # noqa: E501

name_keys = ("name", "action_builder:name", "given_name")
error = "Must provide data with name or given_name when inserting new record"
if not isinstance(data, dict):
raise ValueError(error)
name_check = [
key for key in data.get("person", {}) if key in ("name", "given_name")
]
name_check = [key for key in data.get("person", {}) if key in name_keys]
if not name_check:
raise ValueError(error)

Expand Down Expand Up @@ -290,6 +289,87 @@ def add_section_field_values_to_record(
identifier=identifier, data=data, campaign=campaign
)

def remove_tagging(
self,
identifier=None,
tag_id=None,
tag_name=None,
tagging_id=None,
campaign=None,
):
"""
Remove one or more tags (i.e. custom field value) from an existing entity or connection
record in Action Builder. The basis for this end point is the combination of the tag's
interact ID and that of the specific tagging. The tag ID can usually be determined from
the tag's name, and the tagging ID can be derived if the identifier of the entity or
connection record is supplied instead.
`Args:`
identifier: str
Optional. The unique identifier for an entity or connection record being updated.
If omitted, `tagging_id` must be provided.
tag_id: str
Optional. The unique identifier for the tag being removed. If omitted, `tag_name`
must be provided.
tag_name: str
Optional. The exact name of the tag being removed. May result in an error if
multiple tags (in different fields/sections) have the same name. If omitted,
`tag_id` must be provided.
tagging_id: str
Optional. The unique identifier for the specific application of the tag to an
individual entity or connection record. If omitted, `identifier` must be provided.
campaign: str
Optional. The 36-character "interact ID" of the campaign whose data is to be
retrieved or edited. Not necessary if supplied when instantiating the class.
`Returns:`
API response JSON which contains `{'message': 'Tag has been removed from Taggable
Logbook'}` if successful.
"""

if {tag_name, tag_id} == {None}:
raise ValueError("Please supply a tag_name or tag_id!")

if {identifier, tagging_id} == {None}:
raise ValueError(
"Please supply an entity or connection identifier, or a tagging id!"
)

campaign = self._campaign_check(campaign)
endpoint = "tags/{}/taggings"

if tag_name and {tag_id, tagging_id} == {None}:
tag_data = self.get_tag_by_name(tag_name, campaign=campaign)
tag_count = tag_data.num_rows

if tag_count > 1:
error_msg = f"Found {tag_count} tags with this name. "
error_msg += "Try with using the unique interact ID"
raise ValueError(error_msg)

tag_id = tag_data["identifiers"][0][0].split(":")[1]
logger.info(f"Tag {tag_name} has ID {tag_id}")

if tagging_id and not tag_id:
raise ValueError("Cannot search based on tagging ID alone.")

if tag_id and not tagging_id:
taggings = self._get_all_records(self.campaign, endpoint.format(tag_id))
taggings_filtered = taggings.select_rows(
lambda row: identifier
in row["_links"]["action_builder:connection"]["href"]
if row["item_type"] == "connection"
else identifier in row["osdi:person"]["href"]
)
tagging_id = [
x.split(":")[1]
for x in taggings_filtered["identifiers"][0]
if "action_builder" in x
][0]

logger.info(f"Removing tag {tag_id} from {identifier or tagging_id}")
return self.api.delete_request(
f"campaigns/{campaign}/{endpoint.format(tag_id)}/{tagging_id}"
)

def upsert_connection(self, identifiers, tag_data=None, campaign=None):
"""
Load or update a connection record in Action Builder between two existing entity records.
Expand Down
61 changes: 59 additions & 2 deletions parsons/action_kit/action_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,63 @@ def update_event(self, event_id, **kwargs):
)
logger.info(f"{resp.status_code}: {event_id}")

def get_blackholed_email(self, email):
"""
Get a blackholed email. A blackholed email is an email that has been prevented from
receiving bulk and transactional emails from ActionKit. `Documentation <https://\
docs.actionkit.com/docs/manual/guide/mailings_tools.html#blackhole>`_.
`Args:`
email: str
Blackholed email of the record to get.
`Returns`:
Parsons.Table
The blackholed email data.
"""

return self.paginated_get("blackholedemail", email=email)

def blackhole_email(self, email):
"""
Prevent an email from receiving bulk and transactional emails from ActionKit.
`Documentation <https://docs.actionkit.com/docs/manual/guide/\
mailings_tools.html#blackhole>`_.
`Args:`
user_id: str
Email to blackhole
`Returns:`
API location of new resource
"""

return self._base_post(
endpoint="blackholedemail",
exception_message="Could not blackhole email",
email=email,
)

def delete_user_data(self, email, **kwargs):
"""
Delete user data.
`Args:`
email: str
Email of user to delete data
**kwargs:
Optional arguments and fields to pass to the client. A full list can be found
in the `ActionKit API Documentation <https://docs.actionkit.com/docs/manual/api/\
rest/users.html>`_.
`Returns:`
API location of anonymized user
"""

return self._base_post(
endpoint="eraser",
exception_message="Could not delete user data",
email=email,
**kwargs,
)

def delete_user(self, user_id):
"""
Delete a user.
Expand Down Expand Up @@ -857,7 +914,7 @@ def get_orders(self, limit=None, **kwargs):
ak.get_orders(import_id="my-import-123")
`Returns:`
Parsons.Table
The events data.
The orders data.
"""
return self.paginated_get("order", limit=limit, **kwargs)

Expand Down Expand Up @@ -1034,7 +1091,7 @@ def get_transactions(self, limit=None, **kwargs):
ak.get_transactions(order="order-1")
`Returns:`
Parsons.Table
The events data.
The transactions data.
"""
return self.paginated_get("transaction", limit=limit, **kwargs)

Expand Down
31 changes: 20 additions & 11 deletions parsons/action_network/action_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ def upsert_person(
creating a new one, you must supply an email or mobile number which matches a record
in the database.
Identifiers are intentionally not included as an option on
this method, because their use can cause buggy behavior if
they are not globally unique. ActionNetwork support strongly
encourages developers not to use custom identifiers.
`Args:`
email_address:
Either email_address or mobile_number are required. Can be any of the following
Expand Down Expand Up @@ -171,39 +176,39 @@ def upsert_person(
Adds a person to Action Network
"""
email_addresses_field = None
if type(email_address) == str:
if isinstance(email_address, str):
email_addresses_field = [{"address": email_address}]
elif type(email_address) == list:
if type(email_address[0]) == str:
elif isinstance(email_address, list):
if isinstance(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:
if isinstance(email_address[0], dict):
email_addresses_field = email_address

mobile_numbers_field = None
if type(mobile_number) == str:
if isinstance(mobile_number, str):
mobile_numbers_field = [
{"number": re.sub("[^0-9]", "", mobile_number), "status": mobile_status}
]
elif type(mobile_number) == int:
elif isinstance(mobile_number, int):
mobile_numbers_field = [
{"number": str(mobile_number), "status": mobile_status}
]
elif type(mobile_number) == list:
elif isinstance(mobile_number, list):
if len(mobile_number) > 1:
raise ("Action Network allows only 1 phone number per activist")
if type(mobile_number[0]) == str:
if isinstance(mobile_number[0], list):
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:
if isinstance(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:
if isinstance(mobile_number[0], dict):
mobile_numbers_field = mobile_number

if not email_addresses_field and not mobile_numbers_field:
Expand Down Expand Up @@ -242,7 +247,11 @@ def upsert_person(
entry_id.split(":")[1]
for entry_id in identifiers
if "action_network:" in entry_id
][0]
]
if not person_id:
logger.error(f"Response gave no valid person_id: {identifiers}")
else:
person_id = person_id[0]
if response["created_date"] == response["modified_date"]:
logger.info(f"Entry {person_id} successfully added.")
else:
Expand Down
12 changes: 5 additions & 7 deletions parsons/auth0/auth0.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,11 @@ def get_users_by_email(self, email):
`Returns:`
Table Class
"""
return Table(
requests.get(
f"{self.base_url}/api/v2/users-by-email",
headers=self.headers,
params={"email": email},
).json()
)
url = f"{self.base_url}/api/v2/users-by-email"
val = requests.get(url, headers=self.headers, params={"email": email})
if val.status_code == 429:
raise requests.exceptions.ConnectionError(val.json()["message"])
return Table(val.json())

def upsert_user(
self,
Expand Down
Loading

0 comments on commit f6ee530

Please sign in to comment.