diff --git a/.github/scripts/airtableops.py b/.github/scripts/airtableops.py index ac8799c0..783b7577 100644 --- a/.github/scripts/airtableops.py +++ b/.github/scripts/airtableops.py @@ -249,7 +249,7 @@ def insert_metadata_to_airtable(model, contributor, api_key): if r.status_code == 200: text = r.content data = yaml.safe_load(text) - + airtable_data = {} airtable_data["Identifier"] = model airtable_data["Slug"] = data["Slug"] @@ -286,9 +286,15 @@ def update_readme_from_airtable(repo, path): subparsers = parser.add_subparsers(dest="command") # Main commands - airtable_insert = subparsers.add_parser("airtable-insert", help="Insert metadata to AirTable") - airtable_update = subparsers.add_parser("airtable-update", help="Update metadata to AirTable") - readme_update = subparsers.add_parser("readme-update", help="Update README from AirTable") + airtable_insert = subparsers.add_parser( + "airtable-insert", help="Insert metadata to AirTable" + ) + airtable_update = subparsers.add_parser( + "airtable-update", help="Update metadata to AirTable" + ) + readme_update = subparsers.add_parser( + "readme-update", help="Update README from AirTable" + ) # Options for airtable-insert airtable_insert.add_argument("--model", type=str, required=True) @@ -310,7 +316,7 @@ def update_readme_from_airtable(repo, path): if args.command == "airtable-insert": print("Inserting metadata to AirTable") insert_metadata_to_airtable(args.model, args.contributor, args.api_key) - + elif args.command == "airtable-update": print("Updating metadata to AirTable") # update_metadata_to_airtable(args.user, args.repo, args.branch, args.api_key) @@ -322,4 +328,4 @@ def update_readme_from_airtable(repo, path): else: print("Invalid command") parser.print_help() - exit(1) \ No newline at end of file + exit(1) diff --git a/ersilia/cli/commands/catalog.py b/ersilia/cli/commands/catalog.py index d239f5c7..8605acd9 100644 --- a/ersilia/cli/commands/catalog.py +++ b/ersilia/cli/commands/catalog.py @@ -86,7 +86,7 @@ def catalog( except Exception as e: click.echo(click.style(f"Error fetching model metadata: {e}", fg="red")) return - + # The idea here is to deter the user from running ersilia catalog --local --hub if local and hub: click.echo( @@ -96,7 +96,7 @@ def catalog( err=True, ) return - + mc = ModelCatalog() mc.only_identifier = False if more else True @@ -104,28 +104,28 @@ def catalog( if browser: mc.airtable() return - + catalog_table = mc.hub() - else: # This will work even if the user doesn't explicitly specify the --local flag + else: # This will work even if the user doesn't explicitly specify the --local flag if browser: click.echo( click.style( "Error: Cannot show local models in the browser.\nPlease use the --hub option to see models in the browser.", - fg="red" + fg="red", ) ) return catalog_table = mc.local() if not catalog_table.data: click.echo( - click.style( - "No local models available. Please fetch a model by running 'ersilia fetch' command", - fg="red", + click.style( + "No local models available. Please fetch a model by running 'ersilia fetch' command", + fg="red", ) ) return - + if file_name is None: catalog = catalog_table.as_table() if as_table else catalog_table.as_json() else: diff --git a/ersilia/cli/commands/example.py b/ersilia/cli/commands/example.py index 3c56dd79..fbfec6c9 100644 --- a/ersilia/cli/commands/example.py +++ b/ersilia/cli/commands/example.py @@ -14,8 +14,8 @@ def example_cmd(): # Example usage: ersilia example {MODEL} -n 10 [--file_name {FILE_NAME} --simple/--complete] @ersilia_cli.group( - short_help="Generate sample of Ersilia models or model inputs", - help="""This command can sample both ersilia models, or inputs for a given or currently running model.\n + short_help="Generate sample of Ersilia models or model inputs", + help="""This command can sample both ersilia models, or inputs for a given or currently running model.\n For the model input, the number of examples can be specified, as well as a file name.\n Simple inputs only contain the essential information, while complete inputs contain key and other fields, potentially.\n For ersilia models, only model identifiers are returned for a given sample size. @@ -24,7 +24,6 @@ def example_cmd(): def example(): pass - @example.command() @click.argument("model", required=False, default=None, type=click.STRING) @click.option("--n_samples", "-n", default=5, type=click.INT) @@ -39,7 +38,10 @@ def inputs(model, n_samples, file_name, simple, predefined): model_id = session.current_model_id() if model_id is None: click.echo( - click.style("Error: No model id given and no model found running in this shell.", fg="red"), + click.style( + "Error: No model id given and no model found running in this shell.", + fg="red", + ), err=True, ) return @@ -54,7 +56,6 @@ def inputs(model, n_samples, file_name, simple, predefined): else: eg.example(n_samples, file_name, simple, try_predefined=predefined) - @example.command() @click.option("--n_samples", "-n", default=5, type=click.INT) @click.option("--file_name", "-f", default=None, type=click.STRING) @@ -64,4 +65,4 @@ def models(n_samples, file_name): if file_name is None: echo(json.dumps(sampler.sample(n_samples), indent=4)) else: - sampler.sample(n_samples, file_name) \ No newline at end of file + sampler.sample(n_samples, file_name) diff --git a/ersilia/hub/content/catalog.py b/ersilia/hub/content/catalog.py index 8e3302ec..45db5381 100644 --- a/ersilia/hub/content/catalog.py +++ b/ersilia/hub/content/catalog.py @@ -236,8 +236,7 @@ def _get_service_class(self, card): def airtable(self): """List models available in AirTable Ersilia Model Hub base""" if webbrowser: - webbrowser.open("https://airtable.com/shrUcrUnd7jB9ChZV") #TODO Hardcoded - + webbrowser.open("https://airtable.com/shrUcrUnd7jB9ChZV") # TODO Hardcoded def hub(self): """List models available in Ersilia model hub from the S3 JSON""" @@ -249,7 +248,7 @@ def hub(self): status = self._get_status(model) if status == "In Progress": continue - + identifier = self._get_item(model, "identifier") if self.only_identifier: R += [[identifier]] @@ -258,9 +257,11 @@ def hub(self): title = self._get_title(model) R += [[identifier, slug, title]] - columns = ["Identifier"] if self.only_identifier else ["Identifier", "Slug", "Title"] + columns = ( + ["Identifier"] if self.only_identifier else ["Identifier", "Slug", "Title"] + ) return CatalogTable(R, columns=columns) - + def local(self): """List models available locally""" mc = ModelCard() diff --git a/ersilia/io/input.py b/ersilia/io/input.py index 389548bf..fe857ba1 100644 --- a/ersilia/io/input.py +++ b/ersilia/io/input.py @@ -273,7 +273,7 @@ def example(self, n_samples, file_name, simple, try_predefined): if try_predefined is True and file_name is not None: self.logger.debug("Trying with predefined input") predefined_available = self.predefined_example(file_name) - + if predefined_available: with open(file_name, "r") as f: return f.read() diff --git a/ersilia/serve/standard_api.py b/ersilia/serve/standard_api.py index 5762deaf..41d30c5d 100644 --- a/ersilia/serve/standard_api.py +++ b/ersilia/serve/standard_api.py @@ -64,7 +64,6 @@ def _read_information_file(self): with open(os.path.join(self.path, INFORMATION_FILE), "r") as f: info = json.load(f) return info - except FileNotFoundError: self.logger.debug( f"Error: File '{INFORMATION_FILE}' not found in the path '{self.path}'" @@ -295,6 +294,7 @@ def serialize_to_csv(self, input_data, result, output_data): result[idx] = item[list(item.keys())[0]] assert len(input_data) == len(result) + with open(output_data, "w") as f: writer = csv.writer(f) writer.writerow(self.header) diff --git a/ersilia/utils/exceptions_utils/card_exceptions.py b/ersilia/utils/exceptions_utils/card_exceptions.py index 98270795..e9a99052 100644 --- a/ersilia/utils/exceptions_utils/card_exceptions.py +++ b/ersilia/utils/exceptions_utils/card_exceptions.py @@ -21,6 +21,7 @@ def __init__(self): self.hints = "" ErsiliaError.__init__(self, self.message, self.hints) + # TODO Unused - remove class BaseInformationError(ErsiliaError): def __init__(self): diff --git a/ersilia/utils/exceptions_utils/exceptions.py b/ersilia/utils/exceptions_utils/exceptions.py index d71faaf0..70521455 100644 --- a/ersilia/utils/exceptions_utils/exceptions.py +++ b/ersilia/utils/exceptions_utils/exceptions.py @@ -93,7 +93,9 @@ def run_from_terminal(self): output_file = os.path.join(framework_dir, "example_output.csv") tmp_folder = make_temp_dir(prefix="ersilia-") log_file = os.path.join(tmp_folder, "terminal.log") - run_command("ersilia example inputs {0} -n 3 -f {1}".format(self.model_id, input_file)) + run_command( + "ersilia example inputs {0} -n 3 -f {1}".format(self.model_id, input_file) + ) cmd = "bash {0} {1} {2} {3} 2>&1 | tee -a {4}".format( exec_file, framework_dir, input_file, output_file, log_file ) diff --git a/ersilia/utils/exceptions_utils/hubdata_exceptions.py b/ersilia/utils/exceptions_utils/hubdata_exceptions.py index ae2b9025..f96903c8 100644 --- a/ersilia/utils/exceptions_utils/hubdata_exceptions.py +++ b/ersilia/utils/exceptions_utils/hubdata_exceptions.py @@ -1,5 +1,6 @@ from .exceptions import ErsiliaError + # Note: Not really used anywhere right now except in the sanitize class class InvalidUrlInAirtableError(ErsiliaError): def __init__(self, url): diff --git a/test/cli/test_close.py b/test/cli/test_close.py index 67ef4c3a..35c2dc50 100644 --- a/test/cli/test_close.py +++ b/test/cli/test_close.py @@ -17,10 +17,11 @@ def mock_close(): @pytest.fixture def mock_session(): - with patch.object(Session, "current_model_id", return_value=MODEL_ID), patch.object( - Session, "current_service_class", return_value="pulled_docker" - ), patch.object(Session, "tracking_status", return_value=False), patch.object( - Session, "current_output_source", return_value="LOCAL_ONLY" + with ( + patch.object(Session, "current_model_id", return_value=MODEL_ID), + patch.object(Session, "current_service_class", return_value="pulled_docker"), + patch.object(Session, "tracking_status", return_value=False), + patch.object(Session, "current_output_source", return_value="LOCAL_ONLY"), ): yield diff --git a/test/cli/test_run.py b/test/cli/test_run.py index c2d3f587..e5e8b82f 100644 --- a/test/cli/test_run.py +++ b/test/cli/test_run.py @@ -118,10 +118,11 @@ def mock_post_side_effect(input, output, output_source): @pytest.fixture def mock_session(compound_csv): - with patch.object(Session, "current_model_id", return_value=MODEL_ID), patch.object( - Session, "current_service_class", return_value="pulled_docker" - ), patch.object(Session, "tracking_status", return_value=False), patch.object( - Session, "current_output_source", return_value="LOCAL_ONLY" + with ( + patch.object(Session, "current_model_id", return_value=MODEL_ID), + patch.object(Session, "current_service_class", return_value="pulled_docker"), + patch.object(Session, "tracking_status", return_value=False), + patch.object(Session, "current_output_source", return_value="LOCAL_ONLY"), ): yield diff --git a/test/playground/runner.py b/test/playground/runner.py new file mode 100644 index 00000000..6e2c0b43 --- /dev/null +++ b/test/playground/runner.py @@ -0,0 +1,68 @@ +import subprocess +import yaml +from pathlib import Path + + +class NoxSession: + def __init__(self, name): + self.name = name + + def execute(self, noxfile): + try: + subprocess.run( + ["nox", "-f", noxfile, "-s", self.name], + check=True, + ) + print(f"Session '{self.name}' executed successfully.") + except subprocess.CalledProcessError as e: + print(f"Error executing session '{self.name}': {e}") + + +class NoxRunner: + def __init__(self, config_path="config.yml", noxfile="noxfile.py"): + self.original_dir = Path.cwd() + self.config_path = Path(config_path) + self.noxfile = noxfile + self.config = yaml.safe_load(self.config_path.read_text()) + self.nox_command = "nox" + self.queue = [] + + def update_yaml_values(self, new_values: dict): + existing_config = yaml.safe_load(self.config_path.read_text()) + existing_config.update(new_values) + self.config_path.write_text(yaml.dump(existing_config)) + + def get_python_version(self): + return self.config.get("python_version", "3.10.10") + + def add_session(self, session_name): + self.queue.append(NoxSession(session_name)) + + def execute_all(self): + for session in self.queue: + session.execute(self.noxfile) + self.queue.clear() + + def clear_queue(self): + self.queue.clear() + + def setup(self): + self.add_session("setup") + + def test_from_github(self): + self.add_session("test_from_github") + + def test_from_dockerhub(self): + self.add_session("test_from_dockerhub") + + def test_auto_fetcher_decider(self): + self.add_session("test_auto_fetcher_decider") + + def test_fetch_multiple_models(self): + self.add_session("test_fetch_multiple_models") + + def test_serve_multiple_models(self): + self.add_session("test_serve_multiple_models") + + def test_conventional_run(self): + self.add_session("test_conventional_run") diff --git a/test/test_models.py b/test/test_models.py index d898f799..630e562b 100644 --- a/test/test_models.py +++ b/test/test_models.py @@ -29,12 +29,11 @@ def mock_set_apis(): @pytest.fixture def mock_session(): - with patch.object( - Session, "current_model_id", return_value=MODELS[1] - ), patch.object( - Session, "current_service_class", return_value="docker" - ), patch.object(Session, "tracking_status", return_value=False), patch.object( - Session, "current_output_source", return_value="LOCAL_ONLY" + with ( + patch.object(Session, "current_model_id", return_value=MODELS[1]), + patch.object(Session, "current_service_class", return_value="docker"), + patch.object(Session, "tracking_status", return_value=False), + patch.object(Session, "current_output_source", return_value="LOCAL_ONLY"), ): yield