Creating Meeting OAuth in Python 3.7

Description
I am trying to create a meeting with OAuth with python 3.7. The below written code works perfectly for getting the meetings lists. Since the recent update, this code does not work anymore. However I am puzzled how to adopt the code and the documentation did not help very much.

Error
{‘code’: 300, ‘message’: ‘Request Body should be a valid JSON object.’}

Which App Type (OAuth / Chatbot / JWT / Webhook)?
OAuth

Which Endpoint/s?
API endpoint: https://api.zoom.us/v2

How To Reproduce (If applicable)

The code is used is divided into:

  1. Utility classes and functions
  2. Zoom.us REST API Python Client
  3. API Call to create meeting

This is the code:

#1. Utility classes and functions
“”“Utility classes and functions”""

from future import absolute_import, unicode_literals

from urllib.parse import quote

import contextlib
import json
import requests
import time
import jwt

API_VERSION_2 = 2

class ApiClient(object):
“”“Simple wrapper for REST API requests”""

def __init__(self, base_uri=None, timeout=15, **kwargs):
    """Setup a new API Client
    :param base_uri: The base URI to the API
    :param timeout: The timeout to use for requests
    :param kwargs: Any other attributes. These will be added as
                       attributes to the ApiClient object.
    """
    self.base_uri = base_uri
    self.timeout = timeout
    for k, v in kwargs.items():
        setattr(self, k, v)

@property
def timeout(self):
    """The timeout"""
    return self._timeout

@timeout.setter
def timeout(self, value):
    """The default timeout"""
    if value is not None:
        try:
            value = int(value)
        except ValueError:
            raise ValueError("timeout value must be an integer")
    self._timeout = value

@property
def base_uri(self):
    """The base_uri"""
    return self._base_uri

@base_uri.setter
def base_uri(self, value):
    """The default base_uri"""
    if value and value.endswith("/"):
        value = value[:-1]
    self._base_uri = value

def url_for(self, endpoint):
    """Get the URL for the given endpoint
    :param endpoint: The endpoint
    :return: The full URL for the endpoint
    """
    if not endpoint.startswith("/"):
        endpoint = "/{}".format(endpoint)
    if endpoint.endswith("/"):
        endpoint = endpoint[:-1]
    return self.base_uri + endpoint

def get_request(self, endpoint, params=None, headers=None):
    """Helper function for GET requests
    :param endpoint: The endpoint
    :param params: The URL parameters
    :param headers: request headers
    :return: The :class:``requests.Response`` object for this request
    """
    if headers is None and self.config.get("version") == API_VERSION_2:
        headers = {"Authorization": "Bearer {}".format(self.config.get("token"))}
    return requests.get(
        self.url_for(endpoint), params=params, headers=headers, timeout=self.timeout
    )

def post_request(
    self, endpoint, params=None, data=None, headers=None, cookies=None
):
    """Helper function for POST requests
    :param endpoint: The endpoint
    :param params: The URL parameters
    :param data: The data (either as a dict or dumped JSON string) to
                 include with the POST
    :param headers: request headers
    :param cookies: request cookies
    :return: The :class:``requests.Response`` object for this request
    """
    if data and not is_str_type(data):
        data = json.dumps(data)
    if headers is None and self.config.get("version") == API_VERSION_2:
        headers = {
            "Authorization": "Bearer {}".format(self.config.get("token")),
            "Content-Type": "application/json",
        }
    return requests.post(
        self.url_for(endpoint),
        params=params,
        data=data,
        headers=headers,
        cookies=cookies,
        timeout=self.timeout,
    )

def patch_request(
    self, endpoint, params=None, data=None, headers=None, cookies=None
):
    """Helper function for PATCH requests
    :param endpoint: The endpoint
    :param params: The URL parameters
    :param data: The data (either as a dict or dumped JSON string) to
                 include with the PATCH
    :param headers: request headers
    :param cookies: request cookies
    :return: The :class:``requests.Response`` object for this request
    """
    if data and not is_str_type(data):
        data = json.dumps(data)
    if headers is None and self.config.get("version") == API_VERSION_2:
        headers = {
            "Authorization": "Bearer {}".format(self.config.get("token")),
            "Content-Type": "application/json",
        }
    return requests.patch(
        self.url_for(endpoint),
        params=params,
        data=data,
        headers=headers,
        cookies=cookies,
        timeout=self.timeout,
    )

def delete_request(
    self, endpoint, params=None, data=None, headers=None, cookies=None
):
    """Helper function for DELETE requests
    :param endpoint: The endpoint
    :param params: The URL parameters
    :param data: The data (either as a dict or dumped JSON string) to
                 include with the DELETE
    :param headers: request headers
    :param cookies: request cookies
    :return: The :class:``requests.Response`` object for this request
    """
    if data and not is_str_type(data):
        data = json.dumps(data)
    if headers is None and self.config.get("version") == API_VERSION_2:
        headers = {"Authorization": "Bearer {}".format(self.config.get("token"))}
    return requests.delete(
        self.url_for(endpoint),
        params=params,
        data=data,
        headers=headers,
        cookies=cookies,
        timeout=self.timeout,
    )

def put_request(self, endpoint, params=None, data=None, headers=None, cookies=None):
    """Helper function for PUT requests
    :param endpoint: The endpoint
    :param params: The URL paramaters
    :param data: The data (either as a dict or dumped JSON string) to
                 include with the PUT
    :param headers: request headers
    :param cookies: request cookies
    :return: The :class:``requests.Response`` object for this request
    """
    if data and not is_str_type(data):
        data = json.dumps(data)
    if headers is None and self.config.get("version") == API_VERSION_2:
        headers = {"Authorization": "Bearer {}".format(self.config.get("token"))}
    return requests.put(
        self.url_for(endpoint),
        params=params,
        data=data,
        headers=headers,
        cookies=cookies,
        timeout=self.timeout,
    )

@contextlib.contextmanager
def ignored(*exceptions):
“”“Simple context manager to ignore expected Exceptions
:param *exceptions: The exceptions to safely ignore
“””
try:
yield
except exceptions:
pass

def is_str_type(val):
“”“Check whether the input is of a string type.
We use this method to ensure python 2-3 capatibility.
:param val: The value to check wither it is a string
:return: In python2 it will return True if :attr:val is either an
instance of str or unicode. In python3 it will return True if
it is an instance of str
“””
with ignored(NameError):
return isinstance(val, basestring)
return isinstance(val, str)

def require_keys(d, keys, allow_none=True):
“”“Require that the object have the given keys
:param d: The dict the check
:param keys: The keys to check :attr:obj for. This can either be a single
string, or an iterable of strings
:param allow_none: Whether None values are allowed
:raises:
:ValueError: If any of the keys are missing from the obj
“””
if is_str_type(keys):
keys = [keys]
for k in keys:
if k not in d:
raise ValueError("’{}’ must be set".format(k))
if not allow_none and d[k] is None:
raise ValueError("’{}’ cannot be None".format(k))
return True

def date_to_str(d):
“”“Convert date and datetime objects to a string
Note, this does not do any timezone conversion.
:param d: The :class:datetime.date or :class:datetime.datetime to
convert to a string
:returns: The string representation of the date
“””
return d.strftime("%Y-%m-%dT%H:%M:%SZ")

def generate_jwt(key, secret):
header = {“alg”: “HS256”, “typ”: “JWT”}

payload = {"iss": key, "exp": int(time.time() + 3600)}

token = jwt.encode(payload, secret, algorithm="HS256", headers=header)
return token.decode("utf-8")

def encode_uuid(val):
“”“Encode UUID as described by ZOOM API documentation
> Note: Please double encode your UUID when using this API if the UUID
> begins with a '/'or contains ‘//’ in it.
:param val: The UUID to encode
:returns: The encoded UUID
“””
if val[0] == “/” or “//” in val:
val = quote(quote(val, safe=""), safe="")
return val

print(“done”)

#2. Zoom.us REST API Python Client
“”“Zoom.us REST API Python Client”""

from future import absolute_import, unicode_literals

from zoomus import components, util
from zoomus.util import API_VERSION_2

API_BASE_URIS = {
API_VERSION_2: “https://api.zoom.us/v2”,
}

COMPONENT_CLASSES = {
API_VERSION_2: {
“user”: components.user.UserComponentV2,
“meeting”: components.meeting.MeetingComponentV2,
#“metric”: components.metric.MetricComponentV2,
#“past_meeting”: components.past_meeting.PastMeetingComponentV2,
“report”: components.report.ReportComponentV2,
“webinar”: components.webinar.WebinarComponentV2,
“recording”: components.recording.RecordingComponentV2,
#“phone”: components.phone.PhoneComponentV2,
},
}

class ZoomClient(util.ApiClient):
“”“Zoom.us REST API Python Client”""

"""Base URL for Zoom API"""

def __init__(
    self, api_key, api_secret, data_type="json", timeout=15, version=API_VERSION_2
):
    """Create a new Zoom client
    :param api_key: The Zooom.us API key
    :param api_secret: The Zoom.us API secret
    :param data_type: The expected return data type. Either 'json' or 'xml'
    :param timeout: The time out to use for API requests
    """
    try:
        BASE_URI = API_BASE_URIS[version]
        self.components = COMPONENT_CLASSES[version].copy()
    except KeyError:
        raise RuntimeError("API version not supported: %s" % version)

    super(ZoomClient, self).__init__(base_uri=BASE_URI, timeout=timeout)

    # Setup the config details
    self.config = {
        "api_key": api_key,
        "api_secret": api_secret,
        "data_type": data_type,
        "version": version,
        "token": util.generate_jwt(api_key, api_secret),
    }

    # Instantiate the components
    for key in self.components.keys():
        self.components[key] = self.components[key](
            base_uri=BASE_URI, config=self.config
        )

def __enter__(self):
    return self

def __exit__(self, exc_type, exc_val, exc_tb):
    return

def refresh_token(self):
    self.config["token"] = (
        util.generate_jwt(self.config["api_key"], self.config["api_secret"]),
    )

@property
def api_key(self):
    """The Zoom.us api_key"""
    return self.config.get("api_key")

@api_key.setter
def api_key(self, value):
    """Set the api_key"""
    self.config["api_key"] = value
    self.refresh_token()

@property
def api_secret(self):
    """The Zoom.us api_secret"""
    return self.config.get("api_secret")

@api_secret.setter
def api_secret(self, value):
    """Set the api_secret"""
    self.config["api_secret"] = value
    self.refresh_token()

@property
def meeting(self):
    """Get the meeting component"""
    return self.components.get("meeting")

@property
def metric(self):
    """Get the metric component"""
    return self.components.get("metric")

@property
def report(self):
    """Get the report component"""
    return self.components.get("report")

@property
def user(self):
    """Get the user component"""
    return self.components.get("user")

@property
def webinar(self):
    """Get the webinar component"""
    return self.components.get("webinar")

@property
def recording(self):
    """Get the recording component"""
    return self.components.get("recording")

@property
def phone(self):
    """Get the phone component"""
    return self.components.get("phone")

print(“done”)

#3. API Call to create meeting

import datetime
import json

client = ZoomClient(‘api_key’, ‘api_sec’)

user_list_response = client.user.list()
user_list = json.loads(user_list_response.content)

datetime_object = datetime.datetime.strptime(“2020-03-21 20:00:00+00:00”[:19], ‘%Y-%m-%d %H:%M:%S’)
datetime_object = datetime_object.strftime(’%Y-%m-%dT%H:%M:%SZ’)
datetime_object = datetime.datetime.strptime(datetime_object, ‘%Y-%m-%dT%H:%M:%SZ’)

for user in user_list[‘users’]:
user_id = user[‘id’]
#print(json.loads(client.meeting.list(user_id=user_id).content)) #THIS WORKS
print(json_file = json.loads(client.meeting.create(user_id=user_id, host_id=“host_id”, duration=60, timezone=“Europe/Vienna”, topic=“Sophie & Chris”, type=“2”, start_time=datetime_object).content))

Additional context
Can anybody please help with the current code or provide a code snippet for this problem? Thank you!

Hey @bartnik.christian,

It seems like your request body JSON is invalid. You can please send me your request body in JSON format so I can debug?

Thanks,
Tommy