-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathcanvas-export.py
192 lines (139 loc) · 6.37 KB
/
canvas-export.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#!/usr/bin/env python3
"""
Allows the user to export files from courses via the Canvas API.
TODO: Support for epub export
"""
__author__ = "Matt Howard <http://matt.how/>"
__version__ = "0.1"
__status__ = "Prototype"
import requests
import argparse
import os
import time
class CanvasClient(object):
# dleay 1 second between polling the API
POLL_DELAY = 1
def __init__(self, access_token, api_base):
self._access_token = access_token
self._api_base = api_base
# credit to Christian Barcenas (@cbarcenas) for this method, with modification
def _request(self, method, path, **kwargs):
# Construct full URL
url = requests.compat.urljoin(self._api_base, path)
# Set headers, overriding as necessary
headers_override = kwargs.get('headers', {})
kwargs['headers'] = {}
kwargs['headers'].update(headers_override)
kwargs['headers'].setdefault('Authorization', 'Bearer %s' % self._access_token)
response = requests.request(method, url, **kwargs)
response.raise_for_status()
return response
# credit to Christian Barcenas (@cbarcenas) for this method
def _consume_pagination(self, path, **kwargs):
while path:
response = self._request('GET', path, **kwargs)
items = response.json()
yield from items
path = response.links.get('next', {}).get('url')
def get_courses(self):
return list(self._consume_pagination('/api/v1/courses'))
def export_course(self, course_id):
'''Creates an export, waits on it to be ready, and returns a stream of the export'''
export_post = self._request('POST', '/api/v1/courses/%d/content_exports' % course_id, data={'export_type': 'zip'})
export_post_obj = export_post.json()
progress_url = export_post_obj['progress_url']
export_id = export_post_obj['id']
completion = 0
while completion < 100:
time.sleep(self.POLL_DELAY)
progress_response = self._request('GET', progress_url)
completion = progress_response.json()['completion']
print('Export %d%% complete' % completion)
export_get_obj = {'workflow_state': 'exporting'}
while export_get_obj['workflow_state'] == 'exporting':
time.sleep(self.POLL_DELAY)
export_get_obj = self._request('GET', '/api/v1/courses/%d/content_exports/%d' % (course_id, export_id)).json()
if export_get_obj['workflow_state'] != 'exported':
raise RuntimeError('Illegal export workflow_state %s' % export_get_obj['workflow_state'])
return self._request('GET', export_get_obj['attachment']['url'])
def get_access_token():
if 'CANVAS_ACCESS_TOKEN' not in os.environ:
raise ValueError('CANVAS_ACCESS_TOKEN environmental variable not set')
else:
return os.environ['CANVAS_ACCESS_TOKEN']
def get_api_base():
if 'CANVAS_API_BASE' not in os.environ:
raise ValueError('CANVAS_API_BASE environmental variable not set')
else:
return os.environ['CANVAS_API_BASE']
def download_response(response, filename):
with open(filename, 'wb') as fd:
for chunk in response.iter_content(chunk_size=128):
fd.write(chunk)
def generate_parser():
parser = argparse.ArgumentParser(description='Export and download course files from Canvas LMS')
parser.add_argument('-l', '--list', action='store_true',
help='list all available courses by code and name then exits')
parser.add_argument('-a', '--all', dest='export_all', action='store_true',
help='export all courses')
parser.add_argument('-f', '--force', action='store_true',
help='overwrite existing files rather than skipping them')
parser.add_argument('courses', metavar='COURSE', nargs='*',
help='either the code or id of a course you want to export')
return parser
def make_course_map(course_idens, ids_to_code):
'''Generates a dict of id => course_code based off a list of identifiers
Arguments:
course_idens - A list of strings either representing ids or course codes
ids_to_code - A dict of id => course code for all available courses
'''
codes_to_id = {v: k for k, v in ids_to_code.items()}
courses_to_export = {}
for course_iden in course_idens:
if course_iden in codes_to_id:
# define by code
course_id = codes_to_id[course_iden]
courses_to_export[course_id] = course_iden
elif course_iden.isdigit():
# define by id
course_id = int(course_iden)
if course_id in ids_to_code:
courses_to_export[course_id] = ids_to_code[course_id]
else:
print('Skipping course id %d, course not found' % course_id)
else:
print('Skipping %s, course not found' % course_iden)
return courses_to_export
def main():
parser = generate_parser()
args = parser.parse_args()
# config client class
access_token = get_access_token()
api_base = get_api_base()
client = CanvasClient(access_token, api_base)
all_courses = client.get_courses()
if args.list:
for course in all_courses:
print('%s\t%s' % (course['course_code'], course['name']))
return
ids_to_code = {course['id']: course['course_code'] for course in all_courses}
courses_to_export = None
if args.export_all:
courses_to_export = ids_to_code
else:
courses_to_export = make_course_map(args.courses, ids_to_code)
if not len(courses_to_export):
print("No courses to export. Exiting.")
return
for course_id, course_code in courses_to_export.items():
download_filename = '%s.zip' % course_code
if os.path.isfile(download_filename) and not args.force:
print('Skipping \'%s\' because it already exists' % download_filename)
else:
print('Requesting %s' % course_code)
content_export_response = client.export_course(course_id)
print('Downloading %s' % course_code)
download_response(content_export_response, download_filename)
print('Download complete')
if __name__ == "__main__":
main()