SRE.params

  1import os
  2import re
  3from datetime import datetime
  4from typing import Literal
  5
  6from pathlib import Path
  7
  8sre_uid = 1100
  9sre_gid = 1100
 10sre_user = "sre"
 11docker_gid = 988
 12
 13admin_uids = []
 14admin_gids = [2000]
 15
 16main_sre_dir = "/opt/sre"
 17
 18# Override main_sre_dir with the actual install root derived from this file's
 19# location, so a single build/checkout works from any install path. Without
 20# this, lib_dir stays at the build-time value and `from utils import ...` in
 21# srelab.py files fails when sre is installed somewhere other than /opt/sre.
 22# The marker file (src/sre.py) sits in a world-readable directory so this
 23# probe works whether we're imported by the privileged CLI or by sysreseval
 24# running as an unprivileged student (lab/ and lib/ are sre:sre-only).
 25_candidate = Path(__file__).resolve().parent.parent.parent
 26if (_candidate / "src" / "sre.py").is_file():
 27    main_sre_dir = str(_candidate)
 28del _candidate
 29sre_pub_dir = "/var/lib/sre"
 30sre_user_public_dir = "/home/sre"
 31
 32lab_dir = main_sre_dir + "/lab"
 33lib_dir = main_sre_dir + "/lib"
 34
 35
 36
 37
 38# debug_mode:
 39# - allow environment variables SRE_WRAPPER and SRE_PUB_DIR to overcome the default value (insecure)
 40# - allow "sysreseval --debug"
 41debug_mode = True #False
 42
 43# If True, sysreseval will use the last field in the gecos string and save it in the answers
 44# of the user as email (to be used in the pdfs generated with "sre outline")
 45email_in_gecos_last_field: bool = True
 46
 47max_docker_concurrency = 16
 48
 49display_marks_in_auto_evaluations_by_default = False
 50use_numerical_marks_by_default = True
 51default_maximum_mark = 20
 52
 53# Scope bitmask values for GradeElement.scope. A grade element is visible in
 54# self-eval (sre eval --auto-eval) if scope & SELF_EVAL_SCOPE, and in non-auto
 55# eval / outline / sheet if scope & EXO_EVAL_SCOPE.
 56SELF_EVAL_SCOPE = 1
 57EXO_EVAL_SCOPE = 2
 58BOTH_EVAL_SCOPE = SELF_EVAL_SCOPE | EXO_EVAL_SCOPE  # 3
 59grade_scopes = (SELF_EVAL_SCOPE, EXO_EVAL_SCOPE, BOTH_EVAL_SCOPE)
 60
 61default_eval_interval_during_exams = 60  # default duration between two evaluations in exam mode
 62default_eval_interval_without_exam_mode = 60  # default interval (seconds) for eval outside exam mode when the lab does not define eval_interval_without_exam_mode (0 for no periodic evals)
 63default_save_record_interval_during_exams = 60     # use 0 to disable saving
 64
 65default_exam_duration = 90  # default exam duration in minutes when "duration" is absent from exam.json
 66
 67
 68default_inactivity_threshold_in_watch_command = 90  # default inactivity (no eval) from a project before triggering an alert in watch
 69default_dashboard_refresh_interval_in_watch_command = 1
 70
 71max_duration_between_exam_pre_start_and_start = 60  # duration in seconds
 72
 73default_exec_shell = "/bin/bash"
 74
 75#################################################################################
 76#
 77# Security settings
 78#
 79#################################################################################
 80
 81allow_privileged_machines = True
 82disable_volume_mount_on_root_partition = False
 83
 84# For identifying the users, use the environment variable SUDO_USER instead of USER_USERNAME
 85# SUDO_USER is set by sudo itself, thus not spoofable (but in some configurations don't give the current USER variable)
 86# USER_USERNAME is set by sre-wrapper from USER (spoofable by the user)
 87use_sudo_user_for_username = False
 88
 89# Should projects be able to execute commands on the host (which might be useful)
 90# shell : execution through subprocess.run with shell=True which allow pipes
 91# split : execution through shlex.split to forbid pipes
 92# False : no execution permitted on the host
 93execute_commands_on_host: Literal["shell", "split", False] = "shell"
 94
 95
 96authorized_src_dir = [main_sre_dir + '/lab', '/home']
 97
 98
 99#################################################################################
100#
101# Don't modify parameters below
102#
103#################################################################################
104
105sre_version = "0.9"
106default_docker_image_version = "1.28"
107sre_docker_namespace = "sysreseval"
108
109def sre_docker_image(image_name: str = "base") -> str:
110    return f"{sre_docker_namespace}/{image_name}:{default_docker_image_version}"
111
112default_docker_image = sre_docker_image()
113
114sre_wrapper = main_sre_dir + "/bin/sre-wrapper"
115sysreseval_exe = main_sre_dir + "/bin/sysreseval"
116sre_exe = main_sre_dir + "/sbin/sre"
117
118if debug_mode:
119    sre_wrapper = os.environ.get("SRE_WRAPPER", sre_wrapper)
120    sre_pub_dir = os.environ.get("SRE_PUB_DIR", sre_pub_dir)
121
122sre_projects_dir = sre_pub_dir + "/projects"
123
124
125
126
127archive_dirs = [sre_pub_dir + '/archives']
128
129sre_name_env_variable = "SRE_LAB_NAME"
130sre_xauth_cookie_env_variable = "SRE_XAUTH_COOKIE"
131sre_host_ip_env_variable = "SRE_HOST_IP"
132
133sre_host_ip = "172.17.0.1"
134
135exetests_path = lib_dir + "/exetests.py"
136exetests_machines_path = '/usr/local/sbin/exetests.py'
137exetests_env_name = 'EXETESTS'
138exetests_separator = '@@@'
139default_timeout = 20
140
141exit_code_flavor_form_needed = 3
142exit_code_flavor_not_allowed = 4
143
144default_show_nat_network = True
145default_host_network_color = "deepskyblue"
146default_host_network_name = "Internet"
147default_host_network_shape = "hexagon"
148default_host_network_exploded = False
149default_host_network_edge_relative_length = 1.0
150
151default_machine_shape = "box"
152default_network_shape = "ellipse"
153
154pdf_schema_file = "schema.pdf"
155pdf_info_file = "informations.pdf"
156
157# External terminal emulator used to open machine connections.
158# The command is built as: terminal_cmd_prefix[:-1] + [terminal_title_opt, title] + terminal_cmd_prefix[-1:] + [sre_wrapper, "connect", project, machine]
159# xterm / xfce4-terminal use "-e" and "-title"; mate-terminal / gnome-terminal use "--" and "--title"
160# terminal_cmd_prefix = ["/usr/bin/xterm", "-e"]
161# terminal_title_opt = "-title"
162terminal_cmd_prefix = ["/usr/bin/mate-terminal", "--"]
163terminal_title_opt = "--title"
164
165# Terminal appearance defaults (overridable via ~/.config/sysreseval)
166terminal_font_size = 12  # font size in points
167terminal_color_scheme = "black_on_white"  # "white_on_black" or "black_on_white"
168content_font_size = 12  # font size in points for information/questions views
169system_font_size = 10  # font size in points for menus, titles, labels
170
171graphicdir = main_sre_dir + "/graphics"
172thats_all_folks_svg = graphicdir + "/Thats_all_folks.svg"
173sysreseval_logo_svg = graphicdir + "/sysreseval.svg"
174machine_icon_svg_file = graphicdir + "/machine.svg"
175machine_forbidden_icon_svg_file = graphicdir + "/machine-forbidden.svg"
176switch_icon_svg_file = graphicdir + "/switch.svg"
177
178exam_only_affix = ["_EXAM_", "_OLD_", "_DRAFT_", "_TESTS_"]
179
180exam_json_name = "exam.json"
181exam_json_keyword = "exam_json"
182exam_start_after = "start_after"
183exam_end_before = "end_before"
184exam_eval_interval = "eval_interval"
185exam_pre_start_date = "pre_start_date"
186exam_started_at = "started_at"
187exam_ended_at = "ended_at"
188exam_duration = "duration"
189exam_labs = "labs"
190exam_record_sessions = "record_sessions"
191
192
193def parse_lab_entry(entry) -> tuple[str, str | None]:
194    """Parse a labs entry: new [lab, flavor] list or legacy plain string.
195    Returns (lab_cli_arg, flavor_name_or_None)."""
196    if isinstance(entry, list):
197        return entry[0], entry[1]
198    return entry, None  # backward compat with old exam.json
199
200
201srelab_py_name = "srelab.py"
202titles_file_name = "titles.json"
203data_json_name = "data.json"
204private_dir_name = ".private"
205files_dir_name = "files"
206private_mount_dir_name = "mnt"
207user_public_dir_name = "user_public_dir"
208shared_dir_name = "shared"
209records_dir_name = "records"
210recorder_idle_time_limit = 10  # seconds; passed to asciinema --idle-time-limit to compress idle gaps in .cast files
211recorder_config_dir = sre_pub_dir + "/asciinema"  # ASCIINEMA_CONFIG_HOME — avoids asciinema falling back to $HOME/.config (which the sre user cannot write)
212use_asciinema_for_records = True  # True: prefer asciinema (fall back to script if missing). False: always record with script(1).
213eval_in_progress_name = "eval_in_progress"
214auto_eval_log_name = "auto_eval.log"
215debug_project_marker_name = "debug_project"
216auto_eval_count_keyword = "auto_eval_count"
217info_json_name = "info.json"
218
219answer_dir_name = "answers"
220answer_file_name = "answers.json"
221cheat_file_name = "cheat.json"
222
223hostname_keyword = "hostname"
224login_keyword = "login"
225fullname_keyword = "fullname"
226email_keyword = "email"
227language_keyword = "language"
228
229# Languages selectable in the GUI language priority dialog
230available_language_in_interface = ['en', 'fr']
231language_display_names = {'en': 'English', 'fr': 'Français'}
232
233running_lab_name_keyword = "running_lab_name"
234eval_date_keyword = "eval_date"
235re_eval_date_keyword = "re_eval_date"
236
237sysreseval_answers_updated_at = 'answers_updated_at'
238sysreseval_exam_time_remaining = 'exam_time_remaining'
239sysreseval_exam_started_at = 'exam_started_at'
240sysreseval_exam_duration = 'exam_duration'
241sysreseval_exam_mode = 'exam_mode'
242
243initial_state_name = "initial"
244
245self_grade_timestamp_dir = sre_pub_dir + "/last_self_grades"
246
247svg_graph_name = "scheme.svg"
248
249srelab_link_name = "srelab"
250
251graphviz_default_nodesep = 0.8
252graphviz_default_ranksep = 1.5
253graphviz_default_splines = "curved"
254graphviz_default_overlap = "prism"
255
256
257class SRE:
258    args = None
259    module_rvlab = None
260    eval_obj = None
261    username = None
262
263
264def datetime_to_string(dt) -> str:
265    return f"{dt:%Y%m%d%H%M%S}"
266
267
268def string_to_datetime(string) -> datetime:
269    return datetime.strptime(string, "%Y%m%d%H%M%S")
270
271
272def get_lab_name_from_cli_arg(lab_cli_arg: str, is_path: bool) -> str:
273    if is_path:
274        lab_cli_arg = os.path.abspath(lab_cli_arg)
275    return lab_cli_arg.replace("/", "@")
276
277
278def get_running_lab_name(lab_name: str, instance_start_date: datetime, username: str = None) -> str:
279    if username is None:
280        if SRE.username is None:
281            username = ""
282        else:
283            username = SRE.username
284    if exetests_separator in lab_name:
285        raise ValueError(f"lab_name must not contain '{exetests_separator}': {lab_name!r}")
286    if exetests_separator in username:
287        raise ValueError(f"username must not contain '{exetests_separator}': {username!r}")
288    return f"{datetime_to_string(instance_start_date)}@@@{lab_name}@@@{username}"
289
290
291running_lab_name_match_pattern = '^([0-9]+)@@@(.+)@@@(.+)$'
292
293
294def get_lab_name_from_running_lab_name(running_lab_name: str) -> str:
295    match = re.match(running_lab_name_match_pattern, running_lab_name)
296    if match:
297        return match.group(2)
298    else:
299        return 'ERROR-running_lab_name-ILLEGAL-FORMAT'
300
301
302def get_username_from_running_lab_name(running_lab_name: str) -> str:
303    match = re.match(running_lab_name_match_pattern, running_lab_name)
304    if match:
305        return match.group(3)
306    return ''
307
308
309def get_abbreviated_lab_name_from_running_lab_name(running_lab_name: str) -> str:
310    name = get_lab_name_from_running_lab_name(running_lab_name).replace("@", "/").rpartition("/")[2]
311    return name.removesuffix(".py")
312
313
314def get_current_srelab_file_from_running_lab_name(running_lab_name: str) -> str:
315    lab_name = get_lab_name_from_running_lab_name(running_lab_name)
316    pathstr = lab_name.replace("@", "/")
317    if not pathstr.startswith("/"):
318        abspath = (Path(lab_dir) / pathstr).resolve()
319    else:
320        abspath = Path(pathstr).resolve()
321    if abspath.name.endswith(".py"):
322        return str(abspath)
323    else:
324        return str(abspath / srelab_py_name)
325
326
327def get_srelab_dir(running_lab_name: str):
328    lab_name = get_lab_name_from_running_lab_name(running_lab_name)
329    pathstr = lab_name.replace("@", "/")
330    if not pathstr.startswith("/"):
331        abspath = (Path(lab_dir) / pathstr).resolve()
332    else:
333        abspath = Path(pathstr).resolve()
334    if abspath.name.endswith(".py"):
335        return None
336    else:
337        return str(abspath)
338
339
340def project_has_directory(running_lab_name=None) -> bool:
341    if running_lab_name.endswith(".py"):
342        return False
343    else:
344        return True
345
346
347def get_archive_name(running_lab_name: str, date: datetime) -> str:
348    return f"{datetime_to_string(date)}_{running_lab_name}.zst"
349
350
351def private_lab_dir(running_lab_name: str) -> str:
352    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}"
353
354
355def auto_eval_log_filename(running_lab_name: str) -> str:
356    return f"{private_lab_dir(running_lab_name)}/{auto_eval_log_name}"
357
358
359def debug_project_marker_filename(running_lab_name: str) -> str:
360    return f"{private_lab_dir(running_lab_name)}/{debug_project_marker_name}"
361
362
363def link_to_user_public_dir(running_lab_name: str) -> str:
364    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{user_public_dir_name}"
365
366
367def public_lab_dir(running_lab_name: str) -> str:
368    return f"{sre_projects_dir}/{running_lab_name}"
369
370
371def info_filename(running_lab_name: str) -> str:
372    return f"{sre_projects_dir}/{running_lab_name}/{info_json_name}"
373
374
375def data_filename(running_lab_name: str) -> str:
376    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{data_json_name}"
377
378
379def private_mount_dir(running_lab_name: str) -> str:
380    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{private_mount_dir_name}"
381
382
383def files_dir(running_lab_name: str) -> str:
384    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{files_dir_name}"
385
386
387def records_dir(running_lab_name: str) -> str:
388    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{records_dir_name}"
389
390
391def srelab_link_filename(running_lab_name: str) -> str:
392    return f"{private_lab_dir(running_lab_name)}/{srelab_link_name}"
393
394
395def graph_filename(running_lab_name: str) -> str:
396    return f"{sre_projects_dir}/{running_lab_name}/{svg_graph_name}"
397
398
399def answers_filename(running_lab_name: str) -> str:
400    return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}/{answer_file_name}"
401
402
403def cheat_filename(running_lab_name: str) -> str:
404    return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}/{cheat_file_name}"
405
406
407def answers_dir(running_lab_name: str) -> str:
408    return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}"
409
410
411def self_grade_timestamp_file(lab_name: str) -> str:
412    return f"{self_grade_timestamp_dir}/{lab_name}"
sre_uid = 1100
sre_gid = 1100
sre_user = 'sre'
docker_gid = 988
admin_uids = []
admin_gids = [2000]
main_sre_dir = $PWD
sre_pub_dir = '/var/lib/sre'
sre_user_public_dir = '/home/sre'
lab_dir = '/Users/em/Orsay/Info/sre/lab'
lib_dir = '/Users/em/Orsay/Info/sre/lib'
debug_mode = True
email_in_gecos_last_field: bool = True
max_docker_concurrency = 16
display_marks_in_auto_evaluations_by_default = False
use_numerical_marks_by_default = True
default_maximum_mark = 20
SELF_EVAL_SCOPE = 1
EXO_EVAL_SCOPE = 2
BOTH_EVAL_SCOPE = 3
grade_scopes = (1, 2, 3)
default_eval_interval_during_exams = 60
default_eval_interval_without_exam_mode = 60
default_save_record_interval_during_exams = 60
default_exam_duration = 90
default_inactivity_threshold_in_watch_command = 90
default_dashboard_refresh_interval_in_watch_command = 1
max_duration_between_exam_pre_start_and_start = 60
default_exec_shell = '/bin/bash'
allow_privileged_machines = True
disable_volume_mount_on_root_partition = False
use_sudo_user_for_username = False
execute_commands_on_host: Literal['shell', 'split', False] = 'shell'
authorized_src_dir = ['/Users/em/Orsay/Info/sre/lab', '/home']
sre_version = '0.9'
default_docker_image_version = '1.28'
sre_docker_namespace = 'sysreseval'
def sre_docker_image(image_name: str = 'base') -> str:
110def sre_docker_image(image_name: str = "base") -> str:
111    return f"{sre_docker_namespace}/{image_name}:{default_docker_image_version}"
default_docker_image = 'sysreseval/base:1.28'
sre_wrapper = '/Users/em/Orsay/Info/sre/bin/sre-wrapper'
sysreseval_exe = '/Users/em/Orsay/Info/sre/bin/sysreseval'
sre_exe = '/Users/em/Orsay/Info/sre/sbin/sre'
sre_projects_dir = '/var/lib/sre/projects'
archive_dirs = ['/var/lib/sre/archives']
sre_name_env_variable = 'SRE_LAB_NAME'
sre_host_ip_env_variable = 'SRE_HOST_IP'
sre_host_ip = '172.17.0.1'
exetests_path = '/Users/em/Orsay/Info/sre/lib/exetests.py'
exetests_machines_path = '/usr/local/sbin/exetests.py'
exetests_env_name = 'EXETESTS'
exetests_separator = '@@@'
default_timeout = 20
exit_code_flavor_form_needed = 3
exit_code_flavor_not_allowed = 4
default_show_nat_network = True
default_host_network_color = 'deepskyblue'
default_host_network_name = 'Internet'
default_host_network_shape = 'hexagon'
default_host_network_exploded = False
default_host_network_edge_relative_length = 1.0
default_machine_shape = 'box'
default_network_shape = 'ellipse'
pdf_schema_file = 'schema.pdf'
pdf_info_file = 'informations.pdf'
terminal_cmd_prefix = ['/usr/bin/mate-terminal', '--']
terminal_title_opt = '--title'
terminal_font_size = 12
terminal_color_scheme = 'black_on_white'
content_font_size = 12
system_font_size = 10
graphicdir = '/Users/em/Orsay/Info/sre/graphics'
thats_all_folks_svg = '/Users/em/Orsay/Info/sre/graphics/Thats_all_folks.svg'
sysreseval_logo_svg = '/Users/em/Orsay/Info/sre/graphics/sysreseval.svg'
machine_icon_svg_file = '/Users/em/Orsay/Info/sre/graphics/machine.svg'
machine_forbidden_icon_svg_file = '/Users/em/Orsay/Info/sre/graphics/machine-forbidden.svg'
switch_icon_svg_file = '/Users/em/Orsay/Info/sre/graphics/switch.svg'
exam_only_affix = ['_EXAM_', '_OLD_', '_DRAFT_', '_TESTS_']
exam_json_name = 'exam.json'
exam_json_keyword = 'exam_json'
exam_start_after = 'start_after'
exam_end_before = 'end_before'
exam_eval_interval = 'eval_interval'
exam_pre_start_date = 'pre_start_date'
exam_started_at = 'started_at'
exam_ended_at = 'ended_at'
exam_duration = 'duration'
exam_labs = 'labs'
exam_record_sessions = 'record_sessions'
def parse_lab_entry(entry) -> tuple[str, str | None]:
194def parse_lab_entry(entry) -> tuple[str, str | None]:
195    """Parse a labs entry: new [lab, flavor] list or legacy plain string.
196    Returns (lab_cli_arg, flavor_name_or_None)."""
197    if isinstance(entry, list):
198        return entry[0], entry[1]
199    return entry, None  # backward compat with old exam.json

Parse a labs entry: new [lab, flavor] list or legacy plain string. Returns (lab_cli_arg, flavor_name_or_None).

srelab_py_name = 'srelab.py'
titles_file_name = 'titles.json'
data_json_name = 'data.json'
private_dir_name = '.private'
files_dir_name = 'files'
private_mount_dir_name = 'mnt'
user_public_dir_name = 'user_public_dir'
shared_dir_name = 'shared'
records_dir_name = 'records'
recorder_idle_time_limit = 10
recorder_config_dir = '/var/lib/sre/asciinema'
use_asciinema_for_records = True
eval_in_progress_name = 'eval_in_progress'
auto_eval_log_name = 'auto_eval.log'
debug_project_marker_name = 'debug_project'
auto_eval_count_keyword = 'auto_eval_count'
info_json_name = 'info.json'
answer_dir_name = 'answers'
answer_file_name = 'answers.json'
cheat_file_name = 'cheat.json'
hostname_keyword = 'hostname'
login_keyword = 'login'
fullname_keyword = 'fullname'
email_keyword = 'email'
language_keyword = 'language'
available_language_in_interface = ['en', 'fr']
language_display_names = {'en': 'English', 'fr': 'Français'}
running_lab_name_keyword = 'running_lab_name'
eval_date_keyword = 'eval_date'
re_eval_date_keyword = 're_eval_date'
sysreseval_answers_updated_at = 'answers_updated_at'
sysreseval_exam_time_remaining = 'exam_time_remaining'
sysreseval_exam_started_at = 'exam_started_at'
sysreseval_exam_duration = 'exam_duration'
sysreseval_exam_mode = 'exam_mode'
initial_state_name = 'initial'
self_grade_timestamp_dir = '/var/lib/sre/last_self_grades'
svg_graph_name = 'scheme.svg'
graphviz_default_nodesep = 0.8
graphviz_default_ranksep = 1.5
graphviz_default_splines = 'curved'
graphviz_default_overlap = 'prism'
class SRE:
258class SRE:
259    args = None
260    module_rvlab = None
261    eval_obj = None
262    username = None
args = None
module_rvlab = None
eval_obj = None
username = None
def datetime_to_string(dt) -> str:
265def datetime_to_string(dt) -> str:
266    return f"{dt:%Y%m%d%H%M%S}"
def string_to_datetime(string) -> datetime.datetime:
269def string_to_datetime(string) -> datetime:
270    return datetime.strptime(string, "%Y%m%d%H%M%S")
def get_lab_name_from_cli_arg(lab_cli_arg: str, is_path: bool) -> str:
273def get_lab_name_from_cli_arg(lab_cli_arg: str, is_path: bool) -> str:
274    if is_path:
275        lab_cli_arg = os.path.abspath(lab_cli_arg)
276    return lab_cli_arg.replace("/", "@")
def get_running_lab_name( lab_name: str, instance_start_date: datetime.datetime, username: str = None) -> str:
279def get_running_lab_name(lab_name: str, instance_start_date: datetime, username: str = None) -> str:
280    if username is None:
281        if SRE.username is None:
282            username = ""
283        else:
284            username = SRE.username
285    if exetests_separator in lab_name:
286        raise ValueError(f"lab_name must not contain '{exetests_separator}': {lab_name!r}")
287    if exetests_separator in username:
288        raise ValueError(f"username must not contain '{exetests_separator}': {username!r}")
289    return f"{datetime_to_string(instance_start_date)}@@@{lab_name}@@@{username}"
running_lab_name_match_pattern = '^([0-9]+)@@@(.+)@@@(.+)$'
def get_lab_name_from_running_lab_name(running_lab_name: str) -> str:
295def get_lab_name_from_running_lab_name(running_lab_name: str) -> str:
296    match = re.match(running_lab_name_match_pattern, running_lab_name)
297    if match:
298        return match.group(2)
299    else:
300        return 'ERROR-running_lab_name-ILLEGAL-FORMAT'
def get_username_from_running_lab_name(running_lab_name: str) -> str:
303def get_username_from_running_lab_name(running_lab_name: str) -> str:
304    match = re.match(running_lab_name_match_pattern, running_lab_name)
305    if match:
306        return match.group(3)
307    return ''
def get_abbreviated_lab_name_from_running_lab_name(running_lab_name: str) -> str:
310def get_abbreviated_lab_name_from_running_lab_name(running_lab_name: str) -> str:
311    name = get_lab_name_from_running_lab_name(running_lab_name).replace("@", "/").rpartition("/")[2]
312    return name.removesuffix(".py")
def get_current_srelab_file_from_running_lab_name(running_lab_name: str) -> str:
315def get_current_srelab_file_from_running_lab_name(running_lab_name: str) -> str:
316    lab_name = get_lab_name_from_running_lab_name(running_lab_name)
317    pathstr = lab_name.replace("@", "/")
318    if not pathstr.startswith("/"):
319        abspath = (Path(lab_dir) / pathstr).resolve()
320    else:
321        abspath = Path(pathstr).resolve()
322    if abspath.name.endswith(".py"):
323        return str(abspath)
324    else:
325        return str(abspath / srelab_py_name)
def get_srelab_dir(running_lab_name: str):
328def get_srelab_dir(running_lab_name: str):
329    lab_name = get_lab_name_from_running_lab_name(running_lab_name)
330    pathstr = lab_name.replace("@", "/")
331    if not pathstr.startswith("/"):
332        abspath = (Path(lab_dir) / pathstr).resolve()
333    else:
334        abspath = Path(pathstr).resolve()
335    if abspath.name.endswith(".py"):
336        return None
337    else:
338        return str(abspath)
def project_has_directory(running_lab_name=None) -> bool:
341def project_has_directory(running_lab_name=None) -> bool:
342    if running_lab_name.endswith(".py"):
343        return False
344    else:
345        return True
def get_archive_name(running_lab_name: str, date: datetime.datetime) -> str:
348def get_archive_name(running_lab_name: str, date: datetime) -> str:
349    return f"{datetime_to_string(date)}_{running_lab_name}.zst"
def private_lab_dir(running_lab_name: str) -> str:
352def private_lab_dir(running_lab_name: str) -> str:
353    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}"
def auto_eval_log_filename(running_lab_name: str) -> str:
356def auto_eval_log_filename(running_lab_name: str) -> str:
357    return f"{private_lab_dir(running_lab_name)}/{auto_eval_log_name}"
def debug_project_marker_filename(running_lab_name: str) -> str:
360def debug_project_marker_filename(running_lab_name: str) -> str:
361    return f"{private_lab_dir(running_lab_name)}/{debug_project_marker_name}"
def public_lab_dir(running_lab_name: str) -> str:
368def public_lab_dir(running_lab_name: str) -> str:
369    return f"{sre_projects_dir}/{running_lab_name}"
def info_filename(running_lab_name: str) -> str:
372def info_filename(running_lab_name: str) -> str:
373    return f"{sre_projects_dir}/{running_lab_name}/{info_json_name}"
def data_filename(running_lab_name: str) -> str:
376def data_filename(running_lab_name: str) -> str:
377    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{data_json_name}"
def private_mount_dir(running_lab_name: str) -> str:
380def private_mount_dir(running_lab_name: str) -> str:
381    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{private_mount_dir_name}"
def files_dir(running_lab_name: str) -> str:
384def files_dir(running_lab_name: str) -> str:
385    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{files_dir_name}"
def records_dir(running_lab_name: str) -> str:
388def records_dir(running_lab_name: str) -> str:
389    return f"{sre_projects_dir}/{running_lab_name}/{private_dir_name}/{records_dir_name}"
def graph_filename(running_lab_name: str) -> str:
396def graph_filename(running_lab_name: str) -> str:
397    return f"{sre_projects_dir}/{running_lab_name}/{svg_graph_name}"
def answers_filename(running_lab_name: str) -> str:
400def answers_filename(running_lab_name: str) -> str:
401    return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}/{answer_file_name}"
def cheat_filename(running_lab_name: str) -> str:
404def cheat_filename(running_lab_name: str) -> str:
405    return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}/{cheat_file_name}"
def answers_dir(running_lab_name: str) -> str:
408def answers_dir(running_lab_name: str) -> str:
409    return f"{sre_projects_dir}/{running_lab_name}/{answer_dir_name}"
def self_grade_timestamp_file(lab_name: str) -> str:
412def self_grade_timestamp_file(lab_name: str) -> str:
413    return f"{self_grade_timestamp_dir}/{lab_name}"