lms.cli.courses.assignments.scores.upload

Upload scores (and optional comments) for an assignment.

usage: python3 -m lms.cli.courses.assignments.scores.upload
       [-h] [--version] [--server SERVER]
       [--server-type {blackboard,canvas,moodle}] [--auth-user AUTH_USER]
       [--auth-password AUTH_PASSWORD] [--auth-token AUTH_TOKEN]
       [--course COURSE] [--assignment ASSIGNMENT] [--skip-rows SKIP_ROWS]
       [--strict]
       PATH

Upload scores (and optional comments) for an assignment.

positional arguments:
  PATH                  Path to a TSV file where each row has 2-3 columns:
                        user query, score, and comment (optional).

options:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  --course COURSE       The course to target for this operation.
  --assignment ASSIGNMENT
                        The assignment to target for this operation.
  --skip-rows SKIP_ROWS
                        The number of header rows to skip (default: 0).
  --strict              Enable strict mode, which is stricter about what
                        counts as an error (default: False).

server options:
  --server SERVER       The address of the LMS server to connect to.
  --server-type {blackboard,canvas,moodle}
                        The type of LMS being connected to (this can normally
                        be guessed from the server address).

authentication options:
  --auth-user AUTH_USER
                        The user to authenticate with.
  --auth-password AUTH_PASSWORD
                        The password to authenticate with.
  --auth-token AUTH_TOKEN
                        The token to authenticate with.
  1"""
  2Upload scores (and optional comments) for an assignment.
  3"""
  4
  5import argparse
  6import ast
  7import sys
  8import typing
  9
 10import edq.util.dirent
 11
 12import lms.backend.instance
 13import lms.cli.common
 14import lms.cli.parser
 15import lms.model.backend
 16import lms.model.scores
 17import lms.model.users
 18
 19def run_cli(args: argparse.Namespace) -> int:
 20    """ Run the CLI. """
 21
 22    config = args._config
 23
 24    backend = lms.backend.instance.get_backend(**config)
 25
 26    course_query = lms.cli.common.check_required_course(backend, config)
 27    if (course_query is None):
 28        return 1
 29
 30    assignment_query = lms.cli.common.check_required_assignment(backend, config)
 31    if (assignment_query is None):
 32        return 2
 33
 34    scores = _load_scores(backend, args.path, args.skip_rows)
 35
 36    count = backend.courses_assignments_scores_resolve_and_upload(course_query, assignment_query, scores)
 37
 38    print(f"Uploaded {count} Scores")
 39
 40    return lms.cli.common.strict_check(args.strict, (count != len(scores)),
 41        f"Expected to upload {len(scores)} scores, but uploaded {count}.", 3)
 42
 43def _load_scores(
 44        backend: lms.model.backend.APIBackend,
 45        path: str,
 46        skip_rows: bool,
 47        ) -> typing.Dict[lms.model.users.UserQuery, lms.model.scores.ScoreFragment]:
 48    scores = {}
 49
 50    with open(path, 'r', encoding = edq.util.dirent.DEFAULT_ENCODING) as file:
 51        lineno = 0
 52        real_rows = 0
 53        for line in file:
 54            lineno += 1
 55
 56            if (line.strip() == ''):
 57                continue
 58
 59            real_rows += 1
 60
 61            if (real_rows <= skip_rows):
 62                continue
 63
 64            parts = [part.strip() for part in line.split("\t")]
 65            if (len(parts) not in [2, 3]):
 66                raise ValueError(f"File '{path}' line {lineno} has the incorrect number of values. Expecting 2-3, found {len(parts)}.")
 67
 68            user_query = backend.parse_user_query(parts[0])
 69            if (user_query is None):
 70                raise ValueError(f"File '{path}' line {lineno} has a user query that could not be parsed: '{parts[0]}'.")
 71
 72            score = None
 73            if (parts[1] != ''):
 74                try:
 75                    score = float(ast.literal_eval(parts[1]))
 76                except Exception:
 77                    raise ValueError(f"File '{path}' line {lineno} has a score that cannot be converted to a number: '{parts[1]}'.")  # pylint: disable=raise-missing-from
 78
 79            comment = None
 80            if (len(parts) == 3):
 81                comment = parts[2]
 82
 83            scores[user_query] = lms.model.scores.ScoreFragment(score = score, comment = comment)
 84
 85    return scores
 86
 87def main() -> int:
 88    """ Get a parser, parse the args, and call run. """
 89    return run_cli(_get_parser().parse_args())
 90
 91def _get_parser() -> argparse.ArgumentParser:
 92    """ Get the parser. """
 93
 94    parser = lms.cli.parser.get_parser(__doc__.strip(),
 95            include_course = True,
 96            include_assignment = True,
 97            include_skip_rows = True,
 98            include_strict = True,
 99    )
100
101    parser.add_argument('path', metavar = 'PATH',
102        action = 'store', type = str,
103        help = 'Path to a TSV file where each row has 2-3 columns: user query, score, and comment (optional).')
104
105    return parser
106
107if (__name__ == '__main__'):
108    sys.exit(main())
def run_cli(args: argparse.Namespace) -> int:
20def run_cli(args: argparse.Namespace) -> int:
21    """ Run the CLI. """
22
23    config = args._config
24
25    backend = lms.backend.instance.get_backend(**config)
26
27    course_query = lms.cli.common.check_required_course(backend, config)
28    if (course_query is None):
29        return 1
30
31    assignment_query = lms.cli.common.check_required_assignment(backend, config)
32    if (assignment_query is None):
33        return 2
34
35    scores = _load_scores(backend, args.path, args.skip_rows)
36
37    count = backend.courses_assignments_scores_resolve_and_upload(course_query, assignment_query, scores)
38
39    print(f"Uploaded {count} Scores")
40
41    return lms.cli.common.strict_check(args.strict, (count != len(scores)),
42        f"Expected to upload {len(scores)} scores, but uploaded {count}.", 3)

Run the CLI.

def main() -> int:
88def main() -> int:
89    """ Get a parser, parse the args, and call run. """
90    return run_cli(_get_parser().parse_args())

Get a parser, parse the args, and call run.