diff --git a/.gitignore b/.gitignore index d9005f2..de40665 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +/tests/test_data/notebook1/output +/tests/test_data/notebook2/output diff --git a/README.md b/README.md index b22c952..a0e7b37 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ millrun --help │ * notebook_dir_or_file TEXT Path to a notebook file or a directory containing notebooks. │ │ [default: None] │ │ [required] │ -│ * params TEXT JSON file that contains parameters for notebook execution. Can │ +│ * notebook_params TEXT JSON file that contains parameters for notebook execution. Can │ │ either be a 'list of dict' or 'dict of list'. │ │ [default: None] │ │ [required] │ @@ -140,6 +140,38 @@ Where each notebook given to millrun will execute against each dictionary in the This format is offered as a convenience format. Internally, it is converted into "Format 1" prior to execution. +## CLI Profile execution + +As of v0.2.0, `millrun` allows the creation of a "profiles" yaml file which prevents the need for typing really long commands on the command line, especially if, for a particular project, the commands are always going to be the same. + +YAML format: + +The format basically describes the kwargs required to execute the command. + +The top level keys can be arbitrarily named but they represent one command execution. +The values underneath each top level key are the kwargs of the command. + +The only required values are `notebook_dir_or_file` and `notebook_params`. All other params are optional. + +```yaml +notebook1: # This is the name of the profile. A profile is equal to one command on the command line + notebook_dir_or_file: ./notebook1/notebook1.ipynb # Req'd + notebook_params: ./notebook1/notebook1_params.json # Req'd + output_dir: ./notebook1/output # Optional + prepend: # Optional + - name + - design + append: # Optional + - executed + +notebook2: # This profile will be executed immediately after the first profile. It's like running the command again. + notebook_dir_or_file: ./notebook2 + notebook_params: ./notebook2/notebook2_params.json + output_dir: ./notebook2/output + prepend: + - tester +``` + ## CLI parallel execution Since millrun iterates over two dimensions (each notebook and then dict of parameters in the list), there are two ways of parellelizing: @@ -153,7 +185,6 @@ However, this method becomes inefficient if you have MANY notebooks and only 1-3 If you need this use case then feel free to raise an issue and/or contribute a PR to implement it as an option for execution. - ## Troubleshooting There seems to be an un-planned-for behaviour (by me) with the parallel execution where if there is an error in the execution process, that iteration is simply skipped. I don't have any `try`/`except` in the code that causes this. diff --git a/pyproject.toml b/pyproject.toml index b7b3cbe..28435df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ requires-python = ">=3.10" dependencies = [ "papermill>=2.6.0", "rich", + "ruamel-yaml>=0.18.15", "typer>=0.16.0", ] @@ -21,5 +22,7 @@ build-backend = "flit_core.buildapi" [dependency-groups] dev = [ + "black>=25.1.0", "ipykernel>=6.29.5", + "pytest>=8.4.2", ] diff --git a/src/millrun/__init__.py b/src/millrun/__init__.py index a3c61f6..f68df02 100644 --- a/src/millrun/__init__.py +++ b/src/millrun/__init__.py @@ -1,8 +1,8 @@ """ Millrun: A Python library and CLI tool to automate the execution of notebooks -with papermill. +with papermill. """ __version__ = "0.1.1" -from .millrun import execute_run \ No newline at end of file +from .millrun import execute_run diff --git a/src/millrun/cli.py b/src/millrun/cli.py index e893247..10821d4 100644 --- a/src/millrun/cli.py +++ b/src/millrun/cli.py @@ -1,19 +1,17 @@ import json +from ruamel.yaml import YAML from typing import Optional, Any from typing_extensions import Annotated import pathlib import typer -from .millrun import execute_run +from .millrun import execute_run, execute_profile -def _parse_json(filepath: str) -> dict: - with open(filepath, 'r') as file: - return json.load(file) APP_INTRO = typer.style( -""" -AISC sections database W-section selection tool (2023-05-28) + """ +Executes a notebook or directory of notebooks using the provided bulk parameters JSON file """, fg=typer.colors.BRIGHT_YELLOW, bold=True, @@ -23,43 +21,69 @@ def _parse_json(filepath: str) -> dict: add_completion=False, no_args_is_help=True, help=APP_INTRO, - # pretty_exceptions_enable=False, - pretty_exceptions_show_locals=False + pretty_exceptions_show_locals=False, ) + @app.command( - name='run', + name="run", help="Executes a notebook or directory of notebooks using the provided bulk parameters JSON file", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def run( - notebook_dir_or_file: Annotated[str, typer.Argument( - help="Path to a notebook file or a directory containing notebooks.") - ], - params: Annotated[str, typer.Argument( - help=("JSON file that contains parameters for notebook execution. " - "Can either be a 'list of dict' or 'dict of list'."), - callback=lambda value: _parse_json(value), - ) - ], - output_dir: Annotated[Optional[str], typer.Option( - help=("Directory to place output files into. If not provided" - " the file directory will be used."), - ) + notebook_dir_or_file: Annotated[ + Optional[str], + typer.Argument( + help="Path to a notebook file or a directory containing notebooks.", + ), ] = None, - prepend: Annotated[Optional[str], typer.Option( - help=("Prepend components to use on output filename." - "Can use dict keys from 'params' which will be evaluated." - "(Comma-separated values)."), - callback=lambda x: x.split(",") if x else None - ) + notebook_params: Annotated[ + Optional[str], + typer.Argument( + help=( + "JSON file that contains parameters for notebook execution. " + "Can either be a 'list of dict' or 'dict of list'." + ), + ), ] = None, - append: Annotated[Optional[str], typer.Option( - help=("Append components to use on output filename." - "Can use dict keys from 'params' which will be evaluated." - "(Comma-separated values)."), - callback=lambda x: x.split(",") if x else None - ) + profile: Annotated[ + Optional[str], + typer.Argument( + help=( + "A millrun YAML profile file that specifies the notebook_dir_or_file and notebook_params (along with additional options) instead of providing them directly." + ), + ), + ] = None, + output_dir: Annotated[ + Optional[str], + typer.Option( + help=( + "Directory to place output files into. If not provided" + " the current working directory will be used." + ), + ), + ] = None, + prepend: Annotated[ + Optional[str], + typer.Option( + help=( + "Prepend components to use on output filename." + "Can use dict keys from 'params' which will be evaluated." + "(Comma-separated values)." + ), + callback=lambda x: x.split(",") if x else None, + ), + ] = None, + append: Annotated[ + Optional[str], + typer.Option( + help=( + "Append components to use on output filename." + "Can use dict keys from 'params' which will be evaluated." + "(Comma-separated values)." + ), + callback=lambda x: x.split(",") if x else None, + ), ] = None, recursive: bool = False, exclude_glob_pattern: Optional[str] = None, @@ -69,19 +93,27 @@ def run( output_dir = pathlib.Path(output_dir) else: output_dir = pathlib.Path.cwd() - execute_run( - notebook_dir_or_file, - params, - output_dir, - prepend, - append, - recursive, - exclude_glob_pattern, - include_glob_pattern, - use_multiprocessing=True - # **kwargs - ) + + # Automated profile execution + if profile is not None: + profile_file = pathlib.Path.cwd() / pathlib.Path(profile) + execute_profile(profile_file) + + # Typical execution + elif None not in [notebook_dir_or_file, notebook_params]: + execute_run( + notebook_dir_or_file, + notebook_params, + output_dir, + prepend, + append, + recursive, + exclude_glob_pattern, + include_glob_pattern, + use_multiprocessing=True, + # **kwargs + ) if __name__ == "__main__": - app() \ No newline at end of file + app() diff --git a/src/millrun/millrun.py b/src/millrun/millrun.py index 744ebda..7a79dc7 100644 --- a/src/millrun/millrun.py +++ b/src/millrun/millrun.py @@ -1,20 +1,59 @@ import pathlib import json +from ruamel.yaml import YAML from typing import Optional, Any import papermill as pm import functools as ft import multiprocessing from concurrent.futures import ProcessPoolExecutor from rich.progress import Progress, BarColumn, TimeRemainingColumn, TimeElapsedColumn +from rich import print +from rich.text import Text +def execute_profile(profile_path: str, selected_profiles: Optional[list[str]] = None): + """ + Executes millrun according to the arguments provided in the profile file + + The profile is a dict in the following structure: + { + "profile_name1": { + "notebook_dir_or_file": ..., # Req'd key + "notebook_params": ..., # Req'd key + "output_dir": ..., # Optional + "prepend": ..., # Optional + "append": ..., # Optional + "recursive": ..., # Optional + "exclude_glob_pattern": ..., # Optional + "include_glob_pattern": ..., # Optional + "use_multiprocessing": ..., # Optional + }, + "profile_name2": { + ... #same as above + }, + "profile_name3": { + ... # same as above + } + } + """ + profile_path = pathlib.Path(profile_path) + working_directory = profile_path.parent + + profile_data = _parse_yaml(profile_path) + for profile_name, profile_kwargs in profile_data.items(): + if selected_profiles is not None: + if profile_name in selected_profiles: + execute_run(**profile_kwargs, profile_name=profile_name, working_directory=working_directory) + else: + execute_run(**profile_kwargs, profile_name=profile_name, working_directory=working_directory) + def execute_run( notebook_dir_or_file: pathlib.Path | str, - bulk_params: list | dict, + notebook_params: list | dict | str | pathlib.Path, output_dir: Optional[pathlib.Path | str] = None, - output_prepend_components: Optional[list[str]] = None, - output_append_components: Optional[list[str]] = None, + prepend: Optional[list[str]] = None, + append: Optional[list[str]] = None, recursive: bool = False, exclude_glob_pattern: Optional[str] = None, include_glob_pattern: Optional[str] = None, @@ -23,21 +62,21 @@ def execute_run( ) -> list[pathlib.Path] | None: """ Executes the notebooks contained in the notebook_dir_or_file using the parameters in - 'bulk_params'. + 'notebook_params'. 'notebook_dir_or_file': If a directory, then will execute all of the notebooks within the directory - 'bulk_params': + 'notebook_params': Either a dict in the following format: - + { "key1": ["list", "of", "values"], # All lists of values must be same length "key2": ["list", "of", "values"], "key3": ["list", "of", "values"], ... - }, - - -or- + }, + + -or- A list in the following format: @@ -47,15 +86,20 @@ def execute_run( {"key1": "value", "key2": "value"}, ... ] + + -or- + + A str or pathlib.Path to a file + 'output_dir': The directory for all output files. If None, files will be output in the same directory as the source file. - 'output_prepend_components': A list of str representing the keys used - in 'bulk_params'. These keys will be used to retrieve the value - for the key in each iteration of 'bulk_params' and they will - be used to name the output file be prepending them to the + 'prepend': A list of str representing the keys used + in 'notebook_params'. These keys will be used to retrieve the value + for the key in each iteration of 'notebook_params' and they will + be used to name the output file be prepending them to the original filename. If a key is not found then the key will be interpreted as a str literal and will be added asis. - 'output_append_components': Same as the prepend components but + 'append': Same as the prepend components but these components will be used at the end of the original filename. 'recursive': If True, and if 'notebook_dir_or_file' is a directory, then will execute notebooks within all sub-directories. @@ -67,17 +111,19 @@ def execute_run( """ notebook_dir_or_file = pathlib.Path(notebook_dir_or_file) output_dir = pathlib.Path(output_dir) - if isinstance(bulk_params, dict): - unequal_lengths = check_unequal_value_lengths(bulk_params) - if unequal_lengths: - raise ValueError( - f"All lists in the bulk_params dict must be of equal length.\n" - f"The following keys have unequal length: {unequal_lengths}" - ) - bulk_params_list = convert_bulk_params_to_list(bulk_params) - else: - bulk_params_list = bulk_params - + if "working_directory" in kwargs: + notebook_dir_or_file = pathlib.Path(kwargs['working_directory']) / notebook_dir_or_file + output_dir = pathlib.Path(kwargs['working_directory']) / output_dir + if isinstance(notebook_params, (list, dict)): + notebook_params_list = validate_notebook_params(notebook_params) + elif isinstance(notebook_params, (str, pathlib.Path)): + if "working_directory" in kwargs: + print("HERE") + notebook_params = pathlib.Path(kwargs['working_directory']) / notebook_params + params_data = _parse_json(notebook_params) + notebook_params_list = validate_notebook_params(params_data) + + notebook_dir = None notebook_filename = None if notebook_dir_or_file.is_dir(): @@ -93,12 +139,12 @@ def execute_run( if notebook_filename is not None: execute_notebooks( notebook_dir / notebook_filename, - bulk_params_list, - output_prepend_components, - output_append_components, + notebook_params_list, + prepend, + append, output_dir, use_multiprocessing, - **kwargs + **kwargs, ) else: glob_method = notebook_dir.glob @@ -115,62 +161,65 @@ def execute_run( included_paths = set(glob_method(glob_pattern)) notebook_paths = sorted(included_paths - excluded_paths) + if "profile_name" in kwargs: + task_name = f"Executing profile: {kwargs['profile_name']}" + print(task_name) for notebook_path in notebook_paths: - execute_notebooks( notebook_path, - bulk_params_list, - output_prepend_components, - output_append_components, + notebook_params_list, + prepend, + append, output_dir, use_multiprocessing, - **kwargs - ) - + **kwargs, + ) -def check_unequal_value_lengths(bulk_params: dict[str, list]) -> bool | dict: +def check_unequal_value_lengths(notebook_params: dict[str, list]) -> bool | dict: """ Returns False if all list values are equal length. Returns a dict of value lengths otherwise. """ acc = {} - for k, v in bulk_params.items(): + for k, v in notebook_params.items(): try: acc.update({k: len(v)}) except TypeError: - raise ValueError(f"The values of the bulk_param keys must be lists, not: '{k: v}'") + raise ValueError( + f"The values of the bulk_param keys must be lists, not: '{k: v}'" + ) all_values = set(acc.values()) if len(all_values) == 1: return False else: return acc - -def convert_bulk_params_to_list(bulk_params: dict[str, list]): + +def convert_notebook_params_to_list(notebook_params: dict[str, list]): """ Converts a dict of lists into a list of dicts. """ - iter_length = len(list(bulk_params.values())[0]) - bulk_params_list = [] + iter_length = len(list(notebook_params.values())[0]) + notebook_params_list = [] for idx in range(iter_length): inner_acc = {} - for parameter_name, parameter_values in bulk_params.items(): + for parameter_name, parameter_values in notebook_params.items(): inner_acc.update({parameter_name: parameter_values[idx]}) - bulk_params_list.append(inner_acc) - return bulk_params_list + notebook_params_list.append(inner_acc) + return notebook_params_list def execute_notebooks( notebook_path: pathlib.Path, - bulk_params_list: dict[str, Any], - output_prepend_components: list[str], - output_append_components: list[str], + notebook_params_list: dict[str, Any], + prepend: list[str], + append: list[str], output_dir: pathlib.Path, - use_multiprocessing: bool, - **kwargs + use_multiprocessing: bool, + **kwargs, ): - total_variations = len(bulk_params_list) + total_variations = len(notebook_params_list) if not use_multiprocessing: with Progress( "[progress.description]{task.description}", @@ -180,12 +229,12 @@ def execute_notebooks( refresh_per_second=1, # bit slower updates ) as progress: task_id = progress.add_task(notebook_path.name, total=total_variations) - for idx, notebook_params in (list(enumerate(bulk_params_list))): + for idx, notebook_params in list(enumerate(notebook_params_list)): execute_notebook( notebook_filename=notebook_path, notebook_params=notebook_params, - output_prepend_components=output_prepend_components, - output_append_components=output_append_components, + prepend=prepend, + append=append, output_dir=output_dir, **kwargs, ) @@ -198,43 +247,43 @@ def execute_notebooks( TimeElapsedColumn(), refresh_per_second=1, # bit slower updates ) as progress: - # Multiprocessing approach inspired by + # Multiprocessing approach inspired by # https://www.deanmontgomery.com/2022/03/24/rich-progress-and-multiprocessing/ futures = [] # keep track of the jobs with multiprocessing.Manager() as manager: _progress = manager.dict() - overall_progress_task = progress.add_task(f"{notebook_path.name}", visible=True, total=total_variations) + overall_progress_task = progress.add_task( + f"{notebook_path.name}", visible=True, total=total_variations + ) with ProcessPoolExecutor() as executor: - for idx, notebook_params in enumerate(bulk_params_list): + for idx, notebook_params in enumerate(notebook_params_list): futures.append( executor.submit( - execute_notebook, + execute_notebook, notebook_path, notebook_params, - output_prepend_components, - output_append_components, + prepend, + append, output_dir, total_variations, idx, _progress, - overall_progress_task + overall_progress_task, ) ) # monitor the progress: - while (n_finished := sum([future.done() for future in futures])) < len( - futures - ): - progress.update( - overall_progress_task, completed=n_finished + 1 - ) + while ( + n_finished := sum([future.done() for future in futures]) + ) < len(futures): + progress.update(overall_progress_task, completed=n_finished + 1) def execute_notebook( notebook_filename: pathlib.Path, notebook_params: dict, - output_prepend_components: list[str], - output_append_components: list[str], + prepend: list[str], + append: list[str], output_dir: pathlib.Path, total_variations: Optional[int] = None, current_iteration: Optional[int] = None, @@ -243,11 +292,7 @@ def execute_notebook( **kwargs, ): output_name = get_output_name( - notebook_filename, - output_prepend_components, - output_append_components, - notebook_params, - current_iteration + notebook_filename, prepend, append, notebook_params, current_iteration ) pm.execute_notebook( notebook_filename, @@ -255,30 +300,77 @@ def execute_notebook( parameters=notebook_params, progress_bar=False, cwd=str(notebook_filename.parent), - **kwargs + **kwargs, ) if _progress is not None: - _progress[_task_id] = {"progress": current_iteration, "total": total_variations} + _progress[_task_id] = {"progress": current_iteration, "total": total_variations} def get_output_name( notebook_filename: str, - output_prepend_components: list[str] | None, - output_append_components: list[str] | None, + prepend: list[str] | None, + append: list[str] | None, notebook_params: dict[str, Any], - current_index: int + current_index: int, ) -> str: """ Returns the output name given the included components. """ - if output_prepend_components is None: - output_prepend_components = [str(current_index)] - if output_append_components is None: - output_append_components = [] - prepends = [notebook_params.get(comp, comp) for comp in output_prepend_components] - appends = [notebook_params.get(comp, comp) for comp in output_append_components] + if prepend is None: + prepend = [str(current_index)] + if append is None: + append = [] + prepends = [notebook_params.get(comp, comp) for comp in prepend] + appends = [notebook_params.get(comp, comp) for comp in append] prepend_str = "-".join(prepends) append_str = "-".join(appends) notebook_filename = pathlib.Path(notebook_filename) - output_name = "-".join([elem for elem in [prepend_str, notebook_filename.stem, append_str] if elem]) + notebook_filename.suffix - return output_name \ No newline at end of file + output_name = ( + "-".join( + [elem for elem in [prepend_str, notebook_filename.stem, append_str] if elem] + ) + + notebook_filename.suffix + ) + return output_name + + +def _parse_json(filepath: Optional[str] = None) -> dict: + if filepath is None: + return None + filepath = pathlib.Path(filepath) + if not filepath.exists(): + raise ValueError( + f"The notebook parameters file does not exist: {str(filepath)}" + ) + else: + with open(filepath, "r") as file: + return json.load(file) + + + +def _parse_yaml(filepath: Optional[str] = None) -> dict: + if filepath is None: + return None + filepath = pathlib.Path(filepath) + if not filepath.exists(): + raise ValueError(f"The profile file does not exist: {str(filepath)}") + else: + with open(filepath, "r") as file: + yaml = YAML(typ="safe") + profile_data = yaml.load(file) + return profile_data + + +def validate_notebook_params(params_data: list | dict) -> list: + if isinstance(params_data, dict): + unequal_lengths = check_unequal_value_lengths(params_data) + if unequal_lengths: + raise ValueError( + f"All lists in the notebook_params dict must be of equal length.\n" + f"The following keys have unequal length: {unequal_lengths}" + ) + notebook_params_list = convert_notebook_params_to_list(params_data) + elif isinstance(params_data, list): + notebook_params_list = params_data + + return notebook_params_list diff --git a/tests/test_data/notebook1/notebook1.ipynb b/tests/test_data/notebook1/notebook1.ipynb new file mode 100644 index 0000000..4eed5d7 --- /dev/null +++ b/tests/test_data/notebook1/notebook1.ipynb @@ -0,0 +1,70 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "id": "11f395cd-5eb8-4e8c-9529-0457da39a202", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "params", + "parameters" + ] + }, + "outputs": [], + "source": [ + "name = \"TNB01\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9b71b15c-6c39-45a9-8400-e919bcf83cd4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.15\n" + ] + } + ], + "source": [ + "values = len(name)\n", + "calc = 3 * values / 100\n", + "print(calc)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (Structural Python)", + "language": "python", + "name": "sp" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_data/notebook1/notebook1_params.json b/tests/test_data/notebook1/notebook1_params.json new file mode 100644 index 0000000..5e53b0a --- /dev/null +++ b/tests/test_data/notebook1/notebook1_params.json @@ -0,0 +1,5 @@ +[ + {"name": "EKRJ0193"}, + {"name": "RBENBC929"}, + {"name": "FB3"} +] \ No newline at end of file diff --git a/tests/test_data/notebook2/notebook2.ipynb b/tests/test_data/notebook2/notebook2.ipynb new file mode 100644 index 0000000..9c34789 --- /dev/null +++ b/tests/test_data/notebook2/notebook2.ipynb @@ -0,0 +1,70 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "id": "11f395cd-5eb8-4e8c-9529-0457da39a202", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "params", + "parameters" + ] + }, + "outputs": [], + "source": [ + "tester = \"TNB01\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9b71b15c-6c39-45a9-8400-e919bcf83cd4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "285.7142857142857\n" + ] + } + ], + "source": [ + "values = len(tester)\n", + "calc = 4 * values * 100 / 7\n", + "print(calc)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (Structural Python)", + "language": "python", + "name": "sp" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_data/notebook2/notebook2_params.json b/tests/test_data/notebook2/notebook2_params.json new file mode 100644 index 0000000..c7406dd --- /dev/null +++ b/tests/test_data/notebook2/notebook2_params.json @@ -0,0 +1,5 @@ +[ + {"tester": "EKRJ0193"}, + {"tester": "RBENBC929"}, + {"tester": "FB3"} +] \ No newline at end of file diff --git a/tests/test_data/notebook2/notebook3.ipynb b/tests/test_data/notebook2/notebook3.ipynb new file mode 100644 index 0000000..1e7f475 --- /dev/null +++ b/tests/test_data/notebook2/notebook3.ipynb @@ -0,0 +1,70 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "11f395cd-5eb8-4e8c-9529-0457da39a202", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "params", + "parameters" + ] + }, + "outputs": [], + "source": [ + "tester = \"TNB01\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9b71b15c-6c39-45a9-8400-e919bcf83cd4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1500\n" + ] + } + ], + "source": [ + "values = len(tester)\n", + "calc = 3 * values * 100\n", + "print(calc)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (Structural Python)", + "language": "python", + "name": "sp" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_data/profiles.yaml b/tests/test_data/profiles.yaml new file mode 100644 index 0000000..c958693 --- /dev/null +++ b/tests/test_data/profiles.yaml @@ -0,0 +1,16 @@ +notebook1: + notebook_dir_or_file: ./notebook1/notebook1.ipynb + notebook_params: ./notebook1/notebook1_params.json + output_dir: ./notebook1/output + prepend: + - name + - design + append: + - executed + +notebook2: + notebook_dir_or_file: ./notebook2 + notebook_params: ./notebook2/notebook2_params.json + output_dir: ./notebook2/output + prepend: + - tester \ No newline at end of file diff --git a/tests/test_profiles.py b/tests/test_profiles.py new file mode 100644 index 0000000..dcf76da --- /dev/null +++ b/tests/test_profiles.py @@ -0,0 +1,30 @@ + +from millrun.millrun import execute_profile, _parse_yaml +import pathlib + +TEST_DATA_DIR = pathlib.Path(__file__).parent / "test_data" + + +def test__parse_yaml(): + yaml_data = _parse_yaml(TEST_DATA_DIR / "profiles.yaml") + assert yaml_data == { + "notebook1": { + "notebook_dir_or_file": "./notebook1/notebook1.ipynb", + "notebook_params": "./notebook1/notebook1_params.json", + "output_dir": "./notebook1/output", + "prepend": ["name", "design"], + "append": ["executed"], + }, + "notebook2": { + "notebook_dir_or_file": "./notebook2", + "notebook_params": "./notebook2/notebook2_params.json", + "output_dir": "./notebook2/output", + "prepend": ["tester"] + }, + } + +def test_execute_profile(): + yaml_file = TEST_DATA_DIR / "profiles.yaml" + execute_profile(yaml_file) + # If there are no errors, then the assert statement gets hit + assert True \ No newline at end of file diff --git a/uv.lock b/uv.lock index 2b4aad6..4b59f28 100644 --- a/uv.lock +++ b/uv.lock @@ -148,6 +148,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, +] + [[package]] name = "certifi" version = "2025.7.14" @@ -484,6 +518,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + [[package]] name = "ipykernel" version = "6.29.5" @@ -634,28 +677,36 @@ wheels = [ [[package]] name = "millrun" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "papermill" }, { name = "rich" }, + { name = "ruamel-yaml" }, { name = "typer" }, ] [package.dev-dependencies] dev = [ + { name = "black" }, { name = "ipykernel" }, + { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "papermill", specifier = ">=2.6.0" }, - { name = "rich", specifier = ">=14.0.0" }, + { name = "rich" }, + { name = "ruamel-yaml", specifier = ">=0.18.15" }, { name = "typer", specifier = ">=0.16.0" }, ] [package.metadata.requires-dev] -dev = [{ name = "ipykernel", specifier = ">=6.29.5" }] +dev = [ + { name = "black", specifier = ">=25.1.0" }, + { name = "ipykernel", specifier = ">=6.29.5" }, + { name = "pytest", specifier = ">=8.4.2" }, +] [[package]] name = "multidict" @@ -756,6 +807,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313 }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + [[package]] name = "nbclient" version = "0.10.2" @@ -834,6 +894,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -855,6 +924,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + [[package]] name = "prompt-toolkit" version = "3.0.51" @@ -1007,6 +1085,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1314,6 +1410,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 }, ] +[[package]] +name = "ruamel-yaml" +version = "0.18.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/db/f3950f5e5031b618aae9f423a39bf81a55c148aecd15a34527898e752cf4/ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700", size = 146865 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701", size = 119702 }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301 }, + { url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728 }, + { url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230 }, + { url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712 }, + { url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936 }, + { url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580 }, + { url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393 }, + { url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326 }, + { url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079 }, + { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 }, + { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 }, + { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 }, + { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 }, + { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 }, + { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 }, + { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 }, + { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 }, + { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 }, + { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433 }, + { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362 }, + { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118 }, + { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497 }, + { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042 }, + { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831 }, + { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692 }, + { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777 }, + { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523 }, + { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011 }, + { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488 }, + { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066 }, + { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785 }, + { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017 }, + { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270 }, + { url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059 }, + { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583 }, + { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190 }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -1355,6 +1507,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + [[package]] name = "tornado" version = "6.5.1"