autograder.api.config

  1import argparse
  2import copy
  3import typing
  4
  5import edq.util.hash
  6import edq.util.parse
  7import edq.util.time
  8
  9import autograder.api.constants
 10import autograder.model.log
 11import autograder.model.stats
 12import autograder.model.user
 13import autograder.error
 14import autograder.filespec
 15import autograder.util.parse
 16
 17CSV_TO_LIST_DELIMITER: str = ','
 18MAP_KEY_VALUE_SEP: str = '='
 19
 20DEFAULT_CLI_ACTIONS: typing.Dict[typing.Type, str] = {
 21    bool: 'store_true',
 22    edq.util.time.Timestamp: 'store',
 23    float: 'store',
 24    int: 'store',
 25    str: 'store',
 26}
 27""" Map value types to default CLI actions. """
 28
 29@typing.runtime_checkable
 30class CLIAddFunction(typing.Protocol):
 31    """ A function for manually adding APIParams to a parser. """
 32
 33    def __call__(self, parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup], param: 'APIParam') -> None:
 34        """
 35        Add the given APIParam to the parser.
 36        """
 37
 38class APIParam:
 39    """
 40    A definition for a parameter to the autograder API.
 41    This class also contains information for representing these parameters in
 42    config (dicts), on the CLI (argparse options), and in the payload of autograder requests.
 43    """
 44
 45    def __init__(self,
 46            config_key: str,
 47            description: str,
 48            alt_config_key: typing.Union[str, None] = None,
 49            api_key: typing.Union[str, None] = None,
 50            cli_flag: typing.Union[str, None] = None,
 51            api: bool = True,
 52            api_required: typing.Union[bool, None] = None,  # Default to the value of `api`.
 53            cli: bool = True,
 54            cli_required: typing.Union[bool, None] = None,  # Default to false.
 55            value_type: typing.Type = str,
 56            cli_type: typing.Union[typing.Any, None] = None,
 57            skip_clean: bool = False,
 58            hash_value: bool = False,
 59            omit_empty: bool = True,
 60            cli_action: typing.Union[typing.Any, None] = None,
 61            cli_default_value: typing.Any = None,
 62            cli_show_default: typing.Union[bool, None] = None,
 63            cli_extra_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
 64            cli_add_func: typing.Union[CLIAddFunction, None] = None,
 65            **kwargs: typing.Any) -> None:
 66        if (len(config_key.strip()) == 0):
 67            raise autograder.error.APIError(None, "APIParam cannot have an empty key.")
 68
 69        self.config_key: str = config_key
 70        """
 71        The main key/label used to reference this parameter.
 72        Will be used in the application config and config dicts.
 73        """
 74
 75        if (len(description) == 0):
 76            raise autograder.error.APIError(None, "APIParam cannot have an empty description.")
 77
 78        self.description: str = description
 79        """ A description used for this parameter. """
 80
 81        self.alt_config_key: typing.Union[str, None] = alt_config_key
 82        """
 83        An alternative config key that the value could be found under when reading a config file.
 84        A value found under this key will always be stored under self.config_key.
 85        """
 86
 87        if (api_key is None):
 88            api_key = config_key.replace('_', '-')
 89
 90        self.api_key: str = api_key
 91        """ The key in the API request payload for this parameter. """
 92
 93        if (cli_flag is None):
 94            cli_flag = config_key.replace('_', '-')
 95
 96        self.cli_flag: str = cli_flag
 97        """ The flag used on the CLI for this parameter. """
 98
 99        self.api: bool = api
100        """ If this parameter should be included on the API payload. """
101
102        if (api_required is None):
103            api_required = api
104
105        self.api_required: bool = api_required
106        """ If this parameter is required when calling the API. """
107
108        self.cli: bool = cli
109        """ If this parameter should be included on the CLI. """
110
111        if (cli_required is None):
112            cli_required = False
113
114        self.cli_required: bool = cli_required
115        """ If this parameter is required on the CLI. """
116
117        self.value_type: typing.Type = value_type
118        """ The type that a clean value from this parameter should be. """
119
120        if (cli_type is None):
121            cli_type = self.value_type
122
123        self.cli_type: typing.Any = cli_type
124        """
125        The type of value that will be looked for on the CLI.
126        Defaults to the same as `self.value_type`.
127
128        Note that this value will be used as the `type` argument to `argparse.ArgumentParser.add_argument()`.
129        """
130
131        self.skip_clean: bool = skip_clean
132        """ Skip any cleaning steps for this parameter's value. """
133
134        self.hash_value: bool = hash_value
135        """ Hash the value during the cleaning processes (typically used for passwords). """
136
137        self.omit_empty: bool = omit_empty
138        """ Do not include this parameter in the API payload if its value is empty. """
139
140        self.cli_default_value: typing.Any = cli_default_value
141        """ The default value for the CLI argument. """
142
143        if (cli_show_default is None):
144            # Usually we will want to show the default value,
145            # but boolean flags don't make much sense to show defaults with.
146            cli_show_default = (value_type != bool)
147
148        self.cli_show_default: bool = cli_show_default
149        """ Show the default value (in CLI's help) for a non-required param. """
150
151        if (cli_extra_options is None):
152            cli_extra_options = {}
153
154        self.cli_extra_options: typing.Dict[str, typing.Any] = cli_extra_options
155        """
156        Additional options to pass (as kwargs) to `argparse.ArgumentParser.add_argument()`.
157        These should not conflict with the options set using the direct members.
158        """
159
160        self.cli_add_func: typing.Union[CLIAddFunction, None] = cli_add_func
161        """ A function that can be used to override the standard behavior in add_to_parser(). """
162
163        if (self.cli and (self.cli_add_func is None) and (cli_action is None)):
164            # Choose action based on the value's type.
165            cli_action = DEFAULT_CLI_ACTIONS.get(self.value_type, None)
166            if (cli_action is None):
167                raise ValueError(f"Unknown action for value type '{self.value_type}'.")
168
169        self.cli_action: typing.Any = cli_action
170        """ The value that value will be used as the `action` argument to `argparse.ArgumentParser.add_argument()`. """
171
172    def clean_value(self, value: typing.Union[typing.Any, None]) -> typing.Union[typing.Any, None]:
173        """
174        Return a clean version of the given value (according to this API param).
175        An error will be raised if cleaning fails.
176        Empty values will be returned as None.
177        """
178
179        if (self.skip_clean):
180            return value
181
182        if (value is None):
183            return None
184
185        if (issubclass(self.value_type, edq.util.time.Timestamp)):
186            return edq.util.time.Timestamp.guess(value)
187
188        if (issubclass(self.value_type, bool)):
189            return edq.util.parse.boolean(value)
190
191        if (issubclass(self.value_type, int)):
192            return int(value)
193
194        if (issubclass(self.value_type, list)):
195            return autograder.util.parse.string_list(value)
196
197        if (issubclass(self.value_type, str)):
198            value = str(value).strip()
199            if (len(value) == 0):
200                return None
201
202            if (self.hash_value):
203                value = edq.util.hash.sha256_hex(value)
204
205        if (issubclass(self.value_type, autograder.filespec.FileSpec)):
206            return autograder.filespec.parse(value)
207
208        return value
209
210    def optional(self) -> 'APIParam':
211        """ Make a copy of this APIParam, mark the copy as optional, and return the copy. """
212
213        new_param = copy.deepcopy(self)
214        new_param.api_required = False
215        new_param.cli_required = False
216        return new_param
217
218    def required(self) -> 'APIParam':
219        """ Make a copy of this APIParam, mark the copy as required, and return the copy. """
220
221        new_param = copy.deepcopy(self)
222        new_param.api_required = True
223        new_param.cli_required = True
224        return new_param
225
226    def add_to_parser(self, parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup]) -> None:
227        """
228        Add this API parameter to the given parser.
229        If this parameter is not supposed to be represented as a CLI argument, it will not be added.
230        """
231
232        if (not self.cli):
233            return
234
235        # Check if there is a specialized function for this param.
236        if (self.cli_add_func is not None):
237            self.cli_add_func(parser, self)
238            return
239
240        kwargs = {
241            'dest': self.config_key,
242            'action': self.cli_action,
243            'help': self.description,
244        }
245
246        if (self.cli_action not in {'store_true', 'store_false'}):
247            kwargs['type'] = self.cli_type
248
249        if (self.cli_required):
250            kwargs['required'] = True
251        else:
252            kwargs['required'] = False
253            kwargs['default'] = self.cli_default_value
254
255            if (self.cli_show_default):
256                kwargs['help'] += ' (default: %(default)s)'
257
258        kwargs.update(self.cli_extra_options)
259
260        parser.add_argument(f'--{self.cli_flag}', **kwargs)
261
262class ArgumentMap(argparse.Action):
263    """ An argparse Action for key/value pairs separated with MAP_KEY_VALUE_SEP. """
264
265    def __call__(self, parser, namespace, raw_values, option_string = None):  # type: ignore[no-untyped-def]
266        if (not hasattr(namespace, self.dest)):
267            setattr(namespace, self.dest, {})
268
269        all_values = getattr(namespace, self.dest)
270        if (all_values is None):
271            all_values = {}
272            setattr(namespace, self.dest, all_values)
273
274        for raw_value in raw_values:
275            parts = raw_value.split(MAP_KEY_VALUE_SEP, 1)
276            if (len(parts) != 2):
277                raise ValueError(f"Argument map key/value pair ('{raw_value}') is missing a separator '{MAP_KEY_VALUE_SEP}'.")
278
279            all_values[parts[0].strip()] = parts[1].strip()
280
281def _cli_add_func_submission_files(parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup], param: APIParam) -> None:
282    parser.add_argument('files', metavar = 'FILE',
283        action = 'extend', type = str, nargs = '+',
284        help = param.description)
285
286def _cli_add_func_submission_specs(parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup], param: APIParam) -> None:
287    parser.add_argument('submission_specs', metavar = 'SUBMISSION',
288        action = 'extend', type = str, nargs = '+',
289        help = param.description)
290
291def _cli_add_func_upsert_zip(parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup], param: APIParam) -> None:
292    parser.add_argument('path', metavar = 'PATH',
293        action = 'store', type = str,
294        help = param.description)
295
296# This is used as an argparse argument type.
297# See: https://docs.python.org/3/library/argparse.html#type
298# This converts a csv string and returns a list of strings,
299# e.g. --to email1@gmail.com,email2@gmail.com -> ["email1@gmail.com", "email2@gmail.com"].
300# This will strip each compnent.
301def _csv_to_list(raw_argument: typing.Union[None, str]) -> typing.List[str]:
302    if (raw_argument is None):
303        raise ValueError('Parameter argument cannot be None.')
304
305    raw_argument = raw_argument.strip()
306    if (len(raw_argument) == 0):
307        raise ValueError('Parameter argument cannot be empty.')
308
309    return [part.strip() for part in raw_argument.split(CSV_TO_LIST_DELIMITER)]
310
311# Common API params.
312
313PARAM_ALLOW_LATE = APIParam(
314    'allow_late',
315    'Allow this submission to be graded, even if it is late.',
316    value_type = bool,
317    cli_default_value = False,
318)
319
320PARAM_ASSIGNMENT = APIParam(
321    'assignment',
322    'The ID of the assignment to make this request to.',
323    api_key = 'assignment-id',
324    cli_show_default = False,
325)
326
327PARAM_COURSE = APIParam(
328    'course',
329    'The ID of the course to make this request to.',
330    api_key = 'course-id',
331    cli_show_default = False,
332)
333
334PARAM_COURSE_USER_REFERENCES = APIParam(
335    'target_users',
336    ('A list of course user references.'
337    + ' Course user references may be specified in four ways:'
338    + ' 1) Email address of the requested user,'
339    + ' 2) "*" to request all users in the course,'
340    + ' 3) "<course role>" (e.g., student, grader) to request all course users with that role,'
341    + ' and 4) any of the previous options preceded by a minus sign (e.g., "-alice@test.edulinq.org", "-student")'
342    + ' to exclude that user or role from the request.'
343    + ' Default: All users in the course.'),
344    api_required = False,
345    value_type = list,
346    cli_action = 'extend',
347    cli_type = _csv_to_list,
348    cli_show_default = False,
349)
350
351PARAM_DRY_RUN = APIParam(
352    'dry_run',
353    'Do not commit/finalize the operation, just do all the steps and state what the result would look like.',
354    api_required = False,
355    value_type = bool,
356    cli_default_value = False,
357)
358
359PARAM_EMAIL_BODY = APIParam(
360    'body',
361    'The email body.',
362    api_required = False,
363)
364
365PARAM_EMAIL_COURSE_BCC = APIParam(
366    'bcc',
367    'A list of email addresses. Accepts course user references.',
368    api_required = False,
369    value_type = list,
370    cli_action = 'extend',
371    cli_type = _csv_to_list,
372    cli_show_default = False,
373)
374
375PARAM_EMAIL_COURSE_CC = APIParam(
376    'cc',
377    'A list of email addresses. Accepts course user references.',
378    api_required = False,
379    value_type = list,
380    cli_action = 'extend',
381    cli_type = _csv_to_list,
382    cli_show_default = False,
383)
384
385PARAM_EMAIL_COURSE_TO = APIParam(
386    'to',
387    ('A list of email addresses.'
388    + ' Accepts course user references.'
389    + ' Course user references may be specified in four ways:'
390    + ' 1) Email address of the requested user,'
391    + ' 2) "*" to request all users in the course,'
392    + ' 3) "<course role>" (e.g., student, grader)'
393    + ' to request all course users with that role,'
394    + ' and 4) any of the previous options preceded by a minus sign'
395    + ' (e.g., "-alice@test.edulinq.org", "-student")'
396    + ' to exclude that user or role from the request.'),
397    api_required = False,
398    value_type = list,
399    cli_action = 'extend',
400    cli_type = _csv_to_list,
401    cli_show_default = False,
402)
403
404PARAM_EMAIL_HTML = APIParam(
405    'email_html',
406    'Indicates the email body contains HTML.',
407    api_key = 'html',
408    cli_flag = 'html',
409    api_required = False,
410    value_type = bool,
411    cli_default_value = False,
412)
413
414PARAM_EMAIL_SUBJECT = APIParam(
415    'subject',
416    'The email subject.',
417    cli_required = True,
418)
419
420PARAM_FILESPEC_PART_PATH = APIParam(
421    'filespec_path',
422    'The path the filespec points to.',
423    api = False,
424    cli_required = True,
425    cli_flag = 'path',
426    cli_default_value = '',
427    cli_show_default = False,
428)
429
430PARAM_FILESPEC_PART_REFERENCE = APIParam(
431    'filespec_reference',
432    'The reference (e.g., git commit/branch) of the filespec.',
433    api = False,
434    cli_flag = 'reference',
435    cli_default_value = '',
436    cli_show_default = False,
437)
438
439PARAM_FILESPEC_PART_TOKEN = APIParam(
440    'filespec_token',
441    'The token for filespec authentication.',
442    api = False,
443    cli_flag = 'token',
444    cli_default_value = '',
445    cli_show_default = False,
446)
447
448PARAM_FILESPEC_PART_TYPE = APIParam(
449    'filespec_type',
450    'The type of filespec.',
451    api = False,
452    cli_required = True,
453    cli_flag = 'type',
454    cli_default_value = '',
455    cli_show_default = False,
456)
457
458PARAM_FILESPEC_PART_USERNAME = APIParam(
459    'filespec_username',
460    'The username for filespec authentication.',
461    api = False,
462    cli_flag = 'username',
463    cli_default_value = '',
464    cli_show_default = False,
465)
466
467PARAM_FORCE_COMPUTE = APIParam(
468    'force_compute',
469    'Force the server to compute the result, ignoring any existing cache.',
470    api_required = False,
471    value_type = bool,
472    cli_default_value = False,
473)
474
475PARAM_NAME = APIParam(
476    'name',
477    'An optional name to use.',
478    api_required = False,
479)
480
481PARAM_NEW_PASS = APIParam(
482    'new_pass',
483    'The new password.',
484    hash_value = True,
485    cli_required = True,
486)
487
488PARAM_NEW_USER_COURSE = APIParam(
489    'new_course',
490    'An optional course to enroll the new user in',
491    api = False,
492    cli_default_value = '',
493    cli_show_default = False,
494)
495
496PARAM_NEW_USER_COURSE_ROLE = APIParam(
497    'new_course_role',
498    'The course role for the new user.',
499    api = False,
500    cli_default_value = autograder.model.user.CourseRole.STUDENT.value,
501    cli_extra_options = {'choices': [role.value for role in autograder.model.user.CourseRole]},
502)
503
504PARAM_NEW_USER_EMAIL = APIParam(
505    'new_email',
506    'The email for the new user.',
507    api = False,
508    cli_required = True,
509)
510
511PARAM_NEW_USER_NAME = APIParam(
512    'new_name',
513    'The name for the new user.',
514    api = False,
515    cli_default_value = '',
516    cli_show_default = False,
517)
518
519PARAM_NEW_USER_PASS = APIParam(
520    'new_pass',
521    'The password for the new user (random if not supplied).',
522    api = False,
523    cli_default_value = '',
524    cli_show_default = False,
525)
526
527PARAM_NEW_USER_LMS_ID = APIParam(
528    'new_lms_id',
529    'The LMS ID for the new user.',
530    api = False,
531    cli_default_value = '',
532    cli_show_default = False,
533)
534
535PARAM_NEW_USER_SERVER_ROLE = APIParam(
536    'new_role',
537    'The server role for the new user.',
538    api = False,
539    cli_default_value = autograder.model.user.ServerRole.USER.value,
540    cli_extra_options = {'choices': [role.value for role in autograder.model.user.ServerRole]},
541)
542
543PARAM_OVERWRITE_RECORDS = APIParam(
544    'overwrite_records',
545    'Replace any existing records that match the current operation (e.g. re-do existing results).',
546    value_type = bool,
547    cli_default_value = False,
548)
549
550PARAM_OUT_DIR = APIParam(
551    'out_dir',
552    'A directory to write output in.',
553    api = False,
554    cli_default_value = '.',
555)
556
557PARAM_PROXY_EMAIL = APIParam(
558    'proxy_email',
559    'The email of the user the request is pretending to be made under (the submission will be made on behalf of this user).',
560    cli_required = True,
561)
562
563PARAM_PROXY_TIME = APIParam(
564    'proxy_time',
565    ('The proxy timestamp that will be applied to the request.'
566    + ' By default, the earlier time between now and'
567    + ' one minute before the due date will be used.'
568    + ' Use this option to manually set the proxy time.'
569    + ' Timestamps are milliseconds from the UNIX epoch'
570    + ' (https://en.wikipedia.org/wiki/Unix_time).'),
571    value_type = edq.util.time.Timestamp,
572    cli_type = str,
573    api_required = False,
574)
575
576PARAM_QUERY_AFTER = APIParam(
577    'query_after',
578    'If supplied, only return records after this timestamp.',
579    api_key = 'after',
580    cli_flag = 'after',
581    value_type = edq.util.time.Timestamp,
582    api_required = False,
583)
584
585PARAM_QUERY_BEFORE = APIParam(
586    'query_before',
587    'If supplied, only return records before this timestamp.',
588    api_key = 'before',
589    cli_flag = 'before',
590    value_type = edq.util.time.Timestamp,
591    api_required = False,
592)
593
594PARAM_QUERY_LIMIT = APIParam(
595    'limit',
596    'The maximum number of records to return.',
597    api_key = 'limit',
598    cli_flag = 'limit',
599    value_type = int,
600    api_required = False,
601)
602
603PARAM_QUERY_LOG_LEVEL = APIParam(
604    'query_level',
605    'The minimum level of log records to return.',
606    api_key = 'level',
607    cli_flag = 'level',
608    api_required = False,
609    cli_default_value = 'INFO',
610    cli_extra_options = {'choices': autograder.model.log.LOG_LEVEL_TEXT_TO_INT.keys()}
611)
612
613PARAM_QUERY_METRIC_TYPE = APIParam(
614    'query_metric_type',
615    'The type of metric to query for. See: https://github.com/edulinq/autograder-server/blob/main/internal/stats/metrics.go#L29',
616    api_key = 'type',
617    cli_flag = 'metric-type',
618    cli_required = True,
619    cli_extra_options = {'choices': autograder.model.stats.METRIC_TYPES}
620)
621
622PARAM_QUERY_PAST = APIParam(
623    'query_past',
624    'If supplied, only return log records in this duration (using "h", "m", or "s" suffixes) (e.g., "24h", "10m", or "1h10m10s").',
625    api_key = 'past',
626    cli_flag = 'past',
627    api_required = False,
628)
629
630PARAM_QUERY_SORT = APIParam(
631    'query_sort',
632    'Sort the results. -1 for ascending, 0 for no sorting, 1 for descending.',
633    api_key = 'sort',
634    cli_flag = 'sort',
635    value_type = int,
636    api_required = False,
637)
638
639PARAM_QUERY_TARGET_ASSIGNMENT = APIParam(
640    'query_target_assignment',
641    'If supplied, only return records for this assignment.',
642    api_key = 'target-assignment',
643    cli_flag = 'target-assignment',
644    api_required = False,
645    cli_show_default = False,
646)
647
648PARAM_QUERY_TARGET_COURSE = APIParam(
649    'query_target_course',
650    'If supplied, only return records for this course.',
651    api_key = 'target-course',
652    cli_flag = 'target-course',
653    api_required = False,
654    cli_show_default = False,
655)
656
657PARAM_QUERY_TARGET_EMAIL = APIParam(
658    'query_target_email',
659    'If supplied, only return records for this user.',
660    api_key = 'target-email',
661    cli_flag = 'target-email',
662    api_required = False,
663    cli_show_default = False,
664)
665
666PARAM_QUERY_USE_TESTING_DATA = APIParam(
667    'query_use_testing_data',
668    'Query from hard-coded testing data (instead of real data).',
669    api_key = 'use-testing-data',
670    value_type = bool,
671    api_required = False,
672    cli = False,
673)
674
675PARAM_QUERY_WHERE = APIParam(
676    'query_where',
677    'Only includes records with a patching key/value pair.',
678    api_key = 'where',
679    cli_flag = 'where',
680    api_required = False,
681    value_type = dict,
682    cli_action = ArgumentMap,
683    cli_extra_options = {
684        'metavar': (f"KEY{MAP_KEY_VALUE_SEP}VALUE"),
685        'nargs': '+',
686    },
687)
688
689PARAM_RAW_COURSE_USERS = APIParam(
690    'raw_course_users',
691    'Raw course users to operate on.',
692    value_type = list,
693    cli = False,
694    skip_clean = True,
695)
696
697PARAM_RAW_SERVER_USERS = APIParam(
698    'raw_server_users',
699    'Raw server users to operate on.',
700    value_type = list,
701    api_key = 'raw-users',
702    cli = False,
703    skip_clean = True,
704)
705
706PARAM_REGRADE_CUTOFF = APIParam(
707    'regrade_cutoff',
708    ('All submissions occurring before the cutoff time will be regraded.'
709    + ' By default, the current time will be used.'
710    + ' Time is milliseconds from the UNIX epoch'
711    + ' (https://en.wikipedia.org/wiki/Unix_time).'),
712    value_type = int,
713    api_required = False,
714)
715
716PARAM_SEND_EMAILS = APIParam(
717    'send_emails',
718    'Send any relevant emails to users affected by this operation (e.g., a user being enrolled in a course).',
719    value_type = bool,
720    cli_default_value = False,
721)
722
723PARAM_SERVER = APIParam(
724    'server',
725    'The URL of the autograder server to communicate with.',
726    cli_show_default = False,
727)
728
729PARAM_SKIP_BUILD_IMAGES = APIParam(
730    'skip_build_images',
731    'Skip building assignment Docker images.',
732    value_type = bool,
733    cli_default_value = False,
734)
735
736PARAM_SKIP_EMAILS = APIParam(
737    'skip_emails',
738    'Skip sending any emails.',
739    value_type = bool,
740    cli_default_value = False,
741)
742
743PARAM_SKIP_INSERTS = APIParam(
744    'skip_inserts',
745    'Skip insert operations.',
746    value_type = bool,
747    cli_default_value = False,
748)
749
750PARAM_SKIP_LMS_SYNC = APIParam(
751    'skip_lms_sync',
752    'Skip syncing with the LMS.',
753    value_type = bool,
754    cli_default_value = False,
755)
756
757PARAM_SKIP_SOURCE_SYNC = APIParam(
758    'skip_source_sync',
759    'Skip syncing (updating with) the course source.',
760    value_type = bool,
761    cli_default_value = False,
762)
763
764PARAM_SKIP_TEMPLATE_FILES = APIParam(
765    'skip_template_files',
766    'Skip fetching assignment template files.',
767    value_type = bool,
768    cli_default_value = False,
769)
770
771PARAM_SKIP_UPDATES = APIParam(
772    'skip_updates',
773    'Skip update operations.',
774    value_type = bool,
775    cli_default_value = False,
776)
777
778PARAM_SUBMISSION_MESSAGE = APIParam(
779    'message',
780    'An optional message to attach to the submission.',
781    api_required = False,
782)
783
784PARAM_SUBMISSION_FILES = APIParam(
785    'files',
786    'The path to your submission file(s).',
787    value_type = list,
788    api = False,
789    cli_add_func = _cli_add_func_submission_files,
790)
791
792PARAM_SUBMISSION_SPECS = APIParam(
793    'submission_specs',
794    ('A list of submission specifications to analyze.'
795    + ' Submissions may span courses and assignments.'
796    + ' Submissions may be specified in three ways:'
797    + ' 1) "<course id>::<assignment id>::<user email>::<submission short id> for a specific submission,'
798    + ' 2) "<course id>::<assignment id>::<user email> for the given user\'s most recent submission to the given assignment,'
799    + ' and 3) "<course id>::<assignment id> for the most recent submission for all students.'),
800    api_key = 'submissions',
801    cli_flag = 'submissions',
802    value_type = list,
803    cli_add_func = _cli_add_func_submission_specs,
804)
805
806PARAM_TARGET_EMAIL = APIParam(
807    'target_email',
808    'The email of the user that is the target of this request.',
809)
810
811PARAM_TARGET_EMAIL_OR_SELF = APIParam(
812    'target_email',
813    'The email of the user that is the target of this request (defaults to you).',
814    api_required = False,
815)
816
817PARAM_TARGET_USER_OR_SELF = APIParam(
818    'target_user',
819    'The user that is the target of this request (defaults to you).',
820    api_required = False,
821)
822
823PARAM_TARGET_USERS = APIParam(
824    'target_users',
825    ('A list of server user references.'
826    + ' Server user references may be specified in eight ways:'
827    + ' 1) Email address of the requested user,'
828    + ' 2) "*" to request all users in the server,'
829    + ' 3) "<server role>" (e.g., user, creator)'
830    + ' to request all users with that server role,'
831    + ' 4) "<course id>::<course role>" (e.g., course101::student)'
832    + ' to request all users in the target course with that course role,'
833    + ' 5) "*::<course role>" (e.g., *::student)'
834    + ' to request all users with that course role in any course,'
835    + ' 6) "<course id>::*" to request all users in the target course,'
836    + ' 7) "*::*" to request all users enrolled in at least one course,'
837    + ' and 8) any of the previous options preceded by a minus sign'
838    + ' (e.g., "-alice@test.edulinq.org", "-user", "-*::student")'
839    + ' to exclude that user or group from the request.'
840    + ' Default: All users in the server.'),
841    api_required = False,
842    value_type = list,
843    cli_action = 'extend',
844    cli_type = _csv_to_list,
845    cli_show_default = False,
846)
847
848PARAM_TARGET_SUBMISSION_OR_RECENT = APIParam(
849    'target_submission',
850    'The ID of the submission (default to the most recent submission).',
851    api_required = False,
852)
853
854PARAM_TOKEN_ID = APIParam(
855    'token_id',
856    'The id of the token to target.',
857    cli_required = True,
858)
859
860PARAM_UPSERT_FILESPEC = APIParam(
861    'filespec',
862    'A filespec pointing to a course to upload.',
863    value_type = autograder.filespec.FileSpec,
864    cli = False,
865)
866
867PARAM_UPSERT_ZIP = APIParam(
868    'path',
869    ('The path to your course material.'
870    + ' Either a zip file (with .zip extension) or directory (that will get zipped).'),
871    value_type = str,
872    api = False,
873    cli_add_func = _cli_add_func_upsert_zip,
874)
875
876PARAM_USER_EMAIL = APIParam(
877    'user',
878    'The email of the user making this request.',
879    alt_config_key = 'auth_user',
880    api_key = 'user-email',
881    cli_flag = 'user',
882    cli_show_default = False,
883)
884
885PARAM_USER_PASS = APIParam(
886    'pass',
887    'The password of the user making this request.',
888    alt_config_key = 'auth_pass',
889    api_key = 'user-pass',
890    cli_flag = 'pass',
891    hash_value = True,
892    cli_show_default = False,
893)
894
895PARAM_WAIT_FOR_COMPLETION = APIParam(
896    'wait_for_completion',
897    'Wait for the full job to complete before returning.',
898    value_type = bool,
899    cli_default_value = False,
900)
CSV_TO_LIST_DELIMITER: str = ','
MAP_KEY_VALUE_SEP: str = '='
DEFAULT_CLI_ACTIONS: Dict[Type, str] = {<class 'bool'>: 'store_true', <class 'edq.util.time.Timestamp'>: 'store', <class 'float'>: 'store', <class 'int'>: 'store', <class 'str'>: 'store'}

Map value types to default CLI actions.

@typing.runtime_checkable
class CLIAddFunction(typing.Protocol):
30@typing.runtime_checkable
31class CLIAddFunction(typing.Protocol):
32    """ A function for manually adding APIParams to a parser. """
33
34    def __call__(self, parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup], param: 'APIParam') -> None:
35        """
36        Add the given APIParam to the parser.
37        """

A function for manually adding APIParams to a parser.

CLIAddFunction(*args, **kwargs)
1953def _no_init_or_replace_init(self, *args, **kwargs):
1954    cls = type(self)
1955
1956    if cls._is_protocol:
1957        raise TypeError('Protocols cannot be instantiated')
1958
1959    # Already using a custom `__init__`. No need to calculate correct
1960    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1961    if cls.__init__ is not _no_init_or_replace_init:
1962        return
1963
1964    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1965    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1966    # searches for a proper new `__init__` in the MRO. The new `__init__`
1967    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1968    # instantiation of the protocol subclass will thus use the new
1969    # `__init__` and no longer call `_no_init_or_replace_init`.
1970    for base in cls.__mro__:
1971        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1972        if init is not _no_init_or_replace_init:
1973            cls.__init__ = init
1974            break
1975    else:
1976        # should not happen
1977        cls.__init__ = object.__init__
1978
1979    cls.__init__(self, *args, **kwargs)
class APIParam:
 39class APIParam:
 40    """
 41    A definition for a parameter to the autograder API.
 42    This class also contains information for representing these parameters in
 43    config (dicts), on the CLI (argparse options), and in the payload of autograder requests.
 44    """
 45
 46    def __init__(self,
 47            config_key: str,
 48            description: str,
 49            alt_config_key: typing.Union[str, None] = None,
 50            api_key: typing.Union[str, None] = None,
 51            cli_flag: typing.Union[str, None] = None,
 52            api: bool = True,
 53            api_required: typing.Union[bool, None] = None,  # Default to the value of `api`.
 54            cli: bool = True,
 55            cli_required: typing.Union[bool, None] = None,  # Default to false.
 56            value_type: typing.Type = str,
 57            cli_type: typing.Union[typing.Any, None] = None,
 58            skip_clean: bool = False,
 59            hash_value: bool = False,
 60            omit_empty: bool = True,
 61            cli_action: typing.Union[typing.Any, None] = None,
 62            cli_default_value: typing.Any = None,
 63            cli_show_default: typing.Union[bool, None] = None,
 64            cli_extra_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
 65            cli_add_func: typing.Union[CLIAddFunction, None] = None,
 66            **kwargs: typing.Any) -> None:
 67        if (len(config_key.strip()) == 0):
 68            raise autograder.error.APIError(None, "APIParam cannot have an empty key.")
 69
 70        self.config_key: str = config_key
 71        """
 72        The main key/label used to reference this parameter.
 73        Will be used in the application config and config dicts.
 74        """
 75
 76        if (len(description) == 0):
 77            raise autograder.error.APIError(None, "APIParam cannot have an empty description.")
 78
 79        self.description: str = description
 80        """ A description used for this parameter. """
 81
 82        self.alt_config_key: typing.Union[str, None] = alt_config_key
 83        """
 84        An alternative config key that the value could be found under when reading a config file.
 85        A value found under this key will always be stored under self.config_key.
 86        """
 87
 88        if (api_key is None):
 89            api_key = config_key.replace('_', '-')
 90
 91        self.api_key: str = api_key
 92        """ The key in the API request payload for this parameter. """
 93
 94        if (cli_flag is None):
 95            cli_flag = config_key.replace('_', '-')
 96
 97        self.cli_flag: str = cli_flag
 98        """ The flag used on the CLI for this parameter. """
 99
100        self.api: bool = api
101        """ If this parameter should be included on the API payload. """
102
103        if (api_required is None):
104            api_required = api
105
106        self.api_required: bool = api_required
107        """ If this parameter is required when calling the API. """
108
109        self.cli: bool = cli
110        """ If this parameter should be included on the CLI. """
111
112        if (cli_required is None):
113            cli_required = False
114
115        self.cli_required: bool = cli_required
116        """ If this parameter is required on the CLI. """
117
118        self.value_type: typing.Type = value_type
119        """ The type that a clean value from this parameter should be. """
120
121        if (cli_type is None):
122            cli_type = self.value_type
123
124        self.cli_type: typing.Any = cli_type
125        """
126        The type of value that will be looked for on the CLI.
127        Defaults to the same as `self.value_type`.
128
129        Note that this value will be used as the `type` argument to `argparse.ArgumentParser.add_argument()`.
130        """
131
132        self.skip_clean: bool = skip_clean
133        """ Skip any cleaning steps for this parameter's value. """
134
135        self.hash_value: bool = hash_value
136        """ Hash the value during the cleaning processes (typically used for passwords). """
137
138        self.omit_empty: bool = omit_empty
139        """ Do not include this parameter in the API payload if its value is empty. """
140
141        self.cli_default_value: typing.Any = cli_default_value
142        """ The default value for the CLI argument. """
143
144        if (cli_show_default is None):
145            # Usually we will want to show the default value,
146            # but boolean flags don't make much sense to show defaults with.
147            cli_show_default = (value_type != bool)
148
149        self.cli_show_default: bool = cli_show_default
150        """ Show the default value (in CLI's help) for a non-required param. """
151
152        if (cli_extra_options is None):
153            cli_extra_options = {}
154
155        self.cli_extra_options: typing.Dict[str, typing.Any] = cli_extra_options
156        """
157        Additional options to pass (as kwargs) to `argparse.ArgumentParser.add_argument()`.
158        These should not conflict with the options set using the direct members.
159        """
160
161        self.cli_add_func: typing.Union[CLIAddFunction, None] = cli_add_func
162        """ A function that can be used to override the standard behavior in add_to_parser(). """
163
164        if (self.cli and (self.cli_add_func is None) and (cli_action is None)):
165            # Choose action based on the value's type.
166            cli_action = DEFAULT_CLI_ACTIONS.get(self.value_type, None)
167            if (cli_action is None):
168                raise ValueError(f"Unknown action for value type '{self.value_type}'.")
169
170        self.cli_action: typing.Any = cli_action
171        """ The value that value will be used as the `action` argument to `argparse.ArgumentParser.add_argument()`. """
172
173    def clean_value(self, value: typing.Union[typing.Any, None]) -> typing.Union[typing.Any, None]:
174        """
175        Return a clean version of the given value (according to this API param).
176        An error will be raised if cleaning fails.
177        Empty values will be returned as None.
178        """
179
180        if (self.skip_clean):
181            return value
182
183        if (value is None):
184            return None
185
186        if (issubclass(self.value_type, edq.util.time.Timestamp)):
187            return edq.util.time.Timestamp.guess(value)
188
189        if (issubclass(self.value_type, bool)):
190            return edq.util.parse.boolean(value)
191
192        if (issubclass(self.value_type, int)):
193            return int(value)
194
195        if (issubclass(self.value_type, list)):
196            return autograder.util.parse.string_list(value)
197
198        if (issubclass(self.value_type, str)):
199            value = str(value).strip()
200            if (len(value) == 0):
201                return None
202
203            if (self.hash_value):
204                value = edq.util.hash.sha256_hex(value)
205
206        if (issubclass(self.value_type, autograder.filespec.FileSpec)):
207            return autograder.filespec.parse(value)
208
209        return value
210
211    def optional(self) -> 'APIParam':
212        """ Make a copy of this APIParam, mark the copy as optional, and return the copy. """
213
214        new_param = copy.deepcopy(self)
215        new_param.api_required = False
216        new_param.cli_required = False
217        return new_param
218
219    def required(self) -> 'APIParam':
220        """ Make a copy of this APIParam, mark the copy as required, and return the copy. """
221
222        new_param = copy.deepcopy(self)
223        new_param.api_required = True
224        new_param.cli_required = True
225        return new_param
226
227    def add_to_parser(self, parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup]) -> None:
228        """
229        Add this API parameter to the given parser.
230        If this parameter is not supposed to be represented as a CLI argument, it will not be added.
231        """
232
233        if (not self.cli):
234            return
235
236        # Check if there is a specialized function for this param.
237        if (self.cli_add_func is not None):
238            self.cli_add_func(parser, self)
239            return
240
241        kwargs = {
242            'dest': self.config_key,
243            'action': self.cli_action,
244            'help': self.description,
245        }
246
247        if (self.cli_action not in {'store_true', 'store_false'}):
248            kwargs['type'] = self.cli_type
249
250        if (self.cli_required):
251            kwargs['required'] = True
252        else:
253            kwargs['required'] = False
254            kwargs['default'] = self.cli_default_value
255
256            if (self.cli_show_default):
257                kwargs['help'] += ' (default: %(default)s)'
258
259        kwargs.update(self.cli_extra_options)
260
261        parser.add_argument(f'--{self.cli_flag}', **kwargs)

A definition for a parameter to the autograder API. This class also contains information for representing these parameters in config (dicts), on the CLI (argparse options), and in the payload of autograder requests.

APIParam( config_key: str, description: str, alt_config_key: Optional[str] = None, api_key: Optional[str] = None, cli_flag: Optional[str] = None, api: bool = True, api_required: Optional[bool] = None, cli: bool = True, cli_required: Optional[bool] = None, value_type: Type = <class 'str'>, cli_type: Optional[Any] = None, skip_clean: bool = False, hash_value: bool = False, omit_empty: bool = True, cli_action: Optional[Any] = None, cli_default_value: Any = None, cli_show_default: Optional[bool] = None, cli_extra_options: Optional[Dict[str, Any]] = None, cli_add_func: Optional[CLIAddFunction] = None, **kwargs: Any)
 46    def __init__(self,
 47            config_key: str,
 48            description: str,
 49            alt_config_key: typing.Union[str, None] = None,
 50            api_key: typing.Union[str, None] = None,
 51            cli_flag: typing.Union[str, None] = None,
 52            api: bool = True,
 53            api_required: typing.Union[bool, None] = None,  # Default to the value of `api`.
 54            cli: bool = True,
 55            cli_required: typing.Union[bool, None] = None,  # Default to false.
 56            value_type: typing.Type = str,
 57            cli_type: typing.Union[typing.Any, None] = None,
 58            skip_clean: bool = False,
 59            hash_value: bool = False,
 60            omit_empty: bool = True,
 61            cli_action: typing.Union[typing.Any, None] = None,
 62            cli_default_value: typing.Any = None,
 63            cli_show_default: typing.Union[bool, None] = None,
 64            cli_extra_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
 65            cli_add_func: typing.Union[CLIAddFunction, None] = None,
 66            **kwargs: typing.Any) -> None:
 67        if (len(config_key.strip()) == 0):
 68            raise autograder.error.APIError(None, "APIParam cannot have an empty key.")
 69
 70        self.config_key: str = config_key
 71        """
 72        The main key/label used to reference this parameter.
 73        Will be used in the application config and config dicts.
 74        """
 75
 76        if (len(description) == 0):
 77            raise autograder.error.APIError(None, "APIParam cannot have an empty description.")
 78
 79        self.description: str = description
 80        """ A description used for this parameter. """
 81
 82        self.alt_config_key: typing.Union[str, None] = alt_config_key
 83        """
 84        An alternative config key that the value could be found under when reading a config file.
 85        A value found under this key will always be stored under self.config_key.
 86        """
 87
 88        if (api_key is None):
 89            api_key = config_key.replace('_', '-')
 90
 91        self.api_key: str = api_key
 92        """ The key in the API request payload for this parameter. """
 93
 94        if (cli_flag is None):
 95            cli_flag = config_key.replace('_', '-')
 96
 97        self.cli_flag: str = cli_flag
 98        """ The flag used on the CLI for this parameter. """
 99
100        self.api: bool = api
101        """ If this parameter should be included on the API payload. """
102
103        if (api_required is None):
104            api_required = api
105
106        self.api_required: bool = api_required
107        """ If this parameter is required when calling the API. """
108
109        self.cli: bool = cli
110        """ If this parameter should be included on the CLI. """
111
112        if (cli_required is None):
113            cli_required = False
114
115        self.cli_required: bool = cli_required
116        """ If this parameter is required on the CLI. """
117
118        self.value_type: typing.Type = value_type
119        """ The type that a clean value from this parameter should be. """
120
121        if (cli_type is None):
122            cli_type = self.value_type
123
124        self.cli_type: typing.Any = cli_type
125        """
126        The type of value that will be looked for on the CLI.
127        Defaults to the same as `self.value_type`.
128
129        Note that this value will be used as the `type` argument to `argparse.ArgumentParser.add_argument()`.
130        """
131
132        self.skip_clean: bool = skip_clean
133        """ Skip any cleaning steps for this parameter's value. """
134
135        self.hash_value: bool = hash_value
136        """ Hash the value during the cleaning processes (typically used for passwords). """
137
138        self.omit_empty: bool = omit_empty
139        """ Do not include this parameter in the API payload if its value is empty. """
140
141        self.cli_default_value: typing.Any = cli_default_value
142        """ The default value for the CLI argument. """
143
144        if (cli_show_default is None):
145            # Usually we will want to show the default value,
146            # but boolean flags don't make much sense to show defaults with.
147            cli_show_default = (value_type != bool)
148
149        self.cli_show_default: bool = cli_show_default
150        """ Show the default value (in CLI's help) for a non-required param. """
151
152        if (cli_extra_options is None):
153            cli_extra_options = {}
154
155        self.cli_extra_options: typing.Dict[str, typing.Any] = cli_extra_options
156        """
157        Additional options to pass (as kwargs) to `argparse.ArgumentParser.add_argument()`.
158        These should not conflict with the options set using the direct members.
159        """
160
161        self.cli_add_func: typing.Union[CLIAddFunction, None] = cli_add_func
162        """ A function that can be used to override the standard behavior in add_to_parser(). """
163
164        if (self.cli and (self.cli_add_func is None) and (cli_action is None)):
165            # Choose action based on the value's type.
166            cli_action = DEFAULT_CLI_ACTIONS.get(self.value_type, None)
167            if (cli_action is None):
168                raise ValueError(f"Unknown action for value type '{self.value_type}'.")
169
170        self.cli_action: typing.Any = cli_action
171        """ The value that value will be used as the `action` argument to `argparse.ArgumentParser.add_argument()`. """
config_key: str

The main key/label used to reference this parameter. Will be used in the application config and config dicts.

description: str

A description used for this parameter.

alt_config_key: Optional[str]

An alternative config key that the value could be found under when reading a config file. A value found under this key will always be stored under self.config_key.

api_key: str

The key in the API request payload for this parameter.

cli_flag: str

The flag used on the CLI for this parameter.

api: bool

If this parameter should be included on the API payload.

api_required: bool

If this parameter is required when calling the API.

cli: bool

If this parameter should be included on the CLI.

cli_required: bool

If this parameter is required on the CLI.

value_type: Type

The type that a clean value from this parameter should be.

cli_type: Any

The type of value that will be looked for on the CLI. Defaults to the same as self.value_type.

Note that this value will be used as the type argument to argparse.ArgumentParser.add_argument().

skip_clean: bool

Skip any cleaning steps for this parameter's value.

hash_value: bool

Hash the value during the cleaning processes (typically used for passwords).

omit_empty: bool

Do not include this parameter in the API payload if its value is empty.

cli_default_value: Any

The default value for the CLI argument.

cli_show_default: bool

Show the default value (in CLI's help) for a non-required param.

cli_extra_options: Dict[str, Any]

Additional options to pass (as kwargs) to argparse.ArgumentParser.add_argument(). These should not conflict with the options set using the direct members.

cli_add_func: Optional[CLIAddFunction]

A function that can be used to override the standard behavior in add_to_parser().

cli_action: Any

The value that value will be used as the action argument to argparse.ArgumentParser.add_argument().

def clean_value(self, value: Optional[Any]) -> Optional[Any]:
173    def clean_value(self, value: typing.Union[typing.Any, None]) -> typing.Union[typing.Any, None]:
174        """
175        Return a clean version of the given value (according to this API param).
176        An error will be raised if cleaning fails.
177        Empty values will be returned as None.
178        """
179
180        if (self.skip_clean):
181            return value
182
183        if (value is None):
184            return None
185
186        if (issubclass(self.value_type, edq.util.time.Timestamp)):
187            return edq.util.time.Timestamp.guess(value)
188
189        if (issubclass(self.value_type, bool)):
190            return edq.util.parse.boolean(value)
191
192        if (issubclass(self.value_type, int)):
193            return int(value)
194
195        if (issubclass(self.value_type, list)):
196            return autograder.util.parse.string_list(value)
197
198        if (issubclass(self.value_type, str)):
199            value = str(value).strip()
200            if (len(value) == 0):
201                return None
202
203            if (self.hash_value):
204                value = edq.util.hash.sha256_hex(value)
205
206        if (issubclass(self.value_type, autograder.filespec.FileSpec)):
207            return autograder.filespec.parse(value)
208
209        return value

Return a clean version of the given value (according to this API param). An error will be raised if cleaning fails. Empty values will be returned as None.

def optional(self) -> APIParam:
211    def optional(self) -> 'APIParam':
212        """ Make a copy of this APIParam, mark the copy as optional, and return the copy. """
213
214        new_param = copy.deepcopy(self)
215        new_param.api_required = False
216        new_param.cli_required = False
217        return new_param

Make a copy of this APIParam, mark the copy as optional, and return the copy.

def required(self) -> APIParam:
219    def required(self) -> 'APIParam':
220        """ Make a copy of this APIParam, mark the copy as required, and return the copy. """
221
222        new_param = copy.deepcopy(self)
223        new_param.api_required = True
224        new_param.cli_required = True
225        return new_param

Make a copy of this APIParam, mark the copy as required, and return the copy.

def add_to_parser( self, parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup]) -> None:
227    def add_to_parser(self, parser: typing.Union[argparse.ArgumentParser, argparse._ArgumentGroup]) -> None:
228        """
229        Add this API parameter to the given parser.
230        If this parameter is not supposed to be represented as a CLI argument, it will not be added.
231        """
232
233        if (not self.cli):
234            return
235
236        # Check if there is a specialized function for this param.
237        if (self.cli_add_func is not None):
238            self.cli_add_func(parser, self)
239            return
240
241        kwargs = {
242            'dest': self.config_key,
243            'action': self.cli_action,
244            'help': self.description,
245        }
246
247        if (self.cli_action not in {'store_true', 'store_false'}):
248            kwargs['type'] = self.cli_type
249
250        if (self.cli_required):
251            kwargs['required'] = True
252        else:
253            kwargs['required'] = False
254            kwargs['default'] = self.cli_default_value
255
256            if (self.cli_show_default):
257                kwargs['help'] += ' (default: %(default)s)'
258
259        kwargs.update(self.cli_extra_options)
260
261        parser.add_argument(f'--{self.cli_flag}', **kwargs)

Add this API parameter to the given parser. If this parameter is not supposed to be represented as a CLI argument, it will not be added.

class ArgumentMap(argparse.Action):
263class ArgumentMap(argparse.Action):
264    """ An argparse Action for key/value pairs separated with MAP_KEY_VALUE_SEP. """
265
266    def __call__(self, parser, namespace, raw_values, option_string = None):  # type: ignore[no-untyped-def]
267        if (not hasattr(namespace, self.dest)):
268            setattr(namespace, self.dest, {})
269
270        all_values = getattr(namespace, self.dest)
271        if (all_values is None):
272            all_values = {}
273            setattr(namespace, self.dest, all_values)
274
275        for raw_value in raw_values:
276            parts = raw_value.split(MAP_KEY_VALUE_SEP, 1)
277            if (len(parts) != 2):
278                raise ValueError(f"Argument map key/value pair ('{raw_value}') is missing a separator '{MAP_KEY_VALUE_SEP}'.")
279
280            all_values[parts[0].strip()] = parts[1].strip()

An argparse Action for key/value pairs separated with MAP_KEY_VALUE_SEP.

PARAM_ALLOW_LATE = <APIParam object>
PARAM_ASSIGNMENT = <APIParam object>
PARAM_COURSE = <APIParam object>
PARAM_COURSE_USER_REFERENCES = <APIParam object>
PARAM_DRY_RUN = <APIParam object>
PARAM_EMAIL_BODY = <APIParam object>
PARAM_EMAIL_COURSE_BCC = <APIParam object>
PARAM_EMAIL_COURSE_CC = <APIParam object>
PARAM_EMAIL_COURSE_TO = <APIParam object>
PARAM_EMAIL_HTML = <APIParam object>
PARAM_EMAIL_SUBJECT = <APIParam object>
PARAM_FILESPEC_PART_PATH = <APIParam object>
PARAM_FILESPEC_PART_REFERENCE = <APIParam object>
PARAM_FILESPEC_PART_TOKEN = <APIParam object>
PARAM_FILESPEC_PART_TYPE = <APIParam object>
PARAM_FILESPEC_PART_USERNAME = <APIParam object>
PARAM_FORCE_COMPUTE = <APIParam object>
PARAM_NAME = <APIParam object>
PARAM_NEW_PASS = <APIParam object>
PARAM_NEW_USER_COURSE = <APIParam object>
PARAM_NEW_USER_COURSE_ROLE = <APIParam object>
PARAM_NEW_USER_EMAIL = <APIParam object>
PARAM_NEW_USER_NAME = <APIParam object>
PARAM_NEW_USER_PASS = <APIParam object>
PARAM_NEW_USER_LMS_ID = <APIParam object>
PARAM_NEW_USER_SERVER_ROLE = <APIParam object>
PARAM_OVERWRITE_RECORDS = <APIParam object>
PARAM_OUT_DIR = <APIParam object>
PARAM_PROXY_EMAIL = <APIParam object>
PARAM_PROXY_TIME = <APIParam object>
PARAM_QUERY_AFTER = <APIParam object>
PARAM_QUERY_BEFORE = <APIParam object>
PARAM_QUERY_LIMIT = <APIParam object>
PARAM_QUERY_LOG_LEVEL = <APIParam object>
PARAM_QUERY_METRIC_TYPE = <APIParam object>
PARAM_QUERY_PAST = <APIParam object>
PARAM_QUERY_SORT = <APIParam object>
PARAM_QUERY_TARGET_ASSIGNMENT = <APIParam object>
PARAM_QUERY_TARGET_COURSE = <APIParam object>
PARAM_QUERY_TARGET_EMAIL = <APIParam object>
PARAM_QUERY_USE_TESTING_DATA = <APIParam object>
PARAM_QUERY_WHERE = <APIParam object>
PARAM_RAW_COURSE_USERS = <APIParam object>
PARAM_RAW_SERVER_USERS = <APIParam object>
PARAM_REGRADE_CUTOFF = <APIParam object>
PARAM_SEND_EMAILS = <APIParam object>
PARAM_SERVER = <APIParam object>
PARAM_SKIP_BUILD_IMAGES = <APIParam object>
PARAM_SKIP_EMAILS = <APIParam object>
PARAM_SKIP_INSERTS = <APIParam object>
PARAM_SKIP_LMS_SYNC = <APIParam object>
PARAM_SKIP_SOURCE_SYNC = <APIParam object>
PARAM_SKIP_TEMPLATE_FILES = <APIParam object>
PARAM_SKIP_UPDATES = <APIParam object>
PARAM_SUBMISSION_MESSAGE = <APIParam object>
PARAM_SUBMISSION_FILES = <APIParam object>
PARAM_SUBMISSION_SPECS = <APIParam object>
PARAM_TARGET_EMAIL = <APIParam object>
PARAM_TARGET_EMAIL_OR_SELF = <APIParam object>
PARAM_TARGET_USER_OR_SELF = <APIParam object>
PARAM_TARGET_USERS = <APIParam object>
PARAM_TARGET_SUBMISSION_OR_RECENT = <APIParam object>
PARAM_TOKEN_ID = <APIParam object>
PARAM_UPSERT_FILESPEC = <APIParam object>
PARAM_UPSERT_ZIP = <APIParam object>
PARAM_USER_EMAIL = <APIParam object>
PARAM_USER_PASS = <APIParam object>
PARAM_WAIT_FOR_COMPLETION = <APIParam object>