Repository: ucfopen/canvasapi (language: python)

Issue:
Pagination next_link breaking with get_new_quizzes
Pagination is breaking showing the following message:

```
  File "C:\Python\Envs\test_env\lib\site-packages\canvasapi\paginated_list.py", line 49, in __iter__
    new_elements = self._grow()
  File "C:\Python\Envs\test_env\lib\site-packages\canvasapi\paginated_list.py", line 111, in _grow
    new_elements = self._get_next_page()
  File "C:\Python\Envs\test_env\lib\site-packages\canvasapi\paginated_list.py", line 84, in _get_next_page
    re.search(regex, next_link["url"]).group(1) if next_link else None
AttributeError: 'NoneType' object has no attribute 'group'
```

Course with less than 50 New Quizzes don't cause any issues, but it happens in the situations:
- when a quiz copy failed and the try again button is displayed
- having over 50 New Quizzes in one course

Steps to reproduce the behavior:
- Create more than 50 New Quiz type quizzes
- Get data using get_new_quizzes (https://canvasapi.readthedocs.io/en/stable/course-ref.html?highlight=quizzes#canvasapi.course.Course.get_new_quizzes) and iterate through the results.

# Environment information:
- Python version: 3.10.6
- CanvasAPI version: 3.2.0

Could it be related to what is mentioned in this blog post [Forthcoming Update to Canvas API Response Header Capitalization](https://community.canvaslms.com/t5/The-Product-Blog/Forthcoming-Update-to-Canvas-API-Response-Header-Capitalization/ba-p/604018)?


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9abbb42f..3529dcc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,21 @@
 
 ## [Unreleased]
 
+## [3.3.0] - 2023-08-27
+
+### General
+
+- Added documentation for PaginatedList
+- Rework requester URLs to accomodate graphql and new quizzes endpoints (Thanks, [@bennettscience](https://github.com/bennettscience))
+
+### Bugfixes
+
+- Fixed PaginatedList not respecting new quizzes endpoints (Thanks, [@matthewf-ucsd](https://github.com/matthewf-ucsd))
+
+### Backstage
+
+- Updated codecov action
+
 ## [3.2.0] - 2023-05-25
 
 ### New Endpoint Coverage
@@ -621,7 +636,8 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti
 - Fixed some incorrectly defined parameters
 - Fixed an issue where tests would fail due to an improperly configured requires block
 
-[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v3.2.0...develop
+[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v3.3.0...develop
+[3.3.0]: https://github.com/ucfopen/canvasapi/compare/v3.2.0...v3.3.0
 [3.2.0]: https://github.com/ucfopen/canvasapi/compare/v3.1.0...v3.2.0
 [3.1.0]: https://github.com/ucfopen/canvasapi/compare/v3.0.0...v3.1.0
 [3.0.0]: https://github.com/ucfopen/canvasapi/compare/v2.2.0...v3.0.0
diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py
index 9f66fc6c..d86ac5a5 100644
--- a/canvasapi/__init__.py
+++ b/canvasapi/__init__.py
@@ -4,4 +4,4 @@
 
 __all__ = ["Canvas"]
 
-__version__ = "3.2.0"
+__version__ = "3.3.0"
diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py
index aaa23e0d..bc6501af 100644
--- a/canvasapi/canvas.py
+++ b/canvasapi/canvas.py
@@ -1279,7 +1279,7 @@ def graphql(self, query, variables=None, **kwargs):
             _kwargs=combine_kwargs(**kwargs)
             + [("query", query), ("variables", variables)],
             # Needs to call special endpoint without api/v1
-            _url=self.__requester.original_url + "/api/graphql",
+            _url="graphql",
             json=True,
         )
 
diff --git a/canvasapi/course.py b/canvasapi/course.py
index 21bde43f..523a2713 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -480,7 +480,7 @@ def create_new_quiz(self, **kwargs):
         response = self._requester.request(
             "POST",
             endpoint,
-            _url=self._requester.original_url + "/api/quiz/v1/" + endpoint,
+            _url="new_quizzes",
             _kwargs=combine_kwargs(**kwargs),
         )
         response_json = response.json()
@@ -1758,7 +1758,7 @@ def get_new_quiz(self, assignment, **kwargs):
         response = self._requester.request(
             "GET",
             endpoint,
-            _url=self._requester.original_url + "/api/quiz/v1/" + endpoint,
+            _url="new_quizzes",
             _kwargs=combine_kwargs(**kwargs),
         )
         response_json = response.json()
@@ -1783,7 +1783,7 @@ def get_new_quizzes(self, **kwargs):
             self._requester,
             "GET",
             endpoint,
-            _url_override=self._requester.original_url + "/api/quiz/v1/" + endpoint,
+            _url_override="new_quizzes",
             _kwargs=combine_kwargs(**kwargs),
         )
 
diff --git a/canvasapi/new_quiz.py b/canvasapi/new_quiz.py
index 69200154..5b9b4e09 100644
--- a/canvasapi/new_quiz.py
+++ b/canvasapi/new_quiz.py
@@ -21,7 +21,7 @@ def delete(self, **kwargs):
         response = self._requester.request(
             "DELETE",
             endpoint,
-            _url=self._requester.original_url + "/api/quiz/v1/" + endpoint,
+            _url="new_quizzes",
             _kwargs=combine_kwargs(**kwargs),
         )
         response_json = response.json()
@@ -44,7 +44,7 @@ def update(self, **kwargs):
         response = self._requester.request(
             "PATCH",
             endpoint,
-            _url=self._requester.original_url + "/api/quiz/v1/" + endpoint,
+            _url="new_quizzes",
             _kwargs=combine_kwargs(**kwargs),
         )
         response_json = response.json()
diff --git a/canvasapi/paginated_list.py b/canvasapi/paginated_list.py
index 9decf747..bf799874 100644
--- a/canvasapi/paginated_list.py
+++ b/canvasapi/paginated_list.py
@@ -28,6 +28,24 @@ def __init__(
         _url_override=None,
         **kwargs
     ):
+        """
+        :param content_class: The expected type to return in the list.
+        :type content_class: class
+        :param requester: The requester to pass HTTP requests through.
+        :type requester: :class:`canvasapi.requester.Requester`
+        :param request_method: HTTP request method
+        :type request_method: str
+        :param first_url: Canvas endpoint for the initial request
+        :type first_url: str
+        :param extra_attribs: Extra data to include in the request
+        :type extra_attribs: dict
+        :param _root: Specify a nested property from Canvas to use for the resulting list.
+        :type _root: str
+        :param _url_override: "new_quizzes" or "graphql" for specific Canvas endpoints.
+        Other URLs may be specified for third-party requests.
+        :type _url_override: str
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of type content_class
+        """
         self._elements = list()
 
         self._requester = requester
@@ -78,7 +96,10 @@ def _get_next_page(self):
         else:
             next_link = None
 
-        regex = r"{}(.*)".format(re.escape(self._requester.base_url))
+        regex = r"(?:{}|{})(.*)".format(
+            re.escape(self._requester.base_url),
+            re.escape(self._requester.new_quizzes_url),
+        )
 
         self._next_url = (
             re.search(regex, next_link["url"]).group(1) if next_link else None
diff --git a/canvasapi/requester.py b/canvasapi/requester.py
index bda2c991..b36f1685 100644
--- a/canvasapi/requester.py
+++ b/canvasapi/requester.py
@@ -35,6 +35,8 @@ def __init__(self, base_url, access_token):
         # Preserve the original base url and add "/api/v1" to it
         self.original_url = base_url
         self.base_url = base_url + "/api/v1/"
+        self.new_quizzes_url = base_url + "/api/quiz/v1/"
+        self.graphql = base_url + "/api/graphql"
         self.access_token = access_token
         self._session = requests.Session()
         self._cache = []
@@ -145,10 +147,16 @@ def request(
         :param use_auth: Optional flag to remove the authentication
             header from the request.
         :type use_auth: bool
-        :param _url: Optional argument to send a request to a URL
-            outside of the Canvas API. If this is selected and an
-            endpoint is provided, the endpoint will be ignored and
-            only the _url argument will be used.
+        :param _url: Optional argument to specify a request type to Canvas
+            or to send a request to a URL outside of the Canvas API.
+            If set to "new_quizzes", the new quizzes endpoint will be used.
+            If set to "graphql", a graphql POST request will be sent.
+            If any string URL is provided, it will be used instead of the
+            base REST URL.
+            If omitted or set to None, the base_url for the instance REST
+            endpoint will be used.
+            If this is selected and an endpoint is provided, the endpoint
+            will be ignored and only the `_url` argument will be used..
         :type _url: str
         :param _kwargs: A list of 2-tuples representing processed
             keyword arguments to be sent to Canvas as params or data.
@@ -159,7 +167,16 @@ def request(
         :type json: `bool`
         :rtype: :class:`requests.Response`
         """
-        full_url = _url if _url else "{}{}".format(self.base_url, endpoint)
+        # Check for specific URL endpoints available from Canvas. If not
+        # specified, pass the given URL and move on.
+        if not _url:
+            full_url = "{}{}".format(self.base_url, endpoint)
+        elif _url == "new_quizzes":
+            full_url = "{}{}".format(self.new_quizzes_url, endpoint)
+        elif _url == "graphql":
+            full_url = self.graphql
+        else:
+            full_url = _url
 
         if not headers:
             headers = {}


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 2dc96c3c..c975729b 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -40,7 +40,7 @@ jobs:
       - name: Check for missing kwargs
         run: python scripts/find_missing_kwargs.py
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           fail_ci_if_error: true
           token: ${{ secrets.CODECOV_TOKEN }}