def decode(cls, registry: Registry, args): local = registry.get_service(args.local_service, 'local') # TODO: use disambiguate if args.all: raw_resources = registry.find_all_resources(service=args.service, category=cls.name) else: raw_resources = [ registry.find_resource(title=args.title, service=args.service, category=cls.name) ] for raw_resource in raw_resources: try: destination_path = local.find_existing(registry, raw_resource.title) except FileNotFoundError: destination_path = local.make_markdown_filename( raw_resource.title) if args.destination: destination_path = os.path.join(args.destination, destination_path) decoded_markdown, extra_files = cls.decode_json( registry, raw_resource.data, args) local.write(destination_path, decoded_markdown) for path, data in extra_files: local.write(path, data)
def download(cls, registry: Registry, args): blockpy = registry.get_service(args.service, "blockpy") courses = blockpy.api.get('list/courses/')['courses'] potentials = [ course for course in courses if course['url'] == args.title ] if len(potentials) > 1: raise WaltzAmbiguousResource( "Too many courses with URL '{}'".format(args.title)) elif not potentials: raise WaltzResourceNotFound( "No course with URL '{}' found.".format(args.title)) bundle = blockpy.api.get('export/', json={'course_id': potentials[0]['id']}) # Assignments for assignment in bundle['assignments']: registry.store_resource(blockpy.name, 'problem', assignment['url'], "", json.dumps(assignment)) # Memberships groups_assignments = {} for membership in bundle['memberships']: if membership['assignment_group_url'] not in groups_assignments: groups_assignments[membership['assignment_group_url']] = [] groups_assignments[membership['assignment_group_url']].append( membership['assignment_url']) # Groups for group in bundle['groups']: group['problems'] = groups_assignments.get(group['url'], []) registry.store_resource(blockpy.name, 'blockpy_group', group['url'], "", json.dumps(group))
def download_all(cls, registry: Registry, args): canvas = registry.get_service(args.service, "canvas") local = registry.get_service('local') resources = canvas.api.get(cls.endpoint, retrieve_all=True) rows = [] for resource in tqdm(natsorted(resources, key=cls.sort_resource)): try: path = local.find_existing(registry, resource[cls.title_attribute]) rows.append(("Yes", "Yes", resource[cls.title_attribute], os.path.relpath(path))) except WaltzAmbiguousResource as war: paths = "\n".join( os.path.relpath(path) for path in war.args[0]) rows.append( ("Yes", "Multiple", resource[cls.title_attribute], paths)) except FileNotFoundError: rows.append(("Yes", "No", resource[cls.title_attribute], "")) full_resource = canvas.api.get(cls.endpoint + str(resource[cls.id])) registry.store_resource(canvas.name, cls.name, resource[cls.title_attribute], "", json.dumps(full_resource)) print(tabulate(rows, ('Remote', 'Local', 'Title', 'Path'))) print("Downloaded", len(resources), cls.name_plural)
def encode(cls, registry: Registry, args): local = registry.get_service(args.local_service, 'local') source_path = local.find_existing(registry, args.title, folder_file=cls.folder_file) decoded_markdown = local.read(source_path) data = cls.encode_json(registry, decoded_markdown, args) registry.store_resource(args.service, cls.name, args.title, "", data)
def upload(cls, registry: Registry, args): canvas = registry.get_service(args.service, "canvas") raw_resource = registry.find_resource(title=args.title, service=args.service, category=args.category, disambiguate=args.url) full_page = json.loads(raw_resource.data) canvas.api.put("pages/{url}".format(url=full_page['title']), data={ 'wiki_page[title]': full_page['title'], 'wiki_page[body]': full_page['body'], 'wiki_page[published]': full_page['published'] })
def Init(args): if Registry.exists(args.directory): logging.warning("Existing Waltz registry in this directory.") if args.overwrite: Registry.delete(args.directory) else: return Registry.load(args.directory) registry = Registry.init(args.directory) registry.configure_service(Local(args.directory, {'path': args.directory})) registry.save_to_file() return registry
def upload(cls, registry: Registry, args): canvas = registry.get_service(args.service, "canvas") raw_resource = registry.find_resource(title=args.title, service=canvas.name, category=cls.name, disambiguate="") full_assignment = json.loads(raw_resource.data) assignment_data = cls._make_canvas_upload(registry, full_assignment, args) remote_assignment = cls.find(canvas, args.title) if remote_assignment is None: canvas.api.post('assignments/', data=assignment_data) else: canvas.api.put("assignments/{aid}".format(aid=remote_assignment['id']), data=assignment_data)
def upload(cls, registry: Registry, args): blockpy = registry.get_service(args.service, cls.default_service) raw_resource = registry.find_resource(title=args.title, service=args.service, category=args.category, disambiguate=args.url) full_data = json.loads(raw_resource.data) blockpy.api.post("import", json={ 'course_id': full_data['course_id'], 'assignments': [full_data] })
def download(cls, registry: Registry, args): blockpy = registry.get_service(args.service, "blockpy") bundle = blockpy.api.get('export/', json={'assignment_url': args.title}) potentials = bundle['assignments'] # Assignments if len(potentials) > 1: raise WaltzAmbiguousResource( f"Too many problems with URL '{args.title}'") elif not potentials: raise WaltzResourceNotFound( f"No problem with URL '{args.title}' found.") assignment = potentials[0] registry.store_resource(blockpy.name, 'problem', assignment['url'], "", json.dumps(assignment))
def upload(cls, registry: Registry, args): canvas = registry.get_service(args.service, "canvas") # Get the local version raw_resource = registry.find_resource(title=args.title, service=args.service, category=args.category, disambiguate=args.id) local_quiz = json.loads(raw_resource.data) # Get the remote version remote_quiz = cls.find(canvas, args.title) # Either put or post the quiz if remote_quiz is None: cls.upload_new(registry, local_quiz, args) else: cls.upload_edit(registry, remote_quiz, local_quiz, args)
def decode_question(cls, registry: Registry, question, quiz, args): question_type = cls.TYPES[question['question_type']] if args.combine: raw = question_type.decode_json_raw(registry, question, args) raw['text'] = h2m(raw['text']) return raw, None, None local = registry.get_service(args.local_service, 'local') title = question['question_name'] try: destination_path = local.find_existing(registry, args.title, folder_file=title) except FileNotFoundError: destination_path = local.make_markdown_filename(title) if args.banks: first_bank_path = args.banks[0].format( title=make_safe_filename(title), id=question['id'], quiz_title=make_safe_filename(quiz['title']), quiz_id=quiz['id']) destination_path = os.path.join(first_bank_path, destination_path) else: first_bank_path = make_safe_filename(quiz['title']) if args.destination: destination_path = os.path.join(args.destination, first_bank_path, destination_path) else: destination_path = os.path.join(first_bank_path, destination_path) decoded_markdown = question_type.decode_json(registry, question, args) return title, destination_path, decoded_markdown
def upload_new(cls, registry: Registry, local_quiz, args): canvas = registry.get_service(args.service, "canvas") quiz_data = cls._make_canvas_upload(registry, local_quiz, args) created_quiz = canvas.api.post('quizzes/', data=quiz_data) if 'errors' in created_quiz: pprint(created_quiz['errors']) raise WaltzException("Error loading data, see above.") print("Created quiz", local_quiz['title'], "on canvas") # Create the groups group_name_to_id = {} for group in local_quiz['groups'].values(): group_data = QuizGroup._make_canvas_upload(registry, group, args) created_group = canvas.api.post( 'quizzes/{quiz_id}/groups'.format(quiz_id=created_quiz['id']), data=group_data) created_group = created_group['quiz_groups'][ 0] # acbart: Weird response type # acbart: Okay because names are strings and IDs are ints group_name_to_id[created_group['name']] = created_group['id'] group_name_to_id[created_group['id']] = created_group['id'] if local_quiz['groups']: print("Created quiz", local_quiz['title'], "groups on canvas") # Create the questions for question in local_quiz['questions']: if 'quiz_group_id' in question and question[ 'quiz_group_id'] is not None: question['quiz_group_id'] = group_name_to_id[ question['quiz_group_id']] question_data = QuizQuestion._make_canvas_upload( registry, question, args) created_question = canvas.api.post( 'quizzes/{quiz_id}/questions'.format( quiz_id=created_quiz['id']), data=question_data) print("Created quiz", local_quiz['title'], "questions on canvas")
def download(cls, registry: Registry, args): if args.all: cls.download_all(registry, args) return canvas = registry.get_service(args.service, "canvas") resource_json = cls.find(canvas, args.title) if resource_json is not None: try: registry.find_resource(canvas.name, cls.name, args.title, "") print("Downloaded new version of {}: ".format(cls.name), args.title) except WaltzException: print("Downloaded new {}:".format(cls.name), args.title) resource_json = json.dumps(resource_json) registry.store_resource(canvas.name, cls.name, args.title, "", resource_json) return resource_json cls.find_similar(registry, canvas, args)
def Upload(args): """ > waltz upload "Programming 37: For Loops" If service/category not in registry database, Then we can go ask all the services if they already know about this thing. """ registry = Registry.load(args.waltz_directory) resource_category = registry.guess_resource_category(args) resource_category.upload(registry, args)
def encode_question_by_title(cls, registry: Registry, title: str, args): local = registry.get_service(args.local_service, 'local') # TODO: By default limit search to "<Quiz> Questions/" folder? source_path = local.find_existing(registry, title, check_front_matter=True, top_directories=args.banks) decoded_markdown = local.read(source_path) regular, waltz, body = extract_front_matter(decoded_markdown) body = hide_data_in_html(regular, m2h(body)) waltz['question_text'] = body return cls.encode_question(registry, waltz, args)
def Encode(args): """ > waltz encode "Programming 37: For Loops" > waltz encode canvas assignment "Programming 37: For Loops" If we found out the resource category, we can include that in the Registry Database. That might also allow us to infer the Service. """ registry = Registry.load(args.waltz_directory) resource_category = registry.guess_resource_category(args) resource_category.encode(registry, args)
def diff_extra_files(cls, registry: Registry, data, args): local = registry.get_service(args.local_service, 'local') for py_filename in ['on_run', 'starting_code', 'on_change', 'on_eval']: try: source_path = local.find_existing(registry, args.title, folder_file=py_filename, extension='.py') except FileNotFoundError: yield py_filename, "" continue print(source_path) yield source_path, local.read(source_path)
def decode(cls, registry: Registry, args): course = registry.find_resource(title=args.title, service=args.service, category=cls.name) data = json.loads(course.data) original_destination = args.destination for group in data['groups']: custom_args = SimpleNamespace(**vars(args)) custom_args.title = group BlockPyGroup.decode(registry, custom_args) for group, problems in data['problems'].items(): for problem in problems: custom_args = SimpleNamespace(**vars(args)) custom_args.title = problem custom_args.destination = group Problem.decode(registry, custom_args)
def List(args): registry = Registry.load(args.waltz_directory) if not args.service: print("The following services are available:") for service_type, services in registry.services.items(): if services: print("\t", service_type+":") for service in services: print("\t\t", service.name) else: print("\t", service_type + ":", "(none configured)") else: service = registry.get_service(args.service) service.list(registry, args) return registry
def Push(args): registry = Registry.load(args.waltz_directory) resource_category = None if len(args.resource) == 1: local = registry.get_service('local', args.local_service) existing_file = local.find_existing(registry, args.resource[0], False, None) _, waltz, _ = extract_front_matter(local.read(existing_file)) if 'resource' in waltz: # TODO: validate resource category resource_category = registry.get_resource_category(waltz['resource']) args.category = waltz['resource'] args.title = args.resource[0] args.service = registry.get_service(resource_category.default_service).name if resource_category is None: resource_category = registry.guess_resource_category(args) resource_category.encode(registry, args) resource_category.upload(registry, args)
def diff_extra_files(cls, registry: Registry, data, args): local = registry.get_service(args.local_service, 'local') regular, waltz, body = extract_front_matter(data) for question in waltz['questions']: if isinstance(question, str): destination_path = local.find_existing( registry, args.title, folder_file=question, check_front_matter=True, top_directories=args.banks) yield destination_path, local.read(destination_path) elif 'group' in question: for inner_question in question['questions']: if isinstance(inner_question, str): destination_path = local.find_existing( registry, args.title, folder_file=inner_question) yield destination_path, local.read(destination_path)
def Download(args): """ > waltz download --filename for_loops.md > waltz download "Programming 37: For Loops" > waltz download assignment "Final Exam" > waltz download canvas "Final Exam" > waltz download canvas assignment "Final Exam" > waltz download canvas --id 234347437743 > waltz download <Name> > waltz download <Service> <Name> > waltz download <Resource> <Name> > waltz download <Service> <Resource> <Name> > waltz download --parameter <Value> > waltz download --all """ registry = Registry.load(args.waltz_directory) resource_category = registry.guess_resource_category(args) resource_category.download(registry, args) return registry
def handle_registry(args, registry): if registry is None: return Registry.load(args.registry_path) return registry
def parse_command_line(args): parser = argparse.ArgumentParser( prog='waltz', description='Sync resources between services for a course') parser.add_argument( '--waltz_directory', type=str, default="./", help= "Path to the main waltz directory with the Waltz registry and DB file." ) subparsers = parser.add_subparsers(help='Available commands') # Init Waltz parser_init = subparsers.add_parser('init', help='Initialize a new Waltz here') parser_init.add_argument( '--directory', "-d", type=str, default="./", help= "The local directory to use for this waltz; defaults to current directory." ) parser_init.add_argument( '--overwrite', "-o", action="store_true", default=False, help="If used, then overwrites the existing waltz registry.") parser_init.set_defaults(func=actions.Init) # Reset Database parser_reset = subparsers.add_parser( 'reset', help='Reset the Waltz database entirely.') parser_reset.set_defaults(func=actions.Reset) # Configure service parser_configure = subparsers.add_parser( 'configure', help='Configure a new instance of the service.') parser_configure_services = parser_configure.add_subparsers( dest='type', help="The type of the service you are configuring.") for name, service_type in defaults.get_service_types().items(): service_type.add_parser_configure(parser_configure_services) parser_configure.set_defaults(func=actions.Configure) # List Services or Resources parser_list = subparsers.add_parser( 'list', help='List available services or resources') parser_list_services = parser_list.add_subparsers( dest='service', help="The service to search within.") for name, service_type in defaults.get_service_types().items(): service_type.add_parser_list(parser_list_services) registry = Registry.load('./', False) if registry is not None: for name, services in registry.services.items(): for service_type in services: service_type.add_parser_list(parser_list_services, service_type.name) parser_list.set_defaults(func=actions.List) # Show [Course|Service] # Search parser_search = subparsers.add_parser('search', help='Search for a resource.') parser_search.add_argument('category', type=str, help="The category of resource to search") parser_search.add_argument('what', type=str, help="The resource to download") parser_search.add_argument( "--service", type=str, help="The specific service to use in case of ambiguity.") parser_search.set_defaults(func=actions.Search) def add_id_and_url(subparser): subparser.add_argument( "--id", help= "A resource-specific ID to disambiguate this resource definitively." ) subparser.add_argument( "--url", help= "A resource-specific URL to disambiguate this resource definitively." ) subparser.add_argument("--all", action='store_true', help="Get all the resources of this type.") # Download parser_download = subparsers.add_parser( 'download', help='Download the raw version of a resource.') parser_download.add_argument( 'resource', nargs='+', type=str, help="The resource to download. Could be a " "resource title, filename, or some combination of those and the service and category." ) add_id_and_url(parser_download) parser_download.set_defaults(func=actions.Download) # Upload parser_upload = subparsers.add_parser( 'upload', help='Upload the raw version of a resource.') parser_upload.add_argument( 'resource', nargs='+', type=str, help="The resource to download. Could be a " "resource title, filename, or some combination of those and the service and category." ) add_id_and_url(parser_upload) parser_upload.set_defaults(func=actions.Upload) # Decode parser_decode = subparsers.add_parser( 'decode', help='Convert a raw resource into a locally editable one.') parser_decode.add_argument( 'resource', nargs='+', type=str, help="The resource to decode. Could be a " "filename, resource title, or some combination of those and the service and category." ) parser_decode.add_argument( "--local_service", type=str, help="The specific local service to use as an override.") parser_decode.add_argument( "--destination", "-d", type=str, help="The destination directory for this resource.") parser_decode.add_argument( "--combine", "-c", action='store_true', default=False, help="Whether to combine all subresources into a single file.") parser_decode.add_argument( "--hide_answers", action='store_true', default=False, help="Whether to hide answers to any questions.") parser_decode.add_argument( "--banks", nargs="*", type=str, help= "The question bank folders to check. First one will be the location for new questions." ) add_id_and_url(parser_decode) # TODO: Allow override of specific local, but otherwise assume default `local`? parser_decode.set_defaults(func=actions.Decode) # Encode parser_encode = subparsers.add_parser( 'encode', help='Convert a locally editable resource into a raw one.') parser_encode.add_argument( 'resource', nargs='+', type=str, help="The resource to encode. Could be a " "filename, resource title, or some combination of those and the service and category." ) parser_encode.add_argument( "--local_service", type=str, help="The specific local service to use as an override.") parser_encode.add_argument("--banks", nargs="*", type=str, help="The question bank folders to check.") add_id_and_url(parser_encode) parser_encode.set_defaults(func=actions.Encode) # Diff parser_diff = subparsers.add_parser( 'diff', help='Compare the remote version of a resource and the local one.') parser_diff.add_argument( 'resource', nargs='+', type=str, help="The resource to diff. Could be a " "filename, resource title, or some combination of those and the service and category." ) parser_diff.add_argument( "--local_service", type=str, help="The specific local service to use as an override.") parser_diff.add_argument( "--console", action="store_true", help="Do not generate HTML file; just print to console.") parser_diff.add_argument( "--prevent_open", action="store_true", help= "Prevent the generated HTML file from being automatically opened in your browser." ) parser_diff.add_argument("--banks", nargs="*", type=str, help="The question bank folders to check.") parser_diff.add_argument( "--combine", "-c", action='store_true', default=False, help="Whether to combine all subresources into a single file.") parser_diff.add_argument("--hide_answers", action='store_true', default=False, help="Whether to hide answers to any questions.") add_id_and_url(parser_diff) parser_diff.set_defaults(func=actions.Diff) # Push parser_push = subparsers.add_parser( 'push', help='Convert a locally editable resource into a raw one and upload it.' ) parser_push.add_argument( 'resource', nargs='+', type=str, help="The resource to encode and upload. Could be a " "filename, resource title, or some combination of those and the service and category." ) parser_push.add_argument( "--local_service", type=str, help="The specific local service to use as an override.") parser_push.add_argument("--banks", nargs="*", type=str, help="The question bank folders to check.") add_id_and_url(parser_push) parser_push.set_defaults(func=actions.Push) # Pull parser_pull = subparsers.add_parser( 'pull', help='Download a raw resource and convert it to a locally editable one.' ) parser_pull.add_argument( 'resource', nargs='+', type=str, help="The resource to download and decode. Could be a " "filename, resource title, or some combination of those and the service and category." ) parser_pull.add_argument( "--local_service", type=str, help="The specific local service to use as an override.") parser_pull.add_argument("--banks", nargs="*", type=str, help="The question bank folders to check.") parser_pull.add_argument( "--combine", "-c", action='store_true', default=False, help="Whether to combine all subresources into a single file.") parser_pull.add_argument("--hide_answers", action='store_true', default=False, help="Whether to hide answers to any questions.") parser_pull.add_argument( "--destination", "-d", type=str, help="The destination directory for this resource.") add_id_and_url(parser_pull) parser_pull.set_defaults(func=actions.Pull) # Extract # Build # Undo # ... Conclude! parsed = parser.parse_args(args) return parsed.func(parsed)
def Pull(args): registry = Registry.load(args.waltz_directory) resource_category = registry.guess_resource_category(args) resource_category.download(registry, args) resource_category.decode(registry, args)
def Diff(args): registry = Registry.load(args.waltz_directory) resource_category = registry.guess_resource_category(args) resource_category.diff(registry, args)
def Reset(args): registry = Registry.load(args.waltz_directory) registry.reset_database() registry.save_to_file() return registry
def Configure(args): registry = Registry.load(args.waltz_directory) new_service = Service.from_type(args.type).configure(args) registry.configure_service(new_service) registry.save_to_file() return registry
def encode_json(cls, registry: Registry, data: str, args): regular, waltz, body = extract_front_matter(data) # Grab out convenient groups visibility = waltz.get('visibility', {}) forked = waltz.get('forked', {}) identity = waltz.get('identity', {}) files = waltz.get('files', {}) # Grab any extra files extra_files = {} local = registry.get_service(args.local_service, 'local') for py_filename in ['on_run', 'starting_code', 'on_change', 'on_eval']: try: source_path = local.find_existing(registry, args.title, folder_file=py_filename, extension='.py') except FileNotFoundError: extra_files[py_filename] = "" continue extra_files[py_filename] = local.read(source_path) collected = {} for special, prepend in cls.SPECIAL_INSTRUCTOR_FILES_R.items(): for file in files.get(special, []): source_path = local.find_existing(registry, args.title, folder_file=file, extension="") collected[prepend + file] = local.read(source_path) if collected: extra_files['extra_instructor_files'] = json.dumps(collected) else: extra_files['extra_instructor_files'] = "" # And generate the rest of the JSON return json.dumps({ "_schema_version": 2, 'url': waltz['title'], 'name': waltz['display title'], 'type': waltz['type'], 'reviewed': waltz.get('human reviewed', False), 'hidden': visibility.get('hide status'), 'public': visibility.get('publicly indexed'), 'ip_ranges': visibility.get('ip ranges', ""), 'settings': json.dumps(waltz['additional settings']) if waltz['additional settings'] else None, 'forked_id': forked.get('id', None), 'forked_version': forked.get('version', None), 'owner_id': identity['owner id'], 'owner_id__email': identity['owner email'], 'course_id': identity['course id'], 'version': identity['version downloaded'], 'date_created': from_friendly_date(identity.get('created')), 'date_modified': from_friendly_date(identity.get('modified')), 'instructions': body, 'extra_starting_files': "", # TODO: Store sample submissions in BlockPy 'sample_submissions': [], # TODO: Store tags in BlockPy 'tags': [], **extra_files # TODO: Other fields })
def decode_json(cls, registry: Registry, data: str, args): raw_data = json.loads(data) result = CommentedMap() result['title'] = raw_data['url'] result['display title'] = raw_data['name'] result['resource'] = cls.name result['type'] = raw_data['type'] if raw_data['reviewed']: result['human reviewed'] = raw_data['reviewed'] result['visibility'] = CommentedMap() result['visibility']['hide status'] = raw_data['hidden'] result['visibility']['publicly indexed'] = raw_data['public'] if raw_data['ip_ranges']: result['visibility']['ip ranges'] = raw_data['ip_ranges'] result['additional settings'] = json.loads(raw_data['settings'] or "{}") if raw_data['forked_id']: result['forked'] = CommentedMap() # TODO: Look up forked's url for more info; or perhaps automatically have it downloaded along? result['forked']['id'] = raw_data['forked_id'] result['forked']['version'] = raw_data['forked_version'] result['identity'] = CommentedMap() result['identity']['owner id'] = raw_data['owner_id'] result['identity']['owner email'] = raw_data['owner_id__email'] result['identity']['course id'] = raw_data['course_id'] result['identity']['version downloaded'] = raw_data['version'] result['identity']['created'] = to_friendly_date_from_datetime( blockpy_string_to_datetime(raw_data['date_created'])) result['identity']['modified'] = to_friendly_date_from_datetime( blockpy_string_to_datetime(raw_data['date_modified'])) # TODO: Tags # TODO: Sample Submissions. Have a "samples/" folder? # TODO: If args.combine, then put it all into one file files_path = raw_data['url'] result['files'] = CommentedMap() result['files']['path'] = files_path result['files']['hidden but accessible files'] = [] result['files']['instructor only files'] = [] result['files']['extra starting files'] = [] result['files']['read-only files'] = [] # Check if index file exists; if so, that's our directory target local = registry.get_service(args.local_service, 'local') try: index_path = local.find_existing(registry, files_path, folder_file=cls.folder_file) files_path = os.path.dirname(index_path) except FileNotFoundError: pass if hasattr(args, 'destination') and args.destination: files_path = os.path.join(args.destination, files_path) # Then build up the extra instructor files extra_files = [(os.path.join(files_path, "on_run.py"), raw_data['on_run']), (os.path.join(files_path, "starting_code.py"), raw_data['starting_code'])] if raw_data['on_change']: extra_files.append( (os.path.join(files_path, "on_change.py"), raw_data['on_change'])) if raw_data['on_eval']: extra_files.append( (os.path.join(files_path, "on_eval.py"), raw_data['on_eval'])) if raw_data['extra_instructor_files']: # TODO: Create special manifest file for listing special file types (e.g., "&" and "?") extra_instructor_files = json.loads( raw_data['extra_instructor_files']) for eif_filename, eif_contents in extra_instructor_files.items(): if eif_filename[0] in "?!^&*": new_path = os.path.join(files_path, eif_filename[1:]) extra_files.append((new_path, eif_contents)) special_file_type = cls.SPECIAL_INSTRUCTOR_FILES[ eif_filename[0]] result['files'][special_file_type].append(new_path) # Put instructions up front and return the result return add_to_front_matter(raw_data['instructions'], result), extra_files