Repository: ucfopen/canvasapi (language: python)

Issue:
Course Audit Log
**What resource needs additional coverage?**
Course Audit Log

**What endpoints need to be covered?**
- [ ] [Query by course.](https://canvas.instructure.com/doc/api/course_audit_log.html#method.course_audit_api.for_course)
- [ ] [Query by account.](https://canvas.instructure.com/doc/api/course_audit_log.html#method.course_audit_api.for_account)



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2de3e736..360f696d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### General
+
+- Added support for Course Events; new Course Event class. (Thanks, [dmols](https://github.com/dmols))
+
 ### New Endpoint Coverage
 
 - Account Calendars (Thanks, [@dmols](https://github.com/dmols))
@@ -10,6 +14,9 @@
     - Update a calendar's visibility
     - Update many calendars' visibility
     - List all account calendars
+- Query Course Events (Thanks, [@dmols](https://github.com/dmols))
+    - Query by course
+    - Query by account
 - Moderation Grading (Moderation Set)
     - List students selected for moderation
     - Select students for moderation
diff --git a/canvasapi/account.py b/canvasapi/account.py
index e9897dab..d7f71d62 100644
--- a/canvasapi/account.py
+++ b/canvasapi/account.py
@@ -4,6 +4,7 @@
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.course import Course
+from canvasapi.course_event import CourseEvent
 from canvasapi.enrollment import Enrollment
 from canvasapi.enrollment_term import EnrollmentTerm
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
@@ -1688,6 +1689,24 @@ def import_outcome(self, attachment, **kwargs):
             if is_path:
                 attachment.close()
 
+    def query_audit_by_account(self, **kwargs):
+        """
+        List course change events for a specific account.
+
+        :calls: `GET /api/v1/audit/course/accounts/:account_id \
+        <https://canvas.instructure.com/doc/api/course_audit_log.html#method.course_audit_api.for_account>`_
+
+        :rtype: list of :class:`canvasapi.course_event.CourseEvent`
+        """
+
+        return PaginatedList(
+            CourseEvent,
+            self._requester,
+            "GET",
+            "audit/course/accounts/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def show_account_auth_settings(self, **kwargs):
         """
         Return the current state of each account level setting
diff --git a/canvasapi/communication_channel.py b/canvasapi/communication_channel.py
index 91975374..acc86105 100644
--- a/canvasapi/communication_channel.py
+++ b/canvasapi/communication_channel.py
@@ -109,7 +109,6 @@ def update_multiple_preferences(self, notification_preferences, **kwargs):
         :rtype: :class:`canvasapi.notification_preference.NotificationPreference`
         """
         if isinstance(notification_preferences, dict) and notification_preferences:
-
             for key, value in notification_preferences.items():
                 try:
                     if not value["frequency"]:
diff --git a/canvasapi/course.py b/canvasapi/course.py
index 733e7b6d..e57a88f8 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -6,6 +6,7 @@
 from canvasapi.collaboration import Collaboration
 from canvasapi.content_export import ContentExport
 from canvasapi.course_epub_export import CourseEpubExport
+from canvasapi.course_event import CourseEvent
 from canvasapi.custom_gradebook_columns import CustomGradebookColumn
 from canvasapi.discussion_topic import DiscussionTopic
 from canvasapi.exceptions import RequiredFieldMissing
@@ -2370,6 +2371,24 @@ def preview_html(self, html, **kwargs):
         )
         return response.json().get("html", "")
 
+    def query_audit_by_course(self, **kwargs):
+        """
+        Lists course change events for a specific course.
+
+        :calls: `GET /api/v1/audit/course/courses/:course_id \
+        <https://canvas.instructure.com/doc/api/course_audit_log.html#method.course_audit_api.for_course>`_
+
+        :rtype: list of :class:`canvasapi.course_event.CourseEvent`
+        """
+
+        return PaginatedList(
+            CourseEvent,
+            self._requester,
+            "GET",
+            "audit/course/courses/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def remove_usage_rights(self, **kwargs):
         """
         Removes the usage rights for specified files that are under the current course scope
diff --git a/canvasapi/course_event.py b/canvasapi/course_event.py
new file mode 100644
index 00000000..344cbd1e
--- /dev/null
+++ b/canvasapi/course_event.py
@@ -0,0 +1,6 @@
+from canvasapi.canvas_object import CanvasObject
+
+
+class CourseEvent(CanvasObject):
+    def __str__(self):
+        return "{} {} ({})".format(self.name, self.start_at, self.conclude_at)
diff --git a/canvasapi/paginated_list.py b/canvasapi/paginated_list.py
index 9be9fc3e..ce14420d 100644
--- a/canvasapi/paginated_list.py
+++ b/canvasapi/paginated_list.py
@@ -27,7 +27,6 @@ def __init__(
         _root=None,
         **kwargs
     ):
-
         self._elements = list()
 
         self._requester = requester
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index 765ac5f9..f5ce6cd0 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -22,6 +22,7 @@ Class Reference
     conversation-ref
     course-ref
     course-epub-export-ref
+    course-event-ref
     current-user-ref
     custom-gradebook-columns-ref
     discussion-entry-ref
diff --git a/docs/conf.py b/docs/conf.py
index 9d54463c..a05eaed1 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -18,7 +18,7 @@
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath('../'))
+sys.path.insert(0, os.path.abspath("../"))
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -28,37 +28,34 @@
 
 # -- General configuration ------------------------------------------------
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
 
 # Include class __init__ in autodoc
-autoclass_content = 'both'
+autoclass_content = "both"
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
-extensions = [
-    'sphinx.ext.autodoc',
-    'versionwarning.extension'
-]
+extensions = ["sphinx.ext.autodoc", "versionwarning.extension"]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 # source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'canvasapi'
-copyright = u'2017, University of Central Florida (Center for Distributed Learning)'
-author = u'University of Central Florida (Center for Distributed Learning)'
+project = "canvasapi"
+copyright = "2017, University of Central Florida (Center for Distributed Learning)"
+author = "University of Central Florida (Center for Distributed Learning)"
 
 
 # The short X.Y version.
@@ -68,7 +65,7 @@
 
 # sphinx-version-warning configuration
 versionwarning_messages = {
-    'develop': 'You are viewing a development build of the canvasapi docs. <a href="/en/stable">Visit the stable release docs here.</a>',
+    "develop": 'You are viewing a development build of the canvasapi docs. <a href="/en/stable">Visit the stable release docs here.</a>',
 }
 versionwarning_body_selector = "ul.wy-breadcrumbs"
 
@@ -81,38 +78,38 @@
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 # The reST default role (used for this markup: `text`) to use for all
 # documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # If true, keep warnings as "system message" paragraphs in the built documents.
-#keep_warnings = False
+# keep_warnings = False
 
 # If true, `todo` and `todoList` produce output, else they produce nothing.
 todo_include_todos = False
@@ -122,31 +119,31 @@
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'sphinx_rtd_theme'
+html_theme = "sphinx_rtd_theme"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.
 # "<project> v<release> documentation" by default.
-#html_title = u'canvasapi v1.0'
+# html_title = u'canvasapi v1.0'
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (relative to this directory) to use as a favicon of
 # the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -156,124 +153,123 @@
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
 # directly to the root of the documentation.
-#html_extra_path = []
+# html_extra_path = []
 
 # If not None, a 'Last updated on:' timestamp is inserted at every page
 # bottom, using the given strftime format.
 # The empty string is equivalent to '%b %d, %Y'.
-#html_last_updated_fmt = None
+# html_last_updated_fmt = None
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Language to be used for generating the HTML full-text search index.
 # Sphinx supports the following languages:
 #   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
 #   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
-#html_search_language = 'en'
+# html_search_language = 'en'
 
 # A dictionary with options for the search language support, empty by default.
 # 'ja' uses this config value.
 # 'zh' user can custom change `jieba` dictionary path.
-#html_search_options = {'type': 'default'}
+# html_search_options = {'type': 'default'}
 
 # The name of a javascript file (relative to the configuration directory) that
 # implements a search results scorer. If empty, the default will be used.
-#html_search_scorer = 'scorer.js'
+# html_search_scorer = 'scorer.js'
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'canvasapidoc'
+htmlhelp_basename = "canvasapidoc"
 
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-
-# Latex figure (float) alignment
-#'figure_align': 'htbp',
+    # The paper size ('letterpaper' or 'a4paper').
+    #'papersize': 'letterpaper',
+    # The font size ('10pt', '11pt' or '12pt').
+    #'pointsize': '10pt',
+    # Additional stuff for the LaTeX preamble.
+    #'preamble': '',
+    # Latex figure (float) alignment
+    #'figure_align': 'htbp',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, 'canvasapi.tex', u'canvasapi Documentation',
-     u'University of Central Florida (Center for Distributed Learning)', 'manual'),
+    (
+        master_doc,
+        "canvasapi.tex",
+        "canvasapi Documentation",
+        "University of Central Florida (Center for Distributed Learning)",
+        "manual",
+    ),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 
 # -- Options for manual page output ---------------------------------------
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, 'canvasapi', u'canvasapi Documentation',
-     [author], 1)
-]
+man_pages = [(master_doc, "canvasapi", "canvasapi Documentation", [author], 1)]
 
 # If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
 
 
 # -- Options for Texinfo output -------------------------------------------
@@ -282,19 +278,25 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'canvasapi', u'canvasapi Documentation',
-     author, 'canvasapi', 'One line description of project.',
-     'Miscellaneous'),
+    (
+        master_doc,
+        "canvasapi",
+        "canvasapi Documentation",
+        author,
+        "canvasapi",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 # Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
 
 # If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
 
 # How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
 
 # If true, do not generate a @detailmenu in the "Top" node's menu.
-#texinfo_no_detailmenu = False
+# texinfo_no_detailmenu = False
diff --git a/docs/course-event-ref.rst b/docs/course-event-ref.rst
new file mode 100644
index 00000000..750fd273
--- /dev/null
+++ b/docs/course-event-ref.rst
@@ -0,0 +1,6 @@
+===========
+CourseEvent
+===========
+
+.. autoclass:: canvasapi.course_event.CourseEvent
+    :members:
diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py
index 4cf4b2eb..9f223ada 100644
--- a/scripts/validate_docstrings.py
+++ b/scripts/validate_docstrings.py
@@ -5,7 +5,7 @@
 
 import requests
 
-sys.path.append(os.path.join(sys.path[0], '..'))
+sys.path.append(os.path.join(sys.path[0], ".."))
 
 import canvasapi  # noqa
 
@@ -16,21 +16,20 @@ def validate_method(themethod, quiet=False):
         return True
 
     # Docstrings without API calls can't be checked this way
-    if not re.search(r':calls:', inspect.getdoc(themethod)):
+    if not re.search(r":calls:", inspect.getdoc(themethod)):
         return True
-    if not re.search(r'<\S*>', inspect.getdoc(themethod)):
+    if not re.search(r"<\S*>", inspect.getdoc(themethod)):
         return True
 
     method_string = inspect.getfile(themethod) + " " + themethod.__name__
     call_lines = re.findall(
-        '`(POST|GET|PUT|PATCH|DELETE)([^<]*)<([^>]*)>`_',
-        inspect.getdoc(themethod)
+        "`(POST|GET|PUT|PATCH|DELETE)([^<]*)<([^>]*)>`_", inspect.getdoc(themethod)
     )
     if not call_lines:
         if not quiet:
             # Docstring exists, has a :calls: line, contains a URL, but could
             # not be parsed;
-            print('{} Failed to parse :calls: line.'.format(method_string))
+            print("{} Failed to parse :calls: line.".format(method_string))
         return False
     for call_line in call_lines:
         if not validate_docstring(method_string, call_line, quiet):
@@ -40,65 +39,62 @@ def validate_method(themethod, quiet=False):
 
 def validate_docstring(method_string, call_line, quiet):
     docstring_verb, api_URL, doc_URL = call_line
-    api_URL = ''.join(api_URL.split())
-    if api_URL[-1] == '/':
+    api_URL = "".join(api_URL.split())
+    if api_URL[-1] == "/":
         api_URL = api_URL[0:-1]
-    docfile_URL, endpoint_name = re.search('([^#]*)#?(.*)', doc_URL).groups()
+    docfile_URL, endpoint_name = re.search("([^#]*)#?(.*)", doc_URL).groups()
     html_doc_response = requests.get(docfile_URL)
     if html_doc_response.status_code != requests.codes.ok:
         if not quiet:
-            print('{} docstring URL request returned {}'.format(
-                method_string,
-                html_doc_response.status_code
-            ))
+            print(
+                "{} docstring URL request returned {}".format(
+                    method_string, html_doc_response.status_code
+                )
+            )
         return False
 
     endpoint_h2 = re.search(
-        r'<h2[^>]*name=[\'\"]{}[\'\"]'.format(endpoint_name),
-        html_doc_response.text
+        r"<h2[^>]*name=[\'\"]{}[\'\"]".format(endpoint_name), html_doc_response.text
     )
     if not endpoint_name:
         if not quiet:
-            print((
-                '{} docstring URL does not contain an endpoint name in link'
-                ' to API documentation'
-            ).format(method_string))
+            print(
+                (
+                    "{} docstring URL does not contain an endpoint name in link"
+                    " to API documentation"
+                ).format(method_string)
+            )
         return False
     if not endpoint_h2:
         if not quiet:
-            print('{} docstring refers to {} in {}, not found'.format(
-                method_string, endpoint_name, docfile_URL
-            ))
+            print(
+                "{} docstring refers to {} in {}, not found".format(
+                    method_string, endpoint_name, docfile_URL
+                )
+            )
         return False
 
-    endpoint_element_re = re.compile(
-        r'<h3 class=[\"\']endpoint[\"\']>[^<]*<\/h3>'
-    )
+    endpoint_element_re = re.compile(r"<h3 class=[\"\']endpoint[\"\']>[^<]*<\/h3>")
 
     endpoint_search_start_match = endpoint_element_re.search(
-        html_doc_response.text,
-        endpoint_h2.end()
+        html_doc_response.text, endpoint_h2.end()
     )
     if not endpoint_search_start_match:
         if not quiet:
-            print('Found no endpoint after {} in {}'.format(
-                endpoint_name,
-                docfile_URL
-            ))
+            print("Found no endpoint after {} in {}".format(endpoint_name, docfile_URL))
         return False
 
     endpoint_search_start_pos = endpoint_search_start_match.start()
-    after_endpoint_re = re.compile(r'<[^h\/]')
-    endpoint_search_end = after_endpoint_re.search(html_doc_response.text,
-                                                   endpoint_search_start_pos)
+    after_endpoint_re = re.compile(r"<[^h\/]")
+    endpoint_search_end = after_endpoint_re.search(
+        html_doc_response.text, endpoint_search_start_pos
+    )
     if not endpoint_search_end:
         endpoint_search_stop_pos = len(html_doc_response.text)
     else:
         endpoint_search_stop_pos = endpoint_search_end.start()
     endpoint_element_match = endpoint_element_re.search(
-        html_doc_response.text,
-        endpoint_search_start_pos,
-        endpoint_search_stop_pos
+        html_doc_response.text, endpoint_search_start_pos, endpoint_search_stop_pos
     )
     endpoint_element_list = []
     while endpoint_element_match:
@@ -106,21 +102,17 @@ def validate_docstring(method_string, call_line, quiet):
         endpoint_element_match = endpoint_element_re.search(
             html_doc_response.text,
             endpoint_element_match.end(),
-            endpoint_search_stop_pos
+            endpoint_search_stop_pos,
         )
 
     if not endpoint_element_list:
         if not quiet:
-            print('Found no endpoint after {} in {}'.format(
-                endpoint_name,
-                docfile_URL
-            ))
+            print("Found no endpoint after {} in {}".format(endpoint_name, docfile_URL))
         return False
     docfile_lines = []
     for endpoint_element_str in endpoint_element_list:
         docfile_match = re.search(
-            '(POST|GET|PUT|PATCH|DELETE) (.*)',
-            endpoint_element_str
+            "(POST|GET|PUT|PATCH|DELETE) (.*)", endpoint_element_str
         )
         docfile_lines.append(docfile_match.group())
         docfile_verb, docfile_API_URL = docfile_match.groups()
@@ -130,12 +122,14 @@ def validate_docstring(method_string, call_line, quiet):
             continue
         return True
     if not quiet:
-        print('{} docstring {} not found in {} (found {})'.format(
-            method_string,
-            docstring_verb + ' ' + api_URL,
-            doc_URL,
-            str(docfile_lines)
-        ))
+        print(
+            "{} docstring {} not found in {} (found {})".format(
+                method_string,
+                docstring_verb + " " + api_URL,
+                doc_URL,
+                str(docfile_lines),
+            )
+        )
     return False
 
 
@@ -149,5 +143,5 @@ def test_methods():
         validate_method(method_to_test)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     test_methods()


Test patch:
diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json
index 84cdad01..0588d649 100644
--- a/tests/fixtures/account.json
+++ b/tests/fixtures/account.json
@@ -471,6 +471,24 @@
 		],
 		"status_code": 200
 	},
+	"query_audit_by_account": {
+		"method": "GET",
+		"endpoint": "audit/course/accounts/1",
+		"data": [
+			{
+				"id": 1,
+				"name": "UCF",
+				"start_at": "2022-11-18T05:00:00-06:00",
+				"conclude_at": "2022-11-18T15:00:00-06:00"
+			},
+			{
+				"id": 2,
+				"name": "FSU",
+				"start_at": "2022-11-18T05:00:00-06:00",
+				"conclude_at": "2022-11-18T15:00:00-06:00"
+			}
+		]
+	},
 	"multiple": {
 		"method": "GET",
 		"endpoint": "accounts",
diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json
index cc750c9e..6e3f8a97 100644
--- a/tests/fixtures/course.json
+++ b/tests/fixtures/course.json
@@ -287,19 +287,20 @@
 		"method": "GET",
 		"endpoint": "courses/1/epub_exports/1",
 		"data": {
+			"id": 1,
+			"name": "course1",
+			"epub_export": {
 				"id": 1,
-				"name": "course1",
-				"epub_export": {
-					"id": 1,
-					"created_at": "2019-01-01T00:00:00Z",
-					"attachment": {"url":"https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"},
-					"progress_url": "https://dummyurl.com/api/v1/progress/4",
-					"user_id": 4,
-					"workflow_state": "exported"
-					}
+				"created_at": "2019-01-01T00:00:00Z",
+				"attachment": {
+					"url": "https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"
+				},
+				"progress_url": "https://dummyurl.com/api/v1/progress/4",
+				"user_id": 4,
+				"workflow_state": "exported"
+			}
 		},
 		"status_code": 200
-
 	},
 	"get_epub_exports": {
 		"method": "GET",
@@ -307,30 +308,33 @@
 		"data": {
 			"courses": [
 				{
-				"id": 1,
-				"name": "course1",
-				"epub_export": {
 					"id": 1,
-					"created_at": "2019-01-01T00:00:00Z",
-					"attachment": {"url":"https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"},
-					"progress_url": "https://dummyurl.com/api/v1/progress/4",
-					"user_id": 4,
-					"workflow_state": "exported"
+					"name": "course1",
+					"epub_export": {
+						"id": 1,
+						"created_at": "2019-01-01T00:00:00Z",
+						"attachment": {
+							"url": "https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"
+						},
+						"progress_url": "https://dummyurl.com/api/v1/progress/4",
+						"user_id": 4,
+						"workflow_state": "exported"
 					}
 				},
 				{
-				"id": 2,
-				"name": "course2",
-				"epub_export": {
 					"id": 2,
-					"created_at": "2019-06-01T00:00:00Z",
-					"attachment": {"url":"https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"},
-					"progress_url": "https://dummyurl.com/api/v1/progress/4",
-					"user_id": 5,
-					"workflow_state": "exported"
+					"name": "course2",
+					"epub_export": {
+						"id": 2,
+						"created_at": "2019-06-01T00:00:00Z",
+						"attachment": {
+							"url": "https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"
+						},
+						"progress_url": "https://dummyurl.com/api/v1/progress/4",
+						"user_id": 5,
+						"workflow_state": "exported"
 					}
 				}
-
 			]
 		},
 		"status_code": 200
@@ -339,16 +343,18 @@
 		"method": "POST",
 		"endpoint": "courses/1/epub_exports/",
 		"data": {
+			"id": 1,
+			"name": "course1",
+			"epub_export": {
 				"id": 1,
-				"name": "course1",
-				"epub_export": {
-					"id": 1,
-					"created_at": "2019-01-01T00:00:00Z",
-					"attachment": {"url":"https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"},
-					"progress_url": "https://dummyurl.com/api/v1/progress/4",
-					"user_id": 4,
-					"workflow_state": "exported"
-					}
+				"created_at": "2019-01-01T00:00:00Z",
+				"attachment": {
+					"url": "https://dummyurl.com/api/v1/attachments/789?download_frd=1&verifier=bG9sY2F0cyEh"
+				},
+				"progress_url": "https://dummyurl.com/api/v1/progress/4",
+				"user_id": 4,
+				"workflow_state": "exported"
+			}
 		},
 		"status_code": 200
 	},
@@ -475,7 +481,9 @@
 						"grading_type": "points",
 						"course_id": 1,
 						"name": "Test assignment",
-						"submission_types": ["online_text_entry"]
+						"submission_types": [
+							"online_text_entry"
+						]
 					}
 				],
 				"courses": [
@@ -498,9 +506,9 @@
 		},
 		"status_code": 200
 	},
-	"get_outcome_import_status":{
-		"method" : "GET",
-		"endpoint" : "courses/1/outcome_imports/1",
+	"get_outcome_import_status": {
+		"method": "GET",
+		"endpoint": "courses/1/outcome_imports/1",
 		"data": {
 			"id": 1,
 			"created_at": "2013-12-01T23:59:00-06:00",
@@ -515,9 +523,9 @@
 		},
 		"status_code": 200
 	},
-	"get_outcome_import_status_latest":{
-		"method" : "GET",
-		"endpoint" : "courses/1/outcome_imports/latest",
+	"get_outcome_import_status_latest": {
+		"method": "GET",
+		"endpoint": "courses/1/outcome_imports/latest",
 		"data": {
 			"id": 1,
 			"created_at": "2013-12-01T23:59:00-06:00",
@@ -544,43 +552,54 @@
 		"method": "GET",
 		"endpoint": "courses/1/quizzes/assignment_overrides",
 		"data": {
-			"quiz_assignment_overrides": [{
-				"quiz_id": "1",
-				"due_dates": [{
-					"id": 1,
-					"due_at": "2014-02-21T06:59:59Z",
-					"unlock_at": null,
-					"lock_at": "2014-02-21T06:59:59Z",
-					"title": "Project X",
-					"base": true
-				}],
-				"all_dates": [{
-					"id": 1,
-					"due_at": "2014-02-21T06:59:59Z",
-					"unlock_at": null,
-					"lock_at": "2014-02-21T06:59:59Z",
-					"title": "Project X",
-					"base": true
-				}]
-			},{
-				"quiz_id": "2",
-				"due_dates": [{
-					"id": 1,
-					"due_at": "2014-02-21T06:59:59Z",
-					"unlock_at": null,
-					"lock_at": "2014-02-21T06:59:59Z",
-					"title": "Project X",
-					"base": true
-				}],
-				"all_dates": [{
-					"id": 1,
-					"due_at": "2014-02-21T06:59:59Z",
-					"unlock_at": null,
-					"lock_at": "2014-02-21T06:59:59Z",
-					"title": "Project X",
-					"base": true
-				}]
-			}]
+			"quiz_assignment_overrides": [
+				{
+					"quiz_id": "1",
+					"due_dates": [
+						{
+							"id": 1,
+							"due_at": "2014-02-21T06:59:59Z",
+							"unlock_at": null,
+							"lock_at": "2014-02-21T06:59:59Z",
+							"title": "Project X",
+							"base": true
+						}
+					],
+					"all_dates": [
+						{
+							"id": 1,
+							"due_at": "2014-02-21T06:59:59Z",
+							"unlock_at": null,
+							"lock_at": "2014-02-21T06:59:59Z",
+							"title": "Project X",
+							"base": true
+						}
+					]
+				},
+				{
+					"quiz_id": "2",
+					"due_dates": [
+						{
+							"id": 1,
+							"due_at": "2014-02-21T06:59:59Z",
+							"unlock_at": null,
+							"lock_at": "2014-02-21T06:59:59Z",
+							"title": "Project X",
+							"base": true
+						}
+					],
+					"all_dates": [
+						{
+							"id": 1,
+							"due_at": "2014-02-21T06:59:59Z",
+							"unlock_at": null,
+							"lock_at": "2014-02-21T06:59:59Z",
+							"title": "Project X",
+							"base": true
+						}
+					]
+				}
+			]
 		},
 		"status_code": 200
 	},
@@ -661,7 +680,7 @@
 		],
 		"status_code": 200
 	},
-	"get_section":{
+	"get_section": {
 		"method": "GET",
 		"endpoint": "courses/1/sections/1",
 		"data": {
@@ -713,15 +732,15 @@
 		"endpoint": "courses/1/gradebook_history/08-23-2019/graders/1/assignments/1/submissions",
 		"data": [
 			{
-                "submission_id": 4,
-                "versions": null
-            },
-            {
-                "submission_id": 7,
-                "versions": null
-            }
-        ]
-    },
+				"submission_id": 4,
+				"versions": null
+			},
+			{
+				"submission_id": 7,
+				"versions": null
+			}
+		]
+	},
 	"todo_items": {
 		"method": "GET",
 		"endpoint": "courses/1/todo",
@@ -1215,7 +1234,7 @@
 	"show_front_page": {
 		"method": "GET",
 		"endpoint": "courses/1/front_page",
-		"data":{
+		"data": {
 			"id": 1,
 			"url": "front-page",
 			"title": "Front Page"
@@ -1399,11 +1418,10 @@
 	"get_discussion_topic": {
 		"method": "GET",
 		"endpoint": "courses/1/discussion_topics/1",
-		"data":
-			{
-				"id": 1,
-				"title": "Topic 1"
-			},
+		"data": {
+			"id": 1,
+			"title": "Topic 1"
+		},
 		"status_code": 200
 	},
 	"get_file": {
@@ -1415,16 +1433,16 @@
 			"size": 2048
 		},
 		"status_code": 200
-    },
-    "get_file_quota": {
-        "method": "GET",
-        "endpoint": "courses/1/files/quota",
-        "data": {
-            "quota": 524288000,
-            "quota_used": 402653184
-        },
-        "status_code": 200
-    },
+	},
+	"get_file_quota": {
+		"method": "GET",
+		"endpoint": "courses/1/files/quota",
+		"data": {
+			"quota": 524288000,
+			"quota_used": 402653184
+		},
+		"status_code": 200
+	},
 	"get_full_discussion_topic": {
 		"method": "GET",
 		"endpoint": "courses/1/discussion_topics/1/view",
@@ -1472,7 +1490,11 @@
 		"endpoint": "courses/1/discussion_topics/reorder",
 		"data": {
 			"reorder": true,
-			"order": [1, 2, 3]
+			"order": [
+				1,
+				2,
+				3
+			]
 		},
 		"status_code": 200
 	},
@@ -1614,7 +1636,7 @@
 					"2012-01-24T13:00:00-00:00": 19,
 					"2012-01-24T14:00:00-00:00": 13,
 					"2012-01-27T09:00:00-00:00": 23
-			},
+				},
 				"participations": [
 					{
 						"created_at": "2012-01-21T22:00:00-06:00",
@@ -1679,16 +1701,34 @@
 		"endpoint": "courses/1/analytics/users/1/communication",
 		"data": [
 			{
-				"2012-01-24":{
-					"instructorMessages":1,
-					"studentMessages":2
+				"2012-01-24": {
+					"instructorMessages": 1,
+					"studentMessages": 2
 				},
-				"2012-01-27":{
-					"studentMessages":1
+				"2012-01-27": {
+					"studentMessages": 1
 				}
 			}
 		]
 	},
+	"query_audit_by_course": {
+		"method": "GET",
+		"endpoint": "audit/course/courses/1",
+		"data": [
+			{
+				"id": 1,
+				"name": "FIU",
+				"start_at": "2022-11-18T05:00:00-06:00",
+				"conclude_at": "2022-11-18T15:00:00-06:00"
+			},
+			{
+				"id": 2,
+				"name": "UNF",
+				"start_at": "2022-11-18T05:00:00-06:00",
+				"conclude_at": "2022-11-18T15:00:00-06:00"
+			}
+		]
+	},
 	"list_tabs": {
 		"method": "GET",
 		"endpoint": "courses/1/tabs",
@@ -1796,12 +1836,12 @@
 		"method": "PUT",
 		"endpoint": "courses/1/assignments/1/submissions/1",
 		"data": {
-				"id": 1,
-				"assignments_id": 1,
-				"user_id": 1,
-				"html_url": "https://example.com/courses/1/assignments/1/submissions/1",
-				"submission_type": "online_upload",
-				"excused": true
+			"id": 1,
+			"assignments_id": 1,
+			"user_id": 1,
+			"html_url": "https://example.com/courses/1/assignments/1/submissions/1",
+			"submission_type": "online_upload",
+			"excused": true
 		},
 		"status_code": 200
 	},
@@ -1820,15 +1860,13 @@
 		"endpoint": "search/all_courses",
 		"data": [
 			{
-				"course":
-				{
+				"course": {
 					"id": 1,
 					"name": "Course 1"
 				}
 			},
 			{
-				"course":
-				{
+				"course": {
 					"id": 2,
 					"name": "Course 2"
 				}
@@ -1845,11 +1883,20 @@
 			"context_id": 1,
 			"context_type": "Course",
 			"grading_scheme": [
-			  {"name": "A", "value": 0.9},
-			  {"name": "B", "value": 0.8},
-			  {"name": "C", "value": 0.7}
+				{
+					"name": "A",
+					"value": 0.9
+				},
+				{
+					"name": "B",
+					"value": 0.8
+				},
+				{
+					"name": "C",
+					"value": 0.7
+				}
 			]
-		  },
+		},
 		"status_code": 200
 	},
 	"get_grading_periods": {
@@ -1890,26 +1937,41 @@
 		"endpoint": "courses/1/grading_standards",
 		"data": [
 			{
-			"title": "Grading Standard 1",
-			"id": 1,
-			"context_id": 1,
-			"context_type": "Course",
-			"grading_scheme": [
-			  {"name": "A", "value": 0.9},
-			  {"name": "B", "value": 0.8},
-			  {"name": "C", "value": 0.7}
-			]
-		  },
-		  {
-			"title": "Grading Standard 2",
-			"id": 2,
-			"context_id": 1,
-			"context_type": "Course",
-			"grading_scheme": [
-			  {"name": "D", "value": 0.6},
-			  {"name": "F", "value": 0.5}
-			]
-		  }
+				"title": "Grading Standard 1",
+				"id": 1,
+				"context_id": 1,
+				"context_type": "Course",
+				"grading_scheme": [
+					{
+						"name": "A",
+						"value": 0.9
+					},
+					{
+						"name": "B",
+						"value": 0.8
+					},
+					{
+						"name": "C",
+						"value": 0.7
+					}
+				]
+			},
+			{
+				"title": "Grading Standard 2",
+				"id": 2,
+				"context_id": 1,
+				"context_type": "Course",
+				"grading_scheme": [
+					{
+						"name": "D",
+						"value": 0.6
+					},
+					{
+						"name": "F",
+						"value": 0.5
+					}
+				]
+			}
 		],
 		"status_code": 200
 	},
@@ -1923,7 +1985,7 @@
 					{
 						"name": "John Smith",
 						"id": 49,
-						"assignments":[
+						"assignments": [
 							{
 								"id": 44,
 								"description": "<p>homework1</p>"
@@ -1938,7 +2000,7 @@
 					{
 						"name": "Sara Hanks",
 						"id": 32,
-						"assignments":[
+						"assignments": [
 							{
 								"id": 19,
 								"description": "<p>homework2</p>"
@@ -1948,7 +2010,6 @@
 				]
 			}
 		]
-
 	},
 	"get_gradebook_history_details": {
 		"method": "GET",
@@ -1957,12 +2018,20 @@
 			{
 				"id": 27,
 				"name": "John Smith",
-				"assignments": [1, 2, 3]
+				"assignments": [
+					1,
+					2,
+					3
+				]
 			},
 			{
 				"id": 30,
 				"name": "Jenny Parker",
-				"assignments": [4, 5, 6]
+				"assignments": [
+					4,
+					5,
+					6
+				]
 			}
 		]
 	},
@@ -1975,11 +2044,20 @@
 			"context_id": 1,
 			"context_type": "Course",
 			"grading_scheme": [
-			  {"name": "A", "value": 0.9},
-			  {"name": "B", "value": 0.8},
-			  {"name": "C", "value": 0.7}
+				{
+					"name": "A",
+					"value": 0.9
+				},
+				{
+					"name": "B",
+					"value": 0.8
+				},
+				{
+					"name": "C",
+					"value": 0.7
+				}
 			]
-		  },
+		},
 		"status_code": 200
 	},
 	"create_content_migration": {
@@ -2241,7 +2319,10 @@
 			"use_justification": "fair_use",
 			"license": "private",
 			"message": "2 files updated",
-			"file_ids": [1,2]
+			"file_ids": [
+				1,
+				2
+			]
 		},
 		"status_code": 200
 	},
@@ -2250,7 +2331,10 @@
 		"endpoint": "courses/1/usage_rights",
 		"data": {
 			"message": "2 files updated",
-			"file_ids": [1,2]
+			"file_ids": [
+				1,
+				2
+			]
 		},
 		"status_code": 200
 	},
@@ -2316,11 +2400,11 @@
 		],
 		"status_code": 200
 	},
-    "resolve_path": {
+	"resolve_path": {
 		"method": "GET",
 		"endpoint": "courses/1/folders/by_path/Folder_Level_1/Folder_Level_2/Folder_Level_3",
 		"data": [
-            {
+			{
 				"id": 2,
 				"files_count": 0,
 				"folders_count": 1,
@@ -2351,18 +2435,18 @@
 		],
 		"status_code": 200
 	},
-    "resolve_path_null": {
+	"resolve_path_null": {
 		"method": "GET",
 		"endpoint": "courses/1/folders/by_path",
 		"data": [
-            {
+			{
 				"id": 2,
 				"files_count": 0,
 				"folders_count": 1,
 				"name": "course_files",
 				"full_name": "course_files"
 			}
-        ],
+		],
 		"status_code": 200
 	}
 }
diff --git a/tests/test_account.py b/tests/test_account.py
index 7717634d..7c95f662 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -18,6 +18,7 @@
 from canvasapi.authentication_provider import AuthenticationProvider
 from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.course import Course
+from canvasapi.course_event import CourseEvent
 from canvasapi.enrollment import Enrollment
 from canvasapi.enrollment_term import EnrollmentTerm
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
@@ -362,6 +363,27 @@ def test_get_reports(self, m):
         self.assertTrue(hasattr(reports_list[0], "id"))
         self.assertIsInstance(str(reports_list[0]), str)
 
+    # get query by account
+    def test_query_audit_by_account(self, m):
+        register_uris({"account": ["query_audit_by_account"]}, m)
+
+        # Get paginated list and convert to list
+        query = self.account.query_audit_by_account()
+        query_list = list(query)
+
+        # Check that list contains objects of type CourseEvent
+        self.assertEqual(len(query_list), 2)
+        self.assertIsInstance(query_list[0], CourseEvent)
+        self.assertIsInstance(query_list[1], CourseEvent)
+
+        # Verify contents of first object
+        self.assertEqual(query_list[0].id, 1)
+        self.assertEqual(query_list[0].name, "UCF")
+
+        # Verify contents of second object
+        self.assertEqual(query_list[1].id, 2)
+        self.assertEqual(query_list[1].name, "FSU")
+
     # get_subaccounts()
     def test_get_subaccounts(self, m):
         required = {"account": ["subaccounts", "subaccounts_page_2"]}
@@ -1157,7 +1179,6 @@ def test_import_outcome_binary(self, m):
         self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
 
     def test_import_outcome_id(self, m):
-
         register_uris({"account": ["import_outcome"]}, m)
 
         outcome_import = self.account.import_outcome(1)
diff --git a/tests/test_canvas.py b/tests/test_canvas.py
index 1cec0d11..0a0320d3 100644
--- a/tests/test_canvas.py
+++ b/tests/test_canvas.py
@@ -812,7 +812,6 @@ def test_get_eportfolio(self, m):
 
     # get_epub_exports()
     def test_get_epub_exports(self, m):
-
         register_uris({"course": ["get_epub_exports"]}, m)
 
         epub_export_list = self.canvas.get_epub_exports()
diff --git a/tests/test_course.py b/tests/test_course.py
index b9a3daef..5dedb2ed 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -13,6 +13,7 @@
 from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.course import Course, CourseNickname, LatePolicy, Page
 from canvasapi.course_epub_export import CourseEpubExport
+from canvasapi.course_event import CourseEvent
 from canvasapi.custom_gradebook_columns import CustomGradebookColumn
 from canvasapi.discussion_topic import DiscussionTopic
 from canvasapi.enrollment import Enrollment
@@ -753,6 +754,27 @@ def test_get_discussion_topic(self, m):
         self.assertTrue(hasattr(discussion_by_obj, "course_id"))
         self.assertEqual(discussion_by_obj.course_id, 1)
 
+    # get query by course
+    def test_query_audit_by_course(self, m):
+        register_uris({"course": ["query_audit_by_course"]}, m)
+
+        # Get paginated list and convert to list
+        query = self.course.query_audit_by_course()
+        query_list = list(query)
+
+        # Check that list contains objects of type CourseEvent
+        self.assertEqual(len(query_list), 2)
+        self.assertIsInstance(query_list[0], CourseEvent)
+        self.assertIsInstance(query_list[1], CourseEvent)
+
+        # Verify contents of first object
+        self.assertEqual(query_list[0].id, 1)
+        self.assertEqual(query_list[0].name, "FIU")
+
+        # Verify contents of second object
+        self.assertEqual(query_list[1].id, 2)
+        self.assertEqual(query_list[1].name, "UNF")
+
     # get_file()
     def test_get_file(self, m):
         register_uris({"course": ["get_file"]}, m)
@@ -1542,7 +1564,6 @@ def test_import_outcome_binary(self, m):
         self.assertEqual(outcome_import.data["import_type"], "instructure_csv")
 
     def test_import_outcome_id(self, m):
-
         register_uris({"course": ["import_outcome"]}, m)
 
         outcome_import = self.course.import_outcome(1)
diff --git a/tests/test_course_event.py b/tests/test_course_event.py
new file mode 100644
index 00000000..0c593b43
--- /dev/null
+++ b/tests/test_course_event.py
@@ -0,0 +1,30 @@
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestCourseEvent(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {
+                "account": ["get_by_id", "query_audit_by_account"],
+                "course": ["query_audit_by_course"],
+            }
+            register_uris(requires, m)
+
+            self.account = self.canvas.get_account(1)
+
+            # isolate one of the CourseEvent objects
+            self.query_audit_by_account = self.account.query_audit_by_account()[0]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.query_audit_by_account)
+        self.assertIsInstance(string, str)
diff --git a/tests/test_favorite.py b/tests/test_favorite.py
index 6b30759a..05b6cb9a 100644
--- a/tests/test_favorite.py
+++ b/tests/test_favorite.py
@@ -25,7 +25,6 @@ def setUp(self):
 
     # __str__()
     def test_uncapitalized___str__(self, m):
-
         register_uris(
             {
                 "current_user": [
@@ -46,7 +45,6 @@ def test_uncapitalized___str__(self, m):
         self.assertIsInstance(string, str)
 
     def test_capitalized___str__(self, m):
-
         register_uris(
             {
                 "current_user": [
@@ -68,7 +66,6 @@ def test_capitalized___str__(self, m):
 
     # remove()
     def test_remove_uncapitalized_favorite_course(self, m):
-
         register_uris(
             {
                 "current_user": [
@@ -88,7 +85,6 @@ def test_remove_uncapitalized_favorite_course(self, m):
         self.assertEqual(evnt.context_id, 1)
 
     def test_remove_uncapitalized_favorite_group(self, m):
-
         register_uris(
             {
                 "current_user": [
@@ -108,7 +104,6 @@ def test_remove_uncapitalized_favorite_group(self, m):
         self.assertEqual(evnt.context_id, 1)
 
     def test_remove_capitalized_favorite_course(self, m):
-
         register_uris(
             {
                 "current_user": [
@@ -128,7 +123,6 @@ def test_remove_capitalized_favorite_course(self, m):
         self.assertEqual(evnt.context_id, 1)
 
     def test_remove_capitalized_favorite_group(self, m):
-
         register_uris(
             {
                 "current_user": [
diff --git a/tests/test_gradebook_history.py b/tests/test_gradebook_history.py
index 6ccf40b0..00bfcce6 100644
--- a/tests/test_gradebook_history.py
+++ b/tests/test_gradebook_history.py
@@ -10,7 +10,6 @@
 @requests_mock.Mocker()
 class TestDay(unittest.TestCase):
     def setUp(self):
-
         self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
 
         with requests_mock.Mocker() as m:
@@ -29,7 +28,6 @@ def test__str__(self, m):
 @requests_mock.Mocker()
 class TestGrader(unittest.TestCase):
     def setUp(self):
-
         self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
 
         with requests_mock.Mocker() as m:
@@ -50,7 +48,6 @@ def test__str__(self, m):
 @requests_mock.Mocker()
 class TestSubmissionHistory(unittest.TestCase):
     def setUp(self):
-
         self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
 
         with requests_mock.Mocker() as m:
@@ -71,7 +68,6 @@ def test__str__(self, m):
 @requests_mock.Mocker()
 class TestSubmissionVersion(unittest.TestCase):
     def setUp(self):
-
         self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
 
         with requests_mock.Mocker() as m:
diff --git a/tests/test_grading_period.py b/tests/test_grading_period.py
index 8a76fcf2..218c69eb 100644
--- a/tests/test_grading_period.py
+++ b/tests/test_grading_period.py
@@ -20,7 +20,6 @@ def setUp(self):
         )
 
     def test_str(self, m):
-
         test_str = str(self.grading_period)
         self.assertIsInstance(test_str, str)
 
diff --git a/tests/test_outcome_import.py b/tests/test_outcome_import.py
index 1222ea0b..3b563cba 100644
--- a/tests/test_outcome_import.py
+++ b/tests/test_outcome_import.py
@@ -17,6 +17,5 @@ def setUp(self):
         )
 
     def test_str(self, m):
-
         test_str = str(self.outcome_import)
         self.assertIsInstance(test_str, str)
diff --git a/tests/test_page.py b/tests/test_page.py
index 08e3b388..01ac1661 100644
--- a/tests/test_page.py
+++ b/tests/test_page.py
@@ -13,7 +13,6 @@
 @requests_mock.Mocker()
 class TestPage(unittest.TestCase):
     def setUp(self):
-
         self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
 
         with requests_mock.Mocker() as m:
diff --git a/tests/test_scope.py b/tests/test_scope.py
index 2e7b1c45..aea5604d 100644
--- a/tests/test_scope.py
+++ b/tests/test_scope.py
@@ -17,6 +17,5 @@ def setUp(self):
         )
 
     def test_str(self, m):
-
         test_str = str(self.scope)
         self.assertIsInstance(test_str, str)
diff --git a/tests/test_submission.py b/tests/test_submission.py
index cdcb5953..84b7a2dd 100644
--- a/tests/test_submission.py
+++ b/tests/test_submission.py
@@ -14,7 +14,6 @@
 @requests_mock.Mocker()
 class TestSubmission(unittest.TestCase):
     def setUp(self):
-
         self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
 
         with requests_mock.Mocker() as m:
diff --git a/tests/test_user.py b/tests/test_user.py
index 9093b783..a2197097 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -32,7 +32,6 @@
 @requests_mock.Mocker()
 class TestUser(unittest.TestCase):
     def setUp(self):
-
         self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
 
         with requests_mock.Mocker() as m:
diff --git a/tests/test_util.py b/tests/test_util.py
index 863d7e10..cd85648b 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -92,7 +92,6 @@ def test_is_multivalued_zip(self, m):
 
     # combine_kwargs()
     def test_combine_kwargs_empty(self, m):
-
         result = combine_kwargs()
         self.assertIsInstance(result, list)
         self.assertEqual(len(result), 0)
diff --git a/tests_requirements.txt b/tests_requirements.txt
index 3901adee..2eeab8b0 100644
--- a/tests_requirements.txt
+++ b/tests_requirements.txt
@@ -1,6 +1,6 @@
 -r requirements.txt
 
-black
+black>=23.3.0
 coverage
 flake8
 isort