Source code documentation of psef

psef

This package implements the backend for codegrade. Because of historic reasons this backend is named psef.

SPDX-License-Identifier: AGPL-3.0-only

class psef.PsefFlask(import_name, static_url_path=None, static_folder='static', static_host=None, host_matching=False, subdomain_matching=False, template_folder='templates', instance_path=None, instance_relative_config=False, root_path=None)[source]

Bases: flask.app.Flask

Our subclass of flask.

This contains the extra property PsefFlask.do_sanity_checks().

do_sanity_checks

Should we do sanity checks for this app.

Return type:bool
Returns:True if debug or testing is enabled.
max_file_size

The maximum allowed size for normal files.

Note

An individual file has a different limit!

Return type:FileSize
max_large_file_size

The maximum allowed size for large files (such as blackboard zips).

Note

An individual file has a different limit!

Return type:FileSize
max_single_file_size

The maximum allowed size for a single file.

Return type:FileSize
psef.create_app(config: Mapping[KT, VT_co] = None, skip_celery: bool = False, skip_perm_check: bool = True, skip_secret_key_check: bool = False) → Any[source]

Create a new psef app.

Parameters:
  • config (Optional[Mapping[KT, VT_co]]) – The config mapping that can be used to override config.
  • skip_celery (bool) – Set to true to disable sanity checks for celery.
Return type:

Any

Returns:

A new psef app object.

psef.limiter_key_func() → None[source]

This is the default key function for the limiter.

The key function should be set locally at every place the limiter is used so this function always raises a ValueError.

Return type:None

psef.auth

This module implements all authorization functions used by psef.

SPDX-License-Identifier: AGPL-3.0-only

class psef.auth.RequestValidatorMixin(self, key: str, secret: str) → None[source]

Bases: object

A ‘mixin’ for OAuth request validation.

is_valid_request(self, request: Any, parameters: Optional[MutableMapping[str, str]] = None, fake_method: Any = None, handle_error: bool = True) → bool[source]
Validates an OAuth request using the python-oauth2 library:
https://github.com/simplegeo/python-oauth2
Return type:bool
parse_request(self, req: Any, parameters: Optional[MutableMapping[str, str]] = None, fake_method: Optional[Any] = None) → Tuple[str, str, MutableMapping[str, str], MutableMapping[str, str]][source]

This must be implemented for the framework you’re using Returns a tuple: (method, url, headers, parameters) method is the HTTP method: (GET, POST) url is the full absolute URL of the request headers is a dictionary of any headers sent in the request parameters are the parameters sent from the LMS

Parameters:
  • request (object) – The request to be parsed.
  • parameters (dict) – Extra parameters for the given request.
  • fake_method (object) – The fake method to be used.
Return type:

tuple[str, str, dict[str, str], dict[str, str]]

Returns:

A tuple of, respectively, the requets method, url, headers and form, where the last two are a key value mapping.

psef.auth.ensure_any_of_permissions(permissions: List[psef.permissions.CoursePermission], course_id: int) → None[source]
Make sure that the current user has at least one of the given
permissions.
Parameters:
  • permissions (list[CoursePermission]) – The permissions to check for.
  • course_id (int) – The course id of the course that should be used to check for the given permissions.
Return type:

None

Returns:

Nothing.

Raises:

PermissionException – If the current user has none of the given permissions. This will always happen if the list of given permissions is empty.

psef.auth.ensure_can_edit_members_of_group(group: psef.models.group.Group, members: List[psef.models.User]) → None[source]

Make sure that the current user can edit the given group.

Parameters:
  • group (Group) – The group to check for.
  • members (list[User]) – The members you want to add to the group.
Return type:

None

Returns:

Nothing.

Raises:

PermissionException – If the current user cannot edit the given group.

psef.auth.ensure_can_edit_work(work: psef.models.work.Work) → None[source]

Make sure the current user can edit files in the given work.

Parameters:work (Work) – The work the given user should be able to see edit files in.
Return type:None
Returns:Nothing.
Raises:PermissionException – If the user should not be able te edit these files.
psef.auth.ensure_can_see_assignment(assignment: psef.models.assignment.Assignment) → None[source]

Make sure the current user can see the given assignment.

Parameters:assignment (Assignment) – The assignment to check for.
Return type:None
Returns:Nothing.
psef.auth.ensure_can_see_grade(work: psef.models.work.Work) → None[source]

Ensure the current user can see the grade of the given work.

Parameters:

work (Work) – The work to check for.

Return type:

None

Returns:

Nothing

Raises:
psef.auth.ensure_can_see_plagiarims_case(case: psef.models.plagiarism.PlagiarismCase, assignments: bool = True, submissions: bool = True) → None[source]

Make sure the current user can see the given plagiarism case.

Parameters:
  • assignments (bool) – Make sure the user can see the assignments of these cases.
  • submissions (bool) – Make sure the user can see the submissions of these cases.
Return type:

None

Returns:

Nothing

psef.auth.ensure_can_submit_work(assig: psef.models.assignment.Assignment, author: psef.models.user.User) → None[source]

Check if the current user can submit for the given assignment as the given author.

Note

This function also checks if the assignment is a LTI assignment. If this is the case it makes sure the author can do grade passback.

Parameters:
  • assig (Assignment) – The assignment that should be submitted to.
  • author (User) – The author of the submission.
Raises:
  • PermissionException – If there the current user cannot submit for the given author.
  • APIException – If the author is not enrolled in course of the given assignment or if the LTI state was wrong.
Return type:

None

psef.auth.ensure_can_view_files(work: psef.models.work.Work, teacher_files: bool) → None[source]

Make sure the current user can see files in the given work.

Parameters:
  • work (Work) – The work the given user should be able to see files in.
  • teacher_files (bool) – Should the user be able to see teacher files.
Return type:

None

Returns:

Nothing.

Raises:

PermissionException – If the user should not be able te see these files.

psef.auth.ensure_can_view_group(group: psef.models.group.Group) → None[source]

Make sure that the current user can view the given group.

Parameters:group (Group) – The group to check for.
Return type:None
Returns:Nothing.
Raises:PermissionException – If the current user cannot view the given group.
psef.auth.ensure_enrolled(course_id: int, user: Optional[psef.models.User] = None) → None[source]

Ensure that the given user is enrolled in the given course.

Parameters:
  • course_id (int) – The id of the course to check for.
  • user (Optional[User]) – The user to check for. This defaults to the current user.
Return type:

None

Returns:

Nothing.

Raises:
psef.auth.ensure_logged_in() → None[source]

Make sure a user is currently logged in.

Return type:None
Returns:Nothing.
Raises:PermissionException – If there is no logged in user. (NOT_LOGGED_IN)
psef.auth.ensure_permission(permission: Union[psef.permissions.CoursePermission, psef.permissions.GlobalPermission], course_id: Optional[int] = None, *, user: Optional[psef.models.User] = None, extra_message: str = '') → None[source]

Ensure that the current user is logged and has the given permission.

Parameters:
  • permission_name – The name of the permission to check for.
  • course_id (Optional[int]) – The course id of the course that should be used for the course permission, if it is None a role permission is implied. If a course_id is supplied but the given permission is not a course permission (but a role permission) this function will NEVER grant the permission.
  • user (Optional[User]) – The user to check for, defaults to current user when not provided.
  • extra_message (str) – Text that should be appended to the message provided in the raised PermissionException when the permission check fails.
Return type:

None

Returns:

Nothing

Raises:
psef.auth.ensure_valid_oauth(key: str, secret: str, request: Any, parser_cls: Type[CT_co] = <class 'psef.auth._FlaskOAuthValidator'>) → None[source]

Make sure the given oauth key and secret is valid for the given request.

Parameters:
Return type:

None

Returns:

Nothing

psef.auth.init_app(app: Any) → None[source]

Initialize the app by initializing our jwt manager.

Parameters:app (Any) – The flask app to initialize.
Return type:None
psef.auth.login_required(fun: T) → T[source]

Make sure a valid user is logged in at this moment.

Raises:PermissionException – If no user was logged in.
Return type:T
psef.auth.permission_required(permission: psef.permissions.GlobalPermission) → Callable[[T], T][source]

A decorator used to make sure the function decorated is only called with certain permissions.

Parameters:permission (GlobalPermission) – The global permission to check for.
Return type:Callable[[T], T]
Returns:The value of the decorated function if the current user has the required permission.
Raises:PermissionException – If the current user does not have the required permission, this is done in the same way as ensure_permission() does this.
psef.auth.set_current_user(user: psef.models.user.User) → None[source]

Set the current user for this request.

You probably never should use this method, it is only useful after logging in a user.

Parameters:user (User) – The user that should become the current user.
Return type:None
Returns:Nothing

psef.exceptions

This module defines all exceptions used within CodeGrade and their error codes.

SPDX-License-Identifier: AGPL-3.0-only

class psef.exceptions.APICodes[source]

Bases: enum.IntEnum

Internal API codes that are used by APIException objects.

ASSIGNMENT_DEADLINE_UNSET = 28
ASSIGNMENT_GROUP_FULL = 26
ASSIGNMENT_RESULT_GROUP_NOT_READY = 25
BLOCKED_ASSIGNMENT = 11
DISABLED_FEATURE = 15
INACTIVE_USER = 8
INCORRECT_PERMISSION = 0
INSUFFICIENT_GROUP_SIZE = 24
INVALID_ARCHIVE = 21
INVALID_CREDENTIALS = 12
INVALID_FILE_IN_ARCHIVE = 17
INVALID_OAUTH_REQUEST = 14
INVALID_PARAM = 5
INVALID_STATE = 13
INVALID_URL = 9
LOGIN_FAILURE = 7
MISSING_REQUIRED_PARAM = 4
NOT_LOGGED_IN = 1
NO_FILES_SUBMITTED = 18
OBJECT_ALREADY_EXISTS = 20
OBJECT_ID_NOT_FOUND = 2
OBJECT_NOT_FOUND = 10
OBJECT_WRONG_TYPE = 3
PARSING_FAILED = 29
RATE_LIMIT_EXCEEDED = 19
REQUEST_TOO_LARGE = 6
ROUTE_NOT_FOUND = 22
UNKOWN_ERROR = 16
UNSUPPORTED = 27
WEAK_PASSWORD = 23
exception psef.exceptions.APIException(self, message: str, description: str, api_code: psef.exceptions.APICodes, status_code: int, **rest) → None[source]

Bases: Exception

The exception to use if an API call failed.

Parameters:
  • message (str) – The user friendly message to display.
  • description (str) – The description used for debugging.
  • api_code (APICodes) – The error code in the API, should be a constant from this class.
  • status_code (int) – The Http status code to use, should not be 2xx.
  • rest (Any) – All the other fields to return in the JSON object.
class psef.exceptions.APIWarnings[source]

Bases: enum.IntEnum

API codes used to signal warnings to the client.

CONDITION_ALREADY_MET = 2
DEPRECATED = 0
GRADER_NOT_DONE = 1
INVALID_FILENAME = 4
UNASSIGNED_ASSIGNMENTS = 6
WEAK_PASSWORD = 5
exception psef.exceptions.InvalidAssignmentState[source]

Bases: TypeError

Exception used to signal the assignment state is invalid.

exception psef.exceptions.PermissionException(self, message: str, description: str, api_code: psef.exceptions.APICodes, status_code: int, **rest) → None[source]

Bases: psef.exceptions.APIException

The exception used when a permission check fails.

exception psef.exceptions.ValidationException(self, message: str, description: str, code: psef.exceptions.APICodes = <APICodes.INVALID_PARAM: 5>, **rest) → None[source]

Bases: psef.exceptions.APIException

Thrown when some kind of validation fails.

exception psef.exceptions.WeakPasswordException(self, message: str, description: str, code: psef.exceptions.APICodes = <APICodes.INVALID_PARAM: 5>, **rest) → None[source]

Bases: psef.exceptions.ValidationException

Thrown when a password is too weak.

psef.extract_tree

This file contains a common data structure for saving a directory.

SPDX-License-Identifier: AGPL-3.0-only

class psef.extract_tree.ExtractFileTree(self, name: str, parent: Optional[ExtractFileTreeDirectory], values: List[psef.extract_tree.ExtractFileTreeBase]) → None[source]

Bases: psef.extract_tree.ExtractFileTreeDirectory

Type used to represent the top of an extracted file tree.

This is simply a directory with some utility methods.

contains_file

Check if archive contains something other than directories.

Return type:bool
Returns:If the file tree contains actual files
remove_leading_self(self) → None[source]

Removing leading directories in this directory.

This function checks if this directory contains exactly one directory, in this case this directory is removed and its content (of the deleted directory) becomes the content of this directory. The directory is modified in place.

If one of the conditions don’t hold a AssertionError is raised.

Return type:None
class psef.extract_tree.ExtractFileTreeBase(self, name: str, parent: Optional[ExtractFileTreeDirectory]) → None[source]

Bases: object

Base type for an entry in an extracted file tree.

Variables:name – The original name of this file in the archive that was extracted.
delete(self, base_dir: str) → None[source]

Delete the this file and all its children.

Parameters:base_dir (str) – The base directory where the files can be found.
Return type:None
Returns:Nothing.
forget_parent(self) → None[source]
Return type:None
get_full_name(self) → str[source]

Get the full filename of this file including all its parents.

Return type:str
get_name_list(self) → Sequence[str][source]

Get the filename of this file including all its parents as a list.

Return type:Sequence[str]
get_size(self) → NewType.<locals>.new_type[source]

Get the size of this file.

For a normal file this is the amount of space used on disk, and for a directory it is the sum of the space of all its children.

Return type:FileSize
is_dir

Is this file a directory.

Return type:bool
class psef.extract_tree.ExtractFileTreeDirectory(self, name: str, parent: Optional[ExtractFileTreeDirectory], values: List[psef.extract_tree.ExtractFileTreeBase]) → None[source]

Bases: psef.extract_tree.ExtractFileTreeBase

Type used to represent a directory of an extracted file tree.

Variables:values – The items present in this directory.
add_child(self, f: psef.extract_tree.ExtractFileTreeBase) → None[source]

Add a directory as a child.

Parameters:f (ExtractFileTreeBase) – The file to add.
Return type:None
delete(self, base_dir: str) → None[source]

Delete the this file and all its children.

Parameters:base_dir (str) – The base directory where the files can be found.
Return type:None
Returns:Nothing.
fix_duplicate_filenames(self) → None[source]

Fix duplicate filenames in this directory and all its sub directories.

This will rename files when duplicates are detected by adding a `` ($number)`` suffix to the file.

Return type:None
forget_child(self, f: psef.extract_tree.ExtractFileTreeBase) → None[source]

Remove a child as one of our children.

Note

This does not delete the file.

Parameters:f (ExtractFileTreeBase) – The file to forget.
Return type:None
get_all_children(self) → Iterable[psef.extract_tree.ExtractFileTreeBase][source]

Get all the children of this directory.

Return type:Iterable[ExtractFileTreeBase]
Returns:An iterable of all children, including directories.
get_size(self) → NewType.<locals>.new_type[source]

Get the size of this file.

For a normal file this is the amount of space used on disk, and for a directory it is the sum of the space of all its children.

Return type:FileSize
is_dir

Is this file a directory.

Return type:bool
class psef.extract_tree.ExtractFileTreeFile(self, name: str, parent: Optional[ExtractFileTreeDirectory], disk_name: str, size: psef.archive.FileSize) → None[source]

Bases: psef.extract_tree.ExtractFileTreeBase

Type used to represent a file in an extracted file tree.

Variables:diskname – The name of the file saved in the uploads directory.
delete(self, base_dir: str) → None[source]

Delete the this file and all its children.

Parameters:base_dir (str) – The base directory where the files can be found.
Return type:None
Returns:Nothing.
get_size(self) → NewType.<locals>.new_type[source]

Get the size of this file.

For a normal file this is the amount of space used on disk, and for a directory it is the sum of the space of all its children.

Return type:FileSize
is_dir

Is this file a directory.

Return type:bool

psef.blackboard

This module implements the parsing of blackboard gradebook info files.

SPDX-License-Identifier: AGPL-3.0-only

class psef.blackboard.FileInfo[source]

Bases: psef.blackboard.FileInfo

A NamedTuple holding information about a specific file.

Parameters:
  • original_name – The name provided by the user.
  • name – The name as stored in the blackboard gradebook.
class psef.blackboard.SubmissionInfo[source]

Bases: psef.blackboard.SubmissionInfo

A NamedTuple holding information about a submission from a blackboard zip.

Parameters:
  • student_name – The name of the student.
  • student_id – The id of the student in the system of the university.
  • assignment_name – Name of the assignment.
  • created_at – The datetime when the submission was made.
  • grade – The current grade of the submission.
  • text – The html text submission of the student.
  • comment – Comment included by student.
  • files – The files submitted by the user.
psef.blackboard.parse_info_file(file: str) → psef.blackboard.SubmissionInfo[source]

Parses a blackboard gradebook .txt file.

Parameters:file (str) – Path to the file
Returns:The parsed information
Return type:SubmissionInfo

psef.errors

This module implements all errors and warnings for psef.

This module does not contain any error checking or handling.

SPDX-License-Identifier: AGPL-3.0-only

psef.errors.init_app(app: Any) → None[source]

Initialize the flask app by setting an error handler.

Parameters:app (Any) – The app to initialize
Return type:None
psef.errors.make_warning(warning_text: str, code: psef.exceptions.APIWarnings) → NewType.<locals>.new_type[source]

Make a HttpWarning with the given warning and code.

Parameters:
  • warning_text (str) – The text that describes the warning.
  • code (APIWarnings) – The warning code to associate with the warning.
Return type:

HttpWarning

Returns:

A warning with the given text and code.

psef.files

This module is used for file IO and handling and provides functions for extracting and abstracting the structures of directories and archives.

SPDX-License-Identifier: AGPL-3.0-only

class psef.files.FileTreeBase

Bases: dict

exception psef.files.IgnoredFilesException(self, invalid_files: List[psef.ignore.FileDeletion], filter_version: int, original_tree: psef.extract_tree.ExtractFileTree, missing_files: List[Mapping[str, str]]) → None[source]

Bases: psef.exceptions.APIException

The exception used when a permission check fails.

psef.files.check_dir(path: str) → bool[source]

Check if the path is a directory that is readable, writable, and executable for the current user.

Parameters:path (str) – Path to check.
Return type:bool
Returns:True if path has the properties described above, False otherwise.
psef.files.escape_logical_filename(name: str) → str[source]

Escape a logical filename

If the name is a special file or the literal string ‘.’ or ‘..’ the string ‘-USER_PROVIDED’ is appended to the name.

Note

A logical filename is the filename as stored in the database and displayed to the users, called name in the database. This is different from physical filenames which are the names of the files as stored on disk, called filename in the database.

>>> logger.warning = lambda *_, **__: True
>>> helpers.add_warning = lambda *_, **__: True
>>> escape_logical_filename('.')
'.-USER_PROVIDED'
>>> escape_logical_filename('.cg-grade')
'.cg-grade-USER_PROVIDED'
>>> escape_logical_filename('.cg-grade-USER_PROVIDED')
'.cg-grade-USER_PROVIDED-USER_PROVIDED'
>>> escape_logical_filename('normal file')
'normal file'
>>> escape_logical_filename('.bashrc')
'.bashrc'
Parameters:name (str) – The original logical filename.
Return type:str
Returns:An escaped logical filename.
psef.files.extract(file: werkzeug.datastructures.FileStorage, max_size: NewType.<locals>.new_type) → psef.extract_tree.ExtractFileTree[source]

Extracts all files in archive with random name to uploads folder.

Warning

The returned ExtractFileTree may be empty, i.e. contain only directories and no files.

Parameters:
  • file (FileStorage) – The file to extract.
  • max_size (FileSize) – The maximum size of the extracted archive.
Return type:

ExtractFileTree

Returns:

A file tree as generated by rename_directory_structure().

psef.files.extract_to_temp(file: werkzeug.datastructures.FileStorage, max_size: NewType.<locals>.new_type, archive_name: str = 'archive', parent_result_dir: Optional[str] = None) → Tuple[str, NewType.<locals>.new_type][source]

Extracts the contents of file into a temporary directory.

Parameters:
  • file (FileStorage) – The archive to extract.
  • max_size (FileSize) – The maximum size the extracted archive may be.
  • archive_name (str) – The name used for the archive in error messages.
  • parent_result_dir (Optional[str]) – The location the resulting directory should be placed in.
Return type:

tuple[str, FileSize]

Returns:

The pathname of the new temporary directory.

psef.files.get_file_contents(code: psef.models.file.File) → bytes[source]

Get the contents of the given models.File.

Parameters:code (File) – The file object to read.
Return type:bytes
Returns:The contents of the file with newlines.
psef.files.get_file_size(f: str) → NewType.<locals>.new_type[source]
Return type:FileSize
psef.files.get_stat_information(file: psef.models.file.File) → Mapping[str, Any][source]

Get stat information for a given models.File

The resulting object will look like this:

{
    'is_directory': bool, # Is the given file a directory
    'modification_date':  int, # When was the file last modified, as
                               # unix timestamp in utc time.
    'size': int, # The size on disk of the file or 0 if the file is a
                 # directory.
    'id': int, # The id of the given file.
}
Parameters:file (File) – The file to get the stat information for.
Return type:Mapping[str, Any]
Returns:The information as described above.
psef.files.init_app(_: Any) → None[source]
Return type:None
psef.files.process_blackboard_zip(blackboard_zip: werkzeug.datastructures.FileStorage, max_size: NewType.<locals>.new_type) → MutableSequence[Tuple[psef.blackboard.SubmissionInfo, psef.extract_tree.ExtractFileTree]][source]

Process the given blackboard zip file.

This is done by extracting, moving and saving the tree structure of each submission.

Parameters:file – The blackboard gradebook to import
Return type:MutableSequence[tuple[SubmissionInfo, ExtractFileTree]]
Returns:List of tuples (BBInfo, tree)
psef.files.process_files(files: MutableSequence[werkzeug.datastructures.FileStorage], max_size: NewType.<locals>.new_type, force_txt: bool = False, ignore_filter: Optional[psef.ignore.SubmissionFilter] = None, handle_ignore: psef.ignore.IgnoreHandling = <IgnoreHandling.keep: 1>) → psef.extract_tree.ExtractFileTree[source]

Process the given files by extracting, moving and saving their tree structure.

Parameters:
  • files (MutableSequence[FileStorage]) – The files to move and extract
  • max_size (FileSize) – The maximum combined size of all extracted files.
  • force_txt (bool) – Do not extract archive and force all files to be considered to be plain text.
  • ignore_filter (Optional[SubmissionFilter]) – The files and directories that should be ignored.
  • handle_ignore (IgnoreHandling) – Determines how ignored files should be handled.
Return type:

ExtractFileTree

Returns:

The tree of the files as is described by rename_directory_structure()

psef.files.random_file_path(use_mirror_dir: bool = False) → Tuple[str, str][source]

Generates a new random file path in the upload directory.

Parameters:use_mirror_dir (bool) – Use the mirror directory as the basedir of the random file path.
Return type:tuple[str, str]
Returns:The name of the new file and a path to that file
psef.files.rename_directory_structure(rootdir: str) → psef.extract_tree.ExtractFileTreeDirectory[source]

Creates a nested dictionary that represents the folder structure of rootdir.

A tree like:

  • dir1
    • dir 2
      • file 1
      • file 2
    • file 3

will be moved to files given by random_file_path() and the object returned will represent the file structure, which will be something like this:

{
    'dir1': {
        [
            'dir 2':{
                [
                    ('file 1', 'new_name'),
                    ('file 2', 'new_name2')
                ]
            },
            ('file 3', 'new_name3')
        ]
    }
}
Parameters:rootdir (str) – The root directory to rename, files will not be removed
Return type:ExtractFileTreeDirectory
Returns:The tree as described above
psef.files.restore_directory_structure(work: psef.models.work.Work, parent: str, exclude: psef.models.file.FileOwner = <FileOwner.teacher: 2>) → importlib._bootstrap.FileTree[source]

Restores the directory structure recursively for a submission (a models.Work).

The directory structure is returned like this:

{
    "id": 1,
    "name": "rootdir"
    "entries": [
        {
            "id": 2,
            "name": "file1.txt"
        },
        {
            "id": 3,
            "name": "subdir"
            "entries": [
                {
                    "id": 4,
                    "name": "file2.txt."
                },
                {
                    "id": 5,
                    "name": "file3.txt"
                }
            ],
        },
    ],
}
Parameters:
  • work (Work) – A submissions.
  • parent (str) – Path to parent directory.
  • exclude (FileOwner) – The file owner to exclude.
Return type:

FileTree

Returns:

A tree as described.

psef.files.search_path_in_filetree(filetree: importlib._bootstrap.FileTree, path: str) → int[source]

Search for a path in a filetree.

>>> filetree = {
...    "id": 1,
...    "name": "rootdir",
...    "entries": [
...        {
...            "id": 2,
...            "name": "file1.txt"
...        },
...        {
...            "id": 3,
...            "name": "subdir",
...            "entries": [
...                {
...                    "id": 4,
...                    "name": "file2.txt"
...                },
...                {
...                    "id": 5,
...                    "name": "file3.txt"
...                }
...            ],
...        },
...    ],
... }
...
>>> search_path_in_filetree(filetree, "file1.txt")
2
>>> search_path_in_filetree(filetree, "/subdir/")
3
>>> search_path_in_filetree(filetree, "/subdir//file2.txt")
4
>>> search_path_in_filetree(filetree, "Non existing/path")
Traceback (most recent call last):
...
KeyError: 'Path (Non existing/path) not in tree'
Parameters:
  • filetree (FileTree) – The filetree to search.
  • path (str) – The path the search for.
Return type:

int

Returns:

The id of the file associated with the path in the filetree.

psef.files.split_path(path: str) → Tuple[Sequence[str], bool][source]

Split a path into an array of parts of a path.

This functions splits a forward slash separated path into an sequence of the directories of this path. If the given path ends with a ‘/’ it returns that the given path ends with an directory, otherwise the last part is a file, this information is returned as the last part of the returned tuple.

The given path may contain multiple consecutive forward slashes, these are interpreted as a single slash. A leading forward slash is also optional.

Parameters:path (str) – The forward slash separated path to split.
Return type:tuple[Sequence[str], bool]
Returns:A tuple where the first item is the splitted path and the second item is a boolean indicating if the last item of the given path was a directory.

psef.helpers

This module implements generic helpers and convenience functions.

SPDX-License-Identifier: AGPL-3.0-only

class psef.helpers.Comparable(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

A protocol that for comparable variables.

To satisfy this protocol a object should implement the __eq__, __lt__, __gt__, __le__ and``__ge__`` magic functions.

class psef.helpers.EmptyResponse(self) → None[source]

Bases: object

An empty response.

This is a subtype of werkzeug.wrappers.Response where the body is empty and the status code is always 204.

Warning

This class is only used for type hinting and is never actually used! It does not contain any valid data!

class psef.helpers.ExtendedJSONResponse(self) → None[source]

Bases: typing.Generic

A datatype for a JSON response created by using the __extended_to_json__ if available.

This is a subtype of werkzeug.wrappers.Response where the body is a valid JSON object and content-type is application/json.

Warning

This class is only used for type hinting and is never actually used! It does not contain any valid data!

class psef.helpers.JSONResponse(self) → None[source]

Bases: typing.Generic

A datatype for a JSON response.

This is a subtype of werkzeug.wrappers.Response where the body is a valid JSON object and content-type is application/json.

Warning

This class is only used for type hinting and is never actually used! It does not contain any valid data!

psef.helpers.T = ~T

Type vars

psef.helpers.add_deprecate_warning(warning: str) → None[source]

Add a deprecation warning to the request.

Parameters:warning (str) – Explanation about what api is deprecated.
Return type:None
Returns:Nothing
psef.helpers.add_warning(warning: str, code: psef.exceptions.APIWarnings) → None[source]
Return type:None
psef.helpers.between(min_bound: Z, item: Z, max_bound: Z) → Z[source]

Make sure item is between two bounds.

>>> between(0, 5, 10)
5
>>> between(0, -1, 10)
0
>>> between(0, 11, 10)
10
>>> between(10, 5, 0)
Traceback (most recent call last):
...
ValueError: `min_bound` cannot be higher than `max_bound`

Note

min_bound cannot be larger than max_bound. They can be equal.

Parameters:
  • min_bound (Z) – The minimum this function should return
  • max_bound (Z) – The maximum this function should return
  • item (Z) – The item to check
Return type:

Z

Returns:

item if it is between min_bound and max_bound, otherwise the bound is returned that is closest to the item.

psef.helpers.call_external(call_args: List[str], input_callback: Callable[[str], bool] = <function <lambda>>) → Tuple[bool, str][source]

Safely call an external program without any exceptions.

Note

This function should not be used when you don’t want to handle errors as it will silently fail.

Parameters:
  • call_args (list[str]) – The call passed to Popen() with shell set to False.
  • input_callback (Callable[[str], bool]) – The callback that will be called for each line of output. If the callback returns True the given line of output will be skipped.
Return type:

tuple[bool, str]

Returns:

A tuple with the first argument if the process crashed, the second item is stdout and stderr interleaved.

psef.helpers.callback_after_this_request(fun: Callable[[], object]) → Callable[[T], T][source]

Execute a callback after this request without changing the response.

Parameters:fun (Callable[[], object]) – The callback to execute after the current request.
Return type:Callable[[T], T]
Returns:The function that will execute after this request that does that the response as argument, so this function wraps your given callback.
psef.helpers.coerce_json_value_to_typeddict(obj: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]], typeddict: Type[T_TypedDict]) → T_TypedDict[source]

Coerce a json object to a typed dict.

Warning

All types of the typed dict must be types that can be used with isinstance().

Note

The original object is returned, this only checks if all values are valid.

Parameters:
Return type:

T_TypedDict

Returns:

The given value obj.

psef.helpers.defer(function: Callable[[], object]) → Generator[None, None, None][source]

Defer a function call to the end of the context manager.

Param:The function to call.
Return type:Generator[None, None, None]
Returns:A context manager that can be used to execute the given function at the end of the block.
psef.helpers.ensure_json_dict(json_value: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]], replace_log: Optional[Callable[[str, object], object]] = None) → Dict[str, Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]][source]

Make sure that the given json is a JSON dictionary

Parameters:
Return type:

dict[str, Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]]

Returns:

Exactly the same JSON if it is in fact a dictionary.

Raises:

psef.errors.APIException – If the given JSON is not a dictionary. (INVALID_PARAM)

psef.helpers.ensure_keys_in_dict(mapping: Mapping[T, object], keys: Sequence[Tuple[T, Union[Type[CT_co], Tuple[Type[CT_co], ...]]]]) → None[source]

Ensure that the given keys are in the given mapping.

Parameters:
  • mapping (Mapping[T, object]) – The mapping to check.
  • keys (Sequence[tuple[T, Union[type[CT_co], tuple[type[CT_co], …]]]]) – The keys that should be in the mapping. If key is a tuple it is of the form (key, type) where mappping[key] has to be of type type.
Return type:

None

Returns:

Nothing.

Raises:

psef.errors.APIException – If a key from keys is missing in mapping (MISSING_REQUIRED_PARAM)

psef.helpers.escape_like(unescaped_like: str) → str[source]

Escape a string used for the LIKE clause by escaping all wildcards.

Note

The escape characters are “%”, “_” and “”. They are escaped by placing a “” before them.

>>> escape_like('hello')
'hello'
>>> escape_like('This is % a _ string\%')
'This is \\% a \\_ string\\\\\\%'
>>> escape_like('%')
'\\%'
Parameters:unescaped_like (str) – The string to escape
Return type:str
Returns:The same string but escaped
psef.helpers.extended_jsonify(obj: T, status_code: int = 200, use_extended: Union[Callable[[object], bool], type, Tuple[type, ...]] = <class 'object'>) → psef.helpers.ExtendedJSONResponse[~T][T][source]

Create a response with the given object obj as json payload.

This function differs from jsonify() by that it used the __extended_to_json__ magic function if it is available.

Parameters:
  • obj (T) – The object that will be jsonified using CustomExtendedJSONEncoder
  • statuscode – The status code of the response
  • use_extended (Union[Callable[[object], bool], type, tuple[type, …]]) – The __extended_to_json__ method is only used if this function returns something that equals to True. This method is called with object that is currently being encoded. You can also pass a class or tuple as this parameter which is converted to lambda o: isinstance(o, passed_value).
Return type:

ExtendedJSONResponse[T]

Returns:

The response with the jsonified object as payload

psef.helpers.extended_requested() → bool[source]

Check if a extended JSON serialization was requested.

Return type:bool
Returns:The return value of request_arg_true() called with 'extended' as argument.
psef.helpers.filter_all_or_404(model: Type[Y], *criteria) → Sequence[Y][source]

Get all objects of the specified model filtered by the specified criteria.

Note

Y is bound to models.Base, so it should be a SQLAlchemy model.

Parameters:
  • model (type[Y]) – The object to get.
  • criteria (Any) – The criteria to filter with.
Return type:

Sequence[Y]

Returns:

The requested objects.

Raises:

APIException – If no object with the given id could be found. (OBJECT_ID_NOT_FOUND)

psef.helpers.filter_single_or_404(model: Type[Y], *criteria) → Y[source]

Get a single object of the specified model by filtering or raise an exception.

Note

Y is bound to models.Base, so it should be a SQLAlchemy model.

Parameters:
  • model (type[Y]) – The object to get.
  • criteria (Any) – The criteria to filter with.
Return type:

Y

Returns:

The requested object.

Raises:

APIException – If no object with the given id could be found. (OBJECT_ID_NOT_FOUND)

psef.helpers.filter_users_by_name(query: str, base: psef.models.model_types._MyQuery[psef.models.user.User][psef.models.user.User], *, limit: int = 25) → psef.models.model_types._MyQuery[psef.models.user.User][psef.models.user.User][source]

Find users from the given base query using the given query string.

Parameters:
  • query (str) – The string to filter usernames and names of users with.
  • base (_MyQuery[User]) – The query to filter.
  • limit (int) – The amount of users to limit the search too.
Return type:

_MyQuery[User]

Returns:

A new query with the users filtered.

psef.helpers.get_all_subclasses(cls: T_Type) → Iterable[T_Type][source]

Returns all subclasses of the given class.

Stolen from: https://stackoverflow.com/questions/3862310/how-can-i-find-all-subclasses-of-a-class-given-its-name

Parameters:cls (T_Type) – The parent class
Return type:Iterable[T_Type]
Returns:A list of all subclasses
psef.helpers.get_class_by_name(superclass: T_Type, name: str) → T_Type[source]

Get a class with given name

Parameters:
  • superclass (T_Type) – A superclass of the class found
  • name (str) – The name of the class wanted.
Return type:

T_Type

Returns:

The class with the attribute __name__ equal to name. If there are multiple classes with the name name the result can be any one of these classes.

Raises:

ValueError – If the class with the specified name is not found.

psef.helpers.get_files_from_request(*, max_size: NewType.<locals>.new_type, keys: Sequence[str], only_start: bool = False) → MutableSequence[werkzeug.datastructures.FileStorage][source]

Get all the submitted files in the current request.

This function also checks if the files are in the correct format and are lot too large if you provide check_size. This is done for the entire request, not only the processed files.

Parameters:
  • only_small_files – Only allow small files to be uploaded.
  • keys (Sequence[str]) – The keys the files should match in the request.
  • only_start (bool) – If set to false the key of the request should only match the start of one of the keys in keys.
Return type:

MutableSequence[FileStorage]

Returns:

The files in the current request. The length of this list is always at least one and if only_start is false is never larger than the length of keys.

Raises:

APIException – When a given file is not correct.

psef.helpers.get_in_or_error(model: Type[Y], in_column: psef.models.model_types.DbColumn[~T][T], in_values: List[T], options: Optional[List[Any]] = None) → List[Y][source]

Get object by doing an IN query.

This method protects against empty in_values, and will return an empty list in that case. If not all items from the in_values this function will raise an exception.

Parameters:
  • model (type[Y]) – The objects to get.
  • in_column (DbColumn[T]) – The column of the object to perform the in on.
  • in_values (list[T]) – The values used for the IN clause. This may be an empty sequence, which is handled without doing a query.
  • options (Optional[list[Any]]) – A list of options to give to the executed query. This can be used to undefer or eagerly load some columns or relations.
Return type:

list[Y]

Returns:

A list of objects with the same length as in_values.

Raises:

APIException – If on of the items in in_values was not found.

psef.helpers.get_json_dict_from_request(replace_log: Optional[Callable[[str, object], object]] = None) → Dict[str, Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]][source]

Get the JSON dict from this request.

Parameters:replace_log (Optional[Callable[[str, object], object]]) – A function that replaces options in the log.
Return type:dict[str, Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]]
Returns:The JSON found in the request if it is a dictionary.
Raises:psef.errors.APIException – If the found JSON is not a dictionary. (INVALID_PARAM)
psef.helpers.get_key_from_dict(mapping: Mapping[T, object], key: T, default: TT) → TT[source]

Get a key from a mapping of a specific type.

Parameters:
  • mapping (Mapping[T, object]) – The mapping to get the key from.
  • key (T) – The key in the mapping.
  • default (TT) – The default value used if the key is not in the dict.
Return type:

TT

Returns:

The found value of the given default.

Raises:

APIException – If the found value is of a different type than the given default.

psef.helpers.get_or_404(model: Type[Y], object_id: Any, options: Optional[List[Any]] = None, also_error: Optional[Callable[[Y], bool]] = None) → Y[source]

Get the specified object by primary key or raise an exception.

Note

Y is bound to models.Base, so it should be a SQLAlchemy model.

Parameters:
  • model (type[Y]) – The object to get.
  • object_id (Any) – The primary key identifier for the given object.
  • options (Optional[list[Any]]) – A list of options to give to the executed query. This can be used to undefer or eagerly load some columns or relations.
  • also_error (Optional[Callable[[Y], bool]]) – If this function when called with the found object returns True generate the 404 error even though the object was found.
Return type:

Y

Returns:

The requested object.

Raises:

APIException – If no object with the given id could be found. (OBJECT_ID_NOT_FOUND)

psef.helpers.get_request_start_time() → datetime.datetime[source]

Return the start time of the current request.

Returns:The time as returned by the python time module.
Return type:float
psef.helpers.human_readable_size(size: NewType.<locals>.new_type) → str[source]

Get a human readable size.

>>> human_readable_size(512)
'512B'
>>> human_readable_size(1024)
'1KB'
>>> human_readable_size(2.4 * 2 ** 20)
'2.40MB'
>>> human_readable_size(2.4444444 * 2 ** 20)
'2.44MB'
Parameters:size (FileSize) – The size in bytes.
Return type:str
Returns:A string that is the amount of bytes which is human readable.
psef.helpers.init_app(app: flask.app.Flask) → None[source]

Initialize the app.

Parameters:app (Flask) – The flask app to initialize.
Return type:None
psef.helpers.is_sublist(needle: Sequence[T], hay: Sequence[T]) → bool[source]

Check if a needle is present in the given hay.

This is semi efficient, it uses Boyer-Moore however it doesn’t cache the lookup tables.

>>> is_sublist(list(range(10)), list(range(20)))
True
>>> is_sublist(list(range(5, 10)), list(range(20)))
True
>>> is_sublist(list(range(5, 21)), list(range(20)))
False
>>> is_sublist(list(range(20)), list(range(20)))
True
>>> is_sublist(list(range(21)), list(range(20)))
False
>>> is_sublist('thomas', 'hallo thom, ik as dit heel goed thomas, mooi he')
True
>>> is_sublist('saab', 'baas neem een racecar, neem een saab')
True
>>> is_sublist('aaaa', 'aa aaa aaba aaaa')
True
>>> is_sublist('aaaa', 'aa aaa aaba aaaba')
False
>>> is_sublist(['assig2'], ['assig2'])
True
>>> is_sublist(['assig2'], ['assig1'])
False
>>> is_sublist(['assig2'], ['assig1', 'assig2'])
True
Parameters:
  • needle (Sequence[T]) – The thing you are searching for.
  • hay (Sequence[T]) – The thing you are searching in.
Return type:

bool

Returns:

A boolean indicating if needle was found in hay.

psef.helpers.jsonify(obj: T, status_code: int = 200) → psef.helpers.JSONResponse[~T][T][source]

Create a response with the given object obj as json payload.

Parameters:
  • obj (T) – The object that will be jsonified using CustomJSONEncoder
  • statuscode – The status code of the response
Return type:

JSONResponse[T]

Returns:

The response with the jsonified object as payload

psef.helpers.make_empty_response() → psef.helpers.EmptyResponse[source]

Create an empty response.

Return type:EmptyResponse
Returns:A empty response with status code 204
psef.helpers.maybe_apply_sql_slice(sql: psef.models.model_types._MyQuery[~T][T]) → psef.models.model_types._MyQuery[~T][T][source]

Slice the given query if limit is given in the request args.

Parameters:sql (_MyQuery[T]) – The query to slice.
Return type:_MyQuery[T]
Returns:The slices query with limit and offset from the request parameters. If these can not be found in the request parameters the query is returned unaltered.
psef.helpers.raise_file_too_big_exception(max_size: NewType.<locals>.new_type, single_file: bool = False) → mypy_extensions.NoReturn[source]

Get an exception that should be thrown when uploade file is too big.

Parameters:max_size (FileSize) – The maximum size that was overwritten.
Return type:NoReturn
Returns:A exception that should be thrown when file is too big.
psef.helpers.request_arg_true(arg_name: str) → bool[source]

Check if a request arg was set to a ‘truthy’ value.

Parameters:arg_name (str) – The name of the argument to check.
Return type:bool
Returns:True if and only iff the requested get parameter arg_name is present and it value equals (case insensitive) 'true', '1', or '' (empty string).

psef.ignore

gitignore files.

For details for the matching rules, see https://git-scm.com/docs/gitignore.

This code is almost copied verbatim from dulwich.

SPDX-License-Identifier: AGPL-3.0-only

class psef.ignore.DeletionType[source]

Bases: enum.Enum

What type of deletion was the file deletion.

denied_file = 2
empty_directory = 1
leading_directory = 3
class psef.ignore.EmptySubmissionFilter(*args, **kwargs)[source]

Bases: psef.ignore.SubmissionFilter

A SubmissionFilter that does nothing.

This filter allows all files, and all submissions are seen as valid.

CGIGNORE_VERSION = -1
export(self) → Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]][source]

Export the this submission filter.

Return type:Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]
Returns:The exported filter in such a way that when the return value of this function is given to SubmissionFilter.parse() it should produce the exact same filter.
file_allowed(self, f: psef.extract_tree.ExtractFileTreeBase) → Optional[psef.ignore.FileDeletion][source]

Check if the given file adheres to this validator.

Parameters:f (ExtractFileTreeBase) – The file to check.
Return type:Optional[FileDeletion]
Returns:If the submission is valid.
classmethod parse(data: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → psef.ignore.EmptySubmissionFilter[source]

Parse given data as submission filter

Parameters:data (Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]) – The data to parse.
Return type:EmptySubmissionFilter
Returns:An instance of this filter.
Raises:ParseError – When parsing failed for whatever reason.
class psef.ignore.FileDeletion(self, deletion_type: psef.ignore.DeletionType, deleted_file: psef.extract_tree.ExtractFileTreeBase, reason: Union[str, FileRule]) → None[source]

Bases: object

Class representing the deletion of a file or directory.

class psef.ignore.FileRule(self, rule_type: psef.ignore.FileRule.RuleType, file_type: psef.ignore.FileRule.FileType, filename: psef.ignore.FileRule.Filename) → None[source]

Bases: object

A FileRule allows, disallows or requires a certain file or directory.

class FileType[source]

Bases: enum.Enum

Is the file a file or a directory.

directory = 2
file = 1
class Filename(self, filename: str)[source]

Bases: object

A filename is something with a name and dir_names

matches(self, f: psef.extract_tree.ExtractFileTreeBase) → bool[source]

Check if a file matches this filename pattern.

Parameters:f (ExtractFileTreeBase) – The file you want to check.
Return type:bool
Returns:True if this patterns matches the given file, False otherwise.
classmethod split_into_parts(filename: str) → List[str][source]

Split a file into directory parts, respecting escaping.

Parameters:filename (str) – The filename to split.
Return type:list[str]
Returns:A list of file parts.
class RuleType[source]

Bases: enum.Enum

A rule type specifies if a file is allowed, required, or disallowed.

allow = 1
deny = 2
require = 3
static count_chars(needle: str, filename: str) → Tuple[int, List[int]][source]

Count the amount the needle occurs in the filename, with escaping.

>>> FileRule.count_chars('*', '/hello.py/\\**')[0]
1
>>> FileRule.count_chars('*', '/hello.py/\\*')[0]
0
>>> FileRule.count_chars('*', '*/hello*.py/\\*')[0]
2
Parameters:
  • needle (str) – A string with a length of 1, which should be searched.
  • filename (str) – The filename which should be searched for the needle.
Return type:

tuple[int, list[int]]

Returns:

A tuple, the first element is the amount of times needle was found, and the second element are the indices where needle was found.

is_dir_rule

Is this a directory rule

Return type:bool
matches(self, f: psef.extract_tree.ExtractFileTreeBase) → bool[source]

Check if a file matches this rule.

Parameters:f (ExtractFileTreeBase) – The file that should be checked.
Return type:bool
Returns:A boolean indicating if the given file matches this rule.
classmethod parse(data: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → psef.ignore.FileRule[source]

Parse a file rule.

Parameters:data (Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]) – The rule that should be parsed
Return type:FileRule
Returns:The parsed FileRule.
class psef.ignore.IgnoreFilter(self, patterns: Iterable[str]) → None[source]

Bases: object

A ignore filter. This filter consists of multiple Filter.

append_pattern(self, pattern: str, orig_line: str) → None[source]

Add a pattern to the set.

Return type:None
find_matching(self, path: str) → Iterable[psef.ignore.Pattern][source]

Yield all matching patterns for path.

Parameters:path (str) – Path to match
Return type:Iterable[Pattern]
Returns:Iterator over iterators
static read_ignore_patterns(f: Iterable[str]) → Iterable[Tuple[str, str]][source]

Read a git ignore file.

Parameters:f (Iterable[str]) – Iterable to read from
Return type:Iterable[tuple[str, str]]
Returns:List of patterns
class psef.ignore.IgnoreFilterManager(self, global_filters: Union[str, Sequence[str]]) → None[source]

Bases: psef.ignore.SubmissionFilter

Ignore file manager.

CGIGNORE_VERSION = 1
export(self) → Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]][source]

Export the this submission filter.

Return type:Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]
Returns:The exported filter in such a way that when the return value of this function is given to SubmissionFilter.parse() it should produce the exact same filter.
file_allowed(self, f: psef.extract_tree.ExtractFileTreeBase) → Optional[psef.ignore.FileDeletion][source]

Check if the given file adheres to this validator.

Parameters:f (ExtractFileTreeBase) – The file to check.
Return type:Optional[FileDeletion]
Returns:If the submission is valid.
find_matching(self, path: str) → List[psef.ignore.Pattern][source]

Find matching patterns for path.

Stops after the first ignore file with matches.

Parameters:path (str) – Path to check
Return type:list[Pattern]
Returns:Iterator over Pattern instances
is_ignored(self, path: str) → Union[Tuple[bool, str], Tuple[None, None]][source]

Check whether a path is explicitly included or excluded in ignores.

Parameters:path (str) – Path to check
Return type:Union[tuple[bool, str], tuple[None, None]]
Returns:None if the file is not mentioned, True if it is included, False if it is explicitly excluded.
classmethod parse(data: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → psef.ignore.IgnoreFilterManager[source]

Parse given data as submission filter

Parameters:data (Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]) – The data to parse.
Return type:IgnoreFilterManager
Returns:An instance of this filter.
Raises:ParseError – When parsing failed for whatever reason.
class psef.ignore.IgnoreHandling[source]

Bases: enum.IntEnum

Describes what to do with ignored files..

Parameters:
  • keep – Nothing should be done with ignored files, simply keep them.
  • delete – Ignored files should be deleted from the resulting directory.
  • error – An exception should be raised when ignored files are found in the given archive.
delete = 2
error = 3
keep = 1
class psef.ignore.Options(self) → None[source]

Bases: object

All options that can be set.

class OptionName[source]

Bases: enum.Enum

Enum representing the name of each option.

allow_override = _OptionNameValue(required=False, default=False)
delete_empty_directories = _OptionNameValue(required=False, default=False)
remove_leading_directories = _OptionNameValue(required=False, default=True)
get(self, option: psef.ignore.Options.OptionName) → bool[source]

Get an option returning its default value when it is not set.

Parameters:option (OptionName) – The option to get.
Return type:bool
Returns:The value it has been set to or its default value.
parse_option(self, rule: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → None[source]

Parse a option

Return type:None
exception psef.ignore.ParseError(self, msg: str) → None[source]

Bases: psef.exceptions.APIException

The exception raised when parsing failed.

class psef.ignore.Pattern(self, pattern: str, orig_line: str) → None[source]

Bases: object

A single ignore pattern.

match(self, path: str) → bool[source]

Try to match a path against this ignore pattern.

Parameters:path (str) – Path to match (relative to ignore location)
Return type:bool
Returns:boolean
classmethod translate(pat: str) → str[source]

Translate a shell PATTERN to a regular expression.

There is no way to quote meta-characters.

Originally copied from fnmatch in Python 2.7, but modified for Dulwich to cope with features in Git ignore patterns.

Return type:str
class psef.ignore.SubmissionFilter(*args, **kwargs)[source]

Bases: typing_extensions.Protocol

Class representing the base submission filter.

This filters a submission, and checks if the resulting submission is valid.

can_override_ignore_filter

Is it possible for users to override this ignore filter.

Return type:bool
export(self) → Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]][source]

Export the this submission filter.

Return type:Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]
Returns:The exported filter in such a way that when the return value of this function is given to SubmissionFilter.parse() it should produce the exact same filter.
file_allowed(self, f: psef.extract_tree.ExtractFileTreeBase) → Optional[psef.ignore.FileDeletion][source]

Check if the given file adheres to this validator.

Parameters:f (ExtractFileTreeBase) – The file to check.
Return type:Optional[FileDeletion]
Returns:If the submission is valid.
get_missing_files(self, tree: psef.extract_tree.ExtractFileTree) → List[Mapping[str, str]][source]

Get all missing files for the given submission.

Parameters:tree (ExtractFileTree) – The tree to check for.
Return type:list[Mapping[str, str]]
Returns:A list of files, or file patterns, that are missing from the given tree.
classmethod parse(data: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → T_SF[source]

Parse given data as submission filter

Parameters:data (Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]) – The data to parse.
Return type:T_SF
Returns:An instance of this filter.
Raises:ParseError – When parsing failed for whatever reason.
process_submission(self, tree: psef.extract_tree.ExtractFileTree, handle_ignore: psef.ignore.IgnoreHandling) → Tuple[psef.extract_tree.ExtractFileTree, List[psef.ignore.FileDeletion], List[Mapping[str, str]]][source]

Process a submission with the given filter.

This method will apply the filter, respecting the given handle_ignore, deleting files from the given tree (in-place!).

Parameters:
  • tree (ExtractFileTree) – The tree to check.
  • handle_ignore (IgnoreHandling) – The way files that files that are ignored should be handled. There is not difference between error and delete.
Return type:

tuple[ExtractFileTree, list[FileDeletion], list[Mapping[str, str]]]

Returns:

A tuple, first containing the tree (however, the tree given is modified in-place!), a list of files and directories that are deleted, and finally a list of missing files.

class psef.ignore.SubmissionValidator(self, policy: psef.ignore.SubmissionValidator.Policy, options: psef.ignore.Options, rules: List[psef.ignore.FileRule], data: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → None[source]

Bases: psef.ignore.SubmissionFilter

Validate a submission using a config file.

CGIGNORE_VERSION = 2
class Policy[source]

Bases: enum.Enum

The default policy used by this validator.

allow_all_files = 2
deny_all_files = 1
can_override_ignore_filter

Is it possible for users to override this ignore filter.

Return type:bool
export(self) → Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]][source]

Export the this submission filter.

Return type:Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]
Returns:The exported filter in such a way that when the return value of this function is given to SubmissionFilter.parse() it should produce the exact same filter.
file_allowed(self, f: psef.extract_tree.ExtractFileTreeBase) → Optional[psef.ignore.FileDeletion][source]

Check if the given file adheres to this validator.

Parameters:f (ExtractFileTreeBase) – The file to check.
Return type:Optional[FileDeletion]
Returns:If the submission is valid.
get_missing_files(self, tree: psef.extract_tree.ExtractFileTree) → List[Mapping[str, str]][source]

Get all missing files for the given submission.

Parameters:tree (ExtractFileTree) – The tree to check for.
Return type:list[Mapping[str, str]]
Returns:A list of files, or file patterns, that are missing from the given tree.
classmethod parse(data: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → psef.ignore.SubmissionValidator[source]

Parse and validate the given data as a config for the validator.

Parameters:data (Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]) – The data of the config file.
Return type:SubmissionValidator
Returns:A submission validator with policy, options, and rules as described in the given data.
Raises:ParseError – When the given data is not valid.

psef.json_encoders

This module manages all json encoding for the backend.

SPDX-License-Identifier: AGPL-3.0-only

class psef.json_encoders.CustomJSONEncoder(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None)[source]

Bases: json.encoder.JSONEncoder

This JSON encoder is used to enable the JSON serialization of custom classes.

Classes can define their serialization by implementing a __to_json__ method.

default(self, o: Any) → Any[source]

A way to serialize arbitrary methods to JSON.

Classes can use this method by implementing a __to_json__ method that should return a JSON serializable object.

Parameters:obj – The object that should be converted to JSON.
Return type:Any
psef.json_encoders.get_extended_encoder_class(use_extended: Callable[[object], bool]) → Type[CT_co][source]

Get a json encoder class.

Parameters:use_extended (Callable[[object], bool]) – The returned class only uses the __extended_to_json__ method if this callback returns something that is equal to True. This method is called with a single argument that is the object that is currently encoded.
Return type:type[CT_co]
Returns:A class (not a instance!) that can be used as JSONEncoder class.
psef.json_encoders.init_app(app: Any) → None[source]
Return type:None

psef.linters

This module contains all the linters that are integrated in the service.

Integrated linters are ran by the LinterRunner and thus implement run method.

SPDX-License-Identifier: AGPL-3.0-only

class psef.linters.Checkstyle(self, cfg: str) → None[source]

Bases: psef.linters.Linter

Run the Checkstyle linter.

This checks java source files for common errors. It is configured by a xml file. It is not possible to upload your own checkers, neither is it possible to supply some properties such as basedir and file.

DEFAULT_OPTIONS = {'Google style': '<?xml version="1.0"?>\n<!DOCTYPE module PUBLIC\n "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"\n "https://checkstyle.org/dtds/configuration_1_3.dtd">\n\n<!--\n Checkstyle configuration that checks the Google coding conventions from Google Java Style\n that can be found at https://google.github.io/styleguide/javaguide.html.\n\n Checkstyle is very configurable. Be sure to read the documentation at\n http://checkstyle.sf.net (or in your downloaded distribution).\n\n To completely disable a check, just comment it out or delete it from the file.\n\n Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n -->\n\n<module name = "Checker">\n <property name="charset" value="UTF-8"/>\n\n <property name="severity" value="warning"/>\n\n <property name="fileExtensions" value="java, properties, xml"/>\n <!-- Checks for whitespace -->\n <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n <module name="FileTabCharacter">\n <property name="eachLine" value="true"/>\n </module>\n\n <module name="TreeWalker">\n <module name="OuterTypeFilename"/>\n <module name="IllegalTokenText">\n <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>\n <property name="format"\n value="\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)"/>\n <property name="message"\n value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>\n </module>\n <module name="AvoidEscapedUnicodeCharacters">\n <property name="allowEscapesForControlCharacters" value="true"/>\n <property name="allowByTailComment" value="true"/>\n <property name="allowNonPrintableEscapes" value="true"/>\n </module>\n <module name="LineLength">\n <property name="max" value="100"/>\n <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>\n </module>\n <module name="AvoidStarImport"/>\n <module name="OneTopLevelClass"/>\n <module name="NoLineWrap"/>\n <module name="EmptyBlock">\n <property name="option" value="TEXT"/>\n <property name="tokens"\n value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>\n </module>\n <module name="NeedBraces"/>\n <module name="LeftCurly"/>\n <module name="RightCurly">\n <property name="id" value="RightCurlySame"/>\n <property name="tokens"\n value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,\n LITERAL_DO"/>\n </module>\n <module name="RightCurly">\n <property name="id" value="RightCurlyAlone"/>\n <property name="option" value="alone"/>\n <property name="tokens"\n value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,\n INSTANCE_INIT"/>\n </module>\n <module name="WhitespaceAround">\n <property name="allowEmptyConstructors" value="true"/>\n <property name="allowEmptyMethods" value="true"/>\n <property name="allowEmptyTypes" value="true"/>\n <property name="allowEmptyLoops" value="true"/>\n <message key="ws.notFollowed"\n value="WhitespaceAround: \'\'{0}\'\' is not followed by whitespace. Empty blocks may only be represented as \'{}\' when not part of a multi-block statement (4.1.3)"/>\n <message key="ws.notPreceded"\n value="WhitespaceAround: \'\'{0}\'\' is not preceded with whitespace."/>\n </module>\n <module name="OneStatementPerLine"/>\n <module name="MultipleVariableDeclarations"/>\n <module name="ArrayTypeStyle"/>\n <module name="MissingSwitchDefault"/>\n <module name="FallThrough"/>\n <module name="UpperEll"/>\n <module name="ModifierOrder"/>\n <module name="EmptyLineSeparator">\n <property name="allowNoEmptyLineBetweenFields" value="true"/>\n </module>\n <module name="SeparatorWrap">\n <property name="id" value="SeparatorWrapDot"/>\n <property name="tokens" value="DOT"/>\n <property name="option" value="nl"/>\n </module>\n <module name="SeparatorWrap">\n <property name="id" value="SeparatorWrapComma"/>\n <property name="tokens" value="COMMA"/>\n <property name="option" value="EOL"/>\n </module>\n <module name="SeparatorWrap">\n <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->\n <property name="id" value="SeparatorWrapEllipsis"/>\n <property name="tokens" value="ELLIPSIS"/>\n <property name="option" value="EOL"/>\n </module>\n <module name="SeparatorWrap">\n <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->\n <property name="id" value="SeparatorWrapArrayDeclarator"/>\n <property name="tokens" value="ARRAY_DECLARATOR"/>\n <property name="option" value="EOL"/>\n </module>\n <module name="SeparatorWrap">\n <property name="id" value="SeparatorWrapMethodRef"/>\n <property name="tokens" value="METHOD_REF"/>\n <property name="option" value="nl"/>\n </module>\n <module name="PackageName">\n <property name="format" value="^[a-z]+(\\.[a-z][a-z0-9]*)*$"/>\n <message key="name.invalidPattern"\n value="Package name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="TypeName">\n <message key="name.invalidPattern"\n value="Type name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="MemberName">\n <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>\n <message key="name.invalidPattern"\n value="Member name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="ParameterName">\n <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>\n <message key="name.invalidPattern"\n value="Parameter name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="LambdaParameterName">\n <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>\n <message key="name.invalidPattern"\n value="Lambda parameter name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="CatchParameterName">\n <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>\n <message key="name.invalidPattern"\n value="Catch parameter name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="LocalVariableName">\n <property name="tokens" value="VARIABLE_DEF"/>\n <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>\n <message key="name.invalidPattern"\n value="Local variable name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="ClassTypeParameterName">\n <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>\n <message key="name.invalidPattern"\n value="Class type name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="MethodTypeParameterName">\n <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>\n <message key="name.invalidPattern"\n value="Method type name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="InterfaceTypeParameterName">\n <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>\n <message key="name.invalidPattern"\n value="Interface type name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="NoFinalizer"/>\n <module name="GenericWhitespace">\n <message key="ws.followed"\n value="GenericWhitespace \'\'{0}\'\' is followed by whitespace."/>\n <message key="ws.preceded"\n value="GenericWhitespace \'\'{0}\'\' is preceded with whitespace."/>\n <message key="ws.illegalFollow"\n value="GenericWhitespace \'\'{0}\'\' should followed by whitespace."/>\n <message key="ws.notPreceded"\n value="GenericWhitespace \'\'{0}\'\' is not preceded with whitespace."/>\n </module>\n <module name="Indentation">\n <property name="basicOffset" value="2"/>\n <property name="braceAdjustment" value="0"/>\n <property name="caseIndent" value="2"/>\n <property name="throwsIndent" value="4"/>\n <property name="lineWrappingIndentation" value="4"/>\n <property name="arrayInitIndent" value="2"/>\n </module>\n <module name="AbbreviationAsWordInName">\n <property name="ignoreFinal" value="false"/>\n <property name="allowedAbbreviationLength" value="1"/>\n </module>\n <module name="OverloadMethodsDeclarationOrder"/>\n <module name="VariableDeclarationUsageDistance"/>\n <module name="CustomImportOrder">\n <property name="sortImportsInGroupAlphabetically" value="true"/>\n <property name="separateLineBetweenGroups" value="true"/>\n <property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>\n </module>\n <module name="MethodParamPad"/>\n <module name="NoWhitespaceBefore">\n <property name="tokens"\n value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>\n <property name="allowLineBreaks" value="true"/>\n </module>\n <module name="ParenPad"/>\n <module name="OperatorWrap">\n <property name="option" value="NL"/>\n <property name="tokens"\n value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,\n LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>\n </module>\n <module name="AnnotationLocation">\n <property name="id" value="AnnotationLocationMostCases"/>\n <property name="tokens"\n value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>\n </module>\n <module name="AnnotationLocation">\n <property name="id" value="AnnotationLocationVariables"/>\n <property name="tokens" value="VARIABLE_DEF"/>\n <property name="allowSamelineMultipleAnnotations" value="true"/>\n </module>\n <module name="NonEmptyAtclauseDescription"/>\n <module name="JavadocTagContinuationIndentation"/>\n <module name="SummaryJavadoc">\n <property name="forbiddenSummaryFragments"\n value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>\n </module>\n <module name="JavadocParagraph"/>\n <module name="AtclauseOrder">\n <property name="tagOrder" value="@param, @return, @throws, @deprecated"/>\n <property name="target"\n value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>\n </module>\n <module name="JavadocMethod">\n <property name="scope" value="public"/>\n <property name="allowMissingParamTags" value="true"/>\n <property name="allowMissingThrowsTags" value="true"/>\n <property name="allowMissingReturnTag" value="true"/>\n <property name="minLineCount" value="2"/>\n <property name="allowedAnnotations" value="Override, Test"/>\n <property name="allowThrowsTagsForSubclasses" value="true"/>\n </module>\n <module name="MethodName">\n <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>\n <message key="name.invalidPattern"\n value="Method name \'\'{0}\'\' must match pattern \'\'{1}\'\'."/>\n </module>\n <module name="SingleLineJavadoc">\n <property name="ignoreInlineTags" value="false"/>\n </module>\n <module name="EmptyCatchBlock">\n <property name="exceptionVariableName" value="expected"/>\n </module>\n <module name="CommentsIndentation"/>\n </module>\n</module>\n', 'Sun style': '<?xml version="1.0"?>\n<!DOCTYPE module PUBLIC\n "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"\n "https://checkstyle.org/dtds/configuration_1_3.dtd">\n\n<!--\n\n Checkstyle configuration that checks the sun coding conventions from:\n\n - the Java Language Specification at\n http://java.sun.com/docs/books/jls/second_edition/html/index.html\n\n - the Sun Code Conventions at http://java.sun.com/docs/codeconv/\n\n - the Javadoc guidelines at\n http://java.sun.com/j2se/javadoc/writingdoccomments/index.html\n\n - the JDK Api documentation http://java.sun.com/j2se/docs/api/index.html\n\n - some best practices\n\n Checkstyle is very configurable. Be sure to read the documentation at\n http://checkstyle.sf.net (or in your downloaded distribution).\n\n Most Checks are configurable, be sure to consult the documentation.\n\n To completely disable a check, just comment it out or delete it from the file.\n\n Finally, it is worth reading the documentation.\n\n-->\n\n<module name="Checker">\n <!--\n If you set the basedir property below, then all reported file\n names will be relative to the specified directory. See\n https://checkstyle.org/5.x/config.html#Checker\n\n <property name="basedir" value="${basedir}"/>\n -->\n\n <property name="fileExtensions" value="java, properties, xml"/>\n\n <!-- Checks that a package-info.java file exists for each package. -->\n <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage -->\n <module name="JavadocPackage"/>\n\n <!-- Checks whether files end with a new line. -->\n <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->\n <module name="NewlineAtEndOfFile"/>\n\n <!-- Checks that property files contain the same keys. -->\n <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->\n <module name="Translation"/>\n\n <!-- Checks for Size Violations. -->\n <!-- See http://checkstyle.sf.net/config_sizes.html -->\n <module name="FileLength"/>\n\n <!-- Checks for whitespace -->\n <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n <module name="FileTabCharacter"/>\n\n <!-- Miscellaneous other checks. -->\n <!-- See http://checkstyle.sf.net/config_misc.html -->\n <module name="RegexpSingleline">\n <property name="format" value="\\s+$"/>\n <property name="minimum" value="0"/>\n <property name="maximum" value="0"/>\n <property name="message" value="Line has trailing spaces."/>\n </module>\n\n <!-- Checks for Headers -->\n <!-- See http://checkstyle.sf.net/config_header.html -->\n <!-- <module name="Header"> -->\n <!-- <property name="headerFile" value="${checkstyle.header.file}"/> -->\n <!-- <property name="fileExtensions" value="java"/> -->\n <!-- </module> -->\n\n <module name="TreeWalker">\n\n <!-- Checks for Javadoc comments. -->\n <!-- See http://checkstyle.sf.net/config_javadoc.html -->\n <module name="JavadocMethod"/>\n <module name="JavadocType"/>\n <module name="JavadocVariable"/>\n <module name="JavadocStyle"/>\n\n <!-- Checks for Naming Conventions. -->\n <!-- See http://checkstyle.sf.net/config_naming.html -->\n <module name="ConstantName"/>\n <module name="LocalFinalVariableName"/>\n <module name="LocalVariableName"/>\n <module name="MemberName"/>\n <module name="MethodName"/>\n <module name="PackageName"/>\n <module name="ParameterName"/>\n <module name="StaticVariableName"/>\n <module name="TypeName"/>\n\n <!-- Checks for imports -->\n <!-- See http://checkstyle.sf.net/config_import.html -->\n <module name="AvoidStarImport"/>\n <module name="IllegalImport"/> <!-- defaults to sun.* packages -->\n <module name="RedundantImport"/>\n <module name="UnusedImports">\n <property name="processJavadoc" value="false"/>\n </module>\n\n <!-- Checks for Size Violations. -->\n <!-- See http://checkstyle.sf.net/config_sizes.html -->\n <module name="LineLength"/>\n <module name="MethodLength"/>\n <module name="ParameterNumber"/>\n\n <!-- Checks for whitespace -->\n <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n <module name="EmptyForIteratorPad"/>\n <module name="GenericWhitespace"/>\n <module name="MethodParamPad"/>\n <module name="NoWhitespaceAfter"/>\n <module name="NoWhitespaceBefore"/>\n <module name="OperatorWrap"/>\n <module name="ParenPad"/>\n <module name="TypecastParenPad"/>\n <module name="WhitespaceAfter"/>\n <module name="WhitespaceAround"/>\n\n <!-- Modifier Checks -->\n <!-- See http://checkstyle.sf.net/config_modifiers.html -->\n <module name="ModifierOrder"/>\n <module name="RedundantModifier"/>\n\n <!-- Checks for blocks. You know, those {}\'s -->\n <!-- See http://checkstyle.sf.net/config_blocks.html -->\n <module name="AvoidNestedBlocks"/>\n <module name="EmptyBlock"/>\n <module name="LeftCurly"/>\n <module name="NeedBraces"/>\n <module name="RightCurly"/>\n\n <!-- Checks for common coding problems -->\n <!-- See http://checkstyle.sf.net/config_coding.html -->\n <module name="AvoidInlineConditionals"/>\n <module name="EmptyStatement"/>\n <module name="EqualsHashCode"/>\n <module name="HiddenField"/>\n <module name="IllegalInstantiation"/>\n <module name="InnerAssignment"/>\n <module name="MagicNumber"/>\n <module name="MissingSwitchDefault"/>\n <module name="SimplifyBooleanExpression"/>\n <module name="SimplifyBooleanReturn"/>\n\n <!-- Checks for class design -->\n <!-- See http://checkstyle.sf.net/config_design.html -->\n <module name="DesignForExtension"/>\n <module name="FinalClass"/>\n <module name="HideUtilityClassConstructor"/>\n <module name="InterfaceIsType"/>\n <module name="VisibilityModifier"/>\n\n <!-- Miscellaneous other checks. -->\n <!-- See http://checkstyle.sf.net/config_misc.html -->\n <module name="ArrayTypeStyle"/>\n <module name="FinalParameters"/>\n <module name="TodoComment"/>\n <module name="UpperEll"/>\n\n </module>\n\n</module>\n'}
run(self, tempdir: str, emit: Callable[[str, int, str, str], None], process_completed: Callable[[subprocess.CompletedProcess], None]) → None[source]

Run checkstyle

Arguments are the same as for Linter.run().

Return type:None
classmethod validate_config(config: str) → None[source]

Check if the given config is valid for checkstyle.

This also does some extra checks to make sure some invalid properties are not present.

Parameters:config (str) – The config to check.
Return type:None
class psef.linters.Flake8(self, cfg: str) → None[source]

Bases: psef.linters.Linter

Run the Flake8 linter.

This linter checks for errors in python code and checks the pep8 python coding standard. All “noqa”s are disabled when running.

DEFAULT_OPTIONS = {'Empty config file': ''}
run(self, tempdir: str, emit: Callable[[str, int, str, str], None], process_completed: Callable[[subprocess.CompletedProcess], None]) → None[source]

Run the linter on the code in tempdir.

Parameters:
  • tempdir (str) – The temp directory that should contain the code to run the linter on.
  • emit (Callable[[str, int, str, str], None]) – A callback to emit a line of feedback, where the first argument is the filename, the second is the line number, the third is the code of the linter error, and the fourth and last is the message of the linter.
  • process_completed (Callable[[CompletedProcess], None]) – The callback that should be called, directly, after the process has been completed.
Return type:

None

class psef.linters.Linter(self, cfg: str) → None[source]

Bases: abc.ABC

The base class for a linter.

Every linter should inherit from this class as they are discovered by reflecting on subclasses from this class. They should override the run method, and they may override the DEFAULT_OPTIONS variable. If RUN_LINTER is set to False we never actually run the linter, but only create a models.AssignmentLinter for this assignment and a models.LinterInstance for each submission.

Note

If a linter doesn’t override DEFAULT_OPTIONS the user will not have the ability to define a custom configuration for the linter in the frontend.

DEFAULT_OPTIONS = {}
RUN_LINTER = True
run(self, tempdir: str, emit: Callable[[str, int, str, str], None], process_completed: Callable[[subprocess.CompletedProcess], None]) → None[source]

Run the linter on the code in tempdir.

Parameters:
  • tempdir (str) – The temp directory that should contain the code to run the linter on.
  • emit (Callable[[str, int, str, str], None]) – A callback to emit a line of feedback, where the first argument is the filename, the second is the line number, the third is the code of the linter error, and the fourth and last is the message of the linter.
  • process_completed (Callable[[CompletedProcess], None]) – The callback that should be called, directly, after the process has been completed.
Return type:

None

classmethod validate_config(config: str) → None[source]

Verify if the config is correct.

This method should never return something but raise a ValidationException instead.

Parameters:config (str) – The config to validate.
Return type:None
exception psef.linters.LinterCrash(self, error_summary: Optional[str] = None) → None[source]

Bases: Exception

The exception to use that a linter has crashed.

This is a semi-controlled crash, so it should be used when the linter process itself returned some unexpected code or invalid output. It should not be used for programming errors within CodeGrade code processing the output of the linter.

class psef.linters.LinterRunner(self, cls: Type[psef.linters.Linter], cfg: str) → None[source]

Bases: object

This class is used to run a Linter with a specific config on sets of models.Work.

linter
The attached :class:`Linter` that will be ran by this class.
run(self, linter_instance_ids: Sequence[str]) → None[source]

Run this linter runner on the given works.

Note

This method takes a long time to execute, please run it in a thread.

Parameters:linter_instance_ids (Sequence[str]) – A sequence of all the ids of the linter instances which should be run. If this linter instance has already run once its old comments will be removed.
Return type:None
Returns:Nothing
test(self, linter_instance: psef.models.linter.LinterInstance, process_completed: Callable[[subprocess.CompletedProcess], None]) → None[source]

Test the given code (models.Work) and add generated comments.

Parameters:
  • linter_instance (LinterInstance) – The linter instance that will be run. This linter instance is linked to a work from which all files will be restored and the linter will be run on those files.
  • process_completed (Callable[[CompletedProcess], None]) – The callback that should be called by the linter instance after the process, like pylint or java, has been completed.
Return type:

None

Returns:

Nothing

class psef.linters.MixedWhitespace(self, cfg: str) → None[source]

Bases: psef.linters.Linter

Run the MixedWhitespace linter.

This linter checks if a file contains mixed indentation on the same line. It doesn’t catch different types of indentation being used in a file but on different lines. Instead of adding a comment in the sidebar, the mixed whitespace will be highlighted in the code.

RUN_LINTER = False
run(self, tempdir: str, emit: Callable[[str, int, str, str], None], process_completed: Callable[[subprocess.CompletedProcess], None]) → None[source]

Run the linter on the code in tempdir.

Parameters:
  • tempdir (str) – The temp directory that should contain the code to run the linter on.
  • emit (Callable[[str, int, str, str], None]) – A callback to emit a line of feedback, where the first argument is the filename, the second is the line number, the third is the code of the linter error, and the fourth and last is the message of the linter.
  • process_completed (Callable[[CompletedProcess], None]) – The callback that should be called, directly, after the process has been completed.
Return type:

None

class psef.linters.PMD(self, cfg: str) → None[source]

Bases: psef.linters.Linter

Run the PMD linter.

This checks java source files for common errors. It is configured by a xml file. It is not possible to upload your own rules, neither is it possible to use XPath rules.

DEFAULT_OPTIONS = {'Maven': '<?xml version="1.0"?>\n<!--\n\nLicensed to the Apache Software Foundation (ASF) under one\nor more contributor license agreements. See the NOTICE file\ndistributed with this work for additional information\nregarding copyright ownership. The ASF licenses this file\nto you under the Apache License, Version 2.0 (the\n"License"); you may not use this file except in compliance\nwith the License. You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, either express or implied. See the License for the\nspecific language governing permissions and limitations\nunder the License.\n-->\n<ruleset name="Default Maven PMD Plugin Ruleset"\n xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"\n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">\n <description>\n The default ruleset used by the Maven PMD Plugin, when no other ruleset is specified.\n It contains the rules of the old (pre PMD 6.0.0) rulesets java-basic, java-empty, java-imports,\n java-unnecessary, java-unusedcode.\n\n This ruleset might be used as a starting point for an own customized ruleset [0].\n\n [0] https://pmd.github.io/latest/pmd_userdocs_understanding_rulesets.html\n </description>\n <rule ref="category/java/bestpractices.xml/AvoidUsingHardCodedIP" class="hello"/>\n <rule ref="category/java/bestpractices.xml/CheckResultSet"/>\n <rule ref="category/java/bestpractices.xml/UnusedImports"/>\n <rule ref="category/java/bestpractices.xml/UnusedFormalParameter"/>\n <rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>\n <rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>\n <rule ref="category/java/bestpractices.xml/UnusedPrivateMethod"/>\n <rule ref="category/java/codestyle.xml/DontImportJavaLang"/>\n <rule ref="category/java/codestyle.xml/DuplicateImports"/>\n <rule ref="category/java/codestyle.xml/ExtendsObject"/>\n <rule ref="category/java/codestyle.xml/ForLoopShouldBeWhileLoop"/>\n <rule ref="category/java/codestyle.xml/TooManyStaticImports"/>\n <rule ref="category/java/codestyle.xml/UnnecessaryFullyQualifiedName"/>\n <rule ref="category/java/codestyle.xml/UnnecessaryModifier"/>\n <rule ref="category/java/codestyle.xml/UnnecessaryReturn"/>\n <rule ref="category/java/codestyle.xml/UselessParentheses"/>\n <rule ref="category/java/codestyle.xml/UselessQualifiedThis"/>\n <rule ref="category/java/design.xml/CollapsibleIfStatements"/>\n <rule ref="category/java/design.xml/SimplifiedTernary"/>\n <rule ref="category/java/design.xml/UselessOverridingMethod"/>\n <rule ref="category/java/errorprone.xml/AvoidBranchingStatementAsLastInLoop"/>\n <rule ref="category/java/errorprone.xml/AvoidDecimalLiteralsInBigDecimalConstructor"/>\n <rule ref="category/java/errorprone.xml/AvoidMultipleUnaryOperators"/>\n <rule ref="category/java/errorprone.xml/AvoidUsingOctalValues"/>\n <rule ref="category/java/errorprone.xml/BrokenNullCheck"/>\n <rule ref="category/java/errorprone.xml/CheckSkipResult"/>\n <rule ref="category/java/errorprone.xml/ClassCastExceptionWithToArray"/>\n <rule ref="category/java/errorprone.xml/DontUseFloatTypeForLoopIndices"/>\n <rule ref="category/java/errorprone.xml/EmptyCatchBlock"/>\n <rule ref="category/java/errorprone.xml/EmptyFinallyBlock"/>\n <rule ref="category/java/errorprone.xml/EmptyIfStmt"/>\n <rule ref="category/java/errorprone.xml/EmptyInitializer"/>\n <rule ref="category/java/errorprone.xml/EmptyStatementBlock"/>\n <rule ref="category/java/errorprone.xml/EmptyStatementNotInLoop"/>\n <rule ref="category/java/errorprone.xml/EmptySwitchStatements"/>\n <rule ref="category/java/errorprone.xml/EmptySynchronizedBlock"/>\n <rule ref="category/java/errorprone.xml/EmptyTryBlock"/>\n <rule ref="category/java/errorprone.xml/EmptyWhileStmt"/>\n <rule ref="category/java/errorprone.xml/ImportFromSamePackage"/>\n <rule ref="category/java/errorprone.xml/JumbledIncrementer"/>\n <rule ref="category/java/errorprone.xml/MisplacedNullCheck"/>\n <rule ref="category/java/errorprone.xml/OverrideBothEqualsAndHashcode"/>\n <rule ref="category/java/errorprone.xml/ReturnFromFinallyBlock"/>\n <rule ref="category/java/errorprone.xml/UnconditionalIfStatement"/>\n <rule ref="category/java/errorprone.xml/UnnecessaryConversionTemporary"/>\n <rule ref="category/java/errorprone.xml/UnusedNullCheckInEquals"/>\n <rule ref="category/java/errorprone.xml/UselessOperationOnImmutable"/>\n <rule ref="category/java/multithreading.xml/AvoidThreadGroup"/>\n <rule ref="category/java/multithreading.xml/DontCallThreadRun"/>\n <rule ref="category/java/multithreading.xml/DoubleCheckedLocking"/>\n <rule ref="category/java/performance.xml/BigIntegerInstantiation"/>\n <rule ref="category/java/performance.xml/BooleanInstantiation"/>\n</ruleset>\n'}
run(self, tempdir: str, emit: Callable[[str, int, str, str], None], process_completed: Callable[[subprocess.CompletedProcess], None]) → None[source]

Run PMD.

Arguments are the same as for Linter.run().

Return type:None
classmethod validate_config(config: str) → None[source]

Check if the given config is valid for PMD.

This also does some extra checks to make sure some invalid properties are not present.

Parameters:config (str) – The config to check.
Return type:None
class psef.linters.Pylint(self, cfg: str) → None[source]

Bases: psef.linters.Linter

The pylint checker.

This checks python modules for many common errors. It only works on proper modules and will display an error on the first line of every file if the given code was not a proper module.

DEFAULT_OPTIONS = {'Empty config file': ''}
run(self, tempdir: str, emit: Callable[[str, int, str, str], None], process_completed: Callable[[subprocess.CompletedProcess], None]) → None[source]

Run the pylinter.

Arguments are the same as for Linter.run().

Return type:None
psef.linters.get_all_linters() → Dict[str, Dict[str, Union[str, Mapping[str, str]]]][source]

Get an overview of all linters.

The returned linters are all the subclasses of Linter.

Return type:dict[str, dict[str, Union[str, Mapping[str, str]]]]
Returns:A mapping of the name of the linter to a dictionary containing the description and the default options of the linter with that name
>>> @_linter_handlers.register('MyLinter')
... class MyLinter(Linter): pass
>>> MyLinter.__doc__ = "Description"
>>> MyLinter.DEFAULT_OPTIONS = {'wow': 'sers'}
>>> all_linters = get_all_linters()
>>> sorted(all_linters.keys())
['Checkstyle', 'Flake8', 'MixedWhitespace', 'MyLinter', 'PMD', 'Pylint']
>>> linter = all_linters['MyLinter']
>>> linter == {'desc': 'Description', 'opts': {'wow': 'sers'} }
True
psef.linters.get_linter_by_name(name: str) → Type[psef.linters.Linter][source]

Get the linter class associated with the given name.

Parameters:name (str) – The name of the linter wanted.
Return type:type[Linter]
Returns:The linter with the attribute __name__ equal to name. If there are multiple linters with the name name the result can be any one of these linters.
Raises:KeyError – If the linter with the specified name is not found.
psef.linters.init_app(_: Any) → None[source]
Return type:None

psef.lti

This module contains the code used for all LTI functionality.

SPDX-License-Identifier: AGPL-3.0-only AND MIT

This entire file is licensed as AGPL-3.0-only, except for the code copied from https://github.com/ucfopen/lti-template-flask-oauth-tokens which is MIT licensed. This copied code is located between the # START OF MIT LICENSED COPIED WORK # and the # END OF MIT LICENSED COPIED WORK # comment blocks.

class psef.lti.BareBonesLTIProvider(self, params: Mapping[str, str], lti_provider: psef.models.lti_provider.LTIProvider = None) → None[source]

Bases: psef.lti.LTI

The LTI class that implements LTI a for simple “bare bones” LMS.

assignment_id

The id of the current LTI assignment.

Return type:str
assignment_name

The name of the current LTI assignment.

Return type:str
assignment_points_possible

The amount of points possible for the launched assignment.

Return type:Optional[float]
Returns:The possible points or None if this option is not supported or if the data is not present.
assignment_state

The state of the current LTI assignment.

Return type:_AssignmentStateEnum
course_id

The course id of the current LTI course.

Return type:str
course_name

The name of the current LTI course.

Return type:str
get_assignment_deadline(self, default: datetime.datetime = None) → Optional[datetime.datetime][source]

Get the deadline of the current LTI assignment.

Parameters:default (Optional[datetime]) – The value to be returned of the assignment has no deadline. If default.__bool__ is False the current date plus 365 days is used.
Return type:Optional[datetime]
Returns:The deadline of the assignment as a datetime.
has_outcome_service_url(self) → bool[source]

Check if the current LTI request has outcome_service_url field.

This is not the case when launch LTI occurs for viewing a result.

Return type:bool
Returns:A boolean indicating if a sourcedid field was found.
has_result_sourcedid(self) → bool[source]

Check if the current LTI request has a sourcedid field.

Return type:bool
Returns:A boolean indicating if a sourcedid field was found.
outcome_service_url

The url used to passback grades to the LMS.

Return type:str
classmethod passback_grade(*, key: str, secret: str, grade: Union[float, None, int], initial: bool, service_url: str, sourcedid: str, lti_points_possible: Optional[float], submission: psef.models.work.Work, host: str) → None[source]

Do a LTI grade passback.

Parameters:
  • key (str) – The oauth key to use.
  • secret (str) – The oauth secret to use.
  • grade (Union[float, None, int]) – The grade to pass back, between 0 and 10. If it is None the grade will be deleted, if it is a bool no grade information will be send.
  • service_url (str) – The url used for grade passback.
  • sourcedid (str) – The sourcedid used in the grade passback.
  • lti_points_possible (Optional[float]) – The maximum amount of points possible for the assignment we are passing back as reported by the LMS during launch.
  • host (str) – The host of this CodeGrade instance.
Return type:

None

Returns:

The response of the LTI consumer.

result_sourcedid

The sourcedid of the current user for the current assignment.

Return type:str
roles

The normalized roles of the current LTI user.

Return type:Iterable[LTIRole]
static supports_lti_launch_as_result() → bool[source]

Does this LTI consumer support the ltiLaunchUrl as result field.

Return type:bool
username

The username of the current LTI user.

Return type:str
class psef.lti.BlackboardLTI(self, params: Mapping[str, str], lti_provider: psef.models.lti_provider.LTIProvider = None) → None[source]

Bases: psef.lti.BareBonesLTIProvider

The LTI class used for the Blackboard LMS.

class psef.lti.CanvasLTI(self, params: Mapping[str, str], lti_provider: psef.models.lti_provider.LTIProvider = None) → None[source]

Bases: psef.lti.LTI

The LTI class used for the Canvas LMS.

assignment_id

The id of the current LTI assignment.

Return type:str
assignment_name

The name of the current LTI assignment.

Return type:str
assignment_points_possible

The amount of points possible for the launched assignment.

Return type:Optional[float]
assignment_state

The state of the current LTI assignment.

Return type:_AssignmentStateEnum
course_id

The course id of the current LTI course.

Return type:str
course_name

The name of the current LTI course.

Return type:str
get_assignment_deadline(self, default: datetime.datetime = None) → Optional[datetime.datetime][source]

Get the deadline of the current LTI assignment.

Parameters:default (Optional[datetime]) – The value to be returned of the assignment has no deadline. If default.__bool__ is False the current date plus 365 days is used.
Return type:Optional[datetime]
Returns:The deadline of the assignment as a datetime.
static get_custom_extensions() → str[source]

Get a string that will be used verbatim in the LTI xml.

Return type:str
Returns:A string used as extension
static get_lti_properties() → List[psef.lti.LTIProperty][source]

All extension properties used by Canvas.

Return type:list[LTIProperty]
Returns:The extension properties needed.
has_outcome_service_url(self) → bool[source]

Check if the current LTI request has outcome_service_url field.

This is not the case when launch LTI occurs for viewing a result.

Return type:bool
Returns:A boolean indicating if a sourcedid field was found.
has_result_sourcedid(self) → bool[source]

Check if the current LTI request has a sourcedid field.

Return type:bool
Returns:A boolean indicating if a sourcedid field was found.
outcome_service_url

The url used to passback grades to the LMS.

Return type:str
classmethod passback_grade(*, key: str, secret: str, grade: Union[float, None, int], initial: bool, service_url: str, sourcedid: str, lti_points_possible: Optional[float], submission: psef.models.work.Work, host: str) → None[source]

Do a LTI grade passback.

Parameters:
  • key (str) – The oauth key to use.
  • secret (str) – The oauth secret to use.
  • grade (Union[float, None, int]) – The grade to pass back, between 0 and 10. If it is None the grade will be deleted, if it is a bool no grade information will be send.
  • service_url (str) – The url used for grade passback.
  • sourcedid (str) – The sourcedid used in the grade passback.
  • lti_points_possible (Optional[float]) – The maximum amount of points possible for the assignment we are passing back as reported by the LMS during launch.
  • host (str) – The host of this CodeGrade instance.
Return type:

None

Returns:

The response of the LTI consumer.

result_sourcedid

The sourcedid of the current user for the current assignment.

Return type:str
roles

The normalized roles of the current LTI user.

Return type:Iterable[LTIRole]
static supports_deadline() → bool[source]

Determines whether this LMS sends the deadline of an assignment along with the lti launch request. If it does, the deadline for that assignment cannot be changed from within CodeGrade.

Return type:bool
Returns:A boolean indicating whether this LTI instance gives the deadline to CodeGrade.
static supports_lti_common_cartridge() → bool[source]

Canvas supports common cartridges

Return type:bool
static supports_lti_launch_as_result() → bool[source]

Does this LTI consumer support the ltiLaunchUrl as result field.

Return type:bool
static supports_max_points() → bool[source]

Determine whether this LMS supports changing the max points.

Return type:bool
Returns:A boolean indicating whether this LTI instance supports changing the max points.
username

The username of the current LTI user.

Return type:str
class psef.lti.LTI(self, params: Mapping[str, str], lti_provider: psef.models.lti_provider.LTIProvider = None) → None[source]

Bases: object

The base LTI class.

assignment_id

The id of the current LTI assignment.

Return type:str
assignment_name

The name of the current LTI assignment.

Return type:str
assignment_points_possible

The amount of points possible for the launched assignment.

Return type:Optional[float]
Returns:The possible points or None if this option is not supported or if the data is not present.
assignment_state

The state of the current LTI assignment.

Return type:_AssignmentStateEnum
course_id

The course id of the current LTI course.

Return type:str
course_name

The name of the current LTI course.

Return type:str
static create_from_launch_params(params: Mapping[str, str]) → psef.lti.LTI[source]

Create an instance from launch params.

The params should have an lti_class key with the name of the class to be instantiated.

Parameters:params (Mapping[str, str]) – The launch params to create the LTI instance from.
Return type:LTI
Returns:A fresh LTI instance.
static create_from_request(req: flask.wrappers.Request) → psef.lti.LTI[source]

Create an instance from a flask request.

The request should have a form variable that has all the right parameters. So this request should be from an LTI launch.

Parameters:req (Request) – The request to create the LTI instance from.
Return type:LTI
Returns:A fresh LTI instance.
ensure_lti_user(self) → Tuple[psef.models.user.User, Optional[str], Optional[str]][source]

Make sure the current LTI user is logged in as a psef user.

This is done by first checking if we know a user with the current LTI user_id, if this is the case this is the user we log in and return.

Otherwise we check if a user is logged in and this user has no LTI user_id, if this is the case we link the current LTI user_id to the current logged in user and return this user.

Otherwise we create a new user and link this user to current LTI user_id.

Return type:tuple[User, Optional[str], Optional[str]]
Returns:A tuple containing the items in order: the user found as described above, optionally a new token for the user to login with, optionally the updated email of the user as a string, this is None if the email was not updated.
full_name

The name of the current LTI user.

Return type:str
classmethod generate_xml() → str[source]

Generate a config XML for this LTI consumer.

Return type:str
get_assignment(self, user: psef.models.user.User, course: psef.models.course.Course) → psef.models.assignment.Assignment[source]

Get the current LTI assignment as a psef assignment.

Return type:Assignment
get_assignment_deadline(self, default: datetime.datetime = None) → Optional[datetime.datetime][source]

Get the deadline of the current LTI assignment.

Parameters:default (Optional[datetime]) – The value to be returned of the assignment has no deadline. If default.__bool__ is False the current date plus 365 days is used.
Return type:Optional[datetime]
Returns:The deadline of the assignment as a datetime.
get_course(self) → psef.models.course.Course[source]

Get the current LTI course as a psef course.

Return type:Course
static get_custom_extensions() → str[source]

Get a string that will be used verbatim in the LTI xml.

Return type:str
Returns:A string used as extension
static get_lti_properties() → List[psef.lti.LTIProperty][source]

All extension properties used by this LMS.

Return type:list[LTIProperty]
Returns:The extension properties needed.
has_outcome_service_url(self) → bool[source]

Check if the current LTI request has outcome_service_url field.

This is not the case when launch LTI occurs for viewing a result.

Return type:bool
Returns:A boolean indicating if a sourcedid field was found.
has_result_sourcedid(self) → bool[source]

Check if the current LTI request has a sourcedid field.

Return type:bool
Returns:A boolean indicating if a sourcedid field was found.
outcome_service_url

The url used to passback grades to the LMS.

Return type:str
classmethod passback_grade(*, key: str, secret: str, grade: Union[float, None, int], initial: bool, service_url: str, sourcedid: str, lti_points_possible: Optional[float], submission: psef.models.work.Work, host: str) → None[source]

Do a LTI grade passback.

Parameters:
  • key (str) – The oauth key to use.
  • secret (str) – The oauth secret to use.
  • grade (Union[float, None, int]) – The grade to pass back, between 0 and 10. If it is None the grade will be deleted, if it is a bool no grade information will be send.
  • service_url (str) – The url used for grade passback.
  • sourcedid (str) – The sourcedid used in the grade passback.
  • lti_points_possible (Optional[float]) – The maximum amount of points possible for the assignment we are passing back as reported by the LMS during launch.
  • host (str) – The host of this CodeGrade instance.
Return type:

None

Returns:

The response of the LTI consumer.

result_sourcedid

The sourcedid of the current user for the current assignment.

Return type:str
roles

The normalized roles of the current LTI user.

Return type:Iterable[LTIRole]
set_user_course_role(self, user: psef.models.user.User, course: psef.models.course.Course) → Union[str, bool][source]

Set the course role for the given course and user if there is no such role just yet.

The mapping is done using LTI_COURSEROLE_LOOKUPS. If no role could be found a new role is created with the default permissions.

Parameters:
  • user (models.User) – The user to set the course role for.
  • course (models.Course) – The course to connect to user to.
Return type:

Union[str, bool]

Returns:

True if a new role was created.

set_user_role(self, user: psef.models.user.User) → None[source]

Set the role of the given user if the user has no role.

The role is determined according to LTI_SYSROLE_LOOKUPS.

Note

If the role could not be matched the DEFAULT_ROLE configured in the config of the app is used.

Parameters:user (models.User) – The user to set the role for.
Returns:Nothing
Return type:None
static supports_deadline() → bool[source]

Determines whether this LMS sends the deadline of an assignment along with the lti launch request. If it does, the deadline for that assignment cannot be changed from within CodeGrade.

Return type:bool
Returns:A boolean indicating whether this LTI instance gives the deadline to CodeGrade.
static supports_lti_common_cartridge() → bool[source]
Does this LMS support configuration using a Common Cartridge
[common_cartridge].
[common_cartridge]See here for more information.
Return type:bool
static supports_lti_launch_as_result() → bool[source]

Does this LTI consumer support the ltiLaunchUrl as result field.

Return type:bool
static supports_max_points() → bool[source]

Determine whether this LMS supports changing the max points.

Return type:bool
Returns:A boolean indicating whether this LTI instance supports changing the max points.
user_email

The email of the current LTI user.

Return type:str
user_id

The unique id of the current LTI user.

Return type:str
username

The username of the current LTI user.

Return type:str
class psef.lti.LTIDeleteResultOperation[source]

Bases: psef.lti.LTIOperation

A delete result LTI operation.

This operation deletes the result at canvas.

request_type = 'deleteResultRequest'
class psef.lti.LTIInitalReplaceResultOperation(self, data_type: psef.lti.LTIResultDataType, data_value: str, submission_details: psef.lti.SubmissionDetails) → None[source]

Bases: psef.lti.LTIReplaceResultBaseOperation

An initial replaceResult operation, which doens’t provide a grade.

class psef.lti.LTINormalReplaceResultOperation(self, data_type: psef.lti.LTIResultDataType, data_value: str, submission_details: psef.lti.SubmissionDetails, grade: str) → None[source]

Bases: psef.lti.LTIReplaceResultBaseOperation

A normal replaceResult with a grade that is in [0, 1]

class psef.lti.LTIOperation[source]

Bases: abc.ABC

Base class representing a LTI operation.

request_type

Get the name of this operation as required in the LTI xml.

Return type:str
class psef.lti.LTIProperty(self, internal: str, external: str) → None[source]

Bases: object

An LTI property.

Variables:
  • internal – The name the property should be names in the launch params.
  • external – The external name of the property.
class psef.lti.LTIRawReplaceResultOperation(self, data_type: psef.lti.LTIResultDataType, data_value: str, submission_details: psef.lti.SubmissionDetails, grade: str) → None[source]

Bases: psef.lti.LTIReplaceResultBaseOperation

A raw replaceResult with a grade that represents the amount of points a user got for the assignment.

Note

This is currently only supported by Canvas.

class psef.lti.LTIReplaceResultBaseOperation(self, data_type: psef.lti.LTIResultDataType, data_value: str, submission_details: psef.lti.SubmissionDetails) → None[source]

Bases: psef.lti.LTIOperation, abc.ABC

The base replaceResult LTI operation.

request_type = 'replaceResultRequest'
class psef.lti.LTIResultDataType[source]

Bases: enum.Enum

All possible result data options for an LTI replaceResult request.

lti_launch_url = 'ltiLaunchUrl'
text = 'text'
url = 'url'
class psef.lti.LTIRole(self, *, kind: psef.lti.LTIRoleKind, name: str, subnames: Sequence[str]) → None[source]

Bases: object

LTI role, parsed from a role urn. The urn must be of the form urn:lti:{kind}:ims/lis/{name}/{subname}.

Variables:
  • kind – Kind of the role.
  • name – Primary role name.
  • subnames – Secondary role names.
classmethod parse(urn: str) → T_LTI_ROLE[source]

Parse a LTI role from a IMS urn.

Parameters:urn (str) – The string to parse.
Return type:T_LTI_ROLE
Returns:The parsed LTI role.
Raises:LTIRoleException – If the role is not valid.
exception psef.lti.LTIRoleException(self, message: str, description: str, api_code: psef.exceptions.APICodes, status_code: int, **rest) → None[source]

Bases: psef.exceptions.APIException

Thrown when a role could not be parsed.

class psef.lti.LTIRoleKind[source]

Bases: enum.Enum

Kind of an LTI role.

course = 3
get_kind(lti_kind: str) → psef.lti.LTIRoleKind = <bound method LTIRoleKind.get_kind of <enum 'LTIRoleKind'>>[source]
institution = 2
system = 1
class psef.lti.MoodleLTI(self, params: Mapping[str, str], lti_provider: psef.models.lti_provider.LTIProvider = None) → None[source]

Bases: psef.lti.BareBonesLTIProvider

The LTI class used for the Moodle LMS.

classmethod passback_grade(*, key: str, secret: str, grade: Union[float, None, int], initial: bool, service_url: str, sourcedid: str, lti_points_possible: Optional[float], submission: psef.models.work.Work, host: str) → None[source]

Do a LTI grade passback.

Parameters:
  • key (str) – The oauth key to use.
  • secret (str) – The oauth secret to use.
  • grade (Union[float, None, int]) – The grade to pass back, between 0 and 10. If it is None the grade will be deleted, if it is a bool no grade information will be send.
  • service_url (str) – The url used for grade passback.
  • sourcedid (str) – The sourcedid used in the grade passback.
  • lti_points_possible (Optional[float]) – The maximum amount of points possible for the assignment we are passing back as reported by the LMS during launch.
  • host (str) – The host of this CodeGrade instance.
Return type:

None

Returns:

The response of the LTI consumer.

static supports_lti_common_cartridge() → bool[source]

Moodle supports common cartridges

Return type:bool
username

The username of the current LTI user.

Return type:str
class psef.lti.OutcomeRequest(self, *, lis_outcome_service_url: str, lis_result_sourcedid: str, consumer_key: str, consumer_secret: str, lti_operation: psef.lti.LTIOperation, message_identifier: str) → None[source]

Bases: object

Class for generating LTI Outcome Requests.

Outcome Request documentation:
http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html#_Toc319560472
post_outcome_request(self) → psef.lti.OutcomeResponse[source]

Send this OutcomeRequest to the LTI provider.

Return type:OutcomeResponse
Returns:The parsed response from the provider.
class psef.lti.OutcomeResponse(self, input_xml: str) → None[source]

Bases: object

This class consumes LTI Outcome Responses.

Response documentation:
http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html#_Toc319560472
Error code documentation:
http://www.imsglobal.org/gws/gwsv1p0/imsgws_baseProfv1p0.html#1639667
>>> import doctest
>>> import os
>>> doctest.ELLIPSIS_MARKER = '-etc-'
>>> get_file = lambda name: open(
... os.path.join(
...   os.path.dirname(__file__),
...   '..',
...   'test_data',
...   'example_strings',
...   name,
... )).read()
>>> res = OutcomeResponse(get_file('invalid_xml.xml'))
-etc-
>>> res.code_major, res.severity, res.description
('failure', 'error', 'unknown error')
>>> res.is_success, res.is_failure, res.has_warning
(False, True, False)
>>> res = OutcomeResponse(get_file('invalid_replace_result.xml'))
-etc-
>>> res.code_major, res.severity, res.description
('failure', 'error', 'unknown error')
>>> res.is_success, res.is_failure, res.has_warning
(False, True, False)
>>> res = OutcomeResponse(get_file('valid_replace_result.xml'))
>>> (
...   res.code_major,
...   res.severity,
...   res.description,
...   res.message_identifier,
...   res.message_ref_identifier,
... )
('success', 'status', 'Score for 3124567 is now 0.92', '4560', '999999123')
>>> res.is_success, res.is_failure, res.has_warning
(True, False, False)
has_warning

Check if a response had a warning

Return type:bool
Returns:True if the response had a warning
is_failure

Check if a response indicated a failure.

Return type:bool
Returns:True if the response indicated a failure.
is_success

Check if a response indicated a success.

Return type:bool
Returns:True if the response indicated a success.
class psef.lti.SubmissionDetails

Bases: dict

psef.lti.init_app(_: Any) → None[source]
Return type:None

psef.mail

This module is used for all mailing related tasks.

SPDX-License-Identifier: AGPL-3.0-only

psef.mail.send_grade_reminder_email(assig: psef.models.assignment.Assignment, user: psef.models.user.User, mailer: flask_mail.Mail) → None[source]

Remind a user to grade a given assignment.

Parameters:
  • assig (Assignment) – The assignment that has to be graded.
  • user (User) – The user that should resume/start grading.
Mailer:

The mailer used to mail, this is important for performance.

Return type:

None

Returns:

Nothing

psef.mail.send_grader_status_changed_mail(assig: psef.models.assignment.Assignment, user: psef.models.user.User) → None[source]

Send grader status changed mail.

Parameters:
  • assig (Assignment) – The assignment of which the status has changed.
  • user (User) – The user whose status has changed.
Return type:

None

Returns:

Nothing

psef.mail.send_reset_password_email(user: psef.models.user.User) → None[source]

Send the reset password email to a user.

Parameters:user (User) – The user that has requested a reset password email.
Return type:None
Returns:Nothing
psef.mail.send_whopie_done_email(assig: psef.models.assignment.Assignment) → None[source]

Send whoepie done email for the given assignment.

Parameters:assig (Assignment) – The assignment to send the mail for.
Return type:None
Returns:Nothing

psef.models

This module defines all the objects in the database in their relation.

psef.models.assignment

This module defines all models needed for an assignment.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.assignment.Assignment(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class describes a course_models.Course specific assignment.

Variables:
  • name – The name of the assignment.
  • cgignore – The .cgignore file of this assignment.
  • state – The current state the assignment is in.
  • description – UNUSED
  • course – The course this assignment belongs to.
  • created_at – The date this assignment was added.
  • deadline – The deadline of this assignment.
  • _mail_task_id – This is the id of the current task that will email all the TA’s to hurry up with grading.
  • reminder_email_time – The time the reminder email should be sent. To see if we should actually send these reminders look at done_type
  • done_email – The email address we should sent a email if the grading is done. The function email.utils.getaddresses() should be able to parse this string.
  • done_type – The type of reminder that should be sent.
  • assigned_graders – All graders that are assigned to grade mapped by user_id to AssignmentAssignedGrader object.
  • rubric_rows – The rubric rows that make up the rubric for this assignment.
cgignore

The submission filter of this assignment.

Return type:Optional[SubmissionFilter]
change_notifications(self, done_type: Optional[psef.models.assignment.AssignmentDoneType], grader_date: Optional[datetime.datetime], done_email: Optional[str]) → None[source]

Change the notifications for the current assignment.

Parameters:
  • done_type (Optional[AssignmentDoneType]) – How to determine when the assignment is done. Set this value to None to disable the reminder for this assignment.
  • grader_date (Optional[datetime]) – The datetime when to send graders that are causing the assignment to be not done a reminder email.
  • done_email (Optional[str]) – The email to send a notification when grading for this assignment is done.
Return type:

None

connect_division(self, parent: Optional[Assignment]) → List[psef.models.work.Work][source]

Set the division parent of this assignment.

Parameters:parent (Optional[Assignment]) – The assignment that should be the new division parent of this assignment. Set this to None to remove the division parent of this assignment.
Return type:list[Work]
Returns:A list of submissions that couldn’t be assigned and are left unassigned.
divide_submissions(self, user_weights: Sequence[Tuple[user_models.User, float]]) → None[source]

Divide all newest submissions for this assignment between the given users.

This methods prefers to keep submissions assigned to the same grader as much as possible. To get completely new and random assignments first clear all old assignments.

Parameters:user_weights (Sequence[tuple[User, float]]) – A list of tuples that map users and the weights. The weights are used to determine how many submissions should be assigned to a single user.
Return type:None
Returns:Nothing.
get_all_graders(self, sort: bool = True) → psef.models.model_types._MyQuery[typing.Tuple[str, int, bool]][Tuple[str, int, bool]][source]

Get all graders for this assignment.

The graders are retrieved from the database using a single query. The return value is a query with three items selected: the first is the name of the grader, the second is the database id of the user object of grader and the third and last is a boolean indicating if this grader is done grading. You can use this query as an iterator.

Parameters:sort (bool) – Should the graders be sorted by name.
Return type:_MyQuery[tuple[str, int, bool]]
Returns:A query with items selected as described above.
get_all_latest_submissions(self) → psef.models.model_types._MyQuery[psef.models.work.Work][psef.models.work.Work][source]

Get a list of all the latest submissions (work_models.Work) by each user_models.User who has submitted at least one work for this assignment.

Return type:_MyQuery[Work]
Returns:The latest submissions.
get_assigned_grader_ids(self) → Iterable[int][source]

Get the ids of all the graders that have submissions assigned.

Note

This only gets graders with latest submissions assigned to them.

Return type:Iterable[int]
Returns:The ids of the all the graders that have work assigned within this assignnment.
get_assignee_for_submission(self, sub: psef.models.work.Work, *, from_divided: bool = True) → Optional[int][source]

Get the id of the default assignee for a given submission.

This checks if a user has handed in a submission to this assignment before. In that is the case the same assignee is returned. Otherwise, if the assignment has a division parent it checks for an assignee there, and uses the value if it is not None.

If the assignment doesn’t have a submission by the same user and is a parent it will search through the children. There the most common assignee for the submitting user is found. If such an assignee exists this value is returned.

Finally if from_divided is enabled it finds a assignee among the divided graders. If no assignee is found None is returned.

Parameters:
  • sub (Work) – The submission you want to get the assignee for.
  • from_divided (bool) – Get a grader from the divided graders if all other methods fail.
Return type:

Optional[int]

Returns:

The id of the assignee or None if no assignee was found.

get_assignee_from_division_children(self, student_id: int) → Optional[int][source]

Get id of the most common grader for a student in the division children of this assignment.

If this is tied a user is chosen arbitrarily from the most common graders. If the student is not present in the children None is returned.

Parameters:student_id (int) – The id of the student you want to get the assignee for.
Return type:Optional[int]
Returns:The id of the assignee, or None if there is none.
get_divided_amount_missing(self) → Tuple[Mapping[int, float], Callable[[int, bool], Mapping[int, float]]][source]

Get a mapping between user and the amount of submissions that they should be assigned but are not.

For example if we have two graders, John and Dorian that respectively have the weights 1 and 1 assigned. Lets say we have three submissions divided in such a way that John has to grade 2 and Dorian has to grade one, then our function we return that John is missing -0.5 submissions and Dorian is missing 0.5.

Note

If self.assigned_graders is empty this function and its recalculate will always return an empty directory.

Return type:tuple[Mapping[int, float], Callable[[int, bool], Mapping[int, float]]]
Returns:A mapping between user int and the amount missing as described above. Furthermore it returns a function that can be used to recalculate this mapping by given it the user id of the user that was assigned a submission.
get_from_latest_submissions(self, *to_query) → psef.models.model_types._MyQuery[~T][T][source]

Get the given fields from all last submitted submissions.

Parameters:to_query (T) – The field to get from the last submitted submissions.
Return type:_MyQuery[T]
Returns:A query object with the given fields selected from the last submissions.
graders_are_done(self) → bool[source]

Check if the graders of this assignment are done.

Return type:bool
Returns:A boolean indicating if the graders of this assignment are done.
has_group_submissions(self) → bool[source]

Check if the assignment has submissions by a group.

Return type:bool
Returns:True if the assignment has a submission by a group.
has_non_graded_submissions(self, user_id: int) → bool[source]

Check if the user with the given user_id has submissions assigned without a grade.

Parameters:user_id (int) – The id of the user to check for
Return type:bool
Returns:A boolean indicating if user has work assigned that does not have grade or a selected rubric item
is_done

Is the assignment done, which means that grades are open.

Return type:bool
is_hidden

Is the assignment hidden.

Return type:bool
is_lti

Is this assignment a LTI assignment.

Return type:bool
Returns:A boolean indicating if this is the case.
is_open

Is the current assignment open, which means the assignment is in the state students submit work.

Return type:bool
max_grade

Get the maximum grade possible for this assignment.

Return type:float
Returns:The maximum a grade for a submission.
max_rubric_points

Get the maximum amount of points possible for the rubric

Note

This is always higher than zero (so also not zero).

Return type:Optional[float]
Returns:The maximum amount of points.
min_grade = 0

The minimum grade for a submission in this assignment.

set_graders_to_not_done(self, user_ids: Sequence[int], send_mail: bool = False, ignore_errors: bool = False) → None[source]

Set the status of the given graders to ‘not done’.

Parameters:
  • user_ids (Sequence[int]) – The ids of the users that should be set to ‘not done’
  • send_mail (bool) – If True the users who are reset to ‘not done’ will get an email notifying them of this.
  • ignore_errors (bool) – Do not raise an error if a user in user_ids was not yet done.
Raises:

ValueError – If a user in user_ids was not yet done. This can happen because the user has not indicated this yet, because this user does not exist or because of any reason.

Return type:

None

set_max_grade(self, new_val: Union[None, float, int]) → None[source]

Set or unset the maximum grade for this assignment.

Parameters:new_val (Union[None, float, int]) – The new value for _max_grade.
Return type:None
Returns:Nothing.
set_state(self, state: str) → None[source]

Update the current state (class:_AssignmentStateEnum).

You can update the state to hidden, done or open. A assignment can not be updated to ‘submitting’ or ‘grading’ as this is an assignment with state of ‘open’ and, respectively, a deadline before or after the current time.

Parameters:state (str) – The new state, can be ‘hidden’, ‘done’ or ‘open’
Return type:None
Returns:Nothing
should_passback

Should we passback the current grade.

Return type:bool
state_name

The current name of the grade.

Warning

This is not the same as str(self.state).

Return type:str
Returns:The correct name of the current state.
validate_group_set(self, _: str, group_set: Optional[group_models.GroupSet]) → Optional[psef.models.group.GroupSet][source]

Make sure the course id of the group set is the same as the course id of the assignment.

Return type:Optional[GroupSet]
whitespace_linter

Check if this assignment has an associated MixedWhitespace linter.

Note

If the assignment is not yet done we check if the current_user has the permission can_see_grade_before_open.

Return type:bool
Returns:True if there is an AssignmentLinter with name MixedWhitespace and assignment_id.
whitespace_linter_exists

Does this assignment have a whitespace linter.

Return type:bool
class psef.models.assignment.AssignmentAssignedGrader(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

The class creates the link between an user_models.User and an Assignment.

The user linked to the assignment is an assigned grader. In this link the weight is the weight this user was given when assigning.

class psef.models.assignment.AssignmentDoneType[source]

Bases: enum.IntEnum

Describes what type of reminder should be sent.

Parameters:
  • none – Nobody should be e-mailed.
  • assigned_only – Only graders that are assigned will be notified.
  • all_graders – All users that have the permission to grade.
class psef.models.assignment.AssignmentGraderDone(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class creates the link between an user_models.User and an Assignment that exists only when the grader is done.

If a user is linked to the assignment this indicates that this user is done with grading.

Variables:
  • user_id – The id of the user that is linked.
  • assignment_id – The id of the assignment that is linked.
class psef.models.assignment.AssignmentLinter(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

The class is used when a linter (see psef.linters) is used on a Assignment.

Every work_models.Work that is tested is attached by a linter_models.LinterInstance.

The name identifies which linter_models.Linter is used.

Variables:
  • name – The name of the linter which is the __name__ of a subclass of linter_models.Linter.
  • tests – All the linter instances for this linter, this are the recordings of the running of the actual linter (so in the case of the Flake8 metadata about the flake8 program).
  • config – The config that was passed to the linter.
classmethod create_linter(assignment_id: int, name: str, config: str) → psef.models.assignment.AssignmentLinter[source]

Create a new instance of this class for a given Assignment with a given linter_models.Linter

Parameters:
  • assignment_id (int) – The id of the assignment
  • name (str) – Name of the linter
Return type:

AssignmentLinter

Returns:

The created AssignmentLinter

linters_crashed

The amount of linters that have crashed.

Return type:int
linters_done

The amount of linters that are done.

Return type:int
linters_running

The amount of linters that are running.

Return type:int
class psef.models.assignment.AssignmentResult(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

The class creates the link between an user_models.User and an Assignment in the database and the external users LIS sourcedid.

Variables:
  • sourcedid – The sourcedid for this user for this assignment.
  • user_id – The id of the user this belongs to.
  • assignment_id – The id of the assignment this belongs to.

psef.models.comment

This module defines a Comment.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.comment.Comment(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes a comment placed in a File by a user_models.User with the ability to grade.

A comment is always linked to a specific line in a file.

psef.models.course

This module defines a Course.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.course.Course(name=None, lti_course_id=None, lti_provider=None, virtual=False)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class describes a course.

A course can hold a collection of Assignment objects.

Parameters:
  • name – The name of the course
  • lti_course_id – The id of the course in LTI
  • lti_provider – The LTI provider
classmethod create_virtual_course(tree: psef.extract_tree.ExtractFileTree) → psef.models.course.Course[source]

Create a virtual course.

The course will contain a single assignment. The tree should be a single directory with multiple directories under it. For each directory a user will be created and a submission will be created using the files of this directory.

Parameters:tree (ExtractFileTree) – The tree to use to create the submissions.
Return type:Course
Returns:A virtual course with a random name.
get_all_users_in_course(self) → psef.models.model_types._MyQuery[typing.Tuple[psef.models.user.User, psef.models.role.CourseRole]][Tuple[psef.models.user.User, psef.models.role.CourseRole]][source]
Get a query that returns all users in the current course and their
role.
Return type:_MyQuery[tuple[User, CourseRole]]
Returns:A query that contains all users in the current course and their role.
get_all_visible_assignments(self) → Sequence[psef.models.assignment.Assignment][source]

Get all visible assignments for the current user for this course.

Return type:Sequence[Assignment]
Returns:A list of assignments the currently logged in user may see.
class psef.models.course.CourseSnippet(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes a mapping from a keyword to a replacement text that is shared amongst the teachers and TAs of the course.

psef.models.file

This module defines a File.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.file.File(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This object describes a file or directory that stored is stored on the server.

Files are always connected to work_models.Work objects. A directory file does not physically exist but is stored only in the database to preserve the submitted work structure. Each submission should have a single top level file. Each other file in a submission should be directly or indirectly connected to this file via the parent attribute.

delete_from_disk(self) → None[source]

Delete the file from disk if it is not a directory.

Return type:None
Returns:Nothing.
get_diskname(self) → str[source]

Get the absolute path on the disk for this file.

Return type:str
Returns:The absolute path.
static get_exclude_owner(owner: Optional[str], course_id: int) → psef.models.file.FileOwner[source]

Get the FileOwner the current user does not want to see files for.

The result will be decided like this, if the given str is not student, teacher or auto the result will be FileOwner.teacher. If the str is student, the result will be FileOwner.teacher, vica versa for teacher as input. If the input is auto student will be returned if the currently logged in user is a teacher, otherwise it will be student.

Parameters:
  • owner (Optional[str]) – The owner that was given in the GET paramater.
  • course_id (int) – The course for which the files are requested.
Return type:

FileOwner

Returns:

The object determined as described above.

list_contents(self, exclude: psef.models.file.FileOwner) → importlib._bootstrap.FileTree[source]

List the basic file info and the info of its children.

If the file is a directory it will return a tree like this:

{
    'name': 'dir_1',
    'id': 1,
    'entries': [
        {
            'name': 'file_1',
            'id': 2
        },
        {
            'name': 'file_2',
            'id': 3
        },
        {
            'name': 'dir_2',
            'id': 4,
            'entries': []
        }
    ]
}

Otherwise it will formatted like one of the file children of the above tree.

Parameters:exclude (FileOwner) – The file owner to exclude from the tree.
Return type:FileTree
Returns:A tree as described above.
rename_code(self, new_name: str, new_parent: psef.models.file.File, exclude_owner: psef.models.file.FileOwner) → None[source]

Rename the this file to the given new name.

Parameters:
  • new_name (str) – The new name to be given to the given file.
  • new_parent (File) – The new parent of this file.
  • exclude_owner (FileOwner) – The owner to exclude while searching for collisions.
Return type:

None

Returns:

Nothing.

Raises:

APIException – If renaming would result in a naming collision (INVALID_STATE).

class psef.models.file.FileOwner[source]

Bases: enum.IntEnum

Describes to which version of a submission (student’s submission or teacher’s revision) a file belongs. When a student adds or changes a file after the deadline for the assignment has passed, the original file’s owner is set teacher and the new file’s to student.

Parameters:
  • student – The file is in the student’s submission, but changed in the teacher’s revision.
  • teacher – The inverse of student. The file is added or changed in the teacher’s revision.
  • both – The file is not changed in the teacher’s revision and belongs to both versions.

psef.models.group

This module defines all models needed for groups.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.group.Group(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class represents a single group.

A group is a collection of real (non virtual) users that itself is connected to a virtual user. A group is connected to a GroupSet.

Variables:
  • name – The name of the group. This has to be unique in a group set.
  • virtual_user – The virtual user this group is connected to. This user is used to submissions as the group.
  • group_set – The GroupSet this group is connected to.
classmethod contains_users(users: Sequence[user_models.User], *, include_empty: bool = False) → psef.models.model_types._MyQuery[psef.models.group.Group][psef.models.group.Group][source]

Get a query that for all groups that include the given users.

Parameters:
  • users (Sequence[User]) – Filter groups so that at least one of their members is one of these users.
  • include_empty (bool) – Also include empty groups, so without users, in the query.
Return type:

_MyQuery[Group]

Returns:

The query as described above.

classmethod create_group(group_set: psef.models.group.GroupSet, members: MutableSequence[user_models.User], name: Optional[str] = None) → psef.models.group.Group[source]

Create a group with the given members.

Warning

This function does check if the given members are not yet in a group. It however doesn’t check if the current user has the permission to add these users to the group, nor if the members are enrolled in the course.

Parameters:
  • group_set (GroupSet) – In which group set should this group be placed.
  • members (MutableSequence[User]) – The initial members should this group have.
  • name (Optional[str]) – The name of the group. If not given a random name is generated.
Return type:

Group

Returns:

The newly created group.

get_member_lti_states(self, assignment: psef.models.assignment.Assignment) → Mapping[int, bool][source]

Get the lti state of all members in the group.

Check if it is possible to passback the grade for all members in this group. This is done by checking if there is a row for each user, assignment combination in the .assignment_models.AssignmentResult table.

Parameters:assignment (Assignment) – The assignment to get the states for.
Return type:Mapping[int, bool]
Returns:A mapping between user id and if we can passback the grade for this user for every member in this group.
has_a_submission

Is a submission handed in by this group.

Note

This property is cached after the first access within a request.

Return type:bool
class psef.models.group.GroupSet(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class represents a group set.

A group set is a single wrapper over all groups. Every group is part of a group set. The group set itself is connected to a single course and zero or more assignments in this course.

Variables:
  • minimum_size – The minimum size of that group should have before it can be used to submit a submission.
  • maximum_size – The maximum amount of members a group can ever have.
get_valid_group_for_user(self, user: psef.models.user.User) → Optional[psef.models.group.Group][source]

Get the group for the given user.

Parameters:user (User) – The user to get the group for.
Return type:Optional[Group]
Returns:A group if a valid group was found, or None if no group is needed to submit to an assignment connected to this group set.
Raises:APIException – If the amount of members of the found group is less than the minimum size of this group set.
largest_group_size

Get the size of the largest group in this group set.

This group doesn’t have to have submission.

Note

This property is cached within a request, and won’t update after adding groups.

Return type:int
Returns:The size of the largest group in the group set. The value -1 is returned if there are no groups in this group set.
smallest_group_size

Get the size of the smallest group in this group set.

Note

Only groups with a submission are counted.

Note

This property is cached within a request, and won’t update after adding groups.

Return type:int
Returns:The size of the smallest group with a submission in the group set. The value sys.maxsize is returned if there are no groups with a submission in this group set.

psef.models.linter

This module defines a LinterState.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.linter.LinterComment(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes a comment created by a LinterInstance.

Like a Comment it is attached to a specific line in a File.

class psef.models.linter.LinterInstance(work, tester)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes the connection between a assignment.AssignmentLinter and a work_models.Work.

add_comments(self, feedbacks: Mapping[int, Mapping[int, Sequence[Tuple[str, str]]]]) → Iterable[psef.models.linter.LinterComment][source]

Add comments written by this instance.

Parameters:feedbacks (Mapping[int, Mapping[int, Sequence[tuple[str, str]]]]) – The feedback to add, it should be in form as described below.
Return type:Iterable[LinterComment]
Returns:A iterable with comments that have not been added or commited to the database yet.
{
    file_id: {
        line_number: [(linter_code, msg), ...]
    }
}
error_summary

The summary of the error the linter encountered.

Return type:str
Returns:A summary of the error the linter encountered. This will probably be empty when the state is not crashed.
class psef.models.linter.LinterState[source]

Bases: enum.IntEnum

Describes in what state a LinterInstance is.

Parameters:
  • running – The linter is currently running.
  • done – The linter has finished without crashing.
  • crashed – The linter has crashed in some way.

psef.models.lti_provider

This module defines all LTI models.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.lti_provider.LTIProvider(key)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class defines the handshake with an LTI

Variables:key – The OAuth consumer key for this LTI provider.
lms_name

The name of the LMS for this LTIProvider.

Getter:Get the LMS name.
Setter:Impossible as this is fixed during startup of CodeGrade.
Return type:str
lti_class

The name of the LTI class to be used for this LTIProvider.

Getter:Get the LTI class name.
Setter:Impossible as this is fixed during startup of CodeGrade.
Return type:type[LTI]
passback_grade(self, sub: psef.models.work.Work, initial: bool) → None[source]

Passback the grade for a given submission to this lti provider.

Parameters:
  • sub (Work) – The submission to passback.
  • initial (bool) – If true no grade will be send, this is to make sure the created_at date is correct in the LMS. Not all providers actually do a passback when this is set to True.
Return type:

None

Returns:

Nothing.

secret

The OAuth consumer secret for this LTIProvider.

Getter:Get the OAuth secret.
Setter:Impossible as all secrets are fixed during startup of codegra.de
Return type:str

psef.models.permission

This module defines a Permission.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.permission.Permission(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model, typing.Generic

This class defines database permissions.

A permission can be a global- or a course- permission. Global permissions describe the ability to do something general, e.g. create a course or the usage of snippets. These permissions are connected to a Role which is hold be a User. Similarly course permissions are bound to a CourseRole. These roles are assigned to users only in the context of a single Course. Thus a user can hold different permissions in different courses.

Warning

Think twice about using this class directly! You probably want a non database permission (see permissions.py) which are type checked and WAY faster. If you need to check if a user has a certain permission use the User.has_permission() of, even better, psef.auth.ensure_permission() functions.

Variables:
  • default_value – The default value for this permission.
  • course_permission – Indicates if this permission is for course specific actions. If this is the case a user can have this permission for a subset of all the courses. If course_permission is False this permission is global for the entire site.
classmethod get_all_permissions(perm_type: Type[_T]) → Sequence[psef.models.permission.Permission[~_T][_T]][source]

Get all database permissions of a certain type.

Parameters:perm_type (type[_T]) – The type of permission to get.
Return type:Sequence[Permission[_T]]
Returns:A list of all database permissions of the given type.
classmethod get_all_permissions_from_list(perms: Sequence[_T]) → Sequence[psef.models.permission.Permission[~_T][_T]][source]

Get database permissions corresponding to a list of permissions.

Parameters:perms (Sequence[_T]) – The permissions to get the database permission of.
Return type:Sequence[Permission[_T]]
Returns:A list of all requested database permission.
classmethod get_name_column() → psef.models.model_types.DbColumn[str][str][source]

Get the name column in the database for the permissions.

Return type:DbColumn[str]
Returns:The name column of permissions.
classmethod get_permission(perm: _T) → psef.models.permission.Permission[~_T][_T][source]

Get a database permission from a permission.

Parameters:perm (_T) – The permission to get the database permission of.
Return type:Permission[_T]
Returns:The correct database permission.
value

Get the permission value of the database permission.

Returns:The permission of this database permission.

psef.models.plagiarism

This module defines a PlagiarismCase.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.plagiarism.PlagiarismCase(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describe a case of possible plagiarism.

Variables:
  • work1_id – The id of the first work to be associated with this possible case of plagiarism.
  • work2_id – The id of the second work to be associated with this possible case of plagiarism.
  • created_at – When was this case created.
  • plagiarism_run_id – The PlagiarismRun in which this case was discovered.
  • match_avg – The average similarity between the two matches. What the value exactly means differs per provider.
  • match_max – The maximum similarity between the two matches. What the value exactly means differs per provider.
class psef.models.plagiarism.PlagiarismMatch(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes a possible plagiarism match between two files.

Variables:
  • file1_id – The id of the first file associated with this match.
  • file1_start – The start position of the first file associated with this match. This position can be (and probably is) a line but it could also be a byte offset.
  • file1_end – The end position of the first file associated with this match. This position can be (and probably is) a line but it could also be a byte offset.
  • file2_id – Same as file1_id but of the second file.
  • file2_start – Same as file1_start but for the second file.
  • file2_end – Same as file1_end but for the second file.
class psef.models.plagiarism.PlagiarismRun(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes a run for a plagiarism provider.

Variables:
  • state – The state this run is in.
  • log – The log on stdout and stderr we got from running the plagiarism provider. This is only available if the state is done or crashed.
  • json_config – The config used for this run saved in a sorted association list.
  • assignment_id – The id of the assignment this belongs to.
plagiarism_cls

Get the class of the plagiarism provider of this run.

Return type:type[PlagiarismProvider]
Returns:The class of this plagiarism provider run.
provider_name

The provider name of this plagiarism run.

Type:returns
Return type:str
class psef.models.plagiarism.PlagiarismState[source]

Bases: enum.IntEnum

Describes in what state a PlagiarismRun is.

Parameters:
  • running – The provider is currently running.
  • done – The provider has finished without crashing.
  • crashed – The provider has crashed in some way.

psef.models.role

This module defines all roles.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.role.AbstractRole(self, name: str, _permissions: MutableMapping[_T, psef.models.permission.Permission[ForwardRef('_T')][_T]] = None) → None[source]

Bases: typing.Generic

An abstract class that implements all functionality a role should have.

get_all_permissions(self) → Mapping[_T, bool][source]

Get all course Permissions for this course role.

Return type:Mapping[_T, bool]
Returns:A name boolean mapping where the name is the name of the permission and the value indicates if this user has this permission.
has_permission(self, permission: _T) → bool[source]

Check whether this course role has the specified Permission.

Parameters:permission (_T) – The permission or permission name
Return type:bool
Returns:True if the course role has the permission
id

The id of this role.

Return type:int
name

The name of this role.

Return type:str
set_permission(self, perm: _T, should_have: bool) → None[source]

Set the given Permission to the given value.

Parameters:
  • should_have (bool) – If this role should have this permission
  • perm (_T) – The permission this role should (not) have
Return type:

None

uses_course_permissions

Does this role use course permissions or global permissions.

Return type:bool
class psef.models.role.CourseRole(name, course, _permissions=None)[source]

Bases: psef.models.role.AbstractRole, sqlalchemy.ext.declarative.api.Model

A course role is used to describe the abilities of a User in a course_models.Course.

Variables:
  • name – The name of this role in the course.
  • course_id – The course_models.Course this role belongs to.
static get_default_course_roles() → Mapping[str, MutableMapping[psef.permissions.CoursePermission, psef.models.permission.Permission[psef.permissions.CoursePermission][psef.permissions.CoursePermission]]][source]

Get all default course roles as specified in the config and their permissions (Permission).

{
    'student': {
        'can_edit_assignment_info': <Permission-object>,
        'can_submit_own_work': <Permission-object>
    }
}
Return type:Mapping[str, MutableMapping[CoursePermission, Permission[CoursePermission]]]
Returns:A name dict mapping where the name is the name of the course-role and the dict is name permission mapping between the name of a permission and the permission object. See above for an example.
classmethod get_initial_course_role(course: psef.models.course.Course) → psef.models.role.CourseRole[source]

Get the initial course role for a given course.

Parameters:course (Course) – The course to get the initial role for.
Return type:CourseRole
Returns:A course role that should be the role for the user creating the course.
uses_course_permissions

Does this role use course permissions or global permissions.

Return type:bool
class psef.models.role.Role(name, _permissions=None)[source]

Bases: psef.models.role.AbstractRole, sqlalchemy.ext.declarative.api.Model

A role defines the set of global permissions Permission of a User.

Variables:name – The name of the global role.
uses_course_permissions

Does this role use course permissions or global permissions.

Return type:bool

psef.models.rubric

This module defines a RubricRow.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.rubric.RubricItem(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class holds the information about a single option/item in a RubricRow.

class JSONBaseSerialization[source]

Bases: dict

The base serialization of a rubric item.

class JSONSerialization[source]

Bases: dict

class psef.models.rubric.RubricRow(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes a row of some rubric.

This class forms the link between Assignment and RubricItem and holds information about the row.

Variables:assignment_id – The assignment id of the assignment that belows to this rubric row.
classmethod create_from_json(header: str, description: str, items: List[psef.models.rubric.RubricItem.JSONBaseSerialization]) → psef.models.rubric.RubricRow[source]

Create a new rubric row for an assignment.

Parameters:
Return type:

RubricRow

Returns:

The newly created row.

is_valid

Check if the current row is valid.

Return type:bool
Returns:False if the row has no items or if the max points of the items is not > 0.
update_from_json(self, header: str, description: str, items: List[psef.models.rubric.RubricItem.JSONBaseSerialization]) → None[source]

Update this rubric in place.

Warning

All items not present in the given items list will be deleted from the rubric row.

Parameters:
Return type:

None

Returns:

Nothing.

update_items_from_json(self, items: List[psef.models.rubric.RubricItem.JSONBaseSerialization]) → None[source]

Update the items of this row in place.

Warning

All items not present in the given items list will be deleted from the rubric row.

Parameters:items (list[JSONBaseSerialization]) – The items (RubricItem) that should be added or updated. If id is in an item it should be an int and the rubric item with the corresponding id will be updated instead of added.
Return type:None
Returns:Nothing.

psef.models.snippet

This module defines a Snippet.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.snippet.Snippet(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

Describes a User specified mapping from a keyword to some string.

classmethod get_all_snippets(user: psef.models.user.User) → Sequence[psef.models.snippet.Snippet][source]

Return all snippets of the given User.

Parameters:user (User) – The user to get the snippets for.
Return type:Sequence[Snippet]
Returns:List of all snippets of the user.

psef.models.user

This module defines a User.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.user.User(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This class describes a user of the system.

Variables:
  • lti_user_id – The id of this user in a LTI consumer.
  • name – The name of this user.
  • role_id – The id of the role this user has.
  • courses – A mapping between course_id and course-role for all courses this user is currently enrolled.
  • email – The e-mail of this user.
  • virtual – Is this user an actual user of the site, or is it a virtual user.
  • password – The password of this user, it is automatically hashed.
  • assignment_results – The way this user can do LTI grade passback.
  • assignments_assigned – A mapping between assignment_ids and AssignmentAssignedGrader objects.
  • reset_email_on_lti – Determines if the email should be reset on the next LTI launch.
can_see_hidden

Can the user see hidden assignments.

Return type:bool
classmethod create_virtual_user(name: str) → psef.models.user.User[source]

Create a virtual user with the given name.

Return type:User
Returns:A newly created virtual user with the given name prepended with ‘Virtual - ‘ and a random username.
get_all_permissions(self, course_id: Union[course.Course, int, None] = None) → Union[Mapping[psef.permissions.CoursePermission, bool], Mapping[psef.permissions.GlobalPermission, bool]][source]

Get all global permissions (Permission) of this user or all course permissions of the user in a specific course.Course.

Parameters:course_id (Union[Course, int, None]) – The course or course id
Return type:Union[Mapping[CoursePermission, bool], Mapping[GlobalPermission, bool]]
Returns:A name boolean mapping where the name is the name of the permission and the value indicates if this user has this permission.
get_all_permissions_in_courses(self) → Mapping[int, Mapping[psef.permissions.CoursePermission, bool]][source]

Get all permissions for all courses the current user is enrolled in

Return type:Mapping[int, Mapping[CoursePermission, bool]]
Returns:A mapping from course id to a mapping from CoursePermission to a boolean indicating if the current user has this permission.
get_permissions_in_courses(self, wanted_perms: Sequence[psef.permissions.CoursePermission]) → Mapping[int, Mapping[psef.permissions.CoursePermission, bool]][source]

Check for specific Permission`s in all courses (:class:.course.Course`) the user is enrolled in.

Please note that passing an empty perms object is supported. However the resulting mapping will be empty.

>>> User().get_permissions_in_courses([])
{}
Parameters:wanted_perms (Sequence[CoursePermission]) – The permissions names to check for.
Return type:Mapping[int, Mapping[CoursePermission, bool]]
Returns:A mapping where the first keys indicate the course id, the values at this are a mapping between the given permission names and a boolean indicating if the current user has this permission for the course with this course id.
get_reset_token(self) → str[source]

Get a token which a user can use to reset his password.

Return type:str
Returns:A token that can be used in User.reset_password() to reset the password of a user.
has_course_permission_once(self, perm: psef.permissions.CoursePermission) → bool[source]

Check whether this user has the specified course Permission in at least one enrolled course.Course.

Parameters:perm (CoursePermission) – The permission or permission name
Return type:bool
Returns:True if the user has the permission once
has_permission(self, permission: Union[psef.permissions.GlobalPermission, psef.permissions.CoursePermission], course_id: Union[course.Course, int, None] = None) → bool[source]

Check whether this user has the specified global or course Permission.

To check a course permission the course_id has to be set.

Parameters:
Return type:

bool

Returns:

Whether the role has the permission or not

Raises:

KeyError – If the permission parameter is a string and no permission with this name exists.

id

The id of the user

is_active

Is the current user an active user.

Todo

Remove this property

Return type:bool
Returns:If the user is active.
reset_password(self, token: str, new_password: str) → None[source]

Reset a users password by using a token.

Note

Don’t forget to commit the database.

Parameters:
Return type:

None

Returns:

Nothing.

Raises:

PermissionException – If something was wrong with the given token.

username

The username of the user

psef.models.work

This module defines all work related tables.

SPDX-License-Identifier: AGPL-3.0-only

class psef.models.work.GradeHistory(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This object is a item in a grade history of a Work.

Variables:
  • changed_at – When was this grade added.
  • is_rubric – Was this grade added as a result of a rubric.
  • passed_back – Was this grade passed back to the LMS through LTI.
  • work – What work does this grade belong to.
  • user – What user added this grade.
class psef.models.work.Work(**kwargs)[source]

Bases: sqlalchemy.ext.declarative.api.Model

This object describes a single work or submission of a user_models.User for an assignment_models.Assignment.

add_file_tree(self, tree: psef.extract_tree.ExtractFileTreeDirectory) → None[source]

Add the given tree to as only files to the current work.

Warning

All previous files will be unlinked from this assignment.

Parameters:tree (ExtractFileTreeDirectory) – The file tree as described by psef.files.rename_directory_structure()
Return type:None
Returns:Nothing
divide_new_work(self) → None[source]

Divide a freshly created work.

First we check if an old work of the same author exists, that case the same grader is assigned. Otherwise we take the grader that misses the most of work.

Return type:None
Returns:Nothing
get_all_feedback(self) → Tuple[Iterable[str], Iterable[str]][source]

Get all feedback for this work.

Return type:tuple[Iterable[str], Iterable[str]]
Returns:A tuple of two iterators both producing human readable representations of the given feedback. The first iterator produces the feedback given by a person and the second the feedback given by the linters.
get_file_children_mapping(self, exclude: psef.models.file.FileOwner) → Mapping[int, Sequence[psef.models.file.File]][source]

Get a mapping that maps a file id to all its children.

This implementation does a single query to the database and runs in O(n*log(n)), so it will be quite a bit quicker than using the children attribute on files if you are going to need all children or all files.

The list of children is sorted on filename.

Parameters:exclude (FileOwner) – The file owners to exclude
Return type:Mapping[int, Sequence[File]]
Returns:A mapping from file id to list of all its children for this submission.
grade

Get the actual current grade for this work.

This is done by not only checking the grade field but also checking if rubric could be found.

Return type:Optional[float]
Returns:The current grade for this work.
has_as_author(self, user: psef.models.user.User) → bool[source]

Check if the given user is (one of) the authors of this submission.

Parameters:user (User) – The user to check for.
Return type:bool
Returns:True if the user is the author of this submission or a member of the group that is the author of this submission.
static limit_to_user_submissions(query: psef.models.model_types._MyQuery[ForwardRef('Work')][Work], user: psef.models.user.User) → psef.models.model_types._MyQuery[psef.models.work.Work][psef.models.work.Work][source]
Limit the given query of submissions to only submission submitted by
the given user.

Note

This is not the same as filtering on the author field as this also checks for groups.

Parameters:
  • query (_MyQuery[Work]) – The query to limit.
  • user (User) – The user to filter for.
Return type:

_MyQuery[Work]

Returns:

The filtered query.

passback_grade(self, initial: bool = False) → None[source]

Initiates a passback of the grade to the LTI consumer via the LTIProvider.

Parameters:initial (bool) – Should we do a initial LTI grade passback with no result so that the real grade won’t show as too late.
Return type:None
Returns:Nothing
remove_selected_rubric_item(self, row_id: int) → None[source]

Deselect selected RubricItem on row.

Deselects the selected rubric item on the given row with _row_id_ (if there are any selected).

Parameters:row_id (int) – The id of the RubricRow from which to deselect rubric items
Return type:None
Returns:Nothing
run_linter(self) → None[source]

Run all linters for the assignment on this work.

All linters that have been used on the assignment will also run on this work.

If the linters feature is disabled this function will simply return and not do anything.

Return type:None
Returns:Nothing
search_file(self, pathname: str, exclude: psef.models.file.FileOwner) → psef.models.file.File[source]

Search for a file in the this directory with the given name.

Parameters:
  • pathname (str) – The path of the file to search for, this may contain leading and trailing slashes which do not have any meaning.
  • exclude (FileOwner) – The fileowner to exclude from search, like described in get_zip().
Return type:

File

Returns:

The found file.

search_file_filters(self, pathname: str, exclude: psef.models.file.FileOwner) → List[Any][source]

Get the filters needed to search for a file in the this directory with a given name.

Parameters:
  • pathname (str) – The path of the file to search for, this may contain leading and trailing slashes which do not have any meaning.
  • exclude (FileOwner) – The fileowner to exclude from search, like described in get_zip().
Return type:

list[Any]

Returns:

The criteria needed to find the file with the given pathname.

select_rubric_items(self, items: List[RubricItem], user: psef.models.user.User, override: bool = False) → None[source]

Selects the given RubricItem.

Note

This also passes back the grade to LTI if this is necessary.

Note

This also sets the actual grade field to None.

Warning

You should do all input sanitation before calling this function. Like checking for duplicate items and correct assignment.

Parameters:
  • item – The item to add.
  • user (User) – The user selecting the item.
Return type:

None

Returns:

Nothing

selected_rubric_points

The amount of points that are currently selected in the rubric for this work.

Return type:float
set_grade(self, new_grade: Optional[float], user: psef.models.user.User, never_passback: bool = False) → psef.models.work.GradeHistory[source]

Set the grade to the new grade.

Note

This also passes back the grade to LTI if this is necessary (see passback_grade()).

Parameters:
  • new_grade (Optional[float]) – The new grade to set
  • user (User) – The user setting the new grade.
  • never_passback (bool) – Never passback the new grade.
Return type:

GradeHistory

Returns:

Nothing

SPDX-License-Identifier: AGPL-3.0-only

psef.parsers

This module implements parsers that raise a APIException when they fail.

SPDX-License-Identifier: AGPL-3.0-only

psef.parsers.parse_datetime(to_parse: object, allow_none: bool = False) → Optional[datetime.datetime][source]

Parse a datetime string using dateutil.

Parameters:
  • to_parse (object) – The object to parse, if this is not a string the parsing will always fail.
  • allow_none (bool) – Allow None to be passed without raising a exception. if to_parse is None and this option is True the result will be None.
Return type:

Optional[datetime]

Returns:

The parsed datetime object.

Raises:

APIException – If the parsing fails for whatever reason.

psef.parsers.parse_email_list(to_parse: object, allow_none: bool = False) → Optional[List[Tuple[str, str]]][source]

Parse email list into a list of emails.

This list should be in the form of a address-list as specified in RFC2822.

Parameters:
  • to_parse (object) – The object to parse, it should be a str if you want it to succeed.
  • allow_none (bool) – If True we will not error if to_parse is None.
Return type:

Optional[list[tuple[str, str]]]

Returns:

A list of addresses or None if to_parse is None and allow_none is True.

Raises:

APIException – If the parsing fails in some way.

psef.parsers.parse_enum(to_parse: object, parse_into_enum: Type[T], allow_none: bool = False, option_name: Optional[str] = None) → Optional[T][source]

Parse the given string to the given parse_into_enum.

Parameters:
  • to_parse (object) – The object to parse. If this value is not a string or None the function will always return a type error.
  • parse_into_enum (type[T]) – The enum to parse to.
  • allow_none (bool) – Allow None to be passed and return None if this is the case. If this value is False and None is passed the function will raise a APIException.
  • option_name (Optional[str]) – The name of the option, only used in error display.
Return type:

Optional[T]

Returns:

A instance of the given enum.

Raises:

APIException – If the parsing fails in some way.

psef.parsers.try_parse_email_list(to_parse: object, allow_none: bool = False) → Optional[str][source]

Try parsing an email list.

This function is basically the same as parse_email_list() but this function returns to_parse stripped of unnecessary whitespaces if the parsing succeeded.

Parameters:
Return type:

Optional[str]

Returns:

Its input stripped of spaces if parsing succeeded.

psef.tasks

This module defines all celery tasks. It is very important that you DO NOT change the way parameters are used or the parameters provided in existing tasks as there may tasks left in the old queue. Instead create a new task and change the mapping of the variables at the bottom of this file.

SPDX-License-Identifier: AGPL-3.0-only

class psef.tasks.CeleryTask[source]

Bases: typing.Generic

__weakref__

list of weak references to the object (if defined)

class psef.tasks.MyCelery(self, *args, **kwargs) → None[source]

Bases: psef.tasks.Celery

A subclass of celery that makes sure tasks are always called with a flask app context

__init__(self, *args, **kwargs) → None[source]

Initialize self. See help(type(self)) for accurate signature.

Return type:None
psef.tasks.init_app(app: Any) → None[source]

Initialize tasks for the given flask app.

Param:The flask app to initialize for.
Return type:None
Returns:Nothing

psef.plagiarism

This module defines all classes and function needed for the plagiarism checking infrastructure.

The actual providers are located in the psef.plagiarism_providers module.

SPDX-License-Identifier: AGPL-3.0-only

class psef.plagiarism.Option(self, name: str, title: str, description: str, type: psef.plagiarism.OptionTypes, mandatory: bool, possible_options: Optional[Sequence[str]], placeholder: Optional[str] = None) → None[source]

Bases: object

Represents a single option for a plagiarism provider.

Parameters:
  • name (str) – The name of the option.
  • title (str) – The title of the option.
  • description (str) – The description of the options.
  • type (OptionTypes) – The type of this option.
  • mandatory (bool) – If this is True the plagiarism provider can only function when this option is supplied.
  • possible_options (Optional[Sequence[str]]) – Possible values for this option. This value has to be a non empty sequence if the type is multiselect or singleselect.
placeholder = None
class psef.plagiarism.OptionTypes[source]

Bases: enum.IntEnum

Describes the type of the option

Parameters:
  • multiselect – An option that should be one or more values from the possible_options list. It should be passed back as an array of values, not indices!.
  • singleselect – The same as multiselect, only it can be one value and it should be passed back as that value, not an array.
  • numbervalue – An option that should be a number.
  • strvalue – An option that should be a string.
multiselect = 1
numbervalue = 3
singleselect = 2
strvalue = 4
class psef.plagiarism.PlagiarismProvider[source]

Bases: object

The (abstract) base class every plagiarism provider should inherit from.

The main functionality each implementation should provide are options and mapping those options to a program call (the actual plagiarism checking should be done in an external program). This program is called from a celery task and should produce log on stdout and stderr, and a csv file that should follow the format expected by process_output_csv(). If the csv needs to be transformed this can be done using PlagiarismProvider.transform_csv() which is called just before the csv file is processed.

static get_options() → Sequence[psef.plagiarism.Option][source]

Get all possible options for this provider.

This function needs to be implemented by the provider.

Return type:Sequence[Option]
Returns:A list of possible options for this plagiarism provider.
get_program_call(self) → List[str][source]

Get a program call list that runs the checker.

This list is used as the first argument to subprocess.check_output(), with shell=False. It can contain two special entries:

1. '{ restored_dir }': This will be replaced with the location of the files that need to be checked.

2. '{ result_dir }': This will be replaced with the directory where the result csv needs to be placed.

This function needs to be implemented by the provider.

Return type:list[str]
Returns:A list as that can be used with subprocess.check_output().
static get_progress_from_line(prefix: str, line: str) → Optional[Tuple[int, int]][source]

Get the progress of the plagiarism provider.

This function is called for every line of output produced by the provider. This should return two values, the first value should be the amount of submissions processed and the second the total amount. These values first indicate that this many submissions have been parsed. When the first value is equal to the second value it means that all submissions have been parsed and that comparing has started.

You should return None if the given line doesn’t contain progress information.

Return type:Optional[tuple[int, int]]
Returns:Two values or None as described above.
matches_output

The path of the csv file that contains the matches in the correct format.

This function needs to be implemented by the provider.

Return type:str
Returns:The path of the result csv relative to the { result_dir }.
set_options(self, values: Union[Dict[str, Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], List[Union[str, int, float, bool, None, Dict[str, Any], List[Any]]], str, int, float, bool, None, Dict[str, Any], List[Any]]) → None[source]

Set the options for this plagiarism provider.

Parameters:values (Union[dict[str, Union[str, int, float, bool, None, dict[str, Any], list[Any]]], list[Union[str, int, float, bool, None, dict[str, Any], list[Any]]], str, int, float, bool, None, dict[str, Any], list[Any]]) – The options for this run, this can still include the provider and old_assignments key.
Return type:None
Returns:Nothing.
Raises:APIException – If the values are not the correct format, if mandatory options miss, if an option has an incorrect value or if given value was not recognized.
static supports_base_code() → bool[source]

Does this provider support base code.

Return type:bool
Returns:A boolean indicating if the provider supports base code.
static supports_progress() → bool[source]

Does this provider support showing progress to the user.

Return type:bool
Returns:True if this provider supports progress detection.
static transform_csv(csvfile: str) → str[source]
Transform the csv file outputed by the plagiarism checker to
something that is readable by process_output_csv().
Parameters:csvfile (str) – The location of the old csv file.
Return type:str
Returns:The location of the csv file that can be processed by process_output_csv().
psef.plagiarism.init_app(app: Any) → None[source]

Initialize providers for the given flask app.

Param:The flask app to initialize for.
Return type:None
Returns:Nothing
psef.plagiarism.process_output_csv(lookup_map: Dict[str, int], old_submissions: Container[int], file_tree_lookup: Dict[int, importlib._bootstrap.FileTree], csvfile: str, delimiter: str = ';') → List[psef.models.plagiarism.PlagiarismCase][source]

Process the outputted csv file into plagiarism cases with matches.

Each line of the csvfile should have the following items separated by delimiter:

  1. The top level directory of the first submission;
  2. The top level directory of the second submission;
  3. The amount that submission 1 matched with submission 2;
  4. The amount that submission 2 matched with submission 1;
  5. The relative path of first file that matched;
  6. The start position were the match for the first file begins;
  7. The end position were the match for the first file ends;
  8. The relative path of second file that matched;
  9. The start position were the match for the second file begins;
  10. The end position were the match for the second file ends;

Fields 5-10 can occur any number of times, but have to occur at least once.

Parameters:
  • lookup_map (dict[str, int]) – A dictionary that should map the name of each toplevel directory to a submission id.
  • old_submissions (Container[int]) – Some sort of set that contains the ids of all submissions that are old. If a match between two of those submissions is found this match in not inserted into the database.
  • file_tree_lookup (dict[int, FileTree]) – A lookup that maps submission ids to their file trees.
  • csvfile (str) – The location of the csv file that follow the above format.
  • delimiter (str) – The delimiter used for this csv file.
Return type:

list[PlagiarismCase]

psef.permissions

This file contains all permissions used by CodeGrade in a python enum.

Warning

Do not edit this enum as it is automatically generated by “generate_permissions_py.py”.

SPDX-License-Identifier: AGPL-3.0-only

class psef.permissions.BasePermission[source]

Bases: enum.Enum

The base of a permission.

Do not use this class to get permissions, as it has none!

create_map(mapping: Any) → Any = <function BasePermission.create_map>[source]
get_by_name(name: str) → __T = <bound method BasePermission.get_by_name of <enum 'BasePermission'>>[source]
class psef.permissions.CoursePermission[source]

Bases: psef.permissions.BasePermission

The course permissions used by CodeGrade.

Warning

Do not edit this enum as it is automatically generated by “generate_permissions_py.py”.

Variables:
  • can_submit_others_work – Users with this permission can submit work to an assignment for other users. This means they can submit work that will have another user as the author.
  • can_submit_own_work – Users with this permission can submit their work to assignments of this course. Usually only students have this permission.
  • can_edit_others_work – Users with this permission can edit files in the submissions of this course. Usually TAs and teachers have this permission, so they can change files in the CodeGra.de filesystem if code doesn’t compile, for example.
  • can_grade_work – Users with this permission can grade submissions.
  • can_see_grade_before_open – Users with this permission can see the grade for a submission before an assignment is set to “done”.
  • can_see_others_work – Users with this permission can see submissions of other users of this course.
  • can_see_assignments – Users with this permission can view the assignments of this course.
  • can_see_hidden_assignments – Users with this permission can view assignments of this course that are set to “hidden”.
  • can_use_linter – Users with this permission can run linters on all submissions of an assignment.
  • can_edit_assignment_info – Users with this permission can update the assignment info such as name, deadline and status.
  • can_assign_graders – Users with this permission can assign a grader to submissions of assignments.
  • can_edit_cgignore – Users with this permission can edit the .cgignore file for an assignment.
  • can_upload_bb_zip – Users with this permission can upload a zip file with submissions in the BlackBoard format.
  • can_edit_course_roles – Users with this permission can assign or remove permissions from course roles and add new course roles.
  • can_edit_course_users – Users with this permission can add users to this course and assign roles to those users.
  • can_create_assignment – Users with this permission can create new assignments for this course.
  • can_upload_after_deadline – Users with this permission can submit their work after the deadline of an assignment.
  • can_see_assignee – Users with this permission can see who is assigned to assess a submission.
  • manage_rubrics – Users with this permission can update the rubrics for the assignments of this course.
  • can_view_own_teacher_files – Users with this permission can view the teacher’s revision of their submitted work.
  • can_see_grade_history – Users with this permission can see the grade history of an assignment.
  • can_delete_submission – Users with this permission can delete submissions.
  • can_update_grader_status – Users with this permission can change the status of graders for this course, whether they are done grading their assigned submissions or not.
  • can_update_course_notifications – Users with this permission can change the all notifications that are configured for this course. This includes when to send them and who to send them to.
  • can_edit_maximum_grade – Users with this permission can edit the maximum grade possible, and therefore also determine if getting a ‘bonus’ for an assignment is also possible.
  • can_view_plagiarism – Users with this permission can view the summary of a plagiarism check and see details of a plagiarism case. To view a plagiarism case between this and another course, the user must also have either this permission, or both “See assignments” and “See other’s work” in the other course.
  • can_manage_plagiarism – Users with this permission can add and delete plagiarism runs.
  • can_list_course_users – Users with this permission can see all users of this course including the name of their role.
  • can_edit_own_groups – Users with this permission can edit groups they are in. This means they can join groups, add users to groups they are in and change the name of groups they are in. They cannot remove users from groups they are in, except for themselves.
  • can_edit_others_groups – Users with this permission can edit groups they are not in, they can add users, remove users and rename all groups. Users with this permission can also edit groups they are in.
  • can_edit_groups_after_submission – Users with this permission can edit groups which handed in a submission. Users with this permission cannot automatically edit groups, they also need either “Edit own groups” or “Edit others groups”.
  • can_view_others_groups – Users with this permission can view groups they are not in, and the members of these groups.
  • can_edit_group_assignment – Users with this permission can change an assignment into a group assignment, and change the minimum and maximum required group size.
  • can_edit_group_set – Users with this permissions can create, delete and edit group sets.
  • can_create_groups – Users with this permission can create new groups in group assignments.
  • can_view_course_snippets – Users with this permission can see the snippets of this course, and use them while writing feedback.
  • can_manage_course_snippets – Users with this permission can create, edit, and delete snippets for this course.
can_assign_graders = _PermissionValue(item=10, default_value=False)
can_create_assignment = _PermissionValue(item=15, default_value=False)
can_create_groups = _PermissionValue(item=34, default_value=True)
can_delete_submission = _PermissionValue(item=21, default_value=False)
can_edit_assignment_info = _PermissionValue(item=9, default_value=False)
can_edit_cgignore = _PermissionValue(item=11, default_value=False)
can_edit_course_roles = _PermissionValue(item=13, default_value=False)
can_edit_course_users = _PermissionValue(item=14, default_value=False)
can_edit_group_assignment = _PermissionValue(item=32, default_value=False)
can_edit_group_set = _PermissionValue(item=33, default_value=False)
can_edit_groups_after_submission = _PermissionValue(item=30, default_value=False)
can_edit_maximum_grade = _PermissionValue(item=24, default_value=False)
can_edit_others_groups = _PermissionValue(item=29, default_value=False)
can_edit_others_work = _PermissionValue(item=2, default_value=False)
can_edit_own_groups = _PermissionValue(item=28, default_value=True)
can_grade_work = _PermissionValue(item=3, default_value=False)
can_list_course_users = _PermissionValue(item=27, default_value=True)
can_manage_course_snippets = _PermissionValue(item=36, default_value=False)
can_manage_plagiarism = _PermissionValue(item=26, default_value=False)
can_see_assignee = _PermissionValue(item=17, default_value=False)
can_see_assignments = _PermissionValue(item=6, default_value=True)
can_see_grade_before_open = _PermissionValue(item=4, default_value=False)
can_see_grade_history = _PermissionValue(item=20, default_value=False)
can_see_hidden_assignments = _PermissionValue(item=7, default_value=False)
can_see_others_work = _PermissionValue(item=5, default_value=False)
can_submit_others_work = _PermissionValue(item=0, default_value=False)
can_submit_own_work = _PermissionValue(item=1, default_value=True)
can_update_course_notifications = _PermissionValue(item=23, default_value=False)
can_update_grader_status = _PermissionValue(item=22, default_value=False)
can_upload_after_deadline = _PermissionValue(item=16, default_value=False)
can_upload_bb_zip = _PermissionValue(item=12, default_value=False)
can_use_linter = _PermissionValue(item=8, default_value=False)
can_view_course_snippets = _PermissionValue(item=35, default_value=False)
can_view_others_groups = _PermissionValue(item=31, default_value=True)
can_view_own_teacher_files = _PermissionValue(item=19, default_value=True)
can_view_plagiarism = _PermissionValue(item=25, default_value=False)
manage_rubrics = _PermissionValue(item=18, default_value=False)
class psef.permissions.GlobalPermission[source]

Bases: psef.permissions.BasePermission

The global permissions used by CodeGrade.

Warning

Do not edit this enum as it is automatically generated by “generate_permissions_py.py”.

Variables:
  • can_add_users – Users with this permission can add other users to the website.
  • can_use_snippets – Users with this permission can use the snippets feature on the website.
  • can_edit_own_info – Users with this permission can edit their own personal information.
  • can_edit_own_password – Users with this permission can edit their own password.
  • can_create_courses – Users with this permission can create new courses.
  • can_manage_site_users – Users with this permission can change the global permissions for other users on the site.
  • can_search_users – Users with this permission can search for users on the side, this means they can see all other users on the site.
can_add_users = _PermissionValue(item=0, default_value=False)
can_create_courses = _PermissionValue(item=4, default_value=False)
can_edit_own_info = _PermissionValue(item=2, default_value=True)
can_edit_own_password = _PermissionValue(item=3, default_value=True)
can_manage_site_users = _PermissionValue(item=5, default_value=False)
can_search_users = _PermissionValue(item=6, default_value=True)
can_use_snippets = _PermissionValue(item=1, default_value=True)
psef.permissions.database_permissions_sanity_check(app: Any) → None[source]

Check if database has all the correct permissions.

Return type:None
psef.permissions.init_app(app: Any, skip_perm_check: bool) → None[source]

Initialize flask app

Return type:None

psef.plagiarism_providers

Module contents

This module implements the provider for the JPlag plagiarism checker.

This provider only works with the codegrade fork of JPlag as other versions do not output the needed csv file.

SPDX-License-Identifier: AGPL-3.0-only

class psef.plagiarism_providers.jplag.JPlag(self) → None[source]

Bases: psef.plagiarism.PlagiarismProvider

This class implements the JPlag plagiarism provider.

static get_options() → Sequence[psef.plagiarism.Option][source]

Get all possible options for JPlag.

Return type:Sequence[Option]
Returns:The possible options.
get_program_call(self) → List[str][source]

Get the program call for JPlag.

Return type:list[str]
Returns:A list as that can be used with subprocess.check_output() to run JPlag.
static get_progress_from_line(prefix: str, line: str) → Optional[Tuple[int, int]][source]

Get the progress of the plagiarism provider.

This function is called for every line of output produced by the provider. This should return two values, the first value should be the amount of submissions processed and the second the total amount. These values first indicate that this many submissions have been parsed. When the first value is equal to the second value it means that all submissions have been parsed and that comparing has started.

You should return None if the given line doesn’t contain progress information.

Return type:Optional[tuple[int, int]]
Returns:Two values or None as described above.
matches_output

The path were the result csv is placed.

Return type:str
Returns:The specified path
static supports_progress() → bool[source]

Does this provider support showing progress to the user.

Return type:bool
Returns:True if this provider supports progress detection.