From f6579bc93b142cf233728007017d99a8887fd44f Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Sat, 30 May 2020 15:38:26 -0700 Subject: [PATCH 01/35] Add tests for configure_exchange --- .../tests/test_configure_exchange.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 ngshare_exchange/tests/test_configure_exchange.py diff --git a/ngshare_exchange/tests/test_configure_exchange.py b/ngshare_exchange/tests/test_configure_exchange.py new file mode 100644 index 0000000..8d67322 --- /dev/null +++ b/ngshare_exchange/tests/test_configure_exchange.py @@ -0,0 +1,36 @@ +from traitlets.config import Config + +from .. import configure_exchange +from ..collect import ExchangeCollect +from ..exchange import Exchange +from ..fetch_assignment import ExchangeFetchAssignment +from ..fetch_feedback import ExchangeFetchFeedback +from ..list import ExchangeList +from ..release_assignment import ExchangeReleaseAssignment +from ..release_feedback import ExchangeReleaseFeedback +from ..submit import ExchangeSubmit + + +def check_classes(c: Config): + assert c.ExchangeFactory.collect == ExchangeCollect + assert c.ExchangeFactory.exchange == Exchange + assert c.ExchangeFactory.fetch_assignment == ExchangeFetchAssignment + assert c.ExchangeFactory.fetch_feedback == ExchangeFetchFeedback + assert c.ExchangeFactory.list == ExchangeList + assert c.ExchangeFactory.release_assignment == ExchangeReleaseAssignment + assert c.ExchangeFactory.release_feedback == ExchangeReleaseFeedback + assert c.ExchangeFactory.submit == ExchangeSubmit + + +def test(): + url = 'http://ngshare' + c = Config() + configure_exchange.configureExchange(c, url) + check_classes(c) + assert c.ExchangeFactory.exchange.ngshare_url.fget(Exchange) == url + + +def test_no_url(): + c = Config() + configure_exchange.configureExchange(c) + check_classes(c) From 0f82261d93b45e0ef89713a954539f0450c42988 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Sat, 30 May 2020 17:18:28 -0700 Subject: [PATCH 02/35] Increase coverage of collect.py --- ngshare_exchange/collect.py | 7 ------ ngshare_exchange/tests/test_collect.py | 32 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/ngshare_exchange/collect.py b/ngshare_exchange/collect.py index d2dc1c9..24ed485 100644 --- a/ngshare_exchange/collect.py +++ b/ngshare_exchange/collect.py @@ -9,13 +9,6 @@ from nbgrader.utils import parse_utc -# pwd is for matching unix names with student ide, so we shouldn't import it on -# windows machines -if sys.platform != 'win32': - import pwd -else: - pwd = None - def groupby(l, key=lambda x: x): d = defaultdict(list) diff --git a/ngshare_exchange/tests/test_collect.py b/ngshare_exchange/tests/test_collect.py index 38b123f..4815b14 100644 --- a/ngshare_exchange/tests/test_collect.py +++ b/ngshare_exchange/tests/test_collect.py @@ -54,6 +54,21 @@ def _mock_requests_collect(self): ) self.requests_mocker.get(url, json=self._get_submission) + def _mock_requests_error_submission(self): + """ + Mock's ngshare's GET submissions, which responds with the submissions, + and GET submission, which returns an error. + """ + url = '{}/submissions/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + self.requests_mocker.get(url, json=self._get_submissions) + + url = '{}/submission/{}/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id, self.student_id + ) + self.requests_mocker.get(url, status_code=404) + def _mock_requests_subdir(self, subdirectory, subdirectory_file): """ Mock's ngshare's GET submissions, which responds with the submission, @@ -126,6 +141,7 @@ def test_unsuccessful(self): def test_no_course_id(self): """Does collecting without a course id throw an error?""" + self.collect.coursedir.course_id = '' self._mock_requests_collect() with pytest.raises(ExchangeError): self.collect.start() @@ -180,6 +196,17 @@ def test_collect_update(self): with open(timestamp_path, 'r') as timestamp_file: assert timestamp_file.read() == '2002' + self.timestamp_template + def test_collect_update_already_updated(self): + self.num_submissions = 1 + self._mock_requests_collect() + self.collect.start() + timestamp_path = self.submission_dir() / 'timestamp.txt' + mtime1 = os.path.getmtime(timestamp_path) + self.collect.update = True + self.collect.start() + mtime2 = os.path.getmtime(timestamp_path) + assert mtime1 == mtime2 + def test_collect_subdirectories(self): subdir = 'foo' subfile = 'temp.txt' @@ -194,3 +221,8 @@ def test_collect_subdirectories(self): / subdir / subfile ).is_file() + + def test_collect_ngshare_error_submission(self): + self.num_submissions = 1 + self._mock_requests_error_submission() + self.collect.start() From af96af05af4139097e03951787f6ba7b6e94dee9 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Sat, 30 May 2020 22:44:26 -0700 Subject: [PATCH 03/35] Increase test coverage and fix bugs in list.py --- ngshare_exchange/list.py | 10 +- ngshare_exchange/tests/test_list.py | 191 ++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 5 deletions(-) diff --git a/ngshare_exchange/list.py b/ngshare_exchange/list.py index dcfa813..0d94f07 100644 --- a/ngshare_exchange/list.py +++ b/ngshare_exchange/list.py @@ -169,7 +169,7 @@ def _get_submissions(self, assignments, student_id=None): ) if feedback_checksums is None: self.log.error('Failed to check for feedback.') - feedback_checksums = [] + feedback_checksums = {} notebooks = _merge_notebooks_feedback( notebook_ids, feedback_checksums ) @@ -353,16 +353,15 @@ def nb_key(nb): notebooks = sorted(assignment['notebooks'], key=nb_key) else: - notebooks = sorted( - self._get_notebooks( - info['course_id'], info['assignment_id'] - ) + notebooks = self._get_notebooks( + info['course_id'], info['assignment_id'] ) if notebooks is None: self.log.error( 'Failed to get list of assignment ' 'notebooks.' ) notebooks = [] + notebooks = sorted(notebooks) if not notebooks: self.log.warning( @@ -456,6 +455,7 @@ def nb_key(nb): info['notebooks'].append(nb_info) if info['status'] == 'submitted': + assert not self.remove if info['notebooks']: has_local_feedback = all( [nb['has_local_feedback'] for nb in info['notebooks']] diff --git a/ngshare_exchange/tests/test_list.py b/ngshare_exchange/tests/test_list.py index d7a0751..db089be 100644 --- a/ngshare_exchange/tests/test_list.py +++ b/ngshare_exchange/tests/test_list.py @@ -121,6 +121,70 @@ def _get_submission(self, request: PreparedRequest, context): def _get_submissions(self, request: PreparedRequest, context): return self._get_student_submissions(request, context) + def _mock_error_assignment(self): + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + self.requests_mocker.get(url, status_code=404) + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id2, self.assignment_id + ) + self.requests_mocker.get(url, status_code=404) + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id2 + ) + self.requests_mocker.get(url, status_code=404) + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id2, self.assignment_id2 + ) + self.requests_mocker.get(url, status_code=404) + + def _mock_error_assignments(self): + url = '{}/assignments/{}'.format(self.base_url, self.course_id) + self.requests_mocker.get(url, status_code=404) + url = '{}/assignments/{}'.format(self.base_url, self.course_id2) + self.requests_mocker.get(url, status_code=404) + + def _mock_error_feedback(self): + url = '{}/feedback/{}/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id, self.student_id + ) + self.requests_mocker.get(url, status_code=404) + url = '{}/feedback/{}/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id2, self.student_id + ) + self.requests_mocker.get(url, status_code=404) + + def _mock_error_submission(self): + url = '{}/submission/{}/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id, self.student_id + ) + self.requests_mocker.get(url, status_code=404) + + def _mock_error_submissions(self): + url = '{}/submissions/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + self.requests_mocker.get(url, status_code=404) + + url = '{}/submissions/{}/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id, self.student_id + ) + self.requests_mocker.get(url, status_code=404) + + def _mock_error_unrelease(self): + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + self.requests_mocker.delete(url, status_code=404) + + def _mock_no_notebook(self): + url = '{}/submission/{}/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id, self.student_id + ) + json = {'success': True, 'files': []} + self.requests_mocker.get(url, json=json) + def _mock_requests_list(self): """ Mocks ngshare's GET courses, GET assignments, GET assignment, DELETE @@ -267,6 +331,22 @@ def test_unsuccessful(self): except Exception as e: assert issubclass(type(e), ExchangeError) + def test_list_by_student_id_1(self): + self.num_courses = 2 + self.num_assignments = 1 + self.list.coursedir.course_id = self.course_id + self.list.coursedir.student_id = self.student_id + self.list.inbound = True + self.list.start() + + def test_list_by_student_id_2(self): + self.num_courses = 2 + self.num_assignments = 1 + self.list.coursedir.course_id = self.course_id + self.list.coursedir.student_id = '' + self.list.inbound = True + self.list.start() + def test_list_released_2x1_course1(self): self.num_courses = 2 self.num_assignments = 1 @@ -427,6 +507,15 @@ def test_list_fetched(self): ) ) + def test_list_remove_inbound(self): + self.num_assignments = 2 + self.list.coursedir.assignment_id = self.assignment_id + self.list.inbound = True + self.list.remove = True + self.list.start() + assert not self.test_failed + assert not self.test_completed + def test_list_remove_outbound(self): self.num_assignments = 2 self.list.coursedir.assignment_id = self.assignment_id @@ -501,6 +590,33 @@ def test_list_inbound_2(self): ) ) + def test_list_inbound_no_notebooks(self): + self._mock_no_notebook() + self.num_assignments = 1 + self.num_submissions = 1 + self.list.coursedir.assignment_id = self.assignment_id + self.list.inbound = True + self.list.start() + assert ( + self._read_log() + == dedent( + """ + [WARNING] No notebooks found for assignment "{}" in course "{}" + [INFO] Submitted assignments: + [INFO] {} {} {} {} (no feedback available) + """ + ) + .lstrip() + .format( + self.assignment_id, + self.course_id, + self.course_id, + self.student_id, + self.assignment_id, + self.timestamp1, + ) + ) + def test_list_cached_0(self): self.num_assignments = 1 self.is_instructor = False @@ -570,6 +686,19 @@ def test_list_cached_2(self): ) ) + def test_list_not_in_course(self): + tester = self + + class DummyAuthenticator(Authenticator): + def get_student_courses(self, student_id): + return [tester.course_id2] + + self.num_courses = 2 + self.num_assignments = 1 + self.list.coursedir.course_id = self.course_id + self.list.authenticator = DummyAuthenticator() + self.list.start() + def test_list_remove_cached(self): self._submit() self._submit( @@ -988,3 +1117,65 @@ def test_list_feedback_cached_fetched2(self): self.timestamp2, ) ) + + def test_list_path_includes_course(self): + self.num_courses = 2 + self.num_assignments = 1 + self.list.coursedir.course_id = self.course_id + self.list.path_includes_course = True + self.list.start() + + def test_list_ngshare_error_assignment(self): + self._mock_error_assignment() + self.num_courses = 1 + self.num_assignments = 1 + self.list.coursedir.course_id = self.course_id + self.list.start() + + def test_list_ngshare_error_assignments(self): + self._mock_error_assignments() + self.num_courses = 1 + self.num_assignments = 1 + self.list.coursedir.course_id = self.course_id + self.list.start() + + def test_list_ngshare_error_feedback_1(self): + self._mock_error_feedback() + self.num_assignments = 1 + self.num_submissions = 1 + self.list.coursedir.assignment_id = self.assignment_id + self.list.inbound = True + self.list.start() + + def test_list_ngshare_error_feedback_2(self): + self._mock_error_feedback() + self.num_assignments = 1 + self._submit() + self.is_instructor = False + self.list.cached = True + self.list.coursedir.assignment_id = self.assignment_id + self.list.start() + + def test_list_ngshare_error_submission(self): + self._mock_error_submission() + self.num_assignments = 1 + self.num_submissions = 1 + self.list.coursedir.assignment_id = self.assignment_id + self.list.inbound = True + self.list.start() + + def test_list_ngshare_error_submissions(self): + self._mock_error_submissions() + self.num_courses = 2 + self.num_assignments = 1 + self.list.coursedir.course_id = self.course_id + self.list.coursedir.student_id = self.student_id + self.list.inbound = True + self.list.start() + + def test_list_ngshare_error_unrelease(self): + self._mock_error_unrelease() + self.num_assignments = 2 + self.list.coursedir.assignment_id = self.assignment_id + self.list.remove = True + self.list.start() From f37b0b5707eed8c4962b1035d704349d2fd7a348 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Sun, 31 May 2020 13:27:48 -0700 Subject: [PATCH 04/35] Increase coverage of release_feedback.py --- .../tests/test_release_feedback.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/ngshare_exchange/tests/test_release_feedback.py b/ngshare_exchange/tests/test_release_feedback.py index 438542d..218a937 100644 --- a/ngshare_exchange/tests/test_release_feedback.py +++ b/ngshare_exchange/tests/test_release_feedback.py @@ -156,3 +156,36 @@ def test_release_multiple_students(self): self.student_id = 'student_2' self._mock_requests_release() self.release_feedback.start() + + def test_release_exclude_1(self): + self.feedback_file = 'feedback.html' + self.timestamp = 'some_timestamp' + self._mock_requests_release() + self.release_feedback.coursedir.student_id_exclude = 'fake_id' + self.release_feedback.start() + assert not self.test_failed + assert self.test_completed + + def test_release_exclude_2(self): + self.feedback_file = 'feedback.html' + self.timestamp = 'some_timestamp' + self._mock_requests_release() + self.release_feedback.coursedir.student_id_exclude = self.student_id + self.release_feedback.start() + assert not self.test_failed + assert not self.test_completed + + def test_release_bad_assignment_id(self): + self.feedback_file = 'feedback.html' + self.timestamp = 'some_timestamp' + self._mock_requests_release() + old_init_src = self.release_feedback.init_src + + def new_init_src(): + old_init_src() + self.release_feedback.coursedir.assignment_id = 'fake_id' + + self.release_feedback.init_src = new_init_src + self.release_feedback.start() + assert not self.test_failed + assert not self.test_completed From 7609db35d4521b2989900bc116d8de3e352506c8 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Sun, 31 May 2020 16:05:04 -0700 Subject: [PATCH 05/35] Increase coverage of submit.py --- ngshare_exchange/tests/test_submit.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ngshare_exchange/tests/test_submit.py b/ngshare_exchange/tests/test_submit.py index 63048e2..cda8a56 100644 --- a/ngshare_exchange/tests/test_submit.py +++ b/ngshare_exchange/tests/test_submit.py @@ -148,8 +148,11 @@ def _prepare_submission( course_dir, assignment_id=TestExchange.assignment_id, notebook_id=TestExchange.notebook_id, + path_includes_course=False, ): course_dir = Path(course_dir).absolute() + if path_includes_course: + course_dir = course_dir / self.course_id assignment_dir = course_dir / assignment_id os.makedirs(assignment_dir) shutil.copyfile( @@ -366,3 +369,27 @@ def test_submit_file_size(self): self.submit.start() assert not self.test_failed assert self.test_completed + + def test_submit_path_includes_course(self, tmpdir_factory): + self._mock_requests_submit() + self.submit.path_includes_course = True + try: + self.submit.start() + except ExchangeError: + pass + course_dir = Path(tmpdir_factory.mktemp(self.course_id)).absolute() + self._prepare_submission(course_dir, path_includes_course=True) + os.chdir(course_dir) + self.submit.start() + assert not self.test_failed + assert self.test_completed + + def test_submit_student_id(self): + self._mock_requests_submit() + self.submit.coursedir.student_id = self.student_id + try: + self.submit.start() + except ExchangeError: + pass + assert not self.test_failed + assert not self.test_completed From 9e418a78a3699c8d8155010b8445bb2a245ad901 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Sun, 31 May 2020 17:05:00 -0700 Subject: [PATCH 06/35] Increase coverage, fix bugs in release_assignment --- ngshare_exchange/release_assignment.py | 21 +---- .../tests/test_release_assignment.py | 89 ++++++++++++++++++- 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/ngshare_exchange/release_assignment.py b/ngshare_exchange/release_assignment.py index 8feae5f..975a8c2 100644 --- a/ngshare_exchange/release_assignment.py +++ b/ngshare_exchange/release_assignment.py @@ -1,19 +1,5 @@ #!/usr/bin/python import os -import shutil -from stat import ( - S_IRUSR, - S_IWUSR, - S_IXUSR, - S_IRGRP, - S_IWGRP, - S_IXGRP, - S_IROTH, - S_IWOTH, - S_IXOTH, - S_ISGID, - ST_MODE, -) from traitlets import Bool @@ -35,7 +21,7 @@ def _load_config(self, cfg, **kwargs): 'Use ExchangeReleaseAssignment in config, not ExchangeRelease. Outdated config:\n%s', '\n'.join( 'ExchangeRelease.{key} = {value!r}'.format( - key=key, value=svalue + key=key, value=value ) for (key, value) in cfg.ExchangeRelease.items() ), @@ -46,9 +32,6 @@ def _load_config(self, cfg, **kwargs): super(ExchangeReleaseAssignment, self)._load_config(cfg, **kwargs) - def ensure_root(self): - pass - def init_src(self): self.src_path = self.coursedir.format_path( self.coursedir.release_directory, '.', self.coursedir.assignment_id @@ -112,14 +95,12 @@ def assignment_exists(self): self.coursedir.assignment_id ) ) - return True else: self.fail( 'Destination already exists, add --force to overwrite: {} {}'.format( self.coursedir.course_id, self.coursedir.assignment_id ) ) - return True return False diff --git a/ngshare_exchange/tests/test_release_assignment.py b/ngshare_exchange/tests/test_release_assignment.py index 6f51b01..1256927 100644 --- a/ngshare_exchange/tests/test_release_assignment.py +++ b/ngshare_exchange/tests/test_release_assignment.py @@ -25,13 +25,24 @@ def _get_assignments(self, request: PreparedRequest, context): return {'success': True, 'assignments': [self.assignment_id]} return {'success': True, 'assignments': []} + def _mock_error_release(self): + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + self.requests_mocker.post(url, status_code=404) + + def _mock_error_unrelease(self): + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + self.requests_mocker.delete(url, status_code=404) + def _mock_requests_release(self): ''' Mocks ngshare's GET assignments, which responds with no assignments, and POST assignment, which verifies the request. ''' url = '{}/assignments/{}'.format(self.base_url, self.course_id) - print(url) response = {'success': True, 'assignments': []} self.requests_mocker.get(url, json=response) @@ -160,3 +171,79 @@ def test_force_rerelease(self): self.release_assignment.start() assert not self.test_failed assert self.test_completed + + def test_release_old_config(self): + self.released = False + self._mock_requests_release() + old_load_config = ExchangeReleaseAssignment._load_config + + def new_load_config(releaser, cfg): + cfg.ExchangeRelease.force = True + old_load_config(releaser, cfg) + + ExchangeReleaseAssignment._load_config = new_load_config + self.release_assignment = self._new_release_assignment() + assert self.release_assignment.force + assert 'ExchangeRelease' not in self.release_assignment.config + self.release_assignment.start() + + assert not self.test_failed + assert self.test_completed + + def test_release_missing(self): + assignment_dir = self.course_dir / 'release' / self.assignment_id + nb_path = assignment_dir / (self.notebook_id + '.ipynb') + os.remove(nb_path) + os.rmdir(assignment_dir) + + self.released = False + self._mock_requests_release() + try: + self.release_assignment.start() + except ExchangeError: + pass + + assert not self.test_failed + assert not self.test_completed + + def test_release_not_generated(self): + assignment_dir = self.course_dir / 'release' / self.assignment_id + nb_path = assignment_dir / (self.notebook_id + '.ipynb') + os.remove(nb_path) + os.rmdir(assignment_dir) + + assignment_dir = self.course_dir / 'source' / self.assignment_id + assignment_dir.mkdir(parents=True) + + self.released = False + self._mock_requests_release() + try: + self.release_assignment.start() + except ExchangeError: + pass + + assert not self.test_failed + assert not self.test_completed + + def test_release_ngshare_error_release(self): + self._mock_requests_release() + self._mock_error_release() + try: + self.release_assignment.start() + except ExchangeError: + pass + + assert not self.test_failed + assert not self.test_completed + + def test_release_ngshare_error_unrelease(self): + self._mock_requests_force_rerelease() + self._mock_error_unrelease() + self.released = True + try: + self.release_assignment.start() + except ExchangeError: + pass + + assert not self.test_failed + assert not self.test_completed From f041a61258b95952e2ff8e073d0643bd74cd233b Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Sun, 31 May 2020 22:07:58 -0700 Subject: [PATCH 07/35] Increase coverage, fix bugs for fetch_assignment --- ngshare_exchange/exchange.py | 7 +- ngshare_exchange/fetch_assignment.py | 14 ++- .../tests/test_fetch_assignment.py | 94 +++++++++++++++++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/ngshare_exchange/exchange.py b/ngshare_exchange/exchange.py index dc9887b..83c87aa 100644 --- a/ngshare_exchange/exchange.py +++ b/ngshare_exchange/exchange.py @@ -4,6 +4,7 @@ import glob import requests import fnmatch +from pathlib import Path from textwrap import dedent @@ -140,7 +141,7 @@ def _cache_default(self): ), ).tag(config=True) - def decode_dir(self, src_dir, dest_dir, ignore=None): + def decode_dir(self, src_dir, dest_dir, ignore=None, noclobber=False): ''' decode an encoded directory tree and saw the decoded files to des src_dir: en encoded directory tree @@ -151,7 +152,7 @@ def decode_dir(self, src_dir, dest_dir, ignore=None): ''' # check if the destination directory exists if not os.path.exists(dest_dir): - os.mkdir(dest_dir) + Path(dest_dir).mkdir(parents=True) for src_file in src_dir: src_path = src_file['path'] @@ -160,6 +161,8 @@ def decode_dir(self, src_dir, dest_dir, ignore=None): file_name = path_components[1] dest_path = os.path.join(dest_dir, file_name) + if noclobber and os.path.isfile(dest_path): + continue # the file could be in a subdirectory, check if directory exists if not os.path.exists(dir_name) and dir_name != '': subdir = os.path.join(dest_dir, dir_name) diff --git a/ngshare_exchange/fetch_assignment.py b/ngshare_exchange/fetch_assignment.py index 8449ddf..965d89e 100644 --- a/ngshare_exchange/fetch_assignment.py +++ b/ngshare_exchange/fetch_assignment.py @@ -1,14 +1,10 @@ #!/usr/bin/python import os -import shutil - -from traitlets import Bool from nbgrader.exchange.abc import ( ExchangeFetchAssignment as ABCExchangeFetchAssignment, ) from .exchange import Exchange -from nbgrader.utils import check_mode class ExchangeFetchAssignment(Exchange, ABCExchangeFetchAssignment): @@ -25,7 +21,7 @@ def _load_config(self, cfg, **kwargs): ) cfg.ExchangeFetchAssignment.merge(cfg.ExchangeFetch) - del cfg.ExchangeFetchAssignment + del cfg.ExchangeFetch super(ExchangeFetchAssignment, self)._load_config(cfg, **kwargs) @@ -61,9 +57,11 @@ def init_dest(self): def do_copy(self, files): """Copy the src dir to the dest dir omitting the self.coursedir.ignore globs.""" if os.path.isdir(self.dest_path): - self.coursedir.ignore = True self.decode_dir( - files, self.dest_path, ignore=self.ignore_patterns() + files, + self.dest_path, + ignore=self.ignore_patterns(), + noclobber=True, ) else: self.decode_dir(files, self.dest_path) @@ -85,4 +83,4 @@ def copy_files(self): try: self.do_copy(response['files']) except: - self.log.warning('Could not decode the assignment') + self.fail('Could not decode the assignment') diff --git a/ngshare_exchange/tests/test_fetch_assignment.py b/ngshare_exchange/tests/test_fetch_assignment.py index c6598af..5136088 100644 --- a/ngshare_exchange/tests/test_fetch_assignment.py +++ b/ngshare_exchange/tests/test_fetch_assignment.py @@ -1,5 +1,6 @@ import base64 import hashlib +import os from pathlib import Path import shutil @@ -36,6 +37,48 @@ def _mock_requests_fetch(self): response = {'success': True, 'files': files} self.requests_mocker.get(url, json=response) + def _mock_requests_fetch_2(self): + ''' + Mock's ngshare's GET assignment, which responds with the assignment with + two notebooks. + ''' + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + content = None + with open(self.files_path / 'test.ipynb', 'rb') as notebook: + content = notebook.read() + md5 = hashlib.md5() + md5.update(content) + checksum = md5.hexdigest() + content = base64.b64encode(content).decode() + files = [ + { + 'path': self.notebook_id + '.ipynb', + 'content': content, + 'checksum': checksum, + }, + { + 'path': self.notebook_id + '2.ipynb', + 'content': content, + 'checksum': checksum, + }, + ] + response = {'success': True, 'files': files} + self.requests_mocker.get(url, json=response) + + def _mock_requests_fetch_bad_response(self): + ''' + Mock's ngshare's GET assignment, which responds with the assignment with + bad encoding. + ''' + url = '{}/assignment/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id + ) + files = [{'path': self.notebook_id + '.ipynb'}] + response = {'success': True, 'files': files} + self.requests_mocker.get(url, json=response) + def _new_fetch_assignment( self, course_id=TestExchange.course_id, @@ -100,6 +143,7 @@ def test_replace(self): self.fetch_assignment.start() def test_replace_no_overwrite(self): + self._mock_requests_fetch_2() self.fetch_assignment.start() self.fetch_assignment.replace_missing_files = True # Make sure files aren't overwritten. @@ -109,12 +153,19 @@ def test_replace_no_overwrite(self): shutil.copyfile( self.files_path / 'submitted-changed.ipynb', notebook_path ) + notebook_path_2 = ( + self.course_dir + / self.assignment_id + / (self.notebook_id + '2.ipynb') + ) + os.remove(notebook_path_2) with open(notebook_path, 'rb') as file: contents1 = file.read() self.fetch_assignment.start() with open(notebook_path, 'rb') as file: contents2 = file.read() assert contents1 == contents2 + assert notebook_path_2.exists() def test_fetch_multiple_courses(self, tmpdir_factory): self.fetch_assignment.start() @@ -134,3 +185,46 @@ def test_fetch_multiple_courses(self, tmpdir_factory): self.course_dir / self.assignment_id / (self.notebook_id + '.ipynb') ) assert notebook_path.is_file() + + def test_fetch_old_config(self): + old_load_config = ExchangeFetchAssignment._load_config + + def new_load_config(releaser, cfg): + cfg.ExchangeFetch.replace_missing_files = True + old_load_config(releaser, cfg) + + ExchangeFetchAssignment._load_config = new_load_config + self.fetch_assignment = self._new_fetch_assignment() + assert self.fetch_assignment.replace_missing_files + assert 'ExchangeFetch' not in self.fetch_assignment.config + self.fetch_assignment.start() + + def test_fetch_path_includes_course(self): + self.fetch_assignment.path_includes_course = True + self.fetch_assignment.start() + notebook_path = ( + self.course_dir + / self.course_id + / self.assignment_id + / (self.notebook_id + '.ipynb') + ) + assert notebook_path.is_file() + with open(self.files_path / 'test.ipynb', 'rb') as reference_file, open( + notebook_path, 'rb' + ) as actual_file: + assert actual_file.read() == reference_file.read() + + def test_fetch_no_course_access(self): + def new_has_access(student_id, course_id): + return False + + self.fetch_assignment.authenticator.has_access = new_has_access + with pytest.raises(ExchangeError): + self.fetch_assignment.start() + + def test_fetch_ngshare_bad_response(self): + self._mock_requests_fetch_bad_response() + try: + self.fetch_assignment.start() + except ExchangeError: + pass From d4d0949e1dec01f94042a214afca53a3a20e6d3b Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Mon, 1 Jun 2020 10:19:07 -0700 Subject: [PATCH 08/35] Increase coverage, fix bugs for fetch_feedback.py --- ngshare_exchange/fetch_feedback.py | 27 +---- ngshare_exchange/tests/test_fetch_feedback.py | 100 +++++++++++++++--- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/ngshare_exchange/fetch_feedback.py b/ngshare_exchange/fetch_feedback.py index 17d29e6..8e6e6d0 100644 --- a/ngshare_exchange/fetch_feedback.py +++ b/ngshare_exchange/fetch_feedback.py @@ -1,42 +1,19 @@ #!/usr/bin/python import os -import shutil import glob +from pathlib import Path from nbgrader.exchange.abc import ( ExchangeFetchFeedback as ABCExchangeFetchFeedback, ) from .exchange import Exchange -from nbgrader.utils import ( - check_mode, - notebook_hash, - make_unique_key, - get_username, -) -from nbgrader.utils import parse_utc - class ExchangeFetchFeedback(Exchange, ABCExchangeFetchFeedback): def init_src(self): if self.coursedir.course_id == '': self.fail('No course id specified. Re-run with --course flag.') - if self.coursedir.student_id != '*': - - # An explicit student id has been specified on the command line; we use it as student_id - if ( - '*' in self.coursedir.student_id - or '+' in self.coursedir.student_id - ): - self.fail( - "The student ID should contain no '*' nor '+'; got {}".format( - self.coursedir.student_id - ) - ) - student_id = self.coursedir.student_id - else: - student_id = get_username() self.cache_path = os.path.join(self.cache, self.coursedir.course_id) assignment_id = ( self.coursedir.assignment_id @@ -71,7 +48,7 @@ def init_dest(self): # check if feedback folder exists if not os.path.exists(self.dest_path): - os.mkdir(self.dest_path) + Path(self.dest_path).mkdir(parents=True) def copy_files(self): self.log.info('Fetching feedback from server') diff --git a/ngshare_exchange/tests/test_fetch_feedback.py b/ngshare_exchange/tests/test_fetch_feedback.py index ace3574..d69222c 100644 --- a/ngshare_exchange/tests/test_fetch_feedback.py +++ b/ngshare_exchange/tests/test_fetch_feedback.py @@ -15,6 +15,24 @@ class TestExchangeFetchFeedback(TestExchange): timestamp = 'some_timestamp' + def _mock_bad_feedback(self): + ''' + Mock's ngshare's GET feedback, which responds with a bad response. + ''' + + url = '{}/feedback/{}/{}/{}'.format( + self.base_url, self.course_id, self.assignment_id, self.student_id + ) + + files = [{'path': self.notebook_id + '.html'}] + + response = { + 'success': True, + 'timestamp': self.timestamp, + 'files': files, + } + self.requests_mocker.get(url, json=response) + def _mock_requests_fetch(self): ''' Mock's ngshare's GET feedback, which responds with the feedback file. @@ -58,8 +76,7 @@ def _new_fetch_feedback( ) class DummyAuthenticator(Authenticator): - def has_access(self, student_id, course_id): - return True + pass retvalue.authenticator = DummyAuthenticator() retvalue.assignment_dir = str(self.course_dir.absolute()) @@ -73,10 +90,30 @@ def init_fetch_feedback(self, tmpdir_factory): self._mock_requests_fetch() def test_404(self): + submission_name = '{}+{}+{}'.format( + self.student_id, self.assignment_id, self.timestamp + ) + timestamp_path = self.cache_dir / self.course_id / submission_name + os.makedirs(timestamp_path) + + timestamp_file = timestamp_path / 'timestamp.txt' + with open(timestamp_file, 'w') as f: + f.write(self.timestamp) + self.mock_404() self.fetch_feedback.start() def test_unsuccessful(self): + submission_name = '{}+{}+{}'.format( + self.student_id, self.assignment_id, self.timestamp + ) + timestamp_path = self.cache_dir / self.course_id / submission_name + os.makedirs(timestamp_path) + + timestamp_file = timestamp_path / 'timestamp.txt' + with open(timestamp_file, 'w') as f: + f.write(self.timestamp) + self.mock_unsuccessful() self.fetch_feedback.start() @@ -87,13 +124,6 @@ def test_no_course_id(self): except Exception as e: assert issubclass(type(e), ExchangeError) - def test_bad_student_id(self): - self.fetch_feedback.coursedir.student_id = '***' - try: - self.fetch_feedback.start() - except Exception as e: - assert issubclass(type(e), ExchangeError) - def test_fetch(self): # set chache folder @@ -122,11 +152,35 @@ def test_fetch(self): with open(feedback_path, 'rb') as actual_file: assert actual_file.read() == reference_file.read() - def test_wrong_student_id(self): - self.fetch_feedback.coursedir.student_id = 'xx+xx' + def test_fetch_path_includes_course(self): - with pytest.raises(ExchangeError): - self.fetch_feedback.start() + # set chache folder + + submission_name = '{}+{}+{}'.format( + self.student_id, self.assignment_id, self.timestamp + ) + timestamp_path = self.cache_dir / self.course_id / submission_name + os.makedirs(timestamp_path) + + timestamp_file = timestamp_path / 'timestamp.txt' + with open(timestamp_file, 'w') as f: + f.write(self.timestamp) + + self.fetch_feedback.path_includes_course = True + self.fetch_feedback.start() + + feedback_path = ( + self.course_dir + / self.course_id + / self.assignment_id + / 'feedback' + / self.timestamp + / (self.notebook_id + '.html') + ) + assert feedback_path.is_file() + with open(self.files_path / 'feedback.html', 'rb') as reference_file: + with open(feedback_path, 'rb') as actual_file: + assert actual_file.read() == reference_file.read() def test_fetch_multiple_feedback(self): timestamp1 = 'timestamp1' @@ -218,3 +272,23 @@ def test_fetch_multiple_courses(self, tmpdir_factory): / (self.notebook_id + '.html') ) assert feedback_path.is_file() + + def test_fetch_no_submissions(self): + self.fetch_feedback.start() + + def test_fetch_nghsare_bad_feedback(self): + submission_name = '{}+{}+{}'.format( + self.student_id, self.assignment_id, self.timestamp + ) + timestamp_path = self.cache_dir / self.course_id / submission_name + os.makedirs(timestamp_path) + + timestamp_file = timestamp_path / 'timestamp.txt' + with open(timestamp_file, 'w') as f: + f.write(self.timestamp) + + self._mock_bad_feedback() + try: + self.fetch_feedback.start() + except ExchangeError: + pass From 5cc52ada8797b92cb7723da5f74d3982b0296f76 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Mon, 1 Jun 2020 13:10:39 -0700 Subject: [PATCH 09/35] Increase coverage and fix bugs for exchange.py --- ngshare_exchange/exchange.py | 30 +--- ngshare_exchange/tests/test_exchange.py | 176 ++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 27 deletions(-) create mode 100644 ngshare_exchange/tests/test_exchange.py diff --git a/ngshare_exchange/exchange.py b/ngshare_exchange/exchange.py index 83c87aa..7bbdb4b 100644 --- a/ngshare_exchange/exchange.py +++ b/ngshare_exchange/exchange.py @@ -194,7 +194,9 @@ def encode_dir(self, src_dir, ignore=None): # check if you have a subdir sub_dir = subdir.split(os.sep)[-1] if sub_dir != self.coursedir.assignment_id: - file_path = sub_dir + os.sep + file_name + file_path = os.path.join( + os.path.relpath(subdir, src_dir), file_name + ) else: file_path = file_name @@ -260,32 +262,6 @@ def do_copy(self, src, dest, log=None): log=self.log, ), ) - # copytree copies access mode too - so we must add go+rw back to it if - # we are in groupshared. - if self.coursedir.groupshared: - for dirname, _, filenames in os.walk(dest): - # dirs become ug+rwx - st_mode = os.stat(dirname).st_mode - if st_mode & 0o2770 != 0o2770: - try: - os.chmod(dirname, (st_mode | 0o2770) & 0o2777) - except PermissionError: - self.log.warning( - "Could not update permissions of %s to make it groupshared", - dirname, - ) - - for filename in filenames: - filename = os.path.join(dirname, filename) - st_mode = os.stat(filename).st_mode - if st_mode & 0o660 != 0o660: - try: - os.chmod(filename, (st_mode | 0o660) & 0o777) - except PermissionError: - self.log.warning( - "Could not update permissions of %s to make it groupshared", - filename, - ) def ignore_patterns(self): """ diff --git a/ngshare_exchange/tests/test_exchange.py b/ngshare_exchange/tests/test_exchange.py new file mode 100644 index 0000000..691f93f --- /dev/null +++ b/ngshare_exchange/tests/test_exchange.py @@ -0,0 +1,176 @@ +import json +import logging +from pathlib import Path +import os +from shutil import copyfile + +from jupyter_core.paths import jupyter_data_dir +from nbgrader.exchange import ExchangeError +import requests +from requests import PreparedRequest +from _pytest.logging import LogCaptureFixture +import pytest + +from .. import Exchange +from .base import TestExchange + + +default_cache = None + + +@pytest.fixture(scope='module', autouse=True) +def get_default_cache(): + global default_cache + default_cache = Exchange.cache.get(Exchange()) + + +class TestExchangeClass(TestExchange): + def _get_encoded_tree(self): + return [ + {'path': 'problem1.ipynb', 'content': 'abcdefgh'}, + {'path': 'ignore_me.ipynb', 'content': 'ijklmnop'}, + ] + + def _get_response(self, status_code, json): + url = 'http://_get_response' + self.requests_mocker.get(url, status_code=status_code, json=json) + response = requests.get(url) + return response + + def _new_exchange( + self, + course_id=TestExchange.course_id, + assignment_id=TestExchange.assignment_id, + student_id=TestExchange.student_id, + ): + return self._new_exchange_object( + Exchange, course_id, assignment_id, student_id + ) + + @pytest.fixture(autouse=True) + def init_exchange(self): + env_proxy = 'PROXY_PUBLIC_SERVICE_HOST' + if env_proxy in os.environ: + del os.environ[env_proxy] + if not hasattr(self, 'default_cache'): + self.default_cache = Exchange.cache + self.exchange = self._new_exchange() + os.chdir(self.course_dir) + + def test_ngshare_url(self): + url = 'http://some.random.url' + self.exchange._ngshare_url = url + assert self.exchange.ngshare_url == url + + def test_proxy_public_url(self): + os.environ['PROXY_PUBLIC_SERVICE_HOST'] = 'http://proxy-public' + self.exchange._ngshare_url = '' + url = self.exchange.ngshare_url + assert url == 'http://proxy-public/services/ngshare' + + def test_no_ngshare_url(self): + self.exchange._ngshare_url = '' + with pytest.raises(Exception): + self.exchange.ngshare_url + + def test_check_response_good(self): + url = self.exchange.ngshare_url + response = self._get_response(200, {'success': True}) + assert response.json() == self.exchange._ngshare_api_check_error( + response, url + ) + + def test_check_response_bad_status(self): + url = self.exchange.ngshare_url + response = self._get_response(400, {'success': True}) + assert response.json() == self.exchange._ngshare_api_check_error( + response, url + ) + + def test_check_response_unsuccessful(self): + url = self.exchange.ngshare_url + response = self._get_response(200, {'success': False}) + assert None is self.exchange._ngshare_api_check_error(response, url) + + def test_check_response_bad_status_unsuccessful(self): + url = self.exchange.ngshare_url + response = self._get_response(400, {'success': False}) + assert None is self.exchange._ngshare_api_check_error(response, url) + + def test_ngshare_headers(self): + token = 'unique_token' + os.environ['JUPYTERHUB_API_TOKEN'] = token + + def request_handler(request: PreparedRequest, context): + assert 'Authorization' in request.headers + assert request.headers['Authorization'] == 'token ' + token + return {'success': True, 'passed': True} + + url = self.exchange.ngshare_url + self.requests_mocker.get(url, json=request_handler) + response = self.exchange.ngshare_api_get('') + assert 'passed' in response + + def test_ngshare_exception(self): + url = self.exchange.ngshare_url + self.requests_mocker.get(url, exc=requests.exceptions.ConnectionError) + response = self.exchange.ngshare_api_get('') + assert response is None + + def test_default_cache_dir(self): + dir = default_cache + assert Path(dir) == Path(jupyter_data_dir()) / 'nbgrader_cache' + + def test_decode_ignore(self): + src = self._get_encoded_tree() + ignore_patterns = self.exchange.ignore_patterns() + self.exchange.coursedir.ignore.append('ignore_me*') + self.exchange.decode_dir(src, self.course_dir, ignore=ignore_patterns) + assert (self.course_dir / 'problem1.ipynb').exists() + assert not (self.course_dir / 'ignore_me.ipynb').exists() + + def test_decode_no_ignore(self): + src = self._get_encoded_tree() + self.exchange.coursedir.ignore.append('ignore_me*') + self.exchange.decode_dir(src, self.course_dir, ignore=None) + assert (self.course_dir / 'problem1.ipynb').exists() + assert (self.course_dir / 'ignore_me.ipynb').exists() + + def test_encode_subdir(self): + assignment_dir = self.course_dir / self.assignment_id + nb_path = assignment_dir / 'path' / 'to' / 'notebook' / 'nb.ipynb' + nb_path.parent.mkdir(parents=True) + test_path = Path(__file__).parent / 'files' / 'test.ipynb' + copyfile(test_path, nb_path) + encoded = json.loads(self.exchange.encode_dir(assignment_dir)['files']) + assert encoded[0]['path'] == 'path/to/notebook/nb.ipynb' + + def test_not_implemented(self): + with pytest.raises(NotImplementedError): + self.exchange.copy_files() + with pytest.raises(NotImplementedError): + self.exchange.init_dest() + with pytest.raises(NotImplementedError): + self.exchange.init_src() + + def test_assignment_not_found(self, caplog: LogCaptureFixture): + def read_log(caplog): + log_records = [ + '[{}] {}\n'.format(x.levelname, x.getMessage()) + for x in caplog.get_records('call') + ] + caplog.clear() + return ''.join(log_records) + + dummy_path = self.course_dir / 'dummy' + real_path = self.course_dir / self.assignment_id + dummy_path.mkdir() + real_path.mkdir() + caplog.set_level(logging.ERROR) + self.exchange.src_path = str(dummy_path) + + try: + self.exchange._assignment_not_found(str(dummy_path), str(real_path)) + except ExchangeError: + pass + assert 0 <= read_log(caplog).find('Did you mean: {}'.format(real_path)) From eac69838c036d3cc6910e8003eb5f4f61ca25b57 Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Mon, 1 Jun 2020 23:18:52 -0700 Subject: [PATCH 10/35] Increase coverage, fix bugs in course_management --- ngshare_exchange/course_management.py | 10 +- .../tests/files/empty_gradebook.db | Bin 0 -> 155648 bytes ngshare_exchange/tests/files/gradebook.db | Bin 0 -> 155648 bytes .../tests/test_course_management.py | 320 +++++++++++++++++- 4 files changed, 319 insertions(+), 11 deletions(-) create mode 100644 ngshare_exchange/tests/files/empty_gradebook.db create mode 100644 ngshare_exchange/tests/files/gradebook.db diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index 8a2ac63..95b1bb8 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -108,7 +108,9 @@ def delete(url, data): try: response = requests.delete(url, data=data, headers=header) - response.raise_for_status + response.raise_for_status() + except requests.exceptions.ConnectionError: + prRed('Could not establish connection to ngshare server') except Exception: check_status_code(response) @@ -190,7 +192,9 @@ def add_students(args): student_dict = {} student_id = row[cols_dict['student_id']] if len(student_id.replace(' ', '')) == 0: - prRed('Student ID cannot be empty (row {})'.format(i + 1)) + prRed( + 'Student ID cannot be empty (row {})'.format(i + 1), False + ) continue first_name = row[cols_dict['first_name']] last_name = row[cols_dict['last_name']] @@ -225,7 +229,7 @@ def add_students(args): else: prRed( 'There was an error adding {} to {}: {}'.format( - user, course_id, s['message'] + user, args.course_id, s['message'] ), False, ) diff --git a/ngshare_exchange/tests/files/empty_gradebook.db b/ngshare_exchange/tests/files/empty_gradebook.db new file mode 100644 index 0000000000000000000000000000000000000000..79a4b2b4f26feb6fab42f51fa808c4d7a2258e0f GIT binary patch literal 155648 zcmeI(&2QW09l&uhjzn9wWw&XlV-N{(l0st5kLR|1Q0*~0R#|0009ILK;Y#IT$~(tGM#++Ig=v- z2q1s}0tg_000IagfB*sr#77|Q6ovJFd;&sA5I_I{1Q0*~0R#|0009ILh(Lhn{}G^) zB?1T_fB*srAb1Q0*~0R#|0009ILKmY**A`syF{}G^)B?1T_fB*sr zAb#31o-}c%mPAj5I_I{1Q0*~0R#|0009ILh(Lhn{}G^)B?1T_fB*srAb1Q0*~0R#|0009ILKmY**A`syF{}G^)B?1T_fB*srAb3hH`cBo@(M>}u8P#fgV`6Qqkl!w-+xZVS3Tk_|x{w~Jl+~Z) zx7Kduw-#2Gvubg3TP@z**jQB4BXzxIv=8Q%-|INqu#BBXV^2FG2kX)OAcxhfStr+z zx7I((Z+)uXE_|vkRLbtYyT$cS?iSPq=ZcGJJ;-@cb+T~N+}PYItlum;=PiU%WYw+0 zjlx!;xK_B+KGV%Ooy|?Al)|i(sMO2GL-W_wie+f}zSVHwYi$N?xxM*HZsK^S^RjNcyp+UmcK2$A>q z4v6;AjWKWxN8E1EPH6XxX5FZo_l;7}ny)TjbB`Sr;8trCvOale)7f{@DP?V@Uz~gf zZQ0)%eP`rYC*=GS(uawptjx_xPj0&{F(`Skl@jcO{ohKzyl<3s%Me;lsj=U*yWvph z*wup$vhTuf=P9(I)?TkMtjHX;|xf|KOhkgQBiNe8R0b*m7q z-)9A5zj(q>^FkK(Ad${qIh#^GnCkbzS{KoBt=HtiElF0Ur=`Q)R+rrB483mSj8MFG zUvhN$?p2zmrFAtzr+HQNen*U&Ua1Cc&g*}xHB;L+jb>-vaPsP`66x&HnUs>B?)SO< z4l?I$4B69(VP{G)K0K5-H}ua$=7*W&_*Y|>M*lJT+w_C<^^xC={9^d}@M!YC^55h? zoZU!%mHfS(j2{FLI7I@#nwOO;SEPf{)<{=1YCDyZ_Lo}+`cqeJ~ z{aU-P3NHn+haanMZ~F9s*VWgYIc=%ozRU}mdpie>rojY1d_|`Nbe1jQj?tUKIJ0)m z&wRV^vu!7BO|S3k)vn#0eW;VvUtoCI3u}z7Vb-5d*yRug+F-8dr|3Cp^{yeD+FmVq z!}QXnqk47kgKEp(##>8s$4PFFer`y<_he~CRuo10;(a$@yG^}pxW7~(SglIz>qT|p>Y|#<+H;%iY)bi&(r=7+4YXzV>73o0xl@6aH;&0!g=`=3cq+pnlm2*bm-F zDVy1T0W%xbear5KeOB@P^ueVmS;=OlgU?-B@u>Lj4S`rZHh}qCNu{%^ucwst`F@`; zptEaD=*`sOMMYNT=cT7ZLC@1-2&H;m_*9_-13uDsq2{bzNGU(f^>ghWVZyg6{82|) zJO6^2gl<##nOu01eN9$!IqBKsRyAzjw(?VS7v{VWxbEWv;ukC)dyMe@ReKz|SJiiS zjk4+u15Pup+RJUtEr5NG&2DM;4SUjKJqkvVUe({91cWZNs0xMZnhJs|-d&OVgtRk1DvR~=9Zx|Yx{HULx`rM;jk2Q4d;BqbqAb%$ zNV}030R#|0009ILKmY**5I_I{1X?a|LkzF13<-j!?EH+9oSUDYn#x^K zlAH50b2HPKY_o6wE9&<5|MC6*Ee{z7KmY**5I_I{1Q0*~0R#|0phW`a_y0G$LtxMU zp9}HlEh+@rB7gt_2q1s}0tg_000IagfI#a6E}U2yYWlzb6?MM;dfB*srAbXmYfkl009ILKmY**5I_I{ z1Q2K!0p9;_mq<_!1Q0*~0R#|0009ILKmY**S|`B!|E;qnX9N&H009ILKmY**5I_I{ z1lmP__y5}^5|jf01Q0*~0R#|0009ILKmdW(3Gn`Z>ukvx0R#|0009ILKmY**5I_Kd zb`j{0zYqo^UkL;MjsIyNGw|oW?*=COU-Ulg`(xj)quaeRv42Fqie8C^LVt)ni+tMs zH1tVG>6+|b6>o}Pi>Jg>?Q$M3w;b^o;lI4x9LWs<1Q0*~f&ZJp-ta&~I&(&R{Gg&{ z3R*_hwRBc16simH#r4#}MoQjTcz-!1S69p9v1mRg|8!w}@y5dX_;fNMudZ##tGAbz zC*)YPq!zX6MkRS=-_~+P%aqI8=>u}8Jhz2T2HMmrfyaDv@(t*l)-nuxQ7cvIt?LWKq+W_9;qkKH7XnjRuk7ZG1N*m_5i~bd@1Ce*vHi`DZjnQ-TN8D)8W@xvy zosw42?`m1EHBTjHtZfGc*lvx1tdAesSmNDSSXvxy6epJ2gThXwkL!l z(%G}(t7|*^>xP-v{wfhmOm>B(g^@<*?RJogv(RmfL3+$_NPkba zaI*XF!oZIPLj7O#o$LKu?{8xFVppTTjsCLdYEN(IKasyiet%*)^hM}*Mlv1H`zrrbAhGv>Z1tv3 z?m1n3(Hzre%huCaAaiGBv#Y5$!1o`~>;TPai@#%ZhA`%+J?&<`k^1?DnYO5wcGW`7 z>ivDFnbe(NIN1m07&Vt!cRXQ~BXFVh#(Hjwx`S5d7=c}@vjyieJ$de+Ufns-bkbQk zKY8{r$&IU@6_Ou3o*a!xk|cgMZv|{?N6l&0FF6n_dsUM;;PFDGXUjX9d~JDc!5^u8 zYmF(#j+)NyXt{jFOIy;koSt@KYZvkTwYBBc!fIf0+Wt{C>)qmw)Z$HJUR!%kORMtu z)P$@gjImAPOj!D{)aV*-8Kjfe)49ak&66rt(z8ZyW(@_V-#Tb?S5ftC>t=zTR=nlW zy^#@XDE&mVs*tzETY4){nY{H)8h=r}pY~ORKv1ow6S!%x@1{e&sQV|3`&w+U*;xTu zTfMchzOb~qA=|FrA)Q!xE6fmG%$9T7#R>UhQQg_jmG6}hBH$e21El1BI#?3Iw+X?m<<(yfk%(1HPjLh6L{V z>P}Ao}wZJc1MTVkG1T!?ilkHf<;jgKmY**5I_I{1Q0*~0R#{@ zrU3K*W5Tf;0tg_000IagfB*srAb Date: Tue, 2 Jun 2020 09:35:34 -0700 Subject: [PATCH 11/35] Remove unused class in test_fetch_feedback.py --- ngshare_exchange/tests/test_fetch_feedback.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ngshare_exchange/tests/test_fetch_feedback.py b/ngshare_exchange/tests/test_fetch_feedback.py index d69222c..da70aa4 100644 --- a/ngshare_exchange/tests/test_fetch_feedback.py +++ b/ngshare_exchange/tests/test_fetch_feedback.py @@ -75,10 +75,7 @@ def _new_fetch_feedback( ExchangeFetchFeedback, course_id, assignment_id, student_id ) - class DummyAuthenticator(Authenticator): - pass - - retvalue.authenticator = DummyAuthenticator() + retvalue.authenticator = Authenticator() retvalue.assignment_dir = str(self.course_dir.absolute()) return retvalue From f207dbb5beb845d477150fbb00899dbdcd9f697f Mon Sep 17 00:00:00 2001 From: Lawrence Lee Date: Tue, 2 Jun 2020 09:42:38 -0700 Subject: [PATCH 12/35] Add .coveragerc --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..98a10ed --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = ngshare_exchange/tests/* + +[report] +precision = 2 From c656444544f6eb89bfc0e7729362a85e194b27de Mon Sep 17 00:00:00 2001 From: Lawrence37 <45837045+Lawrence37@users.noreply.github.com> Date: Tue, 2 Jun 2020 14:16:25 -0700 Subject: [PATCH 13/35] did some changes Make strings use single quotes and remove an unnecessary line of code. Co-authored-by: aalmanza1998 <52981614+aalmanza1998@users.noreply.github.com> --- ngshare_exchange/list.py | 1 - ngshare_exchange/tests/test_course_management.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ngshare_exchange/list.py b/ngshare_exchange/list.py index 0d94f07..ce3065f 100644 --- a/ngshare_exchange/list.py +++ b/ngshare_exchange/list.py @@ -455,7 +455,6 @@ def nb_key(nb): info['notebooks'].append(nb_info) if info['status'] == 'submitted': - assert not self.remove if info['notebooks']: has_local_feedback = all( [nb['has_local_feedback'] for nb in info['notebooks']] diff --git a/ngshare_exchange/tests/test_course_management.py b/ngshare_exchange/tests/test_course_management.py index b4b0b7b..86d5984 100644 --- a/ngshare_exchange/tests/test_course_management.py +++ b/ngshare_exchange/tests/test_course_management.py @@ -307,9 +307,9 @@ def test_add_students_db(self, capsys, tmpdir_factory): with tempfile.NamedTemporaryFile() as f: f.writelines( [ - b"student_id,first_name,last_name,email\n", - b"sid1,jane,doe,jd@mail.com\n", - b"sid2,john,perez,jp@mail.com\n", + b'student_id,first_name,last_name,email\n', + b'sid1,jane,doe,jd@mail.com\n', + b'sid2,john,perez,jp@mail.com\n', ] ) f.flush() @@ -376,9 +376,9 @@ def test_add_students_unsuccessful(self, capsys, tmp_path): with tempfile.NamedTemporaryFile() as f: f.writelines( [ - b"student_id,first_name,last_name,email\n", - b"sid1,jane,doe,jd@mail.com\n", - b"sid2,john,perez,jp@mail.com\n", + b'student_id,first_name,last_name,email\n', + b'sid1,jane,doe,jd@mail.com\n', + b'sid2,john,perez,jp@mail.com\n', ] ) f.flush() @@ -526,9 +526,9 @@ def test_add_students_parsing(self, capsys): self._mock_add_students() with tempfile.NamedTemporaryFile() as f: f.write(b'student_id,first_name,last_name,email\n') - f.write(b"sid1,jane,doe,jd@mail.com\n") + f.write(b'sid1,jane,doe,jd@mail.com\n') f.write(b',jane,doe,jd@mail.com\n') - f.write(b"sid2,john,perez,jp@mail.com\n") + f.write(b'sid2,john,perez,jp@mail.com\n') f.flush() cm.main(['add_students', self.course_id, f.name, '--no-gb']) From a1cbea287c2655c1d503235fb546da3e52488d13 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 15:06:56 -0700 Subject: [PATCH 14/35] use urllib.parse.quote to create url --- ngshare_exchange/fetch_assignment.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ngshare_exchange/fetch_assignment.py b/ngshare_exchange/fetch_assignment.py index 965d89e..73bf7e1 100644 --- a/ngshare_exchange/fetch_assignment.py +++ b/ngshare_exchange/fetch_assignment.py @@ -1,5 +1,6 @@ #!/usr/bin/python import os +from urllib.parse import quote from nbgrader.exchange.abc import ( ExchangeFetchAssignment as ABCExchangeFetchAssignment, @@ -33,9 +34,11 @@ def init_src(self): ): self.fail('You do not have access to this course.') - self.src_path = '/assignment/{}/{}'.format( + + url = '/assignment/{}/{}'.format( self.coursedir.course_id, self.coursedir.assignment_id ) + self.src_path = quote(url, safe='/', encoding=None, errors=None) def init_dest(self): if self.path_includes_course: From d7beb30ead9d56d9be438a286fab68f6e804d790 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 15:15:21 -0700 Subject: [PATCH 15/35] fix style --- ngshare_exchange/fetch_assignment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ngshare_exchange/fetch_assignment.py b/ngshare_exchange/fetch_assignment.py index 73bf7e1..d4b1e17 100644 --- a/ngshare_exchange/fetch_assignment.py +++ b/ngshare_exchange/fetch_assignment.py @@ -34,7 +34,6 @@ def init_src(self): ): self.fail('You do not have access to this course.') - url = '/assignment/{}/{}'.format( self.coursedir.course_id, self.coursedir.assignment_id ) From 5a07e85e68038d860572b34c7dcb11f3bf835941 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 15:27:41 -0700 Subject: [PATCH 16/35] encode urls --- ngshare_exchange/exchange.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ngshare_exchange/exchange.py b/ngshare_exchange/exchange.py index 7bbdb4b..172f71b 100644 --- a/ngshare_exchange/exchange.py +++ b/ngshare_exchange/exchange.py @@ -5,6 +5,7 @@ import requests import fnmatch from pathlib import Path +from urllib.parse import quote from textwrap import dedent @@ -36,6 +37,7 @@ class Exchange(ABCExchange): @property def ngshare_url(self): + if self._ngshare_url: return self._ngshare_url if 'PROXY_PUBLIC_SERVICE_HOST' in os.environ: @@ -101,14 +103,20 @@ def ngshare_api_request(self, method, url, data=None, params=None): return None return self._ngshare_api_check_error(response, url) + def encode_url(self, url) + return quote(url, safe='/', encoding=None, errors=None) + def ngshare_api_get(self, url, params=None): - return self.ngshare_api_request('GET', url, params=params) + encoded_url = self.encode_url(url) + return self.ngshare_api_request('GET', encoded_url, params=params) def ngshare_api_post(self, url, data, params=None): - return self.ngshare_api_request('POST', url, data=data, params=params) + encoded_url = self.encode_url(url) + return self.ngshare_api_request('POST', encoded_url, data=data, params=params) def ngshare_api_delete(self, url, params=None): - return self.ngshare_api_request('DELETE', url, params=params) + encoded_url = self.encode_url(url) + return self.ngshare_api_request('DELETE', encoded_url, params=params) assignment_dir = Unicode( '.', From 5593ba0722b761b22fb60916d240240ce428b851 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 15:28:56 -0700 Subject: [PATCH 17/35] fix typo --- ngshare_exchange/exchange.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ngshare_exchange/exchange.py b/ngshare_exchange/exchange.py index 172f71b..eca5a05 100644 --- a/ngshare_exchange/exchange.py +++ b/ngshare_exchange/exchange.py @@ -103,7 +103,7 @@ def ngshare_api_request(self, method, url, data=None, params=None): return None return self._ngshare_api_check_error(response, url) - def encode_url(self, url) + def encode_url(self, url): return quote(url, safe='/', encoding=None, errors=None) def ngshare_api_get(self, url, params=None): @@ -112,7 +112,9 @@ def ngshare_api_get(self, url, params=None): def ngshare_api_post(self, url, data, params=None): encoded_url = self.encode_url(url) - return self.ngshare_api_request('POST', encoded_url, data=data, params=params) + return self.ngshare_api_request( + 'POST', encoded_url, data=data, params=params + ) def ngshare_api_delete(self, url, params=None): encoded_url = self.encode_url(url) From ea8761518b547e57f15bcad5c53b309b62da7c75 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 15:43:46 -0700 Subject: [PATCH 18/35] use single quotes consistently --- ngshare_exchange/collect.py | 36 ++++++------ ngshare_exchange/course_management.py | 2 +- ngshare_exchange/exchange.py | 44 +++++++-------- ngshare_exchange/fetch_assignment.py | 6 +- ngshare_exchange/list.py | 76 +++++++++++++------------- ngshare_exchange/release_assignment.py | 6 +- ngshare_exchange/release_feedback.py | 24 ++++---- ngshare_exchange/submit.py | 38 ++++++------- 8 files changed, 115 insertions(+), 117 deletions(-) diff --git a/ngshare_exchange/collect.py b/ngshare_exchange/collect.py index 24ed485..62fe540 100644 --- a/ngshare_exchange/collect.py +++ b/ngshare_exchange/collect.py @@ -19,15 +19,15 @@ def groupby(l, key=lambda x: x): class ExchangeCollect(Exchange, ABCExchangeCollect): def _get_submission(self, course_id, assignment_id, student_id): - """ + ''' Returns the student's submission. A submission is a dictionary - containing a "timestamp" and "files" list. Each file in the list is a - dictionary containing the "path" relative to the assignment root and the - "content" as an ASCII representation of the base64 encoded bytes. - """ + containing a 'timestamp' and 'files' list. Each file in the list is a + dictionary containing the 'path' relative to the assignment root and the + 'content' as an ASCII representation of the base64 encoded bytes. + ''' response = self.ngshare_api_get( - "/submission/{}/{}/{}".format(course_id, assignment_id, student_id) + '/submission/{}/{}/{}'.format(course_id, assignment_id, student_id) ) if response is None: self.log.error('An error occurred downloading a submission.') @@ -44,10 +44,10 @@ def _get_submission(self, course_id, assignment_id, student_id): return {'timestamp': timestamp, 'files': files} def _get_submission_list(self, course_id, assignment_id): - """ + ''' Returns a list of submission entries. Each entry is a dictionary - containing the "student_id" and "timestamp". - """ + containing the 'student_id' and 'timestamp'. + ''' response = self.ngshare_api_get( '/submissions/{}/{}'.format(course_id, assignment_id) ) @@ -67,7 +67,7 @@ def _sort_by_timestamp(self, records): def init_src(self): if self.coursedir.course_id == '': - self.fail("No course id specified. Re-run with --course flag.") + self.fail('No course id specified. Re-run with --course flag.') records = self._get_submission_list( self.coursedir.course_id, self.coursedir.assignment_id @@ -85,13 +85,13 @@ def init_dest(self): def copy_files(self): if len(self.src_records) == 0: self.log.warning( - "No submissions of '{}' for course '{}' to collect".format( + 'No submissions of "{}" for course "{}" to collect'.format( self.coursedir.assignment_id, self.coursedir.course_id ) ) else: self.log.info( - "Processing {} submissions of '{}' for course '{}'".format( + 'Processing {} submissions of "{}" for course "{}"'.format( len(self.src_records), self.coursedir.assignment_id, self.coursedir.course_id, @@ -128,14 +128,14 @@ def copy_files(self): if copy: if updating: self.log.info( - "Updating submission: {} {}".format( + 'Updating submission: {} {}'.format( student_id, self.coursedir.assignment_id ) ) shutil.rmtree(dest_path) else: self.log.info( - "Collecting submission: {} {}".format( + 'Collecting submission: {} {}'.format( student_id, self.coursedir.assignment_id ) ) @@ -150,19 +150,19 @@ def copy_files(self): else: if self.update: self.log.info( - "No newer submission to collect: {} {}".format( + 'No newer submission to collect: {} {}'.format( student_id, self.coursedir.assignment_id ) ) else: self.log.info( - "Submission already exists, use --update to update: {} {}".format( + 'Submission already exists, use --update to update: {} {}'.format( student_id, self.coursedir.assignment_id ) ) def do_copy(self, src, dest): - """ + ''' Repurposed version of Exchange.do_copy. - """ + ''' self.decode_dir(src, dest) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index 95b1bb8..bdc4816 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -51,7 +51,7 @@ def ngshare_url(): return _ngshare_url except Exception as e: prRed( - "Cannot determine ngshare URL. Please check your nbgrader_config.py!", + 'Cannot determine ngshare URL. Please check your nbgrader_config.py!', False, ) prRed(e) diff --git a/ngshare_exchange/exchange.py b/ngshare_exchange/exchange.py index eca5a05..36edc36 100644 --- a/ngshare_exchange/exchange.py +++ b/ngshare_exchange/exchange.py @@ -29,9 +29,9 @@ class Exchange(ABCExchange): _ngshare_url = Unicode( help=dedent( - """ + ''' Override default ngshare URL - """ + ''' ), ).tag(config=True) @@ -43,16 +43,16 @@ def ngshare_url(self): if 'PROXY_PUBLIC_SERVICE_HOST' in os.environ: # we are in a kubernetes environment, so dns based service discovery should work # assuming the service is called ngshare, which it should - return "http://proxy-public/services/ngshare" + return 'http://proxy-public/services/ngshare' else: raise ValueError( - "ngshare url not configured in a non-k8s environment! Please configure the URL manually in nbgrader_config.py" + 'ngshare url not configured in a non-k8s environment! Please configure the URL manually in nbgrader_config.py' ) def _ngshare_api_check_error(self, response, url): if response.status_code != requests.codes.ok: self.log.error( - "ngshare service returned invalid status code %d.", + 'ngshare service returned invalid status code %d.', response.status_code, ) @@ -60,7 +60,7 @@ def _ngshare_api_check_error(self, response, url): response = response.json() except Exception: self.log.exception( - "ngshare service returned non-JSON content: '%s'.", + 'ngshare service returned non-JSON content: "%s".', response.text, ) return None @@ -68,12 +68,12 @@ def _ngshare_api_check_error(self, response, url): if not response['success']: if 'message' not in response: self.log.error( - "ngshare endpoint %s returned failure without an error message.", + 'ngshare endpoint %s returned failure without an error message.', url, ) else: self.log.error( - "ngshare endpoint %s returned failure: %s", + 'ngshare endpoint %s returned failure: %s', url, response['message'], ) @@ -123,10 +123,10 @@ def ngshare_api_delete(self, url, params=None): assignment_dir = Unicode( '.', help=dedent( - """ + ''' Local path for storing student assignments. Defaults to '.' which is normally Jupyter's notebook_dir. - """ + ''' ), ).tag(config=True) @@ -142,12 +142,12 @@ def _cache_default(self): path_includes_course = Bool( False, help=dedent( - """ + ''' Whether the path for fetching/submitting assignments should be prefixed with the course name. If this is `False`, then the path will be something like `./ps1`. If this is `True`, then the path will be something like `./course123/ps1`. - """ + ''' ), ).tag(config=True) @@ -220,17 +220,17 @@ def encode_dir(self, src_dir, ignore=None): return dir_tree def init_src(self): - """Compute and check the source paths for the transfer.""" + '''Compute and check the source paths for the transfer.''' raise NotImplementedError def init_dest(self): - """Compute and check the destination paths for the transfer.""" + '''Compute and check the destination paths for the transfer.''' raise NotImplementedError def copy_files(self): - """Actually do the file transfer.""" + '''Actually do the file transfer.''' raise NotImplementedError @@ -256,12 +256,12 @@ def _assignment_not_found(self, src_path, other_path): raise ExchangeError(msg) def do_copy(self, src, dest, log=None): - """ + ''' Copy the src dir to the dest dir, omitting excluded file/directories, non included files, and too large files, as specified by the options coursedir.ignore, coursedir.include and coursedir.max_file_size. - """ + ''' shutil.copytree( src, dest, @@ -274,7 +274,7 @@ def do_copy(self, src, dest, log=None): ) def ignore_patterns(self): - """ + ''' Returns a function which decides whether or not a file should be ignored. The function has the signature ignore_patterns(directory, filename, filesize) -> bool @@ -284,7 +284,7 @@ def ignore_patterns(self): will be ignored. If self.coursedir.include exists, filenames not matching the patterns will be ignored. If self.coursedir.max_file_size exists, files exceeding that size in kilobytes will be ignored. - """ + ''' exclude = self.coursedir.ignore include = self.coursedir.include max_file_size = self.coursedir.max_file_size @@ -297,7 +297,7 @@ def ignore_patterns(directory, filename, filesize): ): if log: log.debug( - "Ignoring excluded file '{}' (see config option " + 'Ignoring excluded file "{}" (see config option ' 'CourseDirectory.ignore)'.format(fullname) ) return True @@ -306,14 +306,14 @@ def ignore_patterns(directory, filename, filesize): ): if log: log.debug( - "Ignoring non included file '{}' (see config " + 'Ignoring non included file "{}" (see config ' 'option CourseDirectory.include)'.format(fullname) ) return True elif max_file_size and filesize > 1000 * max_file_size: if log: log.warning( - "Ignoring file too large '{}' (see config " + 'Ignoring file too large "{}" (see config ' 'option CourseDirectory.max_file_size)'.format(fullname) ) return True diff --git a/ngshare_exchange/fetch_assignment.py b/ngshare_exchange/fetch_assignment.py index d4b1e17..ff2e19b 100644 --- a/ngshare_exchange/fetch_assignment.py +++ b/ngshare_exchange/fetch_assignment.py @@ -1,6 +1,5 @@ #!/usr/bin/python import os -from urllib.parse import quote from nbgrader.exchange.abc import ( ExchangeFetchAssignment as ABCExchangeFetchAssignment, @@ -34,10 +33,9 @@ def init_src(self): ): self.fail('You do not have access to this course.') - url = '/assignment/{}/{}'.format( + self.src_path = '/assignment/{}/{}'.format( self.coursedir.course_id, self.coursedir.assignment_id ) - self.src_path = quote(url, safe='/', encoding=None, errors=None) def init_dest(self): if self.path_includes_course: @@ -57,7 +55,7 @@ def init_dest(self): ) def do_copy(self, files): - """Copy the src dir to the dest dir omitting the self.coursedir.ignore globs.""" + '''Copy the src dir to the dest dir omitting the self.coursedir.ignore globs.''' if os.path.isdir(self.dest_path): self.decode_dir( files, diff --git a/ngshare_exchange/list.py b/ngshare_exchange/list.py index ce3065f..39f6e49 100644 --- a/ngshare_exchange/list.py +++ b/ngshare_exchange/list.py @@ -15,12 +15,12 @@ def _checksum(path): def _merge_notebooks_feedback(notebook_ids, checksums): - """ - Returns a list of dictionaries with "notebook_id" and "feedback_checksum". + ''' + Returns a list of dictionaries with 'notebook_id' and 'feedback_checksum'. ``notebook_ids`` - A list of notebook IDs. ``checksum`` - A dictionary mapping notebook IDs to checksums. - """ + ''' merged = [] for nb_id in notebook_ids: if nb_id not in checksums.keys(): @@ -32,10 +32,10 @@ def _merge_notebooks_feedback(notebook_ids, checksums): def _parse_notebook_id(path, extension='.ipynb'): - """ + ''' Returns the notebook_id from the path. If the path is not a file with the extension, returns None. - """ + ''' split_name = os.path.splitext(os.path.split(path)[1]) if split_name[1] == extension: return split_name[0] @@ -44,12 +44,12 @@ def _parse_notebook_id(path, extension='.ipynb'): class ExchangeList(Exchange, ABCExchangeList): def _get_assignments(self, course_ids): - """ + ''' Returns a list of assignments. Each assignment is a dictionary containing the course_id and assignment_id. ``course_ids`` - A list of course IDs. - """ + ''' assignments = [] for course_id in course_ids: response = self.ngshare_api_get('/assignments/{}'.format(course_id)) @@ -69,9 +69,9 @@ def _get_assignments(self, course_ids): return assignments def _get_courses(self): - """ + ''' Returns a list of course_ids. - """ + ''' response = self.ngshare_api_get('/courses') if response is None: @@ -81,11 +81,11 @@ def _get_courses(self): def _get_feedback_checksums( self, course_id, assignment_id, student_id, timestamp ): - """ + ''' Returns the checksums of all feedback files for a specific submission. This is a dictionary mapping all notebook_ids to the feedback file's checksum. - """ + ''' url = '/feedback/{}/{}/{}'.format(course_id, assignment_id, student_id) params = {'list_only': 'true', 'timestamp': timestamp} @@ -102,9 +102,9 @@ def _get_feedback_checksums( return checksums def _get_notebooks(self, course_id, assignment_id): - """ + ''' Returns a list of notebook_ids from the assignment. - """ + ''' url = '/assignment/{}/{}'.format(course_id, assignment_id) params = {'list_only': 'true'} @@ -118,17 +118,17 @@ def _get_notebooks(self, course_id, assignment_id): ] def _get_submissions(self, assignments, student_id=None): - """ + ''' Returns a list of submissions. Each submission is a dictionary - containing the "course_id", "assignment_id", "student_id", "timestamp" - and a list of "notebooks". Each notebook is a dictionary containing a - "notebook_id" and "feedback_checksum". + containing the 'course_id', 'assignment_id', 'student_id', 'timestamp' + and a list of 'notebooks'. Each notebook is a dictionary containing a + 'notebook_id' and 'feedback_checksum'. - ``assignments`` - A list of dictionaries containing "course_id" and - "assignment_id". + ``assignments`` - A list of dictionaries containing 'course_id' and + 'assignment_id'. ``student_id`` - Used to specify a specific student's submissions to get. If None, submissions from all students are fetched if permitted. - """ + ''' submissions = [] for assignment in assignments: course_id = assignment['course_id'] @@ -188,9 +188,9 @@ def _get_submissions(self, assignments, student_id=None): def _get_submission_notebooks( self, course_id, assignment_id, student_id, timestamp ): - """ + ''' Returns a list of notebook_ids from a submission. - """ + ''' url = '/submission/{}/{}/{}'.format( course_id, assignment_id, student_id ) @@ -209,9 +209,9 @@ def _get_submission_notebooks( return notebooks def _unrelease_assignment(self, course_id, assignment_id): - """ + ''' Unrelease a released assignment. - """ + ''' url = '/assignment/{}/{}'.format(course_id, assignment_id) return self.ngshare_api_delete(url) @@ -271,34 +271,34 @@ def parse_assignment(self, assignment): 'timestamp': assignment['timestamp'], } elif self.cached: - regexp = r".*/(?P.*)/(?P.*)\+(?P.*)\+(?P.*)" + regexp = r'.*/(?P.*)/(?P.*)\+(?P.*)\+(?P.*)' else: return assignment m = re.match(regexp, assignment) if m is None: raise RuntimeError( - "Could not match '%s' with regexp '%s'", assignment, regexp + 'Could not match "%s" with regexp "%s"', assignment, regexp ) return m.groupdict() def format_inbound_assignment(self, info): - msg = "{course_id} {student_id} {assignment_id} {timestamp}".format( + msg = '{course_id} {student_id} {assignment_id} {timestamp}'.format( **info ) if info['status'] == 'submitted': if info['has_local_feedback'] and not info['feedback_updated']: - msg += " (feedback already fetched)" + msg += ' (feedback already fetched)' elif info['has_exchange_feedback']: - msg += " (feedback ready to be fetched)" + msg += ' (feedback ready to be fetched)' else: - msg += " (no feedback available)" + msg += ' (no feedback available)' return msg def format_outbound_assignment(self, info): - msg = "{course_id} {assignment_id}".format(**info) + msg = '{course_id} {assignment_id}'.format(**info) if os.path.exists(info['assignment_id']): - msg += " (already downloaded)" + msg += ' (already downloaded)' return msg def copy_files(self): @@ -517,32 +517,32 @@ def nb_key(nb): return assignments def list_files(self): - """List files.""" + '''List files.''' assignments = self.parse_assignments() if self.inbound or self.cached: - self.log.info("Submitted assignments:") + self.log.info('Submitted assignments:') for assignment in assignments: for info in assignment['submissions']: self.log.info(self.format_inbound_assignment(info)) else: - self.log.info("Released assignments:") + self.log.info('Released assignments:') for info in assignments: self.log.info(self.format_outbound_assignment(info)) return assignments def remove_files(self): - """List and remove files.""" + '''List and remove files.''' assignments = self.parse_assignments() if self.inbound or self.cached: - self.log.info("Removing submitted assignments:") + self.log.info('Removing submitted assignments:') for assignment in assignments: for info in assignment['submissions']: self.log.info(self.format_inbound_assignment(info)) else: - self.log.info("Removing released assignments:") + self.log.info('Removing released assignments:') for info in assignments: self.log.info(self.format_outbound_assignment(info)) diff --git a/ngshare_exchange/release_assignment.py b/ngshare_exchange/release_assignment.py index 975a8c2..33a511a 100644 --- a/ngshare_exchange/release_assignment.py +++ b/ngshare_exchange/release_assignment.py @@ -47,7 +47,7 @@ def init_src(self): # Looks like the instructor forgot to assign self.fail( - "Assignment found in '{}' but not '{}', run `nbgrader generate_assignment` first.".format( + 'Assignment found in "{}" but not "{}", run `nbgrader generate_assignment` first.'.format( source, self.src_path ) ) @@ -61,7 +61,7 @@ def init_src(self): def init_dest(self): if self.coursedir.course_id == '': - self.fail("No course id specified. Re-run with --course flag.") + self.fail('No course id specified. Re-run with --course flag.') self.dest_path = '/assignment/{}/{}'.format( self.coursedir.course_id, self.coursedir.assignment_id ) @@ -81,7 +81,7 @@ def assignment_exists(self): if self.coursedir.assignment_id in response['assignments']: if self.force: self.log.info( - "Overwriting files: {} {}".format( + 'Overwriting files: {} {}'.format( self.coursedir.course_id, self.coursedir.assignment_id ) ) diff --git a/ngshare_exchange/release_feedback.py b/ngshare_exchange/release_feedback.py index f1fc0c1..8e00250 100644 --- a/ngshare_exchange/release_feedback.py +++ b/ngshare_exchange/release_feedback.py @@ -23,7 +23,7 @@ def init_src(self): def init_dest(self): if self.coursedir.course_id == '': - self.fail("No course id specified. Re-run with --course flag.") + self.fail('No course id specified. Re-run with --course flag.') def copy_files(self): if self.coursedir.student_id_exclude: @@ -32,23 +32,23 @@ def copy_files(self): exclude_students = set() staged_feedback = {} # Maps student IDs to submissions. - html_files = glob.glob(os.path.join(self.src_path, "*.html")) + html_files = glob.glob(os.path.join(self.src_path, '*.html')) for html_file in html_files: regexp = re.escape(os.path.sep).join( [ self.coursedir.format_path( self.coursedir.feedback_directory, - "(?P.*)", + '(?P.*)', self.coursedir.assignment_id, escape=True, ), - "(?P.*).html", + '(?P.*).html', ] ) m = re.match(regexp, html_file) if m is None: - msg = "Could not match '%s' with regexp '%s'" % ( + msg = 'Could not match "%s" with regexp "%s"' % ( html_file, regexp, ) @@ -59,7 +59,7 @@ def copy_files(self): student_id = gd['student_id'] notebook_id = gd['notebook_id'] if student_id in exclude_students: - self.log.debug("Skipping student '{}'".format(student_id)) + self.log.debug('Skipping student "{}"'.format(student_id)) continue feedback_dir = os.path.split(html_file)[0] @@ -79,8 +79,8 @@ def copy_files(self): for student_id, submission in staged_feedback.items(): # Student. for timestamp, feedback_info in submission.items(): # Submission. self.log.info( - "Releasing feedback for student '{}' on " - "assignment '{}/{}/{}' ({})".format( + 'Releasing feedback for student "{}" on ' + 'assignment "{}/{}/{}" ({})'.format( student_id, self.coursedir.course_id, self.coursedir.assignment_id, @@ -97,12 +97,12 @@ def copy_files(self): self.log.info('Feedback released.') def post_feedback(self, student_id, timestamp, feedback_info): - """ + ''' Uploads feedback files for a specific submission. ``feedback_info`` - A list of feedback files. Each feedback file is - represented as a dictionary with a "path" to the local feedback file and - "notebook_id" of the corresponding notebook. - """ + represented as a dictionary with a 'path' to the local feedback file and + 'notebook_id' of the corresponding notebook. + ''' url = '/feedback/{}/{}/{}'.format( self.coursedir.course_id, self.coursedir.assignment_id, student_id ) diff --git a/ngshare_exchange/submit.py b/ngshare_exchange/submit.py index c45fdd2..f1aa065 100644 --- a/ngshare_exchange/submit.py +++ b/ngshare_exchange/submit.py @@ -9,9 +9,9 @@ class ExchangeSubmit(Exchange, ABCExchangeSubmit): def _get_assignment_notebooks(self, course_id, assignment_id): - """ + ''' Returns a list of relative paths for all files in the assignment. - """ + ''' url = '/assignment/{}/{}'.format(course_id, assignment_id) params = {'list_only': 'true'} @@ -30,10 +30,10 @@ def init_src(self): root = os.path.join( self.coursedir.course_id, self.coursedir.assignment_id ) - other_path = os.path.join(self.coursedir.course_id, "*") + other_path = os.path.join(self.coursedir.course_id, '*') else: root = self.coursedir.assignment_id - other_path = "*" + other_path = '*' self.src_path = os.path.abspath(os.path.join(self.assignment_dir, root)) self.coursedir.assignment_id = os.path.split(self.src_path)[-1] if not os.path.isdir(self.src_path): @@ -43,7 +43,7 @@ def init_src(self): def init_dest(self): if self.coursedir.course_id == '': - self.fail("No course id specified. Re-run with --course flag.") + self.fail('No course id specified. Re-run with --course flag.') self.cache_path = os.path.join(self.cache, self.coursedir.course_id) if self.coursedir.student_id != '*': @@ -68,36 +68,36 @@ def check_filename_diff(self): release_diff = list() for filename in released_notebooks: if filename in submitted_notebooks: - release_diff.append("{}: {}".format(filename, 'FOUND')) + release_diff.append('{}: {}'.format(filename, 'FOUND')) else: missing = True - release_diff.append("{}: {}".format(filename, 'MISSING')) + release_diff.append('{}: {}'.format(filename, 'MISSING')) # Look for extra notebooks in submitted notebooks extra = False submitted_diff = list() for filename in submitted_notebooks: if filename in released_notebooks: - submitted_diff.append("{}: {}".format(filename, 'OK')) + submitted_diff.append('{}: {}'.format(filename, 'OK')) else: extra = True - submitted_diff.append("{}: {}".format(filename, 'EXTRA')) + submitted_diff.append('{}: {}'.format(filename, 'EXTRA')) if missing or extra: - diff_msg = "Expected:\n\t{}\nSubmitted:\n\t{}".format( + diff_msg = 'Expected:\n\t{}\nSubmitted:\n\t{}'.format( '\n\t'.join(release_diff), '\n\t'.join(submitted_diff), ) if missing and self.strict: self.fail( - "Assignment {} not submitted. " - "There are missing notebooks for the submission:\n{}" - "".format(self.coursedir.assignment_id, diff_msg) + 'Assignment {} not submitted. ' + 'There are missing notebooks for the submission:\n{}' + ''.format(self.coursedir.assignment_id, diff_msg) ) else: self.log.warning( - "Possible missing notebooks and/or extra notebooks " - "submitted for assignment {}:\n{}" - "".format(self.coursedir.assignment_id, diff_msg) + 'Possible missing notebooks and/or extra notebooks ' + 'submitted for assignment {}:\n{}' + ''.format(self.coursedir.assignment_id, diff_msg) ) def post_submission(self, src_path): @@ -112,7 +112,7 @@ def post_submission(self, src_path): return response['timestamp'] def copy_files(self): - self.log.info("Source: {}".format(self.src_path)) + self.log.info('Source: {}'.format(self.src_path)) # copy to the real location self.check_filename_diff() @@ -131,11 +131,11 @@ def copy_files(self): if not os.path.isdir(self.cache_path): os.makedirs(self.cache_path) self.do_copy(self.src_path, cache_path) - with open(os.path.join(cache_path, "timestamp.txt"), "w") as fh: + with open(os.path.join(cache_path, 'timestamp.txt'), 'w') as fh: fh.write(self.timestamp) self.log.info( - "Submitted as: {} {} {}".format( + 'Submitted as: {} {} {}'.format( self.coursedir.course_id, self.coursedir.assignment_id, str(self.timestamp), From 0af7c6c3f13a6bce9a97293463d65b2b4b7f8370 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 15:45:36 -0700 Subject: [PATCH 19/35] avoid in line comments --- ngshare_exchange/release_feedback.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ngshare_exchange/release_feedback.py b/ngshare_exchange/release_feedback.py index 8e00250..2c3d461 100644 --- a/ngshare_exchange/release_feedback.py +++ b/ngshare_exchange/release_feedback.py @@ -69,15 +69,18 @@ def copy_files(self): timestamp = timestamp_file.read() if student_id not in staged_feedback.keys(): - staged_feedback[student_id] = {} # Maps timestamp to feedback. + # Maps timestamp to feedback. + staged_feedback[student_id] = {} if timestamp not in staged_feedback[student_id].keys(): - staged_feedback[student_id][timestamp] = [] # List of info. + # List of info. + staged_feedback[student_id][timestamp] = [] staged_feedback[student_id][timestamp].append( {'notebook_id': notebook_id, 'path': html_file} ) - - for student_id, submission in staged_feedback.items(): # Student. - for timestamp, feedback_info in submission.items(): # Submission. + # Student. + for student_id, submission in staged_feedback.items(): + # Submission. + for timestamp, feedback_info in submission.items(): self.log.info( 'Releasing feedback for student "{}" on ' 'assignment "{}/{}/{}" ({})'.format( From 33ddb30b600af41c551ecaa2eb7ceed66b405ba3 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 16:02:59 -0700 Subject: [PATCH 20/35] more clean up. remove unused imports, unused code, etc --- ngshare_exchange/collect.py | 1 - ngshare_exchange/course_management.py | 3 --- ngshare_exchange/submit.py | 11 +---------- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/ngshare_exchange/collect.py b/ngshare_exchange/collect.py index 62fe540..d602b00 100644 --- a/ngshare_exchange/collect.py +++ b/ngshare_exchange/collect.py @@ -1,6 +1,5 @@ import os import shutil -import sys from collections import defaultdict import base64 diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index bdc4816..3e02565 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -1,10 +1,7 @@ import os import sys -import getopt import requests import csv -import pwd -import grp import subprocess import json import argparse diff --git a/ngshare_exchange/submit.py b/ngshare_exchange/submit.py index f1aa065..a071ca1 100644 --- a/ngshare_exchange/submit.py +++ b/ngshare_exchange/submit.py @@ -1,10 +1,8 @@ -import base64 import os -import json from nbgrader.exchange.abc import ExchangeSubmit as ABCExchangeSubmit from .exchange import Exchange -from nbgrader.utils import find_all_notebooks, parse_utc +from nbgrader.utils import find_all_notebooks class ExchangeSubmit(Exchange, ABCExchangeSubmit): @@ -46,13 +44,6 @@ def init_dest(self): self.fail('No course id specified. Re-run with --course flag.') self.cache_path = os.path.join(self.cache, self.coursedir.course_id) - if self.coursedir.student_id != '*': - self.fail( - 'Submitting assignments with an explicit student ID is ' - 'not possible with ngshare.' - ) - else: - student_id = self.username def check_filename_diff(self): released_notebooks = self._get_assignment_notebooks( From 947d87b2b88837a997d383ca20e44d26886bdb25 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 16:09:21 -0700 Subject: [PATCH 21/35] add back check for student id, but remove assignment --- ngshare_exchange/submit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ngshare_exchange/submit.py b/ngshare_exchange/submit.py index a071ca1..a09bee3 100644 --- a/ngshare_exchange/submit.py +++ b/ngshare_exchange/submit.py @@ -44,6 +44,11 @@ def init_dest(self): self.fail('No course id specified. Re-run with --course flag.') self.cache_path = os.path.join(self.cache, self.coursedir.course_id) + if self.coursedir.student_id != '*': + self.fail( + 'Submitting assignments with an explicit student ID is ' + 'not possible with ngshare.' + ) def check_filename_diff(self): released_notebooks = self._get_assignment_notebooks( From d5d13e32833a23412818e73cde4e403976b3c962 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 16:25:48 -0700 Subject: [PATCH 22/35] change exchange to match https://github.com/jupyter/nbgrader/pull/1319 --- ngshare_exchange/exchange.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/ngshare_exchange/exchange.py b/ngshare_exchange/exchange.py index 36edc36..bffbdef 100644 --- a/ngshare_exchange/exchange.py +++ b/ngshare_exchange/exchange.py @@ -6,6 +6,7 @@ import fnmatch from pathlib import Path from urllib.parse import quote +from rapidfuzz import fuzz from textwrap import dedent @@ -238,20 +239,12 @@ def start(self): return super(Exchange, self).start() def _assignment_not_found(self, src_path, other_path): - msg = 'Assignment not found at: {}'.format(src_path) + msg = "Assignment not found at: {}".format(src_path) self.log.fatal(msg) found = glob.glob(other_path) if found: - - # Normally it is a bad idea to put imports in the middle of - # a function, but we do this here because otherwise fuzzywuzzy - # prints an annoying message about python-Levenshtein every - # time nbgrader is run. - - from fuzzywuzzy import fuzz - scores = sorted([(fuzz.ratio(self.src_path, x), x) for x in found]) - self.log.error('Did you mean: %s', scores[-1][1]) + self.log.error("Did you mean: %s", scores[-1][1]) raise ExchangeError(msg) From d6ae52de947bbbf4a3623645e8d15950e4c12aa4 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 16:31:24 -0700 Subject: [PATCH 23/35] encode urls in course management, change encoding in exchange --- ngshare_exchange/course_management.py | 12 +++++++++--- ngshare_exchange/exchange.py | 14 +++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index 3e02565..f59b3d6 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -5,6 +5,7 @@ import subprocess import json import argparse +from urllib.parse import quote # https://www.geeksforgeeks.org/print-colors-python-terminal/ def prRed(skk, exit=True): @@ -86,11 +87,16 @@ def check_message(response): return response +def encode_url(url): + return quote(url, safe='/', encoding=None, errors=None) + + def post(url, data): header = get_header() + encoded_url = encode_url(url) try: - response = requests.post(url, data=data, headers=header) + response = requests.post(encoded_url, data=data, headers=header) response.raise_for_status() except requests.exceptions.ConnectionError: prRed('Could not establish connection to ngshare server') @@ -102,9 +108,9 @@ def post(url, data): def delete(url, data): header = get_header() - + encoded_url = encode_url(url) try: - response = requests.delete(url, data=data, headers=header) + response = requests.delete(encoded_url, data=data, headers=header) response.raise_for_status() except requests.exceptions.ConnectionError: prRed('Could not establish connection to ngshare server') diff --git a/ngshare_exchange/exchange.py b/ngshare_exchange/exchange.py index bffbdef..21ac734 100644 --- a/ngshare_exchange/exchange.py +++ b/ngshare_exchange/exchange.py @@ -82,6 +82,7 @@ def _ngshare_api_check_error(self, response, url): return response def ngshare_api_request(self, method, url, data=None, params=None): + encoded_url = self.encode_url(url) try: headers = None if 'JUPYTERHUB_API_TOKEN' in os.environ: @@ -91,7 +92,7 @@ def ngshare_api_request(self, method, url, data=None, params=None): } response = requests.request( method, - self.ngshare_url + url, + self.ngshare_url + encoded_url, headers=headers, data=data, params=params, @@ -108,18 +109,13 @@ def encode_url(self, url): return quote(url, safe='/', encoding=None, errors=None) def ngshare_api_get(self, url, params=None): - encoded_url = self.encode_url(url) - return self.ngshare_api_request('GET', encoded_url, params=params) + return self.ngshare_api_request('GET', url, params=params) def ngshare_api_post(self, url, data, params=None): - encoded_url = self.encode_url(url) - return self.ngshare_api_request( - 'POST', encoded_url, data=data, params=params - ) + return self.ngshare_api_request('POST', url, data=data, params=params) def ngshare_api_delete(self, url, params=None): - encoded_url = self.encode_url(url) - return self.ngshare_api_request('DELETE', encoded_url, params=params) + return self.ngshare_api_request('DELETE', url, params=params) assignment_dir = Unicode( '.', From c5df16b8935774fe6b3f980448698ed57fab207d Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 16:43:16 -0700 Subject: [PATCH 24/35] add dependency to setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f44377f..51e63c2 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ def get_version(rel_path): ], python_requires='>=3.6', install_requires=[ - 'fuzzywuzzy', + 'rapidfuzz', 'traitlets', 'jupyter_core', #'nbgrader>=0.7.0', # unfortunately not out yet? From f81ad3ac83e2d0b7710dadbfea110831a8f3e377 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 17:08:58 -0700 Subject: [PATCH 25/35] encode url in tests --- ngshare_exchange/tests/test_course_management.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ngshare_exchange/tests/test_course_management.py b/ngshare_exchange/tests/test_course_management.py index 86d5984..a26f9b8 100644 --- a/ngshare_exchange/tests/test_course_management.py +++ b/ngshare_exchange/tests/test_course_management.py @@ -12,6 +12,7 @@ from requests_mock import Mocker import urllib import tempfile +from urllib.parse import quote from .. import course_management as cm @@ -26,7 +27,8 @@ def parse_body(body: str): return dict(urllib.parse.parse_qsl(body)) -NGSHARE_URL = 'http://127.0.0.1:12121/api' +url = 'http://127.0.0.1:12121/api' +NGSHARE_URL = quote(url, safe='/', encoding=None, errors=None) global _ngshare_url cm._ngshare_url = NGSHARE_URL From 1f9ba53107d30e0a855a26b6711541d92e8aed58 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 17:34:42 -0700 Subject: [PATCH 26/35] fix course management --- ngshare_exchange/course_management.py | 20 +++++++++---------- .../tests/test_course_management.py | 4 +--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index f59b3d6..84ced37 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -96,7 +96,7 @@ def post(url, data): encoded_url = encode_url(url) try: - response = requests.post(encoded_url, data=data, headers=header) + response = requests.post(ngshare_url() + encoded_url, data=data, headers=header) response.raise_for_status() except requests.exceptions.ConnectionError: prRed('Could not establish connection to ngshare server') @@ -110,7 +110,7 @@ def delete(url, data): header = get_header() encoded_url = encode_url(url) try: - response = requests.delete(encoded_url, data=data, headers=header) + response = requests.delete(ngshare_url() + encoded_url, data=data, headers=header) response.raise_for_status() except requests.exceptions.ConnectionError: prRed('Could not establish connection to ngshare server') @@ -122,7 +122,7 @@ def delete(url, data): def create_course(args): instructors = args.instructors or [] - url = '{}/course/{}'.format(ngshare_url(), args.course_id) + url = '/course/{}'.format(args.course_id) data = {'user': get_username(), 'instructors': json.dumps(instructors)} response = post(url, data) @@ -132,7 +132,7 @@ def create_course(args): def add_student(args): # add student to ngshare student = User(args.student_id, args.first_name, args.last_name, args.email) - url = '{}/student/{}/{}'.format(ngshare_url(), args.course_id, student.id) + url = '/student/{}/{}'.format(args.course_id, student.id) data = { 'user': get_username(), 'first_name': student.first_name, @@ -209,7 +209,7 @@ def add_students(args): student_dict['email'] = email students.append(student_dict) - url = '{}/students/{}'.format(ngshare_url(), args.course_id) + url = '/students/{}'.format(args.course_id) data = {'user': get_username(), 'students': json.dumps(students)} response = post(url, data) @@ -251,7 +251,7 @@ def remove_students(args): if not args.no_gb: remove_jh_student(student, args.force) - url = '{}/student/{}/{}'.format(ngshare_url(), args.course_id, student) + url = '/student/{}/{}'.format(args.course_id, student) data = {'user': get_username()} response = delete(url, data) prGreen( @@ -260,8 +260,8 @@ def remove_students(args): def add_instructor(args): - url = '{}/instructor/{}/{}'.format( - ngshare_url(), args.course_id, args.instructor_id + url = '/instructor/{}/{}'.format( + args.course_id, args.instructor_id ) data = { 'user': get_username(), @@ -279,8 +279,8 @@ def add_instructor(args): def remove_instructor(args): - url = '{}/instructor/{}/{}'.format( - ngshare_url(), args.course_id, args.instructor_id + url = '/instructor/{}/{}'.format( + args.course_id, args.instructor_id ) data = {'user': get_username()} response = delete(url, data) diff --git a/ngshare_exchange/tests/test_course_management.py b/ngshare_exchange/tests/test_course_management.py index a26f9b8..86d5984 100644 --- a/ngshare_exchange/tests/test_course_management.py +++ b/ngshare_exchange/tests/test_course_management.py @@ -12,7 +12,6 @@ from requests_mock import Mocker import urllib import tempfile -from urllib.parse import quote from .. import course_management as cm @@ -27,8 +26,7 @@ def parse_body(body: str): return dict(urllib.parse.parse_qsl(body)) -url = 'http://127.0.0.1:12121/api' -NGSHARE_URL = quote(url, safe='/', encoding=None, errors=None) +NGSHARE_URL = 'http://127.0.0.1:12121/api' global _ngshare_url cm._ngshare_url = NGSHARE_URL From 948be23fab7f1300b13cef90f7814ac1c45945e4 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Tue, 2 Jun 2020 17:36:03 -0700 Subject: [PATCH 27/35] fix style --- ngshare_exchange/course_management.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index 84ced37..19cb95d 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -96,7 +96,9 @@ def post(url, data): encoded_url = encode_url(url) try: - response = requests.post(ngshare_url() + encoded_url, data=data, headers=header) + response = requests.post( + ngshare_url() + encoded_url, data=data, headers=header + ) response.raise_for_status() except requests.exceptions.ConnectionError: prRed('Could not establish connection to ngshare server') @@ -110,7 +112,9 @@ def delete(url, data): header = get_header() encoded_url = encode_url(url) try: - response = requests.delete(ngshare_url() + encoded_url, data=data, headers=header) + response = requests.delete( + ngshare_url() + encoded_url, data=data, headers=header + ) response.raise_for_status() except requests.exceptions.ConnectionError: prRed('Could not establish connection to ngshare server') @@ -260,9 +264,7 @@ def remove_students(args): def add_instructor(args): - url = '/instructor/{}/{}'.format( - args.course_id, args.instructor_id - ) + url = '/instructor/{}/{}'.format(args.course_id, args.instructor_id) data = { 'user': get_username(), 'first_name': args.first_name, @@ -279,9 +281,7 @@ def add_instructor(args): def remove_instructor(args): - url = '/instructor/{}/{}'.format( - args.course_id, args.instructor_id - ) + url = '/instructor/{}/{}'.format(args.course_id, args.instructor_id) data = {'user': get_username()} response = delete(url, data) prGreen( From 47d448d10f01dfdf41cb35de153d542f0e7e709d Mon Sep 17 00:00:00 2001 From: rkevin Date: Wed, 3 Jun 2020 19:35:07 -0700 Subject: [PATCH 28/35] Add warning when username contains uppercase --- ngshare_exchange/course_management.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index 19cb95d..2a549bc 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -124,8 +124,21 @@ def delete(url, data): return check_message(response) +def check_username_warning(users): + invalid_usernames = [n for n in users if n != n.lower()] + if invalid_usernames: + prRed( + 'The following usernames have upper-case letters. Normally JupyterHub forces usernames to be lowercase. If the user has trouble accessing the course, you should add their lowercase username to ngshare instead.', + False, + ) + for user in invalid_usernames: + prRed(user, False) + + def create_course(args): instructors = args.instructors or [] + check_username_warning(instructors) + url = '/course/{}'.format(args.course_id) data = {'user': get_username(), 'instructors': json.dumps(instructors)} @@ -135,6 +148,7 @@ def create_course(args): def add_student(args): # add student to ngshare + check_username_warning([student.id]) student = User(args.student_id, args.first_name, args.last_name, args.email) url = '/student/{}/{}'.format(args.course_id, student.id) data = { @@ -213,6 +227,7 @@ def add_students(args): student_dict['email'] = email students.append(student_dict) + check_username_warning([student['username'] for student in students]) url = '/students/{}'.format(args.course_id) data = {'user': get_username(), 'students': json.dumps(students)} @@ -264,6 +279,7 @@ def remove_students(args): def add_instructor(args): + check_username_warning([args.instructor_id]) url = '/instructor/{}/{}'.format(args.course_id, args.instructor_id) data = { 'user': get_username(), From b1bd889fc354661d6825d1513153964d5fcd386c Mon Sep 17 00:00:00 2001 From: rkevin Date: Thu, 4 Jun 2020 00:05:29 -0700 Subject: [PATCH 29/35] fix stupid error --- ngshare_exchange/course_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index 2a549bc..abb9930 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -148,7 +148,7 @@ def create_course(args): def add_student(args): # add student to ngshare - check_username_warning([student.id]) + check_username_warning([args.student_id]) student = User(args.student_id, args.first_name, args.last_name, args.email) url = '/student/{}/{}'.format(args.course_id, student.id) data = { From 44df29d60ea4848f3da97f5f9ba905c83c16c084 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Thu, 4 Jun 2020 22:57:31 -0700 Subject: [PATCH 30/35] add tests, fix bugs in course management --- ngshare_exchange/course_management.py | 13 +- .../tests/test_course_management.py | 140 +++++++++++++++++- 2 files changed, 141 insertions(+), 12 deletions(-) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index abb9930..2002d7d 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -19,6 +19,10 @@ def prGreen(skk): print('\033[92m {}\033[00m'.format(skk)) +def prYellow(skk): + print("\033[93m {}\033[00m".format(skk)) + + class User: def __init__(self, id, first_name, last_name, email): self.id = id @@ -127,12 +131,11 @@ def delete(url, data): def check_username_warning(users): invalid_usernames = [n for n in users if n != n.lower()] if invalid_usernames: - prRed( + prYellow( 'The following usernames have upper-case letters. Normally JupyterHub forces usernames to be lowercase. If the user has trouble accessing the course, you should add their lowercase username to ngshare instead.', - False, ) for user in invalid_usernames: - prRed(user, False) + prYellow(user) def create_course(args): @@ -238,7 +241,9 @@ def add_students(args): user = s['username'] if s['success']: prGreen( - '{} was sucessfuly added to {}'.format(user, args.course_id) + '{} was successfuly added to {}'.format( + user, args.course_id + ) ) student = User( user, diff --git a/ngshare_exchange/tests/test_course_management.py b/ngshare_exchange/tests/test_course_management.py index 86d5984..0ecdbb7 100644 --- a/ngshare_exchange/tests/test_course_management.py +++ b/ngshare_exchange/tests/test_course_management.py @@ -16,11 +16,19 @@ def remove_color(s): + # https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') result = ansi_escape.sub('', s) return result +def get_out_array(out): + out = remove_color(out) + out = out.split('\n') + out.remove('') + return out + + def parse_body(body: str): # https://stackoverflow.com/questions/48018622/how-can-see-the-request-data#51052385 return dict(urllib.parse.parse_qsl(body)) @@ -43,6 +51,7 @@ class TestCourseManagement: instructor_id = 'mi1' course_created = False + bad_user_warning_message = 'The following usernames have upper-case letters. Normally JupyterHub forces usernames to be lowercase. If the user has trouble accessing the course, you should add their lowercase username to ngshare instead.' @pytest.fixture(autouse=True) def init(self, requests_mock: Mocker): @@ -69,6 +78,7 @@ def _mock_all(self, request: PreparedRequest, content): def _get_user_info(self, request: PreparedRequest, context): request = parse_body(request.body) + print(request) if 'first_name' not in request: return {'success': False, 'message': 'Please supply first name'} elif 'last_name' not in request: @@ -111,6 +121,26 @@ def _get_students_info(self, request: PreparedRequest, context): else: return {'success': False, 'message': 'wrong students passed in'} + def _get_bad_students_info(self, request: PreparedRequest, context): + request = parse_body(request.body) + students = eval(request['students']) + + if ( + 'Not_good' == students[0]['username'] + and 'Bad' == students[1]['username'] + and '123' == students[2]['username'] + ): + return { + 'success': True, + 'status': [ + {'username': 'Not_good', 'success': True}, + {'username': 'Bad', 'success': True}, + {'username': '123', 'success': True}, + ], + } + else: + return {'success': False, 'message': 'wrong students passed in'} + def _get_students_info_unsuccessful( self, request: PreparedRequest, context ): @@ -185,6 +215,10 @@ def _mock_add_students_unsuccessful(self): url, json=self._get_students_info_unsuccessful ) + def _mock_add_bad_students(self): + url = '{}/students/{}'.format(NGSHARE_URL, self.course_id) + self.requests_mocker.post(url, json=self._get_bad_students_info) + def _mock_add_instructor(self): url = '{}/instructor/{}/{}'.format( NGSHARE_URL, self.course_id, self.instructor_id @@ -203,7 +237,7 @@ def _mock_remove_instructor(self): ) self.requests_mocker.delete(url, json=self._get_user) - def test_crete_course(self, capsys): + def test_create_course(self, capsys): self._mock_create_course() cm.main(['create_course', self.course_id] + self.instructors) out, err = capsys.readouterr() @@ -225,6 +259,96 @@ def test_crete_course(self, capsys): assert se.type == SystemExit assert se.value.code == -1 + def test_create_course_instructor_warning(self, capsys, tmpdir_factory): + # test passing in a list with one bad username + self._mock_create_course() + self.instructors = ['BadUsername', 'goodusername'] + cm.main(['create_course', self.course_id] + self.instructors) + + out, err = capsys.readouterr() + out = get_out_array(out) + + assert self.bad_user_warning_message in out[0] + assert 'BadUsername' in out[1] + assert 'Successfully created math101' in out[-1] + + def test_add_student_warning(self, capsys, tmpdir_factory): + # test trying to add a student with bad username + self.student_id = 'BAD_username' + self._mock_add_student() + cm.main( + [ + 'add_student', + self.course_id, + self.student_id, + '-f', + 'jane', + '-l', + 'doe', + '-e' 'jd@mail.com', + '--no-gb', + ] + ) + + out, err = capsys.readouterr() + out = get_out_array(out) + + assert self.bad_user_warning_message in out[0] + assert 'BAD_username' in out[1] + assert 'Successfully added/updated BAD_username on math101' in out[-1] + + def test_add_instructor_warning(self, capsys, tmpdir_factory): + # test adding an instructor with a bad username + self.instructor_id = 'Bad_Inst' + self._mock_add_instructor() + cm.main( + [ + 'add_instructor', + self.course_id, + self.instructor_id, + '-f', + 'john', + '-l', + 'doe', + '-e', + 'jd@mail.com', + ] + ) + + out, err = capsys.readouterr() + out = get_out_array(out) + + assert self.bad_user_warning_message in out[0] + assert 'Bad_Inst' in out[1] + assert 'Successfully added Bad_Inst as an instructor to math' in out[-1] + + def test_add_students_warning(self, capsys, tmpdir_factory): + # check add students with bad usernames + tmp_dir = tmpdir_factory.mktemp(self.course_id) + os.chdir(tmp_dir) + self._add_empty_gradebook(tmp_dir) + self._mock_add_bad_students() + with tempfile.NamedTemporaryFile() as f: + f.writelines( + [ + b'student_id,first_name,last_name,email\n', + b'Not_good,jane,doe,jd@mail.com\n', + b'Bad,john,perez,jp@mail.com\n', + b'123,john,perez,jp@mail.com\n', + ] + ) + f.flush() + cm.main(['add_students', self.course_id, f.name]) + + out, err = capsys.readouterr() + out = get_out_array(out) + assert self.bad_user_warning_message in out[0] + assert 'Not_good' in out[1] + assert 'Bad' in out[2] + assert 'Not_good was successfuly added to math101' in out[-3] + assert 'Bad was successfuly added to math101' in out[-2] + assert '123 was successfuly added to math101' in out[-1] + def test_add_student(self, capsys): # test missing course id with pytest.raises(SystemExit) as se: @@ -315,8 +439,8 @@ def test_add_students_db(self, capsys, tmpdir_factory): f.flush() cm.main(['add_students', self.course_id, f.name]) out, err = capsys.readouterr() - assert 'sid1 was sucessfuly added to math101' in out - assert 'sid2 was sucessfuly added to math101' in out + assert 'sid1 was successfuly added to math101' in out + assert 'sid2 was successfuly added to math101' in out gb = Gradebook('sqlite:///gradebook.db', course_id=self.course_id) students = gb.students @@ -368,8 +492,8 @@ def test_add_students(self, capsys, tmp_path): f.flush() cm.main(['add_students', self.course_id, f.name, '--no-gb']) out, err = capsys.readouterr() - assert 'sid1 was sucessfuly added to math101' in out - assert 'sid2 was sucessfuly added to math101' in out + assert 'sid1 was successfuly added to math101' in out + assert 'sid2 was successfuly added to math101' in out def test_add_students_unsuccessful(self, capsys, tmp_path): self._mock_add_students_unsuccessful() @@ -385,7 +509,7 @@ def test_add_students_unsuccessful(self, capsys, tmp_path): cm.main(['add_students', self.course_id, f.name, '--no-gb']) out, err = capsys.readouterr() assert 'There was an error adding sid1 to math101: ' in out - assert 'sid2 was sucessfuly added to math101' in out + assert 'sid2 was successfuly added to math101' in out def test_add_instructor(self, capsys): self._mock_add_instructor() @@ -533,9 +657,9 @@ def test_add_students_parsing(self, capsys): cm.main(['add_students', self.course_id, f.name, '--no-gb']) out, err = capsys.readouterr() - assert 'sid1 was sucessfuly added to math101' in out + assert 'sid1 was successfuly added to math101' in out assert 'Student ID cannot be empty (row 2)' in out - assert 'sid2 was sucessfuly added to math101' in out + assert 'sid2 was successfuly added to math101' in out def test_get_username(self): jhu = 'JUPYTERHUB_USER' From 3f69ebd1f9916023e731ee1bfb66643e45d91f47 Mon Sep 17 00:00:00 2001 From: aalmanza1998 Date: Fri, 5 Jun 2020 12:57:09 -0700 Subject: [PATCH 31/35] fix typo --- ngshare_exchange/course_management.py | 2 +- .../tests/test_course_management.py | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ngshare_exchange/course_management.py b/ngshare_exchange/course_management.py index 2002d7d..17084b1 100644 --- a/ngshare_exchange/course_management.py +++ b/ngshare_exchange/course_management.py @@ -241,7 +241,7 @@ def add_students(args): user = s['username'] if s['success']: prGreen( - '{} was successfuly added to {}'.format( + '{} was successfully added to {}'.format( user, args.course_id ) ) diff --git a/ngshare_exchange/tests/test_course_management.py b/ngshare_exchange/tests/test_course_management.py index 0ecdbb7..360de42 100644 --- a/ngshare_exchange/tests/test_course_management.py +++ b/ngshare_exchange/tests/test_course_management.py @@ -345,9 +345,9 @@ def test_add_students_warning(self, capsys, tmpdir_factory): assert self.bad_user_warning_message in out[0] assert 'Not_good' in out[1] assert 'Bad' in out[2] - assert 'Not_good was successfuly added to math101' in out[-3] - assert 'Bad was successfuly added to math101' in out[-2] - assert '123 was successfuly added to math101' in out[-1] + assert 'Not_good was successfully added to math101' in out[-3] + assert 'Bad was successfully added to math101' in out[-2] + assert '123 was successfully added to math101' in out[-1] def test_add_student(self, capsys): # test missing course id @@ -439,8 +439,8 @@ def test_add_students_db(self, capsys, tmpdir_factory): f.flush() cm.main(['add_students', self.course_id, f.name]) out, err = capsys.readouterr() - assert 'sid1 was successfuly added to math101' in out - assert 'sid2 was successfuly added to math101' in out + assert 'sid1 was successfully added to math101' in out + assert 'sid2 was successfully added to math101' in out gb = Gradebook('sqlite:///gradebook.db', course_id=self.course_id) students = gb.students @@ -492,8 +492,8 @@ def test_add_students(self, capsys, tmp_path): f.flush() cm.main(['add_students', self.course_id, f.name, '--no-gb']) out, err = capsys.readouterr() - assert 'sid1 was successfuly added to math101' in out - assert 'sid2 was successfuly added to math101' in out + assert 'sid1 was successfully added to math101' in out + assert 'sid2 was successfully added to math101' in out def test_add_students_unsuccessful(self, capsys, tmp_path): self._mock_add_students_unsuccessful() @@ -509,7 +509,7 @@ def test_add_students_unsuccessful(self, capsys, tmp_path): cm.main(['add_students', self.course_id, f.name, '--no-gb']) out, err = capsys.readouterr() assert 'There was an error adding sid1 to math101: ' in out - assert 'sid2 was successfuly added to math101' in out + assert 'sid2 was successfully added to math101' in out def test_add_instructor(self, capsys): self._mock_add_instructor() @@ -657,9 +657,9 @@ def test_add_students_parsing(self, capsys): cm.main(['add_students', self.course_id, f.name, '--no-gb']) out, err = capsys.readouterr() - assert 'sid1 was successfuly added to math101' in out + assert 'sid1 was successfully added to math101' in out assert 'Student ID cannot be empty (row 2)' in out - assert 'sid2 was successfuly added to math101' in out + assert 'sid2 was successfully added to math101' in out def test_get_username(self): jhu = 'JUPYTERHUB_USER' From 24c55f189e076e5d65733425feba92c6770d567d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=AE=B5=E9=80=B8?= <13691419520@163.com> Date: Mon, 8 Jun 2020 14:32:28 -0700 Subject: [PATCH 32/35] lxylxy123456 -> LibreTexts --- README.md | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f336885..b83067e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # ngshare_exchange -[![Build Status](https://travis-ci.org/lxylxy123456/ngshare_exchange.svg?branch=master)](https://travis-ci.org/lxylxy123456/ngshare_exchange) -[![codecov](https://codecov.io/gh/lxylxy123456/ngshare_exchange/branch/master/graph/badge.svg)](https://codecov.io/gh/lxylxy123456/ngshare_exchange) +[![Build Status](https://travis-ci.org/LibreTexts/ngshare_exchange.svg?branch=master)](https://travis-ci.org/LibreTexts/ngshare_exchange) +[![codecov](https://codecov.io/gh/LibreTexts/ngshare_exchange/branch/master/graph/badge.svg)](https://codecov.io/gh/LibreTexts/ngshare_exchange) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Documentation Status](https://readthedocs.org/projects/ngshare-exchange/badge/?version=latest)](https://ngshare-exchange.readthedocs.io/en/latest/?badge=latest) -Custom [nbgrader](https://github.com/jupyter/nbgrader) exchange to be used with [ngshare](https://github.com/lxylxy123456/ngshare). This should be installed in the singleuser image of [Z2JH](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) to allow the users to use ngshare. +Custom [nbgrader](https://github.com/jupyter/nbgrader) exchange to be used with [ngshare](https://github.com/LibreTexts/ngshare). This should be installed in the singleuser image of [Z2JH](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) to allow the users to use ngshare. # Installation instructions diff --git a/setup.py b/setup.py index f44377f..2ec9374 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def get_version(rel_path): description="nbgrader exchange to use with ngshare", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/lxylxy123456/ngshare_exchange", + url="https://github.com/LibreTexts/ngshare_exchange", packages=setuptools.find_packages(), include_package_data=True, classifiers=[ From 007b9f1178586f454b9780387d2c14eed8f6c679 Mon Sep 17 00:00:00 2001 From: rkevin Date: Fri, 12 Jun 2020 12:55:48 -0700 Subject: [PATCH 33/35] fix travis pip packages being out of date --- .travis.yml | 5 ++--- testing_reqs.txt | 6 ++++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 testing_reqs.txt diff --git a/.travis.yml b/.travis.yml index af84b00..3e4f016 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -language: python + language: python python: - 3.6 - 3.7 @@ -20,12 +20,11 @@ before_install: install: - python3 -m pip install . before_script: -- python3 -m pip install pytest pytest-cov pytest-tornado black requests_mock +- python3 -m pip install -r testing_reqs.txt script: - python3 -m pytest ./ngshare_exchange/ --cov=./ngshare_exchange/ - python3 -m black -S -l 80 --check . after_success: -- python3 -m pip install "codecov>=2.1.0" - codecov deploy: provider: pypi diff --git a/testing_reqs.txt b/testing_reqs.txt new file mode 100644 index 0000000..40a2b5d --- /dev/null +++ b/testing_reqs.txt @@ -0,0 +1,6 @@ +pytest>=5.2.1 +pytest-cov>=2.10.0 +pytest-tornado>=0.8.0 +black>=19.10b0 +codecov>=2.1.0 +requests_mock>=1.8.0 From 1673b659da831feea992aa612664e85002a546f3 Mon Sep 17 00:00:00 2001 From: rkevin Date: Fri, 12 Jun 2020 14:03:44 -0700 Subject: [PATCH 34/35] 0.5.1 release --- ngshare_exchange/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngshare_exchange/version.py b/ngshare_exchange/version.py index 2b8877c..93b60a1 100644 --- a/ngshare_exchange/version.py +++ b/ngshare_exchange/version.py @@ -1 +1 @@ -__version__ = '0.5.0' +__version__ = '0.5.1' From c3e0efb639f8f949ed62eeefdd43f69ed59c3c69 Mon Sep 17 00:00:00 2001 From: rkevin Date: Fri, 12 Jun 2020 14:24:01 -0700 Subject: [PATCH 35/35] fix typo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3e4f016..cfc06bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ - language: python +language: python python: - 3.6 - 3.7