PK|'LR!nbcollate/__init__.py"Collate Jupyter classroom assignment and submission notebooks" __version__ = "0.3.1" from .nbcollate import nbcollate, nb_clear_outputs, remove_duplicate_answers, sort_answers, get_answer_tuples PKz'L/ nbcollate/cli.py#!/usr/bin/env python """Create a combined notebook. The first path is the assignment notebook. Remaining paths are student notebooks. """ import argparse import logging import os import sys import nbformat import nbformat.reader import nbcollate as nbc from . import nbcollate def safe_read(nbf): """A wrapper for nbformat.read, that prints a warning and returns None on bad notebooks. """ try: return nbformat.read(nbf, as_version=4) except nbformat.reader.NotJSONError: print('while reading', nbf) def capitalize(s): return s[:1].upper() + s[1:] def collate(master_nb_path, submission_paths, args): """Collate notebooks. Arguments --------- master_nb_path: str The master notebook. submission_paths: [str] A list of notebook file pathnames. """ if args.verbose: logging.basicConfig(format='%(message)s', level=logging.INFO) submission_nbs = [safe_read(nbf) for nbf in submission_paths] submission_nbs = [collated_nb for collated_nb in submission_nbs if collated_nb] master_nb = safe_read(master_nb_path) assert master_nb labels = None if args.label: labels = [capitalize(os.path.splitext(os.path.split(f)[1])[0].replace('-', ' ')) for f in submission_paths] collated_nb = nbcollate(master_nb, submission_nbs, labels=labels) suffix = "-collated" root, ext = os.path.splitext(args.notebook_files[0]) collated_nb_path = "{}{}{}".format(root, suffix, ext) if args.out: collated_nb_path = os.path.join( args.out, os.path.split(collated_nb_path)[1]) if not args.force and os.path.exists(collated_nb_path): # FIXME raise condition; instead open w/ os.O_CREAT | os.O_WRONLY err = FileExistsError() err.filename = collated_nb_path raise err if not args.dry_run: with open(collated_nb_path, 'w') as f: nbformat.write(collated_nb, f) print('wrote', collated_nb_path) def main(args=sys.argv[1:]): "Create a collated notebook." parser = argparse.ArgumentParser(description=__doc__) nb_nargs = '*' if '--version' in args else '+' parser.add_argument('-f', '--force', action='store_true', help="Force overwrite existing file") parser.add_argument('-n', '--dry-run', help="Dry run") parser.add_argument('-o', '--out', type=str, help="Output directory") parser.add_argument('-v', '--verbose', action='store_true') parser.add_argument('--label', action='store_true', help="Label answers by notebook") parser.add_argument('--version', action='store_true') parser.add_argument('notebook_files', nargs=nb_nargs, metavar='NOTEBOOK_FILE') args = parser.parse_args(args) if args.version: print('nbcollate version', nbc.__version__) return if not args.notebook_files: parser.error('the following arguments are required: NOTEBOOK_FILE') master_file, *submission_files = args.notebook_files # Remove the master file from the answer files. This allows the CLI # to be used in the pattern `nbcollate master.ipynb *.ipynb`. if master_file in submission_files: submission_files = [f for f in submission_files if f != master_file] try: collate(master_file, submission_files, args) except FileExistsError: sys.stderr.write("Output file already exists. Repeat with --force to replace it.\n") sys.exit(1) PK'L)* Vnbcollate/nbcollate.py"""Collate an assignment and answer Jupyter notebooks into a single notebook. This script is designed to support active reading. It takes as input a set of Jupyter notebooks as well as some target cells which define a set of reading exercises. The script processes the collection of notebooks and builds a notebook which summarizes the responses to each question. Based on work by Paul Ruvolo. Rewritten by Oliver Steele """ import re # from collections import Iterable, OrderedDict from collections import namedtuple from difflib import SequenceMatcher from itertools import starmap import nbformat # QUESTION_RE = r'#+ (Exercise|Question)' def nb_clear_outputs(nb): "Clear the output cells from a Jupyter notebook." for cell in nb.cells: if 'outputs' in cell: cell['outputs'] = [] def nbcollate(assignment_nb, submission_nbs, *, ids=None, labels=None, clear_outputs=False): """Create a notebook based on assignment_nb, that incorporates answers from student_nbs. Args: assignment_nb: a Jupyter notebook with the assignment submission_nbs: a dict or iterable whose values are notebooks with answers Returns: A Jupyter notebook """ if isinstance(submission_nbs, dict): assert not ids ids = list(submission_nbs.keys()) submission_nbs = list(submission_nbs.values()) def label_cell(s_name): return nbformat.v4.new_markdown_cell(source='**{}**'.format(s_name)) Opcode = namedtuple('opcode', ['op', 'i1', 'i2', 'j1', 'j2']) changes = sorted((oc.i2, i, oc, nb.cells[oc.j1:oc.j2]) for i, nb in enumerate(submission_nbs) for oc in starmap(Opcode, NotebookMatcher(assignment_nb, nb).get_opcodes())) output_cells = assignment_nb.cells[:] di = 0 for _, i, opcode, b_cells in changes: op, i1, i2, j1, j2 = opcode if op in ('insert', 'replace'): i0 = i2 + di b_cells = [c.copy() for c in b_cells] if ids: for c in b_cells: c.metadata = c.metadata.copy() c.metadata.nbcollate_source = ids[i] if labels: b_cells = [label_cell(labels[i])] + b_cells output_cells[i0:i0] = b_cells di += len(b_cells) nb = assignment_nb.copy() nb.cells = [c.copy() for c in output_cells if c.source.strip()] if clear_outputs: nb_clear_outputs(nb) return nb def cell_strings(nb): return [cell.source.strip() for cell in nb.cells] def NotebookMatcher(nb1, nb2): return SequenceMatcher(None, cell_strings(nb1), cell_strings(nb2)) def isections(nb): section = (None, []) for cell in nb.cells: m = re.match(r'^##+\s*(.+)', cell.source) if m: if section[-1]: yield section section = (m.group(1), []) section[-1].append(cell) if section[-1]: yield section def remove_duplicate_answers(nb): dups = [] for _, cells in isections(nb): seen = set() for c in cells: h = c.source.strip() if h in seen: dups.append(c) seen.add(h) for d in dups: nb.cells.remove(d) def sort_answers(nb): dups = [] out = [] for _, cells in isections(nb): out += sorted(cells, key=lambda c: len(c.source.splitlines())) nb.cells = out def get_cell_source_id(cell): return getattr(cell.metadata, 'nbcollate_source', None) def get_answer_tuples(nb): "Return a set of tuples (student_id, prompt_title) of answered prompts." return {(title, get_cell_source_id(c)) for title, cells in isections(nb) for c in cells if get_cell_source_id(c) is not None} PK7LJDXCC!nbcollate-0.3.1.dist-info/LICENSEMIT License Copyright (c) 2016-2017 Paul Ruvolo and Oliver Steele Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HxQPnbcollate-0.3.1.dist-info/WHEEL1 0 RZq+D-Dv;_[*7Fp 8MRq%_:==ߘPT PK!H)Ք "nbcollate-0.3.1.dist-info/METADATAVr6}Wc)ɸIӺVؚHnOHBj@pM ([/=ghEMbr%s8N$״a9ȢTBPv]>=&4Դ9u׭cJA5J5 6+R5,ѴT ͳnዴTMcLl⊗LZkrBn_bIt&\E"/dܢ%'r9g-P:8꼅< g{!ܨnyDZ0&#阬XL,9m/*_RN6jy4,﹌ m.kHǷ?b-w!d=n#Xv^7E_o5 zׄL ю@>FY\2 ,sۖt ([SBF#*U D`̜Ͷg3txF%CHlr;Wu 7mV;JY4Y!4xaZ V.sYSӮuycSe>hfz ѻ,Vo-3e & lu4%gGifIn- ,~alv=]=#ILh- /+mGs~> KhP#O;R*(livFm mK_ĩ +%hTz--E0]PG:H=NS<A02z\%{ +B j`eR] 艹fvQ(jz/P=L%-}_HAO/RJA wA-N$ HRAX9lwF~vEKzl4Qsa*V8QV ;Agx7s&8"p#%{`&[ FX}z|: