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

Run the CLI.

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

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