Skip to content

Commit

Permalink
Zoom Polls (#886)
Browse files Browse the repository at this point in the history
* Merge main into major-release (#814)

* Use black formatting in addition to flake8 (#796)

* Run black formatter on entire repository

* Update requirements.txt and CONTRIBUTING.md to reflect black format

* Use black linting in circleci test job

* Use longer variable name to resolve flake8 E741

* Move noqa comments back to proper lines after black reformat

* Standardize S3 Prefix Conventions (#803)

This PR catches exception errors when a user does not exhaustive access to keys in an S3 bucket

* Add Default Parameter Flexibility (#807)

Skips over new `/` logic checks if prefix is `None` (which is true by default)

* MoveOn Shopify / AK changes (#801)

* Add access_token authentication option for Shopify

* Remove unnecessary check
The access token will either be None or explicitly set; don't worry about an empty string.

* Add get_orders function and test

* Add get_transactions function and test

* Add function and test to get order

* style fixes

* style fixes

---------

Co-authored-by: sjwmoveon <[email protected]>
Co-authored-by: Alex French <[email protected]>
Co-authored-by: Kathy Nguyen <[email protected]>

* Catch File Extensions in S3 Prefix (#809)

* add exception handling

* Shortened logs for flake8

* add logic for default case

* added file logic + note to user

* restructured prefix logic

This change moves the prefix -> prefix/ logic into a try/except block ... this will be more robust to most use cases, while adding flexibility that we desire for split-permission buckets

* drop nested try/catch + add verbose error log

* Add error message verbosity

Co-authored-by: willyraedy <[email protected]>

---------

Co-authored-by: willyraedy <[email protected]>

---------

Co-authored-by: Austin Weisgrau <[email protected]>
Co-authored-by: Ian <[email protected]>
Co-authored-by: Cody Gordon <[email protected]>
Co-authored-by: sjwmoveon <[email protected]>
Co-authored-by: Alex French <[email protected]>
Co-authored-by: Kathy Nguyen <[email protected]>
Co-authored-by: willyraedy <[email protected]>

* black format

* black format

* jwt -> s2s oauth

* scaffold new functions

* add docs

* return

* DatabaseConnector Interface to Major Release (#815)

* Create the DatabaseConnector

* Implement DatabaseConnector for the DB connectors

* Add DatabaseConnector to std imports

* Flake8 fix

* Remove reference to padding in copy()

* Add database_discover and fix inheritance

* Remove strict_length from copy()

* Put strict_length back in original order

* Remove strict_length stub from BQ

* Fix discover_database export statement

* Add return annotation to mysql table_exists

* Black formatter pass

* Add more documentation on when you should use

* Add developer notes.

* Fix code block documentation

* Enhance discover database

* Add unit tests for discover database

* Fix unit tests

* Add two more tests

* Reverse Postgres string_length change

---------

Co-authored-by: Jason Walker <[email protected]>

* add type handling

* pass in updated params

* move access token function

* ok let's rock!!

* make changes

* pass access token key only

* use temporary client to gen token

* mock request in constructor

* drop unused imports

* add changes

* scaffolding tests

* Add multiple python versions to CI tests (#858)

* Add multiple python versions to CI tests

* Remove duplicate key

* Combine CI jobs

* Update ubuntu image and actually install Python versions

* Replace pyenv with apt-get to install python versions

* Remove sudo

* Remove get from 'apt-get'

* Update apt before attempting to install

* Add ppa/deadsnakes repository

* Add prereq

* Fix typo

* Add -y to install command

* Move -y to correct spot

* Add more -ys

* Add some echoes to debug

* Switch back to pyenv approach

* Remove tests from circleci config and move to new github actions config

Note: no caching yet, this is more of a proof of concept

* Split out Mac tests into seaparate file

* Set testing environmental variable separately

* First attempt to add depdendency cache

* Remove windows tests for now

* Fix circleci config

* Fix circleci for real this time

* Add tests on merging of PRs and update readme to show we do not support for Python 3.7

* Enable passing `identifiers` to ActionNetwork `upsert_person()` (#861)

* Enable passing `identifiers` to ActionNetwork upsert_person

* Remove unused arguments from method

self.get_page method doesn't exist and that method call doesn't return
anything. The return statement works fine as-is to return all tags and
handles pagination on its own.

* Include deprecated per_page argument for backwards compatibility

Emit a deprecation warning if this argument is used

* Include examples in docstring for `identifiers` argument

* Expand documentation on ActionNetwork identifiers

* Add pre-commit hook config to run flake8 and black on commit (#864)

Notes added to README on how to install and set up

* black format

* black format

* jwt -> s2s oauth

* scaffold new functions

* add docs

* return

* add type handling

* pass in updated params

* move access token function

* ok let's rock!!

* make changes

* pass access token key only

* use temporary client to gen token

* mock request in constructor

* drop unused imports

* add changes

* scaffolding tests

* write unit tests

* added testing

* drop typing (for now)

* update docstring typing

* add tests

* write functions

* update typing

* add poll results

* update table output

* fix tests

* uhhh run it back

* add scope requirements

* add to docs

We can add more here if folks see fit

* one for the money two for the show

---------

Co-authored-by: Jason <[email protected]>
Co-authored-by: Austin Weisgrau <[email protected]>
Co-authored-by: Cody Gordon <[email protected]>
Co-authored-by: sjwmoveon <[email protected]>
Co-authored-by: Alex French <[email protected]>
Co-authored-by: Kathy Nguyen <[email protected]>
Co-authored-by: willyraedy <[email protected]>
Co-authored-by: Jason Walker <[email protected]>
Co-authored-by: Shauna <[email protected]>
  • Loading branch information
10 people authored Sep 26, 2023
1 parent 4b24f40 commit 7ac73e2
Show file tree
Hide file tree
Showing 3 changed files with 789 additions and 4 deletions.
2 changes: 1 addition & 1 deletion docs/zoom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ participants of past meetings via the `Zoom API <https://developers.zoom.us/docs
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.)
which gives your ``Zoom`` connector read permission on endpoints of your choosing (`meetings`, `webinars`, `reports`, etc.)

***********
Quick Start
Expand Down
256 changes: 253 additions & 3 deletions parsons/zoom/zoom.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import jwt
import datetime
import uuid

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -119,6 +120,61 @@ def _get_request(self, endpoint, data_key, params=None, **kwargs):
data.extend(self.client.data_parse(r))
return Table(data)

def __handle_nested_json(self, table: Table, column: str) -> Table:
"""
This function unpacks JSON values from Zoom's API, which are often
objects nested in lists
`Args`:
table: parsons.Table
Parsons Table of Zoom API responses
column: str
Column name of nested JSON
`Returns`:
Parsons Table
"""

return Table(table.unpack_list(column=column)).unpack_dict(
column=f"{column}_0", prepend_value=f"{column}_"
)

def __process_poll_results(self, tbl: Table) -> Table:
"""
Unpacks nested poll results values from the Zoom reports endpoint
`Args`:
tbl: parsons.Table
Table of poll results derived from Zoom API request
`Returns`:
Parsons Table
"""

# Add surrogate key
tbl.add_column("poll_taker_id", lambda _: str(uuid.uuid4()))

# Unpack values
tbl = tbl.unpack_nested_columns_as_rows(
"question_details", key="poll_taker_id", expand_original=True
)

# Remove extraneous columns
tbl.remove_column("poll_taker_id")
tbl.remove_column("question_details")

# Unpack question values
tbl = tbl.unpack_dict(
"question_details_value", include_original=True, prepend=False
)

# Remove column from API response
tbl.remove_column("question_details_value")
tbl.remove_column("uid")

return tbl

def get_users(self, status="active", role_id=None):
"""
Get users.
Expand Down Expand Up @@ -181,7 +237,7 @@ def get_past_meeting(self, meeting_uuid):
Get metadata regarding a past meeting.
`Args:`
meeting_id: str
meeting_id: int
The meeting id
`Returns:`
Parsons Table
Expand All @@ -197,7 +253,7 @@ def get_past_meeting_participants(self, meeting_id):
Get past meeting participants.
`Args:`
meeting_id: str
meeting_id: int
The meeting id
`Returns:`
Parsons Table
Expand All @@ -215,7 +271,7 @@ def get_meeting_registrants(self, meeting_id):
Get meeting registrants.
`Args:`
meeting_id: str
meeting_id: int
The meeting id
`Returns:`
Parsons Table
Expand Down Expand Up @@ -275,3 +331,197 @@ def get_webinar_registrants(self, webinar_id):
tbl = self._get_request(f"webinars/{webinar_id}/registrants", "registrants")
logger.info(f"Retrieved {tbl.num_rows} webinar registrants.")
return tbl

def get_meeting_poll_metadata(self, meeting_id, poll_id) -> Table:
"""
Get metadata about a specific poll for a given meeting ID
Required scopes: `meeting:read`
`Args`:
meeting_id: int
Unique identifier for Zoom meeting
poll_id: int
Unique identifier for poll
`Returns`:
Parsons Table of all polling responses
"""

endpoint = f"meetings/{meeting_id}/polls/{poll_id}"
tbl = self._get_request(endpoint=endpoint, data_key="questions")

if type(tbl) == dict:
logger.debug(f"No poll data returned for poll ID {poll_id}")
return Table(tbl)

logger.info(
f"Retrieved {tbl.num_rows} rows of metadata [meeting={meeting_id} poll={poll_id}]"
)

return self.__handle_nested_json(table=tbl, column="prompts")

def get_meeting_all_polls_metadata(self, meeting_id) -> Table:
"""
Get metadata for all polls for a given meeting ID
Required scopes: `meeting:read`
`Args`:
meeting_id: int
Unique identifier for Zoom meeting
`Returns`:
Parsons Table of all polling responses
"""

endpoint = f"meetings/{meeting_id}/polls"
tbl = self._get_request(endpoint=endpoint, data_key="polls")

if type(tbl) == dict:
logger.debug(f"No poll data returned for meeting ID {meeting_id}")
return Table(tbl)

logger.info(f"Retrieved {tbl.num_rows} polls for meeting ID {meeting_id}")

return self.__handle_nested_json(table=tbl, column="questions")

def get_past_meeting_poll_metadata(self, meeting_id) -> Table:
"""
List poll metadata of a past meeting.
Required scopes: `meeting:read`
`Args`:
meeting_id: int
The meeting's ID or universally unique ID (UUID).
`Returns`:
Parsons Table of poll results
"""

endpoint = f"past_meetings/{meeting_id}/polls"
tbl = self._get_request(endpoint=endpoint, data_key="questions")

if type(tbl) == dict:
logger.debug(f"No poll data returned for meeting ID {meeting_id}")
return Table(tbl)

logger.info(f"Retrieved {tbl.num_rows} polls for meeting ID {meeting_id}")

return self.__handle_nested_json(table=tbl, column="prompts")

def get_webinar_poll_metadata(self, webinar_id, poll_id) -> Table:
"""
Get metadata for a specific poll for a given webinar ID
Required scopes: `webinar:read`
`Args`:
webinar_id: str
Unique identifier for Zoom webinar
poll_id: int
Unique identifier for poll
`Returns`:
Parsons Table of all polling responses
"""

endpoint = f"webinars/{webinar_id}/polls/{poll_id}"
tbl = self._get_request(endpoint=endpoint, data_key="questions")

if type(tbl) == dict:
logger.debug(f"No poll data returned for poll ID {poll_id}")
return Table(tbl)

logger.info(
f"Retrieved {tbl.num_rows} rows of metadata [meeting={webinar_id} poll={poll_id}]"
)

return self.__handle_nested_json(table=tbl, column="prompts")

def get_webinar_all_polls_metadata(self, webinar_id) -> Table:
"""
Get metadata for all polls for a given webinar ID
Required scopes: `webinar:read`
`Args`:
webinar_id: str
Unique identifier for Zoom webinar
`Returns`:
Parsons Table of all polling responses
"""

endpoint = f"webinars/{webinar_id}/polls"
tbl = self._get_request(endpoint=endpoint, data_key="polls")

if type(tbl) == dict:
logger.debug(f"No poll data returned for webinar ID {webinar_id}")
return Table(tbl)

logger.info(f"Retrieved {tbl.num_rows} polls for meeting ID {webinar_id}")

return self.__handle_nested_json(table=tbl, column="questions")

def get_past_webinar_poll_metadata(self, webinar_id) -> Table:
"""
Retrieves the metadata for Webinar Polls of a specific Webinar
Required scopes: `webinar:read`
`Args`:
webinar_id: str
The webinar's ID or universally unique ID (UUID).
`Returns`:
Parsons Table of all polling responses
"""

endpoint = f"past_webinars/{webinar_id}/polls"
tbl = self._get_request(endpoint=endpoint, data_key="questions")

if type(tbl) == dict:
logger.debug(f"No poll data returned for webinar ID {webinar_id}")
return Table(tbl)

logger.info(f"Retrieved {tbl.num_rows} polls for meeting ID {webinar_id}")

return self.__handle_nested_json(table=tbl, column="prompts")

def get_meeting_poll_results(self, meeting_id) -> Table:
"""
Get a report of poll results for a past meeting
Required scopes: `report:read:admin`
"""

endpoint = f"report/meetings/{meeting_id}/polls"
tbl = self._get_request(endpoint=endpoint, data_key="questions")

if type(tbl) == dict:
logger.debug(f"No poll data returned for meeting ID {meeting_id}")
return Table(tbl)

logger.info(f"Retrieved {tbl.num_rows} reults for meeting ID {meeting_id}")

return self.__process_poll_results(tbl=tbl)

def get_webinar_poll_results(self, webinar_id) -> Table:
"""
Get a report of poll results for a past webinar
Required scopes: `report:read:admin`
"""

endpoint = f"report/webinars/{webinar_id}/polls"
tbl = self._get_request(endpoint=endpoint, data_key="questions")

if type(tbl) == dict:
logger.debug(f"No poll data returned for webinar ID {webinar_id}")
return Table(tbl)

logger.info(f"Retrieved {tbl.num_rows} reults for webinar ID {webinar_id}")

return self.__process_poll_results(tbl=tbl)
Loading

0 comments on commit 7ac73e2

Please sign in to comment.