diff --git a/bin/pg_dump.py b/bin/pg_dump.py index 88f93d548..9d266a6d3 100755 --- a/bin/pg_dump.py +++ b/bin/pg_dump.py @@ -17,7 +17,7 @@ call([ 'docker', 'exec', - 'codabench-db-1', + 'db', 'bash', '-c', f'PGPASSWORD=$DB_PASSWORD pg_dump -Fc -U $DB_USERNAME $DB_NAME > /app/backups/{dump_name}' @@ -25,5 +25,5 @@ # Push/destroy dump call([ - 'docker', 'exec', 'codabench-django-1', 'python', 'manage.py', 'upload_backup', f'{dump_name}' + 'docker', 'exec', 'django', 'python', 'manage.py', 'upload_backup', f'{dump_name}' ]) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 573e72a94..147ed2d2f 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -10,75 +10,167 @@ import tempfile import time import uuid +import requests +import websockets +import yaml +import docker +import logging +import sys # This is only needed for the pytests to pass from shutil import make_archive from urllib.error import HTTPError from urllib.parse import urlparse from urllib.request import urlretrieve from zipfile import ZipFile, BadZipFile -import docker -from rich.progress import Progress +from urllib3 import Retry + from rich.pretty import pprint -import requests -import websockets -import yaml -from billiard.exceptions import SoftTimeLimitExceeded -from celery import Celery, shared_task, utils +from rich.progress import Progress from kombu import Queue, Exchange -from urllib3 import Retry +from celery import Celery, shared_task, utils, signals +from billiard.exceptions import SoftTimeLimitExceeded + +from logs_loguru import configure_logging, colorize_run_args -# This is only needed for the pytests to pass -import sys +logger = logging.getLogger(__name__) sys.path.append("/app/src/settings/") -from celery import signals -import logging -logger = logging.getLogger(__name__) -from logs_loguru import configure_logging, colorize_run_args -import json +# ----------------------------------------------- +# Settings +# ----------------------------------------------- +class Settings: + + @staticmethod + def get(key, default=None): + """ + Return the env var value if set, else default; returns None if not set and no default. + """ + val = os.getenv(key) + + if val is not None: + return val + + if default is not None: + return default + + logger.warning(f"Environment variable '{key}' not found and no default provided.") + return None + + @staticmethod + def to_bool(val): + try: + if isinstance(val, bool): + return val + + val_str = str(val).strip() + + if val_str in ("true", "True", "TRUE", "1"): + return True + if val_str in ("false", "False", "FALSE", "0"): + return False + + logger.warning(f"Failed to parse boolean from '{val}'") + return val + + except Exception as e: + logger.warning(f"Failed to parse boolean from '{val}': {e}") + return val + + # Directories + # NOTE: we need to pass this directory to docker/podman so it knows where to store things! + HOST_DIRECTORY = get("HOST_DIRECTORY", "/tmp/codabench/") + MAX_CACHE_DIR_SIZE_GB = float(get("MAX_CACHE_DIR_SIZE_GB", 10)) + BASE_DIR = "/codabench/" # base directory inside the container + CACHE_DIR = os.path.join(BASE_DIR, "cache") + + # Constants + DOCKER = "docker" + PODMAN = "podman" + LOG_LEVEL_DEBUG = "debug" + + # Defaults + DEFAULT_SOCKETS = { + DOCKER: "unix:///var/run/docker.sock", + PODMAN: "unix:///run/user/1000/podman/podman.sock", + } + + # env variables + LOG_LEVEL = get("LOG_LEVEL", "INFO").lower() + SERIALIZED = get("SERIALIZED", "false") + + USE_GPU = to_bool(get("USE_GPU", "false")) + CONTAINER_ENGINE_EXECUTABLE = get("CONTAINER_ENGINE_EXECUTABLE", DOCKER).lower() + GPU_DEVICE = get("GPU_DEVICE", "nvidia.com/gpu=all") + + CONTAINER_SOCKET = get("CONTAINER_SOCKET", DEFAULT_SOCKETS.get(CONTAINER_ENGINE_EXECUTABLE)) + + COMPETITION_CONTAINER_NETWORK_DISABLED = to_bool(get("COMPETITION_CONTAINER_NETWORK_DISABLED", "False")) + COMPETITION_CONTAINER_HTTP_PROXY = get("COMPETITION_CONTAINER_HTTP_PROXY", "") + COMPETITION_CONTAINER_HTTPS_PROXY = get("COMPETITION_CONTAINER_HTTPS_PROXY", "") + + CODALAB_IGNORE_CLEANUP_STEP = to_bool(get("CODALAB_IGNORE_CLEANUP_STEP")) + + WORKER_BUNDLE_URL_REWRITE = get("WORKER_BUNDLE_URL_REWRITE", "").strip() + + +# ----------------------------------------------- +# Program Kind +# ----------------------------------------------- +# NOTE: This is not used, to be used in next PR +class ProgramKind: + INGESTION_PROGRAM = "ingestion_program" + SCORING_PROGRAM = "scoring_program" + SUBMISSION = "submission" + + +# ----------------------------------------------- +# Submission status +# ----------------------------------------------- +class SubmissionStatus: + NONE = "None" + SUBMITTING = "Submitting" + SUBMITTED = "Submitted" + PREPARING = "Preparing" + RUNNING = "Running" + SCORING = "Scoring" + FINISHED = "Finished" + FAILED = "Failed" + + AVAILABLE_STATUSES = ( + NONE, + SUBMITTING, + SUBMITTED, + PREPARING, + RUNNING, + SCORING, + FINISHED, + FAILED, + ) # ----------------------------------------------- # Logging # ----------------------------------------------- configure_logging( - os.environ.get("LOG_LEVEL", "INFO"), os.environ.get("SERIALIZED", "false") + Settings.LOG_LEVEL, Settings.SERIALIZED ) # ----------------------------------------------- # Initialize Docker or Podman depending on .env # ----------------------------------------------- -if os.environ.get("USE_GPU", "false").lower() == "true": - logger.info( - "Using " - + os.environ.get("CONTAINER_ENGINE_EXECUTABLE", "docker").upper() - + "with GPU capabilites : " - + os.environ.get("GPU_DEVICE", "nvidia.com/gpu=all") - + " network_disabled for the competition container is set to " - + os.environ.get("COMPETITION_CONTAINER_NETWORK_DISABLED", "False") - ) -else: - logger.info( - "Using " - + os.environ.get("CONTAINER_ENGINE_EXECUTABLE", "docker").upper() - + " without GPU capabilities. " - + "network_disabled for the competition container is set to " - + os.environ.get("COMPETITION_CONTAINER_NETWORK_DISABLED", "False") - ) +logger.info( + f"Using {Settings.CONTAINER_ENGINE_EXECUTABLE} " + f"{'with GPU capabilities: ' + Settings.GPU_DEVICE if Settings.USE_GPU else 'without GPU capabilities'}. " + f"Network disabled for the competition container is set to {Settings.COMPETITION_CONTAINER_NETWORK_DISABLED}" +) -if os.environ.get("CONTAINER_ENGINE_EXECUTABLE", "docker").lower() == "docker": - client = docker.APIClient( - base_url=os.environ.get("CONTAINER_SOCKET", "unix:///var/run/docker.sock"), - version="auto", - ) -elif os.environ.get("CONTAINER_ENGINE_EXECUTABLE").lower() == "podman": - client = docker.APIClient( - base_url=os.environ.get( - "CONTAINER_SOCKET", "unix:///run/user/1000/podman/podman.sock" - ), - version="auto", - ) +# Intializing client +# NOTE: CONTAINER_SOCKET is set in Settings based on CONTAINER_ENGINE_EXECUTABLE which must has either podman or docker +client = docker.APIClient( + base_url=Settings.CONTAINER_SOCKET, + version="auto", +) # ----------------------------------------------- @@ -147,8 +239,8 @@ def show_progress(line, progress): total=total, ) except Exception as e: - if os.environ.get("LOG_LEVEL", "info").lower() == "debug": - logger.exception("There was an error showing the progress bar") + if Settings.LOG_LEVEL == Settings.LOG_LEVEL_DEBUG: + logger.exception(f"There was an error showing the progress bar: {e}") # ----------------------------------------------- @@ -171,39 +263,6 @@ def setup_celery_logging(**kwargs): queue_arguments={"x-max-priority": 10}, ), ] -# ----------------------------------------------- -# Directories -# ----------------------------------------------- -# Setup base directories used by all submissions -# note: we need to pass this directory to docker/podman so it knows where to store things! -HOST_DIRECTORY = os.environ.get("HOST_DIRECTORY", "/tmp/codabench/") -BASE_DIR = "/codabench/" # base directory inside the container -CACHE_DIR = os.path.join(BASE_DIR, "cache") -MAX_CACHE_DIR_SIZE_GB = float(os.environ.get("MAX_CACHE_DIR_SIZE_GB", 10)) - - -# ----------------------------------------------- -# Submission status -# ----------------------------------------------- -# Status options for submissions -STATUS_NONE = "None" -STATUS_SUBMITTING = "Submitting" -STATUS_SUBMITTED = "Submitted" -STATUS_PREPARING = "Preparing" -STATUS_RUNNING = "Running" -STATUS_SCORING = "Scoring" -STATUS_FINISHED = "Finished" -STATUS_FAILED = "Failed" -AVAILABLE_STATUSES = ( - STATUS_NONE, - STATUS_SUBMITTING, - STATUS_SUBMITTED, - STATUS_PREPARING, - STATUS_RUNNING, - STATUS_SCORING, - STATUS_FINISHED, - STATUS_FAILED, -) # ----------------------------------------------- @@ -232,7 +291,7 @@ def rewrite_bundle_url_if_needed(url): Example: http://localhost:9000|http://minio:9000 """ - rule = os.getenv("WORKER_BUNDLE_URL_REWRITE", "").strip() + rule = Settings.WORKER_BUNDLE_URL_REWRITE if not rule or "|" not in rule: return url src, dst = rule.split("|", 1) @@ -265,13 +324,11 @@ def run_wrapper(run_args): msg = f"Docker image pull failed: {msg}" else: msg = "Docker image pull failed." - run._update_status(STATUS_FAILED, extra_information=msg) + run._update_status(SubmissionStatus.FAILED, extra_information=msg) raise except SoftTimeLimitExceeded: run._update_status( - STATUS_FAILED, - extra_information="Execution time limit exceeded.", - ) + SubmissionStatus.FAILED, extra_information="Execution time limit exceeded.") raise except SubmissionException as e: msg = str(e).strip() @@ -279,11 +336,11 @@ def run_wrapper(run_args): msg = f"Submission failed: {msg}. See logs for more details." else: msg = "Submission failed. See logs for more details." - run._update_status(STATUS_FAILED, extra_information=msg) + run._update_status(SubmissionStatus.FAILED, extra_information=msg) raise except Exception as e: # Catch any exception to avoid getting stuck in Running status - run._update_status(STATUS_FAILED, extra_information=traceback.format_exc()) + run._update_status(SubmissionStatus.FAILED, extra_information=traceback.format_exc()) raise finally: try: @@ -348,6 +405,8 @@ def get_folder_size_in_gb(folder): def delete_files_in_folder(folder): + if not os.path.isdir(folder): + return for filename in os.listdir(folder): file_path = os.path.join(folder, filename) if os.path.isfile(file_path) or os.path.islink(file_path): @@ -396,13 +455,11 @@ def __init__(self, run_args): # Directories for the run self.watch = True self.completed_program_counter = 0 - self.root_dir = tempfile.mkdtemp(prefix=f'{self.run_related_name}__', dir=BASE_DIR) + self.root_dir = tempfile.mkdtemp(prefix=f'{self.run_related_name}__', dir=Settings.BASE_DIR) self.bundle_dir = os.path.join(self.root_dir, "bundles") self.input_dir = os.path.join(self.root_dir, "input") self.output_dir = os.path.join(self.root_dir, "output") - self.data_dir = os.path.join( - HOST_DIRECTORY, "data" - ) # absolute path to data in the host + self.data_dir = os.path.join(Settings.HOST_DIRECTORY, "data") # absolute path to data in the host self.logs = {} # Details for submission @@ -573,9 +630,9 @@ def _update_submission(self, data): def _update_status(self, status, extra_information=None): # Update submission status - if status not in AVAILABLE_STATUSES: + if status not in SubmissionStatus.AVAILABLE_STATUSES: raise SubmissionException( - f"Status '{status}' is not in available statuses: {AVAILABLE_STATUSES}" + f"Status '{status}' is not in available statuses: {SubmissionStatus.AVAILABLE_STATUSES}" ) data = {"status": status, "status_details": extra_information} try: @@ -681,7 +738,7 @@ def _get_bundle(self, url, destination, cache=True): # Hash url and download it if it doesn't exist url_without_params = url.split("?")[0] url_hash = hashlib.sha256(url_without_params.encode("utf8")).hexdigest() - bundle_file = os.path.join(CACHE_DIR, url_hash) + bundle_file = os.path.join(Settings.CACHE_DIR, url_hash) download_needed = not os.path.exists(bundle_file) else: if not os.path.exists(self.bundle_dir): @@ -738,26 +795,18 @@ async def _run_container_engine_cmd(self, container, kind): websocket = None try: websocket_url = f"{self.websocket_url}?kind={kind}" - logger.debug( - "Connecting to " - + websocket_url - + "for container " - + str(container.get("Id")) - ) + logger.debug(f"Connecting to {websocket_url} for container {str(container.get('Id'))}") websocket = await asyncio.wait_for( websockets.connect(websocket_url), timeout=10.0 ) - logger.debug( - "connected to " - + str(websocket_url) - + "for container " - + str(container.get("Id")) - ) + logger.debug(f"connected to {websocket_url} for container {str(container.get('Id'))}") + except Exception as e: logger.error( f"There was an error trying to connect to the websocket on the codabench instance: {e}" ) - if os.environ.get("LOG_LEVEL", "info").lower() == "debug": + + if Settings.LOG_LEVEL == Settings.LOG_LEVEL_DEBUG: logger.exception(e) start = time.time() @@ -774,10 +823,7 @@ async def _run_container_engine_cmd(self, container, kind): ) # If we enter the for loop after the container exited, the program will get stuck - if ( - client.inspect_container(container)["State"]["Status"].lower() - == "running" - ): + if client.inspect_container(container)["State"]["Status"].lower() == "running": logger.debug( "Show the logs and stream them to codabench " + container.get("Id") ) @@ -793,7 +839,7 @@ async def _run_container_engine_cmd(self, container, kind): ) except Exception as e: logger.error(e) - + # Errors elif log[1] is not None: stderr_chunks.append(log[1]) @@ -812,7 +858,7 @@ async def _run_container_engine_cmd(self, container, kind): logger.error( f"There was an error while starting the container and getting the logs: {e}" ) - if os.environ.get("LOG_LEVEL", "info").lower() == "debug": + if Settings.LOG_LEVEL == Settings.LOG_LEVEL_DEBUG: logger.exception(e) # Get the return code of the competition container once done @@ -831,12 +877,7 @@ async def _run_container_engine_cmd(self, container, kind): logger.error(e) client.remove_container(container, force=True) - logger.debug( - "Container " - + container.get("Id") - + "exited with status code : " - + str(return_Code["StatusCode"]) - ) + logger.debug(f"Container {container.get('Id')} exited with status code : {str(return_Code['StatusCode'])}") except ( requests.exceptions.ReadTimeout, @@ -849,7 +890,7 @@ async def _run_container_engine_cmd(self, container, kind): finally: try: # Last chance of removing container - client.remove_container(container_id, force=True) + client.remove_container(container.get("Id"), force=True) except Exception: pass @@ -885,11 +926,11 @@ def _get_host_path(self, *paths): path = os.path.join(*paths) # pull front of path, which points to the location inside the container - path = path[len(BASE_DIR) :] + path = path[len(Settings.BASE_DIR):] # add host to front, so when we run commands in the container on the host they # can be seen properly - path = os.path.join(HOST_DIRECTORY, path) + path = os.path.join(Settings.HOST_DIRECTORY, path) # Create if necessary os.makedirs(path, exist_ok=True) @@ -934,15 +975,15 @@ async def _run_program_directory(self, program_dir, kind): logger.info(f"Metadata path is {os.path.join(program_dir, metadata_path)}") with open(os.path.join(program_dir, metadata_path), "r") as metadata_file: try: # try to find a command in the metadata, in other cases set metadata to None - metadata = yaml.load(metadata_file.read(), Loader=yaml.FullLoader) + metadata = yaml.safe_load(metadata_file.read()) logger.info(f"Metadata contains:\n {metadata}") if isinstance(metadata, dict): # command found command = metadata.get("command") else: command = None except yaml.YAMLError as e: - logger.error("Error parsing YAML file: ", e) - print("Error parsing YAML file: ", e) + logger.error(f"Error parsing YAML file: {e}") + print(f"Error parsing YAML file: {e}") command = None if not command and kind == "ingestion": raise SubmissionException( @@ -1044,14 +1085,16 @@ async def _run_program_directory(self, program_dir, kind): "SETUID", "SYS_CHROOT", ] + # Configure whether or not we use the GPU. Also setting auto_remove to False because - if os.environ.get("CONTAINER_ENGINE_EXECUTABLE", "docker").lower() == "docker": + if Settings.CONTAINER_ENGINE_EXECUTABLE == Settings.DOCKER: security_options = ["no-new-privileges"] else: security_options = ["label=disable"] + # Setting the device ID like this allows users to specify which gpu to use in the .env file, with all being the default if no value is given - device_id = [os.environ.get("GPU_DEVICE", "nvidia.com/gpu=all")] - if os.environ.get("USE_GPU", "false").lower() == "true": + device_id = [Settings.GPU_DEVICE] + if Settings.USE_GPU: logger.info("Running the container with GPU capabilities") host_config = client.create_host_config( auto_remove=False, @@ -1081,26 +1124,10 @@ async def _run_program_directory(self, program_dir, kind): if kind == "ingestion" else self.program_container_name ) - # Disable or not the competition container access to Internet (False by default) - container_network_disabled = os.environ.get( - "COMPETITION_CONTAINER_NETWORK_DISABLED", "" - ) + # Creating container + # COMPETITION_CONTAINER_NETWORK_DISABLED: Disable or not the competition container access to Internet (False by default) # HTTP and HTTPS proxy for the competition container if needed - competition_container_proxy_http = os.environ.get( - "COMPETITION_CONTAINER_HTTP_PROXY", "" - ) - competition_container_proxy_http = ( - "http_proxy=" + competition_container_proxy_http - ) - - competition_container_proxy_https = os.environ.get( - "COMPETITION_CONTAINER_HTTPS_PROXY", "" - ) - competition_container_proxy_https = ( - "https_proxy=" + competition_container_proxy_https - ) - container = client.create_container( self.container_image, name=container_name, @@ -1111,12 +1138,13 @@ async def _run_program_directory(self, program_dir, kind): working_dir="/app/program", environment=[ "PYTHONUNBUFFERED=1", - competition_container_proxy_http, - competition_container_proxy_https, + "http_proxy=" + Settings.COMPETITION_CONTAINER_HTTP_PROXY, + "https_proxy=" + Settings.COMPETITION_CONTAINER_HTTPS_PROXY, ], - network_disabled=container_network_disabled.lower() == "true", + network_disabled=Settings.COMPETITION_CONTAINER_NETWORK_DISABLED, ) - logger.debug("Created container : " + str(container)) + + logger.debug("Created container: " + str(container)) logger.info("Volume configuration of the container: ") pprint(volumes_config) # This runs the container engine command and asynchronously passes data back via websocket @@ -1183,29 +1211,23 @@ def _put_file(self, url, file=None, raw_data=None, content_type="application/zip logger.info(f"response: {resp}") logger.info(f"content: {resp.content}") - def _prep_cache_dir(self, max_size=MAX_CACHE_DIR_SIZE_GB): - if not os.path.exists(CACHE_DIR): - os.mkdir(CACHE_DIR) + def _prep_cache_dir(self, max_size=Settings.MAX_CACHE_DIR_SIZE_GB): + if not os.path.exists(Settings.CACHE_DIR): + os.mkdir(Settings.CACHE_DIR) logger.info("Checking if cache directory needs to be pruned...") - if get_folder_size_in_gb(CACHE_DIR) > max_size: + if get_folder_size_in_gb(Settings.CACHE_DIR) > max_size: logger.info("Pruning cache directory") - delete_files_in_folder(CACHE_DIR) + delete_files_in_folder(Settings.CACHE_DIR) else: logger.info("Cache directory does not need to be pruned!") def prepare(self): hostname = utils.nodenames.gethostname() if self.is_scoring: - self._update_status( - STATUS_RUNNING, extra_information=f"scoring_hostname-{hostname}" - ) + self._update_status(SubmissionStatus.RUNNING, extra_information=f"scoring_hostname-{hostname}") else: - self._update_status( - STATUS_RUNNING, extra_information=f"ingestion_hostname-{hostname}" - ) - if not self.is_scoring: # Only during prediction step do we want to announce "preparing" - self._update_status(STATUS_PREPARING) + self._update_status(SubmissionStatus.PREPARING, extra_information=f"ingestion_hostname-{hostname}") # Setup cache and prune if it's out of control self._prep_cache_dir() @@ -1245,6 +1267,7 @@ def prepare(self): # Before the run starts we want to download images, they may take a while to download # and to do this during the run would subtract from the participants time. self._get_container_image(self.container_image) + self._update_status(SubmissionStatus.RUNNING) def start(self): program_dir = os.path.join(self.root_dir, "program") @@ -1278,25 +1301,25 @@ def start(self): "error_message": error_message, "is_scoring": self.is_scoring, } - # Some cleanup - for kind, logs in self.logs.items(): - containers_to_kill = [] - containers_to_kill.append(self.ingestion_container_name) - containers_to_kill.append(self.program_container_name) - logger.debug( - "Trying to kill and remove container " + str(containers_to_kill) - ) - for container in containers_to_kill: - try: - client.remove_container(str(container), force=True) - except docker.errors.APIError as e: - logger.error(e) - except Exception as e: - logger.error( - f"There was a problem killing {containers_to_kill}: {e}" - ) - if os.environ.get("LOG_LEVEL", "info").lower() == "debug": - logger.exception(e) + # Cleanup containers + containers_to_kill = [ + self.ingestion_container_name, + self.program_container_name + ] + logger.debug( + "Trying to kill and remove container " + str(containers_to_kill) + ) + for container in containers_to_kill: + try: + client.remove_container(str(container), force=True) + except docker.errors.APIError as e: + logger.error(e) + except Exception as e: + logger.error( + f"There was a problem killing {containers_to_kill}: {e}" + ) + if Settings.LOG_LEVEL == Settings.LOG_LEVEL_DEBUG: + logger.exception(e) # Send data to be written to ingestion/scoring std_err self._update_submission(execution_time_limit_exceeded_data) # Send error through web socket to the frontend @@ -1347,7 +1370,7 @@ def start(self): logger.error( f"There was a problem killing {containers_to_kill}: {e}" ) - if os.environ.get("LOG_LEVEL", "info").lower() == "debug": + if Settings.LOG_LEVEL == Settings.LOG_LEVEL_DEBUG: logger.exception(e) if kind == "program": self.program_exit_code = return_code @@ -1366,9 +1389,6 @@ def start(self): # set logs of this kind to None, since we handled them already logger.info("Program finished") signal.alarm(0) - # Ensure loop is cleaned up - loop.close() - asyncio.set_event_loop(None) if self.is_scoring: # Check if scoring program failed @@ -1381,15 +1401,15 @@ def start(self): failed_rc = (program_rc is None) or (program_rc != 0) if had_async_exc or failed_rc: self._update_status( - STATUS_FAILED, + SubmissionStatus.FAILED, extra_information=f"program_rc={program_rc}, async={task_results}", ) # Raise so upstream marks failed immediately raise SubmissionException("Child task failed or non-zero return code") - self._update_status(STATUS_FINISHED) + self._update_status(SubmissionStatus.FINISHED) else: - self._update_status(STATUS_SCORING) + self._update_status(SubmissionStatus.SCORING) def push_scores(self): """This is only ran at the end of the scoring step""" @@ -1410,7 +1430,7 @@ def push_scores(self): elif os.path.exists(os.path.join(self.output_dir, "scores.txt")): scores_file = os.path.join(self.output_dir, "scores.txt") with open(scores_file) as f: - scores = yaml.load(f, yaml.Loader) + scores = yaml.safe_load(f) else: raise SubmissionException( "Could not find scores file, did the scoring program output it?" @@ -1461,7 +1481,7 @@ def push_output(self): self._put_dir(self.scoring_result, self.output_dir) def clean_up(self): - if os.environ.get("CODALAB_IGNORE_CLEANUP_STEP"): + if Settings.CODALAB_IGNORE_CLEANUP_STEP: logger.warning( f"CODALAB_IGNORE_CLEANUP_STEP mode enabled, ignoring clean up of: {self.root_dir}" ) diff --git a/compute_worker/uv.lock b/compute_worker/uv.lock index 6a9f9d311..7f7b06e72 100644 --- a/compute_worker/uv.lock +++ b/compute_worker/uv.lock @@ -72,27 +72,27 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, - { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, - { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, - { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, - { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, - { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, - { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, - { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, - { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] @@ -281,11 +281,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -330,7 +330,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -338,9 +338,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] diff --git a/documentation/docs/Organizers/Benchmark_Creation/Benchmark-Examples.md b/documentation/docs/Organizers/Benchmark_Creation/Benchmark-Examples.md index 3c82874b5..d685ea4fe 100644 --- a/documentation/docs/Organizers/Benchmark_Creation/Benchmark-Examples.md +++ b/documentation/docs/Organizers/Benchmark_Creation/Benchmark-Examples.md @@ -20,6 +20,10 @@ We propose three versions of the [Classify Wheat Seeds](https://github.com/codal [Mini-AutoML Bundle](https://github.com/codalab/competition-examples/tree/master/codabench/mini-automl) is a benchmark template for Codabench, featuring code submission to multiple datasets (tasks). +### Shortest Path on Weighted Graph (SPoWG) + +[SPoWG Bundle](https://github.com/codalab/competition-examples/tree/master/codabench/shortest_path) is a benchmark template for Codabench, featuring code submission to solve an optimization problem on graphs. + ### AutoWSL You can find two versions of the [Automated Weakly Supervised Learning Benchmark](https://github.com/codalab/competition-examples/tree/master/codabench/autowsl): diff --git a/pyproject.toml b/pyproject.toml index ba4285da6..9662fa05c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "djangorestframework-csv==3.0.1", "drf-extensions==0.8.0", "markdown==3.10.2", - "pygments==2.19.2", + "pygments==2.20.0", "drf-writable-nested==0.7.2", "flex==6.14.1", "pyrabbit2==1.0.7", @@ -54,7 +54,7 @@ dependencies = [ "twisted==25.5.0", "ipdb==0.13.13", "jinja2==3.1.6", - "requests==2.33.0", + "requests==2.33.1", "drf-extra-fields==3.7.0", "botocore==1.42.50", "s3transfer==0.16.0", diff --git a/setup.cfg b/setup.cfg index 16f5d925e..7132b51b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [flake8] -ignore = E501,F405,W504,F541 +ignore = E501,F405,W504,F541,W503 # Flake plugins: inline-quotes = single diff --git a/src/apps/api/serializers/submission_leaderboard.py b/src/apps/api/serializers/submission_leaderboard.py index 368f8fec4..5e86fd3fc 100644 --- a/src/apps/api/serializers/submission_leaderboard.py +++ b/src/apps/api/serializers/submission_leaderboard.py @@ -8,10 +8,15 @@ class SubmissionScoreSerializer(serializers.ModelSerializer): index = serializers.IntegerField(source='column.index', read_only=True) column_key = serializers.CharField(source='column.key', read_only=True) + precision = serializers.IntegerField(source='column.precision', read_only=True) + is_primary = serializers.SerializerMethodField() class Meta: model = SubmissionScore - fields = ('id', 'index', 'score', 'column_key') + fields = ('id', 'index', 'score', 'column_key', 'precision', 'is_primary') + + def get_is_primary(self, obj): + return obj.column.index == obj.column.leaderboard.primary_index class SubmissionLeaderBoardSerializer(serializers.ModelSerializer): diff --git a/src/apps/api/tests/test_cleanup.py b/src/apps/api/tests/test_cleanup.py index a927800af..1dd67a2ce 100644 --- a/src/apps/api/tests/test_cleanup.py +++ b/src/apps/api/tests/test_cleanup.py @@ -56,8 +56,19 @@ def setUp(self): DataFactory(created_by=user, type=Data.INGESTION_PROGRAM), DataFactory(created_by=user, type=Data.SCORING_PROGRAM), DataFactory(created_by=user, type=Data.INPUT_DATA), - DataFactory(created_by=user, type=Data.REFERENCE_DATA), - DataFactory(created_by=user, type=Data.PUBLIC_DATA) + DataFactory(created_by=user, type=Data.REFERENCE_DATA) + ] + + # Create unused starting kits + self.unused_starting_kits = [ + DataFactory(created_by=user, type=Data.STARTING_KIT), + DataFactory(created_by=user, type=Data.STARTING_KIT) + ] + + # Create unused competition bundles + self.unused_competition_bundles = [ + DataFactory(created_by=user, type=Data.COMPETITION_BUNDLE), + DataFactory(created_by=user, type=Data.COMPETITION_BUNDLE) ] self.client.login(username='test_user', password='test_user') @@ -72,6 +83,8 @@ def test_cleanup_stats(self): assert content["unused_datasets_programs"] == len(self.unused_datasets_programs) assert content["unused_submissions"] == len(self.unused_submissions) assert content["failed_submissions"] == len(self.failed_submissions) + assert content["unused_starting_kits"] == len(self.unused_starting_kits) + assert content["unused_competition_bundles"] == len(self.unused_competition_bundles) def test_delete_unused_tasks(self): @@ -132,3 +145,33 @@ def test_delete_failed_submissions(self): assert resp.status_code == 200 content = json.loads(resp.content) assert content["failed_submissions"] == 0 + + def test_delete_unused_starting_kits(self): + + url = reverse('delete_unused_starting_kits') + resp = self.client.delete(url) + assert resp.status_code == 200 + content = json.loads(resp.content) + assert content["success"] + assert content["message"] == "Unused starting kits deleted successfully" + + url = reverse('user_quota_cleanup') + resp = self.client.get(url) + assert resp.status_code == 200 + content = json.loads(resp.content) + assert content["unused_starting_kits"] == 0 + + def test_delete_unused_competition_bundles(self): + + url = reverse('delete_unused_competition_bundles') + resp = self.client.delete(url) + assert resp.status_code == 200 + content = json.loads(resp.content) + assert content["success"] + assert content["message"] == "Unused competition bundles deleted successfully" + + url = reverse('user_quota_cleanup') + resp = self.client.get(url) + assert resp.status_code == 200 + content = json.loads(resp.content) + assert content["unused_competition_bundles"] == 0 diff --git a/src/apps/api/urls.py b/src/apps/api/urls.py index cad6fde62..640b8a954 100644 --- a/src/apps/api/urls.py +++ b/src/apps/api/urls.py @@ -54,6 +54,8 @@ path('delete_unused_datasets/', quota.delete_unused_datasets, name="delete_unused_datasets"), path('delete_unused_submissions/', quota.delete_unused_submissions, name="delete_unused_submissions"), path('delete_failed_submissions/', quota.delete_failed_submissions, name="delete_failed_submissions"), + path('delete_unused_starting_kits/', quota.delete_unused_starting_kits, name="delete_unused_starting_kits"), + path('delete_unused_competition_bundles/', quota.delete_unused_competition_bundles, name="delete_unused_competition_bundles"), # User account path('delete_account/', profiles.delete_account, name="delete_account"), diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index 7cacb433b..53b9d2193 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -18,7 +18,7 @@ from rest_framework.response import Response from rest_framework.renderers import JSONRenderer from rest_framework_csv.renderers import CSVRenderer -from api.pagination import LargePagination +from api.pagination import DynamicChoicePagination, LargePagination from api.renderers import ZipRenderer from rest_framework.viewsets import ModelViewSet from api.serializers.competitions import CompetitionSerializerSimple, PhaseSerializer, \ @@ -774,10 +774,17 @@ def rerun_submissions(self, request, pk): def get_leaderboard(self, request, pk): phase = self.get_object() if phase.competition.fact_sheet: - fact_sheet_keys = [(phase.competition.fact_sheet[question]['key'], phase.competition.fact_sheet[question]['title']) - for question in phase.competition.fact_sheet if phase.competition.fact_sheet[question]['is_on_leaderboard'] == 'true'] + fact_sheet_keys = [ + ( + phase.competition.fact_sheet[question]['key'], + phase.competition.fact_sheet[question]['title'] + ) + for question in phase.competition.fact_sheet + if phase.competition.fact_sheet[question]['is_on_leaderboard'] == 'true' + ] else: fact_sheet_keys = None + query = LeaderboardPhaseSerializer(phase).data response = { 'title': query['leaderboard']['title'], @@ -787,9 +794,11 @@ def get_leaderboard(self, request, pk): 'fact_sheet_keys': fact_sheet_keys or None, 'primary_index': query['leaderboard']['primary_index'] } + columns = [col for col in query['columns']] submissions_keys = {} submission_detailed_results = {} + for submission in query['submissions']: submission_key = f"{submission['owner']}{submission['parent'] or submission['id']}" # gather detailed result from submissions for each task @@ -814,6 +823,7 @@ def get_leaderboard(self, request, pk): 'organization': submission['organization'], 'created_when': submission['created_when'] }) + for score in submission['scores']: # to check if a column is found @@ -851,6 +861,21 @@ def get_leaderboard(self, request, pk): for k, v in submissions_keys.items(): response['submissions'][v]['detailed_results'] = submission_detailed_results[k] + # --- pagination addition --- + total_count = len(response['submissions']) + paginator = DynamicChoicePagination() + paginated_submissions = paginator.paginate_queryset(response['submissions'], request, view=self) + if paginated_submissions is None: + paginated_submissions = response['submissions'] + + response['submissions'] = paginated_submissions + response['count'] = total_count + response['page_size'] = getattr(paginator, 'requested_page_size', request.query_params.get('page_size', 50)) + response['next'] = paginator.get_next_link() + response['previous'] = paginator.get_previous_link() + response['allowed_page_sizes'] = [50, 100, 500, 'all'] + # --- end pagination addition --- + for task in query['tasks']: # This can be used to rendered variable columns on each task tempTask = { @@ -862,6 +887,7 @@ def get_leaderboard(self, request, pk): for col in columns: tempTask['columns'].append(col) response['tasks'].append(tempTask) + return Response(response) diff --git a/src/apps/api/views/quota.py b/src/apps/api/views/quota.py index 0b99ff3a2..6cc927517 100644 --- a/src/apps/api/views/quota.py +++ b/src/apps/api/views/quota.py @@ -21,7 +21,9 @@ def user_quota_cleanup(request): unused_datasets_programs = Data.objects.filter( Q(created_by=request.user) & ~Q(type=Data.SUBMISSION) & - ~Q(type=Data.COMPETITION_BUNDLE) + ~Q(type=Data.COMPETITION_BUNDLE) & + ~Q(type=Data.PUBLIC_DATA) & + ~Q(type=Data.STARTING_KIT) ).exclude( Q(task_ingestion_programs__isnull=False) | Q(task_input_datas__isnull=False) | @@ -42,11 +44,29 @@ def user_quota_cleanup(request): Q(status=Submission.FAILED) ).count() + # Get unused starting kits count + unused_starting_kits = Data.objects.filter( + Q(created_by=request.user) & + Q(type=Data.STARTING_KIT) & + Q(competition__isnull=True) & + Q(phase_starting_kit__isnull=True) + ).count() + + # Get unused competition bundles + unused_competition_bundles = Data.objects.filter( + Q(created_by=request.user) & + Q(type=Data.COMPETITION_BUNDLE) & + Q(competition__isnull=True) & + Q(competition_bundles__isnull=True) + ).count() + return Response({ "unused_tasks": unused_tasks, "unused_datasets_programs": unused_datasets_programs, "unused_submissions": unused_submissions, - "failed_submissions": failed_submissions + "failed_submissions": failed_submissions, + "unused_starting_kits": unused_starting_kits, + "unused_competition_bundles": unused_competition_bundles }) @@ -84,7 +104,9 @@ def delete_unused_datasets(request): Data.objects.filter( Q(created_by=request.user) & ~Q(type=Data.SUBMISSION) & - ~Q(type=Data.COMPETITION_BUNDLE) + ~Q(type=Data.COMPETITION_BUNDLE) & + ~Q(type=Data.PUBLIC_DATA) & + ~Q(type=Data.STARTING_KIT) ).exclude( Q(task_ingestion_programs__isnull=False) | Q(task_input_datas__isnull=False) | @@ -144,3 +166,47 @@ def delete_failed_submissions(request): "success": False, "message": f"{e}" }) + + +@api_view(['DELETE']) +def delete_unused_starting_kits(request): + try: + Data.objects.filter( + Q(created_by=request.user) & + Q(type=Data.STARTING_KIT) & + Q(competition__isnull=True) & + Q(phase_starting_kit__isnull=True) + ).delete() + + return Response({ + "success": True, + "message": "Unused starting kits deleted successfully" + }) + except Exception as e: + logger.error(f"UNUSED STARTING KITS DELETION --- {e}") + return Response({ + "success": False, + "message": f"{e}" + }) + + +@api_view(['DELETE']) +def delete_unused_competition_bundles(request): + try: + Data.objects.filter( + Q(created_by=request.user) & + Q(type=Data.COMPETITION_BUNDLE) & + Q(competition__isnull=True) & + Q(competition_bundles__isnull=True) + ).delete() + + return Response({ + "success": True, + "message": "Unused competition bundles deleted successfully" + }) + except Exception as e: + logger.error(f"UNUSED COMPETITION BUNDLES DELETION --- {e}") + return Response({ + "success": False, + "message": f"{e}" + }) diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 8477e1c62..34e739e5d 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -674,8 +674,10 @@ def cancel(self, status=CANCELLED): sub.cancel(status=status) celery_app = app # If a custom queue is set, we need to fetch the appropriate celery app - if self.phase.competition.queue: - celery_app = app_for_vhost(str(self.phase.competition.queue.vhost)) + # NOTE: We fetch the queue from submission and not from competition to be sure that we are using the correct queue + # Originally we were using queue from the competition (self.phase.competition.queue) + if self.queue: + celery_app = app_for_vhost(str(self.queue.vhost)) # We need to convert the UUID given by celery into a byte like object otherwise it won't work celery_app.control.revoke(str(self.celery_task_id), terminate=True) self.status = status diff --git a/src/apps/competitions/tasks.py b/src/apps/competitions/tasks.py index 4990d04f5..d72e9191a 100644 --- a/src/apps/competitions/tasks.py +++ b/src/apps/competitions/tasks.py @@ -195,9 +195,9 @@ def _send_to_compute_worker(submission, is_scoring): time_limit = submission.phase.execution_time_limit + time_padding if submission.phase.competition.queue: # if the competition is running on a custom queue, not the default queue - submission.queue_name = submission.phase.competition.queue.name or '' + submission.queue = submission.phase.competition.queue run_args['execution_time_limit'] = submission.phase.execution_time_limit # use the competition time limit - submission.save(update_fields=["queue_name"]) + submission.save(update_fields=["queue"]) if submission.status == Submission.SUBMITTING: # Don't want to mark an already-prepared submission as "submitted" again, so # only do this if we were previously "SUBMITTING" @@ -353,11 +353,14 @@ def mark_status_as_failed_and_delete_dataset(competition_creation_status, detail with NamedTemporaryFile(mode="w+b") as temp_file: logger.info(f"Download competition bundle: {competition_dataset.data_file.name}") competition_bundle_url = make_url_sassy(competition_dataset.data_file.url) - with requests.get(competition_bundle_url, stream=True) as r: - r.raise_for_status() - for chunk in r.iter_content(chunk_size=8192): - temp_file.write(chunk) - r.close() + try: + with requests.get(competition_bundle_url, stream=True) as r: + r.raise_for_status() + for chunk in r.iter_content(chunk_size=8192): + temp_file.write(chunk) + r.close() + except requests.exceptions.RequestException as e: + raise CompetitionUnpackingException(f"Failed to download bundle from storage: {e}") # seek back to the start of the tempfile after writing to it.. temp_file.seek(0) @@ -371,10 +374,17 @@ def mark_status_as_failed_and_delete_dataset(competition_creation_status, detail # Read metadata (competition.yaml) yaml_path = os.path.join(temp_directory, "competition.yaml") if not os.path.exists(yaml_path): - raise CompetitionUnpackingException("competition.yaml is missing from zip, check your folder structure " - "to make sure it is in the root directory.") - with open(yaml_path) as f: - competition_yaml = yaml.safe_load(f.read()) + raise CompetitionUnpackingException( + "competition.yaml is missing from zip, check your folder structure " + "to make sure it is in the root directory." + ) + try: + with open(yaml_path) as f: + competition_yaml = yaml.safe_load(f.read()) + except yaml.YAMLError as e: + raise CompetitionUnpackingException(f"Error parsing competition.yaml: {e}") + except Exception as e: + raise CompetitionUnpackingException(f"Failed to read competition.yaml: {e}") yaml_version = str(competition_yaml.get('version', '1')) @@ -428,11 +438,14 @@ def _get_error_string(error_dict): mark_status_as_failed_and_delete_dataset(status, message) raise e - except Exception as e: # noqa: E722 + except Exception as e: # These are critical uncaught exceptions, make sure the end user is at least informed # that unpacking has failed -- do not share unhandled exception details logger.error(traceback.format_exc()) - message = "Unpacking the bundle failed. Here is the error log: {}".format(e) + if isinstance(e, KeyError): + message = f"Unpacking the bundle failed. A required key or referenced index ({e}) was not found in the YAML. Check that all mandatory fields are present and that any item referenced by index is correctly defined." + else: + message = f"Unpacking the bundle failed. Here is the error log: {e}" mark_status_as_failed_and_delete_dataset(status, message) diff --git a/src/static/js/ours/client.js b/src/static/js/ours/client.js index f43bfaaa2..fa169c5a8 100644 --- a/src/static/js/ours/client.js +++ b/src/static/js/ours/client.js @@ -147,6 +147,10 @@ CODALAB.api = { get_leaderboard_for_render: function (phase_pk) { return CODALAB.api.request('GET', `${URLS.API}phases/${phase_pk}/get_leaderboard/`) }, + get_leaderboard_for_render: function (phase_pk, params = {}) { + return CODALAB.api.request('GET', `${URLS.API}phases/${phase_pk}/get_leaderboard/`, params) + }, + update_submission_score: function (pk, data) { return CODALAB.api.request('PATCH', `${URLS.API}submission_scores/${pk}/`, data) }, @@ -388,6 +392,12 @@ CODALAB.api = { delete_failed_submissions: () => { return CODALAB.api.request('DELETE', `${URLS.API}delete_failed_submissions/`) }, + delete_unused_starting_kits: () => { + return CODALAB.api.request('DELETE', `${URLS.API}delete_unused_starting_kits/`) + }, + delete_unused_competition_bundles: () => { + return CODALAB.api.request('DELETE', `${URLS.API}delete_unused_competition_bundles/`) + }, /*--------------------------------------------------------------------- User Account ---------------------------------------------------------------------*/ diff --git a/src/static/riot/competitions/bundle_management.tag b/src/static/riot/competitions/bundle_management.tag index 635bc9f99..b7253dc5a 100644 --- a/src/static/riot/competitions/bundle_management.tag +++ b/src/static/riot/competitions/bundle_management.tag @@ -4,13 +4,13 @@ - - +
@@ -23,32 +23,32 @@ - - + onclick="{show_info_modal.bind(this, bundle)}"> + - - + + - + @@ -56,8 +56,8 @@ - - - - - - - - - - - - + + + + + + + + + + + +
File Name
{ dataset.name }{ bundle.name } - { pretty_bytes(dataset.file_size) }{ timeSince(Date.parse(dataset.created_when)) } ago{ pretty_bytes(bundle.file_size) }{ timeSince(Date.parse(bundle.created_when)) } ago - -
- +
+
- No Datasets Yet! + No Competition Bundles Yet!
- +
- No submissions have been added to this leaderboard yet! -
- - - - - - {index + 1} - { submission.owner }{ submission.organization.name } - { pretty_date(submission.created_when) } - {submission.id} - - - - {get_score(column, submission)} -
+ No submissions have been added to this leaderboard yet! +
+ + + + + + {get_row_number(index)} + { submission.owner }{ submission.organization.name } + { pretty_date(submission.created_when) } + {submission.id} + + + + {get_score(column, submission)} +
+ + +