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 CSV 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 CSV 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 CSV 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 CSV 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_MESSAGE = APIParam(
476    'message',
477    'An optional message to include with this operation.',
478    api_required = False,
479)
480
481PARAM_NAME = APIParam(
482    'name',
483    'An optional name to use.',
484    api_required = False,
485)
486
487PARAM_NEW_PASS = APIParam(
488    'new_pass',
489    'The new password.',
490    hash_value = True,
491    cli_required = True,
492)
493
494PARAM_NEW_USER_COURSE = APIParam(
495    'new_course',
496    'An optional course to enroll the new user in',
497    api = False,
498    cli_default_value = '',
499    cli_show_default = False,
500)
501
502PARAM_NEW_USER_COURSE_ROLE = APIParam(
503    'new_course_role',
504    'The course role for the new user.',
505    api = False,
506    cli_default_value = autograder.model.user.CourseRole.STUDENT.value,
507    cli_extra_options = {'choices': [role.value for role in autograder.model.user.CourseRole]},
508)
509
510PARAM_NEW_USER_EMAIL = APIParam(
511    'new_email',
512    'The email for the new user.',
513    api = False,
514    cli_required = True,
515)
516
517PARAM_NEW_USER_NAME = APIParam(
518    'new_name',
519    'The name for the new user.',
520    api = False,
521    cli_default_value = '',
522    cli_show_default = False,
523)
524
525PARAM_NEW_USER_PASS = APIParam(
526    'new_pass',
527    'The password for the new user (random if not supplied).',
528    api = False,
529    cli_default_value = '',
530    cli_show_default = False,
531)
532
533PARAM_NEW_USER_LMS_ID = APIParam(
534    'new_lms_id',
535    'The LMS ID for the new user.',
536    api = False,
537    cli_default_value = '',
538    cli_show_default = False,
539)
540
541PARAM_NEW_USER_SERVER_ROLE = APIParam(
542    'new_role',
543    'The server role for the new user.',
544    api = False,
545    cli_default_value = autograder.model.user.ServerRole.USER.value,
546    cli_extra_options = {'choices': [role.value for role in autograder.model.user.ServerRole]},
547)
548
549PARAM_OVERWRITE_RECORDS = APIParam(
550    'overwrite_records',
551    'Replace any existing records that match the current operation (e.g. re-do existing results).',
552    value_type = bool,
553    cli_default_value = False,
554)
555
556PARAM_OUT_DIR = APIParam(
557    'out_dir',
558    'A directory to write output in.',
559    api = False,
560    cli_default_value = '.',
561)
562
563PARAM_PROXY_EMAIL = APIParam(
564    'proxy_email',
565    'The email of the user the request is pretending to be made under (the submission will be made on behalf of this user).',
566    cli_required = True,
567)
568
569PARAM_PROXY_TIME = APIParam(
570    'proxy_time',
571    ('The proxy timestamp that will be applied to the request.'
572    + ' By default, the earlier time between now and'
573    + ' one minute before the due date will be used.'
574    + ' Use this option to manually set the proxy time.'
575    + ' Timestamps are milliseconds from the UNIX epoch'
576    + ' (https://en.wikipedia.org/wiki/Unix_time).'),
577    value_type = edq.util.time.Timestamp,
578    cli_type = str,
579    api_required = False,
580)
581
582PARAM_QUERY_AFTER = APIParam(
583    'query_after',
584    'If supplied, only return records after this timestamp.',
585    api_key = 'after',
586    cli_flag = 'after',
587    value_type = edq.util.time.Timestamp,
588    api_required = False,
589)
590
591PARAM_QUERY_BEFORE = APIParam(
592    'query_before',
593    'If supplied, only return records before this timestamp.',
594    api_key = 'before',
595    cli_flag = 'before',
596    value_type = edq.util.time.Timestamp,
597    api_required = False,
598)
599
600PARAM_QUERY_LIMIT = APIParam(
601    'limit',
602    'The maximum number of records to return.',
603    api_key = 'limit',
604    cli_flag = 'limit',
605    value_type = int,
606    api_required = False,
607)
608
609PARAM_QUERY_LOG_LEVEL = APIParam(
610    'query_level',
611    'The minimum level of log records to return.',
612    api_key = 'level',
613    cli_flag = 'level',
614    api_required = False,
615    cli_default_value = 'INFO',
616    cli_extra_options = {'choices': autograder.model.log.LOG_LEVEL_TEXT_TO_INT.keys()}
617)
618
619PARAM_QUERY_METRIC_TYPE = APIParam(
620    'query_metric_type',
621    'The type of metric to query for. See: https://github.com/edulinq/autograder-server/blob/main/internal/stats/metrics.go#L29',
622    api_key = 'type',
623    cli_flag = 'metric-type',
624    cli_required = True,
625    cli_extra_options = {'choices': autograder.model.stats.METRIC_TYPES}
626)
627
628PARAM_QUERY_PAST = APIParam(
629    'query_past',
630    'If supplied, only return log records in this duration (using "h", "m", or "s" suffixes) (e.g., "24h", "10m", or "1h10m10s").',
631    api_key = 'past',
632    cli_flag = 'past',
633    api_required = False,
634)
635
636PARAM_QUERY_SORT = APIParam(
637    'query_sort',
638    'Sort the results. -1 for ascending, 0 for no sorting, 1 for descending.',
639    api_key = 'sort',
640    cli_flag = 'sort',
641    value_type = int,
642    api_required = False,
643)
644
645PARAM_QUERY_TARGET_ASSIGNMENT = APIParam(
646    'query_target_assignment',
647    'If supplied, only return records for this assignment.',
648    api_key = 'target-assignment',
649    cli_flag = 'target-assignment',
650    api_required = False,
651    cli_show_default = False,
652)
653
654PARAM_QUERY_TARGET_COURSE = APIParam(
655    'query_target_course',
656    'If supplied, only return records for this course.',
657    api_key = 'target-course',
658    cli_flag = 'target-course',
659    api_required = False,
660    cli_show_default = False,
661)
662
663PARAM_QUERY_TARGET_EMAIL = APIParam(
664    'query_target_email',
665    'If supplied, only return records for this user.',
666    api_key = 'target-email',
667    cli_flag = 'target-email',
668    api_required = False,
669    cli_show_default = False,
670)
671
672PARAM_QUERY_USE_TESTING_DATA = APIParam(
673    'query_use_testing_data',
674    'Query from hard-coded testing data (instead of real data).',
675    api_key = 'use-testing-data',
676    value_type = bool,
677    api_required = False,
678    cli = False,
679)
680
681PARAM_QUERY_WHERE = APIParam(
682    'query_where',
683    'Only includes records with a patching key/value pair.',
684    api_key = 'where',
685    cli_flag = 'where',
686    api_required = False,
687    value_type = dict,
688    cli_action = ArgumentMap,
689    cli_extra_options = {
690        'metavar': (f"KEY{MAP_KEY_VALUE_SEP}VALUE"),
691        'nargs': '+',
692    },
693)
694
695PARAM_RAW_COURSE_USERS = APIParam(
696    'raw_course_users',
697    'Raw course users to operate on.',
698    value_type = list,
699    cli = False,
700    skip_clean = True,
701)
702
703PARAM_RAW_SERVER_USERS = APIParam(
704    'raw_server_users',
705    'Raw server users to operate on.',
706    value_type = list,
707    api_key = 'raw-users',
708    cli = False,
709    skip_clean = True,
710)
711
712PARAM_REGRADE_CUTOFF = APIParam(
713    'regrade_cutoff',
714    ('All submissions occurring before the cutoff time will be regraded.'
715    + ' By default, the current time will be used.'
716    + ' Time is milliseconds from the UNIX epoch'
717    + ' (https://en.wikipedia.org/wiki/Unix_time).'),
718    value_type = int,
719    api_required = False,
720)
721
722PARAM_SEND_EMAILS = APIParam(
723    'send_emails',
724    'Send any relevant emails to users affected by this operation (e.g., a user being enrolled in a course).',
725    value_type = bool,
726    cli_default_value = False,
727)
728
729PARAM_SERVER = APIParam(
730    'server',
731    'The URL of the autograder server to communicate with.',
732    cli_show_default = False,
733)
734
735PARAM_SKIP_BUILD_IMAGES = APIParam(
736    'skip_build_images',
737    'Skip building assignment Docker images.',
738    value_type = bool,
739    cli_default_value = False,
740)
741
742PARAM_SKIP_EMAILS = APIParam(
743    'skip_emails',
744    'Skip sending any emails.',
745    value_type = bool,
746    cli_default_value = False,
747)
748
749PARAM_SKIP_INSERTS = APIParam(
750    'skip_inserts',
751    'Skip insert operations.',
752    value_type = bool,
753    cli_default_value = False,
754)
755
756PARAM_SKIP_LMS_SYNC = APIParam(
757    'skip_lms_sync',
758    'Skip syncing with the LMS.',
759    value_type = bool,
760    cli_default_value = False,
761)
762
763PARAM_SKIP_SOURCE_SYNC = APIParam(
764    'skip_source_sync',
765    'Skip syncing (updating with) the course source.',
766    value_type = bool,
767    cli_default_value = False,
768)
769
770PARAM_SKIP_TEMPLATE_FILES = APIParam(
771    'skip_template_files',
772    'Skip fetching assignment template files.',
773    value_type = bool,
774    cli_default_value = False,
775)
776
777PARAM_SKIP_UPDATES = APIParam(
778    'skip_updates',
779    'Skip update operations.',
780    value_type = bool,
781    cli_default_value = False,
782)
783
784PARAM_STATUS_ACTIVE = APIParam(
785    'active',
786    'Mark the course as active. Omit to mark it as inactive.',
787    api_required = False,
788    value_type = bool,
789    cli_default_value = False,
790)
791
792PARAM_STATUS_CLEAR = APIParam(
793    'clear',
794    'Ignore the target owner and remove all statuses the caller has permission to remove.',
795    api_required = False,
796    value_type = bool,
797    cli_default_value = False,
798)
799
800PARAM_STATUS_FORCE = APIParam(
801    'force',
802    'If the status already exists for the caller, overwrite it.',
803    api_required = False,
804    value_type = bool,
805    cli_default_value = False,
806)
807
808PARAM_SUBMISSION_FILES = APIParam(
809    'files',
810    'The path to your submission file(s).',
811    value_type = list,
812    api = False,
813    cli_add_func = _cli_add_func_submission_files,
814)
815
816PARAM_SUBMISSION_SPECS = APIParam(
817    'submission_specs',
818    ('A list of submission specifications to analyze.'
819    + ' Submissions may span courses and assignments.'
820    + ' Submissions may be specified in three ways:'
821    + ' 1) "<course id>::<assignment id>::<user email>::<submission short id> for a specific submission,'
822    + ' 2) "<course id>::<assignment id>::<user email> for the given user\'s most recent submission to the given assignment,'
823    + ' and 3) "<course id>::<assignment id> for the most recent submission for all students.'),
824    api_key = 'submissions',
825    cli_flag = 'submissions',
826    value_type = list,
827    cli_add_func = _cli_add_func_submission_specs,
828)
829
830PARAM_TARGET_ASSIGNMENTS = APIParam(
831    'target_assignments',
832    ('A CSV list of assignment IDs to target.'
833    + ' Default: All assignments in the course.'),
834    api_required = False,
835    value_type = list,
836    cli_action = 'extend',
837    cli_type = _csv_to_list,
838    cli_show_default = False,
839)
840
841PARAM_TARGET_EMAIL = APIParam(
842    'target_email',
843    'The email of the user that is the target of this request.',
844)
845
846PARAM_TARGET_EMAIL_OR_SELF = APIParam(
847    'target_email',
848    'The email of the user that is the target of this request (defaults to you).',
849    api_required = False,
850)
851
852PARAM_TARGET_OWNER = APIParam(
853    'target_owner',
854    'The email of the course status owner that is the target of this request (defaults to you).',
855    api_required = False,
856)
857
858PARAM_TARGET_USER_OR_SELF = APIParam(
859    'target_user',
860    'The user that is the target of this request (defaults to you).',
861    api_required = False,
862)
863
864PARAM_TARGET_USERS = APIParam(
865    'target_users',
866    ('A CSV list of server user references.'
867    + ' Server user references may be specified in eight ways:'
868    + ' 1) Email address of the requested user,'
869    + ' 2) "*" to request all users in the server,'
870    + ' 3) "<server role>" (e.g., user, creator)'
871    + ' to request all users with that server role,'
872    + ' 4) "<course id>::<course role>" (e.g., course101::student)'
873    + ' to request all users in the target course with that course role,'
874    + ' 5) "*::<course role>" (e.g., *::student)'
875    + ' to request all users with that course role in any course,'
876    + ' 6) "<course id>::*" to request all users in the target course,'
877    + ' 7) "*::*" to request all users enrolled in at least one course,'
878    + ' and 8) any of the previous options preceded by a minus sign'
879    + ' (e.g., "-alice@test.edulinq.org", "-user", "-*::student")'
880    + ' to exclude that user or group from the request.'
881    + ' Default: All users in the server.'),
882    api_required = False,
883    value_type = list,
884    cli_action = 'extend',
885    cli_type = _csv_to_list,
886    cli_show_default = False,
887)
888
889PARAM_TARGET_SUBMISSION_OR_RECENT = APIParam(
890    'target_submission',
891    'The ID of the submission (default to the most recent submission).',
892    api_required = False,
893)
894
895PARAM_TOKEN_ID = APIParam(
896    'token_id',
897    'The id of the token to target.',
898    cli_required = True,
899)
900
901PARAM_UPSERT_FILESPEC = APIParam(
902    'filespec',
903    'A filespec pointing to a course to upload.',
904    value_type = autograder.filespec.FileSpec,
905    cli = False,
906)
907
908PARAM_UPSERT_ZIP = APIParam(
909    'path',
910    ('The path to your course material.'
911    + ' Either a zip file (with .zip extension) or directory (that will get zipped).'),
912    value_type = str,
913    api = False,
914    cli_add_func = _cli_add_func_upsert_zip,
915)
916
917PARAM_USER_EMAIL = APIParam(
918    'user',
919    'The email of the user making this request.',
920    alt_config_key = 'auth_user',
921    api_key = 'user-email',
922    cli_flag = 'user',
923    cli_show_default = False,
924)
925
926PARAM_USER_PASS = APIParam(
927    'pass',
928    'The password of the user making this request.',
929    alt_config_key = 'auth_pass',
930    api_key = 'user-pass',
931    cli_flag = 'pass',
932    hash_value = True,
933    cli_show_default = False,
934)
935
936PARAM_WAIT_FOR_COMPLETION = APIParam(
937    'wait_for_completion',
938    'Wait for the full job to complete before returning.',
939    value_type = bool,
940    cli_default_value = False,
941)
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_MESSAGE = <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_STATUS_ACTIVE = <APIParam object>
PARAM_STATUS_CLEAR = <APIParam object>
PARAM_STATUS_FORCE = <APIParam object>
PARAM_SUBMISSION_FILES = <APIParam object>
PARAM_SUBMISSION_SPECS = <APIParam object>
PARAM_TARGET_ASSIGNMENTS = <APIParam object>
PARAM_TARGET_EMAIL = <APIParam object>
PARAM_TARGET_EMAIL_OR_SELF = <APIParam object>
PARAM_TARGET_OWNER = <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>