From 3fb4e53e882832fc1a9e0b9eed3712303175c136 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Mar 2025 11:42:31 +0000 Subject: [PATCH 1/3] Implement countdown widget --- home/widgets.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/home/widgets.py b/home/widgets.py index 31fef04..37d8709 100644 --- a/home/widgets.py +++ b/home/widgets.py @@ -1,5 +1,7 @@ """AiiDAlab basic widgets.""" +from datetime import datetime +import subprocess from threading import Timer import ipywidgets as ipw @@ -187,3 +189,53 @@ def _refresh_output(self, _=None): self._output.value = ( self.template.format(text=self.value) if self.value else "" ) + + +class CountDownWidget(ipw.VBox): + """A countdown w.r.t the AiiDAlab container creation time.""" + + TEMPLATE = "

Time remaining: {countdown}

" + FINAL_WARNING = "

AiiDAlab container shutdown imminent - please save your work!

" + + def __init__(self, duration: str, **kwargs): + self.reference_time = datetime.strptime(duration, "%H:%M:%S") + + self.countdown = ipw.HTML() + + super().__init__( + children=[ + self.countdown, + ], + **kwargs, + ) + + def start(self): + Timer(1, self._update_countdown).start() + + def _update_countdown(self): + try: + remaining, time_running_out = self._remaining_time() + message = self.TEMPLATE.format( + color="red" if time_running_out else "black", + countdown=remaining, + ) + except Exception: + remaining = None + message = "Failed to obtain remaining time." + self.countdown.value = message + if remaining != "00:00:00": + self.start() + else: + self.countdown.value = self.FINAL_WARNING + + def _remaining_time(self) -> tuple[str, bool]: + process = subprocess.check_output(["ps", "-o", "etime=", "-p", "1"]) + elapsed_str = process.decode().strip() + if len(elapsed_str) < 6: + elapsed_str = f"00:{elapsed_str}" + elapsed_time = datetime.strptime(elapsed_str, "%H:%M:%S") + remaining = self.reference_time - elapsed_time + if remaining.total_seconds() >= 0: + time_running_out = remaining.total_seconds() < 60 * 5 + return (datetime.min + remaining).strftime("%H:%M:%S"), time_running_out + return "00:00:00", True From 25052d5c77f4bd8c7ae485e857f04db4707d5956 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 4 Mar 2025 11:42:43 +0000 Subject: [PATCH 2/3] Use countdown widget in main notebook --- home/widgets.py | 2 +- start.ipynb | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/home/widgets.py b/home/widgets.py index 37d8709..3e8ca20 100644 --- a/home/widgets.py +++ b/home/widgets.py @@ -1,7 +1,7 @@ """AiiDAlab basic widgets.""" -from datetime import datetime import subprocess +from datetime import datetime from threading import Timer import ipywidgets as ipw diff --git a/start.ipynb b/start.ipynb index bc51cec..211bdb4 100644 --- a/start.ipynb +++ b/start.ipynb @@ -46,6 +46,26 @@ " parsed_url = urlparse.parse_qs(url.query)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from yaml import safe_load\n", + "\n", + "from home.widgets import CountDownWidget\n", + "\n", + "# For temporary deployments (e.g. demo server), duration is read from a local\n", + "# config file. This will initiate the countdown widget.\n", + "demo_server_config_file = Path.home() / \".aiidalab\" / \"demo-server-config.yml\"\n", + "if demo_server_config_file.exists():\n", + " demo_server_config = safe_load(demo_server_config_file.read_text())\n", + " countdown = CountDownWidget(duration=demo_server_config.get(\"duration\", \"12:00:00\"))\n", + " countdown.start()\n", + " display(countdown)" + ] + }, { "cell_type": "code", "execution_count": null, From 1dc7d3ec1ec9fcfceff36464e2804f9727b77ca6 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 7 Mar 2025 10:44:07 +0000 Subject: [PATCH 3/3] Change `duration` to `lifetime` and add key check --- home/widgets.py | 4 ++-- start.ipynb | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/home/widgets.py b/home/widgets.py index 3e8ca20..c7fe5ab 100644 --- a/home/widgets.py +++ b/home/widgets.py @@ -197,8 +197,8 @@ class CountDownWidget(ipw.VBox): TEMPLATE = "

Time remaining: {countdown}

" FINAL_WARNING = "

AiiDAlab container shutdown imminent - please save your work!

" - def __init__(self, duration: str, **kwargs): - self.reference_time = datetime.strptime(duration, "%H:%M:%S") + def __init__(self, lifetime: str, **kwargs): + self.reference_time = datetime.strptime(lifetime, "%H:%M:%S") self.countdown = ipw.HTML() diff --git a/start.ipynb b/start.ipynb index 211bdb4..ccd6207 100644 --- a/start.ipynb +++ b/start.ipynb @@ -60,10 +60,11 @@ "# config file. This will initiate the countdown widget.\n", "demo_server_config_file = Path.home() / \".aiidalab\" / \"demo-server-config.yml\"\n", "if demo_server_config_file.exists():\n", - " demo_server_config = safe_load(demo_server_config_file.read_text())\n", - " countdown = CountDownWidget(duration=demo_server_config.get(\"duration\", \"12:00:00\"))\n", - " countdown.start()\n", - " display(countdown)" + " demo_server_config: dict = safe_load(demo_server_config_file.read_text()) # type: ignore\n", + " if \"lifetime\" in demo_server_config:\n", + " countdown = CountDownWidget(lifetime=demo_server_config[\"lifetime\"])\n", + " countdown.start()\n", + " display(countdown)" ] }, {