Skip to content

Commit 5bf0224

Browse files
authored
Merge pull request #714 from ucfopen/develop
Release v3.5.0
2 parents 9f03017 + 1294e15 commit 5bf0224

8 files changed

Lines changed: 84 additions & 62 deletions

File tree

AUTHORS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@
3232
- Caitlin Fabian [@Caitlin-Fabian](https://github.com/Caitlin-Fabian)
3333
- Cameron Cuff [@ctcuff](https://github.com/ctcuff)
3434
- Catherine Abbruzzese [@cat0698](https://github.com/cat0698)
35+
- Christopher Nitta [@cjnitta](https://github.com/cjnitta)
3536
- Craig Thompson [@craigdsthompson](https://github.com/craigdsthompson)
3637
- Dalton Durst [@UniversalSuperBox](https://github.com/UniversalSuperBox)
3738
- Damian Sweeney [@damianfs](https://github.com/damianfs)
39+
- Daniel Bosk [@dbosk](https://github.com/dbosk)
3840
- Daniel Brinkman [@DanBrink91](https://github.com/DanBrink91)
3941
- Daniel Grobani [@dgrobani](https://github.com/dgrobani)
42+
- Daniel Gawne [@dgwn](https://github.com/dgwn)
4043
- Daniel Molares [@dmols](https://github.com/dmols)
4144
- David Warden [@dfwarden](https://github.com/dfwarden)
4245
- Davis Goff [@Goff-Davis](https://github.com/Goff-Davis)

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
## [Unreleased]
44

5+
## [3.5.0] - 2026-03-12
6+
7+
### General
8+
9+
- Updated `RateLimitExceeded` exception to trigger on HTTP 429 instead of old 403.
10+
- Add a default User-Agent header
11+
12+
### Bugfixes
13+
14+
- `QuizGroup` fixed to have `course_id` of the quiz's `course_id`, instead of the `quiz_id` (Thanks, [@cjnitta](https://github.com/cjnitta) and [@dgwn](https://github.com/dgwn))
15+
516
## [3.4.0] - 2025-11-10
617

718
### New Endpoint Coverage
@@ -656,7 +667,8 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti
656667
- Fixed some incorrectly defined parameters
657668
- Fixed an issue where tests would fail due to an improperly configured requires block
658669

659-
[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v3.4.0...develop
670+
[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v3.5.0...develop
671+
[3.5.0]: https://github.com/ucfopen/canvasapi/compare/v3.4.0...v3.5.0
660672
[3.4.0]: https://github.com/ucfopen/canvasapi/compare/v3.3.0...v3.4.0
661673
[3.3.0]: https://github.com/ucfopen/canvasapi/compare/v3.2.0...v3.3.0
662674
[3.2.0]: https://github.com/ucfopen/canvasapi/compare/v3.1.0...v3.2.0

canvasapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
__all__ = ["Canvas"]
66

7-
__version__ = "3.4.0"
7+
__version__ = "3.5.0"

canvasapi/quiz.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def create_question_group(self, quiz_groups, **kwargs):
112112
)
113113

114114
response_json = response.json()
115-
response_json["quiz_groups"][0].update({"course_id": self.id})
115+
response_json["quiz_groups"][0].update({"course_id": self.course_id})
116116

117117
return QuizGroup(self._requester, response_json.get("quiz_groups")[0])
118118

canvasapi/requester.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def request(
133133
_url=None,
134134
_kwargs=None,
135135
json=False,
136-
**kwargs
136+
**kwargs,
137137
):
138138
"""
139139
Make a request to the Canvas API and return the response.
@@ -167,6 +167,8 @@ def request(
167167
:type json: `bool`
168168
:rtype: :class:`requests.Response`
169169
"""
170+
from canvasapi import __version__
171+
170172
# Check for specific URL endpoints available from Canvas. If not
171173
# specified, pass the given URL and move on.
172174
if not _url:
@@ -185,6 +187,9 @@ def request(
185187
auth_header = {"Authorization": "Bearer {}".format(self.access_token)}
186188
headers.update(auth_header)
187189

190+
if "User-Agent" not in headers:
191+
headers["User-Agent"] = f"python-canvasapi/{__version__}"
192+
188193
# Convert kwargs into list of 2-tuples and combine with _kwargs.
189194
_kwargs = _kwargs or []
190195
_kwargs.extend(kwargs.items())
@@ -262,21 +267,19 @@ def request(
262267
else:
263268
raise Unauthorized(response.json())
264269
elif response.status_code == 403:
265-
if b"Rate Limit Exceeded" in response.content:
266-
remaining = str(
267-
response.headers.get("X-Rate-Limit-Remaining", "Unknown")
268-
)
269-
raise RateLimitExceeded(
270-
"Rate Limit Exceeded. X-Rate-Limit-Remaining: {}".format(remaining)
271-
)
272-
else:
273-
raise Forbidden(response.text)
270+
raise Forbidden(response.text)
274271
elif response.status_code == 404:
275272
raise ResourceDoesNotExist("Not Found")
276273
elif response.status_code == 409:
277274
raise Conflict(response.text)
278275
elif response.status_code == 422:
279276
raise UnprocessableEntity(response.text)
277+
elif response.status_code == 429:
278+
raise RateLimitExceeded(
279+
"Rate Limit Exceeded. X-Rate-Limit-Remaining: {}".format(
280+
response.headers.get("X-Rate-Limit-Remaining", "Unknown")
281+
)
282+
)
280283
elif response.status_code > 400:
281284
# generic catch-all for error codes
282285
raise CanvasException(

docs/exceptions.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ Quick Guide
1717
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
1818
| :class:`~canvasapi.exceptions.Forbidden` | 403 | Canvas has denied access to the resource for this user. |
1919
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
20-
| :class:`~canvasapi.exceptions.RateLimitExceeded` | 403 | Canvas is throttling this request. Try again later. |
21-
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2220
| :class:`~canvasapi.exceptions.ResourceDoesNotExist` | 404 | Canvas could not locate the requested resource. |
2321
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2422
| :class:`~canvasapi.exceptions.Conflict` | 409 | Canvas had a conflict with an existing resource. |
2523
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2624
| :class:`~canvasapi.exceptions.UnprocessableEntity` | 422 | Canvas was unable to process the request. |
2725
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
26+
| :class:`~canvasapi.exceptions.RateLimitExceeded` | 429 | Canvas is throttling this request. Try again later. |
27+
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
2828
| :class:`~canvasapi.exceptions.RequiredFieldMissing` | N/A | A required keyword argument was not included. |
2929
+-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+
3030
| :class:`~canvasapi.exceptions.CanvasException` | N/A | An unknown error was thrown. |
@@ -83,11 +83,6 @@ Class Reference
8383

8484
The :class:`~canvasapi.exceptions.Forbidden` exception is thrown when Canvas returns an HTTP 403 error.
8585

86-
.. autoclass:: canvasapi.exceptions.RateLimitExceeded
87-
:members:
88-
89-
The :class:`~canvasapi.exceptions.RateLimitExceeded` exception is thrown when Canvas returns an HTTP 403 error that includes the body "403 Forbidden (Rate Limit Exceeded)". It will include the value of the ``X-Rate-Limit-Remaining`` header (if available) for reference.
90-
9186
.. autoclass:: canvasapi.exceptions.Conflict
9287
:members:
9388

@@ -97,3 +92,8 @@ Class Reference
9792
:members:
9893

9994
The :class:`~canvasapi.exceptions.UnprocessableEntity` exception is thrown when Canvas returns an HTTP 422 error.
95+
96+
.. autoclass:: canvasapi.exceptions.RateLimitExceeded
97+
:members:
98+
99+
The :class:`~canvasapi.exceptions.RateLimitExceeded` exception is thrown when Canvas returns an HTTP 429 error. It will include the value of the ``X-Rate-Limit-Remaining`` header (if available) for reference.

tests/fixtures/requests.json

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,6 @@
2626
"data": {},
2727
"status_code": 403
2828
},
29-
"403_rate_limit": {
30-
"method": "ANY",
31-
"endpoint": "403_rate_limit",
32-
"data": "403 Forbidden (Rate Limit Exceeded)",
33-
"headers": {
34-
"X-Rate-Limit-Remaining": "3.14159265359",
35-
"X-Request-Cost": "1.61803398875"
36-
},
37-
"status_code": 403
38-
},
39-
"403_rate_limit_no_remaining_header": {
40-
"method": "ANY",
41-
"endpoint": "403_rate_limit_no_remaining_header",
42-
"data": "403 Forbidden (Rate Limit Exceeded)",
43-
"headers": {
44-
"X-Request-Cost": "1.61803398875"
45-
},
46-
"status_code": 403
47-
},
4829
"404": {
4930
"method": "ANY",
5031
"endpoint": "404",
@@ -63,6 +44,29 @@
6344
"data": {},
6445
"status_code": 422
6546
},
47+
"429_rate_limit": {
48+
"method": "ANY",
49+
"endpoint": "429_rate_limit",
50+
"data": {
51+
"error": "Rate limit exceeded. Please wait and try again."
52+
},
53+
"headers": {
54+
"X-Rate-Limit-Remaining": "3.14159265359",
55+
"X-Request-Cost": "1.61803398875"
56+
},
57+
"status_code": 429
58+
},
59+
"429_rate_limit_no_remaining_header": {
60+
"method": "ANY",
61+
"endpoint": "429_rate_limit_no_remaining_header",
62+
"data": {
63+
"error": "Rate limit exceeded. Please wait and try again."
64+
},
65+
"headers": {
66+
"X-Request-Cost": "1.61803398875"
67+
},
68+
"status_code": 429
69+
},
6670
"500": {
6771
"method": "ANY",
6872
"endpoint": "500",
@@ -117,4 +121,4 @@
117121
"data": {},
118122
"status_code": 200
119123
}
120-
}
124+
}

tests/test_requester.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,6 @@ def test_request_403(self, m):
153153
with self.assertRaises(Forbidden):
154154
self.requester.request("GET", "403")
155155

156-
def test_request_403_RateLimitExeeded(self, m):
157-
register_uris({"requests": ["403_rate_limit"]}, m)
158-
159-
with self.assertRaises(RateLimitExceeded) as exc:
160-
self.requester.request("GET", "403_rate_limit")
161-
162-
self.assertEqual(
163-
exc.exception.message,
164-
"Rate Limit Exceeded. X-Rate-Limit-Remaining: 3.14159265359",
165-
)
166-
167-
def test_request_403_RateLimitExeeded_no_remaining_header(self, m):
168-
register_uris({"requests": ["403_rate_limit_no_remaining_header"]}, m)
169-
170-
with self.assertRaises(RateLimitExceeded) as exc:
171-
self.requester.request("GET", "403_rate_limit_no_remaining_header")
172-
173-
self.assertEqual(
174-
exc.exception.message,
175-
"Rate Limit Exceeded. X-Rate-Limit-Remaining: Unknown",
176-
)
177-
178156
def test_request_404(self, m):
179157
register_uris({"requests": ["404"]}, m)
180158

@@ -193,6 +171,28 @@ def test_request_422(self, m):
193171
with self.assertRaises(UnprocessableEntity):
194172
self.requester.request("GET", "422")
195173

174+
def test_request_429_RateLimitExeeded(self, m):
175+
register_uris({"requests": ["429_rate_limit"]}, m)
176+
177+
with self.assertRaises(RateLimitExceeded) as exc:
178+
self.requester.request("GET", "429_rate_limit")
179+
180+
self.assertEqual(
181+
exc.exception.message,
182+
"Rate Limit Exceeded. X-Rate-Limit-Remaining: 3.14159265359",
183+
)
184+
185+
def test_request_429_RateLimitExeeded_no_remaining_header(self, m):
186+
register_uris({"requests": ["429_rate_limit_no_remaining_header"]}, m)
187+
188+
with self.assertRaises(RateLimitExceeded) as exc:
189+
self.requester.request("GET", "429_rate_limit_no_remaining_header")
190+
191+
self.assertEqual(
192+
exc.exception.message,
193+
"Rate Limit Exceeded. X-Rate-Limit-Remaining: Unknown",
194+
)
195+
196196
def test_request_500(self, m):
197197
register_uris({"requests": ["500"]}, m)
198198

0 commit comments

Comments
 (0)