diff --git a/docs/airmeet.rst b/docs/airmeet.rst
new file mode 100644
index 0000000000..627c2fdb76
--- /dev/null
+++ b/docs/airmeet.rst
@@ -0,0 +1,63 @@
+Airmeet
+=======
+
+********
+Overview
+********
+
+`Airmeet `_ is a webinar platform. This connector supports
+fetching events ("Airmeets"), sessions, participants, and other event data via the
+`Airmeet Public API for Event Details `_.
+
+.. note::
+ Authentication
+ You must create an Access Key and Secret Key via the Airmeet website. These are used by the ``Airmeet`` class to fetch
+ an access token which is used for subsequent interactions with the API. There are three region-based API endpoints; see
+ the `Airmeet API documentation `_ for details.
+
+***********
+Quick Start
+***********
+
+To instantiate the ``Airmeet`` class, you can either store your API endpoint, access key, and secret key as environmental
+variables (``AIRMEET_URI``, ``AIRMEET_ACCESS_KEY``, ``AIRMEET_SECRET_KEY``) or pass them in as arguments.
+
+.. code-block:: python
+
+ from parsons import Airmeet
+
+ # First approach: Use API credentials via environmental variables
+ airmeet = Airmeet()
+
+ # Second approach: Pass API credentials as arguments (airmeet_uri is optional)
+ airmeet = Airmeet(
+ airmeet_uri='https://api-gateway.airmeet.com/prod',
+ airmeet_access_key="my_access_key",
+ airmeet_secret_key="my_secret_key
+ )
+
+You can then call various endpoints:
+
+.. code-block:: python
+
+ # Fetch the list of Airmeets.
+ events_tbl = airmeet.list_airmeets()
+
+ # Fetch the list of sessions in an Airmeet.
+ sessions_tbl = airmeet.fetch_airmeet_sessions("my_airmeet_id")
+
+ # Fetch the list of registrations for an Airmeet, sorted in order
+ # of registration date.
+ participants_tbl = airmeet.fetch_airmeet_participants(
+ "my_airmeet_id", sorting_direction="ASC"
+ )
+
+ # Fetch the list of session attendees.
+ session_attendees_tbl = airmeet.fetch_session_attendance("my_session_id")
+
+***
+API
+***
+
+.. autoclass :: parsons.Airmeet
+ :inherited-members:
diff --git a/docs/index.rst b/docs/index.rst
index bb057fbca3..2df26096cb 100755
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -182,6 +182,7 @@ Indices and tables
action_kit
action_builder
action_network
+ airmeet
airtable
alchemer
auth0
diff --git a/parsons/__init__.py b/parsons/__init__.py
index ebc743e37f..6432ac0224 100644
--- a/parsons/__init__.py
+++ b/parsons/__init__.py
@@ -31,6 +31,7 @@
("parsons.action_kit.action_kit", "ActionKit"),
("parsons.action_builder.action_builder", "ActionBuilder"),
("parsons.action_network.action_network", "ActionNetwork"),
+ ("parsons.airmeet.airmeet", "Airmeet"),
("parsons.airtable.airtable", "Airtable"),
("parsons.alchemer.alchemer", "Alchemer"),
("parsons.alchemer.alchemer", "SurveyGizmo"),
diff --git a/parsons/airmeet/__init__.py b/parsons/airmeet/__init__.py
new file mode 100644
index 0000000000..0bdbec4253
--- /dev/null
+++ b/parsons/airmeet/__init__.py
@@ -0,0 +1,3 @@
+from parsons.airmeet.airmeet import Airmeet
+
+__all__ = ["Airmeet"]
diff --git a/parsons/airmeet/airmeet.py b/parsons/airmeet/airmeet.py
new file mode 100644
index 0000000000..37791cb5f0
--- /dev/null
+++ b/parsons/airmeet/airmeet.py
@@ -0,0 +1,424 @@
+from parsons.utilities import check_env
+from parsons.etl.table import Table
+from parsons.utilities.api_connector import APIConnector
+
+AIRMEET_DEFAULT_URI = "https://api-gateway.airmeet.com/prod/"
+
+
+class Airmeet(object):
+ """
+ Instantiate class.
+
+ `Args:`
+ airmeet_uri: string
+ The URI of the Airmeet API endpoint. Not required. The default
+ is https://api-gateway.airmeet.com/prod/. You can set an
+ ``AIRMEET_URI`` env variable or use this parameter when
+ instantiating the class.
+ airmeet_access_key: string
+ The Airmeet API access key.
+ airmeet_secret_key: string
+ The Airmeet API secret key.
+
+ For instructions on how to generate an access key and secret key set,
+ see `Airmeet's Event Details API documentation
+ `_.
+ """
+
+ def __init__(self, airmeet_uri=None, airmeet_access_key=None, airmeet_secret_key=None):
+ """
+ Authenticate with the Airmeet API and update the connection headers
+ with the access token.
+
+ `Args:`
+ airmeet_uri: string
+ The Airmeet API endpoint.
+ airmeet_access_key: string
+ The Airmeet API access key.
+ airmeet_secret_key: string
+ The Airmeet API secret key.
+ `Returns:`
+ ``None``
+ """
+ self.uri = check_env.check("AIRMEET_URI", airmeet_uri, optional=True) or AIRMEET_DEFAULT_URI
+ self.client = APIConnector(self.uri)
+ self.airmeet_client_key = check_env.check("AIRMEET_ACCESS_KEY", airmeet_access_key)
+ self.airmeet_client_secret = check_env.check("AIRMEET_SECRET_KEY", airmeet_secret_key)
+ self.client.headers = {
+ "X-Airmeet-Access-Key": self.airmeet_client_key,
+ "X-Airmeet-Secret-Key": self.airmeet_client_secret,
+ }
+ response = self.client.post_request(url="auth", success_codes=[200])
+ self.token = response["token"]
+
+ # API calls expect the token in the header.
+ self.client.headers = {
+ "Content-Type": "application/json",
+ "X-Airmeet-Access-Token": self.token,
+ }
+
+ def _get_all_pages(self, url, page_size=50, **kwargs) -> Table:
+ """
+ Get all the results from an Airmeet API url, handling pagination based
+ on the returned pageCount.
+
+ `Args:`
+ page_size: 50
+ The number of items to get per page. The max allowed varies by
+ API call. For details, see `Airmeet's Event Details API
+ documentation
+ `_.
+ **kwargs:
+ Additional parameters to include in the request.
+ `Returns:`
+ ``None``
+ """
+ results = []
+ cursor_after = "" # For getting the next set of results
+ kwargs["size"] = page_size
+
+ # Initial API call to get the first page of data
+ response = self.client.get_request(url=url, params=kwargs)
+
+ # Some APIs are asynchronous and will return a 202 if the request
+ # should be tried again after five minutes, because the results
+ # set needs to be built.
+ if "statusCode" in response and response["statusCode"] != 200:
+ raise Exception(response)
+ else:
+ results.extend(response["data"])
+
+ if "cursors" in response and response["cursors"]["pageCount"] > 1:
+ cursor_after = response["cursors"]["after"]
+
+ # Fetch subsequent pages if needed
+ for _ in range(2, response["cursors"]["pageCount"] + 1):
+ kwargs["after"] = cursor_after
+ response = self.client.get_request(url=url, params=kwargs)
+ results.extend(response["data"])
+ cursor_after = response["cursors"]["after"]
+
+ return Table(results)
+
+ def list_airmeets(self) -> Table:
+ """
+ Get the list of Airmeets. The API excludes any Airmeets that are
+ Archived (Deleted).
+
+ `Returns:`
+ Parsons.Table
+ List of Airmeets
+ """
+ return self._get_all_pages(url="airmeets", page_size=500)
+
+ def fetch_airmeet_participants(
+ self, airmeet_id, sorting_key="registrationDate", sorting_direction="DESC"
+ ) -> Table:
+ """
+ Get all participants (registrations) for a specific Airmeet, handling
+ pagination based on the returned totalUserCount. This API doesn't use
+ cursors for paging, so we can't use _get_all_pages() here.
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ sorting_key: string
+ The key to sort the participants by. Can be 'name', 'email', or
+ 'registrationDate' (the default).
+ sorting_direction: string
+ Can be either 'ASC' or 'DESC' (the default).
+ `Returns:`
+ Parsons.Table
+ List of participants for the Airmeet event
+ """
+ participants = [] # List to hold all participants
+ page_size = 1000 # Maximum number of results per page
+
+ # Initial API call to get the total user count and first page of data
+ response = self.client.get_request(
+ url=f"airmeet/{airmeet_id}/participants",
+ params={
+ "pageNumber": 1,
+ "resultSize": page_size,
+ "sortingKey": sorting_key,
+ "sortingDirection": sorting_direction,
+ },
+ )
+ participants.extend(response["participants"])
+
+ # Calculate total pages needed based on totalUserCount.
+ total_count = response["totalUserCount"]
+ total_pages = (total_count + page_size - 1) // page_size # This rounds up the division.
+
+ # Fetch subsequent pages if needed.
+ for page in range(2, total_pages + 1):
+ response = self.client.get_request(
+ url=f"airmeet/{airmeet_id}/participants",
+ params={
+ "pageNumber": page,
+ "resultSize": page_size,
+ "sortingKey": sorting_key,
+ "sortingDirection": sorting_direction,
+ },
+ )
+ participants.extend(response["participants"])
+
+ return Table(participants)
+
+ def fetch_airmeet_sessions(self, airmeet_id) -> Table:
+ """
+ Get the list of sessions for an Airmeet.
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of sessions for this Airmeet event
+ """
+ response = self.client.get_request(url=f"airmeet/{airmeet_id}/info")
+ return Table(response["sessions"])
+
+ def fetch_airmeet_info(self, airmeet_id, lists_to_tables=False):
+ """
+ Get the data for an Airmeet (event), which include the list of
+ sessions, session hosts/cohosts, and various other info.
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ lists_to_tables: bool
+ If True, will convert any dictionary values that are lists
+ to Tables.
+ `Returns:`
+ Dict containing the Airmeet data
+ """
+ response = self.client.get_request(url=f"airmeet/{airmeet_id}/info")
+ if lists_to_tables:
+ for k in response:
+ if isinstance(response[k], list):
+ response[k] = Table(response[k])
+ return response
+
+ def fetch_airmeet_custom_registration_fields(self, airmeet_id) -> Table:
+ """
+ Get the list of custom registration fields for an Airmeet.
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of custom registration fields for this Airmeet event
+ """
+ response = self.client.get_request(url=f"airmeet/{airmeet_id}/custom-fields")
+ return Table(response["customFields"])
+
+ def fetch_event_attendance(self, airmeet_id) -> Table:
+ """
+ Get all attendees for an Airmeet, handling pagination based on the
+ returned pageCount.
+
+ Results include attendance only from sessions with a status of
+ `FINISHED`. Maximum number of results per page = 50.
+
+ "This is an Asynchronous API. If you get a 202 code in response,
+ please try again after 5 minutes."
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of attendees for this Airmeet event
+ """
+ return self._get_all_pages(url=f"airmeet/{airmeet_id}/attendees", page_size=50)
+
+ def fetch_session_attendance(self, session_id) -> Table:
+ """
+ Get all attendees for a specific Airmeet session, handling pagination
+ based on the returned pageCount.
+
+ Results are available only for sessions with a status of `FINISHED`.
+ Maximum number of results per page = 50.
+
+ "This is an Asynchronous API. If you get a 202 code in response,
+ please try again after 5 minutes."
+
+ `Args:`
+ session_id: string
+ The id of the session.
+ `Returns:`
+ Parsons.Table
+ List of attendees for this session
+ """
+ return self._get_all_pages(url=f"session/{session_id}/attendees", page_size=50)
+
+ def fetch_airmeet_booths(self, airmeet_id) -> Table:
+ """
+ Get the list of booths for a specific Airmeet by ID.
+
+ `CAUTION: This method is untested. Booths are available only in
+ certain Airmeet plans.`
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of booths for this Airmeet
+ """
+ response = self.client.get_request(url=f"airmeet/{airmeet_id}/booths")
+ return Table(response["booths"] or [])
+
+ def fetch_booth_attendance(self, airmeet_id, booth_id) -> Table:
+ """
+ Get all attendees for a specific Airmeet booth, handling pagination
+ based on the returned pageCount.
+
+ Results are available only for events with a status of `FINISHED`.
+ Maximum number of results per page = 50.
+
+ "This is an Asynchronous API. If you get a 202 code in response,
+ please try again after 5 minutes."
+
+ `CAUTION: This method is untested. Booths are available only in
+ certain Airmeet plans.`
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ booth_id: string
+ The id of the booth.
+ `Returns:`
+ Parsons.Table
+ List of attendees for this booth
+ """
+ return self._get_all_pages(
+ url=f"airmeet/{airmeet_id}/booth/{booth_id}/booth-attendance", page_size=50
+ )
+
+ def fetch_poll_responses(self, airmeet_id) -> Table:
+ """
+ Get a list of the poll responses in an Airmeet, handling pagination
+ based on the returned pageCount.
+
+ Maximum number of results per page = 50.
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of users. For each user, the value for the "polls"
+ key is a list of poll questions and answers for that user.
+ """
+ return self._get_all_pages(url=f"airmeet/{airmeet_id}/polls", page_size=50)
+
+ def fetch_questions_asked(self, airmeet_id) -> Table:
+ """
+ Get a list of the questions asked in an Airmeet.
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of users. For each user, the value for the "questions"
+ key is a list of that user's questions.
+ """
+ response = self.client.get_request(url=f"airmeet/{airmeet_id}/questions")
+ return Table(response["data"])
+
+ def fetch_event_tracks(self, airmeet_id) -> Table:
+ """
+ Get a list of the tracks in a specific Airmeet by ID.
+
+ `CAUTION: This method is untested. Event tracks are available only in
+ certain Airmeet plans.`
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of event tracks
+ """
+ response = self.client.get_request(url=f"airmeet/{airmeet_id}/tracks")
+ return Table(response["tracks"])
+
+ def fetch_registration_utms(self, airmeet_id) -> Table:
+ """
+ Get all the UTM parameters captured during registration, handling
+ pagination based on the returned pageCount.
+
+ Maximum number of results per page = ?? (documentation doesn't say,
+ but assume 50 like the other asynchronous APIs).
+
+ "This is an Asynchronous API. If you get a 202 code in response,
+ please try again after 5 minutes."
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ `Returns:`
+ Parsons.Table
+ List of UTM parameters captured during registration
+ """
+ return self._get_all_pages(url=f"airmeet/{airmeet_id}/utms", page_size=50)
+
+ def download_session_recordings(self, airmeet_id, session_id=None) -> Table:
+ """
+ Get a list of recordings for a specific Airmeet (and optionally a
+ specific session in that Airmeet). The data for each recording
+ includes a download link which is valid for 6 hours.
+
+ The API returns "recordingsCount" and "totalCount", which implies
+ that the results could be paged like in fetch_airmeet_participants().
+ The API docs don't specify if that's the case, but this method will
+ need to be updated if it is.
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ session_id: string
+ (optional) If provided, limits results to only the recording
+ of the specified session.
+ `Returns:`
+ Parsons.Table
+ List of session recordings
+ """
+ kwargs = {}
+ if session_id:
+ kwargs["sessionIds"] = session_id
+ response = self.client.get_request(url=f"airmeet/{airmeet_id}/session-recordings", **kwargs)
+ return Table(response["recordings"])
+
+ def fetch_event_replay_attendance(self, airmeet_id, session_id=None) -> Table:
+ """
+ Get all replay attendees for a specific Airmeet (and optionally a
+ specific session in that Airmeet), handling pagination based on the
+ returned pageCount.
+
+ Results are available only for events with a status of `FINISHED`.
+ Maximum number of results per page = 50.
+
+ "This is an Asynchronous API. If you get a 202 code in response,
+ please try again after 5 minutes."
+
+ `Args:`
+ airmeet_id: string
+ The id of the Airmeet.
+ session_id: string
+ (optional) If provided, limits results to only attendees of
+ the specified session.
+ `Returns:`
+ Parsons.Table
+ List of event replay attendees
+ """
+ attendees = self._get_all_pages(
+ url=f"airmeet/{airmeet_id}/event-replay-attendees", page_size=50
+ )
+ if session_id is not None:
+ attendees = attendees.select_rows("{session_id} == '" + session_id + "'")
+ return attendees
diff --git a/test/test_airmeet.py b/test/test_airmeet.py
new file mode 100644
index 0000000000..13369bbe9e
--- /dev/null
+++ b/test/test_airmeet.py
@@ -0,0 +1,508 @@
+import os
+import pytest
+import requests_mock
+import unittest
+from unittest import mock
+from parsons import Airmeet, Table
+
+ENV_PARAMETERS = {
+ "AIRMEET_URI": "https://env_api_endpoint",
+ "AIRMEET_ACCESS_KEY": "env_access_key",
+ "AIRMEET_SECRET_KEY": "env_secret_key",
+}
+
+
+class TestAirmeet(unittest.TestCase):
+ @requests_mock.Mocker()
+ def setUp(self, m):
+ m.post("https://api-gateway.airmeet.com/prod/auth", json={"token": "test_token"})
+ self.airmeet = Airmeet(airmeet_access_key="fake_key", airmeet_secret_key="fake_secret")
+ self.airmeet.client = mock.MagicMock()
+
+ def tearDown(self):
+ pass
+
+ @requests_mock.Mocker()
+ @mock.patch.dict(os.environ, ENV_PARAMETERS)
+ def test_from_environ(self, m):
+ m.post("https://env_api_endpoint/auth", json={"token": "test_token"})
+ airmeet = Airmeet()
+ self.assertEqual(airmeet.uri, "https://env_api_endpoint")
+ self.assertEqual(airmeet.airmeet_client_key, "env_access_key")
+ self.assertEqual(airmeet.airmeet_client_secret, "env_secret_key")
+ self.assertEqual(airmeet.token, "test_token")
+
+ def test_get_all_pages_single_page(self):
+ # Simulate API response for a single page without further cursors.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [{"id": "1", "name": "Item 1"}],
+ "cursors": {"pageCount": 1, "after": None},
+ }
+ )
+
+ url = "airmeet/some_endpoint"
+ result = self.airmeet._get_all_pages(url)
+
+ self.airmeet.client.get_request.assert_called_once_with(url=url, params={"size": 50})
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "Table should contain exactly one record"
+
+ def test_get_all_pages_multiple_pages(self):
+ # Simulate API responses for multiple pages.
+ responses = [
+ {
+ "data": [{"id": "1", "name": "Item 1"}],
+ "cursors": {"pageCount": 2, "after": "abc123"},
+ },
+ {
+ "data": [{"id": "2", "name": "Item 2"}],
+ "cursors": {"pageCount": 2, "after": None},
+ }, # Last page
+ ]
+ self.airmeet.client.get_request = mock.MagicMock(
+ side_effect=lambda *args, **kwargs: responses.pop(0)
+ )
+
+ url = "airmeet/some_endpoint"
+ result = self.airmeet._get_all_pages(url)
+
+ calls = [
+ mock.call(url=url, params={"size": 50, "after": "abc123"}),
+ mock.call(url=url, params={"size": 50, "after": "abc123"}),
+ ]
+ self.airmeet.client.get_request.assert_has_calls(calls, any_order=True)
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 2, "Table should contain records from both pages"
+
+ def test_list_airmeets(self):
+ # Test get the list of Airmeets.
+ self.airmeet.client = mock.MagicMock()
+
+ result = self.airmeet.list_airmeets()
+
+ self.airmeet.client.get_request.assert_called_with(
+ url="airmeets",
+ params={"size": 500},
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+
+ def test_fetch_airmeet_participants_single_page(self):
+ # Simulate API response for a single page of participants. This
+ # particular API doesn't use cursors like the other ones that can have
+ # multiple pages, which is why this is a separate test.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "participants": [{"user_id": "abc123", "name": "Test User 1"}],
+ "userCount": 1,
+ "totalUserCount": 1,
+ }
+ )
+
+ result = self.airmeet.fetch_airmeet_participants("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/participants",
+ params={
+ "pageNumber": 1,
+ "resultSize": 1000,
+ "sortingKey": "registrationDate",
+ "sortingDirection": "DESC",
+ },
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "Table should contain exactly one record"
+
+ def test_fetch_airmeet_participants_multiple_pages(self):
+ # Simulate API responses for multiple pages of participants. This
+ # particular API doesn't use cursors like the other ones that can have
+ # multiple pages, which is why this is a separate test.
+
+ # The connector requests 1000 at a time, so we return a totalUserCount
+ # of 2000 here to make it request a second page.
+ responses = [
+ {
+ "participants": [{"user_id": "abc123", "name": "Test User 1"}],
+ "userCount": 1,
+ "totalUserCount": 2000,
+ },
+ {
+ "participants": [{"user_id": "def456", "name": "Test User 1"}],
+ "userCount": 1,
+ "totalUserCount": 2000,
+ }, # Last page
+ ]
+ self.airmeet.client.get_request = mock.MagicMock(
+ side_effect=lambda *args, **kwargs: responses.pop(0)
+ )
+
+ result = self.airmeet.fetch_airmeet_participants("test_airmeet_id")
+
+ calls = [
+ mock.call(
+ url="airmeet/test_airmeet_id/participants",
+ params={
+ "pageNumber": 1,
+ "resultSize": 1000,
+ "sortingKey": "registrationDate",
+ "sortingDirection": "DESC",
+ },
+ ),
+ mock.call(
+ url="airmeet/test_airmeet_id/participants",
+ params={
+ "pageNumber": 2,
+ "resultSize": 1000,
+ "sortingKey": "registrationDate",
+ "sortingDirection": "DESC",
+ },
+ ),
+ ]
+ self.airmeet.client.get_request.assert_has_calls(calls, any_order=True)
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 2, "Table should contain records from both pages"
+
+ def test_fetch_airmeet_sessions(self):
+ # Test get the list of sessions for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "name": "Test Event",
+ "sessions": [
+ {"sessionid": "test_session_id_1", "name": "Test Session 1"},
+ {"sessionid": "test_session_id_2", "name": "Test Session 2"},
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_airmeet_sessions("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(url="airmeet/test_airmeet_id/info")
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 2, "Table should contain both records"
+
+ def test_fetch_airmeet_info(self):
+ # Test get the Airmeet info.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "name": "Test Event",
+ "sessions": [
+ {"sessionid": "test_session_id_1", "name": "Test Session 1"},
+ {"sessionid": "test_session_id_2", "name": "Test Session 2"},
+ ],
+ "session_hosts": [{"id": "abc123", "name": "Test Host 1"}],
+ }
+ )
+
+ result = self.airmeet.fetch_airmeet_info("test_airmeet_id", lists_to_tables=True)
+
+ self.airmeet.client.get_request.assert_called_once_with(url="airmeet/test_airmeet_id/info")
+ assert isinstance(result, dict), "The result should be a Table"
+ assert isinstance(result["sessions"], Table), "The sessions should be a Table"
+ assert isinstance(result["session_hosts"], Table), "The session hosts should be a Table"
+ assert len(result["sessions"]) == 2, "Sessions Table should contain exactly two records"
+ assert (
+ len(result["session_hosts"]) == 1
+ ), "Session hosts Table should contain exactly one record"
+
+ def test_fetch_airmeet_custom_registration_fields(self):
+ # Test get the custom registration fields for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "customFields": [
+ {"fieldId": "test_field_id_1", "label": "Test Label 1"},
+ {"fieldId": "test_field_id_2", "label": "Test Label 2"},
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_airmeet_custom_registration_fields("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/custom-fields"
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 2, "Table should contain exactly two records"
+
+ def test_fetch_event_attendance(self):
+ # Test get the attendees for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [
+ {
+ "name": "Test User 1",
+ "user_id": "abc123",
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_event_attendance("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/attendees",
+ params={"size": 50},
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "The result should contain exactly one record"
+
+ def test_fetch_session_attendance(self):
+ # Test get the attendees for a session.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [
+ {
+ "name": "Test User 1",
+ "user_id": "abc123",
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_session_attendance("test_session_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="session/test_session_id/attendees",
+ params={"size": 50},
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "The result should contain exactly one record"
+
+ def test_fetch_session_attendance_exception_202(self):
+ # Test that an asynchronous API raises an exception if it returns
+ # a statusCode == 202.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "statusCode": 202,
+ "statusMessage": "Preparing your results. Try after 5 minutes"
+ + "to get the updated results",
+ }
+ )
+
+ with pytest.raises(Exception):
+ self.airmeet.fetch_session_attendance("test_session_id")
+
+ def test_fetch_session_attendance_exception_400(self):
+ # Test that the sessions attendees API raises an exception if it
+ # returns a statusCode == 400.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": {},
+ "statusCode": 400,
+ "statusMessage": "Session status is not valid",
+ }
+ )
+
+ with pytest.raises(Exception):
+ self.airmeet.fetch_session_attendance("test_session_id")
+
+ def test_fetch_airmeet_booths(self):
+ # Test get the booths for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "booths": [
+ {
+ "uid": "test_booth_uid_1",
+ "name": "Test Booth 1",
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_airmeet_booths("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/booths"
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "The result should contain exactly one record"
+
+ def test_fetch_booth_attendance(self):
+ # Test get the attendees for a booth.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [
+ {
+ "name": "Test User 1",
+ "user_id": "abc123",
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_booth_attendance(
+ "test_airmeet_id",
+ booth_id="test_booth_id",
+ )
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/booth/test_booth_id/booth-attendance",
+ params={"size": 50},
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "The result should contain exactly one record"
+
+ def test_fetch_poll_responses(self):
+ # Test get the poll responses for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [
+ {
+ "name": "Test User 1",
+ "polls": [
+ {"question": "Poll Question 1", "answer": "Poll Answer 1"},
+ {"question": "Poll Question 2", "answer": "Poll Answer 2"},
+ ],
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_poll_responses("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/polls",
+ params={"size": 50},
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result[0]["polls"]) == 2, "The record should contain exactly two poll responses"
+
+ def test_fetch_questions_asked(self):
+ # Test get the questions asked for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [
+ {
+ "name": "Test User 1",
+ "questions": [
+ {"question": "Question 1", "session_id": "session_id_1"},
+ {"question": "Question 2", "session_id": "session_id_2"},
+ ],
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_questions_asked("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/questions"
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result[0]["questions"]) == 2, "The record should contain exactly two questions"
+
+ def test_fetch_event_tracks(self):
+ # Test get the tracks for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "tracks": [
+ {
+ "uid": "test_track_uid_1",
+ "name": "Test Track 1",
+ "sessions": [
+ "session_id_1",
+ "session_id_2",
+ ],
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_event_tracks("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/tracks"
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result[0]["sessions"]) == 2, "The record should contain exactly two session ids"
+
+ def test_fetch_registration_utms(self):
+ # Test get the registration UTMs for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [
+ {
+ "airmeetId": "test_airmeet_id",
+ "email": "test@example.com",
+ "id": 1,
+ "utms": {
+ "utm_campaign": "test_utm_campaign",
+ "utm_medium": None,
+ "utm_source": "test_utm_source",
+ },
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_registration_utms("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/utms",
+ params={"size": 50},
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "The result should contain exactly one record"
+ assert len(result[0]["utms"]) == 3, "The record should contain exactly three UTMs"
+
+ def test_download_session_recordings(self):
+ # Test get the session recordings for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "recordings": [
+ {
+ "session_id": "test_session_id_1",
+ "session_name": "Test Session 1",
+ "download_link": "https://example.com/test_download_link",
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.download_session_recordings(
+ "test_airmeet_id",
+ session_id="test_session_id",
+ )
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/session-recordings",
+ sessionIds="test_session_id",
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "The result should contain exactly one record"
+
+ def test_fetch_event_replay_attendance(self):
+ # Test get the replay attendees for an Airmeet.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": [
+ {
+ "id": 1,
+ "name": "Test User 1",
+ "session_id": "test_session_id",
+ }
+ ],
+ }
+ )
+
+ result = self.airmeet.fetch_event_replay_attendance("test_airmeet_id")
+
+ self.airmeet.client.get_request.assert_called_once_with(
+ url="airmeet/test_airmeet_id/event-replay-attendees",
+ params={"size": 50},
+ )
+ assert isinstance(result, Table), "The result should be a Table"
+ assert len(result) == 1, "The result should contain exactly one record"
+
+ def test_fetch_event_replay_attendance_exception_400(self):
+ # Test that the replay attendees API raises an exception if it returns
+ # a statusCode == 400.
+ self.airmeet.client.get_request = mock.MagicMock(
+ return_value={
+ "data": {},
+ "statusCode": 400,
+ "statusMessage": "Airmeet status is not valid",
+ }
+ )
+
+ with pytest.raises(Exception):
+ self.airmeet.fetch_event_replay_attendance("test_airmeet_id")