def load_default_config(ipython_dir=None): """Load the default config file from the default ipython_dir. This is useful for embedded shells. """ if ipython_dir is None: ipython_dir = get_ipython_dir() profile_dir = os.path.join(ipython_dir, 'profile_default') config = Config() for cf in Application._load_config_files("ipython_config", path=profile_dir): config.update(cf) return config
def load_default_config(ipython_dir=None): """Load the default config file from the default ipython_dir. This is useful for embedded shells. """ if ipython_dir is None: ipython_dir = get_ipython_dir() profile_dir = os.path.join(ipython_dir, 'profile_default') config = Config() for cf in Application._load_config_files("ipython_config", path=profile_dir): config.update(cf) return config
def __init__(self, course_dir=None, auto=False) -> 'Course': """Initialize a course from a config file. :param course_dir: The directory your course. If none, defaults to current working directory. :type course_dir: str :param auto: Suppress all prompts, automatically answering yes. :type auto: bool :returns: A Course object for performing operations on an entire course at once. :rtype: Course """ #=======================================# # Working Directory & Git Sync # #=======================================# # Set up the working directory. If no course_dir has been specified, then it # is assumed that this is the course directory. self.working_directory = course_dir if course_dir is not None else os.getcwd( ) repo = Repo(self.working_directory) # Before we do ANYTHING, make sure our working directory is clean with no # untracked files! Unless we're running a automated job, in which case we # don't want to fail for an unexpected reason. if (repo.is_dirty() or repo.untracked_files) and (not auto): continue_with_dirty = input(""" Your repository is currently in a dirty state (modifications or untracked changes are present). We strongly suggest that you resolve these before proceeding. Continue? [y/n]:""") # if they didn't say no, exit if continue_with_dirty.lower() != 'y': sys.exit("Exiting...") # PRINT BANNER print( AsciiTable([['Initializing Course and Pulling Instructors Repo'] ]).table) # pull the latest copy of the repo utils.pull_repo(repo_dir=self.working_directory) # Make sure we're running our nbgrader commands within our instructors repo. # this will contain our gradebook database, our source directory, and other # things. config = Config() config.CourseDirectory.root = self.working_directory #=======================================# # Load Config # #=======================================# # Check for an nbgrader config file... if not os.path.exists( os.path.join(self.working_directory, 'nbgrader_config.py')): # if there isn't one, make sure there's at least a rudaux config file if not os.path.exists( os.path.join(self.working_directory, 'rudaux_config.py')): sys.exit(""" You do not have nbgrader_config.py or rudaux_config.py in your current directory. We need at least one of these to set up your course parameters! You can specify a directory with the course_dir argument if you wish. """) # use the traitlets Application class directly to load nbgrader config file. # reference: # https://github.com/jupyter/nbgrader/blob/41f52873c690af716c796a6003d861e493d45fea/nbgrader/server_extensions/validate_assignment/handlers.py#L35-L37 # ._load_config_files() returns a generator, so if the config is missing, # the generator will act similarly to an empty array # load rudaux_config if it exists, otherwise just bring in nbgrader_config. for rudaux_config in Application._load_config_files( 'rudaux_config', path=self.working_directory): config.merge(rudaux_config) for nbgrader_config in Application._load_config_files( 'nbgrader_config', path=self.working_directory): config.merge(nbgrader_config) #=======================================# # Set Config Params # #=======================================# ## NBGRADER PARAMS # If the user set the exchange, perform home user expansion if necessary if config.get('Exchange', {}).get('root') is not None: # perform home user expansion. Should not throw an error, but may try: # expand home user in-place config['Exchange']['root'] = os.path.expanduser( config['Exchange']['root']) except: pass ## CANVAS PARAMS # Before we continue, make sure we have all of the necessary parameters. self.course_id = config.get('Canvas', {}).get('course_id') self.canvas_url = config.get('Canvas', {}).get('canvas_url') self.external_tool_name = config.get('Canvas', {}).get('external_tool_name') self.external_tool_level = config.get('Canvas', {}).get('external_tool_level') # The canvas url should have no trailing slash self.canvas_url = re.sub(r"/$", "", self.canvas_url) ## GITHUB PARAMS self.stu_repo_url = config.get('GitHub', {}).get('stu_repo_url', '') self.assignment_release_path = config.get( 'GitHub', {}).get('assignment_release_path') self.ins_repo_url = config.get('GitHub', {}).get('ins_repo_url') # subpath not currently supported # self.ins_dir_subpath = config.get('GitHub').get('ins_dir_subpath') ## JUPYTERHUB PARAMS self.hub_url = config.get('JupyterHub', {}).get('hub_url') # The hub url should have no trailing slash self.hub_url = re.sub(r"/$", "", self.hub_url) # Get Storage directory & type self.storage_path = config.get('JupyterHub', {}).get('storage_path', ) self.zfs = config.get('JupyterHub', {}).get('zfs') # Optional, default is false! self.zfs_regex = config.get('JupyterHub', {}).get('zfs_regex') # default is false! self.zfs_datetime_pattern = config.get('JupyterHub', {}).get( 'zfs_datetime_pattern') # default is false! # Note hub_prefix, not base_url, to avoid any ambiguity self.hub_prefix = config.get('JupyterHub', {}).get('base_url') # If prefix was set, make sure it has no trailing slash, but a preceding # slash if self.hub_prefix is not None: self.hub_prefix = re.sub(r"/$", "", self.hub_prefix) if re.search(r"^/", self.hub_prefix) is None: self.hub_prefix = fr"/{self.hub_prefix}" ## COURSE PARAMS self.grading_image = config.get('Course', {}).get('grading_image') self.tmp_dir = config.get('Course', {}).get('tmp_dir') assignment_list = config.get('Course', {}).get('assignments') self.course_timezone = config.get('Course', {}).get('timezone') self.system_timezone = pendulum.now(tz='local').timezone.name ## Repurpose the rest of the params for later batches ## (Hang onto them in case we need something) self._full_config = config #=======================================# # Validate URLs (Slightly) # #=======================================# urls = { 'JupyterHub.hub_url': self.hub_url, 'Canvas.canvas_url': self.canvas_url } for key, value in urls.items(): if re.search(r"^https{0,1}", value) is None: sys.exit(f""" You must specify the scheme (e.g. https://) for all URLs. You are missing the scheme in "{key}": {value} """) if re.search(r".git$", value) is not None: sys.exit(f""" Please do not use .git-appended URLs. You have used a .git url in "{key}": {value} """) #=======================================# # Check For Required Params # #=======================================# # Finally, before we continue, make sure all of our required parameters were # specified in the config file(s) required_params = { "Canvas.course_id": self.course_id, "Canvas.canvas_url": self.canvas_url, "GitHub.stu_repo_url": self.stu_repo_url, "GitHub.ins_repo_url": self.ins_repo_url, "JupyterHub.hub_url": self.hub_url, "Course.assignments": assignment_list } # If any are none... if None in required_params.values(): # Figure out which ones are none and let the user know. for key, value in required_params.items(): if value is None: print(f" \"{key}\" is missing.") sys.exit( 'Please make sure you have specified all required parameters in your config file.' ) #=======================================# # Check For Optional Params # #=======================================# # Now look for all of our optional parameters. If any are missing, let the # user know we'll be using the default. optional_params = { "assignment_release_path": { "value": self.assignment_release_path, "default": 'materials', "config_name": "GitHub.assignment_release_path" }, # "assignment_source_path": { # "value": self.assignment_source_path, # "default": "source", # "config_name": "c.GitHub.assignment_source_path" # }, "hub_prefix": { "value": self.hub_prefix, "default": "", "config_name": "JupyterHub.base_url" }, "zfs": { "value": self.zfs, "default": False, "config_name": "JupyterHub.zfs" }, "zfs_regex": { "value": self.zfs_regex, "default": r'\d{4}-\d{2}-\d{2}-\d{4}', "config_name": "JupyterHub.zfs_regex" }, "zfs_datetime_pattern": { "value": self.zfs_datetime_pattern, "default": 'YYYY-MM-DD-HHmm', "config_name": "JupyterHub.zfs_datetime_pattern" }, "course_timezone": { "value": self.course_timezone, "default": 'US/Pacific', "config_name": "Course.timezone" }, "grading_image": { "value": self.grading_image, "default": 'ubcdsci/r-dsci-grading', "config_name": "Course.grading_image" }, "tmp_dir": { "value": self.tmp_dir, "default": os.path.join(Path.home(), 'tmp'), "config_name": "Course.tmp_dir" }, "external_tool_name": { "value": self.external_tool_name, "default": 'Jupyter', "config_name": "Canvas.external_tool_name" }, "external_tool_level": { "value": self.external_tool_level, "default": 'course', "config_name": "Canvas.external_tool_level" } } for key, param in optional_params.items(): if param.get('value') is None: setattr(self, key, param.get('default')) print( f" \"{param.get('config_name')}\" is missing, using default parameter of \"{getattr(self, key)}\"" ) # Make sure no preceding or trailing slashes in assignment release path self.assignment_release_path = re.sub(r"/$", "", self.assignment_release_path) self.assignment_release_path = re.sub(r"^/", "", self.assignment_release_path) # Since we are using the student repo URL for the Launch URLs # (i.e. telling nbgitpuller where to find the notebook), # if the user provided an SSH url, we need the https version as well. self.stu_launch_url = utils.generate_git_urls( self.stu_repo_url).get('plain_https') #! this is cheating a bit, but we can get the repo name this way #! Fix me in the future self.ins_repo_name = os.path.split( utils.generate_git_urls(self.ins_repo_url).get('plain_https'))[1] self.stu_repo_name = os.path.split(self.stu_launch_url)[1] #=======================================# # Set Canvas Token # #=======================================# canvas_token_name = config.get('Canvas').get('token_name') if canvas_token_name is None: print("Searching for default Canvas token, CANVAS_TOKEN...") canvas_token_name = 'CANVAS_TOKEN' self.canvas_token = self._get_token(canvas_token_name) #=======================================# # Finalize Setting Params # #=======================================# # set up the nbgrader api with our merged config files self.nb_api = NbGraderAPI(config=config) # assign init params to object # self.canvas_token = self._get_token(canvas_token_name) # self.course = self._get_course() # Set crontab # self.cron = CronTab(user=True) # We need to use the system crontab because we'll be making ZFS snapshots # which requires elevated permissions self.cron = CronTab(user=True) #=======================================# # Instantiate Assignments # #=======================================# # Subclass assignment for this course: class CourseAssignment(rudaux.Assignment): course = self instantiated_assignments = [] for _assignment in assignment_list: assignment = CourseAssignment( name=_assignment.get('name'), duedate=_assignment.get('duedate'), duetime=_assignment.get( 'duetime', '23:59:59'), # default is 1 sec to midnight points=_assignment.get('points', 0), # default is zero points manual=_assignment.get('manual', False), # default is no manual grading ) instantiated_assignments.append(assignment) self.assignments = instantiated_assignments