From 96eed8b15fcef4c4ab49cef25dcaf6972dead3af Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 16:04:18 -0500 Subject: [PATCH 01/22] allow for test alert sub --- snews_pt/snews_sub.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/snews_pt/snews_sub.py b/snews_pt/snews_sub.py index ac2b082..40a2b7d 100644 --- a/snews_pt/snews_sub.py +++ b/snews_pt/snews_sub.py @@ -83,6 +83,7 @@ class Subscriber: def __init__(self, env_path=None, firedrill_mode=True): snews_pt_utils.set_env(env_path) self.alert_topic = os.getenv("ALERT_TOPIC") + self.connection_test_topic = os.getenv("CONNECTION_TEST_TOPIC") if firedrill_mode: self.alert_topic = os.getenv("FIREDRILL_ALERT_TOPIC") @@ -90,7 +91,7 @@ def __init__(self, env_path=None, firedrill_mode=True): self.default_output = os.path.join(os.getcwd(), os.getenv("ALERT_OUTPUT")) - def subscribe(self, outputfolder=None, auth=True): + def subscribe(self, outputfolder=None, auth=True, is_test=False): """ Subscribe and listen to a given topic Parameters @@ -100,6 +101,7 @@ def subscribe(self, outputfolder=None, auth=True): auth: A `bool` or :class:`Auth ` instance. Defaults to loading from :meth:`auth.load_auth ` if set to True. To disable authentication, set to False. + is_test: bool if True overwrites the subscribed topic with CONNECTION_TEST_TOPIC """ outputfolder = outputfolder or self.default_output @@ -109,8 +111,9 @@ def subscribe(self, outputfolder=None, auth=True): # Initiate hop_stream stream = Stream(until_eos=False, auth=auth) + TOPIC = self.connection_test_topic if is_test else self.alert_topic try: - with stream.open(self.alert_topic, "r") as s: + with stream.open(TOPIC, "r") as s: for message in s: # Access message dictionary from JSOBlob message = message.content @@ -122,7 +125,7 @@ def subscribe(self, outputfolder=None, auth=True): click.secho('Done', fg='green') - def subscribe_and_redirect_alert(self, outputfolder=None, auth=True, _display=True, _return='file'): + def subscribe_and_redirect_alert(self, outputfolder=None, auth=True, _display=True, _return='file', is_test=False): """ subscribe generator """ outputfolder = outputfolder or self.default_output @@ -130,10 +133,11 @@ def subscribe_and_redirect_alert(self, outputfolder=None, auth=True, _display=Tr click.style(f'ALERT', bg='red', bold=True) + '\nBroker:' + click.style(f'{ self.alert_topic}', bg='green')) + TOPIC = self.connection_test_topic if is_test else self.alert_topic # Initiate hop_stream stream = Stream(until_eos=False, auth=auth) try: - with stream.open(self.alert_topic, "r") as s: + with stream.open(TOPIC, "r") as s: for message in s: # Access message dictionary from JSONBlobg message = message.content From 7e7b8b78dd94dae249c12005495820a763d8f4be Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 16:07:37 -0500 Subject: [PATCH 02/22] allow CLI too --- snews_pt/__main__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/snews_pt/__main__.py b/snews_pt/__main__.py index 8db5156..20b7c8e 100644 --- a/snews_pt/__main__.py +++ b/snews_pt/__main__.py @@ -77,8 +77,9 @@ def heartbeat(ctx, status, time, firedrill): @click.option('--plugin', '-p', type=str, default="None") @click.option('--outputfolder', '-o', type=str, default="None") @click.option('--firedrill/--no-firedrill', default=True, show_default='True', help='Whether to use firedrill brokers or default ones') +@click.option('--test/--no-test', default=False, show_default='False', help='If True subscribe to test topic') @click.pass_context -def subscribe(ctx, plugin, outputfolder, firedrill): +def subscribe(ctx, plugin, outputfolder, firedrill, test): """ Subscribe to Alert topic Optionally, `plugin` script can be passed The message content as a single dictionary will be passed to @@ -91,10 +92,10 @@ def subscribe(ctx, plugin, outputfolder, firedrill): try: if plugin != "None": print(f"Redirecting output to {plugin}") - for saved_json in sub.subscribe_and_redirect_alert(outputfolder=outputfolder): + for saved_json in sub.subscribe_and_redirect_alert(outputfolder=outputfolder, is_test=test): os.system(f"python {plugin} {saved_json}") else: - sub.subscribe(outputfolder=outputfolder) + sub.subscribe(outputfolder=outputfolder, is_test=test) except KeyboardInterrupt: pass From 678c2d7b8db72d527ec14ac00913e435803e7939 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 16:36:00 -0500 Subject: [PATCH 03/22] updates for tests --- snews_pt/__main__.py | 11 +++++++---- snews_pt/auxiliary/try_scenarios.py | 14 +++++++------- snews_pt/remote_commands.py | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/snews_pt/__main__.py b/snews_pt/__main__.py index 20b7c8e..7d1bfc4 100644 --- a/snews_pt/__main__.py +++ b/snews_pt/__main__.py @@ -144,12 +144,13 @@ def message_schema(ctx, requested_tier): @main.command() @click.option('--firedrill/--no-firedrill', default=True, show_default='True', help='Whether to use firedrill brokers or default ones') -def run_scenarios(firedrill): +@click.option('--test/--no-test', default=True, show_default='True', help='If False sends them to main topic!') +def run_scenarios(firedrill, test): """ """ base = os.path.dirname(os.path.realpath(__file__)) path = os.path.join(base, 'auxiliary/try_scenarios.py') - os.system(f'python3 {path} {firedrill}') + os.system(f'python3 {path} {firedrill} {test}') @main.command() @click.option('--name', '-n', default="TEST", show_default='TEST', help='Set the detectors name') @@ -193,15 +194,17 @@ def write_hb_logs(ctx, firedrill): @main.command() @click.option('--firedrill/--no-firedrill', default=True, show_default='True', help='Whether to use firedrill brokers or default ones') +@click.option('--test/--no-test', default=True, show_default='True', help='If True cleans the test cache') @click.pass_context -def reset_cache(ctx, firedrill): +def reset_cache(ctx, firedrill, test): """ REQUIRES AUTHORIZATION If authorized, drop the current cache at the server """ from .remote_commands import reset_cache reset_cache(detector_name=ctx.obj['DETECTOR_NAME'], admin_pass=ctx.obj['USER_PASS'], - firedrill=firedrill) + firedrill=firedrill, + is_test=test) @main.command() diff --git a/snews_pt/auxiliary/try_scenarios.py b/snews_pt/auxiliary/try_scenarios.py index 2d5428e..702fb6e 100644 --- a/snews_pt/auxiliary/try_scenarios.py +++ b/snews_pt/auxiliary/try_scenarios.py @@ -2,7 +2,10 @@ import json, click, time, sys from os import path as osp from snews_pt.messages import SNEWSMessageBuilder, Publisher +from snews_pt.remote_commands import reset_cache + fd_mode = True if sys.argv[1].lower() == "true" else False +is_test = True if sys.argv[2].lower() == "true" else False with open(osp.join(osp.dirname(__file__), "scenarios.json")) as json_file: data = json.load(json_file) @@ -24,11 +27,8 @@ click.secho("Terminating.") sys.exit() elif scenario=="restart cache": - with Publisher(firedrill_mode=fd_mode, verbose=False) as pub: - #passw = os.getenv("ADMIN_PASS", "NO_AUTH") # need a better test-broker / test-cache - pub.send([{'_id': '0_hard-reset_', 'pass': 'very1secret2password', 'detector_name':'XENONnT', - 'meta':{}}]) - print('> Cache cleaned\n') + reset_cache(firedrill=fd_mode, is_test=is_test) + print('> Cache cleaned\n') else: click.secho(f"\n>>> Testing {scenario}", fg='yellow', bold=True) messages = data[scenario] @@ -38,8 +38,8 @@ time.sleep(1) # clear cache after each scenario with Publisher(firedrill_mode=fd_mode, verbose=False) as pub: - pub.send([{'_id': '0_hard-reset_', 'pass':'very1secret2password', 'detector_name':'XENONnT', - 'meta':{}}]) + + reset_cache(firedrill=fd_mode, is_test=is_test) print('> Cache cleaned\n') except KeyboardInterrupt: break diff --git a/snews_pt/remote_commands.py b/snews_pt/remote_commands.py index 49f57aa..0999027 100644 --- a/snews_pt/remote_commands.py +++ b/snews_pt/remote_commands.py @@ -107,7 +107,7 @@ def write_hb_logs(detector_name=None, admin_pass=None, firedrill=True): fg='blue', bold=True) -def reset_cache(detector_name=None, admin_pass=None, firedrill=True): +def reset_cache(detector_name=None, admin_pass=None, firedrill=True, is_test=True): """ If authorized, drop the current cache at the server Parameters @@ -125,7 +125,7 @@ def reset_cache(detector_name=None, admin_pass=None, firedrill=True): message = {'_id': '0_hard-reset', 'pass': passw, 'detector_name':detector_name, - 'meta':{}} + 'meta':{'is_test':is_test}} topic = os.getenv("FIREDRILL_OBSERVATION_TOPIC") if firedrill else os.getenv("OBSERVATION_TOPIC") pubstream = Stream(until_eos=True, auth=True) From 703dd836757a554bb6cbc165bb92929e3805a39c Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 17:13:18 -0500 Subject: [PATCH 04/22] debug format checker --- snews_pt/auxiliary/try_scenarios.py | 1 - snews_pt/snews_format_checker.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/snews_pt/auxiliary/try_scenarios.py b/snews_pt/auxiliary/try_scenarios.py index 702fb6e..c55c67f 100644 --- a/snews_pt/auxiliary/try_scenarios.py +++ b/snews_pt/auxiliary/try_scenarios.py @@ -38,7 +38,6 @@ time.sleep(1) # clear cache after each scenario with Publisher(firedrill_mode=fd_mode, verbose=False) as pub: - reset_cache(firedrill=fd_mode, is_test=is_test) print('> Cache cleaned\n') except KeyboardInterrupt: diff --git a/snews_pt/snews_format_checker.py b/snews_pt/snews_format_checker.py index 5cb5cb5..209a06a 100644 --- a/snews_pt/snews_format_checker.py +++ b/snews_pt/snews_format_checker.py @@ -69,6 +69,7 @@ def __init__(self, message, log=None): self.log = log or log_default self.bypass = False # bypass if retraction, or remote command (bypasses all time checks!) self.is_test = self.check_if_test() # if True, don't check if times are reasonable (still checks format!) + print(type(self.is_test), self.is_test) def __call__(self, *args, **kwargs): msg_as_json = json.dumps(self.message, sort_keys=True, indent=4) From a8692142e1ca50c34eefd17cdb3190581c12b835 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 17:20:57 -0500 Subject: [PATCH 05/22] revert back debugging --- snews_pt/snews_format_checker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/snews_pt/snews_format_checker.py b/snews_pt/snews_format_checker.py index 209a06a..5cb5cb5 100644 --- a/snews_pt/snews_format_checker.py +++ b/snews_pt/snews_format_checker.py @@ -69,7 +69,6 @@ def __init__(self, message, log=None): self.log = log or log_default self.bypass = False # bypass if retraction, or remote command (bypasses all time checks!) self.is_test = self.check_if_test() # if True, don't check if times are reasonable (still checks format!) - print(type(self.is_test), self.is_test) def __call__(self, *args, **kwargs): msg_as_json = json.dumps(self.message, sort_keys=True, indent=4) From 5d371763eb58c18950ae1ec70b3976c8e0f1c261 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 17:40:01 -0500 Subject: [PATCH 06/22] try tmp fix on scenarios --- snews_pt/auxiliary/scenarios.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/snews_pt/auxiliary/scenarios.json b/snews_pt/auxiliary/scenarios.json index aae959e..25d1f60 100644 --- a/snews_pt/auxiliary/scenarios.json +++ b/snews_pt/auxiliary/scenarios.json @@ -4,13 +4,21 @@ "detector_name": "XENONnT", "neutrino_time": "2030-01-01T12:34:45.678999", "p_val": 0.98, - "is_test": true + "is_test": true, + "meta": + { + "is_test": true + } }, { "detector_name": "DS-20K", "neutrino_time": "2030-01-01T12:34:47.678999", "p_val": 0.98, - "is_test": true + "is_test": true, + "meta": + { + "is_test": true + } } ], "exact 10s coincidence": [ From cf47b2b2eed96a69dc7fa1c4a583b4c1153051fb Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 17:41:06 -0500 Subject: [PATCH 07/22] revert scenarios --- snews_pt/auxiliary/scenarios.json | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/snews_pt/auxiliary/scenarios.json b/snews_pt/auxiliary/scenarios.json index 25d1f60..aae959e 100644 --- a/snews_pt/auxiliary/scenarios.json +++ b/snews_pt/auxiliary/scenarios.json @@ -4,21 +4,13 @@ "detector_name": "XENONnT", "neutrino_time": "2030-01-01T12:34:45.678999", "p_val": 0.98, - "is_test": true, - "meta": - { - "is_test": true - } + "is_test": true }, { "detector_name": "DS-20K", "neutrino_time": "2030-01-01T12:34:47.678999", "p_val": 0.98, - "is_test": true, - "meta": - { - "is_test": true - } + "is_test": true } ], "exact 10s coincidence": [ From 35a59612e41d0d914e799d7e3d9dfa4ee8832690 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Thu, 15 Aug 2024 17:50:00 -0500 Subject: [PATCH 08/22] minor --- snews_pt/remote_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/snews_pt/remote_commands.py b/snews_pt/remote_commands.py index 0999027..edd07f9 100644 --- a/snews_pt/remote_commands.py +++ b/snews_pt/remote_commands.py @@ -125,7 +125,8 @@ def reset_cache(detector_name=None, admin_pass=None, firedrill=True, is_test=Tru message = {'_id': '0_hard-reset', 'pass': passw, 'detector_name':detector_name, - 'meta':{'is_test':is_test}} + 'is_test': is_test, + 'meta':{}} topic = os.getenv("FIREDRILL_OBSERVATION_TOPIC") if firedrill else os.getenv("OBSERVATION_TOPIC") pubstream = Stream(until_eos=True, auth=True) From f81774de3718c57a57fda6856a04ac1147dec20c Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 10:24:28 -0500 Subject: [PATCH 09/22] do we still need mongodb? --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index bcfa0a6..d3f9278 100755 --- a/setup.py +++ b/setup.py @@ -25,7 +25,6 @@ 'pytest~=8.0', 'pytest-console-scripts~=1.4', 'pytest-cov~=4.1', - 'pytest-mongodb~=2.4', 'pytest-runner~=6.0', 'virtualenv~=20.13', ], From 8a97f8bd08a243546602f7b8e443e632c8867cdc Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 10:38:37 -0500 Subject: [PATCH 10/22] update requirements --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index e0f62f5..12caf89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -click~=8.1 -hop-client~=0.9 +click==8.1.7 +hop-client==0.9 # Ensure compatibility with other dependencies inquirer~=2.8 -ipython~=8.7 -numpy~=1.26 -python-dotenv~=0.19 -setuptools~=69.0 -wheel~=0.42 +ipython==7.32.0 # Downgraded for compatibility +numpy==1.26.3 +python-dotenv==0.19.2 +setuptools==62.1.0 # Downgraded for compatibility +wheel==0.34.2 # Downgraded for compatibility From b47cf9a6982397fe634ac24038d81dcd30401f08 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 10:49:27 -0500 Subject: [PATCH 11/22] fix displayed topic --- snews_pt/snews_sub.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/snews_pt/snews_sub.py b/snews_pt/snews_sub.py index 40a2b7d..ad18dd5 100644 --- a/snews_pt/snews_sub.py +++ b/snews_pt/snews_sub.py @@ -105,13 +105,15 @@ def subscribe(self, outputfolder=None, auth=True, is_test=False): """ outputfolder = outputfolder or self.default_output + TOPIC = self.connection_test_topic if is_test else self.alert_topic + click.echo('You are subscribing to ' + click.style(f'ALERT', bg='red', bold=True) + '\nBroker:' + - click.style(f'{ self.alert_topic}', bg='green')) + click.style(f'{ TOPIC}', bg='green')) # Initiate hop_stream stream = Stream(until_eos=False, auth=auth) - TOPIC = self.connection_test_topic if is_test else self.alert_topic + try: with stream.open(TOPIC, "r") as s: for message in s: @@ -129,11 +131,11 @@ def subscribe_and_redirect_alert(self, outputfolder=None, auth=True, _display=Tr """ subscribe generator """ outputfolder = outputfolder or self.default_output + TOPIC = self.connection_test_topic if is_test else self.alert_topic click.echo('You are subscribing to ' + click.style(f'ALERT', bg='red', bold=True) + '\nBroker:' + - click.style(f'{ self.alert_topic}', bg='green')) + click.style(f'{ TOPIC}', bg='green')) - TOPIC = self.connection_test_topic if is_test else self.alert_topic # Initiate hop_stream stream = Stream(until_eos=False, auth=auth) try: From 257ba8ecc2d189b0c83049064117a1b51df434f7 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 11:03:48 -0500 Subject: [PATCH 12/22] bypass testconnection displays --- snews_pt/snews_sub.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/snews_pt/snews_sub.py b/snews_pt/snews_sub.py index ad18dd5..527d8c7 100644 --- a/snews_pt/snews_sub.py +++ b/snews_pt/snews_sub.py @@ -119,6 +119,11 @@ def subscribe(self, outputfolder=None, auth=True, is_test=False): for message in s: # Access message dictionary from JSOBlob message = message.content + try: + if message['_id'] == "0_test-connection": + continue + except: + pass # Save and display save_message(message, outputfolder) snews_pt_utils.display_gif() @@ -143,6 +148,11 @@ def subscribe_and_redirect_alert(self, outputfolder=None, auth=True, _display=Tr for message in s: # Access message dictionary from JSONBlobg message = message.content + try: + if message['_id'] == "0_test-connection": + continue + except: + pass # Save and display file = save_message(message, outputfolder, return_file=True) if _display: From ce60d2d5a21e1448011c06b031d53fcfab1705a6 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 11:09:32 -0500 Subject: [PATCH 13/22] fix testing scenarios --- snews_pt/auxiliary/try_scenarios.py | 1 + 1 file changed, 1 insertion(+) diff --git a/snews_pt/auxiliary/try_scenarios.py b/snews_pt/auxiliary/try_scenarios.py index c55c67f..8d6b2ed 100644 --- a/snews_pt/auxiliary/try_scenarios.py +++ b/snews_pt/auxiliary/try_scenarios.py @@ -33,6 +33,7 @@ click.secho(f"\n>>> Testing {scenario}", fg='yellow', bold=True) messages = data[scenario] for msg in messages: # send one by one and sleep in between + msg['is_test'] = is_test print(msg, "\n\n") SNEWSMessageBuilder(**msg).send_messages(firedrill_mode=fd_mode) time.sleep(1) From 371d77d72fb6e0d127c96314080b57894fb19170 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 11:26:22 -0500 Subject: [PATCH 14/22] scenarios are always test --- snews_pt/auxiliary/try_scenarios.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/snews_pt/auxiliary/try_scenarios.py b/snews_pt/auxiliary/try_scenarios.py index 8d6b2ed..e5d555b 100644 --- a/snews_pt/auxiliary/try_scenarios.py +++ b/snews_pt/auxiliary/try_scenarios.py @@ -7,6 +7,11 @@ fd_mode = True if sys.argv[1].lower() == "true" else False is_test = True if sys.argv[2].lower() == "true" else False +if not is_test: + click.secho("This script is only for testing purposes, and uses past neutrino times.\n" + "We are running the scenarios in test mode", fg='red', bold=True) + is_test = True + with open(osp.join(osp.dirname(__file__), "scenarios.json")) as json_file: data = json.load(json_file) @@ -33,7 +38,6 @@ click.secho(f"\n>>> Testing {scenario}", fg='yellow', bold=True) messages = data[scenario] for msg in messages: # send one by one and sleep in between - msg['is_test'] = is_test print(msg, "\n\n") SNEWSMessageBuilder(**msg).send_messages(firedrill_mode=fd_mode) time.sleep(1) From eb60f54dc3f68b22197c8e36b013591871fb1933 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 11:39:40 -0500 Subject: [PATCH 15/22] less strict requirements --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 12caf89..02b361f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -click==8.1.7 -hop-client==0.9 # Ensure compatibility with other dependencies +click~=8.1.7 +hop-client~=0.9 # Ensure compatibility with other dependencies inquirer~=2.8 -ipython==7.32.0 # Downgraded for compatibility -numpy==1.26.3 -python-dotenv==0.19.2 -setuptools==62.1.0 # Downgraded for compatibility -wheel==0.34.2 # Downgraded for compatibility +ipython~=7.32.0 # Downgraded for compatibility +numpy~=1.26.3 +python-dotenv~=0.19.2 +setuptools~=62.1.0 # Downgraded for compatibility +wheel~=0.34.2 # Downgraded for compatibility From 3bec38a945dfdc1e264758b652f0f4a21676c6f4 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 11:45:37 -0500 Subject: [PATCH 16/22] get rid of format checker --- snews_pt/snews_format_checker.py | 274 ------------------------------- 1 file changed, 274 deletions(-) delete mode 100644 snews_pt/snews_format_checker.py diff --git a/snews_pt/snews_format_checker.py b/snews_pt/snews_format_checker.py deleted file mode 100644 index 5cb5cb5..0000000 --- a/snews_pt/snews_format_checker.py +++ /dev/null @@ -1,274 +0,0 @@ - -from datetime import datetime -try: - fromisoformat = datetime.fromisoformat -except AttributeError: - from dateutil.parser import isoparse as fromisoformat - -import os, json -from .core.logging import getLogger, log_file -import warnings -import click -import numpy as np -import re - -log_default = getLogger(__name__) - -def is_valid_iso_utc(text): - """ - Checks if a string is a valid ISO 8601 UTC datetime string, - accepting any precision up to 12 digits and an optional Z. - - Args: - text: The string to check. - - Returns: - True if the string is a valid ISO 8601 UTC datetime string, - False otherwise. - """ - pattern = r"^" \ - r"(?P\d{4})" \ - r"(?:-(?P\d{2}))" \ - r"(?:-(?P\d{2}))" \ - r"(?:[Tt ](?P\d{2})" \ - r"(?::(?P\d{2}))" \ - r"(?::(?P\d{2}))" \ - r"(?:\.(?P\d{1,12})?)?)?" \ - r"(?:Z)?" \ - r"$" - match = re.match(pattern, text) - if not match: - return False - # Check if mandatory components are present - if not all([match.group(comp) for comp in ("year", "month", "day", "hour", "minute", "second")]): - return False - # Check if precision part is within limits (max 12 digits) - if match.group("precision") and len(match.group("precision")) > 12: - return False - return True - -# Check if detector name is in registered list. -detector_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'auxiliary/detector_properties.json')) -with open(detector_file) as file: - snews_detectors = json.load(file) -snews_detectors = list(snews_detectors.keys()) - - -class SnewsFormat: - """ Class to validate message formats. It checks; - ID - Detector name - Message type - Times in the message - p values - - """ - def __init__(self, message, log=None): - self.message = message - self.message_keys = message.keys() - self.log = log or log_default - self.bypass = False # bypass if retraction, or remote command (bypasses all time checks!) - self.is_test = self.check_if_test() # if True, don't check if times are reasonable (still checks format!) - - def __call__(self, *args, **kwargs): - msg_as_json = json.dumps(self.message, sort_keys=True, indent=4) - test_str = " -TEST- " if self.is_test else " " - self.log.debug("*"*40+f" LOGS FOR THE{test_str}MESSAGE\n"+ msg_as_json) - valid = True - try: - assert self.check_id() is valid, "_id not valid" # if _id exists - assert self.check_detector() is valid, "detector_name not valid" # if detector name is known - assert self.check_message_type() is valid, "types not valid" # if valid type; check name and times - assert self.check_times() is valid, "neutrino_time not valid" # if times are ISO format and reasonable - assert self.check_pvals() is valid, "p_val not valid"# if exists, pvals are float and reasonable - self.log.info("\t> All checks are passed. Message is in SnewsFormat.") - # self.log.debug("*" * 40 + " END OF lOGS\n") - return valid - except AssertionError as ae: - self.log.error(f"\t> Following check failed: {ae}") - warnings.warn(f"\n\n Following check failed: {click.style(ae, fg='red')} " - f"\nSee the full logs {click.style(log_file, fg='blue')}", UserWarning) - self.log.debug("*" * 40 + " END OF lOGS\n") - return False - except Exception as e: - self.log.error(f"\t> Something went wrong! {e}") - warnings.warn(f"\n\n Something went wrong! \nSee the full logs {click.style(log_file, fg='blue')}", UserWarning) - self.log.debug("*" * 40 + " END OF lOGS\n") - return False - - def check_if_test(self): - """ Check if the submitted message is a test message - - Returns - ------- - True if the message contains ['meta']['is_test'] = True, else False - """ - if "is_test" in self.message_keys: - return self.message['is_test'] - - if "meta" in self.message_keys: - if "is_test" in self.message['meta'].keys(): - return self.message["meta"]["is_test"] - return False - - def check_id(self): - """ check if the id is correct - snews_pt sends messages in mongodb format - which has ti contain an _id field - """ - self.log.debug(f"\t> Checking _id ..") - if "_id" not in self.message_keys: - self.log.error(f"\t> Message without '_id' field") - return False - else: - self.log.info(f"\t> Message has an '_id' field") - idsplit = self.message['_id'].split('_') - if len(idsplit) < 2: - self.log.error(f"\t> Message '_id' has different format. Expected '#_string' got {self.message['_id']}") - return False - return True - - def check_detector(self): - """ Check if the detector name is valid - """ - self.log.debug(f"\t> Checking detector name.") - if self.bypass: - self.log.info(f"\t> Detector name check bypassed.") - return True - if 'detector_name' not in self.message_keys: - self.log.error(f'\t> Does not have required key: "detector_name"') - return False - if self.message['detector_name'] not in snews_detectors: - self.log.error(f'\t> Detector not found: {self.message["detector_name"]}') - return False - self.log.info(f'\t> Detector: {self.message["detector_name"]} valid') - return True - - def check_message_type(self): - """ We expect Tier messages, and Commands, and Tier messages require different checks. - Check what the message type is. - """ - self.log.debug(f"\t> Checking message type..") - if "hard-reset" in self.message['_id']: - self.log.debug("\t> Hard reset. Skipping format check.") - self.bypass = True - - elif "test-connection" in self.message['_id']: - self.log.debug("\t> Test Connection. Skipping format check.") - self.bypass = True - - elif "Retraction" in self.message['_id']: - self.log.debug(f"\t> Retraction Passed. Skipping format check.") - self.bypass = True - - elif "Get-Feedback" in self.message['_id']: - self.log.debug(f"\t> Get-Feedback Passed. Skipping format check.") - self.bypass = True - - elif 'Heartbeat' in self.message['_id']: - self.log.debug("\t> Heartbeat Passed. Checking time and status.") - if not self.check_detector_status(): # if detector_status does not exist, return False - self.log.error("\t> Heartbeat not valid!") - return False - self.bypass = True - - elif "display-heartbeats" in self.message['_id']: - self.log.debug(f"\t> display-heartbeat is passed. Skipping format check.") - self.bypass = True - - elif [i in self.message['_id'] for i in ['TimeTier', 'SigTier', 'CoincidenceTier']]: - self.log.debug(f"\t> Tier message Passed. Checking times.") - - else: - self.log.error(f"\t> Unknown id Passed : {self.message['_id']}.") - return False - self.log.info(f"\t> Message type : {self.message['_id']} valid.") - return True - - def check_detector_status(self): - """ if _id is for heartbeat, - check detector status field and neutrino time - """ - self.log.debug(f"\t> Checking detector status..") - if "detector_status" not in self.message_keys: - self.log.error(f"\t> Heartbeat Message but detector_status not in keys.") - return False - if self.message["detector_status"].upper() not in ["ON", "OFF"]: - self.log.error(f"\t> detector_status not ON or OFF.") - return False - self.log.info(f"\t> detector_status is valid.") - return True - - def check_times(self): - """ Check the neutrino times. Unless it is a test message, - the times should be within last 24 hours to be valid. - - """ - self.log.debug(f"\t> Checking Times..") - if self.bypass: - self.log.info(f"\t> Time checks bypassed.") - return True - - # check if neutrino times exists and in string format - if 'neutrino_time' not in self.message_keys: - self.log.error(f"\t> neutrino_time does not exist!.") - return False - if type(self.message['neutrino_time']) is not str: - self.log.error(f"\t> neutrino_time is not a string!.") - return False - self.log.info(f"\t> neutrino_time exists and is string.") - - # it exists and string, check if ISO format, and reasonable - try: - dateobj = np.datetime64(self.message['neutrino_time']) - self.log.info(f"\t> neutrino_time is ISO formattable.") - except Exception as e: - self.log.error("\t> neutrino_time does not match " - f"SNEWS 2.0 (ISO) format: '%Y-%m-%dT%H:%M:%S.%f'\n\t{e}") - return False - - # neutrino_times exists, and in str - ISO format, check dates - if self.is_test: - # for tests, exact time is irrelevant - self.log.info(f"\t> neutrino_time is checked for is_test=True, not checking time interval.") - return True - now_datetime = datetime.utcnow() - now_datetime64 = np.datetime64(now_datetime) - time_delta = dateobj- now_datetime64 - total_seconds = time_delta / np.timedelta64(1, 's') - - if total_seconds <= -172800.0: - self.log.error(f'\t> neutrino time is more than 48 hrs olds !\n') - return False - - if total_seconds > 0: - self.log.error(f'\t> neutrino time comes from the future, please stop breaking causality.') - return False - # if all passed, return True - self.log.info(f"\t> neutrino_time passed all time checks.") - return True - - def check_pvals(self): - """ check if `p_val` exists, and valid - - """ - self.log.debug(f"\t> Checking p_val..") - if 'p_val' in self.message_keys: - if self.message['p_val'] is None: - self.log.info(f"\t> p_val is defaulted to None.") - return True # does not exist/default to None - - pval = self.message['p_val'] - pval_type = type(pval) - - if pval_type is not float: - self.log.error(f'\t> p value needs to be a float type, type given: {pval_type}.') - return False - if pval >= 1.0 or pval <= 0: - self.log.error(f'\t> {pval} is not a valid p value !') - return False - self.log.info(f"\t> p_val={pval} is valid!") - self.log.info(f"\t> no p_val in message keys.") - # still return True, it is not a must - return True - From 54390600bb075a225ac6624ff833c82f09734db0 Mon Sep 17 00:00:00 2001 From: Segev BenZvi Date: Fri, 16 Aug 2024 11:56:25 -0500 Subject: [PATCH 17/22] Simplify hop-client install in macOS buildbot. --- .github/workflows/mac12-py311-312.yml | 12 +- AUTHORS | 14 ++ ChangeLog | 190 ++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 6 deletions(-) diff --git a/.github/workflows/mac12-py311-312.yml b/.github/workflows/mac12-py311-312.yml index 53a1ea4..6ccc25f 100644 --- a/.github/workflows/mac12-py311-312.yml +++ b/.github/workflows/mac12-py311-312.yml @@ -39,12 +39,12 @@ jobs: - name: Install hop-client run: | - pip install setuptools wheel - wget https://files.pythonhosted.org/packages/64/d1/108cea042128c7ea7790e15e12e3e5ed595bfcf4b051c34fe1064924beba/hop-client-0.9.0.tar.gz - tar -xzf hop-client-0.9.0.tar.gz - cd hop-client-0.9.0 - python setup.py install - cd $GITHUB_WORKSPACE + pip install setuptools wheel hop-client=0.9.0 +# wget https://files.pythonhosted.org/packages/64/d1/108cea042128c7ea7790e15e12e3e5ed595bfcf4b051c34fe1064924beba/hop-client-0.9.0.tar.gz +# tar -xzf hop-client-0.9.0.tar.gz +# cd hop-client-0.9.0 +# python setup.py install +# cd $GITHUB_WORKSPACE - name: Install requirements run: | diff --git a/AUTHORS b/AUTHORS index a5f7063..8e7f3b4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,17 @@ +Alec Habig +Joe Smolsky <76176742+joesmolsky@users.noreply.github.com> +Justin Vasel +KaraMelih Melih +Melih Kara +Ricardo Peres +Sebastian Torres-Lara <53165039+Storreslara@users.noreply.github.com> Sebastian Torres-Lara +Segev BenZvi +Segev BenZvi +Segev BenZvi +Segev BenZvi +Storreslara <53165039+Storreslara@users.noreply.github.com> +joesmolsky <76176742+joesmolsky@users.noreply.github.com> joesmolsky +storreslara diff --git a/ChangeLog b/ChangeLog index 261e6bb..e5ce944 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,196 @@ CHANGES ======= +* get rid of format checker +* less strict requirements +* scenarios are always test +* fix testing scenarios +* bypass testconnection displays +* fix displayed topic +* update requirements +* do we still need mongodb? +* minor +* revert scenarios +* try tmp fix on scenarios +* revert back debugging +* debug format checker +* updates for tests +* allow CLI too +* allow for test alert sub +* pval is not a required arg, don't crash +* Add logos for dark and light backgrounds +* clean up +* Add an FAQ page +* minor +* update subscribe +* updated publish docs +* add badges +* remove old image +* add a badge for arxiv +* add a link to sub and pub +* adjust the restructured locs +* move to a diff dir +* improve quickstart +* minor change +* add hopskotch page to index +* clean and restructure +* fix macOS actions +* looking for hop auth file +* test location change +* test location change +* test location change +* double check the auth.toml file +* just test snews pt +* delete test\_subscribe +* run actions on pull request +* update actions +* update actions +* Add files via upload +* Update README.md + +v1.3.4 +------ + +* clean a line +* add neutrino times for time tier +* placeholder for prod topic +* Fix typo on setup.py +* adjust floating implementation +* adjust tests +* revert firedrill fix +* move firedrill location +* fix machine time overwrite +* time tier accepts floats +* CLI fix for publish and hb +* Modify github testing workflow configs +* Upgrade and clean up dependencies +* add todo for timetier +* Fix stupid typo in YAML file +* Modernize readthedocs version +* Point to documentation requirements +* Add documentation-specific requirements +* Add default language +* update tests +* remove wrong-semicolon test +* remove pandas +* converting to numpy datetimes +* wrong spacing +* missing import +* avoid TimeTier crash +* avoid TimeTier crash +* avoid TimeTier crash +* Fixed iso format issue +* minor improvement, backw compatible +* Relaxed the setup tools, ~= -> >= +* fix tier schema display in CLI +* pvalues in sigtier validation +* bump versions in tests +* print schema also displays kwargs +* better print tier contents +* better print tier contents +* update version +* update tests +* time tier pval validation +* update notebook +* update examples +* fix detector name set reintroduce meta field add is\_test to accepted fields +* don't set the name, just get it +* legacy. is test no longer under meta key, but CS uses this script to check +* set name, fetch if name has already been changed +* check p-value range for coincidence tier + improve md repr +* adapt to new msg construction +* Remove Python 3.7 unit tests +* oopsie fix +* update notebooks +* set name echoes only when not returned +* turn json import into a classmethod +* numpy requirement +* clean-up +* test connection returns bool +* automated server connection test +* remove neutrino time from time tier fields +* fix test script +* new heartbeat test +* typo fix +* fix retraction unit test +* fix timing unit test +* fix significance unit test +* fix coincidence unit test +* fetch and append machine time +* return is\_valid true, and keep tiernames +* convert message schema display +* rename functions +* is\_valid method for Time,HB, and retr tiers +* is\_valid method for Time,HB, and retr tiers +* check detector name in base, add SigTier is valid +* is valid method for coinc tier +* irrelevant fix +* when sending avoid using other-tier keys in the meta +* add a publisher +* times in timing series must be checked +* markdown representations +* Refector message builder, enable to/from JSON +* Test SNEWSMessageBuilder for tier message lists +* Print schema. Rename validate function +* Add SNEWSMessage class hierarchy +* update docstring +* update docstring +* update docstring +* update docstring +* update docstring +* update docstring +* update docstring +* minor +* replace images +* improve docs +* improve docs +* improve docs +* improve docs +* Add link to contributions page +* Add BSD 3-clause license +* Enable message output to JSON +* add screenshots +* start changind the docs + +v1.3.3 +------ + +* fix docstring +* add doc +* add patience +* update notebook +* fix testing + +v1.3.2 +------ + +* Update snews\_sub.py +* add correct topic +* add correct topic +* add write hb logs command +* raise error if not hop0.8.0 +* add connection topic +* remove redundant whcih tier +* remove redundant whcih tier +* change requirements to hop-client v0.8.0 +* change version for hop-client 0.8.0 +* change messaging to hop-client v0.8.0 syntax only +* put JSONBlob import in try/except, add hop8 flag +* replace try/except with if isinstance(JSONBlob) +* add try, except blocks for hop-0.8.0 messages +* update requirements for new hop-client version +* write messages to stream as JSONBlob, hop>=0.6.0 +* fix test-connection +* testing fixes + +v1.3.1 +------ + +* stamp time before sending + +v1.3.0 +------ + * stamp time before sending * check if detector name valid * fix testing From 43a959517ac52a509bfd292cf497cbaf7202fdda Mon Sep 17 00:00:00 2001 From: Segev BenZvi Date: Fri, 16 Aug 2024 11:57:40 -0500 Subject: [PATCH 18/22] Fix typo in pip versioning. --- .github/workflows/mac12-py311-312.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mac12-py311-312.yml b/.github/workflows/mac12-py311-312.yml index 6ccc25f..7de9f77 100644 --- a/.github/workflows/mac12-py311-312.yml +++ b/.github/workflows/mac12-py311-312.yml @@ -39,7 +39,7 @@ jobs: - name: Install hop-client run: | - pip install setuptools wheel hop-client=0.9.0 + pip install setuptools wheel hop-client==0.9.0 # wget https://files.pythonhosted.org/packages/64/d1/108cea042128c7ea7790e15e12e3e5ed595bfcf4b051c34fe1064924beba/hop-client-0.9.0.tar.gz # tar -xzf hop-client-0.9.0.tar.gz # cd hop-client-0.9.0 From f230a37e43972592da91ceaa72c6f898445ec7b2 Mon Sep 17 00:00:00 2001 From: Segev BenZvi Date: Fri, 16 Aug 2024 12:09:29 -0500 Subject: [PATCH 19/22] Try upgrading pip. --- .github/workflows/mac12-py311-312.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mac12-py311-312.yml b/.github/workflows/mac12-py311-312.yml index 7de9f77..74aa98d 100644 --- a/.github/workflows/mac12-py311-312.yml +++ b/.github/workflows/mac12-py311-312.yml @@ -39,6 +39,7 @@ jobs: - name: Install hop-client run: | + python -m pip install --upgrade pip pip install setuptools wheel hop-client==0.9.0 # wget https://files.pythonhosted.org/packages/64/d1/108cea042128c7ea7790e15e12e3e5ed595bfcf4b051c34fe1064924beba/hop-client-0.9.0.tar.gz # tar -xzf hop-client-0.9.0.tar.gz From ea289d49c37749dfb2c478e25cc59364124047a3 Mon Sep 17 00:00:00 2001 From: KaraMelih Date: Fri, 16 Aug 2024 12:18:23 -0500 Subject: [PATCH 20/22] py311 or py312 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d3f9278..8e675f0 100755 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ ], }, - python_requires='~=3.11', + python_requires='>=3.11,<3.13', install_requires=install_requires, extras_require=extras_require, From 443564c062928751525f9d522dd09b0a547f1e21 Mon Sep 17 00:00:00 2001 From: Segev BenZvi Date: Fri, 16 Aug 2024 12:32:06 -0500 Subject: [PATCH 21/22] Delete workflows for old macOS. --- .github/workflows/mac12-py311-312.yml | 84 --------------------------- .github/workflows/mac13-py311-312.yml | 83 -------------------------- 2 files changed, 167 deletions(-) delete mode 100644 .github/workflows/mac12-py311-312.yml delete mode 100644 .github/workflows/mac13-py311-312.yml diff --git a/.github/workflows/mac12-py311-312.yml b/.github/workflows/mac12-py311-312.yml deleted file mode 100644 index 74aa98d..0000000 --- a/.github/workflows/mac12-py311-312.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Mac 12 Python 3.11-12 - -on: - push: - branches: [ main ] - - pull_request: - branches: - - main - -jobs: - build: - runs-on: macos-12 - strategy: - # Add a list of python versions we want to use for testing. - matrix: - python-version: ['3.11', '3.12'] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - pip install flake8 - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: Install librdkafka - run: brew install librdkafka - # - name: Install hop-client - # run: conda install -c conda-forge hop-client - - - name: Install hop-client - run: | - python -m pip install --upgrade pip - pip install setuptools wheel hop-client==0.9.0 -# wget https://files.pythonhosted.org/packages/64/d1/108cea042128c7ea7790e15e12e3e5ed595bfcf4b051c34fe1064924beba/hop-client-0.9.0.tar.gz -# tar -xzf hop-client-0.9.0.tar.gz -# cd hop-client-0.9.0 -# python setup.py install -# cd $GITHUB_WORKSPACE - - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install wheel - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - - shell: bash - env: - USERNAME: ${{ secrets.username }} - PASSWORD: ${{ secrets.password }} - run: | - /usr/bin/expect << HOP - spawn hop auth add - expect "Username:" - send "$USERNAME\n" - expect "Password:" - send "$PASSWORD\n" - expect "Hostname (may be empty):" - send "kafka.scimma.org\n" - expect "Token endpoint (empty if not applicable):" - send "\n" - expect eof - HOP - hop auth locate - - - name: Install snews-pt - run: pip install . - - - name: Check version - run: snews_pt --version - - - name: Run pytest - run: | - pip install pytest - pytest snews_pt diff --git a/.github/workflows/mac13-py311-312.yml b/.github/workflows/mac13-py311-312.yml deleted file mode 100644 index 11b5a81..0000000 --- a/.github/workflows/mac13-py311-312.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Mac 13 Python 3.11-12 - -on: - push: - branches: [ main ] - - pull_request: - branches: - - main - -jobs: - build: - runs-on: macos-13 - strategy: - # Add a list of python versions we want to use for testing. - matrix: - python-version: ['3.11', '3.12'] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - pip install flake8 - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: Install librdkafka - run: brew install librdkafka - # - name: Install hop-client - # run: conda install -c conda-forge hop-client - - - name: Install hop-client - run: | - pip install setuptools wheel - wget https://files.pythonhosted.org/packages/64/d1/108cea042128c7ea7790e15e12e3e5ed595bfcf4b051c34fe1064924beba/hop-client-0.9.0.tar.gz - tar -xzf hop-client-0.9.0.tar.gz - cd hop-client-0.9.0 - python setup.py install - cd $GITHUB_WORKSPACE - - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install wheel - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - - shell: bash - env: - USERNAME: ${{ secrets.username }} - PASSWORD: ${{ secrets.password }} - run: | - /usr/bin/expect << HOP - spawn hop auth add - expect "Username:" - send "$USERNAME\n" - expect "Password:" - send "$PASSWORD\n" - expect "Hostname (may be empty):" - send "kafka.scimma.org\n" - expect "Token endpoint (empty if not applicable):" - send "\n" - expect eof - HOP - hop auth locate - - - name: Install snews-pt - run: pip install . - - - name: Check version - run: snews_pt --version - - - name: Run pytest - run: | - pip install pytest - pytest snews_pt From fdd68003e8bf02ee486dc81353352305ffe3a02d Mon Sep 17 00:00:00 2001 From: Segev BenZvi Date: Fri, 16 Aug 2024 12:35:18 -0500 Subject: [PATCH 22/22] Try Python 3.11 only. --- .github/workflows/mac14-py311-312.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mac14-py311-312.yml b/.github/workflows/mac14-py311-312.yml index 7214fa6..fad793a 100644 --- a/.github/workflows/mac14-py311-312.yml +++ b/.github/workflows/mac14-py311-312.yml @@ -15,7 +15,7 @@ jobs: strategy: # Add a list of python versions we want to use for testing. matrix: - python-version: ['3.11', '3.12'] + python-version: ['3.11'] #, '3.12'] steps: - uses: actions/checkout@v4