From e93b45a540df55ec879760d5c424dda7cb59cfc6 Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sat, 25 May 2024 21:18:44 +0200 Subject: [PATCH 001/121] Add basic skeleton with welcome screen --- nf_core/__main__.py | 34 ++++++++ nf_core/configs/__init__.py | 1 + nf_core/configs/create/__init__.py | 63 ++++++++++++++ nf_core/configs/create/create.tcss | 135 +++++++++++++++++++++++++++++ nf_core/configs/create/utils.py | 41 +++++++++ nf_core/configs/create/welcome.py | 44 ++++++++++ 6 files changed, 318 insertions(+) create mode 100644 nf_core/configs/__init__.py create mode 100644 nf_core/configs/create/__init__.py create mode 100644 nf_core/configs/create/create.tcss create mode 100644 nf_core/configs/create/utils.py create mode 100644 nf_core/configs/create/welcome.py diff --git a/nf_core/__main__.py b/nf_core/__main__.py index 67af238b5c..6375f68cff 100644 --- a/nf_core/__main__.py +++ b/nf_core/__main__.py @@ -36,6 +36,7 @@ "commands": [ "list", "launch", + "configs", "create-params-file", "download", "licences", @@ -82,6 +83,12 @@ "commands": ["create", "test", "lint"], }, ], + "nf-core configs": [ + { + "name": "Config commands", + "commands": ["create"], + }, + ], } click.rich_click.OPTION_GROUPS = { "nf-core modules list local": [{"options": ["--dir", "--json", "--help"]}], @@ -302,6 +309,33 @@ def launch( if not launcher.launch_pipeline(): sys.exit(1) +# nf-core configs +@nf_core_cli.group() +@click.pass_context +def configs(ctx): + """ + Commands to create and manage nf-core configs. + """ + # ensure that ctx.obj exists and is a dict (in case `cli()` is called + # by means other than the `if` block below) + ctx.ensure_object(dict) + + +@configs.command("create") +def create_configs(): + """ + Command to interactively create a nextflow or nf-core config + """ + from nf_core.configs.create import ConfigsCreateApp + + try: + log.info("Launching interactive nf-core configs creation tool.") + app = ConfigsCreateApp() + app.run() + sys.exit(app.return_code or 0) + except UserWarning as e: + log.error(e) + sys.exit(1) # nf-core create-params-file @nf_core_cli.command() diff --git a/nf_core/configs/__init__.py b/nf_core/configs/__init__.py new file mode 100644 index 0000000000..95c830c1b4 --- /dev/null +++ b/nf_core/configs/__init__.py @@ -0,0 +1 @@ +from .create import ConfigsCreateApp diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py new file mode 100644 index 0000000000..8dfa10263c --- /dev/null +++ b/nf_core/configs/create/__init__.py @@ -0,0 +1,63 @@ +"""A Textual app to create a config.""" + +import logging + +## Textual objects +from textual.app import App +from textual.widgets import Button + +## General utilities +from nf_core.configs.create.utils import ( + CreateConfig, + CustomLogHandler, + LoggingConsole, +) + +## nf-core question page imports +from nf_core.configs.create.welcome import WelcomeScreen + +## Logging +log_handler = CustomLogHandler( + console=LoggingConsole(classes="log_console"), + rich_tracebacks=True, + show_time=False, + show_path=False, + markup=True, +) +logging.basicConfig( + level="INFO", + handlers=[log_handler], + format="%(message)s", +) +log_handler.setLevel("INFO") + +## Main workflow +class ConfigsCreateApp(App[CreateConfig]): + """A Textual app to create nf-core configs.""" + + CSS_PATH = "create.tcss" + TITLE = "nf-core configs create" + SUB_TITLE = "Create a new nextflow config with an interactive interface" + BINDINGS = [ + ("d", "toggle_dark", "Toggle dark mode"), + ("q", "quit", "Quit"), + ] + + ## New question screens (sections) loaded here + SCREENS = { + "welcome": WelcomeScreen() + } + + # Log handler + LOG_HANDLER = log_handler + # Logging state + LOGGING_STATE = None + + ## Question dialogue order defined here + def on_mount(self) -> None: + self.push_screen("welcome") + + ## User theme options + def action_toggle_dark(self) -> None: + """An action to toggle dark mode.""" + self.dark: bool = not self.dark diff --git a/nf_core/configs/create/create.tcss b/nf_core/configs/create/create.tcss new file mode 100644 index 0000000000..67394a9de3 --- /dev/null +++ b/nf_core/configs/create/create.tcss @@ -0,0 +1,135 @@ +#logo { + width: 100%; + content-align-horizontal: center; + content-align-vertical: middle; +} +.cta { + layout: horizontal; + margin-bottom: 1; +} +.cta Button { + margin: 0 3; +} + +.pipeline-type-grid { + height: auto; + margin-bottom: 2; +} + +.custom_grid { + height: auto; +} +.custom_grid Switch { + width: auto; +} +.custom_grid Static { + width: 1fr; + margin: 1 8; +} +.custom_grid Button { + width: auto; +} + +.field_help { + padding: 1 1 0 1; + color: $text-muted; + text-style: italic; +} +.validation_msg { + padding: 0 1; + color: $error; +} +.-valid { + border: tall $success-darken-3; +} + +Horizontal{ + width: 100%; + height: auto; +} +.column { + width: 1fr; +} + +HorizontalScroll { + width: 100%; +} +.feature_subtitle { + color: grey; +} + +Vertical{ + height: auto; +} + +.features-container { + padding: 0 4 1 4; +} + +/* Display help messages */ + +.help_box { + background: #333333; + padding: 1 3 0 3; + margin: 0 5 2 5; + overflow-y: auto; + transition: height 50ms; + display: none; + height: 0; +} +.displayed .help_box { + display: block; + height: 12; +} +#show_help { + display: block; +} +#hide_help { + display: none; +} +.displayed #show_help { + display: none; +} +.displayed #hide_help { + display: block; +} + +/* Show password */ + +#show_password { + display: block; +} +#hide_password { + display: none; +} +.displayed #show_password { + display: none; +} +.displayed #hide_password { + display: block; +} + +/* Logging console */ + +.log_console { + height: auto; + background: #333333; + padding: 1 3; + margin: 0 4 2 4; +} + +.hide { + display: none; +} + +/* Layouts */ +.col-2 { + grid-size: 2 1; +} + +.ghrepo-cols { + margin: 0 4; +} +.ghrepo-cols Button { + margin-top: 2; +} diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py new file mode 100644 index 0000000000..868d791dfb --- /dev/null +++ b/nf_core/configs/create/utils.py @@ -0,0 +1,41 @@ +from logging import LogRecord +from typing import Optional + +from pydantic import BaseModel +from rich.logging import RichHandler +from textual.message import Message +from textual.widget import Widget +from textual.widgets import RichLog + +## Logging (TODO: move to common place and share with pipelines logging?) + +class LoggingConsole(RichLog): + file = False + console: Widget + + def print(self, content): + self.write(content) + +class CustomLogHandler(RichHandler): + """A Logging handler which extends RichHandler to write to a Widget and handle a Textual App.""" + + def emit(self, record: LogRecord) -> None: + """Invoked by logging.""" + try: + _app = active_app.get() + except LookupError: + pass + else: + super().emit(record) + +class ShowLogs(Message): + """Custom message to show the logging messages.""" + + pass + +## Config model template + +class CreateConfig(BaseModel): + """Pydantic model for the nf-core create config.""" + + config_type: Optional[str] = None diff --git a/nf_core/configs/create/welcome.py b/nf_core/configs/create/welcome.py new file mode 100644 index 0000000000..659182a37c --- /dev/null +++ b/nf_core/configs/create/welcome.py @@ -0,0 +1,44 @@ +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown, Static + +from nf_core.utils import nfcore_logo + +markdown = """ +# Welcome to the nf-core config creation wizard + +This app will help you create **Nextflow configuration files** +for both **infrastructure** and **pipeline-specific** configs. + +## Config Types + +- **Infrastructure configs** allow you to define the computational environment you +will run the pipelines on (memory, CPUs, scheduling system, container engine +etc.). +- **Pipeline configs** allow you to tweak resources of a particular step of a +pipeline. For example process X should request 8.GB of memory. + +## Using Configs + +The resulting config file can be used with a pipeline with `-c .conf`. + +They can also be added to the centralised +[nf-core/configs](https://github.com/nf-core/configs) repository, where they +can be used by anyone running nf-core pipelines on your infrastructure directly +using `-profile `. +""" + + +class WelcomeScreen(Screen): + """A welcome screen for the app.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Static( + "\n" + "\n".join(nfcore_logo) + "\n", + id="logo", + ) + yield Markdown(markdown) + yield Center(Button("Let's go!", id="start", variant="success"), classes="cta") From 266a1633459b25de78f42c72eed2b319dd507290 Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sat, 25 May 2024 21:22:13 +0200 Subject: [PATCH 002/121] Update changelgo --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16262bd1c3..443d31c7af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ ### Components +### Configs + +- New command: `nf-core configs create wizard` for generating configs for nf-core pipelines ([#3001](https://github.com/nf-core/tools/pull/3001)) + ### General - Update pre-commit hook astral-sh/ruff-pre-commit to v0.4.4 ([#2974](https://github.com/nf-core/tools/pull/2974)) From 71a14752168382769fcd4f660813851c90f4e8da Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sat, 25 May 2024 21:23:07 +0200 Subject: [PATCH 003/121] Fix linting failure --- nf_core/configs/create/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 868d791dfb..f969c4ac86 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -3,6 +3,7 @@ from pydantic import BaseModel from rich.logging import RichHandler +from textual._context import active_app from textual.message import Message from textual.widget import Widget from textual.widgets import RichLog From a583018dc0ac367a5a66628e99004f3e94732ef7 Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sat, 25 May 2024 21:24:04 +0200 Subject: [PATCH 004/121] Linting --- nf_core/__main__.py | 2 ++ nf_core/configs/create/__init__.py | 5 ++--- nf_core/configs/create/utils.py | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/nf_core/__main__.py b/nf_core/__main__.py index 6375f68cff..6d5e624181 100644 --- a/nf_core/__main__.py +++ b/nf_core/__main__.py @@ -309,6 +309,7 @@ def launch( if not launcher.launch_pipeline(): sys.exit(1) + # nf-core configs @nf_core_cli.group() @click.pass_context @@ -337,6 +338,7 @@ def create_configs(): log.error(e) sys.exit(1) + # nf-core create-params-file @nf_core_cli.command() @click.argument("pipeline", required=False, metavar="") diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 8dfa10263c..29237c0531 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -31,6 +31,7 @@ ) log_handler.setLevel("INFO") + ## Main workflow class ConfigsCreateApp(App[CreateConfig]): """A Textual app to create nf-core configs.""" @@ -44,9 +45,7 @@ class ConfigsCreateApp(App[CreateConfig]): ] ## New question screens (sections) loaded here - SCREENS = { - "welcome": WelcomeScreen() - } + SCREENS = {"welcome": WelcomeScreen()} # Log handler LOG_HANDLER = log_handler diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index f969c4ac86..f89873a426 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -10,6 +10,7 @@ ## Logging (TODO: move to common place and share with pipelines logging?) + class LoggingConsole(RichLog): file = False console: Widget @@ -17,6 +18,7 @@ class LoggingConsole(RichLog): def print(self, content): self.write(content) + class CustomLogHandler(RichHandler): """A Logging handler which extends RichHandler to write to a Widget and handle a Textual App.""" @@ -29,13 +31,16 @@ def emit(self, record: LogRecord) -> None: else: super().emit(record) + class ShowLogs(Message): """Custom message to show the logging messages.""" pass + ## Config model template + class CreateConfig(BaseModel): """Pydantic model for the nf-core create config.""" From 5e1dc525093e1a59ffd888bc4c3d612a0dd832dc Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sun, 26 May 2024 13:44:56 +0200 Subject: [PATCH 005/121] Move common util functions/classes to common location --- nf_core/configs/create/__init__.py | 11 ++-- nf_core/configs/create/utils.py | 38 -------------- nf_core/pipelines/create/__init__.py | 7 +-- nf_core/pipelines/create/basicdetails.py | 3 +- nf_core/pipelines/create/finaldetails.py | 3 +- nf_core/pipelines/create/githubrepo.py | 3 +- nf_core/pipelines/create/loggingscreen.py | 3 +- nf_core/pipelines/create/utils.py | 62 ++--------------------- nf_core/utils.py | 62 +++++++++++++++++++++++ 9 files changed, 80 insertions(+), 112 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 29237c0531..5e0ce83417 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -6,16 +6,17 @@ from textual.app import App from textual.widgets import Button +from nf_core.configs.create.utils import CreateConfig + +## nf-core question page imports +from nf_core.configs.create.welcome import WelcomeScreen + ## General utilities -from nf_core.configs.create.utils import ( - CreateConfig, +from nf_core.utils import ( CustomLogHandler, LoggingConsole, ) -## nf-core question page imports -from nf_core.configs.create.welcome import WelcomeScreen - ## Logging log_handler = CustomLogHandler( console=LoggingConsole(classes="log_console"), diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index f89873a426..61099c4206 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -1,44 +1,6 @@ -from logging import LogRecord from typing import Optional from pydantic import BaseModel -from rich.logging import RichHandler -from textual._context import active_app -from textual.message import Message -from textual.widget import Widget -from textual.widgets import RichLog - -## Logging (TODO: move to common place and share with pipelines logging?) - - -class LoggingConsole(RichLog): - file = False - console: Widget - - def print(self, content): - self.write(content) - - -class CustomLogHandler(RichHandler): - """A Logging handler which extends RichHandler to write to a Widget and handle a Textual App.""" - - def emit(self, record: LogRecord) -> None: - """Invoked by logging.""" - try: - _app = active_app.get() - except LookupError: - pass - else: - super().emit(record) - - -class ShowLogs(Message): - """Custom message to show the logging messages.""" - - pass - - -## Config model template class CreateConfig(BaseModel): diff --git a/nf_core/pipelines/create/__init__.py b/nf_core/pipelines/create/__init__.py index da6a693220..ba25053168 100644 --- a/nf_core/pipelines/create/__init__.py +++ b/nf_core/pipelines/create/__init__.py @@ -14,12 +14,9 @@ from nf_core.pipelines.create.loggingscreen import LoggingScreen from nf_core.pipelines.create.nfcorepipeline import NfcorePipeline from nf_core.pipelines.create.pipelinetype import ChoosePipelineType -from nf_core.pipelines.create.utils import ( - CreateConfig, - CustomLogHandler, - LoggingConsole, -) +from nf_core.pipelines.create.utils import CreateConfig from nf_core.pipelines.create.welcome import WelcomeScreen +from nf_core.utils import CustomLogHandler, LoggingConsole log_handler = CustomLogHandler( console=LoggingConsole(classes="log_console"), diff --git a/nf_core/pipelines/create/basicdetails.py b/nf_core/pipelines/create/basicdetails.py index b88ede10d0..6459c5353c 100644 --- a/nf_core/pipelines/create/basicdetails.py +++ b/nf_core/pipelines/create/basicdetails.py @@ -9,7 +9,8 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown -from nf_core.pipelines.create.utils import CreateConfig, TextInput, add_hide_class, remove_hide_class +from nf_core.pipelines.create.utils import CreateConfig, TextInput +from nf_core.utils import add_hide_class, remove_hide_class pipeline_exists_warn = """ > ⚠️ **The pipeline you are trying to create already exists.** diff --git a/nf_core/pipelines/create/finaldetails.py b/nf_core/pipelines/create/finaldetails.py index bd15cf9ddd..7da0edd946 100644 --- a/nf_core/pipelines/create/finaldetails.py +++ b/nf_core/pipelines/create/finaldetails.py @@ -10,7 +10,8 @@ from textual.widgets import Button, Footer, Header, Input, Markdown from nf_core.pipelines.create.create import PipelineCreate -from nf_core.pipelines.create.utils import ShowLogs, TextInput, add_hide_class, remove_hide_class +from nf_core.pipelines.create.utils import TextInput +from nf_core.utils import ShowLogs, add_hide_class, remove_hide_class pipeline_exists_warn = """ > ⚠️ **The pipeline you are trying to create already exists.** diff --git a/nf_core/pipelines/create/githubrepo.py b/nf_core/pipelines/create/githubrepo.py index 99e7b09ab8..ccfe7f5858 100644 --- a/nf_core/pipelines/create/githubrepo.py +++ b/nf_core/pipelines/create/githubrepo.py @@ -13,7 +13,8 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown, Static, Switch -from nf_core.pipelines.create.utils import ShowLogs, TextInput, remove_hide_class +from nf_core.pipelines.create.utils import TextInput +from nf_core.utils import ShowLogs, remove_hide_class log = logging.getLogger(__name__) diff --git a/nf_core/pipelines/create/loggingscreen.py b/nf_core/pipelines/create/loggingscreen.py index f862dccea1..bb98717e57 100644 --- a/nf_core/pipelines/create/loggingscreen.py +++ b/nf_core/pipelines/create/loggingscreen.py @@ -5,8 +5,7 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Markdown, Static -from nf_core.pipelines.create.utils import add_hide_class -from nf_core.utils import nfcore_logo +from nf_core.utils import add_hide_class, nfcore_logo class LoggingScreen(Screen): diff --git a/nf_core/pipelines/create/utils.py b/nf_core/pipelines/create/utils.py index 6006452baf..19c0e7818c 100644 --- a/nf_core/pipelines/create/utils.py +++ b/nf_core/pipelines/create/utils.py @@ -1,18 +1,15 @@ import re -from logging import LogRecord from pathlib import Path from typing import Optional, Union from pydantic import BaseModel, ConfigDict, ValidationError, field_validator -from rich.logging import RichHandler from textual import on -from textual._context import active_app from textual.app import ComposeResult from textual.containers import HorizontalScroll -from textual.message import Message from textual.validation import ValidationResult, Validator -from textual.widget import Widget -from textual.widgets import Button, Input, Markdown, RichLog, Static, Switch +from textual.widgets import Button, Input, Static, Switch + +from nf_core.utils import HelpText class CreateConfig(BaseModel): @@ -123,21 +120,6 @@ def validate(self, value: str) -> ValidationResult: return self.failure(", ".join([err["msg"] for err in e.errors()])) -class HelpText(Markdown): - """A class to show a text box with help text.""" - - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - - def show(self) -> None: - """Method to show the help text box.""" - self.add_class("displayed") - - def hide(self) -> None: - """Method to hide the help text box.""" - self.remove_class("displayed") - - class PipelineFeature(Static): """Widget for the selection of pipeline features.""" @@ -173,44 +155,6 @@ def compose(self) -> ComposeResult: yield HelpText(markdown=self.markdown, classes="help_box") -class LoggingConsole(RichLog): - file = False - console: Widget - - def print(self, content): - self.write(content) - - -class CustomLogHandler(RichHandler): - """A Logging handler which extends RichHandler to write to a Widget and handle a Textual App.""" - - def emit(self, record: LogRecord) -> None: - """Invoked by logging.""" - try: - _app = active_app.get() - except LookupError: - pass - else: - super().emit(record) - - -class ShowLogs(Message): - """Custom message to show the logging messages.""" - - pass - - -## Functions -def add_hide_class(app, widget_id: str) -> None: - """Add class 'hide' to a widget. Not display widget.""" - app.get_widget_by_id(widget_id).add_class("hide") - - -def remove_hide_class(app, widget_id: str) -> None: - """Remove class 'hide' to a widget. Display widget.""" - app.get_widget_by_id(widget_id).remove_class("hide") - - ## Markdown text to reuse in different screens markdown_genomes = """ Nf-core pipelines are configured to use a copy of the most common reference genome files. diff --git a/nf_core/utils.py b/nf_core/utils.py index 8c50f0a49f..305e75e536 100644 --- a/nf_core/utils.py +++ b/nf_core/utils.py @@ -18,6 +18,7 @@ import sys import time from contextlib import contextmanager +from logging import LogRecord from pathlib import Path from typing import Generator, Tuple, Union @@ -30,7 +31,12 @@ import yaml from packaging.version import Version from rich.live import Live +from rich.logging import RichHandler from rich.spinner import Spinner +from textual._context import active_app +from textual.message import Message +from textual.widget import Widget +from textual.widgets import Markdown, RichLog import nf_core @@ -1220,3 +1226,59 @@ def set_wd(path: Path) -> Generator[None, None, None]: yield finally: os.chdir(start_wd) + + +# General textual-related functions and objects + + +class HelpText(Markdown): + """A class to show a text box with help text.""" + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def show(self) -> None: + """Method to show the help text box.""" + self.add_class("displayed") + + def hide(self) -> None: + """Method to hide the help text box.""" + self.remove_class("displayed") + + +class LoggingConsole(RichLog): + file = False + console: Widget + + def print(self, content): + self.write(content) + + +class CustomLogHandler(RichHandler): + """A Logging handler which extends RichHandler to write to a Widget and handle a Textual App.""" + + def emit(self, record: LogRecord) -> None: + """Invoked by logging.""" + try: + _app = active_app.get() + except LookupError: + pass + else: + super().emit(record) + + +class ShowLogs(Message): + """Custom message to show the logging messages.""" + + pass + + +# Functions +def add_hide_class(app, widget_id: str) -> None: + """Add class 'hide' to a widget. Not display widget.""" + app.get_widget_by_id(widget_id).add_class("hide") + + +def remove_hide_class(app, widget_id: str) -> None: + """Remove class 'hide' to a widget. Display widget.""" + app.get_widget_by_id(widget_id).remove_class("hide") From 03eaa52efb548c9fa8c0bb3ef2522b4f733c5df9 Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sun, 26 May 2024 13:49:04 +0200 Subject: [PATCH 006/121] Move textual CSS to common place --- MANIFEST.in | 2 +- nf_core/configs/create/__init__.py | 2 +- nf_core/pipelines/create/__init__.py | 2 +- nf_core/{pipelines/create/create.tcss => textual.tcss} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename nf_core/{pipelines/create/create.tcss => textual.tcss} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 68f115d97f..2226dad230 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,4 +9,4 @@ include nf_core/assets/logo/nf-core-repo-logo-base-lightbg.png include nf_core/assets/logo/nf-core-repo-logo-base-darkbg.png include nf_core/assets/logo/placeholder_logo.svg include nf_core/assets/logo/MavenPro-Bold.ttf -include nf_core/pipelines/create/create.tcss +include nf_core/textual.tcss diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 5e0ce83417..05ec9979fb 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -37,7 +37,7 @@ class ConfigsCreateApp(App[CreateConfig]): """A Textual app to create nf-core configs.""" - CSS_PATH = "create.tcss" + CSS_PATH = "../../textual.tcss" TITLE = "nf-core configs create" SUB_TITLE = "Create a new nextflow config with an interactive interface" BINDINGS = [ diff --git a/nf_core/pipelines/create/__init__.py b/nf_core/pipelines/create/__init__.py index ba25053168..fd1d6c680a 100644 --- a/nf_core/pipelines/create/__init__.py +++ b/nf_core/pipelines/create/__init__.py @@ -36,7 +36,7 @@ class PipelineCreateApp(App[CreateConfig]): """A Textual app to manage stopwatches.""" - CSS_PATH = "create.tcss" + CSS_PATH = "../../textual.tcss" TITLE = "nf-core create" SUB_TITLE = "Create a new pipeline with the nf-core pipeline template" BINDINGS = [ diff --git a/nf_core/pipelines/create/create.tcss b/nf_core/textual.tcss similarity index 100% rename from nf_core/pipelines/create/create.tcss rename to nf_core/textual.tcss From b82b6fb5ea189aef2cbd088127cb7c41bd8881c3 Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sun, 26 May 2024 14:19:10 +0200 Subject: [PATCH 007/121] Add config type question --- nf_core/configs/create/__init__.py | 14 ++++-- nf_core/configs/create/configtype.py | 74 ++++++++++++++++++++++++++++ nf_core/configs/create/welcome.py | 21 ++++---- 3 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 nf_core/configs/create/configtype.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 05ec9979fb..0aff5260a5 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -6,9 +6,9 @@ from textual.app import App from textual.widgets import Button -from nf_core.configs.create.utils import CreateConfig - ## nf-core question page imports +from nf_core.configs.create.configtype import ChooseConfigType +from nf_core.configs.create.utils import CreateConfig from nf_core.configs.create.welcome import WelcomeScreen ## General utilities @@ -46,7 +46,10 @@ class ConfigsCreateApp(App[CreateConfig]): ] ## New question screens (sections) loaded here - SCREENS = {"welcome": WelcomeScreen()} + SCREENS = { + "welcome": WelcomeScreen(), + "choose_type": ChooseConfigType(), + } # Log handler LOG_HANDLER = log_handler @@ -57,6 +60,11 @@ class ConfigsCreateApp(App[CreateConfig]): def on_mount(self) -> None: self.push_screen("welcome") + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle all button pressed events.""" + if event.button.id == "lets_go": + self.push_screen("choose_type") + ## User theme options def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" diff --git a/nf_core/configs/create/configtype.py b/nf_core/configs/create/configtype.py new file mode 100644 index 0000000000..dfd2479017 --- /dev/null +++ b/nf_core/configs/create/configtype.py @@ -0,0 +1,74 @@ +from textual.app import ComposeResult +from textual.containers import Center, Grid +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +markdown_intro = """ +# Choose config type +""" + +markdown_type_nfcore = """ +## Choose _"Infrastructure config"_ if: + +* You want to only define the computational environment you will run all pipelines on + + +""" +markdown_type_custom = """ +## Choose _"Pipeline config"_ if: + +* You just want to tweak resources of a particular step of a specific pipeline. +""" + +markdown_details = """ +## What's the difference? + +_Infrastructure_ configs: + +- Describe the basic necessary information for any nf-core pipeline to +execute +- Define things such as which container engine to use, if there is a scheduler and +which queues to use etc. +- Are suitable for _all_ users on a given computing environment. +- Can be uploaded to [nf-core +configs](https://github.com/nf-core/tools/configs) to be directly accessible +in a nf-core pipeline with `-profile `. +- Are not used to tweak specific parts of a given pipeline (such as a process or +module) + +_Pipeline_ configs + +- Are config files that target specific component of a particular pipeline or pipeline run. + - Example: you have a particular step of the pipeline that often runs out +of memory using the pipeline's default settings. You would use this config to +increase the amount of memory Nextflow supplies that given task. +- Are normally only used by a _single or small group_ of users. +- _May_ also be shared amongst multiple users on the same +computing environment if running similar data with the same pipeline. +- Can _sometimes_ be uploaded to [nf-core +configs](https://github.com/nf-core/tools/configs) as a 'pipeline-specific' +config. + + +""" + + +class ChooseConfigType(Screen): + """Choose whether this will be an infrastructure or pipeline config.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield Grid( + Center( + Markdown(markdown_type_nfcore), + Center(Button("Pipeline config", id="type_infrastructure", variant="success")), + ), + Center( + Markdown(markdown_type_custom), + Center(Button("Infrastructure config", id="type_pipeline", variant="primary")), + ), + classes="col-2 pipeline-type-grid", + ) + yield Markdown(markdown_details) diff --git a/nf_core/configs/create/welcome.py b/nf_core/configs/create/welcome.py index 659182a37c..94dfe2d955 100644 --- a/nf_core/configs/create/welcome.py +++ b/nf_core/configs/create/welcome.py @@ -9,24 +9,21 @@ # Welcome to the nf-core config creation wizard This app will help you create **Nextflow configuration files** -for both **infrastructure** and **pipeline-specific** configs. +for both: -## Config Types - -- **Infrastructure configs** allow you to define the computational environment you -will run the pipelines on (memory, CPUs, scheduling system, container engine -etc.). -- **Pipeline configs** allow you to tweak resources of a particular step of a -pipeline. For example process X should request 8.GB of memory. +- **Infrastructure** configs for defining computing environment for all + pipelines, and +- **Pipeline** configs for defining pipeline-specific resource requirements ## Using Configs -The resulting config file can be used with a pipeline with `-c .conf`. +The resulting config file can be used with a pipeline with adding `-c +.conf` to a `nextflow run` command. They can also be added to the centralised [nf-core/configs](https://github.com/nf-core/configs) repository, where they -can be used by anyone running nf-core pipelines on your infrastructure directly -using `-profile `. +can be used directly by anyone running nf-core pipelines on your infrastructure +specifying `nextflow run -profile `. """ @@ -41,4 +38,4 @@ def compose(self) -> ComposeResult: id="logo", ) yield Markdown(markdown) - yield Center(Button("Let's go!", id="start", variant="success"), classes="cta") + yield Center(Button("Let's go!", id="lets_go", variant="success"), classes="cta") From 5f6b2a4195b29e667b560208c06b865e22b7de26 Mon Sep 17 00:00:00 2001 From: James Fellows Yates Date: Sun, 26 May 2024 14:56:25 +0200 Subject: [PATCH 008/121] Start adding basic details screen. Missing: validation. Not working: Hide URL Question of pipeline configs --- nf_core/configs/create/__init__.py | 22 +++- nf_core/configs/create/basicdetails.py | 119 ++++++++++++++++++++++ nf_core/configs/create/configtype.py | 2 + nf_core/configs/create/create.tcss | 135 ------------------------- nf_core/configs/create/utils.py | 2 + nf_core/configs/create/welcome.py | 2 + 6 files changed, 142 insertions(+), 140 deletions(-) create mode 100644 nf_core/configs/create/basicdetails.py delete mode 100644 nf_core/configs/create/create.tcss diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 0aff5260a5..90f0ddd9fb 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -6,7 +6,8 @@ from textual.app import App from textual.widgets import Button -## nf-core question page imports +## nf-core question page (screen) imports +from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType from nf_core.configs.create.utils import CreateConfig from nf_core.configs.create.welcome import WelcomeScreen @@ -46,10 +47,10 @@ class ConfigsCreateApp(App[CreateConfig]): ] ## New question screens (sections) loaded here - SCREENS = { - "welcome": WelcomeScreen(), - "choose_type": ChooseConfigType(), - } + SCREENS = {"welcome": WelcomeScreen(), "choose_type": ChooseConfigType(), "basic_details": BasicDetails()} + + # Tracking variables + CONFIG_TYPE = None # Log handler LOG_HANDLER = log_handler @@ -64,6 +65,17 @@ def on_button_pressed(self, event: Button.Pressed) -> None: """Handle all button pressed events.""" if event.button.id == "lets_go": self.push_screen("choose_type") + elif event.button.id == "type_infrastructure": + self.CONFIG_TYPE = "infrastructure" + self.push_screen("basic_details") + elif event.button.id == "type_pipeline": + self.CONFIG_TYPE = "pipeline" + self.push_screen("basic_details") + ## General options + if event.button.id == "close_app": + self.exit(return_code=0) + if event.button.id == "back": + self.pop_screen() ## User theme options def action_toggle_dark(self) -> None: diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py new file mode 100644 index 0000000000..6dd7c95dec --- /dev/null +++ b/nf_core/configs/create/basicdetails.py @@ -0,0 +1,119 @@ +"""Get basic contact information to set in params to help with debugging. By +displaying such info in the pipeline run header on run execution""" + +from textwrap import dedent + +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.pipelines.create.utils import TextInput + +config_exists_warn = """ +> ⚠️ **The config file you are trying to create already exists.** +> +> If you continue, you will **overwrite** the existing config. +> Please change the config name to create a different config!. +""" + + +class BasicDetails(Screen): + """Name, description, author, etc.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Basic details + """ + ) + ) + ## TODO Add validation, .conf already exists? + yield TextInput( + "config_name", + "custom", + "Config Name. Used for naming resulting file.", + "", + classes="column", + ) + with Horizontal(): + yield TextInput( + "authorname", + "Boaty McBoatFace", + "Author full name.", + classes="column", + ) + + yield TextInput( + "authorhandle", + "@BoatyMcBoatFace", + "Author Git(Hub) handle.", + classes="column", + ) + + yield TextInput( + "description", + "Description", + "A short description of your config.", + ) + yield TextInput( + "institutional_url", + "https://nf-co.re", + "URL of infrastructure website or owning institutional.", + disabled=self.parent.CONFIG_TYPE == "pipeline", ## TODO not working, why? + ) + ## TODO: reactivate once validation ready + # yield Markdown(dedent(config_exists_warn), id="exist_warn", classes="hide") + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + ## TODO: update functions + # @on(Input.Changed) + # @on(Input.Submitted) + # def show_exists_warn(self): + # """Check if the pipeline exists on every input change or submitted. + # If the pipeline exists, show warning message saying that it will be overriden.""" + # config = {} + # for text_input in self.query("TextInput"): + # this_input = text_input.query_one(Input) + # config[text_input.field_id] = this_input.value + # if Path(config["org"] + "-" + config["name"]).is_dir(): + # remove_hide_class(self.parent, "exist_warn") + # else: + # add_hide_class(self.parent, "exist_warn") + + # def on_screen_resume(self): + # """Hide warn message on screen resume. + # Update displayed value on screen resume.""" + # add_hide_class(self.parent, "exist_warn") + # for text_input in self.query("TextInput"): + # if text_input.field_id == "org": + # text_input.disabled = self.parent.CONFIG_TYPE == "infrastructure" + + # @on(Button.Pressed) + # def on_button_pressed(self, event: Button.Pressed) -> None: + # """Save fields to the config.""" + # config = {} + # for text_input in self.query("TextInput"): + # this_input = text_input.query_one(Input) + # validation_result = this_input.validate(this_input.value) + # config[text_input.field_id] = this_input.value + # if not validation_result.is_valid: + # text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + # else: + # text_input.query_one(".validation_msg").update("") + # try: + # self.parent.TEMPLATE_CONFIG = CreateConfig(**config) + # if event.button.id == "next": + # if self.parent.CONFIG_TYPE == "infrastructure": + # self.parent.push_screen("type_infrastructure") + # elif self.parent.CONFIG_TYPE == "pipeline": + # self.parent.push_screen("type_pipeline") + # except ValueError: + # pass diff --git a/nf_core/configs/create/configtype.py b/nf_core/configs/create/configtype.py index dfd2479017..28269cd0df 100644 --- a/nf_core/configs/create/configtype.py +++ b/nf_core/configs/create/configtype.py @@ -1,3 +1,5 @@ +"""Select which type of config to create to guide questions and order""" + from textual.app import ComposeResult from textual.containers import Center, Grid from textual.screen import Screen diff --git a/nf_core/configs/create/create.tcss b/nf_core/configs/create/create.tcss deleted file mode 100644 index 67394a9de3..0000000000 --- a/nf_core/configs/create/create.tcss +++ /dev/null @@ -1,135 +0,0 @@ -#logo { - width: 100%; - content-align-horizontal: center; - content-align-vertical: middle; -} -.cta { - layout: horizontal; - margin-bottom: 1; -} -.cta Button { - margin: 0 3; -} - -.pipeline-type-grid { - height: auto; - margin-bottom: 2; -} - -.custom_grid { - height: auto; -} -.custom_grid Switch { - width: auto; -} -.custom_grid Static { - width: 1fr; - margin: 1 8; -} -.custom_grid Button { - width: auto; -} - -.field_help { - padding: 1 1 0 1; - color: $text-muted; - text-style: italic; -} -.validation_msg { - padding: 0 1; - color: $error; -} -.-valid { - border: tall $success-darken-3; -} - -Horizontal{ - width: 100%; - height: auto; -} -.column { - width: 1fr; -} - -HorizontalScroll { - width: 100%; -} -.feature_subtitle { - color: grey; -} - -Vertical{ - height: auto; -} - -.features-container { - padding: 0 4 1 4; -} - -/* Display help messages */ - -.help_box { - background: #333333; - padding: 1 3 0 3; - margin: 0 5 2 5; - overflow-y: auto; - transition: height 50ms; - display: none; - height: 0; -} -.displayed .help_box { - display: block; - height: 12; -} -#show_help { - display: block; -} -#hide_help { - display: none; -} -.displayed #show_help { - display: none; -} -.displayed #hide_help { - display: block; -} - -/* Show password */ - -#show_password { - display: block; -} -#hide_password { - display: none; -} -.displayed #show_password { - display: none; -} -.displayed #hide_password { - display: block; -} - -/* Logging console */ - -.log_console { - height: auto; - background: #333333; - padding: 1 3; - margin: 0 4 2 4; -} - -.hide { - display: none; -} - -/* Layouts */ -.col-2 { - grid-size: 2 1; -} - -.ghrepo-cols { - margin: 0 4; -} -.ghrepo-cols Button { - margin-top: 2; -} diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 61099c4206..9d00e037a8 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -1,3 +1,5 @@ +"""Config creation specific functions and classes""" + from typing import Optional from pydantic import BaseModel diff --git a/nf_core/configs/create/welcome.py b/nf_core/configs/create/welcome.py index 94dfe2d955..7bca8100d0 100644 --- a/nf_core/configs/create/welcome.py +++ b/nf_core/configs/create/welcome.py @@ -1,3 +1,5 @@ +"""Intro information to help inform user what we are about to do""" + from textual.app import ComposeResult from textual.containers import Center from textual.screen import Screen From 17971e60243132363173ebf55605e872688e3a77 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Sat, 1 Jun 2024 20:43:28 +0200 Subject: [PATCH 009/121] Fix function calling due to move to generic location --- nf_core/pipelines/create/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nf_core/pipelines/create/__init__.py b/nf_core/pipelines/create/__init__.py index 00bab8de7d..efdb1a3769 100644 --- a/nf_core/pipelines/create/__init__.py +++ b/nf_core/pipelines/create/__init__.py @@ -19,8 +19,8 @@ from nf_core.pipelines.create.welcome import WelcomeScreen from nf_core.utils import CustomLogHandler, LoggingConsole -log_handler = utils.CustomLogHandler( - console=utils.LoggingConsole(classes="log_console"), +log_handler = CustomLogHandler( + console=LoggingConsole(classes="log_console"), rich_tracebacks=True, show_time=False, show_path=False, From 15f5be80d8ebd0ffd1cbe81071cd3f69779dbe09 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Sat, 1 Jun 2024 21:47:32 +0200 Subject: [PATCH 010/121] Copy over @mirpedrol 's writing functions --- nf_core/configs/create/__init__.py | 13 ++- nf_core/configs/create/basicdetails.py | 64 +++++++------- nf_core/configs/create/configtype.py | 12 ++- nf_core/configs/create/create.py | 16 ++++ nf_core/configs/create/final.py | 43 ++++++++++ nf_core/configs/create/utils.py | 112 ++++++++++++++++++++++++- nf_core/pipelines/create/__init__.py | 2 +- 7 files changed, 225 insertions(+), 37 deletions(-) create mode 100644 nf_core/configs/create/create.py create mode 100644 nf_core/configs/create/final.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 90f0ddd9fb..f3ec38c7df 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -9,6 +9,7 @@ ## nf-core question page (screen) imports from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType +from nf_core.configs.create.final import FinalScreen from nf_core.configs.create.utils import CreateConfig from nf_core.configs.create.welcome import WelcomeScreen @@ -47,7 +48,15 @@ class ConfigsCreateApp(App[CreateConfig]): ] ## New question screens (sections) loaded here - SCREENS = {"welcome": WelcomeScreen(), "choose_type": ChooseConfigType(), "basic_details": BasicDetails()} + SCREENS = { + "welcome": WelcomeScreen(), + "choose_type": ChooseConfigType(), + "basic_details": BasicDetails(), + "final": FinalScreen(), + } + + # Initialise config as empty + TEMPLATE_CONFIG = CreateConfig() # Tracking variables CONFIG_TYPE = None @@ -71,6 +80,8 @@ def on_button_pressed(self, event: Button.Pressed) -> None: elif event.button.id == "type_pipeline": self.CONFIG_TYPE = "pipeline" self.push_screen("basic_details") + elif event.button.id == "next": + self.push_screen("final") ## General options if event.button.id == "close_app": self.exit(return_code=0) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 6dd7c95dec..93529f068e 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -3,12 +3,16 @@ from textwrap import dedent +from textual import on from textual.app import ComposeResult from textual.containers import Center, Horizontal from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Markdown +from textual.widgets import Button, Footer, Header, Input, Markdown -from nf_core.pipelines.create.utils import TextInput +from nf_core.configs.create.utils import ( + CreateConfig, + TextInput, +) ## TODO Move somewhere common? config_exists_warn = """ > ⚠️ **The config file you are trying to create already exists.** @@ -33,7 +37,7 @@ def compose(self) -> ComposeResult: ) ## TODO Add validation, .conf already exists? yield TextInput( - "config_name", + "general_config_name", "custom", "Config Name. Used for naming resulting file.", "", @@ -41,29 +45,31 @@ def compose(self) -> ComposeResult: ) with Horizontal(): yield TextInput( - "authorname", + "param_profilecontact", "Boaty McBoatFace", "Author full name.", classes="column", ) yield TextInput( - "authorhandle", + "param_profilecontacthandle", "@BoatyMcBoatFace", "Author Git(Hub) handle.", classes="column", ) yield TextInput( - "description", + "param_configprofiledescription", "Description", "A short description of your config.", ) yield TextInput( - "institutional_url", + "param_configprofileurl", "https://nf-co.re", - "URL of infrastructure website or owning institutional.", - disabled=self.parent.CONFIG_TYPE == "pipeline", ## TODO not working, why? + "URL of infrastructure website or owning institution (only for infrastructure configs).", + disabled=( + self.parent.CONFIG_TYPE == "pipeline" + ), ## TODO update TextInput to accept replace with visibility: https://textual.textualize.io/styles/visibility/ ) ## TODO: reactivate once validation ready # yield Markdown(dedent(config_exists_warn), id="exist_warn", classes="hide") @@ -96,24 +102,22 @@ def compose(self) -> ComposeResult: # if text_input.field_id == "org": # text_input.disabled = self.parent.CONFIG_TYPE == "infrastructure" - # @on(Button.Pressed) - # def on_button_pressed(self, event: Button.Pressed) -> None: - # """Save fields to the config.""" - # config = {} - # for text_input in self.query("TextInput"): - # this_input = text_input.query_one(Input) - # validation_result = this_input.validate(this_input.value) - # config[text_input.field_id] = this_input.value - # if not validation_result.is_valid: - # text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) - # else: - # text_input.query_one(".validation_msg").update("") - # try: - # self.parent.TEMPLATE_CONFIG = CreateConfig(**config) - # if event.button.id == "next": - # if self.parent.CONFIG_TYPE == "infrastructure": - # self.parent.push_screen("type_infrastructure") - # elif self.parent.CONFIG_TYPE == "pipeline": - # self.parent.push_screen("type_pipeline") - # except ValueError: - # pass + ## Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the CreateConfig class) with the values from the text inputs + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update( + "\n".join(validation_result.failure_descriptions) + ) + else: + text_input.query_one(".validation_msg").update("") + try: + self.parent.TEMPLATE_CONFIG = CreateConfig(**config) + except ValueError: + pass diff --git a/nf_core/configs/create/configtype.py b/nf_core/configs/create/configtype.py index 28269cd0df..c0adc1f458 100644 --- a/nf_core/configs/create/configtype.py +++ b/nf_core/configs/create/configtype.py @@ -65,11 +65,19 @@ def compose(self) -> ComposeResult: yield Grid( Center( Markdown(markdown_type_nfcore), - Center(Button("Pipeline config", id="type_infrastructure", variant="success")), + Center( + Button( + "Infrastructure config", + id="type_infrastructure", + variant="success", + ) + ), ), Center( Markdown(markdown_type_custom), - Center(Button("Infrastructure config", id="type_pipeline", variant="primary")), + Center( + Button("Pipeline config", id="type_pipeline", variant="primary") + ), ), classes="col-2 pipeline-type-grid", ) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py new file mode 100644 index 0000000000..0379006ab8 --- /dev/null +++ b/nf_core/configs/create/create.py @@ -0,0 +1,16 @@ +import json + +from nf_core.configs.create.utils import CreateConfig + + +class ConfigCreate: + def __init__(self, template_config: CreateConfig): + self.template_config = template_config + + ## TODO: pull variable and file name so it's using the parameter name -> currently the written json shows that self.template_config.general_config_name is `null` + ## TODO: replace the json.dumping with proper self.template_config parsing and config writing function + + def write_to_file(self): + filename = self.template_config.general_config_name + ".conf" + with open(filename, "w+") as file: + file.write(json.dumps(dict(self.template_config))) diff --git a/nf_core/configs/create/final.py b/nf_core/configs/create/final.py new file mode 100644 index 0000000000..f075faf0b0 --- /dev/null +++ b/nf_core/configs/create/final.py @@ -0,0 +1,43 @@ +from textual import on +from textual.app import ComposeResult +from textual.containers import Center +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.configs.create.create import ( + ConfigCreate, +) +from nf_core.configs.create.utils import TextInput + + +class FinalScreen(Screen): + """A welcome screen for the app.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + """ +# Final step +""" + ) + yield TextInput( + "savelocation", + ".", + "In which directory would you like to save the config?", + ".", + classes="row", + ) + yield Center( + Button("Save and close!", id="close_app", variant="success"), classes="cta" + ) + + def _create_config(self) -> None: + """Create the config.""" + create_obj = ConfigCreate(template_config=self.parent.TEMPLATE_CONFIG) + create_obj.write_to_file() + + @on(Button.Pressed, "#close_app") + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + self._create_config() diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 9d00e037a8..d013d389ff 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -1,11 +1,117 @@ """Config creation specific functions and classes""" -from typing import Optional +from contextlib import contextmanager +from contextvars import ContextVar +from typing import Any, Dict, Iterator, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, ValidationError +from textual import on +from textual.app import ComposeResult +from textual.validation import ValidationResult, Validator +from textual.widgets import Input, Static + +# Use ContextVar to define a context on the model initialization +_init_context_var: ContextVar = ContextVar("_init_context_var", default={}) + + +@contextmanager +def init_context(value: Dict[str, Any]) -> Iterator[None]: + token = _init_context_var.set(value) + try: + yield + finally: + _init_context_var.reset(token) + + +# Define a global variable to store the config type +CONFIG_ISINFRASTRUCTURE_GLOBAL: bool = True class CreateConfig(BaseModel): """Pydantic model for the nf-core create config.""" - config_type: Optional[str] = None + general_config_type: str = None + general_config_name: str = None + param_profilecontact: str = None + param_profilecontacthandle: str = None + param_configprofiledescription: str = None + param_configprofileurl: Optional[str] = None + + model_config = ConfigDict(extra="allow") + + def __init__(self, /, **data: Any) -> None: + """Custom init method to allow using a context on the model initialization.""" + self.__pydantic_validator__.validate_python( + data, + self_instance=self, + context=_init_context_var.get(), + ) + + +## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) +class TextInput(Static): + """Widget for text inputs. + + Provides standard interface for a text input with help text + and validation messages. + """ + + def __init__( + self, field_id, placeholder, description, default=None, password=None, **kwargs + ) -> None: + """Initialise the widget with our values. + + Pass on kwargs upstream for standard usage.""" + super().__init__(**kwargs) + self.field_id: str = field_id + self.id: str = field_id + self.placeholder: str = placeholder + self.description: str = description + self.default: str = default + self.password: bool = password + + def compose(self) -> ComposeResult: + yield Static(self.description, classes="field_help") + yield Input( + placeholder=self.placeholder, + validators=[ValidateConfig(self.field_id)], + value=self.default, + password=self.password, + ) + yield Static(classes="validation_msg") + + @on(Input.Changed) + @on(Input.Submitted) + def show_invalid_reasons( + self, event: Union[Input.Changed, Input.Submitted] + ) -> None: + """Validate the text input and show errors if invalid.""" + if not event.validation_result.is_valid: + self.query_one(".validation_msg").update( + "\n".join(event.validation_result.failure_descriptions) + ) + else: + self.query_one(".validation_msg").update("") + + +## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) + + +class ValidateConfig(Validator): + """Validate any config value, using Pydantic.""" + + def __init__(self, key) -> None: + """Initialise the validator with the model key to validate.""" + super().__init__() + self.key = key + + def validate(self, value: str) -> ValidationResult: + """Try creating a Pydantic object with this key set to this value. + + If it fails, return the error messages.""" + try: + with init_context({"is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL}): + CreateConfig(**{f"{self.key}": value}) + return self.success() + except ValidationError as e: + return self.failure(", ".join([err["msg"] for err in e.errors()])) diff --git a/nf_core/pipelines/create/__init__.py b/nf_core/pipelines/create/__init__.py index efdb1a3769..c11a3ef674 100644 --- a/nf_core/pipelines/create/__init__.py +++ b/nf_core/pipelines/create/__init__.py @@ -58,7 +58,7 @@ class PipelineCreateApp(App[utils.CreateConfig]): } # Initialise config as empty - TEMPLATE_CONFIG = utils.CreateConfig() + TEMPLATE_CONFIG = CreateConfig() # Initialise pipeline type NFCORE_PIPELINE = True From 4af6e9910abe327f1159f8906f1eea8e43859206 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Sun, 2 Jun 2024 15:07:08 +0200 Subject: [PATCH 011/121] Start making config writing function actually write nextflow configs --- nf_core/configs/create/basicdetails.py | 8 ++-- nf_core/configs/create/create.py | 58 ++++++++++++++++++++++++-- nf_core/configs/create/utils.py | 14 +++++-- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 93529f068e..48bb13c18a 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -45,26 +45,26 @@ def compose(self) -> ComposeResult: ) with Horizontal(): yield TextInput( - "param_profilecontact", + "profile_contact", "Boaty McBoatFace", "Author full name.", classes="column", ) yield TextInput( - "param_profilecontacthandle", + "profile_contact_handle", "@BoatyMcBoatFace", "Author Git(Hub) handle.", classes="column", ) yield TextInput( - "param_configprofiledescription", + "config_profile_description", "Description", "A short description of your config.", ) yield TextInput( - "param_configprofileurl", + "config_profile_url", "https://nf-co.re", "URL of infrastructure website or owning institution (only for infrastructure configs).", disabled=( diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 0379006ab8..d26529fe41 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -1,16 +1,66 @@ import json -from nf_core.configs.create.utils import CreateConfig +from nf_core.configs.create.utils import CreateConfig, generate_config_entry class ConfigCreate: def __init__(self, template_config: CreateConfig): self.template_config = template_config - ## TODO: pull variable and file name so it's using the parameter name -> currently the written json shows that self.template_config.general_config_name is `null` - ## TODO: replace the json.dumping with proper self.template_config parsing and config writing function + def construct_contents(self): + parsed_contents = { + "params": { + "config_profile_description": self.template_config.config_profile_description, + "config_profile_contact": "Boaty McBoatFace (@BoatyMcBoatFace)", + } + } + + return parsed_contents def write_to_file(self): + ## File name option filename = self.template_config.general_config_name + ".conf" + + ## Collect all config entries per scope, for later checking scope needs to be written + validparams = { + "config_profile_contact": self.template_config.config_profile_contact, + "config_profile_handle": self.template_config.config_profile_handle, + "config_profile_description": self.template_config.config_profile_description, + } + + print(validparams) + with open(filename, "w+") as file: - file.write(json.dumps(dict(self.template_config))) + + ## Write params + if any(validparams): + file.write("params {\n") + for entry_key, entry_value in validparams.items(): + print(entry_key) + if entry_value is not None: + file.write(generate_config_entry(self, entry_key, entry_value)) + else: + continue + file.write("}\n") + + +# ( +# file.write( +# ' config_profile_contact = "' +# + self.template_config.param_profilecontact +# + " (@" +# + self.template_config.param_profilecontacthandle +# + ')"\n' +# ) +# if self.template_config.param_profilecontact +# else None +# ), +# ( +# file.write( +# ' config_profile_description = "' +# + self.template_config.param_configprofiledescription +# + '"\n' +# ) +# if self.template_config.param_configprofiledescription +# else None +# ), diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index d013d389ff..be59e90b49 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -32,10 +32,10 @@ class CreateConfig(BaseModel): general_config_type: str = None general_config_name: str = None - param_profilecontact: str = None - param_profilecontacthandle: str = None - param_configprofiledescription: str = None - param_configprofileurl: Optional[str] = None + config_profile_contact: str = None + config_profile_handle: str = None + config_profile_description: str = None + config_profile_url: Optional[str] = None model_config = ConfigDict(extra="allow") @@ -115,3 +115,9 @@ def validate(self, value: str) -> ValidationResult: return self.success() except ValidationError as e: return self.failure(", ".join([err["msg"] for err in e.errors()])) + + +def generate_config_entry(self, key, value): + parsed_entry = key + ' = "' + value + '"\n' + print(parsed_entry) + return parsed_entry From 2d7863a4799baaaa6031cbb5b016ff4f53a4f064 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Sat, 8 Jun 2024 11:48:38 +0200 Subject: [PATCH 012/121] Add URL saving, start adding validation: problem unless everything filled in 'NoneType + str' error --- nf_core/configs/create/basicdetails.py | 31 ++---------- nf_core/configs/create/create.py | 65 +++++++++++--------------- nf_core/configs/create/utils.py | 44 +++++++++++++++-- 3 files changed, 71 insertions(+), 69 deletions(-) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 48bb13c18a..3db1c4be28 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -45,14 +45,14 @@ def compose(self) -> ComposeResult: ) with Horizontal(): yield TextInput( - "profile_contact", + "config_profile_contact", "Boaty McBoatFace", "Author full name.", classes="column", ) yield TextInput( - "profile_contact_handle", + "config_profile_handle", "@BoatyMcBoatFace", "Author Git(Hub) handle.", classes="column", @@ -66,42 +66,17 @@ def compose(self) -> ComposeResult: yield TextInput( "config_profile_url", "https://nf-co.re", - "URL of infrastructure website or owning institution (only for infrastructure configs).", + "URL of infrastructure website or owning institution (infrastructure configs only).", disabled=( self.parent.CONFIG_TYPE == "pipeline" ), ## TODO update TextInput to accept replace with visibility: https://textual.textualize.io/styles/visibility/ ) - ## TODO: reactivate once validation ready - # yield Markdown(dedent(config_exists_warn), id="exist_warn", classes="hide") yield Center( Button("Back", id="back", variant="default"), Button("Next", id="next", variant="success"), classes="cta", ) - ## TODO: update functions - # @on(Input.Changed) - # @on(Input.Submitted) - # def show_exists_warn(self): - # """Check if the pipeline exists on every input change or submitted. - # If the pipeline exists, show warning message saying that it will be overriden.""" - # config = {} - # for text_input in self.query("TextInput"): - # this_input = text_input.query_one(Input) - # config[text_input.field_id] = this_input.value - # if Path(config["org"] + "-" + config["name"]).is_dir(): - # remove_hide_class(self.parent, "exist_warn") - # else: - # add_hide_class(self.parent, "exist_warn") - - # def on_screen_resume(self): - # """Hide warn message on screen resume. - # Update displayed value on screen resume.""" - # add_hide_class(self.parent, "exist_warn") - # for text_input in self.query("TextInput"): - # if text_input.field_id == "org": - # text_input.disabled = self.parent.CONFIG_TYPE == "infrastructure" - ## Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the CreateConfig class) with the values from the text inputs @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index d26529fe41..8d1da0753e 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -7,28 +7,40 @@ class ConfigCreate: def __init__(self, template_config: CreateConfig): self.template_config = template_config - def construct_contents(self): - parsed_contents = { - "params": { - "config_profile_description": self.template_config.config_profile_description, - "config_profile_contact": "Boaty McBoatFace (@BoatyMcBoatFace)", - } - } + def construct_params(self, contact, handle, description, url): + final_params = {} - return parsed_contents + if contact != "" or not None: + if handle != "" or not None: + config_contact = contact + " (" + handle + ")" + else: + config_contact = contact + final_params["config_profile_contact"] = config_contact + elif handle != "" or not None: + final_params["config_contact"] = handle + else: + pass + + if description != "" or not None: + final_params["config_profile_description"] = description + + if url != "" or not None: + final_params["config_profile_url"] = url + + return final_params def write_to_file(self): ## File name option + print(self.template_config) filename = self.template_config.general_config_name + ".conf" ## Collect all config entries per scope, for later checking scope needs to be written - validparams = { - "config_profile_contact": self.template_config.config_profile_contact, - "config_profile_handle": self.template_config.config_profile_handle, - "config_profile_description": self.template_config.config_profile_description, - } - - print(validparams) + validparams = self.construct_params( + self.template_config.config_profile_contact, + self.template_config.config_profile_handle, + self.template_config.config_profile_description, + self.template_config.config_profile_url, + ) with open(filename, "w+") as file: @@ -36,31 +48,8 @@ def write_to_file(self): if any(validparams): file.write("params {\n") for entry_key, entry_value in validparams.items(): - print(entry_key) if entry_value is not None: file.write(generate_config_entry(self, entry_key, entry_value)) else: continue file.write("}\n") - - -# ( -# file.write( -# ' config_profile_contact = "' -# + self.template_config.param_profilecontact -# + " (@" -# + self.template_config.param_profilecontacthandle -# + ')"\n' -# ) -# if self.template_config.param_profilecontact -# else None -# ), -# ( -# file.write( -# ' config_profile_description = "' -# + self.template_config.param_configprofiledescription -# + '"\n' -# ) -# if self.template_config.param_configprofiledescription -# else None -# ), diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index be59e90b49..b010c02301 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -1,10 +1,12 @@ """Config creation specific functions and classes""" +import re + from contextlib import contextmanager from contextvars import ContextVar from typing import Any, Dict, Iterator, Optional, Union -from pydantic import BaseModel, ConfigDict, ValidationError +from pydantic import BaseModel, ConfigDict, ValidationError, field_validator from textual import on from textual.app import ComposeResult from textual.validation import ValidationResult, Validator @@ -47,6 +49,43 @@ def __init__(self, /, **data: Any) -> None: context=_init_context_var.get(), ) + @field_validator( + "general_config_name", + ) + @classmethod + def notempty(cls, v: str) -> str: + """Check that string values are not empty.""" + if v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + + @field_validator( + "config_profile_handle", + ) + @classmethod + def handle_prefix(cls, v: str) -> str: + """Check that GitHub handles start with '@'.""" + if not re.match( + r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v + ): ## Regex from: https://github.com/shinnn/github-username-regex + raise ValueError("Handle must start with '@'.") + return v + + @field_validator( + "config_profile_url", + ) + @classmethod + def url_prefix(cls, v: str) -> str: + """Check that institutional web links start with valid URL prefix.""" + if not re.match( + r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", + v, + ): ## Regex from: https://stackoverflow.com/a/3809435 + raise ValueError( + "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." + ) + return v + ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) class TextInput(Static): @@ -118,6 +157,5 @@ def validate(self, value: str) -> ValidationResult: def generate_config_entry(self, key, value): - parsed_entry = key + ' = "' + value + '"\n' - print(parsed_entry) + parsed_entry = " " + key + ' = "' + value + '"\n' return parsed_entry From f0cb5e9ca9508c27d4556740f3926af290f1f43e Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Sun, 30 Jun 2024 11:55:33 +0200 Subject: [PATCH 013/121] Small debugging, now know the issue --- nf_core/configs/create/create.py | 4 ++++ nf_core/configs/create/utils.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 8d1da0753e..9cd27ade17 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -23,10 +23,14 @@ def construct_params(self, contact, handle, description, url): if description != "" or not None: final_params["config_profile_description"] = description + else: + pass if url != "" or not None: final_params["config_profile_url"] = url + print("final_params") + print(final_params) return final_params def write_to_file(self): diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index b010c02301..41591a07ca 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -35,8 +35,8 @@ class CreateConfig(BaseModel): general_config_type: str = None general_config_name: str = None config_profile_contact: str = None - config_profile_handle: str = None - config_profile_description: str = None + config_profile_handle: Optional[str] = None + config_profile_description: Optional[str] = None config_profile_url: Optional[str] = None model_config = ConfigDict(extra="allow") From 66227b7b22cf11e8cb9916a1616763eadfb070f1 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Sun, 30 Jun 2024 14:13:27 +0200 Subject: [PATCH 014/121] Fixing writing of parameters to the input file when no input from user --- nf_core/configs/create/create.py | 24 ++++++++------- nf_core/configs/create/utils.py | 52 ++++++++++++++++---------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 9cd27ade17..333e79e907 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -10,23 +10,22 @@ def __init__(self, template_config: CreateConfig): def construct_params(self, contact, handle, description, url): final_params = {} - if contact != "" or not None: - if handle != "" or not None: + print("c:" + contact) + print("h: " + handle) + + if contact != "": + if handle != "": config_contact = contact + " (" + handle + ")" else: config_contact = contact final_params["config_profile_contact"] = config_contact - elif handle != "" or not None: - final_params["config_contact"] = handle - else: - pass + elif handle != "": + final_params["config_profile_contact"] = handle - if description != "" or not None: + if description != "": final_params["config_profile_description"] = description - else: - pass - if url != "" or not None: + if url != "": final_params["config_profile_url"] = url print("final_params") @@ -46,13 +45,16 @@ def write_to_file(self): self.template_config.config_profile_url, ) + print("validparams") + print(validparams) + with open(filename, "w+") as file: ## Write params if any(validparams): file.write("params {\n") for entry_key, entry_value in validparams.items(): - if entry_value is not None: + if entry_value != "": file.write(generate_config_entry(self, entry_key, entry_value)) else: continue diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 41591a07ca..de961723ea 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -59,32 +59,32 @@ def notempty(cls, v: str) -> str: raise ValueError("Cannot be left empty.") return v - @field_validator( - "config_profile_handle", - ) - @classmethod - def handle_prefix(cls, v: str) -> str: - """Check that GitHub handles start with '@'.""" - if not re.match( - r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v - ): ## Regex from: https://github.com/shinnn/github-username-regex - raise ValueError("Handle must start with '@'.") - return v - - @field_validator( - "config_profile_url", - ) - @classmethod - def url_prefix(cls, v: str) -> str: - """Check that institutional web links start with valid URL prefix.""" - if not re.match( - r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", - v, - ): ## Regex from: https://stackoverflow.com/a/3809435 - raise ValueError( - "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." - ) - return v + # @field_validator( + # "config_profile_handle", + # ) + # @classmethod + # def handle_prefix(cls, v: str) -> str: + # """Check that GitHub handles start with '@'.""" + # if not re.match( + # r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v + # ): ## Regex from: https://github.com/shinnn/github-username-regex + # raise ValueError("Handle must start with '@'.") + # return v + + # @field_validator( + # "config_profile_url", + # ) + # @classmethod + # def url_prefix(cls, v: str) -> str: + # """Check that institutional web links start with valid URL prefix.""" + # if not re.match( + # r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", + # v, + # ): ## Regex from: https://stackoverflow.com/a/3809435 + # raise ValueError( + # "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." + # ) + # return v ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) From 46c4c0a2069e03f2e60e3e6bad500e7aa9eb602a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Wed, 2 Apr 2025 12:02:53 +0200 Subject: [PATCH 015/121] Add back configs create command Co-authored-by: James A. Fellows Yates --- nf_core/__main__.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/nf_core/__main__.py b/nf_core/__main__.py index 0af7ace992..50eb55fa06 100644 --- a/nf_core/__main__.py +++ b/nf_core/__main__.py @@ -1847,6 +1847,37 @@ def command_subworkflows_update( ) +# nf-core configs subcommands +@nf_core_cli.group() +@click.pass_context +def configs(ctx): + """ + Commands to manage nf-core configs. + """ + # ensure that ctx.obj exists and is a dict (in case `cli()` is called + # by means other than the `if` block below) + ctx.ensure_object(dict) + + +# nf-core configs create +@configs.command("create") +@click.pass_context +def create_configs(ctx): + """ + Command to interactively create a nextflow or nf-core config + """ + from nf_core.configs.create import ConfigsCreateApp + + try: + log.info("Launching interactive nf-core configs creation tool.") + app = ConfigsCreateApp() + app.run() + sys.exit(app.return_code or 0) + except UserWarning as e: + log.error(e) + sys.exit(1) + + ## DEPRECATED commands since v3.0.0 From dc22d8f4f2f6fcbd8fa8a6f9a103e6bb6b0efd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Wed, 2 Apr 2025 13:31:04 +0200 Subject: [PATCH 016/121] update to new version of textual and rename CreateConfig classes --- nf_core/configs/create/__init__.py | 14 +++++++------- nf_core/configs/create/basicdetails.py | 6 +++--- nf_core/configs/create/create.py | 8 ++++++-- nf_core/configs/create/utils.py | 6 +++--- nf_core/pipelines/create/__init__.py | 5 ++--- nf_core/pipelines/create/basicdetails.py | 4 ++-- nf_core/pipelines/create/create.py | 18 +++++++++--------- nf_core/pipelines/create/utils.py | 6 +++--- 8 files changed, 35 insertions(+), 32 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 7a8ea151b1..cb78680d5d 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -13,7 +13,7 @@ from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType from nf_core.configs.create.final import FinalScreen -from nf_core.configs.create.utils import CreateConfig +from nf_core.configs.create.utils import ConfigsCreateConfig from nf_core.configs.create.welcome import WelcomeScreen ## General utilities @@ -34,7 +34,7 @@ ## Main workflow -class ConfigsCreateApp(App[CreateConfig]): +class ConfigsCreateApp(App[ConfigsCreateConfig]): """A Textual app to create nf-core configs.""" CSS_PATH = "../../textual.tcss" @@ -47,14 +47,14 @@ class ConfigsCreateApp(App[CreateConfig]): ## New question screens (sections) loaded here SCREENS = { - "welcome": WelcomeScreen(), - "choose_type": ChooseConfigType(), - "basic_details": BasicDetails(), - "final": FinalScreen(), + "welcome": WelcomeScreen, + "choose_type": ChooseConfigType, + "basic_details": BasicDetails, + "final": FinalScreen, } # Initialise config as empty - TEMPLATE_CONFIG = CreateConfig() + TEMPLATE_CONFIG = ConfigsCreateConfig() # Tracking variables CONFIG_TYPE = None diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 826ec87ef8..e59a7f0a08 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -10,7 +10,7 @@ from textual.widgets import Button, Footer, Header, Input, Markdown from nf_core.configs.create.utils import ( - CreateConfig, + ConfigsCreateConfig, TextInput, ) ## TODO Move somewhere common? @@ -77,7 +77,7 @@ def compose(self) -> ComposeResult: classes="cta", ) - ## Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the CreateConfig class) with the values from the text inputs + ## Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" @@ -91,6 +91,6 @@ def on_button_pressed(self, event: Button.Pressed) -> None: else: text_input.query_one(".validation_msg").update("") try: - self.parent.TEMPLATE_CONFIG = CreateConfig(**config) + self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) except ValueError: pass diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 0f49d6c617..f6b6f90b97 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -1,8 +1,12 @@ -from nf_core.configs.create.utils import CreateConfig, generate_config_entry +"""Creates a nextflow config matching the current +nf-core organization specification. +""" + +from nf_core.configs.create.utils import ConfigsCreateConfig, generate_config_entry class ConfigCreate: - def __init__(self, template_config: CreateConfig): + def __init__(self, template_config: ConfigsCreateConfig): self.template_config = template_config def construct_params(self, contact, handle, description, url): diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 9eccdc265a..ed86085f61 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -27,8 +27,8 @@ def init_context(value: Dict[str, Any]) -> Iterator[None]: CONFIG_ISINFRASTRUCTURE_GLOBAL: bool = True -class CreateConfig(BaseModel): - """Pydantic model for the nf-core create config.""" +class ConfigsCreateConfig(BaseModel): + """Pydantic model for the nf-core configs create config.""" general_config_type: Optional[str] = None general_config_name: Optional[str] = None @@ -142,7 +142,7 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: with init_context({"is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL}): - CreateConfig(**{f"{self.key}": value}) + ConfigsCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: return self.failure(", ".join([err["msg"] for err in e.errors()])) diff --git a/nf_core/pipelines/create/__init__.py b/nf_core/pipelines/create/__init__.py index 1b4cb8eff9..bb103ce1fa 100644 --- a/nf_core/pipelines/create/__init__.py +++ b/nf_core/pipelines/create/__init__.py @@ -17,7 +17,6 @@ from nf_core.pipelines.create.loggingscreen import LoggingScreen from nf_core.pipelines.create.nfcorepipeline import NfcorePipeline from nf_core.pipelines.create.pipelinetype import ChoosePipelineType -from nf_core.pipelines.create.utils import CreateConfig from nf_core.pipelines.create.welcome import WelcomeScreen from nf_core.utils import LoggingConsole @@ -34,7 +33,7 @@ logger.addHandler(rich_log_handler) -class PipelineCreateApp(App[utils.CreateConfig]): +class PipelineCreateApp(App[utils.PipelinesCreateConfig]): """A Textual app to manage stopwatches.""" CSS_PATH = "../../textual.tcss" @@ -59,7 +58,7 @@ class PipelineCreateApp(App[utils.CreateConfig]): } # Initialise config as empty - TEMPLATE_CONFIG = CreateConfig() + TEMPLATE_CONFIG = utils.PipelinesCreateConfig() # Initialise pipeline type NFCORE_PIPELINE = True diff --git a/nf_core/pipelines/create/basicdetails.py b/nf_core/pipelines/create/basicdetails.py index c222e1a80e..e511945d9a 100644 --- a/nf_core/pipelines/create/basicdetails.py +++ b/nf_core/pipelines/create/basicdetails.py @@ -9,7 +9,7 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown -from nf_core.pipelines.create.utils import CreateConfig, TextInput +from nf_core.pipelines.create.utils import PipelinesCreateConfig, TextInput from nf_core.utils import add_hide_class, remove_hide_class pipeline_exists_warn = """ @@ -101,7 +101,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: else: text_input.query_one(".validation_msg").update("") try: - self.parent.TEMPLATE_CONFIG = CreateConfig(**config) + self.parent.TEMPLATE_CONFIG = PipelinesCreateConfig(**config) if event.button.id == "next": if self.parent.NFCORE_PIPELINE: self.parent.push_screen("type_nfcore") diff --git a/nf_core/pipelines/create/create.py b/nf_core/pipelines/create/create.py index 1800a5f10e..97a3140423 100644 --- a/nf_core/pipelines/create/create.py +++ b/nf_core/pipelines/create/create.py @@ -18,7 +18,7 @@ import nf_core import nf_core.pipelines.schema import nf_core.utils -from nf_core.pipelines.create.utils import CreateConfig, features_yml_path, load_features_yaml +from nf_core.pipelines.create.utils import PipelinesCreateConfig, features_yml_path, load_features_yaml from nf_core.pipelines.create_logo import create_logo from nf_core.pipelines.lint_utils import run_prettier_on_file from nf_core.pipelines.rocrate import ROCrate @@ -39,7 +39,7 @@ class PipelineCreate: force (bool): Overwrites a given workflow directory with the same name. Defaults to False. Used for tests and sync command. May the force be with you. outdir (str): Path to the local output directory. - template_config (str|CreateConfig): Path to template.yml file for pipeline creation settings. or pydantic model with the customisation for pipeline creation settings. + template_config (str|PipelinesCreateConfig): Path to template.yml file for pipeline creation settings. or pydantic model with the customisation for pipeline creation settings. organisation (str): Name of the GitHub organisation to create the pipeline. Will be the prefix of the pipeline. from_config_file (bool): If true the pipeline will be created from the `.nf-core.yml` config file. Used for tests and sync command. default_branch (str): Specifies the --initial-branch name. @@ -54,21 +54,21 @@ def __init__( no_git: bool = False, force: bool = False, outdir: Optional[Union[Path, str]] = None, - template_config: Optional[Union[CreateConfig, str, Path]] = None, + template_config: Optional[Union[PipelinesCreateConfig, str, Path]] = None, organisation: str = "nf-core", from_config_file: bool = False, default_branch: str = "master", is_interactive: bool = False, ) -> None: - if isinstance(template_config, CreateConfig): + if isinstance(template_config, PipelinesCreateConfig): self.config = template_config elif from_config_file: # Try reading config file try: _, config_yml = nf_core.utils.load_tools_config(outdir if outdir else Path().cwd()) - # Obtain a CreateConfig object from `.nf-core.yml` config file + # Obtain a PipelinesCreateConfig object from `.nf-core.yml` config file if config_yml is not None and getattr(config_yml, "template", None) is not None: - self.config = CreateConfig(**config_yml["template"].model_dump(exclude_none=True)) + self.config = PipelinesCreateConfig(**config_yml["template"].model_dump(exclude_none=True)) else: raise UserWarning("The template configuration was not provided in '.nf-core.yml'.") # Update the output directory @@ -78,7 +78,7 @@ def __init__( elif (name and description and author) or ( template_config and (isinstance(template_config, str) or isinstance(template_config, Path)) ): - # Obtain a CreateConfig object from the template yaml file + # Obtain a PipelinesCreateConfig object from the template yaml file self.config = self.check_template_yaml_info(template_config, name, description, author) self.update_config(organisation, version, force, outdir) else: @@ -140,12 +140,12 @@ def check_template_yaml_info(self, template_yaml, name, description, author): UserWarning: if template yaml file does not exist. """ # Obtain template customization info from template yaml file or `.nf-core.yml` config file - config = CreateConfig() + config = PipelinesCreateConfig() if template_yaml: try: with open(template_yaml) as f: template_yaml = yaml.safe_load(f) - config = CreateConfig(**template_yaml) + config = PipelinesCreateConfig(**template_yaml) except FileNotFoundError: raise UserWarning(f"Template YAML file '{template_yaml}' not found.") diff --git a/nf_core/pipelines/create/utils.py b/nf_core/pipelines/create/utils.py index 56c13ed881..a0ce68567e 100644 --- a/nf_core/pipelines/create/utils.py +++ b/nf_core/pipelines/create/utils.py @@ -35,8 +35,8 @@ def init_context(value: Dict[str, Any]) -> Iterator[None]: features_yml_path = Path(nf_core.__file__).parent / "pipelines" / "create" / "template_features.yml" -class CreateConfig(NFCoreTemplateConfig): - """Pydantic model for the nf-core create config.""" +class PipelinesCreateConfig(NFCoreTemplateConfig): + """Pydantic model for the nf-core pipelines create config.""" model_config = ConfigDict(extra="allow") @@ -150,7 +150,7 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: with init_context({"is_nfcore": NFCORE_PIPELINE_GLOBAL}): - CreateConfig(**{f"{self.key}": value}) + PipelinesCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: return self.failure(", ".join([err["msg"] for err in e.errors()])) From 544fd89685e3087e36a9227f7c5346b6ff81e494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Wed, 2 Apr 2025 15:15:21 +0200 Subject: [PATCH 017/121] add screen asking if the config is nf-core --- nf_core/configs/create/__init__.py | 11 +++- nf_core/configs/create/nfcorequestion.py | 64 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 nf_core/configs/create/nfcorequestion.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index cb78680d5d..2acb70e37e 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -13,6 +13,7 @@ from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType from nf_core.configs.create.final import FinalScreen +from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.utils import ConfigsCreateConfig from nf_core.configs.create.welcome import WelcomeScreen @@ -49,6 +50,7 @@ class ConfigsCreateApp(App[ConfigsCreateConfig]): SCREENS = { "welcome": WelcomeScreen, "choose_type": ChooseConfigType, + "nfcore_question": ChooseNfcoreConfig, "basic_details": BasicDetails, "final": FinalScreen, } @@ -58,6 +60,7 @@ class ConfigsCreateApp(App[ConfigsCreateConfig]): # Tracking variables CONFIG_TYPE = None + NFCORE_CONFIG = None # Log handler LOG_HANDLER = rich_log_handler @@ -74,9 +77,15 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.push_screen("choose_type") elif event.button.id == "type_infrastructure": self.CONFIG_TYPE = "infrastructure" + self.push_screen("nfcore_question") + elif event.button.id == "type_nfcore": + self.NFCORE_CONFIG = True self.push_screen("basic_details") elif event.button.id == "type_pipeline": self.CONFIG_TYPE = "pipeline" + self.push_screen("nfcore_question") + elif event.button.id == "type_custom": + self.NFCORE_CONFIG = False self.push_screen("basic_details") elif event.button.id == "next": self.push_screen("final") @@ -89,4 +98,4 @@ def on_button_pressed(self, event: Button.Pressed) -> None: ## User theme options def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" - self.dark: bool = not self.dark + self.theme: str = "textual-dark" if self.theme == "textual-light" else "textual-light" diff --git a/nf_core/configs/create/nfcorequestion.py b/nf_core/configs/create/nfcorequestion.py new file mode 100644 index 0000000000..4775297135 --- /dev/null +++ b/nf_core/configs/create/nfcorequestion.py @@ -0,0 +1,64 @@ +from textual.app import ComposeResult +from textual.containers import Center, Grid +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +markdown_intro = """ +# Is this configuration file part of the nf-core organisation? +""" + +markdown_type_nfcore = """ +## Choose _"nf-core"_ if: + +Infrastructure configs: +* You want to add the configuration file to the nf-core/configs repository. + +Pipeline configs: +* The configuration file is for an nf-core pipeline. +""" +markdown_type_custom = """ +## Choose _"Custom"_ if: + +All configs: +* You want full control over *all* parameters or options in the config + (including those that are mandatory for nf-core). + +Infrastructure configs: +* You will _never_ add the configuration file to the nf-core/configs repository. + +Pipeline configs: +* The configuration file is for a custom pipeline which will _never_ be part of nf-core. +""" + +markdown_details = """ +## What's the difference? + +Choosing _"nf-core"_ will make the following of the options mandatory: + +Infrastructure configs: +* Providing the name and github handle of the author and contact person. +* Providing a description of the config. +* Providing the URL of the owning institution. +* Setting up `resourceLimits` to set the maximum resources. +""" + + +class ChooseNfcoreConfig(Screen): + """Choose whether this will be an nf-core config or not.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield Grid( + Center( + Markdown(markdown_type_nfcore), + Center(Button("nf-core", id="type_nfcore", variant="success")), + ), + Center( + Markdown(markdown_type_custom), + Center(Button("Custom", id="type_custom", variant="primary")), + ), + classes="col-2 pipeline-type-grid", + ) + yield Markdown(markdown_details) From 33e88ddd0d6bf55958d20c0fbfc0632380776d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Wed, 2 Apr 2025 15:31:41 +0200 Subject: [PATCH 018/121] add validation of basicdetails --- nf_core/configs/create/__init__.py | 2 -- nf_core/configs/create/basicdetails.py | 2 ++ nf_core/configs/create/utils.py | 43 ++++++++++++++++++-------- nf_core/utils.py | 2 +- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 2acb70e37e..8d798d4313 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -87,8 +87,6 @@ def on_button_pressed(self, event: Button.Pressed) -> None: elif event.button.id == "type_custom": self.NFCORE_CONFIG = False self.push_screen("basic_details") - elif event.button.id == "next": - self.push_screen("final") ## General options if event.button.id == "close_app": self.exit(return_code=0) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index e59a7f0a08..649d1b1b88 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -92,5 +92,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: text_input.query_one(".validation_msg").update("") try: self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) + if event.button.id == "next": + self.parent.push_screen("final") except ValueError: pass diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index ed86085f61..95cd0b120b 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict, ValidationError, field_validator from textual import on from textual.app import ComposeResult +from textual.containers import Grid from textual.validation import ValidationResult, Validator from textual.widgets import Input, Static @@ -25,17 +26,26 @@ def init_context(value: Dict[str, Any]) -> Iterator[None]: # Define a global variable to store the config type CONFIG_ISINFRASTRUCTURE_GLOBAL: bool = True +NFCORE_CONFIG_GLOBAL: bool = True class ConfigsCreateConfig(BaseModel): """Pydantic model for the nf-core configs create config.""" general_config_type: Optional[str] = None + """ Config file type (infrastructure or pipeline) """ general_config_name: Optional[str] = None + """ Config name """ config_profile_contact: Optional[str] = None + """ Config contact name """ config_profile_handle: Optional[str] = None + """ Config contact GitHub handle """ config_profile_description: Optional[str] = None + """ Config description """ config_profile_url: Optional[str] = None + """ Config institution URL """ + is_nfcore: Optional[bool] = None + """ Whether the config is part of the nf-core organisation """ model_config = ConfigDict(extra="allow") @@ -106,28 +116,35 @@ def __init__(self, field_id, placeholder, description, default=None, password=No self.password: bool = password def compose(self) -> ComposeResult: - yield Static(self.description, classes="field_help") - yield Input( - placeholder=self.placeholder, - validators=[ValidateConfig(self.field_id)], - value=self.default, - password=self.password, + yield Grid( + Static(self.description, classes="field_help"), + Input( + placeholder=self.placeholder, + validators=[ValidateConfig(self.field_id)], + value=self.default, + password=self.password, + ), + Static(classes="validation_msg"), + classes="text-input-grid", ) - yield Static(classes="validation_msg") @on(Input.Changed) @on(Input.Submitted) def show_invalid_reasons(self, event: Union[Input.Changed, Input.Submitted]) -> None: """Validate the text input and show errors if invalid.""" - if not event.validation_result.is_valid: - self.query_one(".validation_msg").update("\n".join(event.validation_result.failure_descriptions)) + val_msg = self.query_one(".validation_msg") + if not isinstance(val_msg, Static): + raise ValueError("Validation message not found.") + + if event.validation_result is not None and not event.validation_result.is_valid: + # check that val_msg is instance of Static + if isinstance(val_msg, Static): + val_msg.update("\n".join(event.validation_result.failure_descriptions)) else: - self.query_one(".validation_msg").update("") + val_msg.update("") ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) - - class ValidateConfig(Validator): """Validate any config value, using Pydantic.""" @@ -141,7 +158,7 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: - with init_context({"is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL}): + with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL}): ConfigsCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: diff --git a/nf_core/utils.py b/nf_core/utils.py index dd02dae55b..9faa0a2d3f 100644 --- a/nf_core/utils.py +++ b/nf_core/utils.py @@ -1143,7 +1143,7 @@ class NFCoreTemplateConfig(BaseModel): skip_features: Optional[list] = None """ Skip features. See https://nf-co.re/docs/nf-core-tools/pipelines/create for a list of features. """ is_nfcore: Optional[bool] = None - """ Whether the pipeline is an nf-core pipeline. """ + """ Whether the pipeline is an nf-core pipeline """ # convert outdir to str @field_validator("outdir") From 227abc20a059ec9175b72473ddece7cb88796804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Wed, 2 Apr 2025 17:07:49 +0200 Subject: [PATCH 019/121] conditional validation for nf-core configs --- nf_core/configs/create/__init__.py | 11 ++-- nf_core/configs/create/basicdetails.py | 1 + nf_core/configs/create/utils.py | 87 ++++++++++++++++++-------- 3 files changed, 68 insertions(+), 31 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 8d798d4313..e6eab1933c 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -9,12 +9,13 @@ from textual.app import App from textual.widgets import Button +from nf_core.configs.create import utils + ## nf-core question page (screen) imports from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType from nf_core.configs.create.final import FinalScreen from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig -from nf_core.configs.create.utils import ConfigsCreateConfig from nf_core.configs.create.welcome import WelcomeScreen ## General utilities @@ -35,7 +36,7 @@ ## Main workflow -class ConfigsCreateApp(App[ConfigsCreateConfig]): +class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): """A Textual app to create nf-core configs.""" CSS_PATH = "../../textual.tcss" @@ -56,11 +57,11 @@ class ConfigsCreateApp(App[ConfigsCreateConfig]): } # Initialise config as empty - TEMPLATE_CONFIG = ConfigsCreateConfig() + TEMPLATE_CONFIG = utils.ConfigsCreateConfig() # Tracking variables CONFIG_TYPE = None - NFCORE_CONFIG = None + NFCORE_CONFIG = True # Log handler LOG_HANDLER = rich_log_handler @@ -80,12 +81,14 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.push_screen("nfcore_question") elif event.button.id == "type_nfcore": self.NFCORE_CONFIG = True + utils.NFCORE_CONFIG_GLOBAL = True self.push_screen("basic_details") elif event.button.id == "type_pipeline": self.CONFIG_TYPE = "pipeline" self.push_screen("nfcore_question") elif event.button.id == "type_custom": self.NFCORE_CONFIG = False + utils.NFCORE_CONFIG_GLOBAL = False self.push_screen("basic_details") ## General options if event.button.id == "close_app": diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 649d1b1b88..536f5dc232 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -85,6 +85,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: for text_input in self.query("TextInput"): this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) + print(f"validation result {validation_result}") config[text_input.field_id] = this_input.value if not validation_result.is_valid: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 95cd0b120b..c053a22228 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -1,10 +1,11 @@ """Config creation specific functions and classes""" +import re from contextlib import contextmanager from contextvars import ContextVar from typing import Any, Dict, Iterator, Optional, Union -from pydantic import BaseModel, ConfigDict, ValidationError, field_validator +from pydantic import BaseModel, ConfigDict, ValidationError, ValidationInfo, field_validator from textual import on from textual.app import ComposeResult from textual.containers import Grid @@ -67,32 +68,63 @@ def notempty(cls, v: str) -> str: raise ValueError("Cannot be left empty.") return v - # @field_validator( - # "config_profile_handle", - # ) - # @classmethod - # def handle_prefix(cls, v: str) -> str: - # """Check that GitHub handles start with '@'.""" - # if not re.match( - # r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v - # ): ## Regex from: https://github.com/shinnn/github-username-regex - # raise ValueError("Handle must start with '@'.") - # return v - - # @field_validator( - # "config_profile_url", - # ) - # @classmethod - # def url_prefix(cls, v: str) -> str: - # """Check that institutional web links start with valid URL prefix.""" - # if not re.match( - # r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", - # v, - # ): ## Regex from: https://stackoverflow.com/a/3809435 - # raise ValueError( - # "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." - # ) - # return v + @field_validator("config_profile_contact", "config_profile_description") + @classmethod + def notempty_nfcore(cls, v: str, info: ValidationInfo) -> str: + """Check that string values are not empty when the config is nf-core.""" + context = info.context + if context and context["is_nfcore"]: + print("here") + if v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + + @field_validator( + "config_profile_handle", + ) + @classmethod + def handle_prefix(cls, v: str, info: ValidationInfo) -> str: + """Check that GitHub handles start with '@'. + Make providing a handle mandatory for nf-core configs""" + context = info.context + if context and context["is_nfcore"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + elif not re.match( + r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v + ): ## Regex from: https://github.com/shinnn/github-username-regex + raise ValueError("Handle must start with '@'.") + else: + if not v.strip() == "" and not re.match(r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v): + raise ValueError("Handle must start with '@'.") + return v + + @field_validator( + "config_profile_url", + ) + @classmethod + def url_prefix(cls, v: str, info: ValidationInfo) -> str: + """Check that institutional web links start with valid URL prefix.""" + context = info.context + if context and context["is_nfcore"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + elif not re.match( + r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", + v, + ): ## Regex from: https://stackoverflow.com/a/3809435 + raise ValueError( + "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." + ) + else: + if not v.strip() == "" and not re.match( + r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", + v, + ): ## Regex from: https://stackoverflow.com/a/3809435 + raise ValueError( + "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." + ) + return v ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) @@ -159,6 +191,7 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL}): + print(f"global config: {NFCORE_CONFIG_GLOBAL}") ConfigsCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: From 48ce7a5b73b837a62d11b2ac4f787b3d5edd4d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Mon, 5 May 2025 14:25:26 +0200 Subject: [PATCH 020/121] remove config author and url from pipeline configs --- nf_core/configs/create/basicdetails.py | 45 ++++++++++++------------ nf_core/configs/create/create.py | 21 ++++------- nf_core/configs/create/nfcorequestion.py | 1 + nf_core/configs/create/utils.py | 2 -- 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 536f5dc232..7ca6253a55 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -43,34 +43,36 @@ def compose(self) -> ComposeResult: "", classes="column", ) - with Horizontal(): - yield TextInput( - "config_profile_contact", - "Boaty McBoatFace", - "Author full name.", - classes="column", - ) + if self.parent.CONFIG_TYPE == "infrastructure": + with Horizontal(): + yield TextInput( + "config_profile_contact", + "Boaty McBoatFace", + "Author full name.", + classes="column", + ) - yield TextInput( - "config_profile_handle", - "@BoatyMcBoatFace", - "Author Git(Hub) handle.", - classes="column", - ) + yield TextInput( + "config_profile_handle", + "@BoatyMcBoatFace", + "Author Git(Hub) handle.", + classes="column", + ) yield TextInput( "config_profile_description", "Description", "A short description of your config.", ) - yield TextInput( - "config_profile_url", - "https://nf-co.re", - "URL of infrastructure website or owning institution (infrastructure configs only).", - disabled=( - self.parent.CONFIG_TYPE == "pipeline" - ), ## TODO update TextInput to accept replace with visibility: https://textual.textualize.io/styles/visibility/ - ) + if self.parent.CONFIG_TYPE == "infrastructure": + yield TextInput( + "config_profile_url", + "https://nf-co.re", + "URL of infrastructure website or owning institution (infrastructure configs only).", + disabled=( + self.parent.CONFIG_TYPE == "pipeline" + ), ## TODO update TextInput to accept replace with visibility: https://textual.textualize.io/styles/visibility/ + ) yield Center( Button("Back", id="back", variant="default"), Button("Next", id="next", variant="success"), @@ -85,7 +87,6 @@ def on_button_pressed(self, event: Button.Pressed) -> None: for text_input in self.query("TextInput"): this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) - print(f"validation result {validation_result}") config[text_input.field_id] = this_input.value if not validation_result.is_valid: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index f6b6f90b97..0d58551089 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -12,32 +12,26 @@ def __init__(self, template_config: ConfigsCreateConfig): def construct_params(self, contact, handle, description, url): final_params = {} - print("c:" + contact) - print("h: " + handle) - - if contact != "": - if handle != "": + if contact is not None: + if handle is not None: config_contact = contact + " (" + handle + ")" else: config_contact = contact final_params["config_profile_contact"] = config_contact - elif handle != "": + elif handle is not None: final_params["config_profile_contact"] = handle - if description != "": + if description is not None: final_params["config_profile_description"] = description - if url != "": + if url is not None: final_params["config_profile_url"] = url - print("final_params") - print(final_params) return final_params def write_to_file(self): ## File name option - print(self.template_config) - filename = self.template_config.general_config_name + ".conf" + filename = "_".join(self.template_config.general_config_name) + ".conf" ## Collect all config entries per scope, for later checking scope needs to be written validparams = self.construct_params( @@ -47,9 +41,6 @@ def write_to_file(self): self.template_config.config_profile_url, ) - print("validparams") - print(validparams) - with open(filename, "w+") as file: ## Write params if any(validparams): diff --git a/nf_core/configs/create/nfcorequestion.py b/nf_core/configs/create/nfcorequestion.py index 4775297135..f93ad035a9 100644 --- a/nf_core/configs/create/nfcorequestion.py +++ b/nf_core/configs/create/nfcorequestion.py @@ -62,3 +62,4 @@ def compose(self) -> ComposeResult: classes="col-2 pipeline-type-grid", ) yield Markdown(markdown_details) + yield Center(Button("Back", id="back", variant="default"), classes="cta") diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index c053a22228..1351fa95fa 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -74,7 +74,6 @@ def notempty_nfcore(cls, v: str, info: ValidationInfo) -> str: """Check that string values are not empty when the config is nf-core.""" context = info.context if context and context["is_nfcore"]: - print("here") if v.strip() == "": raise ValueError("Cannot be left empty.") return v @@ -191,7 +190,6 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL}): - print(f"global config: {NFCORE_CONFIG_GLOBAL}") ConfigsCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: From 078520a9492f89998773bc4551496bbacd1b5828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Mon, 5 May 2025 15:23:50 +0200 Subject: [PATCH 021/121] fix custom fields using hide class and add pipeline name or path --- nf_core/configs/create/__init__.py | 2 + nf_core/configs/create/basicdetails.py | 74 ++++++++++++++++++-------- nf_core/configs/create/utils.py | 21 ++++++-- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index e6eab1933c..771c711054 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -78,6 +78,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.push_screen("choose_type") elif event.button.id == "type_infrastructure": self.CONFIG_TYPE = "infrastructure" + utils.CONFIG_ISINFRASTRUCTURE_GLOBAL = True self.push_screen("nfcore_question") elif event.button.id == "type_nfcore": self.NFCORE_CONFIG = True @@ -85,6 +86,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.push_screen("basic_details") elif event.button.id == "type_pipeline": self.CONFIG_TYPE = "pipeline" + utils.CONFIG_ISINFRASTRUCTURE_GLOBAL = False self.push_screen("nfcore_question") elif event.button.id == "type_custom": self.NFCORE_CONFIG = False diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 7ca6253a55..5d3206150f 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -13,6 +13,7 @@ ConfigsCreateConfig, TextInput, ) ## TODO Move somewhere common? +from nf_core.utils import add_hide_class, remove_hide_class config_exists_warn = """ > ⚠️ **The config file you are trying to create already exists.** @@ -43,36 +44,44 @@ def compose(self) -> ComposeResult: "", classes="column", ) - if self.parent.CONFIG_TYPE == "infrastructure": - with Horizontal(): - yield TextInput( - "config_profile_contact", - "Boaty McBoatFace", - "Author full name.", - classes="column", - ) + with Horizontal(): + yield TextInput( + "config_profile_contact", + "Boaty McBoatFace", + "Author full name.", + classes="column" + " hide" if self.parent.CONFIG_TYPE == "pipeline" else "", + ) - yield TextInput( - "config_profile_handle", - "@BoatyMcBoatFace", - "Author Git(Hub) handle.", - classes="column", - ) + yield TextInput( + "config_profile_handle", + "@BoatyMcBoatFace", + "Author Git(Hub) handle.", + classes="column" + " hide" if self.parent.CONFIG_TYPE == "pipeline" else "", + ) + yield TextInput( + "config_pipeline_name", + "Pipeline name", + "The pipeline name you want to create the config for.", + classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or not self.parent.NFCORE_CONFIG else "", + ) + yield TextInput( + "config_pipeline_path", + "Pipeline path", + "The path to the pipeline you want to create the config for.", + classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or self.parent.NFCORE_CONFIG else "", + ) yield TextInput( "config_profile_description", "Description", "A short description of your config.", ) - if self.parent.CONFIG_TYPE == "infrastructure": - yield TextInput( - "config_profile_url", - "https://nf-co.re", - "URL of infrastructure website or owning institution (infrastructure configs only).", - disabled=( - self.parent.CONFIG_TYPE == "pipeline" - ), ## TODO update TextInput to accept replace with visibility: https://textual.textualize.io/styles/visibility/ - ) + yield TextInput( + "config_profile_url", + "https://nf-co.re", + "URL of infrastructure website or owning institution (infrastructure configs only).", + classes="hide" if self.parent.CONFIG_TYPE == "pipeline" else "", + ) yield Center( Button("Back", id="back", variant="default"), Button("Next", id="next", variant="success"), @@ -98,3 +107,22 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.parent.push_screen("final") except ValueError: pass + + def on_screen_resume(self): + """Show or hide form fields on resume depending on config type.""" + if self.parent.CONFIG_TYPE == "pipeline": + add_hide_class(self.parent, "config_profile_contact") + add_hide_class(self.parent, "config_profile_handle") + add_hide_class(self.parent, "config_profile_url") + if self.parent.NFCORE_CONFIG: + remove_hide_class(self.parent, "config_pipeline_name") + add_hide_class(self.parent, "config_pipeline_path") + else: + remove_hide_class(self.parent, "config_pipeline_path") + add_hide_class(self.parent, "config_pipeline_name") + if self.parent.CONFIG_TYPE == "infrastructure": + remove_hide_class(self.parent, "config_profile_contact") + remove_hide_class(self.parent, "config_profile_handle") + remove_hide_class(self.parent, "config_profile_url") + add_hide_class(self.parent, "config_pipeline_name") + add_hide_class(self.parent, "config_pipeline_path") diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 1351fa95fa..12310322fd 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -3,6 +3,7 @@ import re from contextlib import contextmanager from contextvars import ContextVar +from pathlib import Path from typing import Any, Dict, Iterator, Optional, Union from pydantic import BaseModel, ConfigDict, ValidationError, ValidationInfo, field_validator @@ -35,6 +36,10 @@ class ConfigsCreateConfig(BaseModel): general_config_type: Optional[str] = None """ Config file type (infrastructure or pipeline) """ + config_pipeline_name: Optional[str] = None + """ The name of the pipeline """ + config_pipeline_path: Optional[str] = None + """ The path to the pipeline """ general_config_name: Optional[str] = None """ Config name """ config_profile_contact: Optional[str] = None @@ -58,9 +63,7 @@ def __init__(self, /, **data: Any) -> None: context=_init_context_var.get(), ) - @field_validator( - "general_config_name", - ) + @field_validator("general_config_name") @classmethod def notempty(cls, v: str) -> str: """Check that string values are not empty.""" @@ -68,7 +71,17 @@ def notempty(cls, v: str) -> str: raise ValueError("Cannot be left empty.") return v - @field_validator("config_profile_contact", "config_profile_description") + @field_validator("config_pipeline_path") + @classmethod + def path_valid(cls, v: str) -> str: + """Check that a path is valid.""" + if v.strip() == "": + raise ValueError("Cannot be left empty.") + if not Path(v).is_dir(): + raise ValueError("Must be a valid path.") + return v + + @field_validator("config_profile_contact", "config_profile_description", "config_pipeline_name") @classmethod def notempty_nfcore(cls, v: str, info: ValidationInfo) -> str: """Check that string values are not empty when the config is nf-core.""" From a55d26bfd1061f980ff65fd2ac650f7a324b9ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Mon, 5 May 2025 16:48:16 +0200 Subject: [PATCH 022/121] add screen to select if config is for an HPC or not --- nf_core/configs/create/__init__.py | 2 + nf_core/configs/create/basicdetails.py | 10 +++-- nf_core/configs/create/hpcquestion.py | 52 ++++++++++++++++++++++++++ nf_core/configs/create/utils.py | 14 ++++--- 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 nf_core/configs/create/hpcquestion.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 771c711054..364601132f 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -15,6 +15,7 @@ from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType from nf_core.configs.create.final import FinalScreen +from nf_core.configs.create.hpcquestion import ChooseHpc from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.welcome import WelcomeScreen @@ -54,6 +55,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "nfcore_question": ChooseNfcoreConfig, "basic_details": BasicDetails, "final": FinalScreen, + "hpc_question": ChooseHpc, } # Initialise config as empty diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 5d3206150f..d9fb416c72 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -49,14 +49,13 @@ def compose(self) -> ComposeResult: "config_profile_contact", "Boaty McBoatFace", "Author full name.", - classes="column" + " hide" if self.parent.CONFIG_TYPE == "pipeline" else "", + classes="column hide" if self.parent.CONFIG_TYPE == "pipeline" else "column", ) - yield TextInput( "config_profile_handle", "@BoatyMcBoatFace", "Author Git(Hub) handle.", - classes="column" + " hide" if self.parent.CONFIG_TYPE == "pipeline" else "", + classes="column hide" if self.parent.CONFIG_TYPE == "pipeline" else "column", ) yield TextInput( "config_pipeline_name", @@ -104,7 +103,10 @@ def on_button_pressed(self, event: Button.Pressed) -> None: try: self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) if event.button.id == "next": - self.parent.push_screen("final") + if self.parent.CONFIG_TYPE == "infrastructure": + self.parent.push_screen("hpc_question") + elif self.parent.CONFIG_TYPE == "pipeline": + self.parent.push_screen("final") except ValueError: pass diff --git a/nf_core/configs/create/hpcquestion.py b/nf_core/configs/create/hpcquestion.py new file mode 100644 index 0000000000..9663db1736 --- /dev/null +++ b/nf_core/configs/create/hpcquestion.py @@ -0,0 +1,52 @@ +from textual.app import ComposeResult +from textual.containers import Center, Grid +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +markdown_intro = """ +# Is this configuration file for an HPC config? +""" + +markdown_type_hpc = """ +## Choose _"HPC"_ if: + +You want to create a config file for an HPC. +""" +markdown_type_pc = """ +## Choose _"PC"_ if: + +You want to create a config file to run your pipeline on a personal computer. +""" + +markdown_details = """ +## What's the difference? + +Choosing _"HPC"_ will add the following configurations: + +* Provide a scheduler +* Provide the name of a queue +* Select if a module system is used +* Select if you need to load other modules +""" + + +class ChooseHpc(Screen): + """Choose whether this will be a config for an HPC or not.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + yield Grid( + Center( + Markdown(markdown_type_hpc), + Center(Button("HPC", id="type_hpc", variant="success")), + ), + Center( + Markdown(markdown_type_pc), + Center(Button("PC", id="type_pc", variant="primary")), + ), + classes="col-2 pipeline-type-grid", + ) + yield Markdown(markdown_details) + yield Center(Button("Back", id="back", variant="default"), classes="cta") diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 12310322fd..1c5b91e94e 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -73,12 +73,14 @@ def notempty(cls, v: str) -> str: @field_validator("config_pipeline_path") @classmethod - def path_valid(cls, v: str) -> str: + def path_valid(cls, v: str, info: ValidationInfo) -> str: """Check that a path is valid.""" - if v.strip() == "": - raise ValueError("Cannot be left empty.") - if not Path(v).is_dir(): - raise ValueError("Must be a valid path.") + context = info.context + if context and not context["is_infrastructure"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + if not Path(v).is_dir(): + raise ValueError("Must be a valid path.") return v @field_validator("config_profile_contact", "config_profile_description", "config_pipeline_name") @@ -202,7 +204,7 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: - with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL}): + with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL, "is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL}): ConfigsCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: From b0ad75992ad50148deb5604c480f3e4607d512e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Fri, 5 Sep 2025 11:38:09 +0200 Subject: [PATCH 023/121] use 'local' for no HPC --- nf_core/configs/create/hpcquestion.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nf_core/configs/create/hpcquestion.py b/nf_core/configs/create/hpcquestion.py index 9663db1736..6556c2d9cd 100644 --- a/nf_core/configs/create/hpcquestion.py +++ b/nf_core/configs/create/hpcquestion.py @@ -12,10 +12,10 @@ You want to create a config file for an HPC. """ -markdown_type_pc = """ -## Choose _"PC"_ if: +markdown_type_local = """ +## Choose _"local"_ if: -You want to create a config file to run your pipeline on a personal computer. +You want to create a config file to run your pipeline on a local computer. """ markdown_details = """ @@ -43,8 +43,8 @@ def compose(self) -> ComposeResult: Center(Button("HPC", id="type_hpc", variant="success")), ), Center( - Markdown(markdown_type_pc), - Center(Button("PC", id="type_pc", variant="primary")), + Markdown(markdown_type_local), + Center(Button("local", id="type_local", variant="primary")), ), classes="col-2 pipeline-type-grid", ) From d557970efec65bb01dda51ae33539380ad67048d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Fri, 5 Sep 2025 12:36:27 +0200 Subject: [PATCH 024/121] add HPC configuration screen --- nf_core/configs/create/__init__.py | 4 + nf_core/configs/create/hpccustomisation.py | 110 +++++++++++++++++++++ nf_core/configs/create/utils.py | 12 ++- 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 nf_core/configs/create/hpccustomisation.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 364601132f..21a95ca51b 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -15,6 +15,7 @@ from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType from nf_core.configs.create.final import FinalScreen +from nf_core.configs.create.hpccustomisation import HpcCustomisation from nf_core.configs.create.hpcquestion import ChooseHpc from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.welcome import WelcomeScreen @@ -56,6 +57,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "basic_details": BasicDetails, "final": FinalScreen, "hpc_question": ChooseHpc, + "hpc_customisation": HpcCustomisation, } # Initialise config as empty @@ -94,6 +96,8 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.NFCORE_CONFIG = False utils.NFCORE_CONFIG_GLOBAL = False self.push_screen("basic_details") + elif event.button.id == "type_hpc": + self.push_screen("hpc_customisation") ## General options if event.button.id == "close_app": self.exit(return_code=0) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py new file mode 100644 index 0000000000..0928f392d9 --- /dev/null +++ b/nf_core/configs/create/hpccustomisation.py @@ -0,0 +1,110 @@ +import subprocess +from typing import Optional + +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Markdown + +from nf_core.configs.create.utils import ( + TextInput, +) + +markdown_intro = """ +# Configure the options for your HPC +""" + + +class HpcCustomisation(Screen): + """Customise the options to create a config for an HPC.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + scheduler = self._get_scheduler() + queues = self._get_queues(scheduler) + module_system_used = self._detect_module_system() + yield Markdown(markdown_intro) + with Horizontal(): + yield TextInput( + "scheduler", + "Scheduler", + "The scheduler in your HPC.", + default=scheduler if scheduler is not None else "Scheduler", + classes="column", + ) + yield TextInput( + "queue", + "Queue", + "The queue in your HPC.", + classes="column", + suggestions=queues, + ) + yield TextInput( + "module_system", + "Other modules to load", + "Do you need to load other software using the module system for your compute nodes?", + classes="hide" if not module_system_used else "", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Continue", id="toconfiguration", variant="success"), + classes="cta", + ) + + def _get_scheduler(self) -> Optional[str]: + """Get the used scheduler""" + try: + subprocess.run(["sinfo", "--version"]) + return "slurm" + except FileNotFoundError: + pass + except subprocess.CalledProcessError: + pass + try: + subprocess.run(["qstat", "--version"]) + return "pbs" + except FileNotFoundError: + pass + except subprocess.CalledProcessError: + pass + try: + subprocess.run(["qstat", "-help"]) + return "sge" + except FileNotFoundError: + pass + except subprocess.CalledProcessError: + pass + return None + + def _get_queues(self, scheduler: Optional[str]) -> list[str]: + """Get the available queues to use for the jobs""" + if scheduler == "slurm": + try: + queues = subprocess.check_output(["sinfo", "-o", '"%P,%c,%m,%l"']).decode("utf-8") + return queues.split("\n") + except subprocess.CalledProcessError: + pass + elif scheduler == "pbs": + try: + queues = subprocess.check_output(["qstat", "-q"]).decode("utf-8") + return queues.split("\n") + except subprocess.CalledProcessError: + pass + elif scheduler == "sge": + try: + queues = subprocess.check_output(["qhost", "-q"]).decode("utf-8") + return queues.split("\n") + except subprocess.CalledProcessError: + pass + return [] + + def _detect_module_system(self) -> bool: + """Detect if a module system is used""" + try: + subprocess.check_output(["module", "--version"]) + except FileNotFoundError: + return False + except subprocess.CalledProcessError: + return False + return True diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 1c5b91e94e..3f7dd54e9d 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -1,15 +1,17 @@ """Config creation specific functions and classes""" import re +from collections.abc import Iterator from contextlib import contextmanager from contextvars import ContextVar from pathlib import Path -from typing import Any, Dict, Iterator, Optional, Union +from typing import Any, Optional, Union from pydantic import BaseModel, ConfigDict, ValidationError, ValidationInfo, field_validator from textual import on from textual.app import ComposeResult from textual.containers import Grid +from textual.suggester import SuggestFromList from textual.validation import ValidationResult, Validator from textual.widgets import Input, Static @@ -18,7 +20,7 @@ @contextmanager -def init_context(value: Dict[str, Any]) -> Iterator[None]: +def init_context(value: dict[str, Any]) -> Iterator[None]: token = _init_context_var.set(value) try: yield @@ -149,7 +151,9 @@ class TextInput(Static): and validation messages. """ - def __init__(self, field_id, placeholder, description, default=None, password=None, **kwargs) -> None: + def __init__( + self, field_id, placeholder, description, default=None, password=None, suggestions=[], **kwargs + ) -> None: """Initialise the widget with our values. Pass on kwargs upstream for standard usage.""" @@ -160,6 +164,7 @@ def __init__(self, field_id, placeholder, description, default=None, password=No self.description: str = description self.default: str = default self.password: bool = password + self.suggestions: list[str] = suggestions def compose(self) -> ComposeResult: yield Grid( @@ -169,6 +174,7 @@ def compose(self) -> ComposeResult: validators=[ValidateConfig(self.field_id)], value=self.default, password=self.password, + suggester=SuggestFromList(self.suggestions, case_sensitive=False), ), Static(classes="validation_msg"), classes="text-input-grid", From 580598b13abd7a79179dbd34913c76337ff71ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Mir=20Pedrol?= Date: Fri, 5 Sep 2025 16:57:14 +0200 Subject: [PATCH 025/121] add screen for final infrastructure config details - containers cache dir not working --- nf_core/configs/create/__init__.py | 6 + nf_core/configs/create/finalinfradetails.py | 166 ++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 nf_core/configs/create/finalinfradetails.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 21a95ca51b..ef8bd8b92f 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -15,6 +15,7 @@ from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType from nf_core.configs.create.final import FinalScreen +from nf_core.configs.create.finalinfradetails import FinalInfraDetails from nf_core.configs.create.hpccustomisation import HpcCustomisation from nf_core.configs.create.hpcquestion import ChooseHpc from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig @@ -58,6 +59,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "final": FinalScreen, "hpc_question": ChooseHpc, "hpc_customisation": HpcCustomisation, + "final_infra_details": FinalInfraDetails, } # Initialise config as empty @@ -98,6 +100,10 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.push_screen("basic_details") elif event.button.id == "type_hpc": self.push_screen("hpc_customisation") + elif event.button.id == "toconfiguration": + self.push_screen("final_infra_details") + elif event.button.id == "finish": + self.push_screen("final") ## General options if event.button.id == "close_app": self.exit(return_code=0) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py new file mode 100644 index 0000000000..41e0d6015b --- /dev/null +++ b/nf_core/configs/create/finalinfradetails.py @@ -0,0 +1,166 @@ +import os +import subprocess +from typing import Optional + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Static, Switch + +from nf_core.configs.create.utils import ( + TextInput, +) +from nf_core.utils import add_hide_class, remove_hide_class + +markdown_intro = """ +# Configure the options for your infrastructure config +""" + + +class FinalInfraDetails(Screen): + """Customise the options to create a config for an infrastructure.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.container_system = None + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(markdown_intro) + container_systems = self._get_container_systems() + yield TextInput( + "container_system", + "Container system", + "What container or software system will you use to run your pipeline?", + classes="", + suggestions=container_systems, + ) + yield Markdown("## Maximum resources") + with Horizontal(): + yield TextInput( + "memory", + "Memory", + "Maximum memory available in your machine.", + classes="column", + ) + yield TextInput( + "cpus", + "CPUs", + "Maximum number of CPUs available in your machine.", + classes="column", + ) + yield TextInput( + "time", + "Time", + "Maximum time to run your jobs.", + classes="column", + ) + yield Markdown("## Do you want to define a global cache directory for containers or conda environments?") + with Horizontal(): + yield TextInput( + "envvar", + "Nextflow cachedir environment variable", + "Environment variable to define a global cache directory.", + classes="", + default=f"NXF_{self.container_system.upper()}_CACHEDIR" if self.container_system is not None else "", + ) + yield TextInput( + "cachedir", + f"NXF_{self.container_system.upper()}_CACHEDIR" if self.container_system is not None else "", + "Define a global cache direcotry.", + classes="", + default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") + if self.container_system is not None + else "", + ) + yield TextInput( + "igenomes_cachedir", + "iGenomes cache directory", + "If you have an iGenomes cache direcotry, specify it.", + classes="hide" if not self.parent.NFCORE_CONFIG else "", + ) + yield TextInput( + "scratch_dir", + "Scratch directory", + "If you have to use a specific scratch direcotry, specify it.", + classes="", + ) + with Horizontal(classes="ghrepo-cols"): + yield Switch(value=False, id="private") + with Vertical(): + yield Static("Delete work directory", classes="") + yield Markdown( + "Select if you want to delete the files in the `work/` directory on successful completion of a run.", + classes="feature_subtitle", + ) + yield TextInput( + "retries", + "Number of retries", + "Specify the number of retries for a failed job.", + classes="", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Finish", id="finish", variant="success"), + classes="cta", + ) + + def _get_container_systems(self) -> list[str]: + """Get the available container systems to use for software handling.""" + module_system_used = self._detect_module_system() + container_systems = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter"] + available_systems = [] + if module_system_used: + for system in container_systems: + try: + output = subprocess.check_output(["module", "avail", "|", "grep", system]).decode("utf-8") + if output: + available_systems.append(system) + except subprocess.CalledProcessError: + continue + else: + for system in container_systems: + try: + output = subprocess.check_output([system]).decode("utf-8") + if output: + available_systems.append(system) + except FileNotFoundError: + continue + except subprocess.CalledProcessError: + continue + return available_systems + + def _detect_module_system(self) -> bool: + """Detect if a module system is used""" + try: + subprocess.check_output(["module", "--version"]) + except FileNotFoundError: + return False + except subprocess.CalledProcessError: + return False + return True + + def _get_set_directory(self, dir: str) -> Optional[str]: + """Get the available cache directories""" + if dir: + set_dir = os.environ.get(dir) + if set_dir: + return set_dir + return None + + @on(Input.Changed) + def get_container_system(self) -> None: + """Get the container system from the input.""" + self.container_system = None + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + if text_input.field_id == "container_system": + self.container_system = this_input.value + if self.container_system is not None: + add_hide_class(self.parent, "cachedir") + add_hide_class(self.parent, "envvar") + else: + remove_hide_class(self.parent, "cachedir") + remove_hide_class(self.parent, "envvar") From b38506fdf6b8a63078c7b9d726559860c51fb0a3 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 10 Dec 2025 16:34:32 +1100 Subject: [PATCH 026/121] add default process resource screen and field validation --- nf_core/configs/create/__init__.py | 2 + nf_core/configs/create/basicdetails.py | 2 +- nf_core/configs/create/defaultprocessres.py | 82 +++++++++++++++++++++ nf_core/configs/create/utils.py | 21 ++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 nf_core/configs/create/defaultprocessres.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index ef8bd8b92f..73f32bda14 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -20,6 +20,7 @@ from nf_core.configs.create.hpcquestion import ChooseHpc from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.welcome import WelcomeScreen +from nf_core.configs.create.defaultprocessres import DefaultProcess ## General utilities from nf_core.utils import LoggingConsole @@ -59,6 +60,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "final": FinalScreen, "hpc_question": ChooseHpc, "hpc_customisation": HpcCustomisation, + "default_process_resources": DefaultProcess, "final_infra_details": FinalInfraDetails, } diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index d9fb416c72..d5f75c0bfe 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -106,7 +106,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: if self.parent.CONFIG_TYPE == "infrastructure": self.parent.push_screen("hpc_question") elif self.parent.CONFIG_TYPE == "pipeline": - self.parent.push_screen("final") + self.parent.push_screen("default_process_resources") except ValueError: pass diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py new file mode 100644 index 0000000000..820cbe2676 --- /dev/null +++ b/nf_core/configs/create/defaultprocessres.py @@ -0,0 +1,82 @@ +"""Get information about which process/label the user wants to configure.""" + +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown + +from nf_core.configs.create.utils import ( + ConfigsCreateConfig, + TextInput, +) ## TODO Move somewhere common? +from nf_core.utils import add_hide_class, remove_hide_class + + +class DefaultProcess(Screen): + """Get default process resource requirements.""" + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Default process resources + """ + ) + ) + yield TextInput( + "default_process_ncpus", + "2", + "Number of CPUs to use by default for all processes.", + "2", + classes="column", + ) + yield TextInput( + "default_process_memgb", + "8", + "Amount of memory in GB to use by default for all processes.", + "8", + classes="column", + ) + yield Markdown("The walltime required by default for all processes.") + with Horizontal(): + yield TextInput( + "default_process_hours", + "1", + "Hours:", + "1", + classes="column", + ) + yield TextInput( + "default_process_minutes", + "0", + "Minutes:", + "0", + classes="column", + ) + yield TextInput( + "default_process_seconds", + "0", + "Seconds:", + "0", + classes="column", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + try: + if event.button.id == "next": + self.parent.push_screen("final") + except ValueError: + pass diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 3f7dd54e9d..081b673baf 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -52,6 +52,16 @@ class ConfigsCreateConfig(BaseModel): """ Config description """ config_profile_url: Optional[str] = None """ Config institution URL """ + default_process_ncpus: Optional[str] = None + """ Default number of CPUs """ + default_process_memgb: Optional[str] = None + """ Default amount of memory """ + default_process_hours: Optional[str] = None + """ Default walltime - hours """ + default_process_minutes: Optional[str] = None + """ Default walltime - minutes """ + default_process_seconds: Optional[str] = None + """ Default walltime - seconds """ is_nfcore: Optional[bool] = None """ Whether the config is part of the nf-core organisation """ @@ -142,6 +152,17 @@ def url_prefix(cls, v: str, info: ValidationInfo) -> str: ) return v + @field_validator("default_process_ncpus", "default_process_memgb", "default_process_hours", "default_process_minutes", "default_process_seconds") + @classmethod + def integer_valid(cls, v: str, info: ValidationInfo) -> str: + """Check that integer values are non-empty and positive.""" + if v.strip() == "": + raise ValueError("Cannot be left empty.") + try: + int(v.strip()) + except ValueError: + raise ValueError("Must be an integer.") + return v ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) class TextInput(Static): From e4dbdfb86e8143d7ea8c2ac35a70d754677b5545 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 18 Dec 2025 14:07:59 +1100 Subject: [PATCH 027/121] fix field validation and config creation --- nf_core/configs/create/basicdetails.py | 4 +- nf_core/configs/create/create.py | 4 +- nf_core/configs/create/defaultprocessres.py | 14 +++++ nf_core/configs/create/utils.py | 68 ++++++++++++++++----- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index d5f75c0bfe..4d3e65f468 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -12,6 +12,7 @@ from nf_core.configs.create.utils import ( ConfigsCreateConfig, TextInput, + init_context ) ## TODO Move somewhere common? from nf_core.utils import add_hide_class, remove_hide_class @@ -101,7 +102,8 @@ def on_button_pressed(self, event: Button.Pressed) -> None: else: text_input.query_one(".validation_msg").update("") try: - self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) + with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) if event.button.id == "next": if self.parent.CONFIG_TYPE == "infrastructure": self.parent.push_screen("hpc_question") diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 0d58551089..738acafabd 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -3,6 +3,7 @@ """ from nf_core.configs.create.utils import ConfigsCreateConfig, generate_config_entry +from re import sub class ConfigCreate: @@ -31,7 +32,8 @@ def construct_params(self, contact, handle, description, url): def write_to_file(self): ## File name option - filename = "_".join(self.template_config.general_config_name) + ".conf" + config_name = str(self.template_config.general_config_name).strip() + filename = sub(r'\s+', '_', config_name) + ".conf" ## Collect all config entries per scope, for later checking scope needs to be written validparams = self.construct_params( diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 820cbe2676..8f49d92db2 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -11,6 +11,7 @@ from nf_core.configs.create.utils import ( ConfigsCreateConfig, TextInput, + init_context ) ## TODO Move somewhere common? from nf_core.utils import add_hide_class, remove_hide_class @@ -75,7 +76,20 @@ def compose(self) -> ComposeResult: @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" + new_config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + new_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") try: + config = self.parent.TEMPLATE_CONFIG.__dict__ + config.update(new_config) + with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) if event.button.id == "next": self.parent.push_screen("final") except ValueError: diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 081b673baf..bf216fba64 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -88,19 +88,37 @@ def notempty(cls, v: str) -> str: def path_valid(cls, v: str, info: ValidationInfo) -> str: """Check that a path is valid.""" context = info.context - if context and not context["is_infrastructure"]: + if context and (not context["is_infrastructure"] and not context["is_nfcore"]): if v.strip() == "": raise ValueError("Cannot be left empty.") if not Path(v).is_dir(): raise ValueError("Must be a valid path.") return v - @field_validator("config_profile_contact", "config_profile_description", "config_pipeline_name") + @field_validator("config_pipeline_name") @classmethod - def notempty_nfcore(cls, v: str, info: ValidationInfo) -> str: - """Check that string values are not empty when the config is nf-core.""" + def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: + """Check that an nf-core pipeline name is valid.""" context = info.context - if context and context["is_nfcore"]: + if context and (not context["is_infrastructure"] and context["is_nfcore"]): + if v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + + @field_validator("config_profile_description") + @classmethod + def notempty_description(cls, v: str) -> str: + """Check that description is not empty when.""" + if v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + + @field_validator("config_profile_contact") + @classmethod + def notempty_contact(cls, v: str, info: ValidationInfo) -> str: + """Check that contact values are not empty when the config is infrastructure.""" + context = info.context + if context and context["is_infrastructure"]: if v.strip() == "": raise ValueError("Cannot be left empty.") return v @@ -113,7 +131,7 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: """Check that GitHub handles start with '@'. Make providing a handle mandatory for nf-core configs""" context = info.context - if context and context["is_nfcore"]: + if context and context["is_infrastructure"]: if v.strip() == "": raise ValueError("Cannot be left empty.") elif not re.match( @@ -132,7 +150,7 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: def url_prefix(cls, v: str, info: ValidationInfo) -> str: """Check that institutional web links start with valid URL prefix.""" context = info.context - if context and context["is_nfcore"]: + if context and context["is_infrastructure"]: if v.strip() == "": raise ValueError("Cannot be left empty.") elif not re.match( @@ -152,16 +170,36 @@ def url_prefix(cls, v: str, info: ValidationInfo) -> str: ) return v - @field_validator("default_process_ncpus", "default_process_memgb", "default_process_hours", "default_process_minutes", "default_process_seconds") + @field_validator("default_process_ncpus", "default_process_memgb") @classmethod - def integer_valid(cls, v: str, info: ValidationInfo) -> str: + def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: """Check that integer values are non-empty and positive.""" - if v.strip() == "": - raise ValueError("Cannot be left empty.") - try: - int(v.strip()) - except ValueError: - raise ValueError("Must be an integer.") + context = info.context + if context and not context["is_infrastructure"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + try: + v_int = int(v.strip()) + except ValueError: + raise ValueError("Must be an integer.") + if not v_int > 0: + raise ValueError("Must be a positive integer.") + return v + + @field_validator("default_process_hours", "default_process_minutes", "default_process_seconds") + @classmethod + def non_neg_integer_valid(cls, v: str, info: ValidationInfo) -> str: + """Check that integer values are non-empty and non-negative.""" + context = info.context + if context and not context["is_infrastructure"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + try: + v_int = int(v.strip()) + except ValueError: + raise ValueError("Must be an integer.") + if not v_int >= 0: + raise ValueError("Must be a non-negative integer.") return v ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) From d1f1cc86f451b2a8a92cd6804d12b34fa8de6164 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 6 Jan 2026 17:01:21 +1100 Subject: [PATCH 028/121] WIP: add custom process config screen --- nf_core/configs/create/__init__.py | 2 + nf_core/configs/create/customprocessres.py | 151 ++++++++++++++++++++ nf_core/configs/create/defaultprocessres.py | 9 +- nf_core/configs/create/utils.py | 18 ++- 4 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 nf_core/configs/create/customprocessres.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 73f32bda14..9e2bf68d49 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -21,6 +21,7 @@ from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.welcome import WelcomeScreen from nf_core.configs.create.defaultprocessres import DefaultProcess +from nf_core.configs.create.customprocessres import CustomProcess ## General utilities from nf_core.utils import LoggingConsole @@ -61,6 +62,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "hpc_question": ChooseHpc, "hpc_customisation": HpcCustomisation, "default_process_resources": DefaultProcess, + "custom_process_resources": CustomProcess, "final_infra_details": FinalInfraDetails, } diff --git a/nf_core/configs/create/customprocessres.py b/nf_core/configs/create/customprocessres.py new file mode 100644 index 0000000000..0418baf698 --- /dev/null +++ b/nf_core/configs/create/customprocessres.py @@ -0,0 +1,151 @@ +"""Get information about which process/label the user wants to configure.""" + +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label +from enum import Enum +from nf_core.utils import add_hide_class, remove_hide_class + +from nf_core.configs.create.utils import ( + ConfigsCreateConfig, + TextInput, + init_context +) ## TODO Move somewhere common? +from nf_core.utils import add_hide_class, remove_hide_class + + +class CustomProcess(Screen): + """Get default process resource requirements.""" + + def __init__(self) -> None: + super().__init__() + self.select_label = False + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Custom process resources + """ + ) + ) + with Horizontal(): + yield TextInput( + "custom_process_name", + "", + "The name of the process you wish to configure.", + "", + classes="column hide" if self.select_label else "column", + ) + yield TextInput( + "custom_process_label", + "", + "The process label you wish to configure.", + "", + classes="column hide" if not self.select_label else "column", + ) + with Horizontal(): + yield Label( + "Selecting a process by name or label:", + id="toggle_process_name_label_text" + ) + yield Switch( + id="toggle_process_name_label", + value=self.select_label, + ) + yield Label( + "label" if self.select_label else "name", + id="name_or_label_text" + ) + yield TextInput( + "custom_process_ncpus", + "2", + "Number of CPUs to use for the process.", + "2", + classes="column", + ) + yield TextInput( + "custom_process_memgb", + "8", + "Amount of memory in GB to use for the process.", + "8", + classes="column", + ) + yield Markdown("The walltime required for the process.") + with Horizontal(): + yield TextInput( + "custom_process_hours", + "1", + "Hours:", + "1", + classes="column", + ) + yield TextInput( + "custom_process_minutes", + "0", + "Minutes:", + "0", + classes="column", + ) + yield TextInput( + "custom_process_seconds", + "0", + "Seconds:", + "0", + classes="column", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + @on(Switch.Changed, "#toggle_process_name_label") + def on_toggle_process_name_label(self, event: Switch.Changed) -> None: + """ Handle toggling the process name/label switch """ + self.select_label = event.value + # Update the input text box and labels + for text_input in self.query("TextInput"): + if text_input.field_id in ["custom_process_name", "custom_process_label"]: + text_input.refresh(repaint=True, layout=True, recompose=True) + if self.select_label: + add_hide_class(self.parent, "custom_process_name") + remove_hide_class(self.parent, "custom_process_label") + else: + add_hide_class(self.parent, "custom_process_label") + remove_hide_class(self.parent, "custom_process_name") + # Update the switch label as well + for label in self.query(Label): + if label.id == "name_or_label_text": + label.update("label" if self.select_label else "name") + + + # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + new_config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + new_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + try: + with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + # First, validate the new config data + ConfigsCreateConfig(**new_config) + # If that passes validation, update the existing config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + if event.button.id == "next": + self.parent.push_screen("final") + except ValueError: + pass diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 8f49d92db2..b450a2025b 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -86,11 +86,12 @@ def on_button_pressed(self, event: Button.Pressed) -> None: else: text_input.query_one(".validation_msg").update("") try: - config = self.parent.TEMPLATE_CONFIG.__dict__ - config.update(new_config) with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): - self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) + # First, validate the new config data + ConfigsCreateConfig(**new_config) + # If that passes validation, update the existing config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) if event.button.id == "next": - self.parent.push_screen("final") + self.parent.push_screen("custom_process_resources") except ValueError: pass diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index bf216fba64..24ff1451b0 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -62,6 +62,20 @@ class ConfigsCreateConfig(BaseModel): """ Default walltime - minutes """ default_process_seconds: Optional[str] = None """ Default walltime - seconds """ + custom_process_ncpus: Optional[str] = None + """ Number of CPUs for process """ + custom_process_memgb: Optional[str] = None + """ Amount of memory for process """ + custom_process_hours: Optional[str] = None + """ Walltime for process - hours """ + custom_process_minutes: Optional[str] = None + """ Walltime for process - minutes """ + custom_process_seconds: Optional[str] = None + """ Walltime for process - seconds """ + named_process_resources: Optional[dict] = None + """ Dictionary containing custom resource requirements for named processes """ + labelled_process_resources: Optional[dict] = None + """ Dictionary containing custom resource requirements for labelled processes """ is_nfcore: Optional[bool] = None """ Whether the config is part of the nf-core organisation """ @@ -170,7 +184,7 @@ def url_prefix(cls, v: str, info: ValidationInfo) -> str: ) return v - @field_validator("default_process_ncpus", "default_process_memgb") + @field_validator("default_process_ncpus", "default_process_memgb", "custom_process_ncpus", "custom_process_memgb") @classmethod def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: """Check that integer values are non-empty and positive.""" @@ -186,7 +200,7 @@ def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a positive integer.") return v - @field_validator("default_process_hours", "default_process_minutes", "default_process_seconds") + @field_validator("default_process_hours", "default_process_minutes", "default_process_seconds", "custom_process_hours", "custom_process_minutes", "custom_process_seconds") @classmethod def non_neg_integer_valid(cls, v: str, info: ValidationInfo) -> str: """Check that integer values are non-empty and non-negative.""" From 753ac5e0cc61ea6b5f601a8e48f9d4ce525fce82 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 7 Jan 2026 17:23:08 +1100 Subject: [PATCH 029/121] WIP: continue work on custom process resource screen --- nf_core/configs/create/customprocessres.py | 101 +++++++++++++-------- 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/nf_core/configs/create/customprocessres.py b/nf_core/configs/create/customprocessres.py index 0418baf698..41635fca21 100644 --- a/nf_core/configs/create/customprocessres.py +++ b/nf_core/configs/create/customprocessres.py @@ -24,6 +24,8 @@ class CustomProcess(Screen): def __init__(self) -> None: super().__init__() self.select_label = False + self.config_stack = [] + self.current_config = {} def compose(self) -> ComposeResult: yield Header() @@ -41,14 +43,7 @@ def compose(self) -> ComposeResult: "", "The name of the process you wish to configure.", "", - classes="column hide" if self.select_label else "column", - ) - yield TextInput( - "custom_process_label", - "", - "The process label you wish to configure.", - "", - classes="column hide" if not self.select_label else "column", + classes="column", ) with Horizontal(): yield Label( @@ -102,6 +97,7 @@ def compose(self) -> ComposeResult: ) yield Center( Button("Back", id="back", variant="default"), + Button("Configure another process", id="another"), Button("Next", id="next", variant="success"), classes="cta", ) @@ -110,17 +106,7 @@ def compose(self) -> ComposeResult: def on_toggle_process_name_label(self, event: Switch.Changed) -> None: """ Handle toggling the process name/label switch """ self.select_label = event.value - # Update the input text box and labels - for text_input in self.query("TextInput"): - if text_input.field_id in ["custom_process_name", "custom_process_label"]: - text_input.refresh(repaint=True, layout=True, recompose=True) - if self.select_label: - add_hide_class(self.parent, "custom_process_name") - remove_hide_class(self.parent, "custom_process_label") - else: - add_hide_class(self.parent, "custom_process_label") - remove_hide_class(self.parent, "custom_process_name") - # Update the switch label as well + # Update the switch label for label in self.query(Label): if label.id == "name_or_label_text": label.update("label" if self.select_label else "name") @@ -130,22 +116,65 @@ def on_toggle_process_name_label(self, event: Switch.Changed) -> None: @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" - new_config = {} + if event.button.id in ["next", "another"]: + tmp_config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + tmp_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + # Validate the config + try: + with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + ConfigsCreateConfig(**tmp_config) + # Add to the config stack + tmp_config['select_label'] = self.select_label + self.config_stack.append(tmp_config) + self.current_config = {} + except ValueError: + pass + elif event.button.id == "back": + try: + self.current_config = self.config_stack.pop() + except IndexError: + self.current_config = {} + + # Reset all input field values for text_input in self.query("TextInput"): this_input = text_input.query_one(Input) - validation_result = this_input.validate(this_input.value) - new_config[text_input.field_id] = this_input.value - if not validation_result.is_valid: - text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) - else: - text_input.query_one(".validation_msg").update("") - try: - with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): - # First, validate the new config data - ConfigsCreateConfig(**new_config) - # If that passes validation, update the existing config - self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) - if event.button.id == "next": - self.parent.push_screen("final") - except ValueError: - pass + this_input.clear() + field_id = text_input.field_id + if field_id in self.current_config: + this_input.insert(self.current_config[field_id], 0) + # Also reset switch + switch_input = self.query_one(Switch) + if 'select_label' in self.current_config: + self.select_label = self.current_config['select_label'] + else: + self.select_label = False + if switch_input.value != self.select_label: + switch_input.toggle() + + if event.button.id == "next": + new_config = {} + for key in ["labelled_process_resources", "named_process_resources"]: + custom_process_resources_dict = self.parent.TEMPLATE_CONFIG.__dict__.get(key) + if custom_process_resources_dict is None: + new_config[key] = {} + else: + new_config[key] = custom_process_resources_dict + for tmp_config in self.config_stack: + select_label = tmp_config.pop('select_label') + process_name_or_label = tmp_config.get('custom_process_name') + if select_label: + key = 'labelled_process_resources' + else: + key = 'named_process_resources' + new_config[key][process_name_or_label] = tmp_config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + self.parent.push_screen("final") + elif event.button.id == "another": + self.parent.push_screen("custom_process_resources") From d15b5305d38a9a3eda7238a0abed1154f658d59c Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 8 Jan 2026 14:09:01 +1100 Subject: [PATCH 030/121] add nonempty validation to process name --- nf_core/configs/create/customprocessres.py | 12 ++++++++++++ nf_core/configs/create/utils.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/nf_core/configs/create/customprocessres.py b/nf_core/configs/create/customprocessres.py index 41635fca21..89ec7fbbea 100644 --- a/nf_core/configs/create/customprocessres.py +++ b/nf_core/configs/create/customprocessres.py @@ -116,6 +116,7 @@ def on_toggle_process_name_label(self, event: Switch.Changed) -> None: @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" + proceed = False if event.button.id in ["next", "another"]: tmp_config = {} for text_input in self.query("TextInput"): @@ -130,6 +131,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: try: with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): ConfigsCreateConfig(**tmp_config) + proceed = True # Add to the config stack tmp_config['select_label'] = self.select_label self.config_stack.append(tmp_config) @@ -137,11 +139,17 @@ def on_button_pressed(self, event: Button.Pressed) -> None: except ValueError: pass elif event.button.id == "back": + proceed = True try: self.current_config = self.config_stack.pop() except IndexError: self.current_config = {} + # Only continue if the user clicked 'next' or 'another' and we have a valid config + # or if the user clicked 'back' + if not proceed: + return + # Reset all input field values for text_input in self.query("TextInput"): this_input = text_input.query_one(Input) @@ -149,6 +157,8 @@ def on_button_pressed(self, event: Button.Pressed) -> None: field_id = text_input.field_id if field_id in self.current_config: this_input.insert(self.current_config[field_id], 0) + else: + text_input.refresh(repaint=True, layout=True, recompose=True) # Also reset switch switch_input = self.query_one(Switch) if 'select_label' in self.current_config: @@ -159,6 +169,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: switch_input.toggle() if event.button.id == "next": + # If finalising the custom resources, add them all to the config now new_config = {} for key in ["labelled_process_resources", "named_process_resources"]: custom_process_resources_dict = self.parent.TEMPLATE_CONFIG.__dict__.get(key) @@ -177,4 +188,5 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) self.parent.push_screen("final") elif event.button.id == "another": + # If configuring another process, push a new copy of this screen to the stack self.parent.push_screen("custom_process_resources") diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 24ff1451b0..8dbce93f73 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -62,6 +62,8 @@ class ConfigsCreateConfig(BaseModel): """ Default walltime - minutes """ default_process_seconds: Optional[str] = None """ Default walltime - seconds """ + custom_process_name: Optional[str] = None + """" Name or label of a process to configure """ custom_process_ncpus: Optional[str] = None """ Number of CPUs for process """ custom_process_memgb: Optional[str] = None @@ -184,6 +186,16 @@ def url_prefix(cls, v: str, info: ValidationInfo) -> str: ) return v + @field_validator("custom_process_name") + @classmethod + def notempty_process_name(cls, v: str, info: ValidationInfo) -> str: + """Check that the custom process name or label isn't empty.""" + context = info.context + if context and not context["is_infrastructure"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + @field_validator("default_process_ncpus", "default_process_memgb", "custom_process_ncpus", "custom_process_memgb") @classmethod def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: From d6d6e117a619848149af3732624a057f93e5c6d8 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 8 Jan 2026 15:05:11 +1100 Subject: [PATCH 031/121] allow reverting from final screen --- nf_core/configs/create/customprocessres.py | 62 +++++++++------------- nf_core/configs/create/final.py | 6 ++- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/nf_core/configs/create/customprocessres.py b/nf_core/configs/create/customprocessres.py index 89ec7fbbea..3fd6dc50b5 100644 --- a/nf_core/configs/create/customprocessres.py +++ b/nf_core/configs/create/customprocessres.py @@ -116,7 +116,6 @@ def on_toggle_process_name_label(self, event: Switch.Changed) -> None: @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" - proceed = False if event.button.id in ["next", "another"]: tmp_config = {} for text_input in self.query("TextInput"): @@ -131,25 +130,37 @@ def on_button_pressed(self, event: Button.Pressed) -> None: try: with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): ConfigsCreateConfig(**tmp_config) - proceed = True # Add to the config stack tmp_config['select_label'] = self.select_label self.config_stack.append(tmp_config) - self.current_config = {} + if event.button.id == "another": + # If configuring another process, push a blank config to the config stack + # and push a new copy of this screen to the screen stack + self.config_stack.append({}) + self.parent.push_screen("custom_process_resources") + else: + # If finalising the custom resources, add them all to the config now + new_config = {} + for key in ["labelled_process_resources", "named_process_resources"]: + new_config[key] = self.parent.TEMPLATE_CONFIG.__dict__.get(key) + if new_config[key] is None: + new_config[key] = {} + for tmp_config in self.config_stack: + select_label = tmp_config['select_label'] + process_name_or_label = tmp_config.get('custom_process_name') + key = "labelled_process_resources" if select_label else "named_process_resources" + new_config[key][process_name_or_label] = tmp_config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + self.parent.push_screen("final") except ValueError: pass - elif event.button.id == "back": - proceed = True - try: - self.current_config = self.config_stack.pop() - except IndexError: - self.current_config = {} - - # Only continue if the user clicked 'next' or 'another' and we have a valid config - # or if the user clicked 'back' - if not proceed: - return + def on_screen_resume(self): + # Grab the last config in the stack if it exists + try: + self.current_config = self.config_stack.pop() + except IndexError: + self.current_config = {} # Reset all input field values for text_input in self.query("TextInput"): this_input = text_input.query_one(Input) @@ -167,26 +178,3 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.select_label = False if switch_input.value != self.select_label: switch_input.toggle() - - if event.button.id == "next": - # If finalising the custom resources, add them all to the config now - new_config = {} - for key in ["labelled_process_resources", "named_process_resources"]: - custom_process_resources_dict = self.parent.TEMPLATE_CONFIG.__dict__.get(key) - if custom_process_resources_dict is None: - new_config[key] = {} - else: - new_config[key] = custom_process_resources_dict - for tmp_config in self.config_stack: - select_label = tmp_config.pop('select_label') - process_name_or_label = tmp_config.get('custom_process_name') - if select_label: - key = 'labelled_process_resources' - else: - key = 'named_process_resources' - new_config[key][process_name_or_label] = tmp_config - self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) - self.parent.push_screen("final") - elif event.button.id == "another": - # If configuring another process, push a new copy of this screen to the stack - self.parent.push_screen("custom_process_resources") diff --git a/nf_core/configs/create/final.py b/nf_core/configs/create/final.py index 457293dd1c..c995640987 100644 --- a/nf_core/configs/create/final.py +++ b/nf_core/configs/create/final.py @@ -28,7 +28,11 @@ def compose(self) -> ComposeResult: ".", classes="row", ) - yield Center(Button("Save and close!", id="close_app", variant="success"), classes="cta") + yield Center( + Button("Back", id="back", variant="default"), + Button("Save and close!", id="close_app", variant="success"), + classes="cta" + ) def _create_config(self) -> None: """Create the config.""" From 3ab1999966a0ec731de227b51836d10f073278f2 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 8 Jan 2026 16:40:23 +1100 Subject: [PATCH 032/121] write basic process config to file --- nf_core/configs/create/create.py | 133 ++++++++++++++++++++++++++----- nf_core/configs/create/final.py | 2 +- 2 files changed, 113 insertions(+), 22 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 738acafabd..d4a0be7c99 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -7,49 +7,140 @@ class ConfigCreate: - def __init__(self, template_config: ConfigsCreateConfig): + def __init__(self, template_config: ConfigsCreateConfig, config_type: str): self.template_config = template_config + self.config_type = config_type - def construct_params(self, contact, handle, description, url): + def construct_info_params(self): final_params = {} + contact = self.template_config.config_profile_contact + handle = self.template_config.config_profile_handle + description = self.template_config.config_profile_description + url = self.template_config.config_profile_url - if contact is not None: - if handle is not None: + if contact: + if handle: config_contact = contact + " (" + handle + ")" else: config_contact = contact final_params["config_profile_contact"] = config_contact - elif handle is not None: + elif handle: final_params["config_profile_contact"] = handle - if description is not None: + if description: final_params["config_profile_description"] = description - if url is not None: + if url: final_params["config_profile_url"] = url return final_params + def construct_params_str(self): + info_params = self.construct_info_params() + + info_params_str_list = [ + f' {key} = "{value}"' + for key, value in info_params.items() + if value + ] + + params_section = [ + 'params {', + *info_params_str_list, + '}', + ] + + return '\n'.join(params_section) + '\n' + + def get_resource_strings(self, cpus, memory, hours, minutes, seconds, prefix=''): + cpus_int = int(cpus) + cpus_str = f'cpus = {cpus_int}' + + memory_int = int(memory) + memory_str = f'memory = {memory_int}.Gb' + + time_h = int(hours) + time_m = int(minutes) + time_s = int(seconds) + time_str = f"time = '{time_h}h {time_m}m {time_s}s'" + + resources = [cpus_str, memory_str, time_str] + return [ + f'{prefix}{res}' + for res in resources + ] + + def construct_process_config_str(self): + process_config_str_list = [] + + # Construct default resources + default_resources = self.get_resource_strings( + cpus=self.template_config.default_process_ncpus, + memory=self.template_config.default_process_memgb, + hours=self.template_config.default_process_hours, + minutes=self.template_config.default_process_minutes, + seconds=self.template_config.default_process_seconds, + prefix=' ' + ) + + # Construct named process resources + named_resources = [] + if self.template_config.named_process_resources: + for process_name, process_resources in self.template_config.named_process_resources.items(): + named_resources.append( + f" withName: '{process_name}'" + " {" + ) + named_resources.extend(self.get_resource_strings( + cpus=process_resources['custom_process_ncpus'], + memory=process_resources['custom_process_memgb'], + hours=process_resources['custom_process_hours'], + minutes=process_resources['custom_process_minutes'], + seconds=process_resources['custom_process_seconds'], + prefix=' ' + )) + named_resources.append(' }') + + # Construct labelled process resources + labelled_resources = [] + if self.template_config.labelled_process_resources: + for process_label, process_resources in self.template_config.labelled_process_resources.items(): + labelled_resources.append( + f" withLabel: '{process_label}'" + " {" + ) + labelled_resources.extend(self.get_resource_strings( + cpus=process_resources['custom_process_ncpus'], + memory=process_resources['custom_process_memgb'], + hours=process_resources['custom_process_hours'], + minutes=process_resources['custom_process_minutes'], + seconds=process_resources['custom_process_seconds'], + prefix=' ' + )) + labelled_resources.append(' }') + + process_section = [ + 'process {', + *default_resources, + *named_resources, + *labelled_resources, + '}', + ] + + return '\n'.join(process_section) + '\n' + def write_to_file(self): ## File name option config_name = str(self.template_config.general_config_name).strip() filename = sub(r'\s+', '_', config_name) + ".conf" ## Collect all config entries per scope, for later checking scope needs to be written - validparams = self.construct_params( - self.template_config.config_profile_contact, - self.template_config.config_profile_handle, - self.template_config.config_profile_description, - self.template_config.config_profile_url, - ) + params_section_str = self.construct_params_str() + + if self.config_type == 'pipeline': + process_section_str = self.construct_process_config_str() + else: + process_section_str = '' with open(filename, "w+") as file: ## Write params - if any(validparams): - file.write("params {\n") - for entry_key, entry_value in validparams.items(): - if entry_value != "": - file.write(generate_config_entry(self, entry_key, entry_value)) - else: - continue - file.write("}\n") + file.write(params_section_str) + file.write(process_section_str) diff --git a/nf_core/configs/create/final.py b/nf_core/configs/create/final.py index c995640987..b9aeddc3c5 100644 --- a/nf_core/configs/create/final.py +++ b/nf_core/configs/create/final.py @@ -36,7 +36,7 @@ def compose(self) -> ComposeResult: def _create_config(self) -> None: """Create the config.""" - create_obj = ConfigCreate(template_config=self.parent.TEMPLATE_CONFIG) + create_obj = ConfigCreate(template_config=self.parent.TEMPLATE_CONFIG, config_type=self.parent.CONFIG_TYPE) create_obj.write_to_file() @on(Button.Pressed, "#close_app") From 52de1f10f27f02507fe62091d948bc6a30a4b400 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 14 Jan 2026 11:38:32 +1100 Subject: [PATCH 033/121] validate final output dir, allow optional custom resources --- nf_core/configs/create/__init__.py | 5 +++-- nf_core/configs/create/create.py | 4 +++- nf_core/configs/create/customprocessres.py | 4 ++-- nf_core/configs/create/defaultprocessres.py | 7 +++++-- nf_core/configs/create/final.py | 20 +++++++++++++++----- nf_core/configs/create/utils.py | 12 ++++++++++++ 6 files changed, 40 insertions(+), 12 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 9e2bf68d49..8522139e14 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -109,11 +109,12 @@ def on_button_pressed(self, event: Button.Pressed) -> None: elif event.button.id == "finish": self.push_screen("final") ## General options - if event.button.id == "close_app": - self.exit(return_code=0) if event.button.id == "back": self.pop_screen() + def close_app(self): + self.exit(return_code=0) + ## User theme options def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index d4a0be7c99..1fa89beb2c 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -7,9 +7,10 @@ class ConfigCreate: - def __init__(self, template_config: ConfigsCreateConfig, config_type: str): + def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir:str = '.'): self.template_config = template_config self.config_type = config_type + self.config_dir = sub(r'/$', '', config_dir) def construct_info_params(self): final_params = {} @@ -131,6 +132,7 @@ def write_to_file(self): ## File name option config_name = str(self.template_config.general_config_name).strip() filename = sub(r'\s+', '_', config_name) + ".conf" + filename = f'{self.config_dir}/{filename}' ## Collect all config entries per scope, for later checking scope needs to be written params_section_str = self.construct_params_str() diff --git a/nf_core/configs/create/customprocessres.py b/nf_core/configs/create/customprocessres.py index 3fd6dc50b5..8bfac5cae8 100644 --- a/nf_core/configs/create/customprocessres.py +++ b/nf_core/configs/create/customprocessres.py @@ -98,7 +98,7 @@ def compose(self) -> ComposeResult: yield Center( Button("Back", id="back", variant="default"), Button("Configure another process", id="another"), - Button("Next", id="next", variant="success"), + Button("Finish", id="finish_config", variant="success"), classes="cta", ) @@ -116,7 +116,7 @@ def on_toggle_process_name_label(self, event: Switch.Changed) -> None: @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" - if event.button.id in ["next", "another"]: + if event.button.id in ["finish_config", "another"]: tmp_config = {} for text_input in self.query("TextInput"): this_input = text_input.query_one(Input) diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index b450a2025b..98205a2b65 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -68,7 +68,8 @@ def compose(self) -> ComposeResult: ) yield Center( Button("Back", id="back", variant="default"), - Button("Next", id="next", variant="success"), + Button("Configure specific processes", id="config_specific_process", variant="success"), + Button("Finish", id="finish_config", variant="success"), classes="cta", ) @@ -91,7 +92,9 @@ def on_button_pressed(self, event: Button.Pressed) -> None: ConfigsCreateConfig(**new_config) # If that passes validation, update the existing config self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) - if event.button.id == "next": + if event.button.id == "config_specific_process": self.parent.push_screen("custom_process_resources") + elif event.button.id == "finish_config": + self.parent.push_screen("final") except ValueError: pass diff --git a/nf_core/configs/create/final.py b/nf_core/configs/create/final.py index b9aeddc3c5..b11cd770a5 100644 --- a/nf_core/configs/create/final.py +++ b/nf_core/configs/create/final.py @@ -2,12 +2,12 @@ from textual.app import ComposeResult from textual.containers import Center from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Markdown +from textual.widgets import Button, Footer, Header, Markdown, Input from nf_core.configs.create.create import ( ConfigCreate, ) -from nf_core.configs.create.utils import TextInput +from nf_core.configs.create.utils import TextInput, ConfigsCreateConfig, init_context class FinalScreen(Screen): @@ -34,12 +34,22 @@ def compose(self) -> ComposeResult: classes="cta" ) - def _create_config(self) -> None: + def _create_config(self, config_dir='.') -> None: """Create the config.""" - create_obj = ConfigCreate(template_config=self.parent.TEMPLATE_CONFIG, config_type=self.parent.CONFIG_TYPE) + create_obj = ConfigCreate(template_config=self.parent.TEMPLATE_CONFIG, config_type=self.parent.CONFIG_TYPE, config_dir=config_dir) create_obj.write_to_file() @on(Button.Pressed, "#close_app") def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" - self._create_config() + # Validate the save location + save_location = self.query_one("TextInput") + save_location_text = save_location.query_one(Input) + try: + with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + ConfigsCreateConfig(savelocation=save_location_text.value) + # If validation passes, create the config + self._create_config(config_dir=save_location_text.value) + self.parent.close_app() + except ValueError: + pass diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 8dbce93f73..37425dffd5 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -80,6 +80,8 @@ class ConfigsCreateConfig(BaseModel): """ Dictionary containing custom resource requirements for labelled processes """ is_nfcore: Optional[bool] = None """ Whether the config is part of the nf-core organisation """ + savelocation: Optional[str] = None + """ Final location of the configuration file """ model_config = ConfigDict(extra="allow") @@ -111,6 +113,16 @@ def path_valid(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a valid path.") return v + @field_validator("savelocation") + @classmethod + def final_path_valid(cls, v: str, info: ValidationInfo) -> str: + """Check that the final save directory is valid.""" + if v.strip() == "": + raise ValueError("Cannot be left empty.") + if not Path(v).is_dir(): + raise ValueError("Must be a valid path to a directory.") + return v + @field_validator("config_pipeline_name") @classmethod def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: From c8b955a82b1d28306175b7c4e23e4a3c42242b37 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 14 Jan 2026 12:54:16 +1100 Subject: [PATCH 034/121] add attributes for infra config to model, clean up context setting --- nf_core/configs/create/__init__.py | 14 +++++++++++ nf_core/configs/create/basicdetails.py | 2 +- nf_core/configs/create/customprocessres.py | 2 +- nf_core/configs/create/defaultprocessres.py | 2 +- nf_core/configs/create/final.py | 2 +- nf_core/configs/create/utils.py | 27 ++++++++++++++++++++- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 8522139e14..1049dbcb86 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -72,6 +72,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): # Tracking variables CONFIG_TYPE = None NFCORE_CONFIG = True + INFRA_ISHPC = False # Log handler LOG_HANDLER = rich_log_handler @@ -103,7 +104,13 @@ def on_button_pressed(self, event: Button.Pressed) -> None: utils.NFCORE_CONFIG_GLOBAL = False self.push_screen("basic_details") elif event.button.id == "type_hpc": + self.INFRA_ISHPC = True + utils.INFRA_ISHPC_GLOBAL = True self.push_screen("hpc_customisation") + elif event.button.id == "type_local": + self.INFRA_ISHPC = False + utils.INFRA_ISHPC_GLOBAL = False + self.push_screen("final_infra_details") elif event.button.id == "toconfiguration": self.push_screen("final_infra_details") elif event.button.id == "finish": @@ -119,3 +126,10 @@ def close_app(self): def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" self.theme: str = "textual-dark" if self.theme == "textual-light" else "textual-light" + + def get_context(self): + return { + "is_nfcore": self.NFCORE_CONFIG, + "is_infrastructure": self.CONFIG_TYPE == "infrastructure", + "is_hpc": self.INFRA_ISHPC + } diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 4d3e65f468..936610ee80 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -102,7 +102,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: else: text_input.query_one(".validation_msg").update("") try: - with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + with init_context(self.parent.get_context()): self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) if event.button.id == "next": if self.parent.CONFIG_TYPE == "infrastructure": diff --git a/nf_core/configs/create/customprocessres.py b/nf_core/configs/create/customprocessres.py index 8bfac5cae8..2d02519d6f 100644 --- a/nf_core/configs/create/customprocessres.py +++ b/nf_core/configs/create/customprocessres.py @@ -128,7 +128,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: text_input.query_one(".validation_msg").update("") # Validate the config try: - with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + with init_context(self.parent.get_context()): ConfigsCreateConfig(**tmp_config) # Add to the config stack tmp_config['select_label'] = self.select_label diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 98205a2b65..6a8c294d8a 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -87,7 +87,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: else: text_input.query_one(".validation_msg").update("") try: - with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + with init_context(self.parent.get_context()): # First, validate the new config data ConfigsCreateConfig(**new_config) # If that passes validation, update the existing config diff --git a/nf_core/configs/create/final.py b/nf_core/configs/create/final.py index b11cd770a5..9ba08e56f6 100644 --- a/nf_core/configs/create/final.py +++ b/nf_core/configs/create/final.py @@ -46,7 +46,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: save_location = self.query_one("TextInput") save_location_text = save_location.query_one(Input) try: - with init_context({"is_nfcore": self.parent.NFCORE_CONFIG, "is_infrastructure": self.parent.CONFIG_TYPE == "infrastructure"}): + with init_context(self.parent.get_context()): ConfigsCreateConfig(savelocation=save_location_text.value) # If validation passes, create the config self._create_config(config_dir=save_location_text.value) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 37425dffd5..53d6ca20db 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -31,6 +31,7 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: # Define a global variable to store the config type CONFIG_ISINFRASTRUCTURE_GLOBAL: bool = True NFCORE_CONFIG_GLOBAL: bool = True +INFRA_ISHPC_GLOBAL: bool = False class ConfigsCreateConfig(BaseModel): @@ -82,6 +83,30 @@ class ConfigsCreateConfig(BaseModel): """ Whether the config is part of the nf-core organisation """ savelocation: Optional[str] = None """ Final location of the configuration file """ + scheduler: Optional[str] = None + """ The scheduler that the HPC uses """ + queue: Optional[str] = None + """ The default queue that the HPC uses """ + module_system: Optional[str] = None + """ Modules to load when running processes """ + container_system: Optional[str] = None + """ The container system the HPC uses """ + memory: Optional[str] = None + """ The maximum memory available to processes """ + cpus: Optional[str] = None + """ The maximum number of CPUs available to processes """ + time: Optional[str] = None + """ The maximum walltime available to processes """ + envvar: Optional[str] = None + """ An environment variable to hold a custom Nextflow container cachedir """ + cachedir: Optional[str] = None + """ An environment variable to hold a custom Nextflow container cachedir """ + igenomes_cachedir: Optional[str] = None + """ A cachedir for iGenomes """ + scratch_dir: Optional[str] = None + """ A scratch directory to use """ + retries: Optional[str] = None + """ Number of retries for failed jobs """ model_config = ConfigDict(extra="allow") @@ -307,7 +332,7 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: - with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL, "is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL}): + with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL, "is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL, "is_hpc": INFRA_ISHPC_GLOBAL}): ConfigsCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: From 9f4393152a944e4bfd8b982b1f47a6d427245a31 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 20 Jan 2026 16:17:44 +1100 Subject: [PATCH 035/121] add separate screens for process names and labels, switches to determine what to configure --- nf_core/configs/create/__init__.py | 10 +- nf_core/configs/create/basicdetails.py | 2 +- nf_core/configs/create/create.py | 91 +++++++----- nf_core/configs/create/defaultprocessres.py | 44 ++---- nf_core/configs/create/labelledprocessres.py | 125 +++++++++++++++++ nf_core/configs/create/namedprocessres.py | 129 ++++++++++++++++++ .../configs/create/pipelineconfigquestion.py | 121 ++++++++++++++++ nf_core/configs/create/utils.py | 24 ++-- 8 files changed, 469 insertions(+), 77 deletions(-) create mode 100644 nf_core/configs/create/labelledprocessres.py create mode 100644 nf_core/configs/create/namedprocessres.py create mode 100644 nf_core/configs/create/pipelineconfigquestion.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 1049dbcb86..d1f37790c1 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -20,8 +20,10 @@ from nf_core.configs.create.hpcquestion import ChooseHpc from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.welcome import WelcomeScreen +from nf_core.configs.create.pipelineconfigquestion import PipelineConfigQuestion from nf_core.configs.create.defaultprocessres import DefaultProcess -from nf_core.configs.create.customprocessres import CustomProcess +from nf_core.configs.create.namedprocessres import NamedProcess +from nf_core.configs.create.labelledprocessres import LabelledProcess ## General utilities from nf_core.utils import LoggingConsole @@ -61,8 +63,10 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "final": FinalScreen, "hpc_question": ChooseHpc, "hpc_customisation": HpcCustomisation, + "pipelineconfigquestion": PipelineConfigQuestion, "default_process_resources": DefaultProcess, - "custom_process_resources": CustomProcess, + "named_process_resources": NamedProcess, + "labelled_process_resources": LabelledProcess, "final_infra_details": FinalInfraDetails, } @@ -73,6 +77,8 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): CONFIG_TYPE = None NFCORE_CONFIG = True INFRA_ISHPC = False + PIPE_CONF_NAMED = False + PIPE_CONF_LABELLED = False # Log handler LOG_HANDLER = rich_log_handler diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 936610ee80..de948aa62e 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -108,7 +108,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: if self.parent.CONFIG_TYPE == "infrastructure": self.parent.push_screen("hpc_question") elif self.parent.CONFIG_TYPE == "pipeline": - self.parent.push_screen("default_process_resources") + self.parent.push_screen("pipelineconfigquestion") except ValueError: pass diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 1fa89beb2c..6b84668b3a 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -47,40 +47,59 @@ def construct_params_str(self): params_section = [ 'params {', + '\n', *info_params_str_list, + '\n', '}', ] - return '\n'.join(params_section) + '\n' + params_section_str = '\n'.join(params_section) + '\n\n' + return sub(r'\n\n\n+', '\n\n', params_section_str) - def get_resource_strings(self, cpus, memory, hours, minutes, seconds, prefix=''): - cpus_int = int(cpus) - cpus_str = f'cpus = {cpus_int}' - - memory_int = int(memory) - memory_str = f'memory = {memory_int}.Gb' - - time_h = int(hours) - time_m = int(minutes) - time_s = int(seconds) - time_str = f"time = '{time_h}h {time_m}m {time_s}s'" + def get_resource_strings(self, cpus, memory, hours, prefix=''): + cpus_str = '' + if cpus: + cpus_int = int(cpus) + cpus_str = f'cpus = {cpus_int}' + + memory_str = '' + if memory: + memory_int = int(memory) + memory_str = f'memory = {memory_int}.GB' + + time_str = '' + if hours: + time_h = None + time_m = None + try: + time_h = int(hours) + except: + try: + time_m = int(float(hours) * 60) + except: + raise ValueError("Non-numeric value supplied for walltime value.") + if time_m is not None and time_m % 60 == 0: + time_h = int(time_m / 60) + if time_h is not None: + time_str = f"time = {time_h}.h" + elif time_m is not None: + time_str = f"time = {time_m}.m" + else: + raise ValueError("Non-numeric value supplied for walltime value.") resources = [cpus_str, memory_str, time_str] return [ f'{prefix}{res}' for res in resources + if res ] def construct_process_config_str(self): - process_config_str_list = [] - # Construct default resources default_resources = self.get_resource_strings( cpus=self.template_config.default_process_ncpus, memory=self.template_config.default_process_memgb, hours=self.template_config.default_process_hours, - minutes=self.template_config.default_process_minutes, - seconds=self.template_config.default_process_seconds, prefix=' ' ) @@ -88,45 +107,55 @@ def construct_process_config_str(self): named_resources = [] if self.template_config.named_process_resources: for process_name, process_resources in self.template_config.named_process_resources.items(): - named_resources.append( - f" withName: '{process_name}'" + " {" - ) - named_resources.extend(self.get_resource_strings( + named_resource_string = self.get_resource_strings( cpus=process_resources['custom_process_ncpus'], memory=process_resources['custom_process_memgb'], hours=process_resources['custom_process_hours'], - minutes=process_resources['custom_process_minutes'], - seconds=process_resources['custom_process_seconds'], prefix=' ' - )) + ) + if not named_resource_string: + continue + named_resources.append( + f" withName: '{process_name}'" + " {" + ) + named_resources.extend(named_resource_string) named_resources.append(' }') + named_resources.append('\n') # Construct labelled process resources labelled_resources = [] if self.template_config.labelled_process_resources: for process_label, process_resources in self.template_config.labelled_process_resources.items(): - labelled_resources.append( - f" withLabel: '{process_label}'" + " {" - ) - labelled_resources.extend(self.get_resource_strings( + labelled_resource_string = self.get_resource_strings( cpus=process_resources['custom_process_ncpus'], memory=process_resources['custom_process_memgb'], hours=process_resources['custom_process_hours'], - minutes=process_resources['custom_process_minutes'], - seconds=process_resources['custom_process_seconds'], prefix=' ' - )) + ) + if not labelled_resource_string: + continue + labelled_resources.append( + f" withLabel: '{process_label}'" + " {" + ) + labelled_resources.extend(labelled_resource_string) labelled_resources.append(' }') + labelled_resources.append('\n') process_section = [ 'process {', + '\n', *default_resources, + '\n', *named_resources, + '\n', *labelled_resources, + '\n', '}', ] - return '\n'.join(process_section) + '\n' + process_section_str = '\n'.join(process_section) + '\n' + + return sub(r'\n\n\n+', '\n\n', process_section_str) def write_to_file(self): ## File name option diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 6a8c294d8a..100873e6fc 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -43,38 +43,21 @@ def compose(self) -> ComposeResult: "8", classes="column", ) - yield Markdown("The walltime required by default for all processes.") - with Horizontal(): - yield TextInput( - "default_process_hours", - "1", - "Hours:", - "1", - classes="column", - ) - yield TextInput( - "default_process_minutes", - "0", - "Minutes:", - "0", - classes="column", - ) - yield TextInput( - "default_process_seconds", - "0", - "Seconds:", - "0", - classes="column", - ) + yield TextInput( + "default_process_hours", + "1", + "The default number of hours of walltime required for processes:", + "1", + classes="column", + ) yield Center( Button("Back", id="back", variant="default"), - Button("Configure specific processes", id="config_specific_process", variant="success"), - Button("Finish", id="finish_config", variant="success"), + Button("Next", id="next", variant="success"), classes="cta", ) # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs - @on(Button.Pressed) + @on(Button.Pressed, "#next") def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" new_config = {} @@ -92,9 +75,12 @@ def on_button_pressed(self, event: Button.Pressed) -> None: ConfigsCreateConfig(**new_config) # If that passes validation, update the existing config self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) - if event.button.id == "config_specific_process": - self.parent.push_screen("custom_process_resources") - elif event.button.id == "finish_config": + # Push the next screen + if self.parent.PIPE_CONF_NAMED: + self.parent.push_screen("named_process_resources") + elif self.parent.PIPE_CONF_LABELLED: + self.parent.push_screen("labelled_process_resources") + else: self.parent.push_screen("final") except ValueError: pass diff --git a/nf_core/configs/create/labelledprocessres.py b/nf_core/configs/create/labelledprocessres.py new file mode 100644 index 0000000000..ce94c72663 --- /dev/null +++ b/nf_core/configs/create/labelledprocessres.py @@ -0,0 +1,125 @@ +"""Get information about which process/label the user wants to configure.""" + +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label +from enum import Enum +from nf_core.utils import add_hide_class, remove_hide_class + +from nf_core.configs.create.utils import ( + ConfigsCreateConfig, + TextInput, + init_context +) ## TODO Move somewhere common? +from nf_core.utils import add_hide_class, remove_hide_class + + +class LabelledProcess(Screen): + """Get labelled process resource requirements.""" + + def __init__(self) -> None: + super().__init__() + self.config_stack = [] + self.current_config = {} + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Custom process resources by label + """ + ) + ) + yield TextInput( + "custom_process_name", + "", + "The process label you wish to configure.", + "", + classes="column", + ) + yield TextInput( + "custom_process_ncpus", + "2", + "Number of CPUs to use for the process.", + "2", + classes="column", + ) + yield TextInput( + "custom_process_memgb", + "8", + "Amount of memory in GB to use for the process.", + "8", + classes="column", + ) + yield TextInput( + "custom_process_hours", + "1", + "The number of hours of walltime required for the process:", + "1", + classes="column", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Configure another process", id="another"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + if event.button.id in ["next", "another"]: + tmp_config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + tmp_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + # Validate the config + try: + with init_context(self.parent.get_context()): + ConfigsCreateConfig(**tmp_config) + # Add to the config stack + self.config_stack.append(tmp_config) + if event.button.id == "another": + # If configuring another process, push a blank config to the config stack + # and push a new copy of this screen to the screen stack + self.config_stack.append({}) + self.parent.push_screen("labelled_process_resources") + else: + # If finalising the custom resources, add them all to the config now + key = "labelled_process_resources" + new_config = {key: {}} + for tmp_config in self.config_stack: + process_label = tmp_config.get('custom_process_name') + new_config[key][process_label] = tmp_config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + self.parent.push_screen("final") + except ValueError: + pass + + def on_screen_resume(self): + # Grab the last config in the stack if it exists + try: + self.current_config = self.config_stack.pop() + except IndexError: + self.current_config = {} + # Reset all input field values + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + this_input.clear() + field_id = text_input.field_id + if field_id in self.current_config: + this_input.insert(self.current_config[field_id], 0) + else: + text_input.refresh(repaint=True, layout=True, recompose=True) diff --git a/nf_core/configs/create/namedprocessres.py b/nf_core/configs/create/namedprocessres.py new file mode 100644 index 0000000000..3cf335afe2 --- /dev/null +++ b/nf_core/configs/create/namedprocessres.py @@ -0,0 +1,129 @@ +"""Get information about which process/label the user wants to configure.""" + +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label +from enum import Enum +from nf_core.utils import add_hide_class, remove_hide_class + +from nf_core.configs.create.utils import ( + ConfigsCreateConfig, + TextInput, + init_context +) ## TODO Move somewhere common? +from nf_core.utils import add_hide_class, remove_hide_class + + +class NamedProcess(Screen): + """Get named process resource requirements.""" + + def __init__(self) -> None: + super().__init__() + self.config_stack = [] + self.current_config = {} + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # Custom process resources by name + """ + ) + ) + yield TextInput( + "custom_process_name", + "", + "The name of the process you wish to configure.", + "", + classes="column", + ) + yield TextInput( + "custom_process_ncpus", + "2", + "Number of CPUs to use for the process.", + "2", + classes="column", + ) + yield TextInput( + "custom_process_memgb", + "8", + "Amount of memory in GB to use for the process.", + "8", + classes="column", + ) + yield TextInput( + "custom_process_hours", + "1", + "The number of hours of walltime required for the process:", + "1", + classes="column", + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Configure another process", id="another"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + if event.button.id in ["next", "another"]: + tmp_config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + tmp_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + # Validate the config + try: + with init_context(self.parent.get_context()): + ConfigsCreateConfig(**tmp_config) + # Add to the config stack + self.config_stack.append(tmp_config) + if event.button.id == "another": + # If configuring another process, push a blank config to the config stack + # and push a new copy of this screen to the screen stack + self.config_stack.append({}) + self.parent.push_screen("named_process_resources") + else: + # If finalising the custom resources, add them all to the config now + key = "named_process_resources" + new_config = {key: {}} + for tmp_config in self.config_stack: + process_name = tmp_config.get('custom_process_name') + new_config[key][process_name] = tmp_config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + # Push the next screen + if self.parent.PIPE_CONF_LABELLED: + self.parent.push_screen("labelled_process_resources") + else: + self.parent.push_screen("final") + except ValueError: + pass + + def on_screen_resume(self): + # Grab the last config in the stack if it exists + try: + self.current_config = self.config_stack.pop() + except IndexError: + self.current_config = {} + # Reset all input field values + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + this_input.clear() + field_id = text_input.field_id + if field_id in self.current_config: + this_input.insert(self.current_config[field_id], 0) + else: + text_input.refresh(repaint=True, layout=True, recompose=True) diff --git a/nf_core/configs/create/pipelineconfigquestion.py b/nf_core/configs/create/pipelineconfigquestion.py new file mode 100644 index 0000000000..a188e0b443 --- /dev/null +++ b/nf_core/configs/create/pipelineconfigquestion.py @@ -0,0 +1,121 @@ +"""Get information about which process/label the user wants to configure.""" + +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, Horizontal +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label +from enum import Enum +from nf_core.utils import add_hide_class, remove_hide_class + +from nf_core.configs.create.utils import ( + ConfigsCreateConfig, + TextInput, + init_context +) ## TODO Move somewhere common? +from nf_core.utils import add_hide_class, remove_hide_class + + +class PipelineConfigQuestion(Screen): + """Determine whether the user wants to configure the default resources and/or specific process names/labels.""" + + def __init__(self) -> None: + super().__init__() + self.config_defaults = False + self.config_named_processes = False + self.config_labels = False + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown( + dedent( + """ + # What would you like to configure? + """ + ) + ) + with Horizontal(): + yield Label( + "Configure default process resources?", + id="toggle_configure_defaults_label" + ) + yield Switch( + id="toggle_configure_defaults", + value=self.config_defaults, + ) + yield Label( + "Yes" if self.config_defaults else "No", + id="toggle_configure_defaults_state_label" + ) + with Horizontal(): + yield Label( + "Configure specific named processes?", + id="toggle_configure_names_label" + ) + yield Switch( + id="toggle_configure_names", + value=self.config_defaults, + ) + yield Label( + "Yes" if self.config_defaults else "No", + id="toggle_configure_names_state_label" + ) + with Horizontal(): + yield Label( + "Configure labels?", + id="toggle_configure_labels_label" + ) + yield Switch( + id="toggle_configure_labels", + value=self.config_defaults, + ) + yield Label( + "Yes" if self.config_defaults else "No", + id="toggle_configure_labels_state_label" + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + @on(Switch.Changed) + def on_toggle_switch(self, event: Switch.Changed) -> None: + """ Handle toggling the switches that determine which pipeline resources to configure """ + valid_toggles = { + 'toggle_configure_defaults': 'config_defaults', + 'toggle_configure_names': 'config_named_processes', + 'toggle_configure_labels': 'config_labels', + } + + if event.switch.id not in valid_toggles: + return + + attr = valid_toggles[event.switch.id] + self.__setattr__(attr, event.value) + + # Update the switch label + for label in self.query(Label): + if label.id == f'{event.switch.id}_state_label': + label.update("Yes" if event.value else "No") + + # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs + @on(Button.Pressed, "#next") + def on_next_button_pressed(self, event: Button.Pressed) -> None: + """Save configuration options and then move to the next screen.""" + # Update app tracking variables for whether to configure named and/or labelled processes + self.parent.PIPE_CONF_NAMED = self.config_named_processes + self.parent.PIPE_CONF_LABELLED = self.config_labels + + # Proceed to next screen depending on what choices the user has made + if self.config_defaults: + self.parent.push_screen("default_process_resources") + elif self.config_named_processes: + self.parent.push_screen("named_process_resources") + elif self.config_labels: + self.parent.push_screen("labelled_process_resources") + else: + self.parent.push_screen("final") diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 53d6ca20db..f3ccb6efdf 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -71,10 +71,6 @@ class ConfigsCreateConfig(BaseModel): """ Amount of memory for process """ custom_process_hours: Optional[str] = None """ Walltime for process - hours """ - custom_process_minutes: Optional[str] = None - """ Walltime for process - minutes """ - custom_process_seconds: Optional[str] = None - """ Walltime for process - seconds """ named_process_resources: Optional[dict] = None """ Dictionary containing custom resource requirements for named processes """ labelled_process_resources: Optional[dict] = None @@ -236,11 +232,11 @@ def notempty_process_name(cls, v: str, info: ValidationInfo) -> str: @field_validator("default_process_ncpus", "default_process_memgb", "custom_process_ncpus", "custom_process_memgb") @classmethod def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: - """Check that integer values are non-empty and positive.""" + """Check that integer values are either empty or positive.""" context = info.context if context and not context["is_infrastructure"]: if v.strip() == "": - raise ValueError("Cannot be left empty.") + return v try: v_int = int(v.strip()) except ValueError: @@ -249,20 +245,20 @@ def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a positive integer.") return v - @field_validator("default_process_hours", "default_process_minutes", "default_process_seconds", "custom_process_hours", "custom_process_minutes", "custom_process_seconds") + @field_validator("default_process_hours", "custom_process_hours") @classmethod - def non_neg_integer_valid(cls, v: str, info: ValidationInfo) -> str: - """Check that integer values are non-empty and non-negative.""" + def non_neg_float_valid(cls, v: str, info: ValidationInfo) -> str: + """Check that numeric values are either empty or non-negative.""" context = info.context if context and not context["is_infrastructure"]: if v.strip() == "": - raise ValueError("Cannot be left empty.") + return v try: - v_int = int(v.strip()) + vf = float(v.strip()) except ValueError: - raise ValueError("Must be an integer.") - if not v_int >= 0: - raise ValueError("Must be a non-negative integer.") + raise ValueError("Must be a number.") + if not vf >= 0: + raise ValueError("Must be a non-negative number.") return v ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) From 655d90e1b4fbfc06bfcc50a884220dbcdfa4d85f Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 29 Jan 2026 10:55:06 +1100 Subject: [PATCH 036/121] use pathlib, clean up time resource string, use snakecase for screen ids --- nf_core/configs/create/__init__.py | 2 +- nf_core/configs/create/basicdetails.py | 2 +- nf_core/configs/create/create.py | 35 ++++++++++++-------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index d1f37790c1..a0aaefcef1 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -63,7 +63,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "final": FinalScreen, "hpc_question": ChooseHpc, "hpc_customisation": HpcCustomisation, - "pipelineconfigquestion": PipelineConfigQuestion, + "pipeline_config_question": PipelineConfigQuestion, "default_process_resources": DefaultProcess, "named_process_resources": NamedProcess, "labelled_process_resources": LabelledProcess, diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index de948aa62e..7214e397b1 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -108,7 +108,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: if self.parent.CONFIG_TYPE == "infrastructure": self.parent.push_screen("hpc_question") elif self.parent.CONFIG_TYPE == "pipeline": - self.parent.push_screen("pipelineconfigquestion") + self.parent.push_screen("pipeline_config_question") except ValueError: pass diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 6b84668b3a..6c37e42c10 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -4,13 +4,18 @@ from nf_core.configs.create.utils import ConfigsCreateConfig, generate_config_entry from re import sub +from pathlib import Path class ConfigCreate: - def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir:str = '.'): + def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir: Path | str = Path('.')): self.template_config = template_config self.config_type = config_type - self.config_dir = sub(r'/$', '', config_dir) + config_dir_path = config_dir if isinstance(config_dir, Path) else Path(config_dir) + assert not config_dir_path.is_file(), f'Error: the path "{str(config_dir_path)}" is a file.' + # Create directory if it doesn't already exist + config_dir_path.mkdir(parents=True, exist_ok=True) + self.config_dir = config_dir_path def construct_info_params(self): final_params = {} @@ -69,23 +74,13 @@ def get_resource_strings(self, cpus, memory, hours, prefix=''): time_str = '' if hours: - time_h = None - time_m = None - try: - time_h = int(hours) - except: - try: - time_m = int(float(hours) * 60) - except: - raise ValueError("Non-numeric value supplied for walltime value.") - if time_m is not None and time_m % 60 == 0: - time_h = int(time_m / 60) - if time_h is not None: + time_h = float(hours) + if time_h.is_integer(): + time_h = int(time_h) time_str = f"time = {time_h}.h" - elif time_m is not None: - time_str = f"time = {time_m}.m" else: - raise ValueError("Non-numeric value supplied for walltime value.") + time_m = int(time_h * 60) + time_str = f"time = {time_m}.m" resources = [cpus_str, memory_str, time_str] return [ @@ -160,8 +155,10 @@ def construct_process_config_str(self): def write_to_file(self): ## File name option config_name = str(self.template_config.general_config_name).strip() - filename = sub(r'\s+', '_', config_name) + ".conf" - filename = f'{self.config_dir}/{filename}' + config_name_clean = sub(r'\W+', '_', config_name) + config_name_clean = sub(r'_+$', '', config_name_clean) + filename = f'{config_name_clean}.conf' + filename = self.config_dir / filename ## Collect all config entries per scope, for later checking scope needs to be written params_section_str = self.construct_params_str() From e7deba0e20a0da931e9ba794806918f35fc3f2d6 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 29 Jan 2026 14:59:10 +1100 Subject: [PATCH 037/121] refactor process config page into single screen with multiple, removable config widgets --- nf_core/configs/create/__init__.py | 7 +- nf_core/configs/create/customprocessres.py | 180 ------------------ nf_core/configs/create/defaultprocessres.py | 15 +- nf_core/configs/create/labelledprocessres.py | 125 ------------ nf_core/configs/create/multiprocessres.py | 174 +++++++++++++++++ nf_core/configs/create/namedprocessres.py | 129 ------------- .../configs/create/pipelineconfigquestion.py | 4 +- nf_core/configs/create/utils.py | 8 +- 8 files changed, 194 insertions(+), 448 deletions(-) delete mode 100644 nf_core/configs/create/customprocessres.py delete mode 100644 nf_core/configs/create/labelledprocessres.py create mode 100644 nf_core/configs/create/multiprocessres.py delete mode 100644 nf_core/configs/create/namedprocessres.py diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index a0aaefcef1..371364e120 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -22,8 +22,7 @@ from nf_core.configs.create.welcome import WelcomeScreen from nf_core.configs.create.pipelineconfigquestion import PipelineConfigQuestion from nf_core.configs.create.defaultprocessres import DefaultProcess -from nf_core.configs.create.namedprocessres import NamedProcess -from nf_core.configs.create.labelledprocessres import LabelledProcess +from nf_core.configs.create.multiprocessres import MultiNamedProcessConfig, MultiLabelledProcessConfig ## General utilities from nf_core.utils import LoggingConsole @@ -65,8 +64,8 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): "hpc_customisation": HpcCustomisation, "pipeline_config_question": PipelineConfigQuestion, "default_process_resources": DefaultProcess, - "named_process_resources": NamedProcess, - "labelled_process_resources": LabelledProcess, + "multi_named_process_config": MultiNamedProcessConfig, + "multi_labelled_process_config": MultiLabelledProcessConfig, "final_infra_details": FinalInfraDetails, } diff --git a/nf_core/configs/create/customprocessres.py b/nf_core/configs/create/customprocessres.py deleted file mode 100644 index 2d02519d6f..0000000000 --- a/nf_core/configs/create/customprocessres.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Get information about which process/label the user wants to configure.""" - -from textwrap import dedent - -from textual import on -from textual.app import ComposeResult -from textual.containers import Center, Horizontal -from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label -from enum import Enum -from nf_core.utils import add_hide_class, remove_hide_class - -from nf_core.configs.create.utils import ( - ConfigsCreateConfig, - TextInput, - init_context -) ## TODO Move somewhere common? -from nf_core.utils import add_hide_class, remove_hide_class - - -class CustomProcess(Screen): - """Get default process resource requirements.""" - - def __init__(self) -> None: - super().__init__() - self.select_label = False - self.config_stack = [] - self.current_config = {} - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - yield Markdown( - dedent( - """ - # Custom process resources - """ - ) - ) - with Horizontal(): - yield TextInput( - "custom_process_name", - "", - "The name of the process you wish to configure.", - "", - classes="column", - ) - with Horizontal(): - yield Label( - "Selecting a process by name or label:", - id="toggle_process_name_label_text" - ) - yield Switch( - id="toggle_process_name_label", - value=self.select_label, - ) - yield Label( - "label" if self.select_label else "name", - id="name_or_label_text" - ) - yield TextInput( - "custom_process_ncpus", - "2", - "Number of CPUs to use for the process.", - "2", - classes="column", - ) - yield TextInput( - "custom_process_memgb", - "8", - "Amount of memory in GB to use for the process.", - "8", - classes="column", - ) - yield Markdown("The walltime required for the process.") - with Horizontal(): - yield TextInput( - "custom_process_hours", - "1", - "Hours:", - "1", - classes="column", - ) - yield TextInput( - "custom_process_minutes", - "0", - "Minutes:", - "0", - classes="column", - ) - yield TextInput( - "custom_process_seconds", - "0", - "Seconds:", - "0", - classes="column", - ) - yield Center( - Button("Back", id="back", variant="default"), - Button("Configure another process", id="another"), - Button("Finish", id="finish_config", variant="success"), - classes="cta", - ) - - @on(Switch.Changed, "#toggle_process_name_label") - def on_toggle_process_name_label(self, event: Switch.Changed) -> None: - """ Handle toggling the process name/label switch """ - self.select_label = event.value - # Update the switch label - for label in self.query(Label): - if label.id == "name_or_label_text": - label.update("label" if self.select_label else "name") - - - # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs - @on(Button.Pressed) - def on_button_pressed(self, event: Button.Pressed) -> None: - """Save fields to the config.""" - if event.button.id in ["finish_config", "another"]: - tmp_config = {} - for text_input in self.query("TextInput"): - this_input = text_input.query_one(Input) - validation_result = this_input.validate(this_input.value) - tmp_config[text_input.field_id] = this_input.value - if not validation_result.is_valid: - text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) - else: - text_input.query_one(".validation_msg").update("") - # Validate the config - try: - with init_context(self.parent.get_context()): - ConfigsCreateConfig(**tmp_config) - # Add to the config stack - tmp_config['select_label'] = self.select_label - self.config_stack.append(tmp_config) - if event.button.id == "another": - # If configuring another process, push a blank config to the config stack - # and push a new copy of this screen to the screen stack - self.config_stack.append({}) - self.parent.push_screen("custom_process_resources") - else: - # If finalising the custom resources, add them all to the config now - new_config = {} - for key in ["labelled_process_resources", "named_process_resources"]: - new_config[key] = self.parent.TEMPLATE_CONFIG.__dict__.get(key) - if new_config[key] is None: - new_config[key] = {} - for tmp_config in self.config_stack: - select_label = tmp_config['select_label'] - process_name_or_label = tmp_config.get('custom_process_name') - key = "labelled_process_resources" if select_label else "named_process_resources" - new_config[key][process_name_or_label] = tmp_config - self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) - self.parent.push_screen("final") - except ValueError: - pass - - def on_screen_resume(self): - # Grab the last config in the stack if it exists - try: - self.current_config = self.config_stack.pop() - except IndexError: - self.current_config = {} - # Reset all input field values - for text_input in self.query("TextInput"): - this_input = text_input.query_one(Input) - this_input.clear() - field_id = text_input.field_id - if field_id in self.current_config: - this_input.insert(self.current_config[field_id], 0) - else: - text_input.refresh(repaint=True, layout=True, recompose=True) - # Also reset switch - switch_input = self.query_one(Switch) - if 'select_label' in self.current_config: - self.select_label = self.current_config['select_label'] - else: - self.select_label = False - if switch_input.value != self.select_label: - switch_input.toggle() diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 100873e6fc..bf149154af 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -52,10 +52,21 @@ def compose(self) -> ComposeResult: ) yield Center( Button("Back", id="back", variant="default"), + Button("Skip", id="skip", variant="default"), Button("Next", id="next", variant="success"), classes="cta", ) + @on(Button.Pressed, "#skip") + def skip_to_next_screen(self) -> None: + # Skip to the next screen without saving + if self.parent.PIPE_CONF_NAMED: + self.parent.push_screen("multi_named_process_config") + elif self.parent.PIPE_CONF_LABELLED: + self.parent.push_screen("multi_labelled_process_config") + else: + self.parent.push_screen("final") + # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs @on(Button.Pressed, "#next") def on_button_pressed(self, event: Button.Pressed) -> None: @@ -77,9 +88,9 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) # Push the next screen if self.parent.PIPE_CONF_NAMED: - self.parent.push_screen("named_process_resources") + self.parent.push_screen("multi_named_process_config") elif self.parent.PIPE_CONF_LABELLED: - self.parent.push_screen("labelled_process_resources") + self.parent.push_screen("multi_labelled_process_config") else: self.parent.push_screen("final") except ValueError: diff --git a/nf_core/configs/create/labelledprocessres.py b/nf_core/configs/create/labelledprocessres.py deleted file mode 100644 index ce94c72663..0000000000 --- a/nf_core/configs/create/labelledprocessres.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Get information about which process/label the user wants to configure.""" - -from textwrap import dedent - -from textual import on -from textual.app import ComposeResult -from textual.containers import Center, Horizontal -from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label -from enum import Enum -from nf_core.utils import add_hide_class, remove_hide_class - -from nf_core.configs.create.utils import ( - ConfigsCreateConfig, - TextInput, - init_context -) ## TODO Move somewhere common? -from nf_core.utils import add_hide_class, remove_hide_class - - -class LabelledProcess(Screen): - """Get labelled process resource requirements.""" - - def __init__(self) -> None: - super().__init__() - self.config_stack = [] - self.current_config = {} - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - yield Markdown( - dedent( - """ - # Custom process resources by label - """ - ) - ) - yield TextInput( - "custom_process_name", - "", - "The process label you wish to configure.", - "", - classes="column", - ) - yield TextInput( - "custom_process_ncpus", - "2", - "Number of CPUs to use for the process.", - "2", - classes="column", - ) - yield TextInput( - "custom_process_memgb", - "8", - "Amount of memory in GB to use for the process.", - "8", - classes="column", - ) - yield TextInput( - "custom_process_hours", - "1", - "The number of hours of walltime required for the process:", - "1", - classes="column", - ) - yield Center( - Button("Back", id="back", variant="default"), - Button("Configure another process", id="another"), - Button("Next", id="next", variant="success"), - classes="cta", - ) - - # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs - @on(Button.Pressed) - def on_button_pressed(self, event: Button.Pressed) -> None: - """Save fields to the config.""" - if event.button.id in ["next", "another"]: - tmp_config = {} - for text_input in self.query("TextInput"): - this_input = text_input.query_one(Input) - validation_result = this_input.validate(this_input.value) - tmp_config[text_input.field_id] = this_input.value - if not validation_result.is_valid: - text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) - else: - text_input.query_one(".validation_msg").update("") - # Validate the config - try: - with init_context(self.parent.get_context()): - ConfigsCreateConfig(**tmp_config) - # Add to the config stack - self.config_stack.append(tmp_config) - if event.button.id == "another": - # If configuring another process, push a blank config to the config stack - # and push a new copy of this screen to the screen stack - self.config_stack.append({}) - self.parent.push_screen("labelled_process_resources") - else: - # If finalising the custom resources, add them all to the config now - key = "labelled_process_resources" - new_config = {key: {}} - for tmp_config in self.config_stack: - process_label = tmp_config.get('custom_process_name') - new_config[key][process_label] = tmp_config - self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) - self.parent.push_screen("final") - except ValueError: - pass - - def on_screen_resume(self): - # Grab the last config in the stack if it exists - try: - self.current_config = self.config_stack.pop() - except IndexError: - self.current_config = {} - # Reset all input field values - for text_input in self.query("TextInput"): - this_input = text_input.query_one(Input) - this_input.clear() - field_id = text_input.field_id - if field_id in self.current_config: - this_input.insert(self.current_config[field_id], 0) - else: - text_input.refresh(repaint=True, layout=True, recompose=True) diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py new file mode 100644 index 0000000000..0e38c81b59 --- /dev/null +++ b/nf_core/configs/create/multiprocessres.py @@ -0,0 +1,174 @@ +"""Get information about which process/label the user wants to configure.""" + +from textwrap import dedent + +from textual import on +from textual.app import ComposeResult +from textual.containers import Center, HorizontalGroup, VerticalScroll +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label +from textual.events import Mount, ScreenResume +from nf_core.utils import add_hide_class, remove_hide_class + +from nf_core.configs.create.utils import ( + ConfigsCreateConfig, + TextInput, + init_context +) ## TODO Move somewhere common? +from nf_core.utils import add_hide_class, remove_hide_class + + +class ProcessConfig(HorizontalGroup): + """Get resource requirements for a single process.""" + + def __init__(self, selector: str) -> None: + super().__init__() + assert selector in ['name', 'label'] + self.selector = selector + + def compose(self) -> ComposeResult: + yield TextInput( + "custom_process_id", + "", + f"Process {self.selector}:", + "", + classes="column", + ) + yield TextInput( + "custom_process_ncpus", + "2", + "# CPUs:", + "2", + classes="column", + ) + yield TextInput( + "custom_process_memgb", + "8", + "Memory (GB):", + "8", + classes="column", + ) + yield TextInput( + "custom_process_hours", + "1", + "Walltime (hours):", + "1", + classes="column", + ) + yield Button( + "-", + id="remove", + variant="error" + ) + + @on(Button.Pressed, "#remove") + def remove_widget(self) -> None: + self.remove() + + +class MultiProcessConfig(Screen): + """Get resource requirements for multiple processes.""" + + def __init__(self, selector_type: str, config_key: str, title: str) -> None: + super().__init__() + assert isinstance(title, str) and title + self.title = title + assert isinstance(selector_type, str) and selector_type + self.selector_type = selector_type + assert isinstance(config_key, str) and config_key + self.config_key = config_key + + def _set_next_screen(self, next_screen: str) -> None: + assert isinstance(next_screen, str) + self.next_screen = next_screen + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Markdown(f'# {self.title}') + yield VerticalScroll( + ProcessConfig(selector=self.selector_type), + ProcessConfig(selector=self.selector_type), + ProcessConfig(selector=self.selector_type), + id="configs" + ) + yield Center( + Button("Add another process", id="another", variant="success") + ) + yield Center( + Button("Back", id="back", variant="default"), + Button("Skip", id="skip", variant="default"), + Button("Next", id="next", variant="success"), + classes="cta", + ) + + @on(Button.Pressed, "#another") + def add_config(self) -> None: + new_config = ProcessConfig(selector='name') + self.query_one("#configs").mount(new_config) + + @on(Button.Pressed, "#next") + def save_and_load_next_screen(self) -> None: + try: + config_list = [] + for config_widget in self.query("ProcessConfig"): + tmp_config = {} + for text_input in config_widget.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + tmp_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + # Validate the config + with init_context(self.parent.get_context()): + ConfigsCreateConfig(**tmp_config) + # Add to the config list + config_list.append(tmp_config) + # Add to the final config + key = self.config_key + new_config = {self.config_key: {}} + for tmp_config in config_list: + process_id = tmp_config.get('custom_process_id') + new_config[self.config_key][process_id] = tmp_config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + # Push the next screen + self.parent.push_screen(self.next_screen) + except ValueError: + pass + + @on(Button.Pressed, "#skip") + def skip_to_next_screen(self) -> None: + self.parent.push_screen(self.next_screen) + + +class MultiNamedProcessConfig(MultiProcessConfig): + def __init__(self) -> None: + super().__init__( + title='Configure processes by name', + selector_type='name', + config_key='named_process_resources' + ) + + @on(Mount) + @on(ScreenResume) + def set_next_screen(self) -> None: + next_screen = "final" + if self.parent.PIPE_CONF_LABELLED: + next_screen = "multi_labelled_process_config" + self._set_next_screen(next_screen) + + +class MultiLabelledProcessConfig(MultiProcessConfig): + def __init__(self) -> None: + super().__init__( + title='Configure processes by label', + selector_type='label', + config_key='labelled_process_resources' + ) + + @on(Mount) + @on(ScreenResume) + def set_next_screen(self) -> None: + self._set_next_screen('final') \ No newline at end of file diff --git a/nf_core/configs/create/namedprocessres.py b/nf_core/configs/create/namedprocessres.py deleted file mode 100644 index 3cf335afe2..0000000000 --- a/nf_core/configs/create/namedprocessres.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Get information about which process/label the user wants to configure.""" - -from textwrap import dedent - -from textual import on -from textual.app import ComposeResult -from textual.containers import Center, Horizontal -from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label -from enum import Enum -from nf_core.utils import add_hide_class, remove_hide_class - -from nf_core.configs.create.utils import ( - ConfigsCreateConfig, - TextInput, - init_context -) ## TODO Move somewhere common? -from nf_core.utils import add_hide_class, remove_hide_class - - -class NamedProcess(Screen): - """Get named process resource requirements.""" - - def __init__(self) -> None: - super().__init__() - self.config_stack = [] - self.current_config = {} - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - yield Markdown( - dedent( - """ - # Custom process resources by name - """ - ) - ) - yield TextInput( - "custom_process_name", - "", - "The name of the process you wish to configure.", - "", - classes="column", - ) - yield TextInput( - "custom_process_ncpus", - "2", - "Number of CPUs to use for the process.", - "2", - classes="column", - ) - yield TextInput( - "custom_process_memgb", - "8", - "Amount of memory in GB to use for the process.", - "8", - classes="column", - ) - yield TextInput( - "custom_process_hours", - "1", - "The number of hours of walltime required for the process:", - "1", - classes="column", - ) - yield Center( - Button("Back", id="back", variant="default"), - Button("Configure another process", id="another"), - Button("Next", id="next", variant="success"), - classes="cta", - ) - - # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs - @on(Button.Pressed) - def on_button_pressed(self, event: Button.Pressed) -> None: - """Save fields to the config.""" - if event.button.id in ["next", "another"]: - tmp_config = {} - for text_input in self.query("TextInput"): - this_input = text_input.query_one(Input) - validation_result = this_input.validate(this_input.value) - tmp_config[text_input.field_id] = this_input.value - if not validation_result.is_valid: - text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) - else: - text_input.query_one(".validation_msg").update("") - # Validate the config - try: - with init_context(self.parent.get_context()): - ConfigsCreateConfig(**tmp_config) - # Add to the config stack - self.config_stack.append(tmp_config) - if event.button.id == "another": - # If configuring another process, push a blank config to the config stack - # and push a new copy of this screen to the screen stack - self.config_stack.append({}) - self.parent.push_screen("named_process_resources") - else: - # If finalising the custom resources, add them all to the config now - key = "named_process_resources" - new_config = {key: {}} - for tmp_config in self.config_stack: - process_name = tmp_config.get('custom_process_name') - new_config[key][process_name] = tmp_config - self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) - # Push the next screen - if self.parent.PIPE_CONF_LABELLED: - self.parent.push_screen("labelled_process_resources") - else: - self.parent.push_screen("final") - except ValueError: - pass - - def on_screen_resume(self): - # Grab the last config in the stack if it exists - try: - self.current_config = self.config_stack.pop() - except IndexError: - self.current_config = {} - # Reset all input field values - for text_input in self.query("TextInput"): - this_input = text_input.query_one(Input) - this_input.clear() - field_id = text_input.field_id - if field_id in self.current_config: - this_input.insert(self.current_config[field_id], 0) - else: - text_input.refresh(repaint=True, layout=True, recompose=True) diff --git a/nf_core/configs/create/pipelineconfigquestion.py b/nf_core/configs/create/pipelineconfigquestion.py index a188e0b443..68e1dab0a2 100644 --- a/nf_core/configs/create/pipelineconfigquestion.py +++ b/nf_core/configs/create/pipelineconfigquestion.py @@ -114,8 +114,8 @@ def on_next_button_pressed(self, event: Button.Pressed) -> None: if self.config_defaults: self.parent.push_screen("default_process_resources") elif self.config_named_processes: - self.parent.push_screen("named_process_resources") + self.parent.push_screen("multi_named_process_config") elif self.config_labels: - self.parent.push_screen("labelled_process_resources") + self.parent.push_screen("multi_labelled_process_config") else: self.parent.push_screen("final") diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index f3ccb6efdf..4494944cd2 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -59,11 +59,7 @@ class ConfigsCreateConfig(BaseModel): """ Default amount of memory """ default_process_hours: Optional[str] = None """ Default walltime - hours """ - default_process_minutes: Optional[str] = None - """ Default walltime - minutes """ - default_process_seconds: Optional[str] = None - """ Default walltime - seconds """ - custom_process_name: Optional[str] = None + custom_process_id: Optional[str] = None """" Name or label of a process to configure """ custom_process_ncpus: Optional[str] = None """ Number of CPUs for process """ @@ -219,7 +215,7 @@ def url_prefix(cls, v: str, info: ValidationInfo) -> str: ) return v - @field_validator("custom_process_name") + @field_validator("custom_process_id") @classmethod def notempty_process_name(cls, v: str, info: ValidationInfo) -> str: """Check that the custom process name or label isn't empty.""" From e6c6690b2a36328bdc1673a2990f1c9ebd86d8fc Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 13 Feb 2026 17:05:53 +1100 Subject: [PATCH 038/121] add hpc question to pipeline config branch --- nf_core/configs/create/__init__.py | 1 + .../configs/create/pipelineconfigquestion.py | 43 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 371364e120..8ec18da880 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -78,6 +78,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): INFRA_ISHPC = False PIPE_CONF_NAMED = False PIPE_CONF_LABELLED = False + PIPE_CONF_HPC = False # Log handler LOG_HANDLER = rich_log_handler diff --git a/nf_core/configs/create/pipelineconfigquestion.py b/nf_core/configs/create/pipelineconfigquestion.py index 68e1dab0a2..0e87c6957d 100644 --- a/nf_core/configs/create/pipelineconfigquestion.py +++ b/nf_core/configs/create/pipelineconfigquestion.py @@ -26,6 +26,7 @@ def __init__(self) -> None: self.config_defaults = False self.config_named_processes = False self.config_labels = False + self.config_hpc = False def compose(self) -> ComposeResult: yield Header() @@ -57,10 +58,10 @@ def compose(self) -> ComposeResult: ) yield Switch( id="toggle_configure_names", - value=self.config_defaults, + value=self.config_named_processes, ) yield Label( - "Yes" if self.config_defaults else "No", + "Yes" if self.config_named_processes else "No", id="toggle_configure_names_state_label" ) with Horizontal(): @@ -70,12 +71,44 @@ def compose(self) -> ComposeResult: ) yield Switch( id="toggle_configure_labels", - value=self.config_defaults, + value=self.config_labels, ) yield Label( - "Yes" if self.config_defaults else "No", + "Yes" if self.config_labels else "No", id="toggle_configure_labels_state_label" ) + yield Markdown( + dedent( + """ + # Will you be using this configuration exclusively on an HPC? + + ## Choose _"Yes"_ if: + + You are configuring processes specifically for running on an HPC. + + ## Choose _"No"_ if: + + This config file will be used across different platforms. + + ## What's the difference? + + Choosing _"Yes"_ will allow you to configure additional HPC-specific directives for each process, including `queue` and `executor`. + """ + ) + ) + with Horizontal(): + yield Label( + "Configure HPC-specific resources?", + id="toggle_configure_hpc_resources_label" + ) + yield Switch( + id="toggle_configure_hpc_resources", + value=self.config_hpc, + ) + yield Label( + "Yes" if self.config_hpc else "No", + id="toggle_configure_hpc_resources_state_label" + ) yield Center( Button("Back", id="back", variant="default"), Button("Next", id="next", variant="success"), @@ -89,6 +122,7 @@ def on_toggle_switch(self, event: Switch.Changed) -> None: 'toggle_configure_defaults': 'config_defaults', 'toggle_configure_names': 'config_named_processes', 'toggle_configure_labels': 'config_labels', + 'toggle_configure_hpc_resources': 'config_hpc', } if event.switch.id not in valid_toggles: @@ -109,6 +143,7 @@ def on_next_button_pressed(self, event: Button.Pressed) -> None: # Update app tracking variables for whether to configure named and/or labelled processes self.parent.PIPE_CONF_NAMED = self.config_named_processes self.parent.PIPE_CONF_LABELLED = self.config_labels + self.parent.PIPE_CONF_HPC = self.config_hpc # Proceed to next screen depending on what choices the user has made if self.config_defaults: From 569d2d109101f1c4514b958bbb6bf4505cfe29c5 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 18 Feb 2026 17:05:14 +1100 Subject: [PATCH 039/121] add custom process queue field and executor toggle to pipeline config --- nf_core/configs/create/multiprocessres.py | 49 ++++++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index 0e38c81b59..a473cb5c88 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -4,7 +4,7 @@ from textual import on from textual.app import ComposeResult -from textual.containers import Center, HorizontalGroup, VerticalScroll +from textual.containers import Center, HorizontalGroup, VerticalScroll, Vertical, Horizontal from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label from textual.events import Mount, ScreenResume @@ -21,10 +21,12 @@ class ProcessConfig(HorizontalGroup): """Get resource requirements for a single process.""" - def __init__(self, selector: str) -> None: + def __init__(self, selector: str, hpc: bool) -> None: super().__init__() assert selector in ['name', 'label'] self.selector = selector + self.hpc = hpc + self.use_local_exec = False def compose(self) -> ComposeResult: yield TextInput( @@ -55,6 +57,27 @@ def compose(self) -> ComposeResult: "1", classes="column", ) + yield TextInput( + "custom_process_queue", + "queue name", + "HPC queue:", + "", + classes=("column" + (" hide" if not self.hpc else "")), + ) + with Vertical(classes=("column" + (" hide" if not self.hpc else "")), id="toggle_use_local_exec_group"): + yield Label( + "Use local executor?", + id="toggle_use_local_exec_label" + ) + with Horizontal(): + yield Switch( + id="toggle_use_local_exec", + value=self.use_local_exec + ) + yield Label( + "Yes" if self.use_local_exec else "No", + id="toggle_use_local_exec_state_label" + ) yield Button( "-", id="remove", @@ -65,6 +88,14 @@ def compose(self) -> ComposeResult: def remove_widget(self) -> None: self.remove() + def update_hpc_status(self, hpc: bool) -> None: + self.hpc = hpc + for id in ["custom_process_queue", "toggle_use_local_exec_group"]: + if self.hpc: + self.get_widget_by_id(id).remove_class("hide") + else: + self.get_widget_by_id(id).add_class("hide") + class MultiProcessConfig(Screen): """Get resource requirements for multiple processes.""" @@ -87,9 +118,9 @@ def compose(self) -> ComposeResult: yield Footer() yield Markdown(f'# {self.title}') yield VerticalScroll( - ProcessConfig(selector=self.selector_type), - ProcessConfig(selector=self.selector_type), - ProcessConfig(selector=self.selector_type), + ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), + ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), + ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), id="configs" ) yield Center( @@ -104,7 +135,7 @@ def compose(self) -> ComposeResult: @on(Button.Pressed, "#another") def add_config(self) -> None: - new_config = ProcessConfig(selector='name') + new_config = ProcessConfig(selector='name', hpc=self.parent.PIPE_CONF_HPC) self.query_one("#configs").mount(new_config) @on(Button.Pressed, "#next") @@ -142,6 +173,12 @@ def save_and_load_next_screen(self) -> None: def skip_to_next_screen(self) -> None: self.parent.push_screen(self.next_screen) + @on(Mount) + @on(ScreenResume) + def update_hide_class(self) -> None: + for config_widget in self.query("ProcessConfig"): + config_widget.update_hpc_status(self.parent.PIPE_CONF_HPC) + class MultiNamedProcessConfig(MultiProcessConfig): def __init__(self) -> None: From d1a0d3ca5117fe59ac21f82df4253de9f69b9e79 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 19 Feb 2026 10:57:12 +1100 Subject: [PATCH 040/121] finish adding hpc specific resources to pipeline conf --- nf_core/configs/create/create.py | 16 +++++++- nf_core/configs/create/multiprocessres.py | 48 ++++++++++++++++++----- nf_core/textual.tcss | 18 ++++++++- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 6c37e42c10..fc5cabb3a6 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -61,7 +61,7 @@ def construct_params_str(self): params_section_str = '\n'.join(params_section) + '\n\n' return sub(r'\n\n\n+', '\n\n', params_section_str) - def get_resource_strings(self, cpus, memory, hours, prefix=''): + def get_resource_strings(self, cpus, memory, hours, queue='', executor='', prefix=''): cpus_str = '' if cpus: cpus_int = int(cpus) @@ -82,7 +82,15 @@ def get_resource_strings(self, cpus, memory, hours, prefix=''): time_m = int(time_h * 60) time_str = f"time = {time_m}.m" - resources = [cpus_str, memory_str, time_str] + queue_str = '' + if queue: + queue_str = f"queue = '{queue}'" + + executor_str = '' + if executor: + executor_str = f"executor = '{executor}'" + + resources = [cpus_str, memory_str, time_str, queue_str, executor_str] return [ f'{prefix}{res}' for res in resources @@ -106,6 +114,8 @@ def construct_process_config_str(self): cpus=process_resources['custom_process_ncpus'], memory=process_resources['custom_process_memgb'], hours=process_resources['custom_process_hours'], + queue=process_resources.get('custom_process_queue', ''), + executor=process_resources.get('executor', ''), prefix=' ' ) if not named_resource_string: @@ -125,6 +135,8 @@ def construct_process_config_str(self): cpus=process_resources['custom_process_ncpus'], memory=process_resources['custom_process_memgb'], hours=process_resources['custom_process_hours'], + queue=process_resources.get('custom_process_queue', ''), + executor=process_resources.get('executor', ''), prefix=' ' ) if not labelled_resource_string: diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index a473cb5c88..4444c3b20a 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -6,7 +6,7 @@ from textual.app import ComposeResult from textual.containers import Center, HorizontalGroup, VerticalScroll, Vertical, Horizontal from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label +from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label, Static from textual.events import Mount, ScreenResume from nf_core.utils import add_hide_class, remove_hide_class @@ -64,25 +64,47 @@ def compose(self) -> ComposeResult: "", classes=("column" + (" hide" if not self.hpc else "")), ) - with Vertical(classes=("column" + (" hide" if not self.hpc else "")), id="toggle_use_local_exec_group"): + with Vertical(classes=("labelled-toggle column" + (" hide" if not self.hpc else "")), id="toggle_use_local_exec_group"): yield Label( "Use local executor?", - id="toggle_use_local_exec_label" + id="toggle_use_local_exec_label", + classes="field_help" ) - with Horizontal(): + with Horizontal(classes="labelled-toggle"): yield Switch( id="toggle_use_local_exec", value=self.use_local_exec ) yield Label( "Yes" if self.use_local_exec else "No", - id="toggle_use_local_exec_state_label" + id="toggle_use_local_exec_state_label", + classes="toggle_use_local_exec_state_label" ) - yield Button( - "-", - id="remove", - variant="error" - ) + yield Static(classes="labelled-toggle-filler") # Filler + with Vertical(classes="labelled-toggle"): + yield Label( + "Remove", + id="remove-process-config", + classes="field_help" + ) + yield Button( + "-", + id="remove", + variant="error", + classes="remove-process-button" + ) + yield Static(classes="labelled-toggle-filler") # Filler + + @on(Switch.Changed, "#toggle_use_local_exec") + def on_toggle_switch(self, event: Switch.Changed) -> None: + """ Handle toggling the local executor switch """ + + self.use_local_exec = event.value + + # Update the switch label + for label in self.query(Label): + if label.id == f'{event.switch.id}_state_label': + label.update("Yes" if event.value else "No") @on(Button.Pressed, "#remove") def remove_widget(self) -> None: @@ -145,6 +167,8 @@ def save_and_load_next_screen(self) -> None: for config_widget in self.query("ProcessConfig"): tmp_config = {} for text_input in config_widget.query("TextInput"): + if "hide" in text_input.classes: + continue this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) tmp_config[text_input.field_id] = this_input.value @@ -152,6 +176,10 @@ def save_and_load_next_screen(self) -> None: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: text_input.query_one(".validation_msg").update("") + if self.parent.PIPE_CONF_HPC: + local_exec_switch = config_widget.query_one("#toggle_use_local_exec") + if local_exec_switch.value: + tmp_config['executor'] = 'local' # Validate the config with init_context(self.parent.get_context()): ConfigsCreateConfig(**tmp_config) diff --git a/nf_core/textual.tcss b/nf_core/textual.tcss index 2bf284d360..d7230c56de 100644 --- a/nf_core/textual.tcss +++ b/nf_core/textual.tcss @@ -29,7 +29,23 @@ .custom_grid Button { width: auto; } - +.labelled-toggle { + padding: 1 1 1 1; + content-align-vertical: middle; + height: 100%; +} +.toggle_use_local_exec_state_label { + padding: 1 1 1 1; + align-vertical: middle; +} +.labelled-toggle-filler { + padding: 1 1 1 1; + content-align-vertical: middle; +} +.remove-process-button { + margin: 1 1 1 1; + align-vertical: middle; +} .text-input-grid { padding: 1 1 1 1; grid-size: 1 3; From e00acf93cc52074ccee2497ac5f44c3b4c877979 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 19 Feb 2026 15:49:46 +1100 Subject: [PATCH 041/121] WIP: working on exporting infra config to file --- nf_core/configs/create/__init__.py | 4 -- nf_core/configs/create/basicdetails.py | 2 + nf_core/configs/create/create.py | 64 +++++++++++++++++- nf_core/configs/create/defaultprocessres.py | 16 ++++- nf_core/configs/create/finalinfradetails.py | 74 ++++++++++++++++----- nf_core/configs/create/hpccustomisation.py | 34 ++++++++-- nf_core/configs/create/utils.py | 42 ++++++++++++ 7 files changed, 210 insertions(+), 26 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 8ec18da880..8c18cd0e3d 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -117,10 +117,6 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.INFRA_ISHPC = False utils.INFRA_ISHPC_GLOBAL = False self.push_screen("final_infra_details") - elif event.button.id == "toconfiguration": - self.push_screen("final_infra_details") - elif event.button.id == "finish": - self.push_screen("final") ## General options if event.button.id == "back": self.pop_screen() diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 7214e397b1..3600444f9b 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -94,6 +94,8 @@ def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" config = {} for text_input in self.query("TextInput"): + if "hide" in text_input.classes: + continue this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) config[text_input.field_id] = this_input.value diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index fc5cabb3a6..5bb2a56502 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -163,6 +163,66 @@ def construct_process_config_str(self): process_section_str = '\n'.join(process_section) + '\n' return sub(r'\n\n\n+', '\n\n', process_section_str) + + def construct_infra_config_str(self): + modules_to_load = ( + self.template_config.container_system + if self.template_config.module and self.template_config.container_system + else '' + ) + if self.template_config.module_system: + if modules_to_load: + modules_to_load += ' ' + modules_to_load += sub(r'\s+', ':', self.template_config.module_system) + memory_str = '' + if self.template_config.memory: + memory_int = int(self.template_config.memory) + memory_str = f'memory = {memory_int}.GB' + time_str = '' + if self.template_config.time: + time_h = float(self.template_config.time) + if time_h.is_integer(): + time_h = int(time_h) + time_str = f'{time_h}.h' + else: + time_m = int(time_h * 60) + time_str = f'{time_m}.m' + resource_limits = { + 'cpus': int(self.template_config.cpus) if self.template_config.cpus else None, + 'memory': memory_str or None, + 'time': time_str or None, + } + resource_limits = {k: v for k, v in resource_limits.items() if v} + resource_limits_str = [ + f'{key}: {value}' + for key, value in resource_limits.items() + ] + resource_limits_str = ', '.join(resource_limits_str) + resource_limits_str = f'[ {resource_limits_str} ]' + process = { + 'executor': self.template_config.scheduler or None, + 'queue': self.template_config.queue or None, + 'module': modules_to_load or None, + 'resourceLimits': resource_limits_str or None, + } + process = {k: v for k, v in process.items() if v} + + process_list = [ + f'{key} = {value}' + for key, value in process.items() + ] + + process_section = [ + 'process {', + '\n', + *process_list, + '\n', + '}', + ] + + process_section_str = '\n'.join(process_section) + '\n' + + return sub(r'\n\n\n+', '\n\n', process_section_str) def write_to_file(self): ## File name option @@ -177,8 +237,10 @@ def write_to_file(self): if self.config_type == 'pipeline': process_section_str = self.construct_process_config_str() + elif self.config_type == 'infrastructure': + process_section_str = self.construct_infra_config_str() else: - process_section_str = '' + raise ValueError(f'Invalid config type: {self.config_type}') with open(filename, "w+") as file: ## Write params diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index bf149154af..ffdaf3727c 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -69,7 +69,7 @@ def skip_to_next_screen(self) -> None: # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs @on(Button.Pressed, "#next") - def on_button_pressed(self, event: Button.Pressed) -> None: + def on_next_button(self, event: Button.Pressed) -> None: """Save fields to the config.""" new_config = {} for text_input in self.query("TextInput"): @@ -95,3 +95,17 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.parent.push_screen("final") except ValueError: pass + + @on(Button.Pressed, "#back") + def on_back_button(self, event: Button.Pressed) -> None: + """Clear the default config info""" + blank_config = {} + for text_input in self.query("TextInput"): + if getattr(self.parent.TEMPLATE_CONFIG, text_input.field_id, None): + blank_config[text_input.field_id] = '' + try: + with init_context(self.parent.get_context()): + # Update the existing config with the blank values + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=blank_config) + except ValueError: + pass diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 41e0d6015b..d9da4d9757 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -10,6 +10,8 @@ from nf_core.configs.create.utils import ( TextInput, + ConfigsCreateConfig, + init_context ) from nf_core.utils import add_hide_class, remove_hide_class @@ -42,7 +44,7 @@ def compose(self) -> ComposeResult: yield TextInput( "memory", "Memory", - "Maximum memory available in your machine.", + "Maximum memory (GB) available in your machine.", classes="column", ) yield TextInput( @@ -54,11 +56,11 @@ def compose(self) -> ComposeResult: yield TextInput( "time", "Time", - "Maximum time to run your jobs.", + "Maximum time (hours) to run your jobs.", classes="column", ) - yield Markdown("## Do you want to define a global cache directory for containers or conda environments?") - with Horizontal(): + with Vertical(id="define-global-cache-dir"): + yield Markdown("## Do you want to define a global cache directory for containers or conda environments?") yield TextInput( "envvar", "Nextflow cachedir environment variable", @@ -71,9 +73,7 @@ def compose(self) -> ComposeResult: f"NXF_{self.container_system.upper()}_CACHEDIR" if self.container_system is not None else "", "Define a global cache direcotry.", classes="", - default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") - if self.container_system is not None - else "", + default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") if self.container_system is not None else "", ) yield TextInput( "igenomes_cachedir", @@ -88,7 +88,7 @@ def compose(self) -> ComposeResult: classes="", ) with Horizontal(classes="ghrepo-cols"): - yield Switch(value=False, id="private") + yield Switch(value=False, id="toggle-delete-work") with Vertical(): yield Static("Delete work directory", classes="") yield Markdown( @@ -155,12 +155,54 @@ def get_container_system(self) -> None: """Get the container system from the input.""" self.container_system = None for text_input in self.query("TextInput"): + if text_input.field_id != "container_system": + continue this_input = text_input.query_one(Input) - if text_input.field_id == "container_system": - self.container_system = this_input.value - if self.container_system is not None: - add_hide_class(self.parent, "cachedir") - add_hide_class(self.parent, "envvar") - else: - remove_hide_class(self.parent, "cachedir") - remove_hide_class(self.parent, "envvar") + self.container_system = this_input.value + if self.container_system: + remove_hide_class(self.parent, "define-global-cache-dir") + else: + add_hide_class(self.parent, "define-global-cache-dir") + break + + @on(Button.Pressed, "#finish") + def on_finish_button(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + new_config = {} + for text_input in self.query("TextInput"): + if "hide" in text_input.classes: + continue + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + new_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + delete_work_switch = self.query_one("#toggle-delete-work") + new_config['delete_work_dir'] = delete_work_switch.value + new_config['module'] = self._detect_module_system() + try: + with init_context(self.parent.get_context()): + # First, validate the new config data + ConfigsCreateConfig(**new_config) + # If that passes validation, update the existing config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + # Push the next screen + self.parent.push_screen("final") + except ValueError: + pass + + @on(Button.Pressed, "#back") + def on_back_button(self, event: Button.Pressed) -> None: + """Clear the default config info""" + blank_config = {} + for text_input in self.query("TextInput"): + if getattr(self.parent.TEMPLATE_CONFIG, text_input.field_id, None): + blank_config[text_input.field_id] = '' + try: + with init_context(self.parent.get_context()): + # Update the existing config with the blank values + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=blank_config) + except ValueError: + pass diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 0928f392d9..911c1ffaa2 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -4,10 +4,13 @@ from textual.app import ComposeResult from textual.containers import Center, Horizontal from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Markdown +from textual.widgets import Button, Footer, Header, Markdown, Input +from textual import on from nf_core.configs.create.utils import ( TextInput, + init_context, + ConfigsCreateConfig ) markdown_intro = """ @@ -30,20 +33,20 @@ def compose(self) -> ComposeResult: "scheduler", "Scheduler", "The scheduler in your HPC.", - default=scheduler if scheduler is not None else "Scheduler", + default=scheduler if scheduler is not None else "local", classes="column", ) yield TextInput( "queue", "Queue", - "The queue in your HPC.", + "The default queue in your HPC.", classes="column", suggestions=queues, ) yield TextInput( "module_system", "Other modules to load", - "Do you need to load other software using the module system for your compute nodes?", + "Do you need to load other software using the module system for your compute nodes? Separate multiple modules by spaces.", classes="hide" if not module_system_used else "", ) yield Center( @@ -108,3 +111,26 @@ def _detect_module_system(self) -> bool: except subprocess.CalledProcessError: return False return True + + @on(Button.Pressed, "#toconfiguration") + def on_button_pressed(self, event: Button.Pressed) -> None: + """Save fields to the config.""" + new_config = {} + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + validation_result = this_input.validate(this_input.value) + new_config[text_input.field_id] = this_input.value + if not validation_result.is_valid: + text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + else: + text_input.query_one(".validation_msg").update("") + try: + with init_context(self.parent.get_context()): + # First, validate the new config data + ConfigsCreateConfig(**new_config) + # If that passes validation, update the existing config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + # Push the next screen + self.parent.push_screen("final_infra_details") + except ValueError: + pass diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 4494944cd2..3c9c300cd7 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -257,6 +257,48 @@ def non_neg_float_valid(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a non-negative number.") return v + @field_validator("cpus", "memory") + @classmethod + def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: + """Check that integer values are either empty or positive.""" + context = info.context + if context and context["is_infrastructure"]: + if v.strip() == "": + return v + try: + v_int = int(v.strip()) + except ValueError: + raise ValueError("Must be an integer.") + if not v_int > 0: + raise ValueError("Must be a positive integer.") + return v + + @field_validator("time") + @classmethod + def non_neg_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: + """Check that numeric values are either empty or non-negative.""" + context = info.context + if context and context["is_infrastructure"]: + if v.strip() == "": + return v + try: + vf = float(v.strip()) + except ValueError: + raise ValueError("Must be a number.") + if not vf >= 0: + raise ValueError("Must be a non-negative number.") + return v + + @field_validator("scheduler", "queue") + @classmethod + def nonemtpy_hpc_details(cls, v: str, info: ValidationInfo) -> str: + """Check that HPC infrastructure details are non-empty""" + context = info.context + if context and context["is_infrastructure"] and context["is_hpc"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) class TextInput(Static): """Widget for text inputs. From 36d7aeda24abdb6ac170c7f5ed9e8e4ef4cec225 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 11:35:42 +1100 Subject: [PATCH 042/121] BUG: GitHub username accepts caps, improve ^@ msg --- nf_core/configs/create/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 3c9c300cd7..812a39ab8a 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -179,9 +179,9 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: if context and context["is_infrastructure"]: if v.strip() == "": raise ValueError("Cannot be left empty.") - elif not re.match( - r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v - ): ## Regex from: https://github.com/shinnn/github-username-regex + elif v and not re.match( + r"^@[a-zA-Z\d](?:[a-zA-Z\d]|-(?=[a-zA-Z\d])){0,38}$", v + ): ## Regex adapted from: https://github.com/shinnn/github-username-regex raise ValueError("Handle must start with '@'.") else: if not v.strip() == "" and not re.match(r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v): From 8df673a2775322b7ddea29735b43265fdc5fd782 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 11:51:34 +1100 Subject: [PATCH 043/121] MAINT: remove duplicate field_validator --- nf_core/configs/create/utils.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 812a39ab8a..6b6a338c0c 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -110,7 +110,7 @@ def __init__(self, /, **data: Any) -> None: context=_init_context_var.get(), ) - @field_validator("general_config_name") + @field_validator("general_config_name", "config_profile_description") @classmethod def notempty(cls, v: str) -> str: """Check that string values are not empty.""" @@ -150,14 +150,6 @@ def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Cannot be left empty.") return v - @field_validator("config_profile_description") - @classmethod - def notempty_description(cls, v: str) -> str: - """Check that description is not empty when.""" - if v.strip() == "": - raise ValueError("Cannot be left empty.") - return v - @field_validator("config_profile_contact") @classmethod def notempty_contact(cls, v: str, info: ValidationInfo) -> str: From aac51fe60bcb9cc1da312e2b174b98bea06f5105 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 12:03:30 +1100 Subject: [PATCH 044/121] MAINT: Tidy git handle validation --- nf_core/configs/create/utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 6b6a338c0c..3f3107ee3e 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -171,13 +171,9 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: if context and context["is_infrastructure"]: if v.strip() == "": raise ValueError("Cannot be left empty.") - elif v and not re.match( - r"^@[a-zA-Z\d](?:[a-zA-Z\d]|-(?=[a-zA-Z\d])){0,38}$", v - ): ## Regex adapted from: https://github.com/shinnn/github-username-regex - raise ValueError("Handle must start with '@'.") - else: - if not v.strip() == "" and not re.match(r"^@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", v): - raise ValueError("Handle must start with '@'.") + if not v.strip() == "" and not re.match(r"^@[aA-zZ\d](?:[aA-zZ\d]|-(?=[aA-zZ\d])){0,38}$", v): + ## Regex adapted from: https://github.com/shinnn/github-username-regex + raise ValueError("Handle must start with '@'.") return v @field_validator( From 8fb1d2768d23a510d3f5387a7158686d8269c2d3 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 12:53:40 +1100 Subject: [PATCH 045/121] MAINT: infra retries validated --- nf_core/configs/create/utils.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 3f3107ee3e..37b4c31fc3 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -216,7 +216,12 @@ def notempty_process_name(cls, v: str, info: ValidationInfo) -> str: @field_validator("default_process_ncpus", "default_process_memgb", "custom_process_ncpus", "custom_process_memgb") @classmethod def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: - """Check that integer values are either empty or positive.""" + """Check that integer values are either empty or positive. + + This contains the same validation as self.pos_integer_valid_infra(). + However, keep infrastructure and pipeline methods decoupled for + easier refactoring in future. + """ context = info.context if context and not context["is_infrastructure"]: if v.strip() == "": @@ -245,10 +250,16 @@ def non_neg_float_valid(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a non-negative number.") return v - @field_validator("cpus", "memory") + @field_validator("cpus", "memory", "retries") @classmethod def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: - """Check that integer values are either empty or positive.""" + """ + Check that integer values are either empty or positive. + + This contains the same validation as self.pos_integer_valid(). + However, keep infrastructure and pipeline methods decoupled for + easier refactoring in future. + """ context = info.context if context and context["is_infrastructure"]: if v.strip() == "": From e38d5127b76493536de33e679b48ce8d32507570 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 13:01:39 +1100 Subject: [PATCH 046/121] MAINT: ui typo --- nf_core/configs/create/finalinfradetails.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index d9da4d9757..d3f6cb07ce 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -71,20 +71,20 @@ def compose(self) -> ComposeResult: yield TextInput( "cachedir", f"NXF_{self.container_system.upper()}_CACHEDIR" if self.container_system is not None else "", - "Define a global cache direcotry.", + "Define a global cache directory.", classes="", default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") if self.container_system is not None else "", ) yield TextInput( "igenomes_cachedir", "iGenomes cache directory", - "If you have an iGenomes cache direcotry, specify it.", + "If you have an iGenomes cache directory, specify it.", classes="hide" if not self.parent.NFCORE_CONFIG else "", ) yield TextInput( "scratch_dir", "Scratch directory", - "If you have to use a specific scratch direcotry, specify it.", + "If you have to use a specific scratch directory, specify it.", classes="", ) with Horizontal(classes="ghrepo-cols"): From 277a8efe115bd16e28476bad0e24ae75a3dd96b2 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 14:12:38 +1100 Subject: [PATCH 047/121] MAINT: validate paths and uris --- nf_core/configs/create/utils.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 37b4c31fc3..71b1359c67 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -32,6 +32,7 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: CONFIG_ISINFRASTRUCTURE_GLOBAL: bool = True NFCORE_CONFIG_GLOBAL: bool = True INFRA_ISHPC_GLOBAL: bool = False +_PATH_PATTERN = re.compile(r"(\/|~\/|~$|\$\{?\w+\}?)(.*)") class ConfigsCreateConfig(BaseModel): @@ -297,6 +298,47 @@ def nonemtpy_hpc_details(cls, v: str, info: ValidationInfo) -> str: if v.strip() == "": raise ValueError("Cannot be left empty.") return v + + @field_validator("cachedir", "scratch_dir") + @classmethod + def is_path_ondisk(cls, v: str, info: ValidationInfo) -> str: + """ + Check that a path looks valid. Does not check if it exists. + + Skip if field is empty. + + Accept: + - absolute paths (^/.+) + - env var prefixed paths (${INFRA_SPECIFIC_VAR}/..., ${HOME}/..., ${projectDir}) + - tilde-prefixed paths (~/...) + """ + v = v.strip() + if v == "": + raise ValueError("Cannot be left empty.") + + if not _PATH_PATTERN.match(v): + raise ValueError( + "Must be an absolute path (/data/scratch), " + "a path relative to home (~/scratch), " + "or a path with an environmental variable (e.g. ${DIR}/scratch)" + ) + return v + + @field_validator("igenomes_cachedir") + @classmethod + def is_path_or_uri(cls, v: str, info: ValidationInfo) -> str: + v = v.strip() + if v == "": + raise ValueError("Cannot be left empty.") + + uri_pattern = re.compile(r"^\w+:\/\/\w+") + if not _PATH_PATTERN.match(v) and not uri_pattern.match(v): + raise ValueError( + "Must be an absolute path with optional environmental variables " + "(e.g. /data/cache, ~/cache, ${DIR}/cache), " + "or a URI (e.g. s3://ngi-igenomes/igenomes/)" + ) + return v ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) class TextInput(Static): From 17d4bac8aa67a507601642df5f775e7ddc1dfb14 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 11 Mar 2026 15:27:22 +1100 Subject: [PATCH 048/121] add igenomes, scratch, retires to final config file, update autofill of container cache dir --- nf_core/configs/create/create.py | 8 +++- nf_core/configs/create/finalinfradetails.py | 43 ++++++++++----------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 5bb2a56502..c3f45109a2 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -23,6 +23,7 @@ def construct_info_params(self): handle = self.template_config.config_profile_handle description = self.template_config.config_profile_description url = self.template_config.config_profile_url + igenomes = self.template_config.igenomes_cachedir if contact: if handle: @@ -39,6 +40,9 @@ def construct_info_params(self): if url: final_params["config_profile_url"] = url + if igenomes: + final_params["igenomes_base"] = igenomes + return final_params def construct_params_str(self): @@ -204,11 +208,13 @@ def construct_infra_config_str(self): 'queue': self.template_config.queue or None, 'module': modules_to_load or None, 'resourceLimits': resource_limits_str or None, + 'scratch': f"'{self.template_config.scratch_dir}'" if self.template_config.scratch_dir else None, + 'maxRetries': self.template_config.retries or None } process = {k: v for k, v in process.items() if v} process_list = [ - f'{key} = {value}' + f' {key} = {value}' for key, value in process.items() ] diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index d9da4d9757..5a6b766a9e 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -26,18 +26,22 @@ class FinalInfraDetails(Screen): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.container_system = None + self.container_system_list = [] + self.cache_dir = None def compose(self) -> ComposeResult: yield Header() yield Footer() yield Markdown(markdown_intro) - container_systems = self._get_container_systems() + self.container_system_list = self._get_container_systems() + self.container_system = self.container_system_list[0] if self.container_system_list else None + yield TextInput( "container_system", "Container system", "What container or software system will you use to run your pipeline?", classes="", - suggestions=container_systems, + suggestions=self.container_system_list, ) yield Markdown("## Maximum resources") with Horizontal(): @@ -59,18 +63,11 @@ def compose(self) -> ComposeResult: "Maximum time (hours) to run your jobs.", classes="column", ) - with Vertical(id="define-global-cache-dir"): + with Vertical(id="define-global-cache-dir", classes="hide" if not self.container_system else ""): yield Markdown("## Do you want to define a global cache directory for containers or conda environments?") - yield TextInput( - "envvar", - "Nextflow cachedir environment variable", - "Environment variable to define a global cache directory.", - classes="", - default=f"NXF_{self.container_system.upper()}_CACHEDIR" if self.container_system is not None else "", - ) yield TextInput( "cachedir", - f"NXF_{self.container_system.upper()}_CACHEDIR" if self.container_system is not None else "", + "/path/to/cache/dir", "Define a global cache direcotry.", classes="", default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") if self.container_system is not None else "", @@ -84,7 +81,7 @@ def compose(self) -> ComposeResult: yield TextInput( "scratch_dir", "Scratch directory", - "If you have to use a specific scratch direcotry, specify it.", + "If you have to use a specific scratch direcotry, specify it. ", classes="", ) with Horizontal(classes="ghrepo-cols"): @@ -154,16 +151,18 @@ def _get_set_directory(self, dir: str) -> Optional[str]: def get_container_system(self) -> None: """Get the container system from the input.""" self.container_system = None - for text_input in self.query("TextInput"): - if text_input.field_id != "container_system": - continue - this_input = text_input.query_one(Input) - self.container_system = this_input.value - if self.container_system: - remove_hide_class(self.parent, "define-global-cache-dir") - else: - add_hide_class(self.parent, "define-global-cache-dir") - break + text_input = self.query_one("#container_system") + this_input = text_input.query_one(Input) + self.container_system = this_input.value + cachedir_text_input = self.query_one("#cachedir") + cachedir_input = cachedir_text_input.query_one(Input) + if self.container_system: + remove_hide_class(self.parent, "define-global-cache-dir") + if not cachedir_input.value: + cachedir_path = self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") + cachedir_input.value = cachedir_path or '' + else: + add_hide_class(self.parent, "define-global-cache-dir") @on(Button.Pressed, "#finish") def on_finish_button(self, event: Button.Pressed) -> None: From 9b0dad66d318236f59f09dbc070c28a781e2ea52 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 16:34:26 +1100 Subject: [PATCH 049/121] BUG: Fix bad merge zzz --- nf_core/configs/create/finalinfradetails.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 96fda2ec60..986f9a662a 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -69,8 +69,6 @@ def compose(self) -> ComposeResult: "cachedir", "/path/to/cache/dir", "Define a global cache direcotry.", - f"NXF_{self.container_system.upper()}_CACHEDIR" if self.container_system is not None else "", - "Define a global cache directory.", classes="", default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") if self.container_system is not None else "", ) @@ -83,7 +81,6 @@ def compose(self) -> ComposeResult: yield TextInput( "scratch_dir", "Scratch directory", - "If you have to use a specific scratch direcotry, specify it. ", "If you have to use a specific scratch directory, specify it.", classes="", ) From 0578de6029a099170b9b1c9af22a2ad9c5643265 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 17:28:33 +1100 Subject: [PATCH 050/121] MAINT: Move containers to imported variable --- nf_core/configs/create/finalinfradetails.py | 5 +++-- nf_core/configs/create/utils.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 986f9a662a..157935e2c2 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -11,7 +11,8 @@ from nf_core.configs.create.utils import ( TextInput, ConfigsCreateConfig, - init_context + init_context, + SUPPORTED_CONTAINERS ) from nf_core.utils import add_hide_class, remove_hide_class @@ -107,7 +108,7 @@ def compose(self) -> ComposeResult: def _get_container_systems(self) -> list[str]: """Get the available container systems to use for software handling.""" module_system_used = self._detect_module_system() - container_systems = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter"] + container_systems = SUPPORTED_CONTAINERS available_systems = [] if module_system_used: for system in container_systems: diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 71b1359c67..0481b11bcc 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -33,7 +33,8 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: NFCORE_CONFIG_GLOBAL: bool = True INFRA_ISHPC_GLOBAL: bool = False _PATH_PATTERN = re.compile(r"(\/|~\/|~$|\$\{?\w+\}?)(.*)") - +# Used by finalinfradetails as it already imports create.utils +SUPPORTED_CONTAINERS = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter"] class ConfigsCreateConfig(BaseModel): """Pydantic model for the nf-core configs create config.""" From bba6c10d80e58fc723eb46b95c85433390311406 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 17:37:36 +1100 Subject: [PATCH 051/121] MAINT: Update pydantic model --- nf_core/configs/create/utils.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 0481b11bcc..c2f0c933a3 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -55,19 +55,19 @@ class ConfigsCreateConfig(BaseModel): """ Config description """ config_profile_url: Optional[str] = None """ Config institution URL """ - default_process_ncpus: Optional[str] = None + default_process_ncpus: Optional[int] = None """ Default number of CPUs """ - default_process_memgb: Optional[str] = None + default_process_memgb: Optional[int] = None """ Default amount of memory """ - default_process_hours: Optional[str] = None + default_process_hours: Optional[float] = None """ Default walltime - hours """ custom_process_id: Optional[str] = None """" Name or label of a process to configure """ - custom_process_ncpus: Optional[str] = None + custom_process_ncpus: Optional[int] = None """ Number of CPUs for process """ - custom_process_memgb: Optional[str] = None + custom_process_memgb: Optional[int] = None """ Amount of memory for process """ - custom_process_hours: Optional[str] = None + custom_process_hours: Optional[float] = None """ Walltime for process - hours """ named_process_resources: Optional[dict] = None """ Dictionary containing custom resource requirements for named processes """ @@ -85,11 +85,11 @@ class ConfigsCreateConfig(BaseModel): """ Modules to load when running processes """ container_system: Optional[str] = None """ The container system the HPC uses """ - memory: Optional[str] = None + memory: Optional[int] = None """ The maximum memory available to processes """ - cpus: Optional[str] = None + cpus: Optional[int] = None """ The maximum number of CPUs available to processes """ - time: Optional[str] = None + time: Optional[float] = None """ The maximum walltime available to processes """ envvar: Optional[str] = None """ An environment variable to hold a custom Nextflow container cachedir """ @@ -99,8 +99,12 @@ class ConfigsCreateConfig(BaseModel): """ A cachedir for iGenomes """ scratch_dir: Optional[str] = None """ A scratch directory to use """ - retries: Optional[str] = None + retries: Optional[int] = None """ Number of retries for failed jobs """ + delete_work_dir: Optional[bool] = None + """ Whether to delete the work directory on successful pipeline complete """ + module: Optional[str] = None + """ Detected module system """ model_config = ConfigDict(extra="allow") From f543644dab15d69b3eb7168f4047a4d3f8a1c04f Mon Sep 17 00:00:00 2001 From: fredjaya Date: Wed, 11 Mar 2026 17:38:31 +1100 Subject: [PATCH 052/121] WIP: Add unvalidated model --- nf_core/configs/create/finalinfradetails.py | 1 + nf_core/configs/create/utils.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 157935e2c2..0288ac64ef 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -37,6 +37,7 @@ def compose(self) -> ComposeResult: self.container_system_list = self._get_container_systems() self.container_system = self.container_system_list[0] if self.container_system_list else None + # TODO: convert to dropdown with contents self.container_system_list yield TextInput( "container_system", "Container system", diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index c2f0c933a3..ffbb9698c0 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -344,6 +344,26 @@ def is_path_or_uri(cls, v: str, info: ValidationInfo) -> str: "or a URI (e.g. s3://ngi-igenomes/igenomes/)" ) return v + + @field_validator("module_system", "module") + @classmethod + def module_system_valid(cls, v: str, info: ValidationInfo) -> str: + ... #TODO + + @field_validator("container_system") + @classmethod + def container_system_valid(cls, v: str, info: ValidationInfo) -> str: + v = v.strip() + if v != "" and v not in SUPPORTED_CONTAINERS: + raise ValueError( + f"Must be one of: {', '.join(SUPPORTED_CONTAINERS)}" + ) + return v + + @field_validator("delete_work_dir") + @classmethod + def is_bool(cls, v: str, info: ValidationInfo) -> str: + ... #TODO ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) class TextInput(Static): From a5a322949f676a0344a30c0384963c7f2f3772f3 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 12 Mar 2026 10:20:27 +1100 Subject: [PATCH 053/121] add container scope --- nf_core/configs/create/create.py | 69 +++++++++++++++++++++- nf_core/configs/create/hpccustomisation.py | 2 +- nf_core/configs/create/utils.py | 4 ++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index c3f45109a2..6d23036803 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -167,8 +167,43 @@ def construct_process_config_str(self): process_section_str = '\n'.join(process_section) + '\n' return sub(r'\n\n\n+', '\n\n', process_section_str) - - def construct_infra_config_str(self): + + def construct_executor_config_str(self): + # TODO: Update Pydantic model and HpcCustomisation/FinalInfraDetails + # to get info for following executor config fields + # queueSize + # pollInterval + # queueStatInterval (non-local) + # submitRateLimit + executor_section = [ + 'executor {', + '\n', + # ... + '\n', + '}', + ] + executor_section_str = '\n'.join(executor_section) + '\n' + return sub(r'\n\n\n+', '\n\n', executor_section_str) + + def construct_container_config_str(self, container_name, cache_dir): + enabled_str = ' enabled = true' + cache_dir_str = f" cacheDir = '{cache_dir}'" + automount_str = ' autoMounts = true' if container_name in ['singularity', 'apptainer'] else '' + container_section = [ + f'{container_name}' + ' {', + '\n', + enabled_str, + cache_dir_str, + automount_str, + '\n', + '}', + ] + + container_section_str = '\n'.join(container_section) + '\n' + + return sub(r'\n\n\n+', '\n\n', container_section_str) + + def construct_infra_process_config_str(self): modules_to_load = ( self.template_config.container_system if self.template_config.module and self.template_config.container_system @@ -229,6 +264,36 @@ def construct_infra_config_str(self): process_section_str = '\n'.join(process_section) + '\n' return sub(r'\n\n\n+', '\n\n', process_section_str) + + def construct_infra_config_str(self): + process_section_str = self.construct_infra_process_config_str() + + # TODO: Uncomment when executor option fields are implemented + # executor_section_str = self.construct_executor_config_str() + + container_section_str = self.construct_container_config_str( + self.template_config.container_system, + self.template_config.cachedir + ) + + cleanup = 'true' if self.template_config.delete_work_dir else 'false' + + unscoped_configs = [ + f'cleanup = {cleanup}' + ] + unscoped_configs_str = '\n'.join(unscoped_configs) + '\n' + + final_config = [ + process_section_str, + # executor_section_str, + container_section_str, + unscoped_configs_str, + ] + + final_config_str = '\n\n'.join(final_config) + '\n' + + return sub(r'\n\n\n+', '\n\n', final_config_str) + def write_to_file(self): ## File name option diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 911c1ffaa2..06c17a3b71 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -78,7 +78,7 @@ def _get_scheduler(self) -> Optional[str]: pass except subprocess.CalledProcessError: pass - return None + return 'local' def _get_queues(self, scheduler: Optional[str]) -> list[str]: """Get the available queues to use for the jobs""" diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 3c9c300cd7..84025a7fd4 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -99,6 +99,10 @@ class ConfigsCreateConfig(BaseModel): """ A scratch directory to use """ retries: Optional[str] = None """ Number of retries for failed jobs """ + module: Optional[bool] = False + """ Whether the infrastructure uses a module system """ + delete_work_dir: Optional[bool] = False + """ Whether to clean up the work directory upon successful completion """ model_config = ConfigDict(extra="allow") From a2708f120454713171fd2f1ed492f872623fb367 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 10:59:26 +1100 Subject: [PATCH 054/121] feat(hpc): added methods to obtain default queues in hpc --- nf_core/configs/create/hpccustomisation.py | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 911c1ffaa2..cf20c11f5e 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -1,4 +1,5 @@ import subprocess +import io from typing import Optional from textual.app import ComposeResult @@ -102,6 +103,59 @@ def _get_queues(self, scheduler: Optional[str]) -> list[str]: pass return [] + def _get_default_queue(self, scheduler: Optional[str]) -> str: + """Get the default queue for the scheduler""" + if scheduler == "slurm": + try: + return self._slurm_get_default_queue() + except FileNotFoundError: + pass + elif scheduler == "pbs": + try: + return self._pbs_get_default_queue() + except subprocess.CalledProcessError: + pass + # TODO: Implement SGE here + return "" + + def _slurm_get_default_queue(self) -> str: + """Get the default queue for Slurm""" + config = {} + # TODO: If slurm is built from source, the config file path can be different + with open("/etc/slurm/slurm.conf", "r") as fp: + config = self._parse_slurm_config(fp) + + for conf in config: + if (conf["Default"] if "Default" in conf.keys() else "NO") == "YES": + return conf["PartitionName"] + + # If no default is set, use the first option + return config[0]["PartitionName"] + + def _pbs_get_default_queue(self) -> str: + pbs_raw_config = subprocess.check_output(["qmgr", "-c", "\"list server\""]).decode("utf-8") + config = self._pbs_parse_config(pbs_raw_config) + return config["default_queue"] + + def _parse_slurm_config(self, fp: Optional[io.TextIOWrapper]) -> list[dict]: + """Parse the Slurm configuration file""" + config = [] + for line in fp.readlines(): + if line.startswith("PartitionName"): + tokens = [i.rsplit("=", 1) for i in line.split()] + config.append({i[0]: i[1] for i in tokens}) + return config + + def _pbs_parse_config(self, raw: Optional[str]) -> dict: + """Parse the PBS configuration file""" + config = {} + for line in [r.strip() for r in raw.split("\n")]: + if "=" not in line: + continue + k, v = [token.strip() for token in line.rstrip("=", 1)] + config[k] = v + return config + def _detect_module_system(self) -> bool: """Detect if a module system is used""" try: From f01d1ef0573bd4da3e48691a49b932bcea740257 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 11:09:24 +1100 Subject: [PATCH 055/121] fix(pbs): fix pbs list command when getting config --- nf_core/configs/create/hpccustomisation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index cf20c11f5e..5fb902af4c 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -133,7 +133,7 @@ def _slurm_get_default_queue(self) -> str: return config[0]["PartitionName"] def _pbs_get_default_queue(self) -> str: - pbs_raw_config = subprocess.check_output(["qmgr", "-c", "\"list server\""]).decode("utf-8") + pbs_raw_config = subprocess.check_output(["qmgr", "-c", "list server"]).decode("utf-8") config = self._pbs_parse_config(pbs_raw_config) return config["default_queue"] From 0a84ba076a9f5a965a6784e73e6f1804f75d868e Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 11:09:46 +1100 Subject: [PATCH 056/121] fix(pbs): fix split --- nf_core/configs/create/hpccustomisation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 5fb902af4c..e991a63605 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -152,7 +152,7 @@ def _pbs_parse_config(self, raw: Optional[str]) -> dict: for line in [r.strip() for r in raw.split("\n")]: if "=" not in line: continue - k, v = [token.strip() for token in line.rstrip("=", 1)] + k, v = [token.strip() for token in line.split("=", 1)] config[k] = v return config From 4b1d628ce8f78996be41e125b2db77b7e584907e Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 11:35:28 +1100 Subject: [PATCH 057/121] feat(queue): autofill default queue if available --- nf_core/configs/create/hpccustomisation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index e991a63605..2d8ea13b52 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -27,6 +27,7 @@ def compose(self) -> ComposeResult: yield Footer() scheduler = self._get_scheduler() queues = self._get_queues(scheduler) + default_queue = self._get_default_queue(scheduler) module_system_used = self._detect_module_system() yield Markdown(markdown_intro) with Horizontal(): @@ -41,6 +42,7 @@ def compose(self) -> ComposeResult: "queue", "Queue", "The default queue in your HPC.", + default=default_queue if default_queue else "", classes="column", suggestions=queues, ) From 377de87106775865927ef734b7381509dddf4c69 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Thu, 12 Mar 2026 11:41:23 +1100 Subject: [PATCH 058/121] WIP: I have made a mess --- nf_core/configs/create/utils.py | 38 +++++++++++---------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index ffbb9698c0..e81e2223e7 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -55,19 +55,19 @@ class ConfigsCreateConfig(BaseModel): """ Config description """ config_profile_url: Optional[str] = None """ Config institution URL """ - default_process_ncpus: Optional[int] = None + default_process_ncpus: Optional[str] = None """ Default number of CPUs """ - default_process_memgb: Optional[int] = None + default_process_memgb: Optional[str] = None """ Default amount of memory """ - default_process_hours: Optional[float] = None + default_process_hours: Optional[str] = None """ Default walltime - hours """ custom_process_id: Optional[str] = None """" Name or label of a process to configure """ - custom_process_ncpus: Optional[int] = None + custom_process_ncpus: Optional[str] = None """ Number of CPUs for process """ - custom_process_memgb: Optional[int] = None + custom_process_memgb: Optional[str] = None """ Amount of memory for process """ - custom_process_hours: Optional[float] = None + custom_process_hours: Optional[str] = None """ Walltime for process - hours """ named_process_resources: Optional[dict] = None """ Dictionary containing custom resource requirements for named processes """ @@ -85,11 +85,11 @@ class ConfigsCreateConfig(BaseModel): """ Modules to load when running processes """ container_system: Optional[str] = None """ The container system the HPC uses """ - memory: Optional[int] = None + memory: Optional[str] = None """ The maximum memory available to processes """ - cpus: Optional[int] = None + cpus: Optional[str] = None """ The maximum number of CPUs available to processes """ - time: Optional[float] = None + time: Optional[str] = None """ The maximum walltime available to processes """ envvar: Optional[str] = None """ An environment variable to hold a custom Nextflow container cachedir """ @@ -99,12 +99,8 @@ class ConfigsCreateConfig(BaseModel): """ A cachedir for iGenomes """ scratch_dir: Optional[str] = None """ A scratch directory to use """ - retries: Optional[int] = None + retries: Optional[str] = None """ Number of retries for failed jobs """ - delete_work_dir: Optional[bool] = None - """ Whether to delete the work directory on successful pipeline complete """ - module: Optional[str] = None - """ Detected module system """ model_config = ConfigDict(extra="allow") @@ -319,7 +315,7 @@ def is_path_ondisk(cls, v: str, info: ValidationInfo) -> str: """ v = v.strip() if v == "": - raise ValueError("Cannot be left empty.") + return v #optional if not _PATH_PATTERN.match(v): raise ValueError( @@ -334,7 +330,7 @@ def is_path_ondisk(cls, v: str, info: ValidationInfo) -> str: def is_path_or_uri(cls, v: str, info: ValidationInfo) -> str: v = v.strip() if v == "": - raise ValueError("Cannot be left empty.") + return v #optional uri_pattern = re.compile(r"^\w+:\/\/\w+") if not _PATH_PATTERN.match(v) and not uri_pattern.match(v): @@ -345,11 +341,6 @@ def is_path_or_uri(cls, v: str, info: ValidationInfo) -> str: ) return v - @field_validator("module_system", "module") - @classmethod - def module_system_valid(cls, v: str, info: ValidationInfo) -> str: - ... #TODO - @field_validator("container_system") @classmethod def container_system_valid(cls, v: str, info: ValidationInfo) -> str: @@ -360,11 +351,6 @@ def container_system_valid(cls, v: str, info: ValidationInfo) -> str: ) return v - @field_validator("delete_work_dir") - @classmethod - def is_bool(cls, v: str, info: ValidationInfo) -> str: - ... #TODO - ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) class TextInput(Static): """Widget for text inputs. From 79da1cbd128b3f29eade35af8a83f643ff394685 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Thu, 12 Mar 2026 11:46:19 +1100 Subject: [PATCH 059/121] MAINT: Max resourcelimits as required fields --- nf_core/configs/create/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index e81e2223e7..8524a89501 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -265,7 +265,7 @@ def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: context = info.context if context and context["is_infrastructure"]: if v.strip() == "": - return v + raise ValueError("Cannot be empty.") try: v_int = int(v.strip()) except ValueError: @@ -281,7 +281,7 @@ def non_neg_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: context = info.context if context and context["is_infrastructure"]: if v.strip() == "": - return v + raise ValueError("Cannot be empty.") try: vf = float(v.strip()) except ValueError: From ad9d33cd486eb0d9f8d5b75eabbf6ca994064f50 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 11:56:00 +1100 Subject: [PATCH 060/121] fix(pbs): fix broken pbs queue listing --- nf_core/configs/create/hpccustomisation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 06c17a3b71..d436b423ea 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -1,4 +1,5 @@ import subprocess +import json from typing import Optional from textual.app import ComposeResult @@ -90,8 +91,8 @@ def _get_queues(self, scheduler: Optional[str]) -> list[str]: pass elif scheduler == "pbs": try: - queues = subprocess.check_output(["qstat", "-q"]).decode("utf-8") - return queues.split("\n") + queues = json.loads(subprocess.check_output(["qstat", "-Q", "-f", "-F", "json"]).decode("utf-8")) + return list(queues["Queue"].keys()) except subprocess.CalledProcessError: pass elif scheduler == "sge": From 0ee7a1c15923261a47ee2e065c782f498414a091 Mon Sep 17 00:00:00 2001 From: fredjaya Date: Thu, 12 Mar 2026 12:01:30 +1100 Subject: [PATCH 061/121] DEV: Placeholder validator --- nf_core/configs/create/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 8524a89501..603a7bdbad 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -350,6 +350,14 @@ def container_system_valid(cls, v: str, info: ValidationInfo) -> str: f"Must be one of: {', '.join(SUPPORTED_CONTAINERS)}" ) return v + + @field_validator("module_system") + @classmethod + def module_system(cls, v: str, info: ValidationInfo) -> str: + #TODO: placeholder validator until functionality is finished + return v + + ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) class TextInput(Static): From 40ef725c562fede77be1e75b758f8461ae7d7964 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 12:06:34 +1100 Subject: [PATCH 062/121] fix(slurm): fix listing slurm queues --- nf_core/configs/create/hpccustomisation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index d436b423ea..f069c7f1a6 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -85,8 +85,9 @@ def _get_queues(self, scheduler: Optional[str]) -> list[str]: """Get the available queues to use for the jobs""" if scheduler == "slurm": try: - queues = subprocess.check_output(["sinfo", "-o", '"%P,%c,%m,%l"']).decode("utf-8") - return queues.split("\n") + queues = subprocess.check_output(["sinfo", "-h", "-o", '%P']).decode("utf-8") + # Remove default * flag + return [i.strip().replace("*", "") for i in queues.split("\n") if i] except subprocess.CalledProcessError: pass elif scheduler == "pbs": From 9896d1b1f37984acfcf64a38e4895c1e87064f2d Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 15:13:52 +1100 Subject: [PATCH 063/121] feat(serial): add serial file --- nf_core/configs/create/serial.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 nf_core/configs/create/serial.py diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py new file mode 100644 index 0000000000..c3b035306c --- /dev/null +++ b/nf_core/configs/create/serial.py @@ -0,0 +1,54 @@ +from typing import Any, Optional + +class NextflowSerial: + def __init__(self, data_dict: Optional[dict], tab_indent: Optional[int] = 4): + self.data_dict = data_dict + + def __getitem__(self, key: Optional[Any]) -> Any: + """Returns value from data""" + return self.data_dict[key] + + def __setitem__(self, key: Optional[Any], value: Optional[any]) -> None: + """Sets value in data""" + self.data_dict[key] = value + + @staticmethod + def _stringify(data: Optional[Any]) -> str: + """Return nextflow compatible value""" + if isinstance(data, str): + return f"'{data}'" + if isinstance(data, bool): + return 'true' if data else 'false' + if isinstance(data, type(None)): + return 'null' + return str(data) # Fallback on the Python str function if no matches + + @staticmethod + def dumps(data_dict: Optional[Any], tab_indent: Optional[int], current_indent: Optional[int], end: Optional[str] = '\n') -> str: + """Recursive function to create configuration file""" + output = "" + if isinstance(data_dict, dict): + for k, v in data_dict.items(): + if isinstance(v, dict): + output += " "*current_indent + output += f"{k} {{\n" + output += NextflowSerial.dumps(v, tab_indent = tab_indent, current_indent = current_indent + tab_indent, end = end) + output += " "*current_indent + output += f"}}{end}" + elif isinstance(v, list): + output += " "*current_indent + output += f"{k} = [" + vi = [] + for i in v: + if isinstance(i, dict): + vi.append("{" + NextflowSerial.dumps(i, tab_indent, current_indent=0, end='') + "}") + continue + vi.append(NextflowSerial.dumps(i, tab_indent, current_indent=0, end = end)) + output += ", ".join(vi) + output += f"]{end}" + else: + output += " "*current_indent + output += f"{k} = {NextflowSerial._stringify(v)}{end}" + return output + else: + return NextflowSerial._stringify(data_dict) From 6cf6cbbe4749ca8c0122b9735c35f300a5b3bb4b Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 16:24:25 +1100 Subject: [PATCH 064/121] feat(serial): add oneliner option --- nf_core/configs/create/serial.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index c3b035306c..8528d07e2b 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -24,13 +24,13 @@ def _stringify(data: Optional[Any]) -> str: return str(data) # Fallback on the Python str function if no matches @staticmethod - def dumps(data_dict: Optional[Any], tab_indent: Optional[int], current_indent: Optional[int], end: Optional[str] = '\n') -> str: + def dumps(data_dict: Optional[Any], tab_indent: Optional[int], current_indent: Optional[int], end: Optional[str] = '\n', indent_start: Optional[bool] = True, one_line: Optional[bool] = False) -> str: """Recursive function to create configuration file""" output = "" if isinstance(data_dict, dict): for k, v in data_dict.items(): if isinstance(v, dict): - output += " "*current_indent + output += " "*current_indent*int(indent_start) output += f"{k} {{\n" output += NextflowSerial.dumps(v, tab_indent = tab_indent, current_indent = current_indent + tab_indent, end = end) output += " "*current_indent @@ -40,15 +40,19 @@ def dumps(data_dict: Optional[Any], tab_indent: Optional[int], current_indent: O output += f"{k} = [" vi = [] for i in v: + oneliner = bool(len(i.keys()) != 1) if isinstance(i, dict): - vi.append("{" + NextflowSerial.dumps(i, tab_indent, current_indent=0, end='') + "}") + vi.append("{"*oneliner + NextflowSerial.dumps(i, tab_indent, current_indent=0, end = '', indent_start = False, one_line = not oneliner) + "}"*oneliner) continue vi.append(NextflowSerial.dumps(i, tab_indent, current_indent=0, end = end)) output += ", ".join(vi) output += f"]{end}" else: - output += " "*current_indent - output += f"{k} = {NextflowSerial._stringify(v)}{end}" + output += " "*current_indent*indent_start + if one_line: + output += f"{k}: {NextflowSerial._stringify(v)}{end}" + else: + output += f"{k} = {NextflowSerial._stringify(v)}{end}" return output else: return NextflowSerial._stringify(data_dict) From ec7a42a5ec01ad65869627fa987c4768a1699306 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 12 Mar 2026 16:33:19 +1100 Subject: [PATCH 065/121] add executor scope and additional executor fields, e.g. poll interval and queu size --- nf_core/configs/create/create.py | 69 +++++++++++++++++---- nf_core/configs/create/finalinfradetails.py | 22 +++++++ nf_core/configs/create/hpccustomisation.py | 7 +++ nf_core/configs/create/utils.py | 8 +++ 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 6d23036803..aa9a04fe25 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -169,16 +169,50 @@ def construct_process_config_str(self): return sub(r'\n\n\n+', '\n\n', process_section_str) def construct_executor_config_str(self): - # TODO: Update Pydantic model and HpcCustomisation/FinalInfraDetails - # to get info for following executor config fields - # queueSize - # pollInterval - # queueStatInterval (non-local) - # submitRateLimit + queue_stat_interval_str = '' + queue_size_str = '' + poll_interval_str = '' + submit_rate_str = '' + + if self.template_config.queue_stat_interval: + queue_stat_interval = float(self.template_config.queue_stat_interval) + if queue_stat_interval.is_integer(): + queue_stat_interval = int(queue_stat_interval) + queue_stat_interval_str = f' queueStatInterval = {queue_stat_interval}.m' + else: + queue_stat_interval = int(queue_stat_interval * 60) + queue_stat_interval_str = f' queueStatInterval = {queue_stat_interval}.s' + + if self.template_config.queue_size: + queue_size = int(self.template_config.queue_size) + queue_size_str = f' queueSize = {queue_size}' + + if self.template_config.poll_interval: + poll_interval = float(self.template_config.poll_interval) + if poll_interval.is_integer(): + poll_interval = int(poll_interval) + poll_interval_str = f' pollInterval = {poll_interval}.m' + else: + poll_interval = int(poll_interval * 60) + poll_interval_str = f' pollInterval = {poll_interval}.s' + + if self.template_config.submit_rate: + submit_rate = int(self.template_config.submit_rate) + submit_rate_str = f" submitRateLimit = '{submit_rate}min'" + + executor_section = [ + queue_stat_interval_str, + queue_size_str, + poll_interval_str, + submit_rate_str + ] + + executor_section = [s for s in executor_section if s] + executor_section = [ 'executor {', '\n', - # ... + *executor_section, '\n', '}', ] @@ -186,15 +220,25 @@ def construct_executor_config_str(self): return sub(r'\n\n\n+', '\n\n', executor_section_str) def construct_container_config_str(self, container_name, cache_dir): + if not container_name: + return [] + enabled_str = ' enabled = true' - cache_dir_str = f" cacheDir = '{cache_dir}'" + cache_dir_str = f" cacheDir = '{cache_dir}'" if cache_dir else '' automount_str = ' autoMounts = true' if container_name in ['singularity', 'apptainer'] else '' + container_section = [ - f'{container_name}' + ' {', - '\n', enabled_str, cache_dir_str, automount_str, + ] + + container_section = [s for s in container_section if s] + + container_section = [ + f'{container_name}' + ' {', + '\n', + *container_section, '\n', '}', ] @@ -268,8 +312,7 @@ def construct_infra_process_config_str(self): def construct_infra_config_str(self): process_section_str = self.construct_infra_process_config_str() - # TODO: Uncomment when executor option fields are implemented - # executor_section_str = self.construct_executor_config_str() + executor_section_str = self.construct_executor_config_str() container_section_str = self.construct_container_config_str( self.template_config.container_system, @@ -285,7 +328,7 @@ def construct_infra_config_str(self): final_config = [ process_section_str, - # executor_section_str, + executor_section_str, container_section_str, unscoped_configs_str, ] diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 0288ac64ef..025ee8e9ba 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -65,6 +65,28 @@ def compose(self) -> ComposeResult: "Maximum time (hours) to run your jobs.", classes="column", ) + with Horizontal(): + yield TextInput( + "queue_size", + "Queue size", + "Number of jobs that can be submitted simultaneously.", + default="300", + classes="column", + ) + yield TextInput( + "poll_interval", + "Poll interval", + "How often to check for successful process completion (minutes).", + default="0.5", + classes="column", + ) + yield TextInput( + "submit_rate", + "Jobs per minutes", + "Maximum number of jobs that can be submitted per minute.", + default="20", + classes="column", + ) with Vertical(id="define-global-cache-dir", classes="hide" if not self.container_system else ""): yield Markdown("## Do you want to define a global cache directory for containers or conda environments?") yield TextInput( diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 2ab9b9d1db..a1b5a6f345 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -47,6 +47,13 @@ def compose(self) -> ComposeResult: classes="column", suggestions=queues, ) + yield TextInput( + "queue_stat_interval", + "Queue stat interval", + "How often to get the queue status from the scheduler (minutes).", + default="0.5", + classes="column", + ) yield TextInput( "module_system", "Other modules to load", diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index ff60dace1b..0ef2623b5e 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -105,6 +105,14 @@ class ConfigsCreateConfig(BaseModel): """ Whether the infrastructure uses a module system """ delete_work_dir: Optional[bool] = False """ Whether to clean up the work directory upon successful completion """ + queue_stat_interval: Optional[str] = None + """ How often to check the HPC queue status. """ + queue_size: Optional[str] = None + """ How many jobs can be submitted to the queue at once. """ + poll_interval: Optional[str] = None + """ How often to check for successful completion of processes. """ + submit_rate: Optional[str] = None + """ How many jobs can be submitted per minute. """ model_config = ConfigDict(extra="allow") From c5713449bb1b95b4f7b046398c2733b92e705c93 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Thu, 12 Mar 2026 16:59:29 +1100 Subject: [PATCH 066/121] feat(serial): add serial method to configuration file --- nf_core/configs/create/utils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 84025a7fd4..d60de946ee 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -114,6 +114,34 @@ def __init__(self, /, **data: Any) -> None: context=_init_context_var.get(), ) + def serial(self): + """Returns a dictionary of the config""" + ret = { + "params": { + "config_profile_contact": self.config_profile_contact, + "config_profile_description": self.config_profile_description, + "config_profile_url": self.config_profile_url, + "igenomes_base": self.igenomes_base + }, + "process": { + "executor": self.scheduler, + "queue": self.queue, + "resourceLimits": [ + {"cpus": self.default_process_ncpus}, + {"memory": self.default_process_memgb}, + {"time": self.default_process_hours} + ], + "scratch": self.scratch_dir, + "maxRetries": self.retries, + }, + self.container_system: { + "enabled": True, + "cacheDir": self.cachedir, + "autoMounts": True + }, + "cleanup": self.delete_work_dir + } + @field_validator("general_config_name") @classmethod def notempty(cls, v: str) -> str: From 9b2feb69e1e2e05947b273acc8bfbd9b91e0fc34 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 13 Mar 2026 10:21:17 +1100 Subject: [PATCH 067/121] bugfix: remove erroneous double up of 'memory' setting --- nf_core/configs/create/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index aa9a04fe25..7c88397e55 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -260,7 +260,7 @@ def construct_infra_process_config_str(self): memory_str = '' if self.template_config.memory: memory_int = int(self.template_config.memory) - memory_str = f'memory = {memory_int}.GB' + memory_str = f'{memory_int}.GB' time_str = '' if self.template_config.time: time_h = float(self.template_config.time) From 131fb3588386f4fba2a5d54171397f907aa5ceca Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 10:37:18 +1100 Subject: [PATCH 068/121] feat(config): replace output methods wiht serial methods --- nf_core/configs/create/create.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 6d23036803..cbb9fde32d 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -3,6 +3,7 @@ """ from nf_core.configs.create.utils import ConfigsCreateConfig, generate_config_entry +from nf_core.configs.create.serial import NextflowSerial from re import sub from pathlib import Path @@ -313,7 +314,10 @@ def write_to_file(self): else: raise ValueError(f'Invalid config type: {self.config_type}') + serial_data = NextflowSerial.dumps(self.template_config.serial_hpc()) + with open(filename, "w+") as file: ## Write params - file.write(params_section_str) - file.write(process_section_str) + file.write(serial_data) + #file.write(params_section_str) + #file.write(process_section_str) From de40267c13950de1f05d1d28546fe07e3ceb4858 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 10:37:47 +1100 Subject: [PATCH 069/121] fix(util): rename serial to serial_hpc --- nf_core/configs/create/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index d60de946ee..08720df477 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -114,7 +114,7 @@ def __init__(self, /, **data: Any) -> None: context=_init_context_var.get(), ) - def serial(self): + def serial_hpc(self): """Returns a dictionary of the config""" ret = { "params": { From c25d25f12a8eca54c009e0e66df68add7b34fdca Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 13 Mar 2026 11:03:19 +1100 Subject: [PATCH 070/121] validate new infrastructure fields --- nf_core/configs/create/utils.py | 52 +++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 0ef2623b5e..bb8c110050 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -35,6 +35,7 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: _PATH_PATTERN = re.compile(r"(\/|~\/|~$|\$\{?\w+\}?)(.*)") # Used by finalinfradetails as it already imports create.utils SUPPORTED_CONTAINERS = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter"] +SUPPORTED_SCHEDULERS = ["local", "pbs", "pbspro", "slurm", "sge"] class ConfigsCreateConfig(BaseModel): """Pydantic model for the nf-core configs create config.""" @@ -91,8 +92,6 @@ class ConfigsCreateConfig(BaseModel): """ The maximum number of CPUs available to processes """ time: Optional[str] = None """ The maximum walltime available to processes """ - envvar: Optional[str] = None - """ An environment variable to hold a custom Nextflow container cachedir """ cachedir: Optional[str] = None """ An environment variable to hold a custom Nextflow container cachedir """ igenomes_cachedir: Optional[str] = None @@ -264,7 +263,7 @@ def non_neg_float_valid(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a non-negative number.") return v - @field_validator("cpus", "memory", "retries") + @field_validator("cpus", "memory", "retries", "queue_size", "submit_rate") @classmethod def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: """ @@ -302,6 +301,22 @@ def non_neg_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a non-negative number.") return v + @field_validator("poll_interval") + @classmethod + def pos_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: + """Check that numeric values are positive.""" + context = info.context + if context and context["is_infrastructure"]: + if v.strip() == "": + raise ValueError("Cannot be empty.") + try: + vf = float(v.strip()) + except ValueError: + raise ValueError("Must be a number.") + if not vf > 0: + raise ValueError("Must be a positive number.") + return v + @field_validator("scheduler", "queue") @classmethod def nonemtpy_hpc_details(cls, v: str, info: ValidationInfo) -> str: @@ -311,7 +326,19 @@ def nonemtpy_hpc_details(cls, v: str, info: ValidationInfo) -> str: if v.strip() == "": raise ValueError("Cannot be left empty.") return v - + + @field_validator("scheduler") + @classmethod + def valid_scheduler(cls, v: str, info: ValidationInfo) -> str: + """Check that the HPC scheduler is supported""" + context = info.context + if context and context["is_infrastructure"] and context["is_hpc"]: + if v.strip() not in SUPPORTED_SCHEDULERS: + raise ValueError( + f"Must be one of: {', '.join(SUPPORTED_SCHEDULERS)}" + ) + return v + @field_validator("cachedir", "scratch_dir") @classmethod def is_path_ondisk(cls, v: str, info: ValidationInfo) -> str: @@ -368,7 +395,22 @@ def container_system_valid(cls, v: str, info: ValidationInfo) -> str: def module_system(cls, v: str, info: ValidationInfo) -> str: #TODO: placeholder validator until functionality is finished return v - + + @field_validator("queue_stat_interval") + @classmethod + def pos_hpc_interval_valid(cls, v: str, info: ValidationInfo) -> str: + """Check that HPC interval values are positive.""" + context = info.context + if context and context["is_infrastructure"] and context["is_hpc"]: + if v.strip() == "": + raise ValueError("Must not be empty.") + try: + vf = float(v.strip()) + except ValueError: + raise ValueError("Must be a number.") + if not vf > 0: + raise ValueError("Must be a positive number.") + return v ## TODO Duplicated from pipelines utils - move to common location if possible (validation seems to be context specific so possibly not) From 9bd081eb05c3010d200a8395274b592152a3f961 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 11:44:56 +1100 Subject: [PATCH 071/121] feat(serial): add option to remove quotes around strings for one-liners --- nf_core/configs/create/serial.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index 8528d07e2b..7f0dbe83cb 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -13,10 +13,11 @@ def __setitem__(self, key: Optional[Any], value: Optional[any]) -> None: self.data_dict[key] = value @staticmethod - def _stringify(data: Optional[Any]) -> str: + def _stringify(data: Optional[Any], quote: Optional[bool] = True) -> str: """Return nextflow compatible value""" + quote = "'"*quote if isinstance(data, str): - return f"'{data}'" + return f"{quote}{data}{quote}" if isinstance(data, bool): return 'true' if data else 'false' if isinstance(data, type(None)): From 0f25c5d5bc2704234bbf2fbfd59b311a8a8d72dd Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 11:45:24 +1100 Subject: [PATCH 072/121] feat(serial): set default values for dumps --- nf_core/configs/create/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index 7f0dbe83cb..1efe6bd7be 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -25,7 +25,7 @@ def _stringify(data: Optional[Any], quote: Optional[bool] = True) -> str: return str(data) # Fallback on the Python str function if no matches @staticmethod - def dumps(data_dict: Optional[Any], tab_indent: Optional[int], current_indent: Optional[int], end: Optional[str] = '\n', indent_start: Optional[bool] = True, one_line: Optional[bool] = False) -> str: + def dumps(data_dict: Optional[Any], tab_indent: Optional[int] = 4, current_indent: Optional[int] = 0, end: Optional[str] = '\n', indent_start: Optional[bool] = True, one_line: Optional[bool] = False) -> str: """Recursive function to create configuration file""" output = "" if isinstance(data_dict, dict): From b9f57afd5aeedd81384a3dd1161b3b32074ecbfd Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 11:45:39 +1100 Subject: [PATCH 073/121] feat(serial): remove quotes around list dicts --- nf_core/configs/create/serial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index 1efe6bd7be..f1f2108e03 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -51,7 +51,8 @@ def dumps(data_dict: Optional[Any], tab_indent: Optional[int] = 4, current_inden else: output += " "*current_indent*indent_start if one_line: - output += f"{k}: {NextflowSerial._stringify(v)}{end}" + # False is set here to disable quotes on strings + output += f"{k}: {NextflowSerial._stringify(v, False)}{end}" else: output += f"{k} = {NextflowSerial._stringify(v)}{end}" return output From e1041cc26d03df050c006f0b50ff450d760083ea Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 11:46:06 +1100 Subject: [PATCH 074/121] fix(serial): output formatted resource information --- nf_core/configs/create/utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 6ae71dba65..2b4598d58b 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -131,18 +131,18 @@ def serial_hpc(self): "config_profile_contact": self.config_profile_contact, "config_profile_description": self.config_profile_description, "config_profile_url": self.config_profile_url, - "igenomes_base": self.igenomes_base + "igenomes_base": self.igenomes_cachedir }, "process": { "executor": self.scheduler, "queue": self.queue, "resourceLimits": [ - {"cpus": self.default_process_ncpus}, - {"memory": self.default_process_memgb}, - {"time": self.default_process_hours} + {"cpus": int(self.cpus)}, + {"memory": self.memory + "GB"}, + {"time": str(float(self.time)) + "h"} ], "scratch": self.scratch_dir, - "maxRetries": self.retries, + "maxRetries": int(self.retries), }, self.container_system: { "enabled": True, @@ -152,6 +152,8 @@ def serial_hpc(self): "cleanup": self.delete_work_dir } + return ret + @field_validator("general_config_name", "config_profile_description") @classmethod def notempty(cls, v: str) -> str: From b00f35082b667026e6aa4e059cfe8a5f82ababc8 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 11:55:49 +1100 Subject: [PATCH 075/121] feat(serial): add base pipeline seiral method --- nf_core/configs/create/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 2b4598d58b..227487d6a8 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -154,6 +154,17 @@ def serial_hpc(self): return ret + def serial_pipeline(self): + """Returns a dictionary of the pipeline config""" + ret = { + "params": { + "config_profile_contact": self.config_profile_contact, + "config_profile_description": self.config_profile_description, + "config_profile_url": self.config_profile_url, + "igenomes_base": self.igenomes_cachedir + } + } + @field_validator("general_config_name", "config_profile_description") @classmethod def notempty(cls, v: str) -> str: From 2e008259167466a1a1439917a2545d9ea7afc27b Mon Sep 17 00:00:00 2001 From: Amarinder Thind Date: Fri, 13 Mar 2026 12:06:12 +1100 Subject: [PATCH 076/121] this with --- nf_core/configs/create/finalinfradetails.py | 42 ++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 025ee8e9ba..59efb37c58 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -6,7 +6,7 @@ from textual.app import ComposeResult from textual.containers import Center, Horizontal, Vertical from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Static, Switch +from textual.widgets import Button, Footer, Header, Input, Markdown, Static, Switch, Select from nf_core.configs.create.utils import ( TextInput, @@ -16,6 +16,7 @@ ) from nf_core.utils import add_hide_class, remove_hide_class + markdown_intro = """ # Configure the options for your infrastructure config """ @@ -36,15 +37,16 @@ def compose(self) -> ComposeResult: yield Markdown(markdown_intro) self.container_system_list = self._get_container_systems() self.container_system = self.container_system_list[0] if self.container_system_list else None - - # TODO: convert to dropdown with contents self.container_system_list - yield TextInput( - "container_system", - "Container system", - "What container or software system will you use to run your pipeline?", - classes="", - suggestions=self.container_system_list, - ) + + self.container_system_list = SUPPORTED_CONTAINERS + self.container_system = self.container_system_list[0] # default + + yield Select( + [(c, c) for c in self.container_system_list], + prompt="Select container system", + id="container_system", + value=self.container_system, #sets default + ) yield Markdown("## Maximum resources") with Horizontal(): yield TextInput( @@ -171,13 +173,10 @@ def _get_set_directory(self, dir: str) -> Optional[str]: return set_dir return None - @on(Input.Changed) - def get_container_system(self) -> None: - """Get the container system from the input.""" - self.container_system = None - text_input = self.query_one("#container_system") - this_input = text_input.query_one(Input) - self.container_system = this_input.value + @on(Select.Changed, "#container_system") + def get_container_system(self,event: Select.Changed) -> None: + """Get the container system from dropdown.""" + self.container_system = event.value cachedir_text_input = self.query_one("#cachedir") cachedir_input = cachedir_text_input.query_one(Input) if self.container_system: @@ -192,6 +191,11 @@ def get_container_system(self) -> None: def on_finish_button(self, event: Button.Pressed) -> None: """Save fields to the config.""" new_config = {} + + #collect dropdown value + select = self.query_one("#container_system", Select) + new_config['container_system'] = select.value + for text_input in self.query("TextInput"): if "hide" in text_input.classes: continue @@ -202,9 +206,13 @@ def on_finish_button(self, event: Button.Pressed) -> None: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: text_input.query_one(".validation_msg").update("") + + # collect switch value delete_work_switch = self.query_one("#toggle-delete-work") new_config['delete_work_dir'] = delete_work_switch.value new_config['module'] = self._detect_module_system() + + # Validate and update the config try: with init_context(self.parent.get_context()): # First, validate the new config data From ad262aea89d0756fdd5b16655a920950f147bf8d Mon Sep 17 00:00:00 2001 From: Amarinder Thind Date: Fri, 13 Mar 2026 12:12:04 +1100 Subject: [PATCH 077/121] Added drop down option for a field --- nf_core/configs/create/finalinfradetails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 59efb37c58..6361578095 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -131,7 +131,7 @@ def compose(self) -> ComposeResult: ) def _get_container_systems(self) -> list[str]: - """Get the available container systems to use for software handling.""" + """Get the available container systems to use for software handling. """ module_system_used = self._detect_module_system() container_systems = SUPPORTED_CONTAINERS available_systems = [] From 1d1fc9ea258f254d1fd991d14cb17e3a37b6acbd Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 12:43:19 +1100 Subject: [PATCH 078/121] feat(utils): add methods --- nf_core/configs/create/utils.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 227487d6a8..fa30467d2e 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -158,12 +158,28 @@ def serial_pipeline(self): """Returns a dictionary of the pipeline config""" ret = { "params": { - "config_profile_contact": self.config_profile_contact, "config_profile_description": self.config_profile_description, - "config_profile_url": self.config_profile_url, - "igenomes_base": self.igenomes_cachedir + }, + "process": { + "cpus": self.default_process_ncpus, + "memory": self.default_process_memgb, + "time": self.default_process_hours } } + + for named in self.named_process_resources.keys(): + ret[named] = self.named_process_resources[named] + + for labelled in self.labelled_process_resources.keys(): + ret[labelled] = self.labelled_process_resources[labelled] + + return ret + + def serial(self): + if self.general_config_type != "pipeline": + return self.serial_hpc() + else: + return self.serial_pipeline() @field_validator("general_config_name", "config_profile_description") @classmethod From 85403af6ceaef0ff58fdb4887a7e7b744c6d8692 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 13 Mar 2026 12:44:07 +1100 Subject: [PATCH 079/121] validate custom process queue --- nf_core/configs/create/utils.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index bb8c110050..55f10a2cf3 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -70,6 +70,8 @@ class ConfigsCreateConfig(BaseModel): """ Amount of memory for process """ custom_process_hours: Optional[str] = None """ Walltime for process - hours """ + custom_process_queue: Optional[str] = None + """ Custom queue for process to override default """ named_process_resources: Optional[dict] = None """ Dictionary containing custom resource requirements for named processes """ labelled_process_resources: Optional[dict] = None @@ -262,6 +264,19 @@ def non_neg_float_valid(cls, v: str, info: ValidationInfo) -> str: if not vf >= 0: raise ValueError("Must be a non-negative number.") return v + + @field_validator("custom_process_queue") + @classmethod + def valid_custom_queue(cls, v: str, info: ValidationInfo) -> str: + """Check that a custom queue is either not set or is a valid string""" + context = info.context + if context and not context["is_infrastructure"]: + if v.strip() == "": + return v + if ' ' in v.strip(): + raise ValueError("Cannot contain spaces") + # TODO: Any other invalid characters? + return v @field_validator("cpus", "memory", "retries", "queue_size", "submit_rate") @classmethod From e96c8dcb835f12e295c2efab47f3d70e23540821 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 13:44:26 +1100 Subject: [PATCH 080/121] revert(create): revert method call to serial() --- nf_core/configs/create/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index e8bc32eaf0..486c798513 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -357,7 +357,7 @@ def write_to_file(self): else: raise ValueError(f'Invalid config type: {self.config_type}') - serial_data = NextflowSerial.dumps(self.template_config.serial_hpc()) + serial_data = NextflowSerial.dumps(self.template_config.serial()) with open(filename, "w+") as file: ## Write params From 892025d88242b5764ffe5f9a7cc17eefb3e9f13a Mon Sep 17 00:00:00 2001 From: Amarinder Thind Date: Fri, 13 Mar 2026 13:53:36 +1100 Subject: [PATCH 081/121] updated pbs to pbspro --- nf_core/configs/create/hpccustomisation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index a1b5a6f345..39c5675c1b 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -77,7 +77,7 @@ def _get_scheduler(self) -> Optional[str]: pass try: subprocess.run(["qstat", "--version"]) - return "pbs" + return "pbspro" except FileNotFoundError: pass except subprocess.CalledProcessError: @@ -100,7 +100,7 @@ def _get_queues(self, scheduler: Optional[str]) -> list[str]: return [i.strip().replace("*", "") for i in queues.split("\n") if i] except subprocess.CalledProcessError: pass - elif scheduler == "pbs": + elif scheduler == "pbspro": try: queues = json.loads(subprocess.check_output(["qstat", "-Q", "-f", "-F", "json"]).decode("utf-8")) return list(queues["Queue"].keys()) @@ -121,7 +121,7 @@ def _get_default_queue(self, scheduler: Optional[str]) -> str: return self._slurm_get_default_queue() except FileNotFoundError: pass - elif scheduler == "pbs": + elif scheduler == "pbspro": try: return self._pbs_get_default_queue() except subprocess.CalledProcessError: From cb1cfffe1f4fc111520dd8b3af3feddb7232e5c7 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 13:55:57 +1100 Subject: [PATCH 082/121] feat(config): change general_config_type to a boolean value is_infrastrucutre --- nf_core/configs/create/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 55f10a2cf3..6939ecd33b 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -40,8 +40,8 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: class ConfigsCreateConfig(BaseModel): """Pydantic model for the nf-core configs create config.""" - general_config_type: Optional[str] = None - """ Config file type (infrastructure or pipeline) """ + is_infrastructure: Optional[bool] = False + """ Config variable to define if this is infrastructure or pipeline """ config_pipeline_name: Optional[str] = None """ The name of the pipeline """ config_pipeline_path: Optional[str] = None From c7deecafbbc5887957c6f24be214acaaf0c9140e Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 14:02:31 +1100 Subject: [PATCH 083/121] feat(is_infra): set to true for hpc configuration --- nf_core/configs/create/hpccustomisation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index a1b5a6f345..e46448f666 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -185,6 +185,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) new_config[text_input.field_id] = this_input.value + new_config["is_infrastructure"] = True if not validation_result.is_valid: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: From 5c92761fbb7788c2c0adf63895c7153092e146c4 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 14:03:53 +1100 Subject: [PATCH 084/121] feat(is_infra): set to true for infra config --- nf_core/configs/create/defaultprocessres.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index ffdaf3727c..19ff4b1382 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -76,6 +76,7 @@ def on_next_button(self, event: Button.Pressed) -> None: this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) new_config[text_input.field_id] = this_input.value + new_config["is_infrastructure"] = True if not validation_result.is_valid: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: From f354b335738624173f83829a9a19b8b6e0409367 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 14:50:01 +1100 Subject: [PATCH 085/121] revert(infra): remove infra from pipeline config --- nf_core/configs/create/defaultprocessres.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 19ff4b1382..ffdaf3727c 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -76,7 +76,6 @@ def on_next_button(self, event: Button.Pressed) -> None: this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) new_config[text_input.field_id] = this_input.value - new_config["is_infrastructure"] = True if not validation_result.is_valid: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: From 6fe09072d2e2ede30c3667ba43a9c5fda4f38dca Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 14:58:46 +1100 Subject: [PATCH 086/121] feat(infra): add infrastructure variable to basic details --- nf_core/configs/create/basicdetails.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 3600444f9b..3f595f252d 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -99,6 +99,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) config[text_input.field_id] = this_input.value + config["is_infrastructure"] = self.parent.CONFIG_TYPE == "infrastructure" if not validation_result.is_valid: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: From 290a2feabffed3d4564bc1ad7265da14b63cfae2 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 15:02:11 +1100 Subject: [PATCH 087/121] revert(infra): remove infrastructure option from hpc config --- nf_core/configs/create/hpccustomisation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index e46448f666..a1b5a6f345 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -185,7 +185,6 @@ def on_button_pressed(self, event: Button.Pressed) -> None: this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) new_config[text_input.field_id] = this_input.value - new_config["is_infrastructure"] = True if not validation_result.is_valid: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: From a22e0ef2383b2997d9f6f2ff0ee36d9b49b8955b Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 15:13:55 +1100 Subject: [PATCH 088/121] fix(utils): remove trailing whitespace --- nf_core/configs/create/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 6939ecd33b..c5037ada92 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -40,7 +40,7 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: class ConfigsCreateConfig(BaseModel): """Pydantic model for the nf-core configs create config.""" - is_infrastructure: Optional[bool] = False + is_infrastructure: Optional[bool] = False """ Config variable to define if this is infrastructure or pipeline """ config_pipeline_name: Optional[str] = None """ The name of the pipeline """ From 98d78b96483a81b82d59d3ef18db6d9d2bf0da6d Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 16:05:34 +1100 Subject: [PATCH 089/121] feat(serial): add withName and withLabel to config --- nf_core/configs/create/utils.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 58849de6e2..4eb3830a32 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -169,15 +169,29 @@ def serial_pipeline(self): } for named in self.named_process_resources.keys(): - ret[named] = self.named_process_resources[named] - + ret["process"][f"withName: '{named}'"] = { + "cpus": self.named_process_resources[named]["custom_process_ncpus"], + "memory": self.named_process_resources[named]["custom_process_memgb"], + "time": self.named_process_resources[named]["custom_process_hours"] + 'h' + } + if "custom_process_queue" in self.named_process_resources[named].keys(): + ret["process"][f"withName: '{named}'"]["queue"] = self.named_process_resources[named]["custom_process_queue"] + if "executor" in self.named_process_resources[named].keys(): + ret["process"][f"withName: '{named}'"]["executor"] = self.named_process_resources[named]["custom_process_queue"] for labelled in self.labelled_process_resources.keys(): - ret[labelled] = self.labelled_process_resources[labelled] - + ret["process"][f"withLabel: '{named}'"] = { + "cpus": self.labelled_process_resources[labelled]["custom_process_ncpus"], + "memory": self.labelled_process_resources[labelled]["custom_process_memgb"], + "time": self.labelled_process_resources[labelled]["custom_process_hours"] + 'h' + } + if "custom_process_queue" in self.labelled_process_resources[labelled].keys(): + ret["process"][f"withLabel: '{named}'"]["queue"] = self.labelled_process_resources[labelled]["custom_process_queue"] + if "executor" in self.labelled_process_resources[labelled].keys(): + ret["process"][f"withLabel: '{named}'"]["executor"] = self.labelled_process_resources[labelled]["executor"] return ret def serial(self): - if self.general_config_type != "pipeline": + if self.is_infrastructure: return self.serial_hpc() else: return self.serial_pipeline() From 7bb3ad048334e77e862ec9e98c84b1f23d7ae215 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 16:07:53 +1100 Subject: [PATCH 090/121] fix(serial): change labelled --- nf_core/configs/create/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 4eb3830a32..73f71cf48e 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -185,9 +185,9 @@ def serial_pipeline(self): "time": self.labelled_process_resources[labelled]["custom_process_hours"] + 'h' } if "custom_process_queue" in self.labelled_process_resources[labelled].keys(): - ret["process"][f"withLabel: '{named}'"]["queue"] = self.labelled_process_resources[labelled]["custom_process_queue"] + ret["process"][f"withLabel: '{labelled}'"]["queue"] = self.labelled_process_resources[labelled]["custom_process_queue"] if "executor" in self.labelled_process_resources[labelled].keys(): - ret["process"][f"withLabel: '{named}'"]["executor"] = self.labelled_process_resources[labelled]["executor"] + ret["process"][f"withLabel: '{labelled}'"]["executor"] = self.labelled_process_resources[labelled]["executor"] return ret def serial(self): From a602113f8507acd91aaa1a4a533ff2b9528e40d6 Mon Sep 17 00:00:00 2001 From: nbtm-sh Date: Fri, 13 Mar 2026 17:50:16 +1100 Subject: [PATCH 091/121] fix(config): fix error when creating withLabel entries --- nf_core/configs/create/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 73f71cf48e..8d7ad9d61e 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -163,8 +163,8 @@ def serial_pipeline(self): }, "process": { "cpus": self.default_process_ncpus, - "memory": self.default_process_memgb, - "time": self.default_process_hours + "memory": self.default_process_memgb + "GB", + "time": self.default_process_hours + "h" } } @@ -179,7 +179,7 @@ def serial_pipeline(self): if "executor" in self.named_process_resources[named].keys(): ret["process"][f"withName: '{named}'"]["executor"] = self.named_process_resources[named]["custom_process_queue"] for labelled in self.labelled_process_resources.keys(): - ret["process"][f"withLabel: '{named}'"] = { + ret["process"][f"withLabel: '{labelled}'"] = { "cpus": self.labelled_process_resources[labelled]["custom_process_ncpus"], "memory": self.labelled_process_resources[labelled]["custom_process_memgb"], "time": self.labelled_process_resources[labelled]["custom_process_hours"] + 'h' From 9dc064051c997e3ce0d724e1d312483c42e02e45 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 24 Mar 2026 10:34:41 +1100 Subject: [PATCH 092/121] add option to drop null values from configs, fix wrong executor value in pipeline configs, add missing config settings --- nf_core/configs/create/create.py | 2 +- nf_core/configs/create/serial.py | 10 +-- nf_core/configs/create/utils.py | 105 +++++++++++++++++++------------ 3 files changed, 71 insertions(+), 46 deletions(-) diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 486c798513..3be80d4e24 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -357,7 +357,7 @@ def write_to_file(self): else: raise ValueError(f'Invalid config type: {self.config_type}') - serial_data = NextflowSerial.dumps(self.template_config.serial()) + serial_data = NextflowSerial.dumps(self.template_config.serial(), drop_null=True) with open(filename, "w+") as file: ## Write params diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index f1f2108e03..74b21c3854 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -25,15 +25,17 @@ def _stringify(data: Optional[Any], quote: Optional[bool] = True) -> str: return str(data) # Fallback on the Python str function if no matches @staticmethod - def dumps(data_dict: Optional[Any], tab_indent: Optional[int] = 4, current_indent: Optional[int] = 0, end: Optional[str] = '\n', indent_start: Optional[bool] = True, one_line: Optional[bool] = False) -> str: + def dumps(data_dict: Optional[Any], tab_indent: Optional[int] = 4, current_indent: Optional[int] = 0, end: Optional[str] = '\n', indent_start: Optional[bool] = True, one_line: Optional[bool] = False, drop_null: Optional[bool] = False) -> str: """Recursive function to create configuration file""" output = "" if isinstance(data_dict, dict): for k, v in data_dict.items(): + if drop_null and v is None: + continue if isinstance(v, dict): output += " "*current_indent*int(indent_start) output += f"{k} {{\n" - output += NextflowSerial.dumps(v, tab_indent = tab_indent, current_indent = current_indent + tab_indent, end = end) + output += NextflowSerial.dumps(v, tab_indent = tab_indent, current_indent = current_indent + tab_indent, end = end, drop_null = drop_null) output += " "*current_indent output += f"}}{end}" elif isinstance(v, list): @@ -43,9 +45,9 @@ def dumps(data_dict: Optional[Any], tab_indent: Optional[int] = 4, current_inden for i in v: oneliner = bool(len(i.keys()) != 1) if isinstance(i, dict): - vi.append("{"*oneliner + NextflowSerial.dumps(i, tab_indent, current_indent=0, end = '', indent_start = False, one_line = not oneliner) + "}"*oneliner) + vi.append("{"*oneliner + NextflowSerial.dumps(i, tab_indent, current_indent=0, end = '', indent_start = False, one_line = not oneliner, drop_null = drop_null) + "}"*oneliner) continue - vi.append(NextflowSerial.dumps(i, tab_indent, current_indent=0, end = end)) + vi.append(NextflowSerial.dumps(i, tab_indent, current_indent=0, end = end, drop_null = drop_null)) output += ", ".join(vi) output += f"]{end}" else: diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 8d7ad9d61e..e028980233 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -125,30 +125,61 @@ def __init__(self, /, **data: Any) -> None: context=_init_context_var.get(), ) + def serial_params(self): + # Determine contact info + contact = '' + if self.config_profile_contact: + contact = self.config_profile_contact + if self.config_profile_handle: + contact += f' ({self.config_profile_handle})' + elif self.config_profile_handle: + contact = self.config_profile_handle + else: + contact = None + ret = { + "params": { + "config_profile_contact": contact, + "config_profile_description": self.config_profile_description or None, + "config_profile_url": self.config_profile_url or None, + "igenomes_base": self.igenomes_cachedir or None, + }, + } + return ret + def serial_hpc(self): """Returns a dictionary of the config""" + # Get params section + params = self.serial_params() + # Determine modules to load + modules_to_load = self.container_system if self.module and self.container_system else '' + if self.module_system: + if modules_to_load: + modules_to_load += ' ' + modules_to_load += re.sub(r'\s+', ':', self.module_system) ret = { - "params": { - "config_profile_contact": self.config_profile_contact, - "config_profile_description": self.config_profile_description, - "config_profile_url": self.config_profile_url, - "igenomes_base": self.igenomes_cachedir + **params, + "executor": { + "queueStatInterval": str(float(self.queue_stat_interval)) + "m" if self.queue_stat_interval else None, + "queueSize": int(self.queue_size) or None, + "pollInterval": str(float(self.poll_interval)) + "m" if self.poll_interval else None, + "submitRateLimit": str(int(self.submit_rate)) + "min" if self.submit_rate else None, }, "process": { - "executor": self.scheduler, - "queue": self.queue, + "executor": self.scheduler or None, + "queue": self.queue or None, "resourceLimits": [ - {"cpus": int(self.cpus)}, - {"memory": self.memory + "GB"}, - {"time": str(float(self.time)) + "h"} + {"cpus": int(self.cpus) if self.cpus else None}, + {"memory": str(float(self.memory)) + "GB" if self.memory else None}, + {"time": str(float(self.time)) + "h" if self.time else None} ], - "scratch": self.scratch_dir, - "maxRetries": int(self.retries), + "scratch": self.scratch_dir or None, + "maxRetries": int(self.retries) or None, + "module": modules_to_load or None, }, self.container_system: { "enabled": True, - "cacheDir": self.cachedir, - "autoMounts": True + "cacheDir": self.cachedir or None, + "autoMounts": True if self.container_system in ['singularity', 'apptainer'] else None }, "cleanup": self.delete_work_dir } @@ -157,37 +188,29 @@ def serial_hpc(self): def serial_pipeline(self): """Returns a dictionary of the pipeline config""" + # Get params section + params = self.serial_params() ret = { - "params": { - "config_profile_description": self.config_profile_description, - }, + **params, "process": { - "cpus": self.default_process_ncpus, - "memory": self.default_process_memgb + "GB", - "time": self.default_process_hours + "h" + "cpus": int(self.default_process_ncpus) if self.default_process_ncpus else None, + "memory": str(float(self.default_process_memgb)) + "GB" if self.default_process_memgb else None, + "time": str(float(self.default_process_hours)) + "h" if self.default_process_hours else None } } - - for named in self.named_process_resources.keys(): - ret["process"][f"withName: '{named}'"] = { - "cpus": self.named_process_resources[named]["custom_process_ncpus"], - "memory": self.named_process_resources[named]["custom_process_memgb"], - "time": self.named_process_resources[named]["custom_process_hours"] + 'h' - } - if "custom_process_queue" in self.named_process_resources[named].keys(): - ret["process"][f"withName: '{named}'"]["queue"] = self.named_process_resources[named]["custom_process_queue"] - if "executor" in self.named_process_resources[named].keys(): - ret["process"][f"withName: '{named}'"]["executor"] = self.named_process_resources[named]["custom_process_queue"] - for labelled in self.labelled_process_resources.keys(): - ret["process"][f"withLabel: '{labelled}'"] = { - "cpus": self.labelled_process_resources[labelled]["custom_process_ncpus"], - "memory": self.labelled_process_resources[labelled]["custom_process_memgb"], - "time": self.labelled_process_resources[labelled]["custom_process_hours"] + 'h' - } - if "custom_process_queue" in self.labelled_process_resources[labelled].keys(): - ret["process"][f"withLabel: '{labelled}'"]["queue"] = self.labelled_process_resources[labelled]["custom_process_queue"] - if "executor" in self.labelled_process_resources[labelled].keys(): - ret["process"][f"withLabel: '{labelled}'"]["executor"] = self.labelled_process_resources[labelled]["executor"] + # Get custom process resources + for selector in ['withName', 'withLabel']: + custom_resources_dict = self.named_process_resources if selector == 'withName' else self.labelled_process_resources + for process_id, process_resources in custom_resources_dict.items(): + ret["process"][f"{selector}: '{process_id}'"] = { + "cpus": int(process_resources["custom_process_ncpus"]) if process_resources["custom_process_ncpus"] else None, + "memory": str(float(process_resources["custom_process_memgb"])) + "GB" if process_resources["custom_process_memgb"] else None, + "time": str(float(process_resources["custom_process_hours"])) + 'h' if process_resources["custom_process_hours"] else None + } + if "custom_process_queue" in process_resources: + ret["process"][f"{selector}: '{process_id}'"]["queue"] = process_resources["custom_process_queue"] if process_resources["custom_process_queue"] else None + if "executor" in process_resources: + ret["process"][f"{selector}: '{process_id}'"]["executor"] = process_resources["executor"] if process_resources["executor"] else None return ret def serial(self): From 2cc93cca7ee5229afb42f55205bd28b47379dac0 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 24 Mar 2026 11:01:46 +1100 Subject: [PATCH 093/121] handle unset custom resources --- nf_core/configs/create/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index e028980233..78a415059e 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -201,6 +201,8 @@ def serial_pipeline(self): # Get custom process resources for selector in ['withName', 'withLabel']: custom_resources_dict = self.named_process_resources if selector == 'withName' else self.labelled_process_resources + if not custom_resources_dict: + continue for process_id, process_resources in custom_resources_dict.items(): ret["process"][f"{selector}: '{process_id}'"] = { "cpus": int(process_resources["custom_process_ncpus"]) if process_resources["custom_process_ncpus"] else None, From 2939e91ca8c907a0fb29ed8d36e52bdefcf1b12e Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 25 Mar 2026 10:33:17 +1100 Subject: [PATCH 094/121] fix linting errors --- nf_core/__main__.py | 6 +- nf_core/configs/create/__init__.py | 8 +- nf_core/configs/create/basicdetails.py | 6 +- nf_core/configs/create/create.py | 352 +----------------- nf_core/configs/create/defaultprocessres.py | 11 +- nf_core/configs/create/final.py | 18 +- nf_core/configs/create/finalinfradetails.py | 45 ++- nf_core/configs/create/hpccustomisation.py | 31 +- nf_core/configs/create/multiprocessres.py | 81 ++-- .../configs/create/pipelineconfigquestion.py | 63 +--- nf_core/configs/create/serial.py | 60 ++- nf_core/configs/create/utils.py | 164 ++++---- 12 files changed, 242 insertions(+), 603 deletions(-) diff --git a/nf_core/__main__.py b/nf_core/__main__.py index a8011771b2..46c5035423 100644 --- a/nf_core/__main__.py +++ b/nf_core/__main__.py @@ -1886,7 +1886,7 @@ def configs(ctx): # by means other than the `if` block below) ctx.ensure_object(dict) - + # nf-core configs create @configs.command("create") @click.pass_context @@ -1904,7 +1904,7 @@ def create_configs(ctx): except UserWarning as e: log.error(e) sys.exit(1) - + # nf-core test-dataset subcommands @nf_core_cli.group(aliases=["t", "td", "tds", "test-datasets"]) @@ -1979,7 +1979,7 @@ def command_test_datasets_list_branches(ctx): """ test_datasets_list_branches(ctx) - + ## DEPRECATED commands since v3.0.0 diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 8c18cd0e3d..97905ee53a 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -14,15 +14,15 @@ ## nf-core question page (screen) imports from nf_core.configs.create.basicdetails import BasicDetails from nf_core.configs.create.configtype import ChooseConfigType +from nf_core.configs.create.defaultprocessres import DefaultProcess from nf_core.configs.create.final import FinalScreen from nf_core.configs.create.finalinfradetails import FinalInfraDetails from nf_core.configs.create.hpccustomisation import HpcCustomisation from nf_core.configs.create.hpcquestion import ChooseHpc +from nf_core.configs.create.multiprocessres import MultiNamedProcessConfig, MultiLabelledProcessConfig from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig -from nf_core.configs.create.welcome import WelcomeScreen from nf_core.configs.create.pipelineconfigquestion import PipelineConfigQuestion -from nf_core.configs.create.defaultprocessres import DefaultProcess -from nf_core.configs.create.multiprocessres import MultiNamedProcessConfig, MultiLabelledProcessConfig +from nf_core.configs.create.welcome import WelcomeScreen ## General utilities from nf_core.utils import LoggingConsole @@ -133,5 +133,5 @@ def get_context(self): return { "is_nfcore": self.NFCORE_CONFIG, "is_infrastructure": self.CONFIG_TYPE == "infrastructure", - "is_hpc": self.INFRA_ISHPC + "is_hpc": self.INFRA_ISHPC, } diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 3f595f252d..6e6c99b5d9 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -9,11 +9,7 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown -from nf_core.configs.create.utils import ( - ConfigsCreateConfig, - TextInput, - init_context -) ## TODO Move somewhere common? +from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context from nf_core.utils import add_hide_class, remove_hide_class config_exists_warn = """ diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 3be80d4e24..17ef2513e2 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -2,14 +2,15 @@ nf-core organization specification. """ -from nf_core.configs.create.utils import ConfigsCreateConfig, generate_config_entry -from nf_core.configs.create.serial import NextflowSerial -from re import sub from pathlib import Path +from re import sub +from nf_core.configs.create.serial import NextflowSerial +from nf_core.configs.create.utils import ConfigsCreateConfig class ConfigCreate: - def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir: Path | str = Path('.')): + + def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir: Path | str = Path(".")): self.template_config = template_config self.config_type = config_type config_dir_path = config_dir if isinstance(config_dir, Path) else Path(config_dir) @@ -18,349 +19,22 @@ def __init__(self, template_config: ConfigsCreateConfig, config_type: str, confi config_dir_path.mkdir(parents=True, exist_ok=True) self.config_dir = config_dir_path - def construct_info_params(self): - final_params = {} - contact = self.template_config.config_profile_contact - handle = self.template_config.config_profile_handle - description = self.template_config.config_profile_description - url = self.template_config.config_profile_url - igenomes = self.template_config.igenomes_cachedir - - if contact: - if handle: - config_contact = contact + " (" + handle + ")" - else: - config_contact = contact - final_params["config_profile_contact"] = config_contact - elif handle: - final_params["config_profile_contact"] = handle - - if description: - final_params["config_profile_description"] = description - - if url: - final_params["config_profile_url"] = url - - if igenomes: - final_params["igenomes_base"] = igenomes - - return final_params - - def construct_params_str(self): - info_params = self.construct_info_params() - - info_params_str_list = [ - f' {key} = "{value}"' - for key, value in info_params.items() - if value - ] - - params_section = [ - 'params {', - '\n', - *info_params_str_list, - '\n', - '}', - ] - - params_section_str = '\n'.join(params_section) + '\n\n' - return sub(r'\n\n\n+', '\n\n', params_section_str) - - def get_resource_strings(self, cpus, memory, hours, queue='', executor='', prefix=''): - cpus_str = '' - if cpus: - cpus_int = int(cpus) - cpus_str = f'cpus = {cpus_int}' - - memory_str = '' - if memory: - memory_int = int(memory) - memory_str = f'memory = {memory_int}.GB' - - time_str = '' - if hours: - time_h = float(hours) - if time_h.is_integer(): - time_h = int(time_h) - time_str = f"time = {time_h}.h" - else: - time_m = int(time_h * 60) - time_str = f"time = {time_m}.m" - - queue_str = '' - if queue: - queue_str = f"queue = '{queue}'" - - executor_str = '' - if executor: - executor_str = f"executor = '{executor}'" - - resources = [cpus_str, memory_str, time_str, queue_str, executor_str] - return [ - f'{prefix}{res}' - for res in resources - if res - ] - - def construct_process_config_str(self): - # Construct default resources - default_resources = self.get_resource_strings( - cpus=self.template_config.default_process_ncpus, - memory=self.template_config.default_process_memgb, - hours=self.template_config.default_process_hours, - prefix=' ' - ) - - # Construct named process resources - named_resources = [] - if self.template_config.named_process_resources: - for process_name, process_resources in self.template_config.named_process_resources.items(): - named_resource_string = self.get_resource_strings( - cpus=process_resources['custom_process_ncpus'], - memory=process_resources['custom_process_memgb'], - hours=process_resources['custom_process_hours'], - queue=process_resources.get('custom_process_queue', ''), - executor=process_resources.get('executor', ''), - prefix=' ' - ) - if not named_resource_string: - continue - named_resources.append( - f" withName: '{process_name}'" + " {" - ) - named_resources.extend(named_resource_string) - named_resources.append(' }') - named_resources.append('\n') - - # Construct labelled process resources - labelled_resources = [] - if self.template_config.labelled_process_resources: - for process_label, process_resources in self.template_config.labelled_process_resources.items(): - labelled_resource_string = self.get_resource_strings( - cpus=process_resources['custom_process_ncpus'], - memory=process_resources['custom_process_memgb'], - hours=process_resources['custom_process_hours'], - queue=process_resources.get('custom_process_queue', ''), - executor=process_resources.get('executor', ''), - prefix=' ' - ) - if not labelled_resource_string: - continue - labelled_resources.append( - f" withLabel: '{process_label}'" + " {" - ) - labelled_resources.extend(labelled_resource_string) - labelled_resources.append(' }') - labelled_resources.append('\n') - - process_section = [ - 'process {', - '\n', - *default_resources, - '\n', - *named_resources, - '\n', - *labelled_resources, - '\n', - '}', - ] - - process_section_str = '\n'.join(process_section) + '\n' - - return sub(r'\n\n\n+', '\n\n', process_section_str) - - def construct_executor_config_str(self): - queue_stat_interval_str = '' - queue_size_str = '' - poll_interval_str = '' - submit_rate_str = '' - - if self.template_config.queue_stat_interval: - queue_stat_interval = float(self.template_config.queue_stat_interval) - if queue_stat_interval.is_integer(): - queue_stat_interval = int(queue_stat_interval) - queue_stat_interval_str = f' queueStatInterval = {queue_stat_interval}.m' - else: - queue_stat_interval = int(queue_stat_interval * 60) - queue_stat_interval_str = f' queueStatInterval = {queue_stat_interval}.s' - - if self.template_config.queue_size: - queue_size = int(self.template_config.queue_size) - queue_size_str = f' queueSize = {queue_size}' - - if self.template_config.poll_interval: - poll_interval = float(self.template_config.poll_interval) - if poll_interval.is_integer(): - poll_interval = int(poll_interval) - poll_interval_str = f' pollInterval = {poll_interval}.m' - else: - poll_interval = int(poll_interval * 60) - poll_interval_str = f' pollInterval = {poll_interval}.s' - - if self.template_config.submit_rate: - submit_rate = int(self.template_config.submit_rate) - submit_rate_str = f" submitRateLimit = '{submit_rate}min'" - - executor_section = [ - queue_stat_interval_str, - queue_size_str, - poll_interval_str, - submit_rate_str - ] - - executor_section = [s for s in executor_section if s] - - executor_section = [ - 'executor {', - '\n', - *executor_section, - '\n', - '}', - ] - executor_section_str = '\n'.join(executor_section) + '\n' - return sub(r'\n\n\n+', '\n\n', executor_section_str) - - def construct_container_config_str(self, container_name, cache_dir): - if not container_name: - return [] - - enabled_str = ' enabled = true' - cache_dir_str = f" cacheDir = '{cache_dir}'" if cache_dir else '' - automount_str = ' autoMounts = true' if container_name in ['singularity', 'apptainer'] else '' - - container_section = [ - enabled_str, - cache_dir_str, - automount_str, - ] - - container_section = [s for s in container_section if s] - - container_section = [ - f'{container_name}' + ' {', - '\n', - *container_section, - '\n', - '}', - ] - - container_section_str = '\n'.join(container_section) + '\n' - - return sub(r'\n\n\n+', '\n\n', container_section_str) - - def construct_infra_process_config_str(self): - modules_to_load = ( - self.template_config.container_system - if self.template_config.module and self.template_config.container_system - else '' - ) - if self.template_config.module_system: - if modules_to_load: - modules_to_load += ' ' - modules_to_load += sub(r'\s+', ':', self.template_config.module_system) - memory_str = '' - if self.template_config.memory: - memory_int = int(self.template_config.memory) - memory_str = f'{memory_int}.GB' - time_str = '' - if self.template_config.time: - time_h = float(self.template_config.time) - if time_h.is_integer(): - time_h = int(time_h) - time_str = f'{time_h}.h' - else: - time_m = int(time_h * 60) - time_str = f'{time_m}.m' - resource_limits = { - 'cpus': int(self.template_config.cpus) if self.template_config.cpus else None, - 'memory': memory_str or None, - 'time': time_str or None, - } - resource_limits = {k: v for k, v in resource_limits.items() if v} - resource_limits_str = [ - f'{key}: {value}' - for key, value in resource_limits.items() - ] - resource_limits_str = ', '.join(resource_limits_str) - resource_limits_str = f'[ {resource_limits_str} ]' - process = { - 'executor': self.template_config.scheduler or None, - 'queue': self.template_config.queue or None, - 'module': modules_to_load or None, - 'resourceLimits': resource_limits_str or None, - 'scratch': f"'{self.template_config.scratch_dir}'" if self.template_config.scratch_dir else None, - 'maxRetries': self.template_config.retries or None - } - process = {k: v for k, v in process.items() if v} - - process_list = [ - f' {key} = {value}' - for key, value in process.items() - ] - - process_section = [ - 'process {', - '\n', - *process_list, - '\n', - '}', - ] - - process_section_str = '\n'.join(process_section) + '\n' - - return sub(r'\n\n\n+', '\n\n', process_section_str) - - def construct_infra_config_str(self): - process_section_str = self.construct_infra_process_config_str() - - executor_section_str = self.construct_executor_config_str() - - container_section_str = self.construct_container_config_str( - self.template_config.container_system, - self.template_config.cachedir - ) - - cleanup = 'true' if self.template_config.delete_work_dir else 'false' - - unscoped_configs = [ - f'cleanup = {cleanup}' - ] - unscoped_configs_str = '\n'.join(unscoped_configs) + '\n' - - final_config = [ - process_section_str, - executor_section_str, - container_section_str, - unscoped_configs_str, - ] - - final_config_str = '\n\n'.join(final_config) + '\n' - - return sub(r'\n\n\n+', '\n\n', final_config_str) - - def write_to_file(self): ## File name option config_name = str(self.template_config.general_config_name).strip() - config_name_clean = sub(r'\W+', '_', config_name) - config_name_clean = sub(r'_+$', '', config_name_clean) - filename = f'{config_name_clean}.conf' + config_name_clean = sub(r"\W+" "_", config_name) + config_name_clean = sub(r"_+$", "", config_name_clean) + filename = f"{config_name_clean}.conf" filename = self.config_dir / filename - ## Collect all config entries per scope, for later checking scope needs to be written - params_section_str = self.construct_params_str() - - if self.config_type == 'pipeline': - process_section_str = self.construct_process_config_str() - elif self.config_type == 'infrastructure': - process_section_str = self.construct_infra_config_str() - else: - raise ValueError(f'Invalid config type: {self.config_type}') + if not ( + self.config_type == "pipeline" or + self.config_type == "infrastructure" + ): + raise ValueError(f"Invalid config type: {self.config_type}") serial_data = NextflowSerial.dumps(self.template_config.serial(), drop_null=True) with open(filename, "w+") as file: ## Write params file.write(serial_data) - #file.write(params_section_str) - #file.write(process_section_str) diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index ffdaf3727c..69e21a6419 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -4,16 +4,11 @@ from textual import on from textual.app import ComposeResult -from textual.containers import Center, Horizontal +from textual.containers import Center from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown -from nf_core.configs.create.utils import ( - ConfigsCreateConfig, - TextInput, - init_context -) ## TODO Move somewhere common? -from nf_core.utils import add_hide_class, remove_hide_class +from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context class DefaultProcess(Screen): @@ -102,7 +97,7 @@ def on_back_button(self, event: Button.Pressed) -> None: blank_config = {} for text_input in self.query("TextInput"): if getattr(self.parent.TEMPLATE_CONFIG, text_input.field_id, None): - blank_config[text_input.field_id] = '' + blank_config[text_input.field_id] = "" try: with init_context(self.parent.get_context()): # Update the existing config with the blank values diff --git a/nf_core/configs/create/final.py b/nf_core/configs/create/final.py index 9ba08e56f6..3908404975 100644 --- a/nf_core/configs/create/final.py +++ b/nf_core/configs/create/final.py @@ -2,12 +2,10 @@ from textual.app import ComposeResult from textual.containers import Center from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Markdown, Input +from textual.widgets import Button, Footer, Header, Input, Markdown -from nf_core.configs.create.create import ( - ConfigCreate, -) -from nf_core.configs.create.utils import TextInput, ConfigsCreateConfig, init_context +from nf_core.configs.create.create import ConfigCreate +from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context class FinalScreen(Screen): @@ -31,12 +29,16 @@ def compose(self) -> ComposeResult: yield Center( Button("Back", id="back", variant="default"), Button("Save and close!", id="close_app", variant="success"), - classes="cta" + classes="cta", ) - def _create_config(self, config_dir='.') -> None: + def _create_config(self, config_dir=".") -> None: """Create the config.""" - create_obj = ConfigCreate(template_config=self.parent.TEMPLATE_CONFIG, config_type=self.parent.CONFIG_TYPE, config_dir=config_dir) + create_obj = ConfigCreate( + template_config=self.parent.TEMPLATE_CONFIG, + config_type=self.parent.CONFIG_TYPE, + config_dir=config_dir, + ) create_obj.write_to_file() @on(Button.Pressed, "#close_app") diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 6361578095..e52be6cb61 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -1,19 +1,13 @@ import os import subprocess -from typing import Optional from textual import on from textual.app import ComposeResult from textual.containers import Center, Horizontal, Vertical from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Static, Switch, Select - -from nf_core.configs.create.utils import ( - TextInput, - ConfigsCreateConfig, - init_context, - SUPPORTED_CONTAINERS -) +from textual.widgets import Button, Footer, Header, Input, Markdown, Select, Static, Switch + +from nf_core.configs.create.utils import SUPPORTED_CONTAINERS, ConfigsCreateConfig, TextInput, init_context from nf_core.utils import add_hide_class, remove_hide_class @@ -37,16 +31,16 @@ def compose(self) -> ComposeResult: yield Markdown(markdown_intro) self.container_system_list = self._get_container_systems() self.container_system = self.container_system_list[0] if self.container_system_list else None - + self.container_system_list = SUPPORTED_CONTAINERS self.container_system = self.container_system_list[0] # default - + yield Select( - [(c, c) for c in self.container_system_list], + [(c, c) for c in self.container_system_list], prompt="Select container system", id="container_system", - value=self.container_system, #sets default - ) + value=self.container_system, # sets default + ) yield Markdown("## Maximum resources") with Horizontal(): yield TextInput( @@ -96,7 +90,10 @@ def compose(self) -> ComposeResult: "/path/to/cache/dir", "Define a global cache direcotry.", classes="", - default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") if self.container_system is not None else "", + default=self._get_set_directory( + f"NXF_{self.container_system.upper()}_CACHEDIR") + if self.container_system is not None + else "", ) yield TextInput( "igenomes_cachedir", @@ -131,7 +128,7 @@ def compose(self) -> ComposeResult: ) def _get_container_systems(self) -> list[str]: - """Get the available container systems to use for software handling. """ + """Get the available container systems to use for software handling.""" module_system_used = self._detect_module_system() container_systems = SUPPORTED_CONTAINERS available_systems = [] @@ -165,7 +162,7 @@ def _detect_module_system(self) -> bool: return False return True - def _get_set_directory(self, dir: str) -> Optional[str]: + def _get_set_directory(self, dir: str) -> str | None: """Get the available cache directories""" if dir: set_dir = os.environ.get(dir) @@ -174,7 +171,7 @@ def _get_set_directory(self, dir: str) -> Optional[str]: return None @on(Select.Changed, "#container_system") - def get_container_system(self,event: Select.Changed) -> None: + def get_container_system(self, event: Select.Changed) -> None: """Get the container system from dropdown.""" self.container_system = event.value cachedir_text_input = self.query_one("#cachedir") @@ -183,7 +180,7 @@ def get_container_system(self,event: Select.Changed) -> None: remove_hide_class(self.parent, "define-global-cache-dir") if not cachedir_input.value: cachedir_path = self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") - cachedir_input.value = cachedir_path or '' + cachedir_input.value = cachedir_path or "" else: add_hide_class(self.parent, "define-global-cache-dir") @@ -192,9 +189,9 @@ def on_finish_button(self, event: Button.Pressed) -> None: """Save fields to the config.""" new_config = {} - #collect dropdown value + # collect dropdown value select = self.query_one("#container_system", Select) - new_config['container_system'] = select.value + new_config["container_system"] = select.value for text_input in self.query("TextInput"): if "hide" in text_input.classes: @@ -209,8 +206,8 @@ def on_finish_button(self, event: Button.Pressed) -> None: # collect switch value delete_work_switch = self.query_one("#toggle-delete-work") - new_config['delete_work_dir'] = delete_work_switch.value - new_config['module'] = self._detect_module_system() + new_config["delete_work_dir"] = delete_work_switch.value + new_config["module"] = self._detect_module_system() # Validate and update the config try: @@ -230,7 +227,7 @@ def on_back_button(self, event: Button.Pressed) -> None: blank_config = {} for text_input in self.query("TextInput"): if getattr(self.parent.TEMPLATE_CONFIG, text_input.field_id, None): - blank_config[text_input.field_id] = '' + blank_config[text_input.field_id] = "" try: with init_context(self.parent.get_context()): # Update the existing config with the blank values diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 39c5675c1b..42297f2810 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -1,19 +1,14 @@ -import subprocess -import json import io -from typing import Optional +import json +import subprocess +from textual import on from textual.app import ComposeResult from textual.containers import Center, Horizontal from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Markdown, Input -from textual import on +from textual.widgets import Button, Footer, Header, Input, Markdown -from nf_core.configs.create.utils import ( - TextInput, - init_context, - ConfigsCreateConfig -) +from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context markdown_intro = """ # Configure the options for your HPC @@ -66,7 +61,7 @@ def compose(self) -> ComposeResult: classes="cta", ) - def _get_scheduler(self) -> Optional[str]: + def _get_scheduler(self) -> str | None: """Get the used scheduler""" try: subprocess.run(["sinfo", "--version"]) @@ -89,13 +84,13 @@ def _get_scheduler(self) -> Optional[str]: pass except subprocess.CalledProcessError: pass - return 'local' + return "local" - def _get_queues(self, scheduler: Optional[str]) -> list[str]: + def _get_queues(self, scheduler: str | None) -> list[str]: """Get the available queues to use for the jobs""" if scheduler == "slurm": try: - queues = subprocess.check_output(["sinfo", "-h", "-o", '%P']).decode("utf-8") + queues = subprocess.check_output(["sinfo", "-h", "-o", "%P"]).decode("utf-8") # Remove default * flag return [i.strip().replace("*", "") for i in queues.split("\n") if i] except subprocess.CalledProcessError: @@ -114,7 +109,7 @@ def _get_queues(self, scheduler: Optional[str]) -> list[str]: pass return [] - def _get_default_queue(self, scheduler: Optional[str]) -> str: + def _get_default_queue(self, scheduler: str | None) -> str: """Get the default queue for the scheduler""" if scheduler == "slurm": try: @@ -133,7 +128,7 @@ def _slurm_get_default_queue(self) -> str: """Get the default queue for Slurm""" config = {} # TODO: If slurm is built from source, the config file path can be different - with open("/etc/slurm/slurm.conf", "r") as fp: + with open("/etc/slurm/slurm.conf") as fp: config = self._parse_slurm_config(fp) for conf in config: @@ -148,7 +143,7 @@ def _pbs_get_default_queue(self) -> str: config = self._pbs_parse_config(pbs_raw_config) return config["default_queue"] - def _parse_slurm_config(self, fp: Optional[io.TextIOWrapper]) -> list[dict]: + def _parse_slurm_config(self, fp: io.TextIOWrapper | None) -> list[dict]: """Parse the Slurm configuration file""" config = [] for line in fp.readlines(): @@ -157,7 +152,7 @@ def _parse_slurm_config(self, fp: Optional[io.TextIOWrapper]) -> list[dict]: config.append({i[0]: i[1] for i in tokens}) return config - def _pbs_parse_config(self, raw: Optional[str]) -> dict: + def _pbs_parse_config(self, raw: str | None) -> dict: """Parse the PBS configuration file""" config = {} for line in [r.strip() for r in raw.split("\n")]: diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index 4444c3b20a..c61993e39d 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -1,21 +1,13 @@ """Get information about which process/label the user wants to configure.""" -from textwrap import dedent - from textual import on from textual.app import ComposeResult -from textual.containers import Center, HorizontalGroup, VerticalScroll, Vertical, Horizontal -from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label, Static +from textual.containers import Center, Horizontal, HorizontalGroup, Vertical, VerticalScroll from textual.events import Mount, ScreenResume -from nf_core.utils import add_hide_class, remove_hide_class +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Label, Markdown, Static, Switch -from nf_core.configs.create.utils import ( - ConfigsCreateConfig, - TextInput, - init_context -) ## TODO Move somewhere common? -from nf_core.utils import add_hide_class, remove_hide_class +from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context class ProcessConfig(HorizontalGroup): @@ -23,7 +15,7 @@ class ProcessConfig(HorizontalGroup): def __init__(self, selector: str, hpc: bool) -> None: super().__init__() - assert selector in ['name', 'label'] + assert selector in ["name", "label"] self.selector = selector self.hpc = hpc self.use_local_exec = False @@ -64,46 +56,32 @@ def compose(self) -> ComposeResult: "", classes=("column" + (" hide" if not self.hpc else "")), ) - with Vertical(classes=("labelled-toggle column" + (" hide" if not self.hpc else "")), id="toggle_use_local_exec_group"): - yield Label( - "Use local executor?", - id="toggle_use_local_exec_label", - classes="field_help" - ) + with Vertical( + classes=("labelled-toggle column" + (" hide" if not self.hpc else "")), id="toggle_use_local_exec_group" + ): + yield Label("Use local executor?", id="toggle_use_local_exec_label", classes="field_help") with Horizontal(classes="labelled-toggle"): - yield Switch( - id="toggle_use_local_exec", - value=self.use_local_exec - ) + yield Switch(id="toggle_use_local_exec", value=self.use_local_exec) yield Label( "Yes" if self.use_local_exec else "No", id="toggle_use_local_exec_state_label", - classes="toggle_use_local_exec_state_label" + classes="toggle_use_local_exec_state_label", ) yield Static(classes="labelled-toggle-filler") # Filler with Vertical(classes="labelled-toggle"): - yield Label( - "Remove", - id="remove-process-config", - classes="field_help" - ) - yield Button( - "-", - id="remove", - variant="error", - classes="remove-process-button" - ) + yield Label("Remove", id="remove-process-config", classes="field_help") + yield Button("-", id="remove", variant="error", classes="remove-process-button") yield Static(classes="labelled-toggle-filler") # Filler @on(Switch.Changed, "#toggle_use_local_exec") def on_toggle_switch(self, event: Switch.Changed) -> None: - """ Handle toggling the local executor switch """ + """Handle toggling the local executor switch""" self.use_local_exec = event.value # Update the switch label for label in self.query(Label): - if label.id == f'{event.switch.id}_state_label': + if label.id == f"{event.switch.id}_state_label": label.update("Yes" if event.value else "No") @on(Button.Pressed, "#remove") @@ -138,16 +116,14 @@ def _set_next_screen(self, next_screen: str) -> None: def compose(self) -> ComposeResult: yield Header() yield Footer() - yield Markdown(f'# {self.title}') + yield Markdown(f"# {self.title}") yield VerticalScroll( ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), - id="configs" - ) - yield Center( - Button("Add another process", id="another", variant="success") + id="configs", ) + yield Center(Button("Add another process", id="another", variant="success")) yield Center( Button("Back", id="back", variant="default"), Button("Skip", id="skip", variant="default"), @@ -157,7 +133,7 @@ def compose(self) -> ComposeResult: @on(Button.Pressed, "#another") def add_config(self) -> None: - new_config = ProcessConfig(selector='name', hpc=self.parent.PIPE_CONF_HPC) + new_config = ProcessConfig(selector="name", hpc=self.parent.PIPE_CONF_HPC) self.query_one("#configs").mount(new_config) @on(Button.Pressed, "#next") @@ -173,23 +149,24 @@ def save_and_load_next_screen(self) -> None: validation_result = this_input.validate(this_input.value) tmp_config[text_input.field_id] = this_input.value if not validation_result.is_valid: - text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) + text_input.query_one(".validation_msg").update( + "\n".join(validation_result.failure_descriptions) + ) else: text_input.query_one(".validation_msg").update("") if self.parent.PIPE_CONF_HPC: local_exec_switch = config_widget.query_one("#toggle_use_local_exec") if local_exec_switch.value: - tmp_config['executor'] = 'local' + tmp_config['executor'] = "local" # Validate the config with init_context(self.parent.get_context()): ConfigsCreateConfig(**tmp_config) # Add to the config list config_list.append(tmp_config) # Add to the final config - key = self.config_key new_config = {self.config_key: {}} for tmp_config in config_list: - process_id = tmp_config.get('custom_process_id') + process_id = tmp_config.get("custom_process_id") new_config[self.config_key][process_id] = tmp_config self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) # Push the next screen @@ -211,9 +188,7 @@ def update_hide_class(self) -> None: class MultiNamedProcessConfig(MultiProcessConfig): def __init__(self) -> None: super().__init__( - title='Configure processes by name', - selector_type='name', - config_key='named_process_resources' + title="Configure processes by name", selector_type="name", config_key="named_process_resources" ) @on(Mount) @@ -228,12 +203,10 @@ def set_next_screen(self) -> None: class MultiLabelledProcessConfig(MultiProcessConfig): def __init__(self) -> None: super().__init__( - title='Configure processes by label', - selector_type='label', - config_key='labelled_process_resources' + title='Configure processes by label', selector_type='label', config_key='labelled_process_resources' ) @on(Mount) @on(ScreenResume) def set_next_screen(self) -> None: - self._set_next_screen('final') \ No newline at end of file + self._set_next_screen("final") \ No newline at end of file diff --git a/nf_core/configs/create/pipelineconfigquestion.py b/nf_core/configs/create/pipelineconfigquestion.py index 0e87c6957d..a7b0fec28c 100644 --- a/nf_core/configs/create/pipelineconfigquestion.py +++ b/nf_core/configs/create/pipelineconfigquestion.py @@ -6,16 +6,7 @@ from textual.app import ComposeResult from textual.containers import Center, Horizontal from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown, Switch, Label -from enum import Enum -from nf_core.utils import add_hide_class, remove_hide_class - -from nf_core.configs.create.utils import ( - ConfigsCreateConfig, - TextInput, - init_context -) ## TODO Move somewhere common? -from nf_core.utils import add_hide_class, remove_hide_class +from textual.widgets import Button, Footer, Header, Label, Markdown, Switch class PipelineConfigQuestion(Screen): @@ -39,44 +30,26 @@ def compose(self) -> ComposeResult: ) ) with Horizontal(): - yield Label( - "Configure default process resources?", - id="toggle_configure_defaults_label" - ) + yield Label("Configure default process resources?", id="toggle_configure_defaults_label") yield Switch( id="toggle_configure_defaults", value=self.config_defaults, ) - yield Label( - "Yes" if self.config_defaults else "No", - id="toggle_configure_defaults_state_label" - ) + yield Label("Yes" if self.config_defaults else "No", id="toggle_configure_defaults_state_label") with Horizontal(): - yield Label( - "Configure specific named processes?", - id="toggle_configure_names_label" - ) + yield Label("Configure specific named processes?", id="toggle_configure_names_label") yield Switch( id="toggle_configure_names", value=self.config_named_processes, ) - yield Label( - "Yes" if self.config_named_processes else "No", - id="toggle_configure_names_state_label" - ) + yield Label("Yes" if self.config_named_processes else "No", id="toggle_configure_names_state_label") with Horizontal(): - yield Label( - "Configure labels?", - id="toggle_configure_labels_label" - ) + yield Label("Configure labels?", id="toggle_configure_labels_label") yield Switch( id="toggle_configure_labels", value=self.config_labels, ) - yield Label( - "Yes" if self.config_labels else "No", - id="toggle_configure_labels_state_label" - ) + yield Label("Yes" if self.config_labels else "No", id="toggle_configure_labels_state_label") yield Markdown( dedent( """ @@ -97,18 +70,12 @@ def compose(self) -> ComposeResult: ) ) with Horizontal(): - yield Label( - "Configure HPC-specific resources?", - id="toggle_configure_hpc_resources_label" - ) + yield Label("Configure HPC-specific resources?", id="toggle_configure_hpc_resources_label") yield Switch( id="toggle_configure_hpc_resources", value=self.config_hpc, ) - yield Label( - "Yes" if self.config_hpc else "No", - id="toggle_configure_hpc_resources_state_label" - ) + yield Label("Yes" if self.config_hpc else "No", id="toggle_configure_hpc_resources_state_label") yield Center( Button("Back", id="back", variant="default"), Button("Next", id="next", variant="success"), @@ -117,12 +84,12 @@ def compose(self) -> ComposeResult: @on(Switch.Changed) def on_toggle_switch(self, event: Switch.Changed) -> None: - """ Handle toggling the switches that determine which pipeline resources to configure """ + """Handle toggling the switches that determine which pipeline resources to configure""" valid_toggles = { - 'toggle_configure_defaults': 'config_defaults', - 'toggle_configure_names': 'config_named_processes', - 'toggle_configure_labels': 'config_labels', - 'toggle_configure_hpc_resources': 'config_hpc', + "toggle_configure_defaults": "config_defaults", + "toggle_configure_names": "config_named_processes", + "toggle_configure_labels": "config_labels", + "toggle_configure_hpc_resources": "config_hpc", } if event.switch.id not in valid_toggles: @@ -133,7 +100,7 @@ def on_toggle_switch(self, event: Switch.Changed) -> None: # Update the switch label for label in self.query(Label): - if label.id == f'{event.switch.id}_state_label': + if label.id == f"{event.switch.id}_state_label": label.update("Yes" if event.value else "No") # Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index 74b21c3854..a5efdf77f6 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -1,31 +1,39 @@ -from typing import Any, Optional +from typing import Any class NextflowSerial: - def __init__(self, data_dict: Optional[dict], tab_indent: Optional[int] = 4): + def __init__(self, data_dict: dict | None, tab_indent: int | None = 4): self.data_dict = data_dict - def __getitem__(self, key: Optional[Any]) -> Any: + def __getitem__(self, key: Any | None) -> Any: """Returns value from data""" return self.data_dict[key] - def __setitem__(self, key: Optional[Any], value: Optional[any]) -> None: + def __setitem__(self, key: Any | None, value: Any | None) -> None: """Sets value in data""" self.data_dict[key] = value @staticmethod - def _stringify(data: Optional[Any], quote: Optional[bool] = True) -> str: + def _stringify(data: Any | None, quote: bool | None = True) -> str: """Return nextflow compatible value""" - quote = "'"*quote + quote = "'" * quote if isinstance(data, str): return f"{quote}{data}{quote}" if isinstance(data, bool): - return 'true' if data else 'false' + return "true" if data else "false" if isinstance(data, type(None)): - return 'null' - return str(data) # Fallback on the Python str function if no matches + return "null" + return str(data) # Fallback on the Python str function if no matches @staticmethod - def dumps(data_dict: Optional[Any], tab_indent: Optional[int] = 4, current_indent: Optional[int] = 0, end: Optional[str] = '\n', indent_start: Optional[bool] = True, one_line: Optional[bool] = False, drop_null: Optional[bool] = False) -> str: + def dumps( + data_dict: Any | None, + tab_indent: int | None = 4, + current_indent: int | None = 0, + end: str | None = "\n", + indent_start: bool | None = True, + one_line: bool | None = False, + drop_null: bool | None = False, + ) -> str: """Recursive function to create configuration file""" output = "" if isinstance(data_dict, dict): @@ -33,25 +41,43 @@ def dumps(data_dict: Optional[Any], tab_indent: Optional[int] = 4, current_inden if drop_null and v is None: continue if isinstance(v, dict): - output += " "*current_indent*int(indent_start) + output += " " * current_indent * int(indent_start) output += f"{k} {{\n" - output += NextflowSerial.dumps(v, tab_indent = tab_indent, current_indent = current_indent + tab_indent, end = end, drop_null = drop_null) - output += " "*current_indent + output += NextflowSerial.dumps( + v, + tab_indent=tab_indent, + current_indent=current_indent + tab_indent, + end=end, + drop_null=drop_null, + ) + output += " " * current_indent output += f"}}{end}" elif isinstance(v, list): - output += " "*current_indent + output += " " * current_indent output += f"{k} = [" vi = [] for i in v: oneliner = bool(len(i.keys()) != 1) if isinstance(i, dict): - vi.append("{"*oneliner + NextflowSerial.dumps(i, tab_indent, current_indent=0, end = '', indent_start = False, one_line = not oneliner, drop_null = drop_null) + "}"*oneliner) + vi.append( + "{" * oneliner + + NextflowSerial.dumps( + i, + tab_indent, + current_indent=0, + end="", + indent_start=False, + one_line=not oneliner, + drop_null=drop_null, + ) + + "}" * oneliner + ) continue - vi.append(NextflowSerial.dumps(i, tab_indent, current_indent=0, end = end, drop_null = drop_null)) + vi.append(NextflowSerial.dumps(i, tab_indent, current_indent=0, end=end, drop_null=drop_null)) output += ", ".join(vi) output += f"]{end}" else: - output += " "*current_indent*indent_start + output += " " * current_indent * indent_start if one_line: # False is set here to disable quotes on strings output += f"{k}: {NextflowSerial._stringify(v, False)}{end}" diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 78a415059e..fd806b30e2 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from contextvars import ContextVar from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from pydantic import BaseModel, ConfigDict, ValidationError, ValidationInfo, field_validator from textual import on @@ -40,79 +40,79 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: class ConfigsCreateConfig(BaseModel): """Pydantic model for the nf-core configs create config.""" - is_infrastructure: Optional[bool] = False + is_infrastructure: bool | None = False """ Config variable to define if this is infrastructure or pipeline """ - config_pipeline_name: Optional[str] = None + config_pipeline_name: str | None = None """ The name of the pipeline """ - config_pipeline_path: Optional[str] = None + config_pipeline_path: str | None = None """ The path to the pipeline """ - general_config_name: Optional[str] = None + general_config_name: str | None = None """ Config name """ - config_profile_contact: Optional[str] = None + config_profile_contact: str | None = None """ Config contact name """ - config_profile_handle: Optional[str] = None + config_profile_handle: str | None = None """ Config contact GitHub handle """ - config_profile_description: Optional[str] = None + config_profile_description: str | None = None """ Config description """ - config_profile_url: Optional[str] = None + config_profile_url: str | None = None """ Config institution URL """ - default_process_ncpus: Optional[str] = None + default_process_ncpus: str | None = None """ Default number of CPUs """ - default_process_memgb: Optional[str] = None + default_process_memgb: str | None = None """ Default amount of memory """ - default_process_hours: Optional[str] = None + default_process_hours: str | None = None """ Default walltime - hours """ - custom_process_id: Optional[str] = None + custom_process_id: str | None = None """" Name or label of a process to configure """ - custom_process_ncpus: Optional[str] = None + custom_process_ncpus: str | None = None """ Number of CPUs for process """ - custom_process_memgb: Optional[str] = None + custom_process_memgb: str | None = None """ Amount of memory for process """ - custom_process_hours: Optional[str] = None + custom_process_hours: str | None = None """ Walltime for process - hours """ - custom_process_queue: Optional[str] = None + custom_process_queue: str | None = None """ Custom queue for process to override default """ - named_process_resources: Optional[dict] = None + named_process_resources: dict | None = None """ Dictionary containing custom resource requirements for named processes """ - labelled_process_resources: Optional[dict] = None + labelled_process_resources: dict | None = None """ Dictionary containing custom resource requirements for labelled processes """ - is_nfcore: Optional[bool] = None + is_nfcore: bool | None = None """ Whether the config is part of the nf-core organisation """ - savelocation: Optional[str] = None + savelocation: str | None = None """ Final location of the configuration file """ - scheduler: Optional[str] = None + scheduler: str | None = None """ The scheduler that the HPC uses """ - queue: Optional[str] = None + queue: str | None = None """ The default queue that the HPC uses """ - module_system: Optional[str] = None + module_system: str | None = None """ Modules to load when running processes """ - container_system: Optional[str] = None + container_system: str | None = None """ The container system the HPC uses """ - memory: Optional[str] = None + memory: str | None = None """ The maximum memory available to processes """ - cpus: Optional[str] = None + cpus: str | None = None """ The maximum number of CPUs available to processes """ - time: Optional[str] = None + time: str | None = None """ The maximum walltime available to processes """ - cachedir: Optional[str] = None + cachedir: str | None = None """ An environment variable to hold a custom Nextflow container cachedir """ - igenomes_cachedir: Optional[str] = None + igenomes_cachedir: str | None = None """ A cachedir for iGenomes """ - scratch_dir: Optional[str] = None + scratch_dir: str | None = None """ A scratch directory to use """ - retries: Optional[str] = None + retries: str | None = None """ Number of retries for failed jobs """ - module: Optional[bool] = False + module: bool | None = False """ Whether the infrastructure uses a module system """ - delete_work_dir: Optional[bool] = False + delete_work_dir: bool | None = False """ Whether to clean up the work directory upon successful completion """ - queue_stat_interval: Optional[str] = None + queue_stat_interval: str | None = None """ How often to check the HPC queue status. """ - queue_size: Optional[str] = None + queue_size: str | None = None """ How many jobs can be submitted to the queue at once. """ - poll_interval: Optional[str] = None + poll_interval: str | None = None """ How often to check for successful completion of processes. """ - submit_rate: Optional[str] = None + submit_rate: str | None = None """ How many jobs can be submitted per minute. """ model_config = ConfigDict(extra="allow") @@ -127,11 +127,11 @@ def __init__(self, /, **data: Any) -> None: def serial_params(self): # Determine contact info - contact = '' + contact = "" if self.config_profile_contact: contact = self.config_profile_contact if self.config_profile_handle: - contact += f' ({self.config_profile_handle})' + contact += f" ({self.config_profile_handle})" elif self.config_profile_handle: contact = self.config_profile_handle else: @@ -151,11 +151,11 @@ def serial_hpc(self): # Get params section params = self.serial_params() # Determine modules to load - modules_to_load = self.container_system if self.module and self.container_system else '' + modules_to_load = self.container_system if self.module and self.container_system else "" if self.module_system: if modules_to_load: - modules_to_load += ' ' - modules_to_load += re.sub(r'\s+', ':', self.module_system) + modules_to_load += " " + modules_to_load += re.sub(r"\s+", ":", self.module_system) ret = { **params, "executor": { @@ -170,7 +170,7 @@ def serial_hpc(self): "resourceLimits": [ {"cpus": int(self.cpus) if self.cpus else None}, {"memory": str(float(self.memory)) + "GB" if self.memory else None}, - {"time": str(float(self.time)) + "h" if self.time else None} + {"time": str(float(self.time)) + "h" if self.time else None}, ], "scratch": self.scratch_dir or None, "maxRetries": int(self.retries) or None, @@ -179,9 +179,9 @@ def serial_hpc(self): self.container_system: { "enabled": True, "cacheDir": self.cachedir or None, - "autoMounts": True if self.container_system in ['singularity', 'apptainer'] else None + "autoMounts": True if self.container_system in ["singularity", "apptainer"] else None, }, - "cleanup": self.delete_work_dir + "cleanup": self.delete_work_dir, } return ret @@ -195,28 +195,46 @@ def serial_pipeline(self): "process": { "cpus": int(self.default_process_ncpus) if self.default_process_ncpus else None, "memory": str(float(self.default_process_memgb)) + "GB" if self.default_process_memgb else None, - "time": str(float(self.default_process_hours)) + "h" if self.default_process_hours else None + "time": str(float(self.default_process_hours)) + "h" if self.default_process_hours else None, } } # Get custom process resources - for selector in ['withName', 'withLabel']: - custom_resources_dict = self.named_process_resources if selector == 'withName' else self.labelled_process_resources + for selector in ["withName", "withLabel"]: + custom_resources_dict = ( + self.named_process_resources if selector == "withName" else self.labelled_process_resources + ) if not custom_resources_dict: continue for process_id, process_resources in custom_resources_dict.items(): ret["process"][f"{selector}: '{process_id}'"] = { - "cpus": int(process_resources["custom_process_ncpus"]) if process_resources["custom_process_ncpus"] else None, - "memory": str(float(process_resources["custom_process_memgb"])) + "GB" if process_resources["custom_process_memgb"] else None, - "time": str(float(process_resources["custom_process_hours"])) + 'h' if process_resources["custom_process_hours"] else None + "cpus": ( + int(process_resources["custom_process_ncpus"]) + if process_resources["custom_process_ncpus"] + else None + ), + "memory": ( + str(float(process_resources["custom_process_memgb"])) + "GB" + if process_resources["custom_process_memgb"] + else None + ), + "time": ( + str(float(process_resources["custom_process_hours"])) + "h" + if process_resources["custom_process_hours"] + else None + ), } if "custom_process_queue" in process_resources: - ret["process"][f"{selector}: '{process_id}'"]["queue"] = process_resources["custom_process_queue"] if process_resources["custom_process_queue"] else None + ret["process"][f"{selector}: '{process_id}'"]["queue"] = ( + process_resources["custom_process_queue"] if process_resources["custom_process_queue"] else None + ) if "executor" in process_resources: - ret["process"][f"{selector}: '{process_id}'"]["executor"] = process_resources["executor"] if process_resources["executor"] else None + ret["process"][f"{selector}: '{process_id}'"]["executor"] = ( + process_resources["executor"] if process_resources["executor"] else None + ) return ret def serial(self): - if self.is_infrastructure: + if self.is_infrastructure: return self.serial_hpc() else: return self.serial_pipeline() @@ -328,7 +346,7 @@ def notempty_process_name(cls, v: str, info: ValidationInfo) -> str: @classmethod def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: """Check that integer values are either empty or positive. - + This contains the same validation as self.pos_integer_valid_infra(). However, keep infrastructure and pipeline methods decoupled for easier refactoring in future. @@ -360,7 +378,7 @@ def non_neg_float_valid(cls, v: str, info: ValidationInfo) -> str: if not vf >= 0: raise ValueError("Must be a non-negative number.") return v - + @field_validator("custom_process_queue") @classmethod def valid_custom_queue(cls, v: str, info: ValidationInfo) -> str: @@ -369,7 +387,7 @@ def valid_custom_queue(cls, v: str, info: ValidationInfo) -> str: if context and not context["is_infrastructure"]: if v.strip() == "": return v - if ' ' in v.strip(): + if " " in v.strip(): raise ValueError("Cannot contain spaces") # TODO: Any other invalid characters? return v @@ -379,7 +397,7 @@ def valid_custom_queue(cls, v: str, info: ValidationInfo) -> str: def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: """ Check that integer values are either empty or positive. - + This contains the same validation as self.pos_integer_valid(). However, keep infrastructure and pipeline methods decoupled for easier refactoring in future. @@ -445,9 +463,7 @@ def valid_scheduler(cls, v: str, info: ValidationInfo) -> str: context = info.context if context and context["is_infrastructure"] and context["is_hpc"]: if v.strip() not in SUPPORTED_SCHEDULERS: - raise ValueError( - f"Must be one of: {', '.join(SUPPORTED_SCHEDULERS)}" - ) + raise ValueError(f"Must be one of: {', '.join(SUPPORTED_SCHEDULERS)}") return v @field_validator("cachedir", "scratch_dir") @@ -465,7 +481,7 @@ def is_path_ondisk(cls, v: str, info: ValidationInfo) -> str: """ v = v.strip() if v == "": - return v #optional + return v # optional if not _PATH_PATTERN.match(v): raise ValueError( @@ -490,21 +506,13 @@ def is_path_or_uri(cls, v: str, info: ValidationInfo) -> str: "or a URI (e.g. s3://ngi-igenomes/igenomes/)" ) return v - + @field_validator("container_system") @classmethod def container_system_valid(cls, v: str, info: ValidationInfo) -> str: v = v.strip() if v != "" and v not in SUPPORTED_CONTAINERS: - raise ValueError( - f"Must be one of: {', '.join(SUPPORTED_CONTAINERS)}" - ) - return v - - @field_validator("module_system") - @classmethod - def module_system(cls, v: str, info: ValidationInfo) -> str: - #TODO: placeholder validator until functionality is finished + raise ValueError(f"Must be one of: {', '.join(SUPPORTED_CONTAINERS)}") return v @field_validator("queue_stat_interval") @@ -563,7 +571,7 @@ def compose(self) -> ComposeResult: @on(Input.Changed) @on(Input.Submitted) - def show_invalid_reasons(self, event: Union[Input.Changed, Input.Submitted]) -> None: + def show_invalid_reasons(self, event: Input.Changed | Input.Submitted) -> None: """Validate the text input and show errors if invalid.""" val_msg = self.query_one(".validation_msg") if not isinstance(val_msg, Static): @@ -591,7 +599,13 @@ def validate(self, value: str) -> ValidationResult: If it fails, return the error messages.""" try: - with init_context({"is_nfcore": NFCORE_CONFIG_GLOBAL, "is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL, "is_hpc": INFRA_ISHPC_GLOBAL}): + with init_context( + { + "is_nfcore": NFCORE_CONFIG_GLOBAL, + "is_infrastructure": CONFIG_ISINFRASTRUCTURE_GLOBAL, + "is_hpc": INFRA_ISHPC_GLOBAL, + } + ): ConfigsCreateConfig(**{f"{self.key}": value}) return self.success() except ValidationError as e: From e9d8d82dd2f5b2a3d6491e8c42831f34b9df0065 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 25 Mar 2026 11:13:11 +1100 Subject: [PATCH 095/121] fix more linting issues --- nf_core/configs/create/__init__.py | 2 +- nf_core/configs/create/create.py | 7 ++---- nf_core/configs/create/finalinfradetails.py | 10 ++++----- nf_core/configs/create/hpccustomisation.py | 8 +++---- nf_core/configs/create/multiprocessres.py | 12 +++++----- nf_core/configs/create/serial.py | 25 ++++++++++++--------- nf_core/configs/create/utils.py | 5 +++-- 7 files changed, 35 insertions(+), 34 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index 97905ee53a..dca542d2c1 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -19,7 +19,7 @@ from nf_core.configs.create.finalinfradetails import FinalInfraDetails from nf_core.configs.create.hpccustomisation import HpcCustomisation from nf_core.configs.create.hpcquestion import ChooseHpc -from nf_core.configs.create.multiprocessres import MultiNamedProcessConfig, MultiLabelledProcessConfig +from nf_core.configs.create.multiprocessres import MultiLabelledProcessConfig, MultiNamedProcessConfig from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.pipelineconfigquestion import PipelineConfigQuestion from nf_core.configs.create.welcome import WelcomeScreen diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 17ef2513e2..dcb79adb04 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -4,12 +4,12 @@ from pathlib import Path from re import sub + from nf_core.configs.create.serial import NextflowSerial from nf_core.configs.create.utils import ConfigsCreateConfig class ConfigCreate: - def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir: Path | str = Path(".")): self.template_config = template_config self.config_type = config_type @@ -27,10 +27,7 @@ def write_to_file(self): filename = f"{config_name_clean}.conf" filename = self.config_dir / filename - if not ( - self.config_type == "pipeline" or - self.config_type == "infrastructure" - ): + if not (self.config_type == "pipeline" or self.config_type == "infrastructure"): raise ValueError(f"Invalid config type: {self.config_type}") serial_data = NextflowSerial.dumps(self.template_config.serial(), drop_null=True) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index e52be6cb61..4be8942078 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -10,7 +10,6 @@ from nf_core.configs.create.utils import SUPPORTED_CONTAINERS, ConfigsCreateConfig, TextInput, init_context from nf_core.utils import add_hide_class, remove_hide_class - markdown_intro = """ # Configure the options for your infrastructure config """ @@ -90,10 +89,9 @@ def compose(self) -> ComposeResult: "/path/to/cache/dir", "Define a global cache direcotry.", classes="", - default=self._get_set_directory( - f"NXF_{self.container_system.upper()}_CACHEDIR") - if self.container_system is not None - else "", + default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") + if self.container_system is not None + else "", ) yield TextInput( "igenomes_cachedir", @@ -208,7 +206,7 @@ def on_finish_button(self, event: Button.Pressed) -> None: delete_work_switch = self.query_one("#toggle-delete-work") new_config["delete_work_dir"] = delete_work_switch.value new_config["module"] = self._detect_module_system() - + # Validate and update the config try: with init_context(self.parent.get_context()): diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 42297f2810..27e5dfe8e3 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -126,7 +126,7 @@ def _get_default_queue(self, scheduler: str | None) -> str: def _slurm_get_default_queue(self) -> str: """Get the default queue for Slurm""" - config = {} + config = None # TODO: If slurm is built from source, the config file path can be different with open("/etc/slurm/slurm.conf") as fp: config = self._parse_slurm_config(fp) @@ -134,7 +134,7 @@ def _slurm_get_default_queue(self) -> str: for conf in config: if (conf["Default"] if "Default" in conf.keys() else "NO") == "YES": return conf["PartitionName"] - + # If no default is set, use the first option return config[0]["PartitionName"] @@ -143,7 +143,7 @@ def _pbs_get_default_queue(self) -> str: config = self._pbs_parse_config(pbs_raw_config) return config["default_queue"] - def _parse_slurm_config(self, fp: io.TextIOWrapper | None) -> list[dict]: + def _parse_slurm_config(self, fp: io.TextIOWrapper) -> list[dict]: """Parse the Slurm configuration file""" config = [] for line in fp.readlines(): @@ -152,7 +152,7 @@ def _parse_slurm_config(self, fp: io.TextIOWrapper | None) -> list[dict]: config.append({i[0]: i[1] for i in tokens}) return config - def _pbs_parse_config(self, raw: str | None) -> dict: + def _pbs_parse_config(self, raw: str) -> dict: """Parse the PBS configuration file""" config = {} for line in [r.strip() for r in raw.split("\n")]: diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index c61993e39d..d19a1ec752 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -157,18 +157,18 @@ def save_and_load_next_screen(self) -> None: if self.parent.PIPE_CONF_HPC: local_exec_switch = config_widget.query_one("#toggle_use_local_exec") if local_exec_switch.value: - tmp_config['executor'] = "local" + tmp_config["executor"] = "local" # Validate the config with init_context(self.parent.get_context()): ConfigsCreateConfig(**tmp_config) # Add to the config list config_list.append(tmp_config) # Add to the final config - new_config = {self.config_key: {}} + new_config = {} for tmp_config in config_list: process_id = tmp_config.get("custom_process_id") - new_config[self.config_key][process_id] = tmp_config - self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update=new_config) + new_config[process_id] = tmp_config + self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update={self.config_key: new_config}) # Push the next screen self.parent.push_screen(self.next_screen) except ValueError: @@ -203,10 +203,10 @@ def set_next_screen(self) -> None: class MultiLabelledProcessConfig(MultiProcessConfig): def __init__(self) -> None: super().__init__( - title='Configure processes by label', selector_type='label', config_key='labelled_process_resources' + title="Configure processes by label", selector_type="label", config_key="labelled_process_resources" ) @on(Mount) @on(ScreenResume) def set_next_screen(self) -> None: - self._set_next_screen("final") \ No newline at end of file + self._set_next_screen("final") diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index a5efdf77f6..18d5c63273 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -1,38 +1,43 @@ from typing import Any + class NextflowSerial: def __init__(self, data_dict: dict | None, tab_indent: int | None = 4): self.data_dict = data_dict def __getitem__(self, key: Any | None) -> Any: """Returns value from data""" + if not isinstance(self.data_dict, dict): + raise ValueError("NextflowSerial object is not initialised, cannot index.") return self.data_dict[key] def __setitem__(self, key: Any | None, value: Any | None) -> None: """Sets value in data""" + if not isinstance(self.data_dict, dict): + raise ValueError("NextflowSerial object is not initialised, cannot index.") self.data_dict[key] = value @staticmethod - def _stringify(data: Any | None, quote: bool | None = True) -> str: + def _stringify(data: Any | None, quote: bool = True) -> str: """Return nextflow compatible value""" - quote = "'" * quote + quote_char = "'" if quote else "" if isinstance(data, str): - return f"{quote}{data}{quote}" + return f"{quote_char}{data}{quote_char}" if isinstance(data, bool): return "true" if data else "false" if isinstance(data, type(None)): return "null" return str(data) # Fallback on the Python str function if no matches - + @staticmethod def dumps( data_dict: Any | None, - tab_indent: int | None = 4, - current_indent: int | None = 0, - end: str | None = "\n", - indent_start: bool | None = True, - one_line: bool | None = False, - drop_null: bool | None = False, + tab_indent: int = 4, + current_indent: int = 0, + end: str = "\n", + indent_start: bool = True, + one_line: bool = False, + drop_null: bool = False, ) -> str: """Recursive function to create configuration file""" output = "" diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index fd806b30e2..be69f4c11d 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -37,6 +37,7 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: SUPPORTED_CONTAINERS = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter"] SUPPORTED_SCHEDULERS = ["local", "pbs", "pbspro", "slurm", "sge"] + class ConfigsCreateConfig(BaseModel): """Pydantic model for the nf-core configs create config.""" @@ -196,7 +197,7 @@ def serial_pipeline(self): "cpus": int(self.default_process_ncpus) if self.default_process_ncpus else None, "memory": str(float(self.default_process_memgb)) + "GB" if self.default_process_memgb else None, "time": str(float(self.default_process_hours)) + "h" if self.default_process_hours else None, - } + }, } # Get custom process resources for selector in ["withName", "withLabel"]: @@ -496,7 +497,7 @@ def is_path_ondisk(cls, v: str, info: ValidationInfo) -> str: def is_path_or_uri(cls, v: str, info: ValidationInfo) -> str: v = v.strip() if v == "": - return v #optional + return v # optional uri_pattern = re.compile(r"^\w+:\/\/\w+") if not _PATH_PATTERN.match(v) and not uri_pattern.match(v): From 3db70bc7cfcb9f850b5d688c52dd58944d4b02ad Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 25 Mar 2026 18:15:39 +1100 Subject: [PATCH 096/121] WIP - add config builder app tests --- .../test_choose_infra_type.svg | 272 ++++++++++++++++++ .../test_create_app/test_choose_pipe_type.svg | 272 ++++++++++++++++++ .../test_create_app/test_choose_type.svg | 270 +++++++++++++++++ ...st_entering_infra_nfcore_basic_details.svg | 267 +++++++++++++++++ ...test_entering_infra_nfcore_hpc_details.svg | 267 +++++++++++++++++ .../test_infra_custom_basic_details.svg | 271 +++++++++++++++++ .../test_infra_nfcore_basic_details.svg | 271 +++++++++++++++++ ..._submitting_infra_nfcore_basic_details.svg | 271 +++++++++++++++++ ...st_submitting_infra_nfcore_hpc_details.svg | 268 +++++++++++++++++ .../test_create_app/test_welcome.svg | 269 +++++++++++++++++ tests/configs/test_create_app.py | 231 +++++++++++++++ 11 files changed, 2929 insertions(+) create mode 100644 tests/configs/__snapshots__/test_create_app/test_choose_infra_type.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_choose_pipe_type.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_choose_type.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_infra_nfcore_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_welcome.svg create mode 100644 tests/configs/test_create_app.py diff --git a/tests/configs/__snapshots__/test_create_app/test_choose_infra_type.svg b/tests/configs/__snapshots__/test_create_app/test_choose_infra_type.svg new file mode 100644 index 0000000000..00b40ae41f --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_choose_infra_type.svg @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Is this configuration file part of the nf-core organisation? + + + +Choose "nf-core" if:Choose "Custom" if: + +Infrastructure configs:All configs: + +• You want to add the configuration file to• You want full control over all parameters or +the nf-core/configs repository.options in the config (including those that +are mandatory for nf-core). +Pipeline configs: +Infrastructure configs: +• The configuration file is for an nf-core +pipeline.• You will never add the configuration file to +the nf-core/configs repository. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + nf-core Pipeline configs: +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +• The configuration file is for a custom +pipeline which will never be part of +nf-core. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Custom  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +What's the difference? + +Choosing "nf-core" will make the following of the options mandatory: + +Infrastructure configs: + +• Providing the name and github handle of the author and contact person. +• Providing a description of the config. +• Providing the URL of the owning institution. +• Setting up resourceLimits to set the maximum resources. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_choose_pipe_type.svg b/tests/configs/__snapshots__/test_create_app/test_choose_pipe_type.svg new file mode 100644 index 0000000000..00b40ae41f --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_choose_pipe_type.svg @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Is this configuration file part of the nf-core organisation? + + + +Choose "nf-core" if:Choose "Custom" if: + +Infrastructure configs:All configs: + +• You want to add the configuration file to• You want full control over all parameters or +the nf-core/configs repository.options in the config (including those that +are mandatory for nf-core). +Pipeline configs: +Infrastructure configs: +• The configuration file is for an nf-core +pipeline.• You will never add the configuration file to +the nf-core/configs repository. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + nf-core Pipeline configs: +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +• The configuration file is for a custom +pipeline which will never be part of +nf-core. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Custom  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +What's the difference? + +Choosing "nf-core" will make the following of the options mandatory: + +Infrastructure configs: + +• Providing the name and github handle of the author and contact person. +• Providing a description of the config. +• Providing the URL of the owning institution. +• Setting up resourceLimits to set the maximum resources. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_choose_type.svg b/tests/configs/__snapshots__/test_create_app/test_choose_type.svg new file mode 100644 index 0000000000..107fb8738e --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_choose_type.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Choose config type + + + +Choose "Infrastructure config" if:Choose "Pipeline config" if: + +• You want to only define the computational• You just want to tweak resources of a +environment you will run all pipelines onparticular step of a specific pipeline. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Infrastructure config  Pipeline config  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +What's the difference? + +Infrastructure configs: + +• Describe the basic necessary information for any nf-core pipeline to execute +• Define things such as which container engine to use, if there is a scheduler and which queues +to use etc. +• Are suitable for all users on a given computing environment. +• Can be uploaded to nf-core configs to be directly accessible in a nf-core pipeline with +-profile <infrastructure_name>. +• Are not used to tweak specific parts of a given pipeline (such as a process or module) + +Pipeline configs + +• Are config files that target specific component of a particular pipeline or pipeline run. +▪ Example: you have a particular step of the pipeline that often runs out of memory using the +pipeline's default settings. You would use this config to increase the amount of memory +Nextflow supplies that given task. +• Are normally only used by a single or small group of users. +• May also be shared amongst multiple users on the same computing environment if running similar +data with the same pipeline. +• Can sometimes be uploaded to nf-core configs as a 'pipeline-specific' config. + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_basic_details.svg new file mode 100644 index 0000000000..3f325136aa --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_basic_details.svg @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +myconfig                                                                                     +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Author full name.Author Git(Hub) handle. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +my name                                   @myhandle                                  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +A cool description                                                                           +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +URL of infrastructure website or owning institution (infrastructure configs only). + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +https://example.com +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg new file mode 100644 index 0000000000..126a6a83fa --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Configure the options for your HPC + + + +The scheduler in your HPC.The default queue in yourHow often to get the queue +HPC.status from the scheduler +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +pbspro                   myqueue                  0.75 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Continue  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg new file mode 100644 index 0000000000..6666701dff --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +custom +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Author full name.Author Git(Hub) handle. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Boaty McBoatFace@BoatyMcBoatFace +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Description +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +URL of infrastructure website or owning institution (infrastructure configs only). + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +https://nf-co.re +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_infra_nfcore_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_infra_nfcore_basic_details.svg new file mode 100644 index 0000000000..6666701dff --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_infra_nfcore_basic_details.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +custom +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Author full name.Author Git(Hub) handle. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Boaty McBoatFace@BoatyMcBoatFace +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Description +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +URL of infrastructure website or owning institution (infrastructure configs only). + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +https://nf-co.re +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_basic_details.svg new file mode 100644 index 0000000000..eb77da4744 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_basic_details.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Is this configuration file for an HPC config? + + + +Choose "HPC" if:Choose "local" if: + +You want to create a config file for an HPC.You want to create a config file to run your +pipeline on a local computer. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + HPC ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ local  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +What's the difference? + +Choosing "HPC" will add the following configurations: + +• Provide a scheduler +• Provide the name of a queue +• Select if a module system is used +• Select if you need to load other modules + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg new file mode 100644 index 0000000000..9cc044d1a7 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive inter… + + +Configure the options for your infrastructure config + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Maximum resources + + + +Maximum memory (GB)Maximum number of CPUsMaximum time (hours) to run +available in your machine.available in your machine.your jobs. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +MemoryCPUsTime +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Number of jobs that can beHow often to check forMaximum number of jobs that +submitted simultaneously.successful process completioncan be submitted per minute. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +300                     0.5                      20                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▂▂ +Do you want to define a global cache directory for containers or conda environments? + + + +Define a global cache direcotry. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/path/to/cache/dir +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you have an iGenomes cache directory, specify it. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +iGenomes cache directory + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_welcome.svg b/tests/configs/__snapshots__/test_create_app/test_welcome.svg new file mode 100644 index 0000000000..a00c894198 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_welcome.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + +                                          ,--./,-. +          ___     __   __   __   ___     /,-._.--~\  +    |\ | |__  __ /  ` /  \ |__) |__         }  { +    | \| |       \__, \__/ |  \ |___     \`-._,-`-, +                                          `._,._,' + + + +Welcome to the nf-core config creation wizard + +This app will help you create Nextflow configuration files for both: + +• Infrastructure configs for defining computing environment for all pipelines, and +• Pipeline configs for defining pipeline-specific resource requirements + + +Using Configs + +The resulting config file can be used with a pipeline with adding -c <filename>.conf to a +nextflow run command. + +They can also be added to the centralised nf-core/configs repository, where they can be used +directly by anyone running nf-core pipelines on your infrastructure specifying nextflow run +-profile <configname>. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Let's go!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py new file mode 100644 index 0000000000..b86391bd1e --- /dev/null +++ b/tests/configs/test_create_app.py @@ -0,0 +1,231 @@ +"""Test Config Create App""" + +from nf_core.configs.create import ConfigsCreateApp + +INIT_FILE = "../../nf_core/configs/create/__init__.py" + + +async def test_app_bindings(): + """Test that the app bindings work.""" + app = ConfigsCreateApp() + async with app.run_test() as pilot: + # Test pressing the D key + assert app.theme == "textual-dark" + await pilot.press("d") + assert app.theme == "textual-light" + await pilot.press("d") + assert app.theme == "textual-dark" + + # Test pressing the Q key + await pilot.press("q") + assert app.return_code == 0 + + +def test_welcome(snap_compare): + """Test snapshot for the first screen in the app. The welcome screen.""" + assert snap_compare(INIT_FILE, terminal_size=(100, 50)) + + +async def click_lets_go(pilot) -> None: + await pilot.click("#lets_go") + + +def test_choose_type(snap_compare): + """Test snapshot for the choose_type screen. + Steps to get to this screen: + screen welcome > press Let's go! > + screen choose_type + """ + + async def run_before(pilot) -> None: + await click_lets_go(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def click_type_infrastructure(pilot) -> None: + await click_lets_go(pilot) + await pilot.click("#type_infrastructure") + + +def test_choose_infra_type(snap_compare): + """Test snapshot for the nfcore_question screen + via the Infrastructure config option. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + screen choose_type + """ + + async def run_before(pilot) -> None: + await click_type_infrastructure(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def click_infra_nfcore(pilot) -> None: + await click_type_infrastructure(pilot) + await pilot.click("#type_nfcore") + + +def test_infra_nfcore_basic_details(snap_compare): + """Test snapshot for the nf-core basic_details screen + when configuring an infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + screen basic_details + """ + + async def run_before(pilot) -> None: + await click_infra_nfcore(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def enter_infra_nfcore_basic_details(pilot) -> None: + await click_infra_nfcore(pilot) + await pilot.click("#general_config_name") + await pilot.press("m", "y", "c", "o", "n", "f", "i", "g") + await pilot.press("tab") + await pilot.press("m", "y", " ", "n", "a", "m", "e") + await pilot.press("tab") + await pilot.press("@", "m", "y", "h", "a", "n", "d", "l", "e") + await pilot.press("tab") + await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press("tab") + await pilot.press("h", "t", "t", "p", "s", ":", "/", "/", "e", "x", "a", "m", "p", "l", "e", ".", "c", "o", "m") + + +async def submit_infra_nfcore_basic_details(pilot) -> None: + await enter_infra_nfcore_basic_details(pilot) + await pilot.click("#next") + + +def test_entering_infra_nfcore_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the nf-core basic_details screen + when configuring an infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + screen basic_details + """ + + async def run_before(pilot) -> None: + await enter_infra_nfcore_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_submitting_infra_nfcore_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the nf-core basic_details screen + when configuring an infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + enter basic details > + click Next > + screen hpc_question + """ + + async def run_before(pilot) -> None: + await submit_infra_nfcore_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def enter_infra_nfcore_hpc_details(pilot) -> None: + await submit_infra_nfcore_basic_details(pilot) + await pilot.click("#type_hpc") + await pilot.click("#scheduler") + await pilot.press("p", "b", "s", "p", "r", "o") + await pilot.press("tab") + await pilot.press("m", "y", "q", "u", "e", "u", "e") + await pilot.press("tab") + await pilot.press("0", ".", "7", "5") + + +async def submit_infra_nfcore_hpc_details(pilot) -> None: + await enter_infra_nfcore_hpc_details(pilot) + await pilot.click("#toconfiguration") + + +def test_entering_infra_nfcore_hpc_details(snap_compare): + """Test snapshot for entering HPC details + when configuring an infrastructure config + for use with nf-core. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + enter basic details > + click Next > + click HPC > + enter HPC details + """ + + async def run_before(pilot) -> None: + await enter_infra_nfcore_hpc_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_submitting_infra_nfcore_hpc_details(snap_compare): + """Test snapshot for entering HPC details + when configuring an infrastructure config + for use with nf-core. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + screen final_infra_details + """ + + async def run_before(pilot) -> None: + await submit_infra_nfcore_hpc_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_infra_custom_basic_details(snap_compare): + """Test snapshot for the custom pipeline basic_details screen + when configuring an infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press Custom > + screen basic_details + """ + + async def run_before(pilot) -> None: + await pilot.click("#lets_go") + await pilot.click("#type_infrastructure") + await pilot.click("#type_custom") + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_choose_pipe_type(snap_compare): + """Test snapshot for the nfcore_question screen + via the Pipeline config option. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + screen choose_type + """ + + async def run_before(pilot) -> None: + await pilot.click("#lets_go") + await pilot.click("#type_pipeline") + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) From 79ade5ed73df162d420a60518e2cc57402aafcf3 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 27 Mar 2026 14:53:43 +1100 Subject: [PATCH 097/121] exclude snapshots from precommit linting --- .pre-commit-config.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1b50818a4..74bcb5ef20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,8 @@ repos: .*\.snap$| nf_core/pipeline-template/subworkflows/.*| nf_core/pipeline-template/modules/.*| - tests/pipelines/__snapshots__/.* + tests/pipelines/__snapshots__/.*| + tests/configs/__snapshots__/.* )$ - id: end-of-file-fixer exclude: | @@ -30,7 +31,8 @@ repos: .*\.snap$| nf_core/pipeline-template/subworkflows/.*| nf_core/pipeline-template/modules/.*| - tests/pipelines/__snapshots__/.* + tests/pipelines/__snapshots__/.*| + tests/configs/__snapshots__/.* )$ - repo: https://github.com/pre-commit/mirrors-mypy rev: "v1.19.1" From a678c9898bb72a0cf5e581d06b9c60a3c61e9b9d Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 27 Mar 2026 17:16:53 +1100 Subject: [PATCH 098/121] WIP update configbuilder app infra tests --- nf_core/configs/create/create.py | 2 +- nf_core/configs/create/serial.py | 2 +- tests/configs/__init__.py | 0 ...st_entering_infra_final_details_docker.svg | 474 ++++++++++++++++++ ...tering_infra_final_details_singularity.svg | 473 +++++++++++++++++ ...itting_infra_final_details_singularity.svg | 267 ++++++++++ tests/configs/test_create_app.py | 174 ++++++- tests/test_configs.py | 27 + 8 files changed, 1409 insertions(+), 10 deletions(-) create mode 100644 tests/configs/__init__.py create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg create mode 100644 tests/test_configs.py diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index dcb79adb04..5f1d5c26f9 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -22,7 +22,7 @@ def __init__(self, template_config: ConfigsCreateConfig, config_type: str, confi def write_to_file(self): ## File name option config_name = str(self.template_config.general_config_name).strip() - config_name_clean = sub(r"\W+" "_", config_name) + config_name_clean = sub(r"\W+", "_", config_name) config_name_clean = sub(r"_+$", "", config_name_clean) filename = f"{config_name_clean}.conf" filename = self.config_dir / filename diff --git a/nf_core/configs/create/serial.py b/nf_core/configs/create/serial.py index 18d5c63273..5f14202635 100644 --- a/nf_core/configs/create/serial.py +++ b/nf_core/configs/create/serial.py @@ -85,7 +85,7 @@ def dumps( output += " " * current_indent * indent_start if one_line: # False is set here to disable quotes on strings - output += f"{k}: {NextflowSerial._stringify(v, False)}{end}" + output += f"{k}: {NextflowSerial._stringify(v)}{end}" else: output += f"{k} = {NextflowSerial._stringify(v)}{end}" return output diff --git a/tests/configs/__init__.py b/tests/configs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg new file mode 100644 index 0000000000..4befd0096c --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg @@ -0,0 +1,474 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Configure the options for your infrastructure config + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +docker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Maximum resources + + + +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) to run +in your machine.available in your machine.your jobs. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +128                      48                       9.5                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Number of jobs that can beHow often to check forMaximum number of jobs that +submitted simultaneously.successful process completioncan be submitted per minute. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +200                      0.25                     25                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Do you want to define a global cache directory for containers or conda environments? + + + +Define a global cache direcotry. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/path/to/cache/dir +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you have an iGenomes cache directory, specify it. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/data/igenomes/cache                                                                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you have to use a specific scratch directory, specify it. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/tmp/scratch                                                                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔Delete work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run. + + + +Specify the number of retries for a failed job. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg new file mode 100644 index 0000000000..7dd420fa1d --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg @@ -0,0 +1,473 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Configure the options for your infrastructure config + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Maximum resources + + + +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) to run +in your machine.available in your machine.your jobs. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +128                      48                       9.5                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Number of jobs that can beHow often to check forMaximum number of jobs that +submitted simultaneously.successful process completioncan be submitted per minute. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +200                      0.25                     25                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Do you want to define a global cache directory for containers or conda environments? + + + +Define a global cache direcotry. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/singularity/cachedir                                                                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you have an iGenomes cache directory, specify it. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/data/igenomes/cache                                                                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you have to use a specific scratch directory, specify it. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/tmp/scratch                                                                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔Delete work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run. + + + +Specify the number of retries for a failed job. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg new file mode 100644 index 0000000000..8823e334ee --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Final step + + + +In which directory would you like to save the config? + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Save and close!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py index b86391bd1e..a21ee63bb8 100644 --- a/tests/configs/test_create_app.py +++ b/tests/configs/test_create_app.py @@ -1,6 +1,12 @@ """Test Config Create App""" +from pathlib import Path +from tempfile import TemporaryDirectory + from nf_core.configs.create import ConfigsCreateApp +from nf_core.configs.create.utils import SUPPORTED_CONTAINERS + +from ..test_configs import INFRA_SINGULARITY_CONFIG INIT_FILE = "../../nf_core/configs/create/__init__.py" @@ -87,15 +93,15 @@ async def run_before(pilot) -> None: async def enter_infra_nfcore_basic_details(pilot) -> None: await click_infra_nfcore(pilot) await pilot.click("#general_config_name") - await pilot.press("m", "y", "c", "o", "n", "f", "i", "g") + await pilot.press(*list("myconfig")) await pilot.press("tab") - await pilot.press("m", "y", " ", "n", "a", "m", "e") + await pilot.press(*list("my name")) await pilot.press("tab") - await pilot.press("@", "m", "y", "h", "a", "n", "d", "l", "e") + await pilot.press(*list("@myhandle")) await pilot.press("tab") - await pilot.press("A", " ", "c", "o", "o", "l", " ", "d", "e", "s", "c", "r", "i", "p", "t", "i", "o", "n") + await pilot.press(*list("A cool description")) await pilot.press("tab") - await pilot.press("h", "t", "t", "p", "s", ":", "/", "/", "e", "x", "a", "m", "p", "l", "e", ".", "c", "o", "m") + await pilot.press(*list("https://example.com")) async def submit_infra_nfcore_basic_details(pilot) -> None: @@ -143,11 +149,11 @@ async def enter_infra_nfcore_hpc_details(pilot) -> None: await submit_infra_nfcore_basic_details(pilot) await pilot.click("#type_hpc") await pilot.click("#scheduler") - await pilot.press("p", "b", "s", "p", "r", "o") + await pilot.press(*list("pbspro")) await pilot.press("tab") - await pilot.press("m", "y", "q", "u", "e", "u", "e") + await pilot.press(*list("myqueue")) await pilot.press("tab") - await pilot.press("0", ".", "7", "5") + await pilot.press(*list("0.75")) async def submit_infra_nfcore_hpc_details(pilot) -> None: @@ -197,6 +203,158 @@ async def run_before(pilot) -> None: assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) +def enter_infra_final_details(container): + assert container in SUPPORTED_CONTAINERS + async def _enter_infra_final_details(pilot) -> None: + await submit_infra_nfcore_hpc_details(pilot) + await pilot.click("#container_system") + for container_system in SUPPORTED_CONTAINERS: + if container_system == container: + break + await pilot.press("down") + await pilot.press("enter") + await pilot.press("tab") + await pilot.press(*list("128")) + await pilot.press("tab") + await pilot.press(*list("48")) + await pilot.press("tab") + await pilot.press(*list("9.5")) + await pilot.press("tab") + await pilot.press(*list("200")) + await pilot.press("tab") + await pilot.press(*list("0.25")) + await pilot.press("tab") + await pilot.press(*list("25")) + await pilot.press("tab") + if container in ['singularity', 'charliecloud']: + await pilot.press(*list(f"/{container}/cachedir")) + await pilot.press("tab") + await pilot.press(*list("/data/igenomes/cache")) + await pilot.press("tab") + await pilot.press(*list("/tmp/scratch")) + await pilot.press("tab") + await pilot.press("enter") + await pilot.press("tab") + await pilot.press(*list("2")) + return _enter_infra_final_details + + +def submit_infra_final_details(container): + async def _submit_infra_final_details(pilot) -> None: + await enter_infra_final_details(container)(pilot) + await pilot.click("#finish") + return _submit_infra_final_details + + +def write_infra_details(container, outdir="."): + assert isinstance(outdir, str) + assert Path(outdir).is_dir() + async def _write_infra_details(pilot) -> None: + await submit_infra_final_details(container)(pilot) + await pilot.click("#savelocation") + await pilot.press(*list(outdir)) + await pilot.click("#close_app") + return _write_infra_details + + +def test_entering_infra_final_details_docker(snap_compare): + """Test snapshot for entering HPC details + when configuring an infrastructure config + for use with nf-core. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + screen final_infra_details + """ + + async def run_before(pilot) -> None: + await enter_infra_final_details('docker')(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_entering_infra_final_details_singularity(snap_compare): + """Test snapshot for entering HPC details + when configuring an infrastructure config + for use with nf-core. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + screen final_infra_details + """ + + async def run_before(pilot) -> None: + await enter_infra_final_details('singularity')(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submitting_infra_final_details_singularity(snap_compare): + """Test snapshot for entering HPC details + when configuring an infrastructure config + for use with nf-core. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + click Finish > + screen final + """ + + async def run_before(pilot) -> None: + await submit_infra_final_details('singularity')(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def test_writing_infra_details_singularity(): + """Test snapshot for entering HPC details + when configuring an infrastructure config + for use with nf-core. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press nf-core > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + click Finish > + click Save and close! + """ + + app = ConfigsCreateApp() + async with app.run_test(size=(100, 50)) as pilot: + # Make a temporary directory to store the output + tmpdir = TemporaryDirectory() + + await write_infra_details('singularity', outdir=tmpdir.name)(pilot) + + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + assert config == INFRA_SINGULARITY_CONFIG + tmpdir.cleanup() + + def test_infra_custom_basic_details(snap_compare): """Test snapshot for the custom pipeline basic_details screen when configuring an infrastructure config. diff --git a/tests/test_configs.py b/tests/test_configs.py new file mode 100644 index 0000000000..f14ceebe7c --- /dev/null +++ b/tests/test_configs.py @@ -0,0 +1,27 @@ +INFRA_SINGULARITY_CONFIG = """\ +params { + config_profile_contact = 'my name (@myhandle)' + config_profile_description = 'A cool description' + config_profile_url = 'https://example.com' + igenomes_base = '/data/igenomes/cache' +} +executor { + queueStatInterval = '0.75m' + queueSize = 200 + pollInterval = '0.25m' + submitRateLimit = '25min' +} +process { + executor = 'pbspro' + queue = 'myqueue' + resourceLimits = [cpus: 48, memory: '128.0GB', time: '9.5h'] + scratch = '/tmp/scratch' + maxRetries = 2 +} +singularity { + enabled = true + cacheDir = '/singularity/cachedir' + autoMounts = true +} +cleanup = true +""" \ No newline at end of file From f290340234543ce9c2faadd5ace8f1f671ce5540 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Mon, 30 Mar 2026 09:33:07 +1100 Subject: [PATCH 099/121] fix test linting, fix typo in finalinfradetails --- nf_core/configs/create/finalinfradetails.py | 2 +- tests/configs/test_create_app.py | 15 ++++++++++----- tests/test_configs.py | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 4be8942078..3eee11fca0 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -87,7 +87,7 @@ def compose(self) -> ComposeResult: yield TextInput( "cachedir", "/path/to/cache/dir", - "Define a global cache direcotry.", + "Define a global cache directory.", classes="", default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") if self.container_system is not None diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py index a21ee63bb8..28eb458f6a 100644 --- a/tests/configs/test_create_app.py +++ b/tests/configs/test_create_app.py @@ -205,6 +205,7 @@ async def run_before(pilot) -> None: def enter_infra_final_details(container): assert container in SUPPORTED_CONTAINERS + async def _enter_infra_final_details(pilot) -> None: await submit_infra_nfcore_hpc_details(pilot) await pilot.click("#container_system") @@ -226,7 +227,7 @@ async def _enter_infra_final_details(pilot) -> None: await pilot.press("tab") await pilot.press(*list("25")) await pilot.press("tab") - if container in ['singularity', 'charliecloud']: + if container in ["singularity", "charliecloud"]: await pilot.press(*list(f"/{container}/cachedir")) await pilot.press("tab") await pilot.press(*list("/data/igenomes/cache")) @@ -236,6 +237,7 @@ async def _enter_infra_final_details(pilot) -> None: await pilot.press("enter") await pilot.press("tab") await pilot.press(*list("2")) + return _enter_infra_final_details @@ -243,17 +245,20 @@ def submit_infra_final_details(container): async def _submit_infra_final_details(pilot) -> None: await enter_infra_final_details(container)(pilot) await pilot.click("#finish") + return _submit_infra_final_details def write_infra_details(container, outdir="."): assert isinstance(outdir, str) assert Path(outdir).is_dir() + async def _write_infra_details(pilot) -> None: await submit_infra_final_details(container)(pilot) await pilot.click("#savelocation") await pilot.press(*list(outdir)) await pilot.click("#close_app") + return _write_infra_details @@ -274,7 +279,7 @@ def test_entering_infra_final_details_docker(snap_compare): """ async def run_before(pilot) -> None: - await enter_infra_final_details('docker')(pilot) + await enter_infra_final_details("docker")(pilot) assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) @@ -296,7 +301,7 @@ def test_entering_infra_final_details_singularity(snap_compare): """ async def run_before(pilot) -> None: - await enter_infra_final_details('singularity')(pilot) + await enter_infra_final_details("singularity")(pilot) assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) @@ -319,7 +324,7 @@ def test_submitting_infra_final_details_singularity(snap_compare): """ async def run_before(pilot) -> None: - await submit_infra_final_details('singularity')(pilot) + await submit_infra_final_details("singularity")(pilot) assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) @@ -346,7 +351,7 @@ async def test_writing_infra_details_singularity(): # Make a temporary directory to store the output tmpdir = TemporaryDirectory() - await write_infra_details('singularity', outdir=tmpdir.name)(pilot) + await write_infra_details("singularity", outdir=tmpdir.name)(pilot) config_file = Path(tmpdir.name) / "myconfig.conf" with open(config_file) as f: diff --git a/tests/test_configs.py b/tests/test_configs.py index f14ceebe7c..39d74d6882 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -24,4 +24,4 @@ autoMounts = true } cleanup = true -""" \ No newline at end of file +""" From 12d4da7a0751120c9316d4908bc398c9bae64325 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Mon, 30 Mar 2026 09:54:32 +1100 Subject: [PATCH 100/121] update final infra detail test snapshots --- .../test_entering_infra_final_details_docker.svg | 2 +- .../test_entering_infra_final_details_singularity.svg | 2 +- .../test_submitting_infra_nfcore_hpc_details.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg index 4befd0096c..5bdfdc71ef 100644 --- a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg @@ -405,7 +405,7 @@ -Define a global cache direcotry. +Define a global cache directory. ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ /path/to/cache/dir diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg index 7dd420fa1d..0b4833245d 100644 --- a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg @@ -404,7 +404,7 @@ -Define a global cache direcotry. +Define a global cache directory. ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ /singularity/cachedir                                                                        diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg index 9cc044d1a7..7c1459b86a 100644 --- a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg @@ -249,7 +249,7 @@ -Define a global cache direcotry. +Define a global cache directory. ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ /path/to/cache/dir From e8f1f9d133d83fe7adf1e21b914a7427a371fa4a Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 21 Apr 2026 16:29:48 +1000 Subject: [PATCH 101/121] WIP: implementing feedback --- nf_core/configs/create/configtype.py | 8 +- nf_core/configs/create/hpccustomisation.py | 89 ++++++++++++++++------ nf_core/configs/create/utils.py | 12 ++- 3 files changed, 79 insertions(+), 30 deletions(-) diff --git a/nf_core/configs/create/configtype.py b/nf_core/configs/create/configtype.py index bc6a350f7c..c95ca0abfd 100644 --- a/nf_core/configs/create/configtype.py +++ b/nf_core/configs/create/configtype.py @@ -12,7 +12,7 @@ markdown_type_nfcore = """ ## Choose _"Infrastructure config"_ if: -* You want to only define the computational environment you will run all pipelines on +* You want to only define the computational environment you will run all pipelines on. """ @@ -28,7 +28,7 @@ _Infrastructure_ configs: - Describe the basic necessary information for any nf-core pipeline to -execute +execute. - Define things such as which container engine to use, if there is a scheduler and which queues to use etc. - Are suitable for _all_ users on a given computing environment. @@ -36,12 +36,12 @@ configs](https://github.com/nf-core/tools/configs) to be directly accessible in a nf-core pipeline with `-profile `. - Are not used to tweak specific parts of a given pipeline (such as a process or -module) +module). _Pipeline_ configs - Are config files that target specific component of a particular pipeline or pipeline run. - - Example: you have a particular step of the pipeline that often runs out + - Example: you have a particular step of the pipeline that often runs out. of memory using the pipeline's default settings. You would use this config to increase the amount of memory Nextflow supplies that given task. - Are normally only used by a _single or small group_ of users. diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 27e5dfe8e3..ca90e295e0 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -6,14 +6,44 @@ from textual.app import ComposeResult from textual.containers import Center, Horizontal from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown +from textual.widgets import Button, Footer, Header, Input, Markdown, Select from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context markdown_intro = """ # Configure the options for your HPC + +Use the following fields to provide important details about your HPC. +""" + +markdown_scheduler = """ +First, select your HPC's scheduler. +The program will attempt to automatically determine your HPC's scheduler. +If it was unsuccessful, you can select one of the supported schedulers from +the drop-down list. +You can also select "Local execution" which will result in jobs defaulting +to run on the same node as Nextflow; this is generally not recommended. +""" + +markdown_queue = """ +Next, supply your HPC's default queue, if required. Again, this will be +auto-filled if possible. +You can leave this field blank if your HPC automatically submits jobs +to a default queue when not specified. + +For more complex queue selection (e.g. based on memory or CPU requirements), +you will need to manually edit the configuration file after completion. + +If you wish to submit different processes to different queues, you will +need to additionally create a pipeline configuration, which will let you +specify alternative HPC queues for each process. """ +markdown_queuestat = """ +Finally, use the following field to control how frequently (in minutes) Nextflow should +request the queue status from the scheduler. This is optional, and will +default to 1 minute if not provided. +""" class HpcCustomisation(Screen): """Customise the options to create a config for an HPC.""" @@ -25,30 +55,36 @@ def compose(self) -> ComposeResult: queues = self._get_queues(scheduler) default_queue = self._get_default_queue(scheduler) module_system_used = self._detect_module_system() + supported_schedulers = { + "Local execution": "local", + "PBS/Torque": "pbs", + "PBS Pro": "pbspro", + "SLURM": "slurm", + } yield Markdown(markdown_intro) - with Horizontal(): - yield TextInput( - "scheduler", - "Scheduler", - "The scheduler in your HPC.", - default=scheduler if scheduler is not None else "local", - classes="column", - ) - yield TextInput( - "queue", - "Queue", - "The default queue in your HPC.", - default=default_queue if default_queue else "", - classes="column", - suggestions=queues, - ) - yield TextInput( - "queue_stat_interval", - "Queue stat interval", - "How often to get the queue status from the scheduler (minutes).", - default="0.5", - classes="column", - ) + yield Markdown(markdown_scheduler) + yield Select( + [(name, keyword) for name, keyword in supported_schedulers.items()], + prompt="Select your HPC's scheduler.", + value=scheduler if scheduler is not None else "local", + classes="column", + id="scheduler" + ) + yield Markdown(markdown_queue) + yield TextInput( + "queue", + "Queue", + "The default queue in your HPC (if required).", + default=default_queue if default_queue else "", + classes="column", + suggestions=queues, + ) + yield TextInput( + "queue_stat_interval", + "Queue stat interval", + "How often to get the queue status from the scheduler (minutes).", + classes="column", + ) yield TextInput( "module_system", "Other modules to load", @@ -176,6 +212,11 @@ def _detect_module_system(self) -> bool: def on_button_pressed(self, event: Button.Pressed) -> None: """Save fields to the config.""" new_config = {} + + # Get scheduler value + select = self.query_one("#scheduler", Select) + new_config["scheduler"] = select.value + for text_input in self.query("TextInput"): this_input = text_input.query_one(Input) validation_result = this_input.validate(this_input.value) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index be69f4c11d..e6b960a50c 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -248,6 +248,14 @@ def notempty(cls, v: str) -> str: raise ValueError("Cannot be left empty.") return v + @field_validator("general_config_name") + @classmethod + def all_lower_case(cls, v: str) -> str: + """Check that string values are all lower-case.""" + if not v.islower(): + raise ValueError("Config names must not contain upper-case letters.") + return v + @field_validator("config_pipeline_path") @classmethod def path_valid(cls, v: str, info: ValidationInfo) -> str: @@ -447,7 +455,7 @@ def pos_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a positive number.") return v - @field_validator("scheduler", "queue") + @field_validator("scheduler") @classmethod def nonemtpy_hpc_details(cls, v: str, info: ValidationInfo) -> str: """Check that HPC infrastructure details are non-empty""" @@ -523,7 +531,7 @@ def pos_hpc_interval_valid(cls, v: str, info: ValidationInfo) -> str: context = info.context if context and context["is_infrastructure"] and context["is_hpc"]: if v.strip() == "": - raise ValueError("Must not be empty.") + return v try: vf = float(v.strip()) except ValueError: From 56c1d81623e559d95cc2be5d4831fc8e21257ba4 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 22 Apr 2026 15:19:03 +1000 Subject: [PATCH 102/121] WIP: implementing more feedback, adding cache dir memory to final infra details --- nf_core/configs/create/finalinfradetails.py | 164 ++++++++++++-------- nf_core/configs/create/utils.py | 35 ++++- 2 files changed, 131 insertions(+), 68 deletions(-) diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 3eee11fca0..fd8111626a 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -14,109 +14,129 @@ # Configure the options for your infrastructure config """ +markdown_max_resources = """ +## Set maximum available resources + +The following fields let you set the maximum available resrouces +on your infrastructure. + +Memory, CPUs, and time must be filled out and all processes run with this +configuration will be capped at these values. + +The queue size, poll interval, and submit rate fields are optional, since +Nextflow has built-in defaults for these values. Consult the Nextflow +documentation for further details on these default values. +""" + +markdown_global_dirs = """ +## Define global directories + +The following fields let you define global directories for +a container/conda image cache, an iGenomes cache, and +a scratch directory in which to run your jobs. + +Each field is optional, but must contain the **full (absolute) path** to +these directories if specified. They may also contain references to +environment variables (e.g. `$CACHEDIR`) and may use the `~` symbol to +refer to your home directory. +""" + class FinalInfraDetails(Screen): """Customise the options to create a config for an infrastructure.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.container_system = None - self.container_system_list = [] - self.cache_dir = None + self.container_system = self._get_container_system() + self.container_system_list = SUPPORTED_CONTAINERS + self.cache_dirs = { + self.container_system: self._get_container_cache_directory(), + } def compose(self) -> ComposeResult: yield Header() yield Footer() yield Markdown(markdown_intro) - self.container_system_list = self._get_container_systems() - self.container_system = self.container_system_list[0] if self.container_system_list else None - - self.container_system_list = SUPPORTED_CONTAINERS - self.container_system = self.container_system_list[0] # default + yield Markdown("Select the container/software management system available on your infrastructure.") yield Select( [(c, c) for c in self.container_system_list], - prompt="Select container system", + prompt="Select container/software management system", id="container_system", - value=self.container_system, # sets default + value=self.container_system, ) - yield Markdown("## Maximum resources") + yield Markdown(markdown_max_resources) with Horizontal(): yield TextInput( "memory", "Memory", - "Maximum memory (GB) available in your machine.", + "Maximum memory (GB) available on your infrastructure (across all nodes).", classes="column", ) yield TextInput( "cpus", "CPUs", - "Maximum number of CPUs available in your machine.", + "Maximum number of CPUs available on your infrastructure (across all nodes).", classes="column", ) yield TextInput( "time", "Time", - "Maximum time (hours) to run your jobs.", + "Maximum time (hours) available to jobs on your infrastructure (across all nodes).", classes="column", ) with Horizontal(): yield TextInput( "queue_size", "Queue size", - "Number of jobs that can be submitted simultaneously.", - default="300", + "Maximum number of jobs that can be submitted simultaneously on your infrastructure (optional).", classes="column", ) yield TextInput( "poll_interval", "Poll interval", - "How often to check for successful process completion (minutes).", - default="0.5", + "How often (in minutes) to check for successful process completion (optional).", classes="column", ) yield TextInput( "submit_rate", "Jobs per minutes", - "Maximum number of jobs that can be submitted per minute.", - default="20", + "Maximum number of jobs that can be submitted per minute (optional).", classes="column", ) - with Vertical(id="define-global-cache-dir", classes="hide" if not self.container_system else ""): - yield Markdown("## Do you want to define a global cache directory for containers or conda environments?") - yield TextInput( - "cachedir", - "/path/to/cache/dir", - "Define a global cache directory.", - classes="", - default=self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") - if self.container_system is not None - else "", - ) + yield Markdown(markdown_global_dirs) + yield TextInput( + "cachedir", + "/path/to/cache/dir", + "If you have a global container/conda cache directory, specify the **full path** here.", + classes="", + default=self._get_container_cache_directory(), + ) yield TextInput( "igenomes_cachedir", "iGenomes cache directory", - "If you have an iGenomes cache directory, specify it.", + "If you have an iGenomes cache directory, specify the **full path** here.", classes="hide" if not self.parent.NFCORE_CONFIG else "", ) yield TextInput( "scratch_dir", "Scratch directory", - "If you have to use a specific scratch directory, specify it.", + "If you want your jobs to run within a scratch directory, specify the full path here.", classes="", ) with Horizontal(classes="ghrepo-cols"): yield Switch(value=False, id="toggle-delete-work") with Vertical(): - yield Static("Delete work directory", classes="") + yield Static("Clean up work directory", classes="") yield Markdown( - "Select if you want to delete the files in the `work/` directory on successful completion of a run.", + "Select if you want to delete the files in the `work/` directory on successful completion of a run (**prevents `resume` functionality**).", classes="feature_subtitle", ) yield TextInput( "retries", "Number of retries", "Specify the number of retries for a failed job.", + default="1", classes="", ) yield Center( @@ -125,30 +145,30 @@ def compose(self) -> ComposeResult: classes="cta", ) - def _get_container_systems(self) -> list[str]: - """Get the available container systems to use for software handling.""" + def _get_container_system(self) -> str: + """Get the default container system to use for software handling.""" module_system_used = self._detect_module_system() container_systems = SUPPORTED_CONTAINERS - available_systems = [] - if module_system_used: - for system in container_systems: - try: - output = subprocess.check_output(["module", "avail", "|", "grep", system]).decode("utf-8") - if output: - available_systems.append(system) - except subprocess.CalledProcessError: - continue - else: - for system in container_systems: + for system in container_systems: + # First, see if the container system is available natively + try: + output = subprocess.check_output([system], stderr=subprocess.STDOUT).decode("utf-8") + if output: + return system + except FileNotFoundError: + pass + except subprocess.CalledProcessError: + pass + # If not available natively, check if module exists + if module_system_used: try: - output = subprocess.check_output([system]).decode("utf-8") + output = subprocess.check_output(["module", "avail", "|", "grep", system], stderr=subprocess.STDOUT).decode("utf-8") if output: - available_systems.append(system) - except FileNotFoundError: - continue + return system except subprocess.CalledProcessError: - continue - return available_systems + pass + # Return the first supported container by default + return container_systems[0] def _detect_module_system(self) -> bool: """Detect if a module system is used""" @@ -168,19 +188,41 @@ def _get_set_directory(self, dir: str) -> str | None: return set_dir return None + def _get_container_cache_directory(self) -> str: + """Try to get the cache directory for the current container system.""" + if not self.container_system: + return "" + cachedir_path = self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") + return cachedir_path or "" + + def get_container_cache_directory(self) -> str: + """Look up the cache directory for the current container system.""" + if not self.container_system: + return "" + if not self.cache_dirs.get(self.container_system, None): + self.cache_dirs[self.container_system] = self._get_container_cache_directory() + return self.cache_dirs[self.container_system] + + @on(Input.Changed) + @on(Input.Submitted) + def set_cache_directory(self) -> None: + """Set the container system cache dir value""" + if not self.container_system: + return None + for text_input in self.query("TextInput"): + this_input = text_input.query_one(Input) + if text_input.field_id == "cachedir": + cachedir_path = str(this_input.value) + self.cache_dirs[self.container_system] = cachedir_path + @on(Select.Changed, "#container_system") def get_container_system(self, event: Select.Changed) -> None: """Get the container system from dropdown.""" - self.container_system = event.value + self.container_system = str(event.value) cachedir_text_input = self.query_one("#cachedir") cachedir_input = cachedir_text_input.query_one(Input) if self.container_system: - remove_hide_class(self.parent, "define-global-cache-dir") - if not cachedir_input.value: - cachedir_path = self._get_set_directory(f"NXF_{self.container_system.upper()}_CACHEDIR") - cachedir_input.value = cachedir_path or "" - else: - add_hide_class(self.parent, "define-global-cache-dir") + cachedir_input.value = self.get_container_cache_directory() @on(Button.Pressed, "#finish") def on_finish_button(self, event: Button.Pressed) -> None: diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index e6b960a50c..600a62bec5 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -34,7 +34,7 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: INFRA_ISHPC_GLOBAL: bool = False _PATH_PATTERN = re.compile(r"(\/|~\/|~$|\$\{?\w+\}?)(.*)") # Used by finalinfradetails as it already imports create.utils -SUPPORTED_CONTAINERS = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter"] +SUPPORTED_CONTAINERS = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter", "conda"] SUPPORTED_SCHEDULERS = ["local", "pbs", "pbspro", "slurm", "sge"] @@ -161,7 +161,7 @@ def serial_hpc(self): **params, "executor": { "queueStatInterval": str(float(self.queue_stat_interval)) + "m" if self.queue_stat_interval else None, - "queueSize": int(self.queue_size) or None, + "queueSize": int(self.queue_size) if self.queue_size else None, "pollInterval": str(float(self.poll_interval)) + "m" if self.poll_interval else None, "submitRateLimit": str(int(self.submit_rate)) + "min" if self.submit_rate else None, }, @@ -174,12 +174,12 @@ def serial_hpc(self): {"time": str(float(self.time)) + "h" if self.time else None}, ], "scratch": self.scratch_dir or None, - "maxRetries": int(self.retries) or None, + "maxRetries": int(self.retries) if self.retries else None, "module": modules_to_load or None, }, self.container_system: { "enabled": True, - "cacheDir": self.cachedir or None, + "cacheDir": self.cachedir if self.container_system in ["singularity", "apptainer", "charliecloud", "conda"] else None, "autoMounts": True if self.container_system in ["singularity", "apptainer"] else None, }, "cleanup": self.delete_work_dir, @@ -401,11 +401,11 @@ def valid_custom_queue(cls, v: str, info: ValidationInfo) -> str: # TODO: Any other invalid characters? return v - @field_validator("cpus", "memory", "retries", "queue_size", "submit_rate") + @field_validator("cpus", "memory", "retries") @classmethod def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: """ - Check that integer values are either empty or positive. + Check that integer values are positive. This contains the same validation as self.pos_integer_valid(). However, keep infrastructure and pipeline methods decoupled for @@ -423,6 +423,27 @@ def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: raise ValueError("Must be a positive integer.") return v + @field_validator("queue_size", "submit_rate") + @classmethod + def pos_integer_optional_valid_infra(cls, v: str, info: ValidationInfo) -> str: + """ + Check that integer values are either empty or positive. + + This contains the same validation as self.pos_integer_valid_infra(), + but allows for empty values as well + """ + context = info.context + if context and context["is_infrastructure"]: + if v.strip() == "": + return v + try: + v_int = int(v.strip()) + except ValueError: + raise ValueError("Must be an integer.") + if not v_int > 0: + raise ValueError("Must be a positive integer.") + return v + @field_validator("time") @classmethod def non_neg_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: @@ -446,7 +467,7 @@ def pos_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: context = info.context if context and context["is_infrastructure"]: if v.strip() == "": - raise ValueError("Cannot be empty.") + return v try: vf = float(v.strip()) except ValueError: From 8ec71d95d0071141d516de151bbe89027f558bdd Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 8 May 2026 16:47:34 +1000 Subject: [PATCH 103/121] WIP: implementing more feedback, finished setting default process resources --- nf_core/configs/create/basicdetails.py | 42 ++++++++++++++++++--- nf_core/configs/create/defaultprocessres.py | 12 +++--- nf_core/configs/create/multiprocessres.py | 12 +++--- nf_core/configs/create/utils.py | 22 ++++++++--- 4 files changed, 65 insertions(+), 23 deletions(-) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 6e6c99b5d9..d76057a257 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -2,12 +2,13 @@ displaying such info in the pipeline run header on run execution""" from textwrap import dedent +from requests import get from textual import on from textual.app import ComposeResult from textual.containers import Center, Horizontal from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Markdown +from textual.widgets import Button, Footer, Header, Input, Markdown, Select from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context from nf_core.utils import add_hide_class, remove_hide_class @@ -54,10 +55,21 @@ def compose(self) -> ComposeResult: "Author Git(Hub) handle.", classes="column hide" if self.parent.CONFIG_TYPE == "pipeline" else "column", ) - yield TextInput( - "config_pipeline_name", - "Pipeline name", - "The pipeline name you want to create the config for.", + if self.parent.CONFIG_TYPE == "pipeline" and self.parent.NFCORE_CONFIG: + pipelines = self.get_valid_nfcore_pipelines() + else: + pipelines = [] + yield Markdown( + "Pipeline name.", + id="config_pipeline_name_text", + classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or not self.parent.NFCORE_CONFIG else "field_help", + ) + yield Select( + [(c, c) for c in pipelines], + prompt="The name of the nf-core pipeline you want to configure", + id="config_pipeline_name", + allow_blank=True, + type_to_search=True, classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or not self.parent.NFCORE_CONFIG else "", ) yield TextInput( @@ -84,6 +96,23 @@ def compose(self) -> ComposeResult: classes="cta", ) + def get_valid_nfcore_pipelines(self) -> list[str]: + url = "https://raw.githubusercontent.com/nf-core/website/refs/heads/main/public/pipeline_names.json" + response = get(url) + if response.status_code != 200: + # Allow fetch to fail safely + return [] + data = response.json() + # If fetch was successful, ensure pipeline list is valid + msg = "Error fetching nf-core pipeline list" + assert isinstance(data, dict), msg + assert "pipeline" in data, msg + pipelines = data["pipeline"] + assert isinstance(pipelines, list), msg + assert len(pipelines) > 0, msg + assert all([isinstance(p, str) for p in pipelines]), msg + return pipelines + ## Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs @on(Button.Pressed) def on_button_pressed(self, event: Button.Pressed) -> None: @@ -119,13 +148,16 @@ def on_screen_resume(self): add_hide_class(self.parent, "config_profile_url") if self.parent.NFCORE_CONFIG: remove_hide_class(self.parent, "config_pipeline_name") + remove_hide_class(self.parent, "config_pipeline_name_text") add_hide_class(self.parent, "config_pipeline_path") else: remove_hide_class(self.parent, "config_pipeline_path") add_hide_class(self.parent, "config_pipeline_name") + add_hide_class(self.parent, "config_pipeline_name_text") if self.parent.CONFIG_TYPE == "infrastructure": remove_hide_class(self.parent, "config_profile_contact") remove_hide_class(self.parent, "config_profile_handle") remove_hide_class(self.parent, "config_profile_url") add_hide_class(self.parent, "config_pipeline_name") + add_hide_class(self.parent, "config_pipeline_name_text") add_hide_class(self.parent, "config_pipeline_path") diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 69e21a6419..2d9001f2de 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -26,23 +26,23 @@ def compose(self) -> ComposeResult: ) yield TextInput( "default_process_ncpus", - "2", + "1", "Number of CPUs to use by default for all processes.", - "2", + "1", classes="column", ) yield TextInput( "default_process_memgb", - "8", + "6", "Amount of memory in GB to use by default for all processes.", - "8", + "6", classes="column", ) yield TextInput( "default_process_hours", - "1", + "4", "The default number of hours of walltime required for processes:", - "1", + "4", classes="column", ) yield Center( diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index d19a1ec752..8c762e018d 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -30,23 +30,23 @@ def compose(self) -> ComposeResult: ) yield TextInput( "custom_process_ncpus", - "2", + "1", "# CPUs:", - "2", + "1", classes="column", ) yield TextInput( "custom_process_memgb", - "8", + "6", "Memory (GB):", - "8", + "6", classes="column", ) yield TextInput( "custom_process_hours", - "1", + "4", "Walltime (hours):", - "1", + "4", classes="column", ) yield TextInput( diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 600a62bec5..532049e23f 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -240,7 +240,7 @@ def serial(self): else: return self.serial_pipeline() - @field_validator("general_config_name", "config_profile_description") + @field_validator("general_config_name") @classmethod def notempty(cls, v: str) -> str: """Check that string values are not empty.""" @@ -248,11 +248,21 @@ def notempty(cls, v: str) -> str: raise ValueError("Cannot be left empty.") return v + @field_validator("config_profile_description") + @classmethod + def notempty_nfcore(cls, v: str, info: ValidationInfo) -> str: + """Check that string values are not empty.""" + context = info.context + if context and context["is_nfcore"] and v.strip() == "": + raise ValueError("Cannot be left empty.") + return v + @field_validator("general_config_name") @classmethod - def all_lower_case(cls, v: str) -> str: + def all_lower_case(cls, v: str, info: ValidationInfo) -> str: """Check that string values are all lower-case.""" - if not v.islower(): + context = info.context + if context and context["is_nfcore"] and not v.islower(): raise ValueError("Config names must not contain upper-case letters.") return v @@ -293,7 +303,7 @@ def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: def notempty_contact(cls, v: str, info: ValidationInfo) -> str: """Check that contact values are not empty when the config is infrastructure.""" context = info.context - if context and context["is_infrastructure"]: + if context and context["is_infrastructure"] and context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") return v @@ -306,7 +316,7 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: """Check that GitHub handles start with '@'. Make providing a handle mandatory for nf-core configs""" context = info.context - if context and context["is_infrastructure"]: + if context and context["is_infrastructure"] and context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") if not v.strip() == "" and not re.match(r"^@[aA-zZ\d](?:[aA-zZ\d]|-(?=[aA-zZ\d])){0,38}$", v): @@ -321,7 +331,7 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: def url_prefix(cls, v: str, info: ValidationInfo) -> str: """Check that institutional web links start with valid URL prefix.""" context = info.context - if context and context["is_infrastructure"]: + if context and context["is_infrastructure"] and context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") elif not re.match( From 17bda0afcaaf4029d55a0792ab4a34b319c7cba9 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Mon, 11 May 2026 15:59:47 +1000 Subject: [PATCH 104/121] WIP: implementing more feedback --- nf_core/configs/create/basicdetails.py | 17 +++--- nf_core/configs/create/multiprocessres.py | 60 ++++++------------- .../configs/create/pipelineconfigquestion.py | 3 +- nf_core/configs/create/utils.py | 32 ++++++++-- nf_core/textual.tcss | 30 +++++++--- 5 files changed, 77 insertions(+), 65 deletions(-) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index d76057a257..959b8b3413 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -24,6 +24,10 @@ class BasicDetails(Screen): """Name, description, author, etc.""" + def __init__(self) -> None: + super().__init__() + self.nf_core_pipelines = self.get_valid_nfcore_pipelines() + def compose(self) -> ComposeResult: yield Header() yield Footer() @@ -55,17 +59,13 @@ def compose(self) -> ComposeResult: "Author Git(Hub) handle.", classes="column hide" if self.parent.CONFIG_TYPE == "pipeline" else "column", ) - if self.parent.CONFIG_TYPE == "pipeline" and self.parent.NFCORE_CONFIG: - pipelines = self.get_valid_nfcore_pipelines() - else: - pipelines = [] yield Markdown( "Pipeline name.", id="config_pipeline_name_text", classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or not self.parent.NFCORE_CONFIG else "field_help", ) yield Select( - [(c, c) for c in pipelines], + [(c, c) for c in self.nf_core_pipelines], prompt="The name of the nf-core pipeline you want to configure", id="config_pipeline_name", allow_blank=True, @@ -98,13 +98,13 @@ def compose(self) -> ComposeResult: def get_valid_nfcore_pipelines(self) -> list[str]: url = "https://raw.githubusercontent.com/nf-core/website/refs/heads/main/public/pipeline_names.json" + msg = "Error fetching nf-core pipeline list" response = get(url) if response.status_code != 200: - # Allow fetch to fail safely + # Allow the fetch to fail, e.g. while offline and configuring non-nf-core pipelines return [] data = response.json() # If fetch was successful, ensure pipeline list is valid - msg = "Error fetching nf-core pipeline list" assert isinstance(data, dict), msg assert "pipeline" in data, msg pipelines = data["pipeline"] @@ -129,6 +129,9 @@ def on_button_pressed(self, event: Button.Pressed) -> None: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: text_input.query_one(".validation_msg").update("") + if self.parent.NFCORE_CONFIG: + select = self.query_one("#config_pipeline_name", Select) + config["config_pipeline_name"] = select.value try: with init_context(self.parent.get_context()): self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index 8c762e018d..265304c081 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -18,71 +18,47 @@ def __init__(self, selector: str, hpc: bool) -> None: assert selector in ["name", "label"] self.selector = selector self.hpc = hpc - self.use_local_exec = False def compose(self) -> ComposeResult: yield TextInput( - "custom_process_id", + f"custom_process_{self.selector}_id", "", f"Process {self.selector}:", "", - classes="column", + classes="custom-process-id", ) yield TextInput( "custom_process_ncpus", "1", "# CPUs:", "1", - classes="column", + classes="custom-process-number", ) yield TextInput( "custom_process_memgb", "6", "Memory (GB):", "6", - classes="column", + classes="custom-process-number", ) yield TextInput( "custom_process_hours", "4", "Walltime (hours):", "4", - classes="column", + classes="custom-process-number", ) yield TextInput( "custom_process_queue", "queue name", - "HPC queue:", + "HPC queue (optional):", "", - classes=("column" + (" hide" if not self.hpc else "")), + classes=("custom-process-queue" + (" hide" if not self.hpc else "")), ) - with Vertical( - classes=("labelled-toggle column" + (" hide" if not self.hpc else "")), id="toggle_use_local_exec_group" - ): - yield Label("Use local executor?", id="toggle_use_local_exec_label", classes="field_help") - with Horizontal(classes="labelled-toggle"): - yield Switch(id="toggle_use_local_exec", value=self.use_local_exec) - yield Label( - "Yes" if self.use_local_exec else "No", - id="toggle_use_local_exec_state_label", - classes="toggle_use_local_exec_state_label", - ) - yield Static(classes="labelled-toggle-filler") # Filler - with Vertical(classes="labelled-toggle"): + with Vertical(classes="remove-process-group"): yield Label("Remove", id="remove-process-config", classes="field_help") yield Button("-", id="remove", variant="error", classes="remove-process-button") - yield Static(classes="labelled-toggle-filler") # Filler - - @on(Switch.Changed, "#toggle_use_local_exec") - def on_toggle_switch(self, event: Switch.Changed) -> None: - """Handle toggling the local executor switch""" - - self.use_local_exec = event.value - - # Update the switch label - for label in self.query(Label): - if label.id == f"{event.switch.id}_state_label": - label.update("Yes" if event.value else "No") + yield Static(classes="remove-process-group-filler") # Filler @on(Button.Pressed, "#remove") def remove_widget(self) -> None: @@ -90,11 +66,11 @@ def remove_widget(self) -> None: def update_hpc_status(self, hpc: bool) -> None: self.hpc = hpc - for id in ["custom_process_queue", "toggle_use_local_exec_group"]: - if self.hpc: - self.get_widget_by_id(id).remove_class("hide") - else: - self.get_widget_by_id(id).add_class("hide") + id = "custom_process_queue" + if self.hpc: + self.get_widget_by_id(id).remove_class("hide") + else: + self.get_widget_by_id(id).add_class("hide") class MultiProcessConfig(Screen): @@ -105,6 +81,7 @@ def __init__(self, selector_type: str, config_key: str, title: str) -> None: assert isinstance(title, str) and title self.title = title assert isinstance(selector_type, str) and selector_type + assert selector_type in ["name", "label"] self.selector_type = selector_type assert isinstance(config_key, str) and config_key self.config_key = config_key @@ -154,10 +131,6 @@ def save_and_load_next_screen(self) -> None: ) else: text_input.query_one(".validation_msg").update("") - if self.parent.PIPE_CONF_HPC: - local_exec_switch = config_widget.query_one("#toggle_use_local_exec") - if local_exec_switch.value: - tmp_config["executor"] = "local" # Validate the config with init_context(self.parent.get_context()): ConfigsCreateConfig(**tmp_config) @@ -166,7 +139,8 @@ def save_and_load_next_screen(self) -> None: # Add to the final config new_config = {} for tmp_config in config_list: - process_id = tmp_config.get("custom_process_id") + process_id_field = f"custom_process_{self.selector_type}_id" + process_id = tmp_config.get(process_id_field) new_config[process_id] = tmp_config self.parent.TEMPLATE_CONFIG = self.parent.TEMPLATE_CONFIG.model_copy(update={self.config_key: new_config}) # Push the next screen diff --git a/nf_core/configs/create/pipelineconfigquestion.py b/nf_core/configs/create/pipelineconfigquestion.py index a7b0fec28c..a3991acb58 100644 --- a/nf_core/configs/create/pipelineconfigquestion.py +++ b/nf_core/configs/create/pipelineconfigquestion.py @@ -65,7 +65,8 @@ def compose(self) -> ComposeResult: ## What's the difference? - Choosing _"Yes"_ will allow you to configure additional HPC-specific directives for each process, including `queue` and `executor`. + Choosing _"Yes"_ will allow you to additionally specify an HPC `queue` for each process or label. + **Note** that this field can be left blank to use the default queue. """ ) ) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 532049e23f..94a6006ef3 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -63,8 +63,10 @@ class ConfigsCreateConfig(BaseModel): """ Default amount of memory """ default_process_hours: str | None = None """ Default walltime - hours """ - custom_process_id: str | None = None - """" Name or label of a process to configure """ + custom_process_name_id: str | None = None + """" Name of a process to configure """ + custom_process_label_id: str | None = None + """" Label of a process to configure """ custom_process_ncpus: str | None = None """ Number of CPUs for process """ custom_process_memgb: str | None = None @@ -351,14 +353,34 @@ def url_prefix(cls, v: str, info: ValidationInfo) -> str: ) return v - @field_validator("custom_process_id") + @field_validator("custom_process_name_id") @classmethod - def notempty_process_name(cls, v: str, info: ValidationInfo) -> str: - """Check that the custom process name or label isn't empty.""" + def valid_process_name(cls, v: str, info: ValidationInfo) -> str: + """Check that the custom process name isn't empty and is valid.""" context = info.context if context and not context["is_infrastructure"]: if v.strip() == "": raise ValueError("Cannot be left empty.") + if context["is_nfcore"] and not re.match( + r"^[A-Z0-9_:.*]+$", + v, + ): + raise ValueError("Must be uppercase and contain only letters, numbers, `_`, `:`, `.`, and `*`") + return v + + @field_validator("custom_process_label_id") + @classmethod + def valid_process_label(cls, v: str, info: ValidationInfo) -> str: + """Check that the custom process label isn't empty and is valid.""" + context = info.context + if context and not context["is_infrastructure"]: + if v.strip() == "": + raise ValueError("Cannot be left empty.") + if context["is_nfcore"] and not re.match( + r"^[a-z0-9_]+$", + v, + ): + raise ValueError("Must be lowercase and contain only letters, numbers, and `_`") return v @field_validator("default_process_ncpus", "default_process_memgb", "custom_process_ncpus", "custom_process_memgb") diff --git a/nf_core/textual.tcss b/nf_core/textual.tcss index d7230c56de..812d3d72f9 100644 --- a/nf_core/textual.tcss +++ b/nf_core/textual.tcss @@ -29,22 +29,34 @@ .custom_grid Button { width: auto; } -.labelled-toggle { - padding: 1 1 1 1; +.custom-process-id { + width: 4fr; + min-width: 10; +} +.custom-process-number { + width: 1fr; + min-width: 10; +} +.custom-process-queue { + width: 2fr; + min-width: 10; +} +.remove-process-group { + padding: 1 0 1 0; content-align-vertical: middle; height: 100%; + width: 10; } -.toggle_use_local_exec_state_label { - padding: 1 1 1 1; - align-vertical: middle; -} -.labelled-toggle-filler { +.remove-process-group-filler { padding: 1 1 1 1; content-align-vertical: middle; } .remove-process-button { - margin: 1 1 1 1; + margin: 1 0 1 0; + padding: 0 0 0 0; align-vertical: middle; + min-width: 7; + width: 7; } .text-input-grid { padding: 1 1 1 1; @@ -154,4 +166,4 @@ Vertical{ } .ghrepo-cols Button { margin-top: 2; -} +} \ No newline at end of file From 803b66cb65779d96f8d1bfb9bf377089c3b3adeb Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 12 May 2026 12:13:54 +1000 Subject: [PATCH 105/121] Fix validation for nf core pipeline configs --- nf_core/configs/create/basicdetails.py | 71 +++++++++++++++++--------- nf_core/configs/create/utils.py | 4 +- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 959b8b3413..d7991a4682 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -7,6 +7,7 @@ from textual import on from textual.app import ComposeResult from textual.containers import Center, Horizontal +from textual.events import Mount, ScreenResume from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown, Select @@ -97,20 +98,26 @@ def compose(self) -> ComposeResult: ) def get_valid_nfcore_pipelines(self) -> list[str]: + """Get a list of valid pipeline names. If the list can't be pulled, return an empty list.""" url = "https://raw.githubusercontent.com/nf-core/website/refs/heads/main/public/pipeline_names.json" - msg = "Error fetching nf-core pipeline list" - response = get(url) + try: + response = get(url) + except: + return [] if response.status_code != 200: - # Allow the fetch to fail, e.g. while offline and configuring non-nf-core pipelines return [] data = response.json() - # If fetch was successful, ensure pipeline list is valid - assert isinstance(data, dict), msg - assert "pipeline" in data, msg + if not isinstance(data, dict): + return [] + if not "pipeline" in data: + return [] pipelines = data["pipeline"] - assert isinstance(pipelines, list), msg - assert len(pipelines) > 0, msg - assert all([isinstance(p, str) for p in pipelines]), msg + if not isinstance(pipelines, list): + return [] + if not len(pipelines) > 0: + return [] + if not all([isinstance(p, str) for p in pipelines]): + return [] return pipelines ## Updates the __init__ initialised TEMPLATE_CONFIG object (which is built from the ConfigsCreateConfig class) with the values from the text inputs @@ -129,9 +136,12 @@ def on_button_pressed(self, event: Button.Pressed) -> None: text_input.query_one(".validation_msg").update("\n".join(validation_result.failure_descriptions)) else: text_input.query_one(".validation_msg").update("") - if self.parent.NFCORE_CONFIG: + if self.parent.CONFIG_TYPE == "pipeline" and self.parent.NFCORE_CONFIG: select = self.query_one("#config_pipeline_name", Select) - config["config_pipeline_name"] = select.value + if select.value != Select.BLANK: + config["config_pipeline_name"] = select.value + elif not config.get("config_pipeline_name", ""): + config["config_pipeline_name"] = "" try: with init_context(self.parent.get_context()): self.parent.TEMPLATE_CONFIG = ConfigsCreateConfig(**config) @@ -143,24 +153,35 @@ def on_button_pressed(self, event: Button.Pressed) -> None: except ValueError: pass - def on_screen_resume(self): - """Show or hide form fields on resume depending on config type.""" - if self.parent.CONFIG_TYPE == "pipeline": + @on(Mount) + @on(ScreenResume) + def show_hide_fields(self) -> None: + """Show/hide fields depending on config type.""" + # Show/hide nf-core required fields + if self.parent.NFCORE_CONFIG: + remove_hide_class(self.parent, "config_profile_contact") + remove_hide_class(self.parent, "config_profile_handle") + remove_hide_class(self.parent, "config_profile_url") + else: add_hide_class(self.parent, "config_profile_contact") add_hide_class(self.parent, "config_profile_handle") add_hide_class(self.parent, "config_profile_url") - if self.parent.NFCORE_CONFIG: - remove_hide_class(self.parent, "config_pipeline_name") - remove_hide_class(self.parent, "config_pipeline_name_text") - add_hide_class(self.parent, "config_pipeline_path") - else: - remove_hide_class(self.parent, "config_pipeline_path") - add_hide_class(self.parent, "config_pipeline_name") - add_hide_class(self.parent, "config_pipeline_name_text") + + # Hide pipeline fields when in infrastructure mode if self.parent.CONFIG_TYPE == "infrastructure": - remove_hide_class(self.parent, "config_profile_contact") - remove_hide_class(self.parent, "config_profile_handle") - remove_hide_class(self.parent, "config_profile_url") add_hide_class(self.parent, "config_pipeline_name") add_hide_class(self.parent, "config_pipeline_name_text") add_hide_class(self.parent, "config_pipeline_path") + return + + # Hide pipeline name fields and show pipeline path field for custom pipeline configs + if not self.parent.NFCORE_CONFIG: + remove_hide_class(self.parent, "config_pipeline_path") + add_hide_class(self.parent, "config_pipeline_name") + add_hide_class(self.parent, "config_pipeline_name_text") + return + + # For nf-core pipelines, hide the pipeline path field and show the dropdown box + add_hide_class(self.parent, "config_pipeline_path") + remove_hide_class(self.parent, "config_pipeline_name") + remove_hide_class(self.parent, "config_pipeline_name_text") diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 94a6006ef3..72f9f63929 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -273,7 +273,7 @@ def all_lower_case(cls, v: str, info: ValidationInfo) -> str: def path_valid(cls, v: str, info: ValidationInfo) -> str: """Check that a path is valid.""" context = info.context - if context and (not context["is_infrastructure"] and not context["is_nfcore"]): + if context and not context["is_infrastructure"] and not context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") if not Path(v).is_dir(): @@ -295,7 +295,7 @@ def final_path_valid(cls, v: str, info: ValidationInfo) -> str: def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: """Check that an nf-core pipeline name is valid.""" context = info.context - if context and (not context["is_infrastructure"] and context["is_nfcore"]): + if context and not context["is_infrastructure"] and context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") return v From 5eb11efdea080a640ca10d78a724617e70a34657 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 13 May 2026 10:58:12 +1000 Subject: [PATCH 106/121] WIP: continue adding tui tests --- tests/configs/test_create_app.py | 584 ++++++++++++++++++++++++++++++- 1 file changed, 572 insertions(+), 12 deletions(-) diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py index 28eb458f6a..057c0e83d9 100644 --- a/tests/configs/test_create_app.py +++ b/tests/configs/test_create_app.py @@ -6,7 +6,7 @@ from nf_core.configs.create import ConfigsCreateApp from nf_core.configs.create.utils import SUPPORTED_CONTAINERS -from ..test_configs import INFRA_SINGULARITY_CONFIG +from ..test_configs import INFRA_SINGULARITY_CONFIG, INFRA_CUSTOM_SINGULARITY_CONFIG, PIPE_NFCORE_CONFIG, PIPE_CUSTOM_CONFIG INIT_FILE = "../../nf_core/configs/create/__init__.py" @@ -148,8 +148,10 @@ async def run_before(pilot) -> None: async def enter_infra_nfcore_hpc_details(pilot) -> None: await submit_infra_nfcore_basic_details(pilot) await pilot.click("#type_hpc") + # Activate the drop-down and select the PBS Pro entry await pilot.click("#scheduler") - await pilot.press(*list("pbspro")) + await pilot.press(*list("PBS Pro")) + await pilot.press("enter") await pilot.press("tab") await pilot.press(*list("myqueue")) await pilot.press("tab") @@ -209,10 +211,7 @@ def enter_infra_final_details(container): async def _enter_infra_final_details(pilot) -> None: await submit_infra_nfcore_hpc_details(pilot) await pilot.click("#container_system") - for container_system in SUPPORTED_CONTAINERS: - if container_system == container: - break - await pilot.press("down") + await pilot.press(*list(container)) await pilot.press("enter") await pilot.press("tab") await pilot.press(*list("128")) @@ -227,7 +226,7 @@ async def _enter_infra_final_details(pilot) -> None: await pilot.press("tab") await pilot.press(*list("25")) await pilot.press("tab") - if container in ["singularity", "charliecloud"]: + if container in ["singularity", "charliecloud", "apptainer", "conda"]: await pilot.press(*list(f"/{container}/cachedir")) await pilot.press("tab") await pilot.press(*list("/data/igenomes/cache")) @@ -360,6 +359,11 @@ async def test_writing_infra_details_singularity(): tmpdir.cleanup() +async def click_infra_custom(pilot) -> None: + await click_type_infrastructure(pilot) + await pilot.click("#type_custom") + + def test_infra_custom_basic_details(snap_compare): """Test snapshot for the custom pipeline basic_details screen when configuring an infrastructure config. @@ -371,13 +375,271 @@ def test_infra_custom_basic_details(snap_compare): """ async def run_before(pilot) -> None: - await pilot.click("#lets_go") - await pilot.click("#type_infrastructure") - await pilot.click("#type_custom") + await click_infra_custom(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def enter_infra_custom_basic_details(pilot) -> None: + await click_infra_custom(pilot) + await pilot.click("#general_config_name") + await pilot.press(*list("myconfig")) + await pilot.press("tab") + await pilot.press(*list("A cool description")) + + +async def submit_infra_custom_basic_details(pilot) -> None: + await enter_infra_custom_basic_details(pilot) + await pilot.click("#next") + + +def test_entering_infra_custom_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the custom basic_details screen + when configuring an infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + screen basic_details + """ + + async def run_before(pilot) -> None: + await enter_infra_custom_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_submitting_infra_custom_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the custom basic_details screen + when configuring an infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + enter basic details > + click Next > + screen hpc_question + """ + + async def run_before(pilot) -> None: + await submit_infra_custom_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def enter_infra_custom_hpc_details(pilot) -> None: + await submit_infra_custom_basic_details(pilot) + await pilot.click("#type_hpc") + # Activate the drop-down and select the PBS Pro entry + await pilot.click("#scheduler") + await pilot.press(*list("PBS Pro")) + await pilot.press("enter") + await pilot.press("tab") + await pilot.press(*list("myqueue")) + await pilot.press("tab") + await pilot.press(*list("0.75")) + + +async def submit_infra_custom_hpc_details(pilot) -> None: + await enter_infra_custom_hpc_details(pilot) + await pilot.click("#toconfiguration") + + +def test_entering_infra_custom_hpc_details(snap_compare): + """Test snapshot for entering HPC details + when configuring a custom infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + enter basic details > + click Next > + click HPC > + enter HPC details + """ + + async def run_before(pilot) -> None: + await enter_infra_custom_hpc_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_submitting_infra_custom_hpc_details(snap_compare): + """Test snapshot for entering HPC details + when configuring a custom infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + screen final_infra_details + """ + + async def run_before(pilot) -> None: + await submit_infra_custom_hpc_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def enter_infra_custom_final_details(container): + assert container in SUPPORTED_CONTAINERS + + async def _enter_infra_custom_final_details(pilot) -> None: + await submit_infra_custom_hpc_details(pilot) + await pilot.click("#container_system") + await pilot.press(*list(container)) + await pilot.press("enter") + await pilot.press("tab") + await pilot.press(*list("128")) + await pilot.press("tab") + await pilot.press(*list("48")) + await pilot.press("tab") + await pilot.press(*list("9.5")) + await pilot.press("tab") + await pilot.press(*list("200")) + await pilot.press("tab") + await pilot.press(*list("0.25")) + await pilot.press("tab") + await pilot.press(*list("25")) + await pilot.press("tab") + if container in ["singularity", "charliecloud", "apptainer", "conda"]: + await pilot.press(*list(f"/{container}/cachedir")) + await pilot.press("tab") + await pilot.press(*list("/tmp/scratch")) + await pilot.press("tab") + await pilot.press("enter") + await pilot.press("tab") + await pilot.press(*list("2")) + + return _enter_infra_custom_final_details + + +def submit_infra_custom_final_details(container): + async def _submit_infra_custom_final_details(pilot) -> None: + await enter_infra_custom_final_details(container)(pilot) + await pilot.click("#finish") + + return _submit_infra_custom_final_details + + +def write_infra_custom_details(container, outdir="."): + assert isinstance(outdir, str) + assert Path(outdir).is_dir() + + async def _write_infra_custom_details(pilot) -> None: + await submit_infra_custom_final_details(container)(pilot) + await pilot.click("#savelocation") + await pilot.press(*list(outdir)) + await pilot.click("#close_app") + + return _write_infra_custom_details + + +def test_entering_infra_custom_final_details_docker(snap_compare): + """Test snapshot for entering HPC details + when configuring a custom infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + screen final_infra_details + """ + + async def run_before(pilot) -> None: + await enter_infra_custom_final_details("docker")(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_entering_infra_custom_final_details_singularity(snap_compare): + """Test snapshot for entering HPC details + when configuring a custom infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + screen final_infra_details + """ + + async def run_before(pilot) -> None: + await enter_infra_custom_final_details("singularity")(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submitting_infra_custom_final_details_singularity(snap_compare): + """Test snapshot for entering HPC details + when configuring a custom infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + click Finish > + screen final + """ + + async def run_before(pilot) -> None: + await submit_infra_custom_final_details("singularity")(pilot) assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) +async def test_writing_infra_custom_details_singularity(): + """Test snapshot for entering HPC details + when configuring a custom infrastructure config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Infrastructure config > + press custom > + enter basic details > + click Next > + click HPC > + enter HPC details > + click Continue > + click Finish > + click Save and close! + """ + + app = ConfigsCreateApp() + async with app.run_test(size=(100, 50)) as pilot: + # Make a temporary directory to store the output + tmpdir = TemporaryDirectory() + + await write_infra_custom_details("singularity", outdir=tmpdir.name)(pilot) + + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + assert config == INFRA_CUSTOM_SINGULARITY_CONFIG + tmpdir.cleanup() + + +async def click_type_pipe(pilot) -> None: + await click_lets_go(pilot) + await pilot.click("#type_pipeline") + + def test_choose_pipe_type(snap_compare): """Test snapshot for the nfcore_question screen via the Pipeline config option. @@ -388,7 +650,305 @@ def test_choose_pipe_type(snap_compare): """ async def run_before(pilot) -> None: - await pilot.click("#lets_go") - await pilot.click("#type_pipeline") + await click_type_pipe(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def click_pipe_nfcore(pilot) -> None: + await click_type_pipe(pilot) + await pilot.click("#type_nfcore") + + +def test_pipe_nfcore_basic_details(snap_compare): + """Test snapshot for the nf-core basic details screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + screen basic_details + """ + + async def run_before(pilot) -> None: + await click_pipe_nfcore(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def enter_pipe_nfcore_basic_details(pilot) -> None: + await click_pipe_nfcore(pilot) + await pilot.click("#general_config_name") + await pilot.press(*list("myconfig")) + await pilot.press("tab") + await pilot.press(*list("my name")) + await pilot.press("tab") + await pilot.press(*list("@myhandle")) + await pilot.click("#config_pipeline_name") + await pilot.press(*list("scdownstream")) + await pilot.press("enter") + await pilot.press("tab") + await pilot.press(*list("A cool description")) + await pilot.press("tab") + await pilot.press(*list("https://example.com")) + + +async def submit_pipe_nfcore_basic_details(pilot) -> None: + await enter_pipe_nfcore_basic_details(pilot) + await pilot.click("#next") + + +def test_entering_pipe_nfcore_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the nf-core basic_details screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + screen basic_details + """ + + async def run_before(pilot) -> None: + await enter_pipe_nfcore_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_submitting_pipe_nfcore_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the nf-core basic_details screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question + """ + + async def run_before(pilot) -> None: + await submit_pipe_nfcore_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def set_pipe_nfcore_options(pilot) -> None: + await submit_pipe_nfcore_basic_details(pilot) + await pilot.click("#toggle_configure_defaults") + await pilot.press("tab") + await pilot.press("enter") + await pilot.press("tab") + await pilot.press("enter") + await pilot.press("tab") + await pilot.press("enter") + + +async def submit_pipe_nfcore_options(pilot) -> None: + await set_pipe_nfcore_options(pilot) + await pilot.click("#next") + + +def test_setting_pipe_nfcore_options(snap_compare): + """Test snapshot for the setting the configure options + on the nf-core pipeline_config_question screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question + """ + + async def run_before(pilot) -> None: + await set_pipe_nfcore_options(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_submitting_pipe_nfcore_options(snap_compare): + """Test snapshot for the setting the configure options + on the nf-core pipeline_config_question screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources + """ + + async def run_before(pilot) -> None: + await submit_pipe_nfcore_options(pilot) assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def enter_pipe_nfcore_default_res(pilot) -> None: + await submit_pipe_nfcore_options(pilot) + await pilot.click("#default_process_ncpus") + await pilot.press("2") + await pilot.press("tab") + await pilot.press("8") + await pilot.press("tab") + await pilot.press(*list("9.5")) + await pilot.press("tab") + + +async def submit_pipe_nfcore_default_res(pilot) -> None: + await enter_pipe_nfcore_default_res(pilot) + await pilot.click("#next") + + +async def enter_pipe_nfcore_named_proc_res(pilot) -> None: + await submit_pipe_nfcore_default_res(pilot) + await pilot.click("#custom_process_name_id") + await pilot.press(*list("SOME:PROC.NAME*")) + await pilot.press("tab") + await pilot.press("3") + await pilot.press("tab") + await pilot.press("4") + await pilot.press("tab") + await pilot.press(*list("5.5")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press(*list("ANOTHER:PROC_NAME")) + await pilot.press("tab") + await pilot.press("2") + await pilot.press("tab") + await pilot.press("3") + await pilot.press("tab") + await pilot.press(*list("4.4")) + await pilot.press("tab") + await pilot.press(*list("customqueue")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("enter") + + +async def submit_pipe_nfcore_named_proc_res(pilot) -> None: + await enter_pipe_nfcore_named_proc_res(pilot) + await pilot.click("#next") + + +async def enter_pipe_nfcore_labelled_proc_res(pilot) -> None: + await submit_pipe_nfcore_default_res(pilot) + await pilot.click("#custom_process_name_id") + await pilot.press(*list("some_proc_label")) + await pilot.press("tab") + await pilot.press("1") + await pilot.press("tab") + await pilot.press("1") + await pilot.press("tab") + await pilot.press(*list("1.1")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press(*list("another_label")) + await pilot.press("tab") + await pilot.press("2") + await pilot.press("tab") + await pilot.press("2") + await pilot.press("tab") + await pilot.press(*list("2.2")) + await pilot.press("tab") + await pilot.press(*list("customqueue")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press(*list("a_third_label")) + await pilot.press("tab") + await pilot.press("backspace") + await pilot.press("tab") + await pilot.press("backspace") + await pilot.press("tab") + await pilot.press("backspace") + await pilot.click("#another") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press(*list("fourth_label")) + await pilot.press("tab") + await pilot.press("4") + await pilot.press("tab") + await pilot.press("backspace") + await pilot.press("tab") + await pilot.press(*list("4.4")) + await pilot.press("tab") + await pilot.press(*list("anotherqueue")) + + +async def submit_pipe_nfcore_labelled_proc_res(pilot) -> None: + await enter_pipe_nfcore_labelled_proc_res(pilot) + await pilot.click("#next") + + +# TODO: Write test_ functions for entering and submitting info on the three resource screens + + +def write_pipe_nfcore_details(outdir="."): + assert isinstance(outdir, str) + assert Path(outdir).is_dir() + + async def _write_pipe_nfcore_details(pilot) -> None: + await submit_pipe_nfcore_labelled_proc_res(pilot) + await pilot.click("#savelocation") + await pilot.press(*list(outdir)) + await pilot.click("#close_app") + + return _write_pipe_nfcore_details + + +async def test_writing_pipe_nfcore_details(): + """Test snapshot for writing a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + enter resources > + click Next > + screen named_process_resources > + enter resources > + click Next > + screen labelled_process_resources > + enter resources > + click next > + click Save and close! + """ + + app = ConfigsCreateApp() + async with app.run_test(size=(100, 50)) as pilot: + # Make a temporary directory to store the output + tmpdir = TemporaryDirectory() + + await write_pipe_nfcore_details(outdir=tmpdir.name)(pilot) + + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + assert config == PIPE_NFCORE_CONFIG + tmpdir.cleanup() + + +# TODO: Add custom pipeline config functions and tests +# TODO: Update INFRA_SINGULARITY_CONFIG +# TODO: Add comparison configs for INFRA_CUSTOM_SINGULARITY_CONFIG, PIPE_NFCORE_CONFIG, and PIPE_CUSTOM_CONFIG From 0420d1d2ef07164db6504aea09ca12a7598f66b0 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Mon, 18 May 2026 16:37:24 +1000 Subject: [PATCH 107/121] finish adding tui tests, test writing configs --- nf_core/configs/create/multiprocessres.py | 2 +- tests/configs/test_create_app.py | 582 +++++++++++++++++++++- tests/test_configs.py | 107 ++++ 3 files changed, 676 insertions(+), 15 deletions(-) diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index 265304c081..95d1942575 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -110,7 +110,7 @@ def compose(self) -> ComposeResult: @on(Button.Pressed, "#another") def add_config(self) -> None: - new_config = ProcessConfig(selector="name", hpc=self.parent.PIPE_CONF_HPC) + new_config = ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC) self.query_one("#configs").mount(new_config) @on(Button.Pressed, "#next") diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py index 057c0e83d9..f42796b54d 100644 --- a/tests/configs/test_create_app.py +++ b/tests/configs/test_create_app.py @@ -325,7 +325,7 @@ def test_submitting_infra_final_details_singularity(snap_compare): async def run_before(pilot) -> None: await submit_infra_final_details("singularity")(pilot) - assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) async def test_writing_infra_details_singularity(): @@ -346,7 +346,7 @@ async def test_writing_infra_details_singularity(): """ app = ConfigsCreateApp() - async with app.run_test(size=(100, 50)) as pilot: + async with app.run_test(size=(100, 100)) as pilot: # Make a temporary directory to store the output tmpdir = TemporaryDirectory() @@ -602,7 +602,7 @@ def test_submitting_infra_custom_final_details_singularity(snap_compare): async def run_before(pilot) -> None: await submit_infra_custom_final_details("singularity")(pilot) - assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) async def test_writing_infra_custom_details_singularity(): @@ -622,7 +622,7 @@ async def test_writing_infra_custom_details_singularity(): """ app = ConfigsCreateApp() - async with app.run_test(size=(100, 50)) as pilot: + async with app.run_test(size=(100, 100)) as pilot: # Make a temporary directory to store the output tmpdir = TemporaryDirectory() @@ -766,7 +766,7 @@ def test_setting_pipe_nfcore_options(snap_compare): async def run_before(pilot) -> None: await set_pipe_nfcore_options(pilot) - assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) def test_submitting_pipe_nfcore_options(snap_compare): @@ -787,7 +787,7 @@ def test_submitting_pipe_nfcore_options(snap_compare): async def run_before(pilot) -> None: await submit_pipe_nfcore_options(pilot) - assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) async def enter_pipe_nfcore_default_res(pilot) -> None: @@ -808,7 +808,7 @@ async def submit_pipe_nfcore_default_res(pilot) -> None: async def enter_pipe_nfcore_named_proc_res(pilot) -> None: await submit_pipe_nfcore_default_res(pilot) - await pilot.click("#custom_process_name_id") + await pilot.press("tab") await pilot.press(*list("SOME:PROC.NAME*")) await pilot.press("tab") await pilot.press("3") @@ -844,8 +844,8 @@ async def submit_pipe_nfcore_named_proc_res(pilot) -> None: async def enter_pipe_nfcore_labelled_proc_res(pilot) -> None: - await submit_pipe_nfcore_default_res(pilot) - await pilot.click("#custom_process_name_id") + await submit_pipe_nfcore_named_proc_res(pilot) + await pilot.press("tab") await pilot.press(*list("some_proc_label")) await pilot.press("tab") await pilot.press("1") @@ -897,7 +897,143 @@ async def submit_pipe_nfcore_labelled_proc_res(pilot) -> None: await pilot.click("#next") -# TODO: Write test_ functions for entering and submitting info on the three resource screens +def test_enter_pipe_nfcore_default_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources + """ + + async def run_before(pilot) -> None: + await enter_pipe_nfcore_default_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submit_pipe_nfcore_default_res(snap_compare): + """Test snapshot for the submitting default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources + """ + + async def run_before(pilot) -> None: + await submit_pipe_nfcore_default_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_enter_pipe_nfcore_named_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources + """ + + async def run_before(pilot) -> None: + await enter_pipe_nfcore_named_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submit_pipe_nfcore_named_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources > + click Next > + screen labelled_process_resources + """ + + async def run_before(pilot) -> None: + await submit_pipe_nfcore_named_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_enter_pipe_nfcore_labelled_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources > + click Next > + screen labelled_process_resources + """ + + async def run_before(pilot) -> None: + await enter_pipe_nfcore_labelled_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submit_pipe_nfcore_labelled_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press nf-core > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources > + click Next > + screen labelled_process_resources > + click Next > + screen labelled_process_resources > + click Next + """ + + async def run_before(pilot) -> None: + await submit_pipe_nfcore_labelled_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) def write_pipe_nfcore_details(outdir="."): @@ -936,7 +1072,7 @@ async def test_writing_pipe_nfcore_details(): """ app = ConfigsCreateApp() - async with app.run_test(size=(100, 50)) as pilot: + async with app.run_test(size=(100, 100)) as pilot: # Make a temporary directory to store the output tmpdir = TemporaryDirectory() @@ -949,6 +1085,424 @@ async def test_writing_pipe_nfcore_details(): tmpdir.cleanup() -# TODO: Add custom pipeline config functions and tests -# TODO: Update INFRA_SINGULARITY_CONFIG -# TODO: Add comparison configs for INFRA_CUSTOM_SINGULARITY_CONFIG, PIPE_NFCORE_CONFIG, and PIPE_CUSTOM_CONFIG +async def click_pipe_custom(pilot) -> None: + await click_type_pipe(pilot) + await pilot.click("#type_custom") + + +def test_pipe_custom_basic_details(snap_compare): + """Test snapshot for the custom basic details screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + screen basic_details + """ + + async def run_before(pilot) -> None: + await click_pipe_custom(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def enter_pipe_custom_basic_details(pilot) -> None: + await click_pipe_custom(pilot) + await pilot.click("#general_config_name") + await pilot.press(*list("myconfig")) + await pilot.press("tab") + await pilot.press(*list(".")) + await pilot.press("tab") + await pilot.press(*list("A cool description")) + + +async def submit_pipe_custom_basic_details(pilot) -> None: + await enter_pipe_custom_basic_details(pilot) + await pilot.click("#next") + + +def test_entering_pipe_custom_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the custom basic_details screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + screen basic_details + """ + + async def run_before(pilot) -> None: + await enter_pipe_custom_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +def test_submitting_pipe_custom_basic_details(snap_compare): + """Test snapshot for the entering basic details + on the custom basic_details screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question + """ + + async def run_before(pilot) -> None: + await submit_pipe_custom_basic_details(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + + +async def set_pipe_custom_options(pilot) -> None: + await submit_pipe_custom_basic_details(pilot) + await pilot.click("#toggle_configure_defaults") + await pilot.press("tab") + await pilot.press("enter") + await pilot.press("tab") + await pilot.press("enter") + await pilot.press("tab") + await pilot.press("enter") + + +async def submit_pipe_custom_options(pilot) -> None: + await set_pipe_custom_options(pilot) + await pilot.click("#next") + + +def test_setting_pipe_custom_options(snap_compare): + """Test snapshot for the setting the configure options + on the custom pipeline_config_question screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question + """ + + async def run_before(pilot) -> None: + await set_pipe_custom_options(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submitting_pipe_custom_options(snap_compare): + """Test snapshot for the setting the configure options + on the custom pipeline_config_question screen + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources + """ + + async def run_before(pilot) -> None: + await submit_pipe_custom_options(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +async def enter_pipe_custom_default_res(pilot) -> None: + await submit_pipe_custom_options(pilot) + await pilot.click("#default_process_ncpus") + await pilot.press("2") + await pilot.press("tab") + await pilot.press("8") + await pilot.press("tab") + await pilot.press(*list("9.5")) + await pilot.press("tab") + + +async def submit_pipe_custom_default_res(pilot) -> None: + await enter_pipe_custom_default_res(pilot) + await pilot.click("#next") + + +async def enter_pipe_custom_named_proc_res(pilot) -> None: + await submit_pipe_custom_default_res(pilot) + await pilot.press("tab") + await pilot.press(*list("some_proc")) + await pilot.press("tab") + await pilot.press("3") + await pilot.press("tab") + await pilot.press("4") + await pilot.press("tab") + await pilot.press(*list("5.5")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press(*list("another_proc")) + await pilot.press("tab") + await pilot.press("2") + await pilot.press("tab") + await pilot.press("3") + await pilot.press("tab") + await pilot.press(*list("4.4")) + await pilot.press("tab") + await pilot.press(*list("customqueue")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("enter") + + +async def submit_pipe_custom_named_proc_res(pilot) -> None: + await enter_pipe_custom_named_proc_res(pilot) + await pilot.click("#next") + + +async def enter_pipe_custom_labelled_proc_res(pilot) -> None: + await submit_pipe_custom_named_proc_res(pilot) + await pilot.press("tab") + await pilot.press(*list("some_proc_label")) + await pilot.press("tab") + await pilot.press("1") + await pilot.press("tab") + await pilot.press("1") + await pilot.press("tab") + await pilot.press(*list("1.1")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press("tab") + await pilot.press(*list("another_label")) + await pilot.press("tab") + await pilot.press("2") + await pilot.press("tab") + await pilot.press("2") + await pilot.press("tab") + await pilot.press(*list("2.2")) + await pilot.press("tab") + await pilot.press(*list("customqueue")) + await pilot.press("tab") + await pilot.press("tab") + await pilot.press(*list("a_third_label")) + await pilot.press("tab") + await pilot.press("backspace") + await pilot.press("tab") + await pilot.press("backspace") + await pilot.press("tab") + await pilot.press("backspace") + await pilot.click("#another") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press("shift+tab") + await pilot.press(*list("fourth_label")) + await pilot.press("tab") + await pilot.press("4") + await pilot.press("tab") + await pilot.press("backspace") + await pilot.press("tab") + await pilot.press(*list("4.4")) + await pilot.press("tab") + await pilot.press(*list("anotherqueue")) + + +async def submit_pipe_custom_labelled_proc_res(pilot) -> None: + await enter_pipe_custom_labelled_proc_res(pilot) + await pilot.click("#next") + + +def test_enter_pipe_custom_default_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources + """ + + async def run_before(pilot) -> None: + await enter_pipe_custom_default_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submit_pipe_custom_default_res(snap_compare): + """Test snapshot for the submitting default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources + """ + + async def run_before(pilot) -> None: + await submit_pipe_custom_default_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_enter_pipe_custom_named_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources + """ + + async def run_before(pilot) -> None: + await enter_pipe_custom_named_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submit_pipe_custom_named_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources > + click Next > + screen labelled_process_resources + """ + + async def run_before(pilot) -> None: + await submit_pipe_custom_named_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_enter_pipe_custom_labelled_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources > + click Next > + screen labelled_process_resources + """ + + async def run_before(pilot) -> None: + await enter_pipe_custom_labelled_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def test_submit_pipe_custom_labelled_proc_res(snap_compare): + """Test snapshot for the entering default resources + when configuring a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + click Next > + screen named_process_resources > + click Next > + screen labelled_process_resources > + click Next > + screen labelled_process_resources > + click Next + """ + + async def run_before(pilot) -> None: + await submit_pipe_custom_labelled_proc_res(pilot) + + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) + + +def write_pipe_custom_details(outdir="."): + assert isinstance(outdir, str) + assert Path(outdir).is_dir() + + async def _write_pipe_custom_details(pilot) -> None: + await submit_pipe_custom_labelled_proc_res(pilot) + await pilot.click("#savelocation") + await pilot.press(*list(outdir)) + await pilot.click("#close_app") + + return _write_pipe_custom_details + + +async def test_writing_pipe_custom_details(): + """Test snapshot for writing a pipeline config. + Steps to get to this screen: + screen welcome > press Let's go! > + press Pipeline config > + press custom > + enter basic details > + click Next > + screen pipeline_config_question > + click Next > + screen default_process_resources > + enter resources > + click Next > + screen named_process_resources > + enter resources > + click Next > + screen labelled_process_resources > + enter resources > + click next > + click Save and close! + """ + + app = ConfigsCreateApp() + async with app.run_test(size=(100, 100)) as pilot: + # Make a temporary directory to store the output + tmpdir = TemporaryDirectory() + + await write_pipe_custom_details(outdir=tmpdir.name)(pilot) + + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + assert config == PIPE_CUSTOM_CONFIG + tmpdir.cleanup() diff --git a/tests/test_configs.py b/tests/test_configs.py index 39d74d6882..10d79a8847 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -25,3 +25,110 @@ } cleanup = true """ + +INFRA_CUSTOM_SINGULARITY_CONFIG = """\ +params { + config_profile_description = 'A cool description' +} +executor { + queueStatInterval = '0.75m' + queueSize = 200 + pollInterval = '0.25m' + submitRateLimit = '25min' +} +process { + executor = 'pbspro' + queue = 'myqueue' + resourceLimits = [cpus: 48, memory: '128.0GB', time: '9.5h'] + scratch = '/tmp/scratch' + maxRetries = 2 +} +singularity { + enabled = true + cacheDir = '/singularity/cachedir' + autoMounts = true +} +cleanup = true +""" + +PIPE_NFCORE_CONFIG = """\ +params { + config_profile_contact = 'my name (@myhandle)' + config_profile_description = 'A cool description' + config_profile_url = 'https://example.com' +} +process { + cpus = 2 + memory = '8.0GB' + time = '9.5h' + withName: 'SOME:PROC.NAME*' { + cpus = 3 + memory = '4.0GB' + time = '5.5h' + } + withName: 'ANOTHER:PROC_NAME' { + cpus = 2 + memory = '3.0GB' + time = '4.4h' + queue = 'customqueue' + } + withLabel: 'some_proc_label' { + cpus = 1 + memory = '1.0GB' + time = '1.1h' + } + withLabel: 'another_label' { + cpus = 2 + memory = '2.0GB' + time = '2.2h' + queue = 'customqueue' + } + withLabel: 'a_third_label' { + } + withLabel: 'fourth_label' { + cpus = 4 + time = '4.4h' + queue = 'anotherqueue' + } +} +""" + +PIPE_CUSTOM_CONFIG = """\ +params { + config_profile_description = 'A cool description' +} +process { + cpus = 2 + memory = '8.0GB' + time = '9.5h' + withName: 'some_proc' { + cpus = 3 + memory = '4.0GB' + time = '5.5h' + } + withName: 'another_proc' { + cpus = 2 + memory = '3.0GB' + time = '4.4h' + queue = 'customqueue' + } + withLabel: 'some_proc_label' { + cpus = 1 + memory = '1.0GB' + time = '1.1h' + } + withLabel: 'another_label' { + cpus = 2 + memory = '2.0GB' + time = '2.2h' + queue = 'customqueue' + } + withLabel: 'a_third_label' { + } + withLabel: 'fourth_label' { + cpus = 4 + time = '4.4h' + queue = 'anotherqueue' + } +} +""" \ No newline at end of file From d834f428b8841717205054167bcbc4c5a314085d Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 19 May 2026 09:32:27 +1000 Subject: [PATCH 108/121] Update TUI test snapshots --- .../test_create_app/test_choose_type.svg | 10 +- .../test_enter_pipe_custom_default_res.svg | 468 +++++++++++++++++ ...st_enter_pipe_custom_labelled_proc_res.svg | 472 +++++++++++++++++ .../test_enter_pipe_custom_named_proc_res.svg | 472 +++++++++++++++++ .../test_enter_pipe_nfcore_default_res.svg | 468 +++++++++++++++++ ...st_enter_pipe_nfcore_labelled_proc_res.svg | 472 +++++++++++++++++ .../test_enter_pipe_nfcore_named_proc_res.svg | 472 +++++++++++++++++ ...st_entering_infra_custom_basic_details.svg | 267 ++++++++++ ...ring_infra_custom_final_details_docker.svg | 476 ++++++++++++++++++ ...infra_custom_final_details_singularity.svg | 475 +++++++++++++++++ ...test_entering_infra_custom_hpc_details.svg | 269 ++++++++++ ...st_entering_infra_final_details_docker.svg | 152 +++--- ...tering_infra_final_details_singularity.svg | 150 +++--- ...test_entering_infra_nfcore_hpc_details.svg | 84 ++-- ...est_entering_pipe_custom_basic_details.svg | 267 ++++++++++ ...est_entering_pipe_nfcore_basic_details.svg | 269 ++++++++++ .../test_infra_custom_basic_details.svg | 36 +- .../test_pipe_custom_basic_details.svg | 271 ++++++++++ .../test_pipe_nfcore_basic_details.svg | 271 ++++++++++ .../test_setting_pipe_custom_options.svg | 473 +++++++++++++++++ .../test_setting_pipe_nfcore_options.svg | 473 +++++++++++++++++ .../test_submit_pipe_custom_default_res.svg | 472 +++++++++++++++++ ...t_submit_pipe_custom_labelled_proc_res.svg | 467 +++++++++++++++++ ...test_submit_pipe_custom_named_proc_res.svg | 472 +++++++++++++++++ .../test_submit_pipe_nfcore_default_res.svg | 472 +++++++++++++++++ ...t_submit_pipe_nfcore_labelled_proc_res.svg | 467 +++++++++++++++++ ...test_submit_pipe_nfcore_named_proc_res.svg | 472 +++++++++++++++++ ..._submitting_infra_custom_basic_details.svg | 271 ++++++++++ ...infra_custom_final_details_singularity.svg | 467 +++++++++++++++++ ...st_submitting_infra_custom_hpc_details.svg | 268 ++++++++++ ...itting_infra_final_details_singularity.svg | 210 +++++++- ...st_submitting_infra_nfcore_hpc_details.svg | 84 ++-- ...t_submitting_pipe_custom_basic_details.svg | 273 ++++++++++ .../test_submitting_pipe_custom_options.svg | 467 +++++++++++++++++ ...t_submitting_pipe_nfcore_basic_details.svg | 273 ++++++++++ .../test_submitting_pipe_nfcore_options.svg | 467 +++++++++++++++++ 36 files changed, 12109 insertions(+), 260 deletions(-) create mode 100644 tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_default_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_labelled_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_named_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_default_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_labelled_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_named_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_docker.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_singularity.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_hpc_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_pipe_custom_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_entering_pipe_nfcore_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_pipe_custom_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_pipe_nfcore_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_setting_pipe_custom_options.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_setting_pipe_nfcore_options.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_default_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_labelled_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_named_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_default_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_labelled_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_named_proc_res.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_final_details_singularity.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_options.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_basic_details.svg create mode 100644 tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_options.svg diff --git a/tests/configs/__snapshots__/test_create_app/test_choose_type.svg b/tests/configs/__snapshots__/test_create_app/test_choose_type.svg index 107fb8738e..0a7a032f0a 100644 --- a/tests/configs/__snapshots__/test_create_app/test_choose_type.svg +++ b/tests/configs/__snapshots__/test_create_app/test_choose_type.svg @@ -213,7 +213,7 @@ - + nf-core configs create — Create a new nextflow config with an interactive interfa… @@ -225,7 +225,7 @@ Choose "Infrastructure config" if:Choose "Pipeline config" if: • You want to only define the computational• You just want to tweak resources of a -environment you will run all pipelines onparticular step of a specific pipeline. +environment you will run all pipelines on.particular step of a specific pipeline. ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔  Infrastructure config  Pipeline config  @@ -238,18 +238,18 @@ Infrastructure configs: -• Describe the basic necessary information for any nf-core pipeline to execute +• Describe the basic necessary information for any nf-core pipeline to execute. • Define things such as which container engine to use, if there is a scheduler and which queues to use etc. • Are suitable for all users on a given computing environment. • Can be uploaded to nf-core configs to be directly accessible in a nf-core pipeline with -profile <infrastructure_name>. -• Are not used to tweak specific parts of a given pipeline (such as a process or module) +• Are not used to tweak specific parts of a given pipeline (such as a process or module). Pipeline configs • Are config files that target specific component of a particular pipeline or pipeline run. -▪ Example: you have a particular step of the pipeline that often runs out of memory using the +▪ Example: you have a particular step of the pipeline that often runs out. of memory using the pipeline's default settings. You would use this config to increase the amount of memory Nextflow supplies that given task. • Are normally only used by a single or small group of users. diff --git a/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_default_res.svg b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_default_res.svg new file mode 100644 index 0000000000..6c27d90418 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_default_res.svg @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Default process resources + + + +Number of CPUs to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Amount of memory in GB to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +8                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +The default number of hours of walltime required for processes: + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +9.5                                                                                          +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_labelled_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_labelled_proc_res.svg new file mode 100644 index 0000000000..0819d56990 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_labelled_proc_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by label — Create a new nextflow config with an interactive i… + + +Configure processes by label + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +some_proc_label                 queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +another_label                   customqueue  -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +a_third_label                   164queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +fourth_label                    6notherqueue -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_named_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_named_proc_res.svg new file mode 100644 index 0000000000..f6dd589d1e --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_custom_named_proc_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by name — Create a new nextflow config with an interactive in… + + +Configure processes by name + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +some_proc                       queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +another_proc                    customqueue  -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_default_res.svg b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_default_res.svg new file mode 100644 index 0000000000..6c27d90418 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_default_res.svg @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Default process resources + + + +Number of CPUs to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Amount of memory in GB to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +8                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +The default number of hours of walltime required for processes: + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +9.5                                                                                          +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_labelled_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_labelled_proc_res.svg new file mode 100644 index 0000000000..0819d56990 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_labelled_proc_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by label — Create a new nextflow config with an interactive i… + + +Configure processes by label + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +some_proc_label                 queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +another_label                   customqueue  -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +a_third_label                   164queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +fourth_label                    6notherqueue -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_named_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_named_proc_res.svg new file mode 100644 index 0000000000..f5c611172f --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_enter_pipe_nfcore_named_proc_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by name — Create a new nextflow config with an interactive in… + + +Configure processes by name + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +SOME:PROC.NAME*                 queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ANOTHER:PROC_NAME               customqueue  -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_basic_details.svg new file mode 100644 index 0000000000..a04066d1c3 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_basic_details.svg @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +myconfig                                                                                     +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +A cool description +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_docker.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_docker.svg new file mode 100644 index 0000000000..841373cd77 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_docker.svg @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Configure the options for your infrastructure config + +Select the container/software management system available on your infrastructure. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +docker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Set maximum available resources + +The following fields let you set the maximum available resrouces on your infrastructure. + +Memory, CPUs, and time must be filled out and all processes run with this configuration will be +capped at these values. + +The queue size, poll interval, and submit rate fields are optional, since Nextflow has built-in +defaults for these values. Consult the Nextflow documentation for further details on these +default values. + + + +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) available +on your infrastructureavailable on yourto jobs on your infrastructure +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +128                      48                       9.5                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +200                      0.25                     25                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Define global directories + +The following fields let you define global directories for a container/conda image cache, an +iGenomes cache, and a scratch directory in which to run your jobs. + +Each field is optional, but must contain the full (absolute) path to these directories if +specified. They may also contain references to environment variables (e.g. $CACHEDIR) and may +use the ~ symbol to refer to your home directory. + + + +If you have a global container/conda cache directory, specify the **full path** here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/path/to/cache/dir +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you want your jobs to run within a scratch directory, specify the full path here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/tmp/scratch                                                                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔Clean up work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run (prevents resume functionality). + + + +Specify the number of retries for a failed job. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_singularity.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_singularity.svg new file mode 100644 index 0000000000..fffd03b168 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_final_details_singularity.svg @@ -0,0 +1,475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Configure the options for your infrastructure config + +Select the container/software management system available on your infrastructure. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Set maximum available resources + +The following fields let you set the maximum available resrouces on your infrastructure. + +Memory, CPUs, and time must be filled out and all processes run with this configuration will be +capped at these values. + +The queue size, poll interval, and submit rate fields are optional, since Nextflow has built-in +defaults for these values. Consult the Nextflow documentation for further details on these +default values. + + + +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) available +on your infrastructureavailable on yourto jobs on your infrastructure +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +128                      48                       9.5                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +200                      0.25                     25                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Define global directories + +The following fields let you define global directories for a container/conda image cache, an +iGenomes cache, and a scratch directory in which to run your jobs. + +Each field is optional, but must contain the full (absolute) path to these directories if +specified. They may also contain references to environment variables (e.g. $CACHEDIR) and may +use the ~ symbol to refer to your home directory. + + + +If you have a global container/conda cache directory, specify the **full path** here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/singularity/cachedir                                                                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you want your jobs to run within a scratch directory, specify the full path here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/tmp/scratch                                                                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔Clean up work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run (prevents resume functionality). + + + +Specify the number of retries for a failed job. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_hpc_details.svg new file mode 100644 index 0000000000..aa3cc3a1ba --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_custom_hpc_details.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Configure the options for your HPC + +Use the following fields to provide important details about your HPC. + +First, select your HPC's scheduler. The program will attempt to automatically determine your +HPC's scheduler. If it was unsuccessful, you can select one of the supported schedulers from the +drop-down list. You can also select "Local execution" which will result in jobs defaulting to +run on the same node as Nextflow; this is generally not recommended. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +PBS Pro +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Next, supply your HPC's default queue, if required. Again, this will be auto-filled if possible. +You can leave this field blank if your HPC automatically submits jobs to a default queue when +not specified. + +For more complex queue selection (e.g. based on memory or CPU requirements), you will need to +manually edit the configuration file after completion. + +If you wish to submit different processes to different queues, you will need to additionally +create a pipeline configuration, which will let you specify alternative HPC queues for each +process. + + + +The default queue in your HPC (if required). + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +myqueue                                                                                      +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +How often to get the queue status from the scheduler (minutes). + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +0.75 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Continue  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg index 5bdfdc71ef..74e9130fb8 100644 --- a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_docker.svg @@ -43,17 +43,19 @@ .terminal-r9 { fill: #a0a0a0;font-style: italic; } .terminal-r10 { fill: #008139 } .terminal-r11 { fill: #b93c5b } -.terminal-r12 { fill: #737373 } -.terminal-r13 { fill: #1e1e1e } -.terminal-r14 { fill: #808080 } -.terminal-r15 { fill: #f4bb6e } -.terminal-r16 { fill: #2d2d2d } -.terminal-r17 { fill: #7ae998 } -.terminal-r18 { fill: #e0e0e0;font-weight: bold } -.terminal-r19 { fill: #0a180e;font-weight: bold } -.terminal-r20 { fill: #0d0d0d } -.terminal-r21 { fill: #495259 } -.terminal-r22 { fill: #ffa62b;font-weight: bold } +.terminal-r12 { fill: #e0e0e0;font-weight: bold } +.terminal-r13 { fill: #f4bb6e } +.terminal-r14 { fill: #737373 } +.terminal-r15 { fill: #1e1e1e } +.terminal-r16 { fill: #808080 } +.terminal-r17 { fill: #808080;font-weight: bold } +.terminal-r18 { fill: #f4bb6e;font-weight: bold } +.terminal-r19 { fill: #2d2d2d } +.terminal-r20 { fill: #7ae998 } +.terminal-r21 { fill: #0a180e;font-weight: bold } +.terminal-r22 { fill: #0d0d0d } +.terminal-r23 { fill: #495259 } +.terminal-r24 { fill: #ffa62b;font-weight: bold } @@ -367,108 +369,108 @@ - + nf-core configs create — Create a new nextflow config with an interactive interfa… Configure the options for your infrastructure config -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -docker -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - -Maximum resources +Select the container/software management system available on your infrastructure. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +docker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + - +Set maximum available resources -Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) to run -in your machine.available in your machine.your jobs. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -128                      48                       9.5                        -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +The following fields let you set the maximum available resrouces on your infrastructure. + +Memory, CPUs, and time must be filled out and all processes run with this configuration will be +capped at these values. + +The queue size, poll interval, and submit rate fields are optional, since Nextflow has built-in +defaults for these values. Consult the Nextflow documentation for further details on these +default values. -Number of jobs that can beHow often to check forMaximum number of jobs that -submitted simultaneously.successful process completioncan be submitted per minute. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -200                      0.25                     25                         -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) available +on your infrastructureavailable on yourto jobs on your infrastructure +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +128                      48                       9.5                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -Do you want to define a global cache directory for containers or conda environments? + - - -Define a global cache directory. - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -/path/to/cache/dir -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +200                      0.25                     25                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + - +Define global directories -If you have an iGenomes cache directory, specify it. - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -/data/igenomes/cache                                                                         -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - +The following fields let you define global directories for a container/conda image cache, an +iGenomes cache, and a scratch directory in which to run your jobs. + +Each field is optional, but must contain the full (absolute) path to these directories if +specified. They may also contain references to environment variables (e.g. $CACHEDIR) and may +use the ~ symbol to refer to your home directory. -If you have to use a specific scratch directory, specify it. +If you have a global container/conda cache directory, specify the **full path** here. ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -/tmp/scratch                                                                                 +/path/to/cache/dir ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔Delete work directory -Select if you want to delete the files in the work/ directory on successful -▁▁▁▁▁▁▁▁completion of a run. + + +If you have an iGenomes cache directory, specify the **full path** here. - - -Specify the number of retries for a failed job. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/data/igenomes/cache                                                                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -2 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + +If you want your jobs to run within a scratch directory, specify the full path here. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Back  Finish  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/tmp/scratch                                                                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +▔▔▔▔▔▔▔▔Clean up work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run (prevents resume functionality). - +Specify the number of retries for a failed job. - - - +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -^p palette +^p palette diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg index 0b4833245d..241571cf2b 100644 --- a/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_final_details_singularity.svg @@ -43,16 +43,18 @@ .terminal-r9 { fill: #a0a0a0;font-style: italic; } .terminal-r10 { fill: #008139 } .terminal-r11 { fill: #b93c5b } -.terminal-r12 { fill: #1e1e1e } -.terminal-r13 { fill: #808080 } -.terminal-r14 { fill: #f4bb6e } -.terminal-r15 { fill: #2d2d2d } -.terminal-r16 { fill: #7ae998 } -.terminal-r17 { fill: #e0e0e0;font-weight: bold } -.terminal-r18 { fill: #0a180e;font-weight: bold } -.terminal-r19 { fill: #0d0d0d } -.terminal-r20 { fill: #495259 } -.terminal-r21 { fill: #ffa62b;font-weight: bold } +.terminal-r12 { fill: #e0e0e0;font-weight: bold } +.terminal-r13 { fill: #f4bb6e } +.terminal-r14 { fill: #1e1e1e } +.terminal-r15 { fill: #808080 } +.terminal-r16 { fill: #808080;font-weight: bold } +.terminal-r17 { fill: #f4bb6e;font-weight: bold } +.terminal-r18 { fill: #2d2d2d } +.terminal-r19 { fill: #7ae998 } +.terminal-r20 { fill: #0a180e;font-weight: bold } +.terminal-r21 { fill: #0d0d0d } +.terminal-r22 { fill: #495259 } +.terminal-r23 { fill: #ffa62b;font-weight: bold } @@ -366,108 +368,108 @@ - + nf-core configs create — Create a new nextflow config with an interactive interfa… Configure the options for your infrastructure config -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -singularity -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - -Maximum resources +Select the container/software management system available on your infrastructure. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + - +Set maximum available resources -Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) to run -in your machine.available in your machine.your jobs. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -128                      48                       9.5                        -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +The following fields let you set the maximum available resrouces on your infrastructure. + +Memory, CPUs, and time must be filled out and all processes run with this configuration will be +capped at these values. + +The queue size, poll interval, and submit rate fields are optional, since Nextflow has built-in +defaults for these values. Consult the Nextflow documentation for further details on these +default values. -Number of jobs that can beHow often to check forMaximum number of jobs that -submitted simultaneously.successful process completioncan be submitted per minute. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -200                      0.25                     25                         -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) available +on your infrastructureavailable on yourto jobs on your infrastructure +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +128                      48                       9.5                        +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -Do you want to define a global cache directory for containers or conda environments? + - - -Define a global cache directory. - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -/singularity/cachedir                                                                        -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +200                      0.25                     25                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + - +Define global directories -If you have an iGenomes cache directory, specify it. - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -/data/igenomes/cache                                                                         -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - +The following fields let you define global directories for a container/conda image cache, an +iGenomes cache, and a scratch directory in which to run your jobs. + +Each field is optional, but must contain the full (absolute) path to these directories if +specified. They may also contain references to environment variables (e.g. $CACHEDIR) and may +use the ~ symbol to refer to your home directory. -If you have to use a specific scratch directory, specify it. +If you have a global container/conda cache directory, specify the **full path** here. ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -/tmp/scratch                                                                                 +/singularity/cachedir                                                                        ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔Delete work directory -Select if you want to delete the files in the work/ directory on successful -▁▁▁▁▁▁▁▁completion of a run. + + +If you have an iGenomes cache directory, specify the **full path** here. - - -Specify the number of retries for a failed job. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/data/igenomes/cache                                                                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -2 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + +If you want your jobs to run within a scratch directory, specify the full path here. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Back  Finish  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/tmp/scratch                                                                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +▔▔▔▔▔▔▔▔Clean up work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run (prevents resume functionality). - +Specify the number of retries for a failed job. - - - +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +2 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -^p palette +^p palette diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg index 126a6a83fa..aa3cc3a1ba 100644 --- a/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg +++ b/tests/configs/__snapshots__/test_create_app/test_entering_infra_nfcore_hpc_details.svg @@ -36,17 +36,19 @@ .terminal-r2 { fill: #e0e0e0 } .terminal-r3 { fill: #a0a3a6 } .terminal-r4 { fill: #0178d4;font-weight: bold } -.terminal-r5 { fill: #a0a0a0;font-style: italic; } -.terminal-r6 { fill: #121212 } -.terminal-r7 { fill: #008139 } -.terminal-r8 { fill: #b93c5b } -.terminal-r9 { fill: #2d2d2d } -.terminal-r10 { fill: #7ae998 } -.terminal-r11 { fill: #e0e0e0;font-weight: bold } -.terminal-r12 { fill: #0a180e;font-weight: bold } -.terminal-r13 { fill: #0d0d0d } -.terminal-r14 { fill: #495259 } -.terminal-r15 { fill: #ffa62b;font-weight: bold } +.terminal-r5 { fill: #121212 } +.terminal-r6 { fill: #191919 } +.terminal-r7 { fill: #7f7f7f } +.terminal-r8 { fill: #a0a0a0;font-style: italic; } +.terminal-r9 { fill: #008139 } +.terminal-r10 { fill: #b93c5b } +.terminal-r11 { fill: #2d2d2d } +.terminal-r12 { fill: #7ae998 } +.terminal-r13 { fill: #e0e0e0;font-weight: bold } +.terminal-r14 { fill: #0a180e;font-weight: bold } +.terminal-r15 { fill: #0d0d0d } +.terminal-r16 { fill: #495259 } +.terminal-r17 { fill: #ffa62b;font-weight: bold } @@ -210,58 +212,58 @@ - + nf-core configs create — Create a new nextflow config with an interactive interfa… Configure the options for your HPC - +Use the following fields to provide important details about your HPC. -The scheduler in your HPC.The default queue in yourHow often to get the queue -HPC.status from the scheduler -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -pbspro                   myqueue                  0.75 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Back  Continue  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - +First, select your HPC's scheduler. The program will attempt to automatically determine your +HPC's scheduler. If it was unsuccessful, you can select one of the supported schedulers from the +drop-down list. You can also select "Local execution" which will result in jobs defaulting to +run on the same node as Nextflow; this is generally not recommended. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +PBS Pro +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Next, supply your HPC's default queue, if required. Again, this will be auto-filled if possible. +You can leave this field blank if your HPC automatically submits jobs to a default queue when +not specified. - - +For more complex queue selection (e.g. based on memory or CPU requirements), you will need to +manually edit the configuration file after completion. - - - +If you wish to submit different processes to different queues, you will need to additionally +create a pipeline configuration, which will let you specify alternative HPC queues for each +process. - +The default queue in your HPC (if required). - - - +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +myqueue                                                                                      +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - +How often to get the queue status from the scheduler (minutes). - - - +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +0.75 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Continue  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -^p palette +^p palette diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_pipe_custom_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_entering_pipe_custom_basic_details.svg new file mode 100644 index 0000000000..a8fa593eab --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_pipe_custom_basic_details.svg @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +myconfig                                                                                     +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +The path to the pipeline you want to create the config for. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +.                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +A cool description +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_entering_pipe_nfcore_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_entering_pipe_nfcore_basic_details.svg new file mode 100644 index 0000000000..8bcba0fafd --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_entering_pipe_nfcore_basic_details.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive inter… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +myconfig                                                                                   +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Author full name.Author Git(Hub) handle. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +my name                                  @myhandle                                 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +Pipeline name. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +scdownstream +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +A cool description                                                                         +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +URL of infrastructure website or owning institution (infrastructure configs only). + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +https://example.com +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next ▇▇ +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg index 6666701dff..b14a4b6ead 100644 --- a/tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg +++ b/tests/configs/__snapshots__/test_create_app/test_infra_custom_basic_details.svg @@ -214,7 +214,7 @@ - + nf-core configs create — Create a new nextflow config with an interactive interfa… @@ -232,34 +232,34 @@ -Author full name.Author Git(Hub) handle. +A short description of your config. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Boaty McBoatFace@BoatyMcBoatFace -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Description +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - -A short description of your config. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Description -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + -URL of infrastructure website or owning institution (infrastructure configs only). + -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -https://nf-co.re -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Back  Next  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_pipe_custom_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_pipe_custom_basic_details.svg new file mode 100644 index 0000000000..3613e08cec --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_pipe_custom_basic_details.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +custom +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +The path to the pipeline you want to create the config for. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Pipeline path +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Description +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_pipe_nfcore_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_pipe_nfcore_basic_details.svg new file mode 100644 index 0000000000..3e2f63c760 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_pipe_nfcore_basic_details.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive inter… + + +Basic details + + + +Config Name. Used for naming resulting file. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +custom +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Author full name.Author Git(Hub) handle. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Boaty McBoatFace@BoatyMcBoatFace +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +Pipeline name. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +The name of the nf-core pipeline you want to configure +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +A short description of your config. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Description +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +URL of infrastructure website or owning institution (infrastructure configs only). + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +https://nf-co.re +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next ▇▇ +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_setting_pipe_custom_options.svg b/tests/configs/__snapshots__/test_create_app/test_setting_pipe_custom_options.svg new file mode 100644 index 0000000000..c23b249f27 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_setting_pipe_custom_options.svg @@ -0,0 +1,473 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +What would you like to configure? + +Configure default process resources?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ +Configure specific named processes?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ +Configure labels?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ + + +Will you be using this configuration exclusively on an HPC? + + +Choose "Yes" if: + +You are configuring processes specifically for running on an HPC. + + +Choose "No" if: + +This config file will be used across different platforms. + + +What's the difference? + +Choosing "Yes" will allow you to additionally specify an HPC queue for each process or label. +Note that this field can be left blank to use the default queue. + +Configure HPC-specific resources?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_setting_pipe_nfcore_options.svg b/tests/configs/__snapshots__/test_create_app/test_setting_pipe_nfcore_options.svg new file mode 100644 index 0000000000..c23b249f27 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_setting_pipe_nfcore_options.svg @@ -0,0 +1,473 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +What would you like to configure? + +Configure default process resources?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ +Configure specific named processes?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ +Configure labels?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ + + +Will you be using this configuration exclusively on an HPC? + + +Choose "Yes" if: + +You are configuring processes specifically for running on an HPC. + + +Choose "No" if: + +This config file will be used across different platforms. + + +What's the difference? + +Choosing "Yes" will allow you to additionally specify an HPC queue for each process or label. +Note that this field can be left blank to use the default queue. + +Configure HPC-specific resources?▔▔▔▔▔▔▔▔Yes + +▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_default_res.svg b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_default_res.svg new file mode 100644 index 0000000000..bef311f2e9 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_default_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by name — Create a new nextflow config with an interactive in… + + +Configure processes by name + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_labelled_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_labelled_proc_res.svg new file mode 100644 index 0000000000..31f6e8e3f0 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_labelled_proc_res.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Final step + + + +In which directory would you like to save the config? + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Save and close!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_named_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_named_proc_res.svg new file mode 100644 index 0000000000..949a6e2262 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_custom_named_proc_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by label — Create a new nextflow config with an interactive i… + + +Configure processes by label + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_default_res.svg b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_default_res.svg new file mode 100644 index 0000000000..bef311f2e9 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_default_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by name — Create a new nextflow config with an interactive in… + + +Configure processes by name + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process name:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_labelled_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_labelled_proc_res.svg new file mode 100644 index 0000000000..31f6e8e3f0 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_labelled_proc_res.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Final step + + + +In which directory would you like to save the config? + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Save and close!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_named_proc_res.svg b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_named_proc_res.svg new file mode 100644 index 0000000000..949a6e2262 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submit_pipe_nfcore_named_proc_res.svg @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + Configure processes by label — Create a new nextflow config with an interactive i… + + +Configure processes by label + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Process label:#MemoryWalltiHPC queueRemove +CPUs:(GB):me(optional): +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +queue name -  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Add another process  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_basic_details.svg new file mode 100644 index 0000000000..eb77da4744 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_basic_details.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Is this configuration file for an HPC config? + + + +Choose "HPC" if:Choose "local" if: + +You want to create a config file for an HPC.You want to create a config file to run your +pipeline on a local computer. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + HPC ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ local  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +What's the difference? + +Choosing "HPC" will add the following configurations: + +• Provide a scheduler +• Provide the name of a queue +• Select if a module system is used +• Select if you need to load other modules + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_final_details_singularity.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_final_details_singularity.svg new file mode 100644 index 0000000000..31f6e8e3f0 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_final_details_singularity.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Final step + + + +In which directory would you like to save the config? + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Save and close!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg new file mode 100644 index 0000000000..f1be698f9e --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive inter… + + +Configure the options for your infrastructure config + +Select the container/software management system available on your infrastructure. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Set maximum available resources + +The following fields let you set the maximum available resrouces on your infrastructure. + +Memory, CPUs, and time must be filled out and all processes run with this configuration will +be capped at these values. + +The queue size, poll interval, and submit rate fields are optional, since Nextflow has +built-in defaults for these values. Consult the Nextflow documentation for further details on +these default values. + + + +Maximum memory (GB)Maximum number of CPUsMaximum time (hours) +available on youravailable on youravailable to jobs on your +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +MemoryCPUsTime▄▄ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Queue sizePoll intervalJobs per minutes +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Define global directories + +The following fields let you define global directories for a container/conda image cache, an +iGenomes cache, and a scratch directory in which to run your jobs. + +Each field is optional, but must contain the full (absolute) path to these directories if + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg index 8823e334ee..31f6e8e3f0 100644 --- a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_final_details_singularity.svg @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg index 7c1459b86a..fd119350c8 100644 --- a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg @@ -41,11 +41,11 @@ .terminal-r7 { fill: #838383 } .terminal-r8 { fill: #0178d4;text-decoration: underline; } .terminal-r9 { fill: #a0a0a0;font-style: italic; } -.terminal-r10 { fill: #191919 } -.terminal-r11 { fill: #737373 } -.terminal-r12 { fill: #b93c5b } -.terminal-r13 { fill: #008139 } -.terminal-r14 { fill: #000000 } +.terminal-r10 { fill: #000000 } +.terminal-r11 { fill: #191919 } +.terminal-r12 { fill: #737373 } +.terminal-r13 { fill: #b93c5b } +.terminal-r14 { fill: #e0e0e0;font-weight: bold } .terminal-r15 { fill: #ffa62b;font-weight: bold } .terminal-r16 { fill: #495259 } @@ -211,57 +211,57 @@ - + nf-core configs create — Create a new nextflow config with an interactive inter… Configure the options for your infrastructure config -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -singularity -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - -Maximum resources +Select the container/software management system available on your infrastructure. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + - +Set maximum available resources -Maximum memory (GB)Maximum number of CPUsMaximum time (hours) to run -available in your machine.available in your machine.your jobs. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -MemoryCPUsTime -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - +The following fields let you set the maximum available resrouces on your infrastructure. + +Memory, CPUs, and time must be filled out and all processes run with this configuration will +be capped at these values. + +The queue size, poll interval, and submit rate fields are optional, since Nextflow has +built-in defaults for these values. Consult the Nextflow documentation for further details on +these default values. -Number of jobs that can beHow often to check forMaximum number of jobs that -submitted simultaneously.successful process completioncan be submitted per minute. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -300                     0.5                      20                        -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + +Maximum memory (GB)Maximum number of CPUsMaximum time (hours)▁▁ +available on youravailable on youravailable to jobs on your +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +MemoryCPUsTime +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▂▂ -Do you want to define a global cache directory for containers or conda environments? + + - - -Define a global cache directory. - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -/path/to/cache/dir -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Queue sizePoll intervalJobs per minutes +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + - +Define global directories -If you have an iGenomes cache directory, specify it. - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -iGenomes cache directory +The following fields let you define global directories for a container/conda image cache, an +iGenomes cache, and a scratch directory in which to run your jobs. + +Each field is optional, but must contain the full (absolute) path to these directories if  d Toggle dark mode  q Quit ^p palette diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_basic_details.svg new file mode 100644 index 0000000000..63231d430b --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_basic_details.svg @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +What would you like to configure? + +Configure default process resources?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ +Configure specific named processes?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ +Configure labels?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ + + +Will you be using this configuration exclusively on an HPC? + + +Choose "Yes" if: + +You are configuring processes specifically for running on an HPC. + + +Choose "No" if: + +This config file will be used across different platforms. + + +What's the difference? + +Choosing "Yes" will allow you to additionally specify an HPC queue for each process or label. +Note that this field can be left blank to use the default queue. + +Configure HPC-specific resources?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_options.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_options.svg new file mode 100644 index 0000000000..1ddf877396 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_custom_options.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Default process resources + + + +Number of CPUs to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Amount of memory in GB to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +6                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +The default number of hours of walltime required for processes: + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +4                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_basic_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_basic_details.svg new file mode 100644 index 0000000000..63231d430b --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_basic_details.svg @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +What would you like to configure? + +Configure default process resources?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ +Configure specific named processes?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ +Configure labels?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ + + +Will you be using this configuration exclusively on an HPC? + + +Choose "Yes" if: + +You are configuring processes specifically for running on an HPC. + + +Choose "No" if: + +This config file will be used across different platforms. + + +What's the difference? + +Choosing "Yes" will allow you to additionally specify an HPC queue for each process or label. +Note that this field can be left blank to use the default queue. + +Configure HPC-specific resources?▔▔▔▔▔▔▔▔No + +▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + d Toggle dark mode  q Quit ^p palette + + + diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_options.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_options.svg new file mode 100644 index 0000000000..1ddf877396 --- /dev/null +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_pipe_nfcore_options.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nf-core configs create + + + + + + + + + + nf-core configs create — Create a new nextflow config with an interactive interfa… + + +Default process resources + + + +Number of CPUs to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +Amount of memory in GB to use by default for all processes. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +6                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +The default number of hours of walltime required for processes: + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +4                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Skip  Next  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +^p palette + + + From 3879c0a7566d4216b52d2da9aa2214046bdd65e6 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 19 May 2026 11:28:08 +1000 Subject: [PATCH 109/121] fix broken diff with dev --- .devcontainer/build-devcontainer/Dockerfile | 4 +- .gitattributes | 1 + .github/actions/create-lint-wf/action.yml | 11 +- .github/snapshots/gpu.nf.test.snap | 2 +- .github/workflows/branch.yml | 2 +- .github/workflows/changelog.py | 4 +- .github/workflows/changelog.yml | 18 +- .github/workflows/create-lint-wf.yml | 6 +- .../create-test-lint-wf-template.yml | 36 +- .github/workflows/create-test-wf.yml | 10 +- .github/workflows/deploy-pypi.yml | 4 +- .github/workflows/fix-linting.yml | 4 +- .github/workflows/lint-code.yml | 2 +- .github/workflows/nextflow-source-test.yml | 36 +- .github/workflows/push_dockerhub.yml | 4 +- .github/workflows/pytest.yml | 31 +- .github/workflows/sync.yml | 8 +- .github/workflows/test_offline_configs.yml | 2 +- .github/workflows/tools-api-docs.yml | 2 +- .../workflows/update-template-snapshots.yml | 10 +- .../workflows/update-textual-snapshots.yml | 4 +- .pre-commit-config.yaml | 16 +- CHANGELOG.md | 171 +- Dockerfile | 4 +- MANIFEST.in | 1 + README.md | 2 +- docs/api/_src/conf.py | 6 +- .../pipeline_lint_tests/actions_nf_test.md | 6 +- .../pipeline_lint_tests/container_configs.md | 5 + docs/api/_src/pipeline_lint_tests/index.md | 1 + docs/api/check_lint_docstrings.py | 90 + docs/api/generate-api-docs.sh | 16 +- nf_core/.pre-commit-prettier-config.yaml | 2 +- nf_core/__main__.py | 873 ++---- nf_core/commands_modules.py | 47 +- nf_core/commands_pipelines.py | 49 +- nf_core/commands_subworkflows.py | 21 +- nf_core/components/components_command.py | 34 +- nf_core/components/components_completion.py | 3 +- nf_core/components/components_differ.py | 4 +- nf_core/components/components_test.py | 18 +- nf_core/components/components_utils.py | 22 +- nf_core/components/create.py | 168 +- nf_core/components/info.py | 23 +- nf_core/components/install.py | 72 +- nf_core/components/lint/__init__.py | 7 +- nf_core/components/nfcore_component.py | 15 +- nf_core/components/patch.py | 39 +- nf_core/components/remove.py | 24 +- nf_core/components/update.py | 133 +- nf_core/configs/create/__init__.py | 4 +- nf_core/configs/create/basicdetails.py | 2 +- .../create/create.tcss} | 0 nf_core/configs/create/finalinfradetails.py | 2 +- nf_core/configs/create/utils.py | 21 +- nf_core/module-template/main.nf | 8 +- nf_core/module-template/tests/main.nf.test.j2 | 4 +- nf_core/modules/bump_versions.py | 13 +- nf_core/modules/create.py | 2 - nf_core/modules/lint/__init__.py | 25 +- nf_core/modules/lint/environment_yml.py | 12 + nf_core/modules/lint/main_nf.py | 207 +- nf_core/modules/lint/meta_yml.py | 56 +- nf_core/modules/lint/module_changes.py | 5 + nf_core/modules/lint/module_deprecations.py | 8 +- nf_core/modules/lint/module_tests.py | 97 +- nf_core/modules/lint/module_todos.py | 8 +- nf_core/modules/lint/module_version.py | 9 +- nf_core/modules/modules_json.py | 97 +- nf_core/modules/modules_repo.py | 13 +- nf_core/modules/modules_utils.py | 24 +- nf_core/modules/update.py | 2 + .../.github/actions/get-shards/action.yml | 2 +- .../.github/actions/nf-test/action.yml | 6 +- .../.github/workflows/awsfulltest.yml | 9 +- .../.github/workflows/awstest.yml | 4 +- .../.github/workflows/branch.yml | 2 +- .../.github/workflows/download_pipeline.yml | 16 +- .../.github/workflows/fix_linting.yml | 20 +- .../.github/workflows/linting.yml | 11 +- .../.github/workflows/linting_comment.yml | 2 +- .../.github/workflows/nf-test.yml | 2 +- .../workflows/template-version-comment.yml | 4 +- .../pipeline-template/.pre-commit-config.yaml | 2 +- nf_core/pipeline-template/README.md | 8 +- nf_core/pipeline-template/conf/base.config | 5 +- .../pipeline-template/docs/CONTRIBUTING.md | 17 +- nf_core/pipeline-template/docs/usage.md | 8 +- nf_core/pipeline-template/main.nf | 12 +- nf_core/pipeline-template/modules.json | 6 +- .../linux_amd64-bd-5cb1a2fa2f18c7c2_1.txt | 822 ++++++ .../linux_arm64-bd-e455e32f745abe68_1.txt | 769 +++++ .../modules/nf-core/fastqc/main.nf | 31 +- .../modules/nf-core/fastqc/meta.yml | 42 +- .../linux_amd64-bd-c1f4a7982b743963_1.txt | 1552 ++++++++++ .../linux_amd64-bd-db7c73dae76bc9e6_1.txt | 126 + .../linux_arm64-bd-40bf3b435e89dc22_1.txt | 1502 ++++++++++ .../linux_arm64-bd-d167b8012595a136_1.txt | 125 + .../modules/nf-core/multiqc/environment.yml | 2 +- .../modules/nf-core/multiqc/main.nf | 6 +- .../modules/nf-core/multiqc/meta.yml | 28 +- .../nf-core/multiqc/tests/main.nf.test | 116 +- .../nf-core/multiqc/tests/main.nf.test.snap | 344 ++- .../nf-core/multiqc/tests/nextflow.config | 1 + nf_core/pipeline-template/nextflow.config | 9 +- .../pipeline-template/nextflow_schema.json | 1 - nf_core/pipeline-template/nf-test.config | 26 +- .../utils_nfcore_pipeline_pipeline/main.nf | 33 +- .../nf-core/utils_nfcore_pipeline/main.nf | 2 +- .../pipeline-template/tests/default.nf.test | 12 +- .../pipeline-template/tests/nextflow.config | 2 +- .../pipeline-template/workflows/pipeline.nf | 13 +- nf_core/pipelines/bump_version.py | 2 +- nf_core/pipelines/containers_utils.py | 204 ++ nf_core/pipelines/create/__init__.py | 9 +- nf_core/pipelines/create/basicdetails.py | 5 +- nf_core/pipelines/create/create.py | 65 +- nf_core/pipelines/create/create.tcss | 141 + nf_core/pipelines/create/finaldetails.py | 8 +- nf_core/pipelines/create/githubrepo.py | 29 +- nf_core/pipelines/create/loggingscreen.py | 3 +- .../pipelines/create/template_features.yml | 2 +- nf_core/pipelines/create/utils.py | 57 +- nf_core/pipelines/create/welcome.py | 2 +- nf_core/pipelines/create_logo.py | 8 +- .../pipelines/download/container_fetcher.py | 7 +- nf_core/pipelines/download/docker.py | 7 +- nf_core/pipelines/download/download.py | 95 +- nf_core/pipelines/download/singularity.py | 84 +- nf_core/pipelines/download/utils.py | 30 +- nf_core/pipelines/download/workflow_repo.py | 67 +- nf_core/pipelines/launch.py | 62 +- nf_core/pipelines/lint/__init__.py | 26 +- nf_core/pipelines/lint/actions_awsfulltest.py | 12 +- nf_core/pipelines/lint/actions_awstest.py | 6 +- nf_core/pipelines/lint/actions_nf_test.py | 8 +- .../lint/actions_schema_validation.py | 6 +- nf_core/pipelines/lint/configs.py | 2 +- nf_core/pipelines/lint/container_configs.py | 78 + nf_core/pipelines/lint/files_exist.py | 8 +- nf_core/pipelines/lint/files_unchanged.py | 13 +- nf_core/pipelines/lint/modules_json.py | 18 +- nf_core/pipelines/lint/multiqc_config.py | 4 +- nf_core/pipelines/lint/nextflow_config.py | 22 +- nf_core/pipelines/lint/nf_test_content.py | 18 +- nf_core/pipelines/lint/plugin_includes.py | 23 +- nf_core/pipelines/lint/readme.py | 6 +- nf_core/pipelines/lint/schema_description.py | 6 +- nf_core/pipelines/lint/system_exit.py | 4 +- nf_core/pipelines/lint/template_strings.py | 9 +- nf_core/pipelines/lint/version_consistency.py | 2 +- nf_core/pipelines/lint_utils.py | 16 +- nf_core/pipelines/list.py | 154 +- nf_core/pipelines/params_file.py | 18 +- nf_core/pipelines/refgenie.py | 17 +- nf_core/pipelines/rocrate.py | 297 +- nf_core/pipelines/schema.py | 105 +- nf_core/pipelines/sync.py | 50 +- nf_core/subworkflows/create.py | 2 - nf_core/subworkflows/install.py | 2 + nf_core/subworkflows/lint/__init__.py | 8 +- nf_core/subworkflows/lint/main_nf.py | 44 +- nf_core/subworkflows/lint/meta_yml.py | 29 +- .../subworkflows/lint/subworkflow_changes.py | 8 + .../lint/subworkflow_if_empty_null.py | 7 +- .../subworkflows/lint/subworkflow_tests.py | 53 +- .../subworkflows/lint/subworkflow_todos.py | 6 +- .../subworkflows/lint/subworkflow_version.py | 9 +- nf_core/subworkflows/update.py | 2 + nf_core/synced_repo.py | 33 +- nf_core/test_datasets/list.py | 2 +- nf_core/test_datasets/test_datasets_utils.py | 29 +- nf_core/utils.py | 219 +- pyproject.toml | 27 +- .../test_components_generate_snapshot.py | 20 + tests/components/test_components_utils.py | 12 +- tests/conftest.py | 30 +- .../mock_pipeline_containers/conf/base.config | 2 +- .../modules/nf-core/rmarkdownnotebook/main.nf | 2 +- .../mock_pipeline_containers/nextflow.config | 4 +- .../main.nf | 79 +- tests/modules/lint/test_environment_yml.py | 2 +- tests/modules/lint/test_main_nf.py | 453 ++- tests/modules/lint/test_meta_yml.py | 192 +- .../modules/lint/test_module_lint_remotes.py | 12 +- tests/modules/lint/test_module_tests.py | 168 +- tests/modules/lint/test_patch.py | 2 +- tests/modules/test_bump_versions.py | 5 +- tests/modules/test_create.py | 109 +- tests/modules/test_modules_json.py | 10 +- tests/modules/test_modules_utils.py | 2 +- tests/modules/test_patch.py | 12 +- tests/modules/test_remove.py | 5 +- tests/modules/test_update.py | 22 +- .../test_create_app/test_welcome.svg | 14 +- tests/pipelines/download/test_container.py | 14 +- tests/pipelines/download/test_docker.py | 1 - tests/pipelines/download/test_download.py | 168 +- tests/pipelines/download/test_singularity.py | 4 +- tests/pipelines/download/test_utils.py | 35 +- tests/pipelines/lint/test_configs.py | 3 +- .../pipelines/lint/test_container_configs.py | 134 + tests/pipelines/lint/test_if_empty_null.py | 1 - tests/pipelines/lint/test_merge_markers.py | 6 +- tests/pipelines/lint/test_nextflow_config.py | 9 +- tests/pipelines/lint/test_nf_test_content.py | 7 +- .../lint/test_rocrate_readme_sync.py | 2 +- tests/pipelines/lint/test_template_strings.py | 1 - .../lint/test_version_consistency.py | 1 - tests/pipelines/test_container_configs.py | 121 + tests/pipelines/test_create.py | 18 +- tests/pipelines/test_create_logo.py | 4 +- tests/pipelines/test_launch.py | 2 +- tests/pipelines/test_lint.py | 29 +- tests/pipelines/test_list.py | 69 +- tests/pipelines/test_refgenie.py | 2 +- tests/pipelines/test_rocrate.py | 290 +- tests/pipelines/test_schema.py | 32 +- tests/pipelines/test_sync.py | 14 +- tests/subworkflows/lint/__init__.py | 0 tests/subworkflows/lint/test_integration.py | 69 + tests/subworkflows/lint/test_local.py | 46 + tests/subworkflows/lint/test_main_nf.py | 114 + tests/subworkflows/lint/test_nf_test.py | 141 + tests/subworkflows/lint/test_patch.py | 61 + tests/subworkflows/test_create.py | 70 - tests/subworkflows/test_install.py | 27 + tests/subworkflows/test_lint.py | 539 ---- tests/subworkflows/test_patch.py | 12 +- tests/subworkflows/test_remove.py | 22 +- tests/subworkflows/test_update.py | 69 +- tests/test_cli.py | 92 +- tests/test_lint_utils.py | 11 +- tests/test_modules.py | 1 - tests/test_subworkflows.py | 2 - tests/test_utils.py | 41 +- tests/utils.py | 1 - uv.lock | 2564 ++++++++--------- 238 files changed, 11865 insertions(+), 4828 deletions(-) create mode 100644 .gitattributes create mode 100644 docs/api/_src/pipeline_lint_tests/container_configs.md create mode 100644 docs/api/check_lint_docstrings.py rename nf_core/{textual.tcss => configs/create/create.tcss} (100%) create mode 100644 nf_core/pipeline-template/modules/nf-core/fastqc/.conda-lock/linux_amd64-bd-5cb1a2fa2f18c7c2_1.txt create mode 100644 nf_core/pipeline-template/modules/nf-core/fastqc/.conda-lock/linux_arm64-bd-e455e32f745abe68_1.txt create mode 100644 nf_core/pipeline-template/modules/nf-core/multiqc/.conda-lock/linux_amd64-bd-c1f4a7982b743963_1.txt create mode 100644 nf_core/pipeline-template/modules/nf-core/multiqc/.conda-lock/linux_amd64-bd-db7c73dae76bc9e6_1.txt create mode 100644 nf_core/pipeline-template/modules/nf-core/multiqc/.conda-lock/linux_arm64-bd-40bf3b435e89dc22_1.txt create mode 100644 nf_core/pipeline-template/modules/nf-core/multiqc/.conda-lock/linux_arm64-bd-d167b8012595a136_1.txt create mode 100644 nf_core/pipelines/containers_utils.py create mode 100644 nf_core/pipelines/create/create.tcss create mode 100644 nf_core/pipelines/lint/container_configs.py create mode 100644 tests/pipelines/lint/test_container_configs.py create mode 100644 tests/pipelines/test_container_configs.py create mode 100644 tests/subworkflows/lint/__init__.py create mode 100644 tests/subworkflows/lint/test_integration.py create mode 100644 tests/subworkflows/lint/test_local.py create mode 100644 tests/subworkflows/lint/test_main_nf.py create mode 100644 tests/subworkflows/lint/test_nf_test.py create mode 100644 tests/subworkflows/lint/test_patch.py delete mode 100644 tests/subworkflows/test_lint.py diff --git a/.devcontainer/build-devcontainer/Dockerfile b/.devcontainer/build-devcontainer/Dockerfile index dfa8eae3a3..8e843ab1bf 100644 --- a/.devcontainer/build-devcontainer/Dockerfile +++ b/.devcontainer/build-devcontainer/Dockerfile @@ -1,6 +1,6 @@ # devcontainers/miniconda image based on debian (bookworm) # see tags and images: https://mcr.microsoft.com/en-us/artifact/mar/devcontainers/miniconda/tags -FROM mcr.microsoft.com/devcontainers/miniconda@sha256:b99720fe7bdce0346dc3c3ff54414ba78ff183d85a5a9d9a2e2e5775ce679347 AS build +FROM mcr.microsoft.com/devcontainers/miniconda@sha256:1a85cf3e3da9d999f994f74cf501b1d02c9aa73ebfa3552f806dbe4570d7d5b5 AS build # copy this repo at current revision COPY . /root/nfcore-tools/ @@ -27,7 +27,7 @@ RUN rm -f /etc/apt/sources.list.d/yarn.list && \ rm -rf /var/lib/apt/lists/* # Final stage to copy only the required files after installation -FROM mcr.microsoft.com/devcontainers/base:2.1.6-debian12@sha256:15143a8c806afb8db57ae2c92876c112d7a6a1ced31cd7aa8b414201782757a4 AS final +FROM mcr.microsoft.com/devcontainers/base:2.1.8-debian12@sha256:17e6cc517b483d1108b333d4c34352b0a21617f0117052e9b259d47113a9dc37 AS final # Copy only the conda environment and site-packages from build stage COPY --from=build /opt/conda /opt/conda diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a19ade077d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union diff --git a/.github/actions/create-lint-wf/action.yml b/.github/actions/create-lint-wf/action.yml index 307f153fbb..0102c71dfb 100644 --- a/.github/actions/create-lint-wf/action.yml +++ b/.github/actions/create-lint-wf/action.yml @@ -16,7 +16,7 @@ runs: export NXF_WORK=$(pwd) - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 with: version: ${{ matrix.NXF_VER }} @@ -40,11 +40,12 @@ runs: pwd ls -l working-directory: create-lint-wf/nf-core-testpipeline + # Run code style linting - - name: run pre-commit + - name: run prek shell: bash - run: uv run pre-commit run --all-files - working-directory: create-lint-wf + run: uv run prek run --config .pre-commit-config.yaml --all-files + working-directory: create-lint-wf/nf-core-testpipeline # Update modules to the latest version - name: nf-core modules update @@ -88,7 +89,7 @@ runs: - name: Upload log file artifact if: ${{ always() }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: nf-core-log-file-${{ matrix.NXF_VER }} path: create-lint-wf/log.txt diff --git a/.github/snapshots/gpu.nf.test.snap b/.github/snapshots/gpu.nf.test.snap index 8ab9a90bf6..5435135a0f 100644 --- a/.github/snapshots/gpu.nf.test.snap +++ b/.github/snapshots/gpu.nf.test.snap @@ -107,7 +107,7 @@ ], "meta": { "nf-test": "0.9.3", - "nextflow": "25.04.0" + "nextflow": "25.10.4" }, "timestamp": "2025-06-16T14:29:10.076573" } diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 0d493d3cd3..240dc2dae0 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -18,7 +18,7 @@ jobs: # If the above check failed, post a comment on the PR explaining the failure - name: Post PR comment if: failure() - uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3 + uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3 with: message: | ## This PR is against the `main` branch :x: diff --git a/.github/workflows/changelog.py b/.github/workflows/changelog.py index 27e604a9b9..bf3d267870 100644 --- a/.github/workflows/changelog.py +++ b/.github/workflows/changelog.py @@ -135,9 +135,7 @@ def _skip_existing_entry_for_this_pr(line: str, same_section: bool = True) -> st # If the line already contains a link to the PR, don't add it again. line = _skip_existing_entry_for_this_pr(line, same_section=False) - if ( - line.startswith("## ") and not line.strip() == "# nf-core/tools: Changelog" - ): # Version header, e.g. "## v2.12dev" + if line.startswith("## ") and line.strip() != "# nf-core/tools: Changelog": # Version header, e.g. "## v2.12dev" print(f"Found version header: {line.strip()}") updated_lines.append(line) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index ca6462ccc9..39beaade4c 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -2,20 +2,17 @@ name: Update CHANGELOG.md on: issue_comment: types: [created] # Add "@nf-core-bot changelog" as a PR comment to trigger this workflow. - pull_request_target: - types: [opened] - branches: - - dev jobs: update_changelog: runs-on: ubuntu-latest + if: contains(github.event.comment.body, '@nf-core-bot changelog') steps: - name: branch-deploy id: branch-deploy if: github.event_name == 'issue_comment' - uses: github/branch-deploy@fded0351b6b79f854b335c11b3d93063461dd288 # v11.1.2 + uses: github/branch-deploy@ddf8ca48e9cdb2d2c7c71dee428308b19af9313f # v11.1.4 with: trigger: "@nf-core-bot changelog" reaction: "eyes" @@ -31,11 +28,7 @@ jobs: env: GH_TOKEN: ${{ secrets.NF_CORE_BOT_AUTH_TOKEN }} run: | - if [[ "${{ github.event_name }}" == "issue_comment" ]]; then - PR_NUMBER="${{ github.event.issue.number }}" - elif [[ "${{ github.event_name }}" == "pull_request_target" ]]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - fi + PR_NUMBER="${{ github.event.issue.number }}" gh pr checkout $PR_NUMBER - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 @@ -72,7 +65,8 @@ jobs: python-version: "3.14" - name: Run pre-commit rules with prek - uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2 + uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2 + continue-on-error: true with: extra-args: --config .pre-commit-config.yaml --all-files @@ -85,5 +79,5 @@ jobs: git config user.name "nf-core-bot" git add ${GITHUB_WORKSPACE}/CHANGELOG.md git status - git commit -m "[automated] Update CHANGELOG.md" + git commit -m "[automated] Update CHANGELOG.md [skip ci]" git push diff --git a/.github/workflows/create-lint-wf.yml b/.github/workflows/create-lint-wf.yml index 14263d0df7..56033d8eaa 100644 --- a/.github/workflows/create-lint-wf.yml +++ b/.github/workflows/create-lint-wf.yml @@ -32,7 +32,7 @@ jobs: strategy: matrix: NXF_VER: - - "25.04.0" + - "25.10.4" - "latest-everything" steps: - name: go to subdirectory and change nextflow workdir @@ -52,7 +52,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -71,7 +71,7 @@ jobs: # Remove TODO statements - name: remove TODO - run: find nf-core-testpipeline -type f -exec sed -i '/TODO nf-core:/d' {} \; + run: find nf-core-testpipeline -type f -exec sed -i 's/TODO nf-core:/TODONT nf-core:/g' {} \; working-directory: create-lint-wf # Uncomment includeConfig statement diff --git a/.github/workflows/create-test-lint-wf-template.yml b/.github/workflows/create-test-lint-wf-template.yml index 564f5b2621..9bb608e97f 100644 --- a/.github/workflows/create-test-lint-wf-template.yml +++ b/.github/workflows/create-test-lint-wf-template.yml @@ -64,7 +64,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -72,12 +72,12 @@ jobs: run: uv sync --all-extras - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 with: version: latest-everything - name: Install nf-test - uses: nf-core/setup-nf-test@v1 + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 with: version: "0.9.4" install-pdiff: true @@ -149,13 +149,14 @@ jobs: run: uv run nf-core --log-file log.txt pipelines sync --dir create-test-lint-wf/my-prefix-testpipeline/ # Run code style linting - - name: Run pre-commit - run: uv run pre-commit run --all-files - working-directory: create-test-lint-wf + - name: Run prek + if: ${{ matrix.TEMPLATE != 'code_linters' && matrix.TEMPLATE != 'all' }} + run: uv run prek run --config .pre-commit-config.yaml --all-files + working-directory: create-test-lint-wf/my-prefix-testpipeline # Remove TODO statements - name: remove TODO - run: find my-prefix-testpipeline -type f -exec sed -i '/TODO nf-core:/d' {} \; + run: find my-prefix-testpipeline -type f -exec sed -i 's/TODO nf-core:/TODONT nf-core:/g' {} \; working-directory: create-test-lint-wf # Uncomment includeConfig statement @@ -179,10 +180,21 @@ jobs: working-directory: create-test-lint-wf # Run code style linting - - name: run pre-commit - shell: bash - run: uv run pre-commit run --all-files - working-directory: create-test-lint-wf + - name: Run prek + id: prek + if: ${{ matrix.TEMPLATE != 'code_linters' && matrix.TEMPLATE != 'all' }} + run: uv run prek run --config .pre-commit-config.yaml --all-files --verbose | tee prek_output.txt + working-directory: create-test-lint-wf/my-prefix-testpipeline + + - name: Report prek warnings + if: ${{ matrix.TEMPLATE != 'code_linters' && matrix.TEMPLATE != 'all' }} + run: | + warnings=$(sed -n '/^ {$/,/^ }$/p' prek_output.txt | jq '.summary.warnings') + if [ "${warnings:-0}" -gt 0 ]; then + echo "### :warning: prek found ${warnings} warning(s)" >> $GITHUB_STEP_SUMMARY + nextflow lint . -o full >> $GITHUB_STEP_SUMMARY 2>&1 || true + fi + working-directory: create-test-lint-wf/my-prefix-testpipeline # Run bump-version - name: nf-core pipelines bump-version @@ -200,7 +212,7 @@ jobs: - name: Upload log file artifact if: ${{ always() }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: nf-core-log-file-${{ matrix.TEMPLATE }} path: create-test-lint-wf/artifact_files.tar diff --git a/.github/workflows/create-test-wf.yml b/.github/workflows/create-test-wf.yml index 90dba2583a..879bd68849 100644 --- a/.github/workflows/create-test-wf.yml +++ b/.github/workflows/create-test-wf.yml @@ -32,7 +32,7 @@ jobs: strategy: matrix: NXF_VER: - - "25.04.0" + - "25.10.4" - "latest-everything" steps: - name: go to working directory @@ -50,7 +50,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -58,12 +58,12 @@ jobs: run: uv sync - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 with: version: ${{ matrix.NXF_VER }} - name: Install nf-test - uses: nf-core/setup-nf-test@v1 + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 with: version: "0.9.4" install-pdiff: true @@ -93,7 +93,7 @@ jobs: - name: Upload log file artifact if: ${{ always() }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: nf-core-log-file-${{ matrix.NXF_VER }} path: create-test-wf/log.txt diff --git a/.github/workflows/deploy-pypi.yml b/.github/workflows/deploy-pypi.yml index 2d9a1c7dca..cd2c7c78d8 100644 --- a/.github/workflows/deploy-pypi.yml +++ b/.github/workflows/deploy-pypi.yml @@ -22,7 +22,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -34,6 +34,6 @@ jobs: - name: Publish dist to PyPI if: github.repository == 'nf-core/tools' - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 with: password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/fix-linting.yml b/.github/workflows/fix-linting.yml index 51c2253ee2..8178e8a2ab 100644 --- a/.github/workflows/fix-linting.yml +++ b/.github/workflows/fix-linting.yml @@ -12,7 +12,7 @@ jobs: steps: - name: branch-deploy id: branch-deploy - uses: github/branch-deploy@fded0351b6b79f854b335c11b3d93063461dd288 # v11.1.2 + uses: github/branch-deploy@ddf8ca48e9cdb2d2c7c71dee428308b19af9313f # v11.1.4 with: trigger: "@nf-core-bot fix linting" reaction: "eyes" @@ -33,7 +33,7 @@ jobs: # Install and run prek - name: Run prek id: prek - uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2 + uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2 with: extra-args: --config .pre-commit-config.yaml --all-files continue-on-error: true diff --git a/.github/workflows/lint-code.yml b/.github/workflows/lint-code.yml index a278165f8d..11d2c5fd6e 100644 --- a/.github/workflows/lint-code.yml +++ b/.github/workflows/lint-code.yml @@ -22,6 +22,6 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Run prek - uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2 + uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2 with: extra-args: --config .pre-commit-config.yaml --all-files diff --git a/.github/workflows/nextflow-source-test.yml b/.github/workflows/nextflow-source-test.yml index 9c53a1145b..92dd32a763 100644 --- a/.github/workflows/nextflow-source-test.yml +++ b/.github/workflows/nextflow-source-test.yml @@ -19,25 +19,13 @@ jobs: repository: nextflow-io/nextflow path: nextflow - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Check out nf-core/tools - with: - ref: dev - path: nf-core-tools - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 - with: - python-version: "3.14" - cache: pip - cache-dependency-path: nf-core-tools/pyproject.toml - - name: Set up Java uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: distribution: "temurin" architecture: x64 cache: gradle + cache-dependency-path: "nextflow/**/*.gradle*" java-version: "21" - name: Build Nextflow @@ -50,10 +38,24 @@ jobs: chmod +x $HOME/.local/bin/nextflow nextflow -version + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + name: Check out nf-core/tools + with: + ref: dev + + # Set up nf-core/tools + - name: Set up Python 3.14 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: "3.14" + + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + - name: Install nf-core/tools - run: | - cd nf-core-tools - uv sync + run: uv sync - name: Create new pipeline run: uv run nf-core pipelines create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface" @@ -63,7 +65,7 @@ jobs: - name: Send email on failure if: failure() - uses: dsfx3d/action-aws-ses@v1 + uses: dsfx3d/action-aws-ses@a0daa3774d2bb09e2bcb512e55bdc3510b6102ef # v1.0.3 env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/push_dockerhub.yml b/.github/workflows/push_dockerhub.yml index 838843d7c3..23411d275c 100644 --- a/.github/workflows/push_dockerhub.yml +++ b/.github/workflows/push_dockerhub.yml @@ -43,7 +43,7 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - name: Log in to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASS }} @@ -74,7 +74,7 @@ jobs: --push --no-cache . - name: Build and push nfcore/devcontainer image - uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3 + uses: devcontainers/ci@b63b30de439b47a52267f241112c5b453b673db5 # v0.3 with: configFile: .devcontainer/build-devcontainer/devcontainer.json imageName: nfcore/devcontainer diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 45d5422b69..26aa1cc689 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -20,6 +20,11 @@ on: release: types: [published] workflow_dispatch: + inputs: + nextflow-version: + description: "Nextflow version to test against" + required: false + default: "latest-stable" # Cancel if a newer run with the same workflow name is queued concurrency: @@ -46,16 +51,19 @@ jobs: tests: ${{ steps.list_tests.outputs.tests }} test: - name: Run ${{matrix.test}} with Python ${{ matrix.python-version }} on ubuntu-latest + name: Run ${{matrix.test}} with Python ${{ matrix.python-version }} and Nextflow ${{ matrix.nextflow-version }} needs: list_tests runs-on: - runs-on=${{ github.run_id }}-run-test - runner=4cpu-linux-x64 strategy: matrix: - # On main branch test with 3.10 and 3.14, otherwise just 3.10 + # On PRs to main test with 3.10 and 3.14, otherwise just 3.10 python-version: ${{ github.base_ref == 'main' && fromJson('["3.10", "3.14"]') || fromJson('["3.10"]') }} test: ${{ fromJson(needs.list_tests.outputs.tests).test }} + # On push/release test both NF versions, otherwise just 25.10.4 + # Note: python×nextflow must stay ≤2 with 88 tests to stay under GitHub's 256-config limit + nextflow-version: ${{ (github.event_name == 'push' || github.event_name == 'release') && fromJson('["25.10.4", "latest-stable"]') || fromJson('["25.10.4"]') }} fail-fast: false # run all tests even if one fails steps: - name: go to subdirectory and change nextflow workdir @@ -74,7 +82,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -84,18 +92,17 @@ jobs: - name: Set up Apptainer if: ${{ startsWith(matrix.test, 'pipelines/download/') }} uses: eWaterCycle/setup-apptainer@4bb22c52d4f63406c49e94c804632975787312b3 # v2.0.0 - with: - apptainer-version: 1.3.4 - - - name: Get current date - id: date - run: echo "date=$(date +'%Y-%m')" >> $GITHUB_ENV - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 + with: + version: ${{ github.event.inputs.nextflow-version || matrix.nextflow-version }} - name: Install nf-test - uses: nf-core/setup-nf-test@v1 + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 + with: + version: 0.9.5 + install-fast-diff: true - name: move coveragerc file up run: | @@ -136,7 +143,7 @@ jobs: echo "test=${test}" >> $GITHUB_ENV - name: Store snapshot report - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() && contains(matrix.test, 'test_create_app') && steps.pytest.outcome == 'failure' with: include-hidden-files: true diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index b84b541523..f239c2eafd 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -82,7 +82,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -90,9 +90,9 @@ jobs: run: uv sync - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 with: - version: "latest-everything" + version: "25.10.4" - name: Set Git default branch from nextflow.config and set git default branch to that or "master" @@ -125,7 +125,7 @@ jobs: - name: Upload sync log file artifact if: ${{ always() }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sync_log_${{ matrix.pipeline }} path: sync_log_${{ matrix.pipeline }}.txt diff --git a/.github/workflows/test_offline_configs.yml b/.github/workflows/test_offline_configs.yml index 8c67d5dc2c..250f456d2c 100644 --- a/.github/workflows/test_offline_configs.yml +++ b/.github/workflows/test_offline_configs.yml @@ -98,7 +98,7 @@ jobs: - name: Create Issue on Test Failure if: ${{ failure() }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: github-token: ${{ secrets.nf_core_bot_auth_token }} script: | diff --git a/.github/workflows/tools-api-docs.yml b/.github/workflows/tools-api-docs.yml index 83b7d6a295..f069711fe4 100644 --- a/.github/workflows/tools-api-docs.yml +++ b/.github/workflows/tools-api-docs.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: trigger API docs build - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: github-token: ${{ secrets.nf_core_bot_auth_token }} script: | diff --git a/.github/workflows/update-template-snapshots.yml b/.github/workflows/update-template-snapshots.yml index 05e07632e8..3edff55a0f 100644 --- a/.github/workflows/update-template-snapshots.yml +++ b/.github/workflows/update-template-snapshots.yml @@ -15,7 +15,7 @@ jobs: steps: - name: branch-deploy id: branch-deploy - uses: github/branch-deploy@fded0351b6b79f854b335c11b3d93063461dd288 # v11.1.2 + uses: github/branch-deploy@ddf8ca48e9cdb2d2c7c71dee428308b19af9313f # v11.1.4 with: trigger: "@nf-core-bot update template snapshots" reaction: "eyes" @@ -61,7 +61,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true @@ -69,12 +69,12 @@ jobs: run: uv sync --all-extras - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 with: version: latest-everything - name: Install nf-test - uses: nf-core/setup-nf-test@v1 + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 with: version: "0.9.4" install-pdiff: true @@ -127,7 +127,7 @@ jobs: - name: Upload snapshot artifact if: steps.nf-test.outcome == 'success' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: snapshot-${{ matrix.TEMPLATE }} path: .github/snapshots/${{ matrix.TEMPLATE }}.nf.test.snap diff --git a/.github/workflows/update-textual-snapshots.yml b/.github/workflows/update-textual-snapshots.yml index dbbf52bf1f..07e172b45d 100644 --- a/.github/workflows/update-textual-snapshots.yml +++ b/.github/workflows/update-textual-snapshots.yml @@ -10,7 +10,7 @@ jobs: steps: - name: branch-deploy id: branch-deploy - uses: github/branch-deploy@fded0351b6b79f854b335c11b3d93063461dd288 # v11.1.2 + uses: github/branch-deploy@ddf8ca48e9cdb2d2c7c71dee428308b19af9313f # v11.1.4 with: trigger: "@nf-core-bot update textual snapshots" reaction: "eyes" @@ -42,7 +42,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74bcb5ef20..3b3706fb17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 + rev: v0.15.12 hooks: - id: ruff-check # linter args: [--fix, --exit-non-zero-on-fix] # sort imports and fix @@ -11,7 +11,7 @@ repos: - id: prettier entry: prettier --experimental-cli --write --ignore-unknown --no-cache additional_dependencies: - - prettier@3.8.1 + - prettier@3.8.3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: @@ -35,9 +35,10 @@ repos: tests/configs/__snapshots__/.* )$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.19.1" + rev: "v2.0.0" hooks: - id: mypy + args: [--ignore-missing-imports, --scripts-are-modules, -n4] additional_dependencies: - types-PyYAML - types-requests @@ -46,6 +47,13 @@ repos: - types-setuptools - pydantic - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.10.7 + rev: 0.11.8 hooks: - id: uv-lock + - repo: local + hooks: + - id: lint-test-docstrings + name: Check lint test names are documented in docstrings + language: system + entry: python3 docs/api/check_lint_docstrings.py + files: ^nf_core/(modules|subworkflows)/lint/.*\.py$ diff --git a/CHANGELOG.md b/CHANGELOG.md index d9dc645753..56398753fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,56 @@ # nf-core/tools: Changelog -## v3.6.0dev +## v4.1.0dev ### General -- Lock file maintenance ([#4044](https://github.com/nf-core/tools/pull/4044)) -- Lock file maintenance ([#4045](https://github.com/nf-core/tools/pull/4045)) -- Update actions/stale digest to b5d41d4 ([#4047](https://github.com/nf-core/tools/pull/4047)) -- hinder renovate from updating packages with a fixed range ([#4058](https://github.com/nf-core/tools/pull/4058)) -- Update actions/upload-artifact action to v7 ([#4061](https://github.com/nf-core/tools/pull/4061)) -- allow harshil alignment™ in version channels ([#4064](https://github.com/nf-core/tools/pull/4064)) -- Update python:3.14-slim Docker digest to 6a27522 ([#4072](https://github.com/nf-core/tools/pull/4072)) -- Update GitHub Actions ([#4067](https://github.com/nf-core/tools/pull/4067)) -- Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.4 ([#4069](https://github.com/nf-core/tools/pull/4069)) +- Update pre-commit hook pre-commit/mirrors-mypy to v2 ([#4270](https://github.com/nf-core/tools/pull/4270)) +- container configs: use correct key for conda configs ([#4269](https://github.com/nf-core/tools/pull/4269)) + +### Linting + +- accept `process_low_memory` as a standard module label ([#4264](https://github.com/nf-core/tools/pull/4264)) + +### Modules + +### Subworkflows + +### Template + +- add `process_low_memory` resource label to `base.config` ([#4264](https://github.com/nf-core/tools/pull/4264)) + +#### Version updates + +## [v4.0.2 - Bold Boa Patch 2](https://github.com/nf-core/tools/releases/tag/4.0.2) - [2026-04-30] + +### General + +- add `pipeline_dir` to search directories for include statements ([#4252](https://github.com/nf-core/tools/pull/4252)) + +### Template + +- fix version capture in downloads_action ([#4251](https://github.com/nf-core/tools/pull/4251)) +- Remove format constraint for igenomes_base ([#4253](https://github.com/nf-core/tools/pull/4253)) + +## [v4.0.1 - Bold Boa Patch](https://github.com/nf-core/tools/releases/tag/4.0.1) - [2026-04-29] + +### General + +- handle prek not being exposed to $PATH ([#4238](https://github.com/nf-core/tools/pull/4238)) +- fix failing api doc generation script ([#4239](https://github.com/nf-core/tools/pull/4239)) +- switch changelog bot trigger only on comments ([#4241](https://github.com/nf-core/tools/pull/4241)) +- fix indentation in generated api docs ([#4245](https://github.com/nf-core/tools/pull/4245)) + +### Modules + +- Allow task.ext.prefix2 in modules linting ([#4234](https://github.com/nf-core/tools/pull/4234)) + +## [v4.0.0 - Bold Boa](https://github.com/nf-core/tools/releases/tag/4.0.0) - [2026-04-28] + +### General + +- Add command to generate pipeline container config files ([#3955](https://github.com/nf-core/tools/pull/3955)) +- sync: don't overwrite the defaultBranch if already set in nextflow.config ([#3939](https://github.com/nf-core/tools/pull/3939)) - Add aliases to common sub-subcommands like `install`, `lint`, etc. ([#3980](https://github.com/nf-core/tools/pull/3980)) | Command | Subcommand | Aliases | @@ -37,51 +75,140 @@ | | `list` | `ls` | | | `list-branches` | `lsb` | -- bump nf-test to 0.9.4 ([#4079](https://github.com/nf-core/tools/pull/4079)) +- Add more ruff rules (B, PTH, RSE, SIM, C4, BLE, A) ([#4034](https://github.com/nf-core/tools/pull/4034)) +- hinder renovate from updating packages with a fixed range ([#4058](https://github.com/nf-core/tools/pull/4058)) - nf-core bot: collect all snapshots before commiting ([#4082](https://github.com/nf-core/tools/pull/4082)) -- Update GitHub Actions to v4 (major) ([#4081](https://github.com/nf-core/tools/pull/4081)) - fix nf-core bot snapshot action ([#4083](https://github.com/nf-core/tools/pull/4083)) -- sync: don't overwrite the defaultBranch if already set in nextflow.config ([#3939](https://github.com/nf-core/tools/pull/3939)) +- Fix GHA notification for AWS full tests job ([#4092](https://github.com/nf-core/tools/pull/4092)) +- Parse manifest.contributors to fill in the RO-Crate ([#4147](https://github.com/nf-core/tools/pull/4147)) +- Isolate test runs to avoid cross talks of Nextflow assets ([#4175](https://github.com/nf-core/tools/pull/4175)) +- Remove `--migrate-pytest` functionality and deprecated pipeline commands ([#4167](https://github.com/nf-core/tools/pull/4167)) +- use prek instead of pre-commit in all instances ([#4187](https://github.com/nf-core/tools/pull/4187)) +- nextflow source CI: set up uv ([#4208](https://github.com/nf-core/tools/pull/4208)) +- automatically handle merge conflicts for changelog files with union ([#4223](https://github.com/nf-core/tools/pull/4223)) + +### Download + +- Fix `nf-core pipelines download --platform` output directory structure and tagging ([#4185](https://github.com/nf-core/tools/pull/4185)) +- Add a fallback for `nextflow inspect` for pipelines without a default for the `--outdir` parameter specified in the test profiles ([#4212](https://github.com/nf-core/tools/pull/4212)) ### Linting - fix failing pytest for lint after samtools topic conversion ([#4026](https://github.com/nf-core/tools/pull/4026)) -- fix incorrect unqoting of `val()` version numbers ([#4042](https://github.com/nf-core/tools/pull/4042)) -- allow versions.yml in the version topics ([#4094](https://github.com/nf-core/tools/pull/4094)) +- capture correct error for invalid .nf-core.yml ([#4080](https://github.com/nf-core/tools/pull/4080)) - Ensure release linting happens on branch named main ([#4121](https://github.com/nf-core/tools/pull/4121)) - Add linting for meta and ext keys ([#4127](https://github.com/nf-core/tools/pull/4127)) - Add strict syntax linting to template with pre-commit ([#4128](https://github.com/nf-core/tools/pull/4128)) +- Lint meta variable names ([#4129](https://github.com/nf-core/tools/pull/4129)) +- Lint against the correct repo ([#4140](https://github.com/nf-core/tools/pull/4140)) - Fix false positive matches to words like text in ext key linting ([#4142](https://github.com/nf-core/tools/pull/4142)) +- Fix changelog bot failing after linting step ([#4155](https://github.com/nf-core/tools/pull/4155)) +- lint for correct syntax for compressed files in stubs ([#4048](https://github.com/nf-core/tools/pull/4048)) +- fix linting with missing input ([#4202](https://github.com/nf-core/tools/pull/4202)) +- fix(lint): use correct config key zenodo_doi ([#4201](https://github.com/nf-core/tools/pull/4201)) +- add missing lint test documentation and add pre-commit check for them ([#4052](https://github.com/nf-core/tools/pull/4052)) ### Modules +- fix incorrect unqoting of `val()` version numbers ([#4042](https://github.com/nf-core/tools/pull/4042)) - Template: have a `main:` section in workflow even when modules are skipped ([#4043](https://github.com/nf-core/tools/pull/4043)) +- allow harshil alignment™ in version channels ([#4064](https://github.com/nf-core/tools/pull/4064)) +- Update example links in main.nf comments ([#4188](https://github.com/nf-core/tools/pull/4188)) - Always prettify modules.json ([#4063](https://github.com/nf-core/tools/pull/4063)) -- Update nf-test module template to topics ([#4113](https://github.com/nf-core/tools/pull/4113)) +- Allow versions.yml in the version topics ([#4094](https://github.com/nf-core/tools/pull/4094)) - switch to gitlab module to test with old version syntax ([#4126](https://github.com/nf-core/tools/pull/4126)) +- linting: Fix version emit for version.yml ([#4095](https://github.com/nf-core/tools/pull/4095)) +- linting: make version topics mandatory ([#4163](https://github.com/nf-core/tools/pull/4163)) +- only check for compressed stubs with .gz as final extension ([#4206](https://github.com/nf-core/tools/pull/4206)) +- modules linting: require registry prefix in container link ([#4209](https://github.com/nf-core/tools/pull/4209)) +- add apptainer support to module template ([#4210](https://github.com/nf-core/tools/pull/4210)) ### Subworkflows - update test to new upstream subworkflow structure ([#4038](https://github.com/nf-core/tools/pull/4038)) - Fix lint: preserve underscores for subworkflow includes via full path ([#4074](https://github.com/nf-core/tools/pull/4074)) - update modules and subworkflows in template ([#4077](https://github.com/nf-core/tools/pull/4077)) +- add `--force` parameter to `modules remove` and `subworkflow remove` ([#4213](https://github.com/nf-core/tools/pull/4213)) -### Pipeline template +### Template - Remove webhook notifications (hook_url, slackreport, adaptivecard) ([#4051](https://github.com/nf-core/tools/pull/4051)). - Use specific nextflow plugins instead: - [nf-slack](https://github.com/seqeralabs/nf-slack) - [nf-teams](https://github.com/nvnieuwk/nf-teams) - Add `.lineage/` directory to the template .gitignore ([#4075](https://github.com/nf-core/tools/pull/4075)). +- Update nf-test module template to topics ([#4113](https://github.com/nf-core/tools/pull/4113)) - Fix nf-core tools version to what is specified in `.nf-core.yml` for the GitHub workflow `download_pipeline.yml` ([#4124](https://github.com/nf-core/tools/pull/4124)). +- Update all documentation URLs to point to equivalents on the new nf-core website documentation structure ([#4135](https://github.com/nf-core/tools/pull/4135)) +- Trigger full nf-test run if scripts in bin/ or schema JSON files are modified ([#3897](https://github.com/nf-core/tools/pull/3897)) +- Add additional fusion specific exit codes ([#4180](https://github.com/nf-core/tools/pull/4180)) +- fix: rename variables in pipeline nf-test template default.nf.test for clarity ([#4189](https://github.com/nf-core/tools/pull/4189)) +- +- bump nextflow to 25.10.4 ([#4190](https://github.com/nf-core/tools/pull/4190)) +- fix jinja variable in template ([#4156](https://github.com/nf-core/tools/pull/4156)) +- fix some nextflow lint warnings in different skipped template versions ([#4191](https://github.com/nf-core/tools/pull/4191)) +- trigger full nf-test run if CI changes ([#4203](https://github.com/nf-core/tools/pull/4203)) +- Include AI and LLM usage guidelines in CONTRIBUTING.md ([#4211](https://github.com/nf-core/tools/pull/4211)) #### Version updates -- Update apptainer setup action version ([#4036](https://github.com/nf-core/tools/pull/4036)) -- Update pre-commit hook astral-sh/uv-pre-commit to v0.10.2 ([#4037](https://github.com/nf-core/tools/pull/4037)) +- Update GitHub Actions ([#4001](https://github.com/nf-core/tools/pull/4001)) - Update dependency prettier to v3.8.1 ([#4003](https://github.com/nf-core/tools/pull/4003)) +- Update dawidd6/action-download-artifact action to v14 ([#4018](https://github.com/nf-core/tools/pull/4018)) +- Update mcr.microsoft.com/devcontainers/miniconda Docker digest to b99720f ([#4022](https://github.com/nf-core/tools/pull/4022)) +- Update apptainer setup action version ([#4036](https://github.com/nf-core/tools/pull/4036)) - Update pre-commit hook astral-sh/uv-pre-commit to v0.10.2 ([#4037](https://github.com/nf-core/tools/pull/4037)) - Update python:3.14-slim Docker digest to 486b809 ([#4039](https://github.com/nf-core/tools/pull/4039)) +- Lock file maintenance ([#4044](https://github.com/nf-core/tools/pull/4044)) +- Lock file maintenance ([#4045](https://github.com/nf-core/tools/pull/4045)) +- Update actions/stale digest to b5d41d4 ([#4047](https://github.com/nf-core/tools/pull/4047)) +- Update dawidd6/action-download-artifact action to v15 ([#4053](https://github.com/nf-core/tools/pull/4053)) +- Lock file maintenance ([#4055](https://github.com/nf-core/tools/pull/4055)) +- Update dawidd6/action-download-artifact action to v16 ([#4059](https://github.com/nf-core/tools/pull/4059)) +- Update actions/upload-artifact action to v7 ([#4061](https://github.com/nf-core/tools/pull/4061)) +- Update GitHub Actions ([#4067](https://github.com/nf-core/tools/pull/4067)) +- Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.4 ([#4069](https://github.com/nf-core/tools/pull/4069)) +- Update pre-commit hook astral-sh/uv-pre-commit to v0.10.7 ([#4070](https://github.com/nf-core/tools/pull/4070)) +- Update python:3.14-slim Docker digest to 6a27522 ([#4072](https://github.com/nf-core/tools/pull/4072)) +- Update dependency textual to v8 ([#4078](https://github.com/nf-core/tools/pull/4078)) +- bump nf-test to 0.9.4 ([#4079](https://github.com/nf-core/tools/pull/4079)) +- Update GitHub Actions to v4 (major) ([#4081](https://github.com/nf-core/tools/pull/4081)) +- Pin dependencies ([#4090](https://github.com/nf-core/tools/pull/4090)) +- Update GitHub Actions ([#4091](https://github.com/nf-core/tools/pull/4091)) +- Update GitHub Actions ([#4137](https://github.com/nf-core/tools/pull/4137)) +- Update codecov/codecov-action action to v6 ([#4138](https://github.com/nf-core/tools/pull/4138)) +- Update astral-sh/setup-uv action to v8 ([#4148](https://github.com/nf-core/tools/pull/4148)) +- Update GitHub Actions ([#4150](https://github.com/nf-core/tools/pull/4150)) +- Update mcr.microsoft.com/devcontainers/base Docker tag to v2.1.7 ([#4151](https://github.com/nf-core/tools/pull/4151)) +- Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.9 ([#4152](https://github.com/nf-core/tools/pull/4152)) +- Update pre-commit hook astral-sh/uv-pre-commit to v0.11.2 ([#4153](https://github.com/nf-core/tools/pull/4153)) +- Update mcr.microsoft.com/devcontainers/miniconda Docker digest to 26b252e ([#4158](https://github.com/nf-core/tools/pull/4158)) +- chore(deps): update python:3.14-slim docker digest to 5e59aae ([#4159](https://github.com/nf-core/tools/pull/4159)) +- Update dependency textual to v8.2.1 ([#4161](https://github.com/nf-core/tools/pull/4161)) +- Update mshick/add-pr-comment digest to f102bb3 ([#4162](https://github.com/nf-core/tools/pull/4162)) +- Update pre-commit hook pre-commit/mirrors-mypy to v1.20.0 ([#4165](https://github.com/nf-core/tools/pull/4165)) +- Update nf-core/setup-nf-test action to v2 ([#4166](https://github.com/nf-core/tools/pull/4166)) +- Update pre-commit hook astral-sh/uv-pre-commit to v0.11.3 ([#4169](https://github.com/nf-core/tools/pull/4169)) +- Update GitHub Actions ([#4170](https://github.com/nf-core/tools/pull/4170)) +- Update dawidd6/action-download-artifact action to v20 ([#4171](https://github.com/nf-core/tools/pull/4171)) +- chore(deps): update pypa/gh-action-pypi-publish digest to cef2210 ([#4178](https://github.com/nf-core/tools/pull/4178)) +- Update pre-commit hook astral-sh/uv-pre-commit to v0.11.4 ([#4179](https://github.com/nf-core/tools/pull/4179)) +- Update actions/github-script action to v9 ([#4182](https://github.com/nf-core/tools/pull/4182)) +- Update actions/upload-artifact digest to 043fb46 ([#4184](https://github.com/nf-core/tools/pull/4184)) +- Update dependency prettier to v3.8.3 ([#4192](https://github.com/nf-core/tools/pull/4192)) +- Update dependency textual to v8.2.3 ([#4194](https://github.com/nf-core/tools/pull/4194)) +- Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.11 ([#4195](https://github.com/nf-core/tools/pull/4195)) +- Lock file maintenance ([#4198](https://github.com/nf-core/tools/pull/4198)) +- Update nf-core/setup-nextflow action to v3 ([#4199](https://github.com/nf-core/tools/pull/4199)) +- Update python:3.14-slim Docker digest to bc389f7 ([#4200](https://github.com/nf-core/tools/pull/4200)) +- Update astral-sh/setup-uv action to v8 ([#4207](https://github.com/nf-core/tools/pull/4207)) +- Update conda-incubator/setup-miniconda action to v4 ([#4215](https://github.com/nf-core/tools/pull/4215)) +- Update dependency textual to v8.2.4 ([#4217](https://github.com/nf-core/tools/pull/4217)) +- Update mcr.microsoft.com/devcontainers/base Docker tag to v2.1.8 ([#4218](https://github.com/nf-core/tools/pull/4218)) +- Update pre-commit hooks ([#4219](https://github.com/nf-core/tools/pull/4219)) +- Update mcr.microsoft.com/devcontainers/miniconda Docker digest to 1a85cf3 ([#4220](https://github.com/nf-core/tools/pull/4220)) +- Update python:3.14-slim Docker digest to 5b3879b ([#4221](https://github.com/nf-core/tools/pull/4221)) +- Update GitHub Actions ([#4222](https://github.com/nf-core/tools/pull/4222)) ## v3.5.2 @@ -104,6 +231,7 @@ ### Template +- add trainling slash for pipelines_testdata_base_path ([#3701](https://github.com/nf-core/tools/pull/3701)) - switch to uv and prek for pipeline linting workflow ([#3942](https://github.com/nf-core/tools/pull/3942)) - add schema to devcontainer.json ([#3908](https://github.com/nf-core/tools/pull/3908)) @@ -125,6 +253,7 @@ ### Subworkflows - Update to new topic version handling ([#3929](https://github.com/nf-core/tools/pull/3929)) +- split subworkflow/test_lint into separate files ([#3965](https://github.com/nf-core/tools/pull/3965)) #### Version updates @@ -709,10 +838,6 @@ We also enabled to install subworkflows with modules from different remotes. | `-p` / `--parallel-downloads` | `-d` / `--parallel-downloads` | | new parameter | `-p` / (`--platform`) | -### Configs - -- New command: `nf-core configs create wizard` for generating configs for nf-core pipelines ([#3001](https://github.com/nf-core/tools/pull/3001)) - ### General - Change default branch to `main` for the nf-core/tools repository diff --git a/Dockerfile b/Dockerfile index 6aef2c3e4a..d3ac94f7dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim@sha256:6a27522252aef8432841f224d9baaa6e9fce07b07584154fa0b9a96603af7456 +FROM python:3.14-slim@sha256:5b3879b6f3cb77e712644d50262d05a7c146b7312d784a18eff7ff5462e77033 LABEL authors="phil.ewels@seqera.io,erik.danielsson@scilifelab.se" \ description="Docker image containing requirements for nf-core/tools" @@ -22,7 +22,7 @@ RUN mkdir -p /usr/share/man/man1 \ # Setup ARG for NXF_VER ENV ARG NXF_VER="" -ENV NXF_VER ${NXF_VER} +ENV NXF_VER=${NXF_VER} # Install Nextflow RUN curl -s https://get.nextflow.io | bash \ && mv nextflow /usr/local/bin \ diff --git a/MANIFEST.in b/MANIFEST.in index c80301607e..ba36475db3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,5 +8,6 @@ include nf_core/assets/logo/nf-core-repo-logo-base-lightbg.png include nf_core/assets/logo/nf-core-repo-logo-base-darkbg.png include nf_core/assets/logo/placeholder_logo.svg include nf_core/assets/logo/MavenPro-Bold.ttf +include nf_core/pipelines/create/create.tcss include nf_core/textual.tcss include nf_core/pipelines/create/template_features.yml diff --git a/README.md b/README.md index 52e59e9b85..88542d2176 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ For documentation of the internal Python functions, please refer to the [Tools P ## Installation -For full installation instructions, please see the [nf-core documentation](https://nf-co.re/docs/nf-core-tools/installation). +For full installation instructions, please see the [nf-core documentation](https://nf-co.re/docs/nf-core-tools/cli/installation). Below is a quick-start for those who know what they're doing: ### Bioconda diff --git a/docs/api/_src/conf.py b/docs/api/_src/conf.py index d5f75f5ccf..4785ea2647 100644 --- a/docs/api/_src/conf.py +++ b/docs/api/_src/conf.py @@ -11,17 +11,17 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os import sys +from pathlib import Path import nf_core -sys.path.insert(0, os.path.abspath("../../../nf_core")) +sys.path.insert(0, str(Path("../../../nf_core").resolve())) # -- Project information ----------------------------------------------------- project = "nf-core/tools" -copyright = "2021, nf-core community" +copyright = "2021, nf-core community" # noqa: A001 - required by Sphinx author = "Numerous nf-core contributors" # The short X.Y version diff --git a/docs/api/_src/pipeline_lint_tests/actions_nf_test.md b/docs/api/_src/pipeline_lint_tests/actions_nf_test.md index 8370ff94bb..c508c136a6 100644 --- a/docs/api/_src/pipeline_lint_tests/actions_nf_test.md +++ b/docs/api/_src/pipeline_lint_tests/actions_nf_test.md @@ -1,5 +1,5 @@ # actions_nf_test - ```{eval-rst} - .. automethod:: nf_core.pipelines.lint.PipelineLint.actions_nf_test - ``` +```{eval-rst} +.. automethod:: nf_core.pipelines.lint.PipelineLint.actions_nf_test +``` diff --git a/docs/api/_src/pipeline_lint_tests/container_configs.md b/docs/api/_src/pipeline_lint_tests/container_configs.md new file mode 100644 index 0000000000..06b76c166a --- /dev/null +++ b/docs/api/_src/pipeline_lint_tests/container_configs.md @@ -0,0 +1,5 @@ +# container_configs + +```{eval-rst} +.. automethod:: nf_core.pipelines.lint.PipelineLint.container_configs +``` diff --git a/docs/api/_src/pipeline_lint_tests/index.md b/docs/api/_src/pipeline_lint_tests/index.md index de0d97b788..5b1b72b0bb 100644 --- a/docs/api/_src/pipeline_lint_tests/index.md +++ b/docs/api/_src/pipeline_lint_tests/index.md @@ -5,6 +5,7 @@ - [actions_nf_test](./actions_nf_test/) - [actions_schema_validation](./actions_schema_validation/) - [base_config](./base_config/) + - [container_configs](./container_configs/) - [files_exist](./files_exist/) - [files_unchanged](./files_unchanged/) - [included_configs](./included_configs/) diff --git a/docs/api/check_lint_docstrings.py b/docs/api/check_lint_docstrings.py new file mode 100644 index 0000000000..b9f27548f4 --- /dev/null +++ b/docs/api/check_lint_docstrings.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Pre-commit hook: verify that every lint test name used in a result tuple is +mentioned in the docstring of its enclosing function. + +Applies to nf-core component lint files where results are accumulated as: + + component.passed.append(("category", "test_name", "message", path)) + component.warned.append(("category", "test_name", "message", path)) + component.failed.append(("category", "test_name", "message", path)) + +Usage (called by pre-commit with the changed files as arguments): + + python scripts/check_lint_docstrings.py nf_core/modules/lint/module_tests.py ... +""" + +import ast +import sys +from pathlib import Path + + +def collect_test_names(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, list[int]]: + """Return {test_name: [line_numbers]} for all result-tuple appends in the function.""" + results: dict[str, list[int]] = {} + # Walk only the direct body — do not descend into nested function definitions. + nodes_to_visit: list[ast.AST] = list(func_node.body) + while nodes_to_visit: + node = nodes_to_visit.pop() + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue # skip nested scopes + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "append" + and isinstance(node.func.value, ast.Attribute) + and node.func.value.attr in ("passed", "warned", "failed") + and len(node.args) == 1 + and isinstance(node.args[0], ast.Tuple) + and len(node.args[0].elts) >= 2 + and isinstance(node.args[0].elts[1], ast.Constant) + and isinstance(node.args[0].elts[1].value, str) + ): + test_name = node.args[0].elts[1].value + results.setdefault(test_name, []).append(node.lineno) + nodes_to_visit.extend(ast.iter_child_nodes(node)) + return results + + +def check_file(path: Path) -> list[str]: + errors = [] + try: + source = path.read_text() + except OSError as e: + return [f"{path}: could not read file: {e}"] + + try: + tree = ast.parse(source, filename=str(path)) + except SyntaxError as e: + return [f"{path}: SyntaxError: {e}"] + + module_stem = path.stem # e.g. "module_tests" from "module_tests.py" + + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if node.name != module_stem: + continue + docstring = ast.get_docstring(node) or "" + test_names = collect_test_names(node) + for test_name, lines in sorted(test_names.items()): + if test_name not in docstring: + errors.append( + f"{path}:{lines[0]}: '{test_name}' used in {node.name}() but not documented in its docstring" + ) + break # only one function per file can match the stem + return errors + + +def main() -> int: + files = [Path(f) for f in sys.argv[1:] if f.endswith(".py")] + all_errors: list[str] = [] + for path in files: + all_errors.extend(check_file(path)) + for error in all_errors: + print(error, file=sys.stderr) + return 1 if all_errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/api/generate-api-docs.sh b/docs/api/generate-api-docs.sh index aa94cdea92..a88adfac22 100644 --- a/docs/api/generate-api-docs.sh +++ b/docs/api/generate-api-docs.sh @@ -3,6 +3,7 @@ # allow --force option and also a --release option (which takes a release name, or "all") force=false releases=() +original_ref="" while [[ $# -gt 0 ]]; do case $1 in @@ -38,6 +39,9 @@ if [[ ${#releases[@]} -eq 0 ]]; then releases+=("dev") fi +# Save current position so we can restore it after each checkout +original_ref=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse HEAD) + # Loop through each release for release in "${releases[@]}"; do # Checkout the release @@ -63,8 +67,10 @@ for release in "${releases[@]}"; do done - # fix syntax in lint/merge_markers.py - sed -i 's/>>>>>>> or <<<<<<>>>>>>`` or ``<<<<<<<``/g' nf_core/lint/merge_markers.py + # fix syntax in lint/merge_markers.py (file may not exist in all releases) + if [[ -f nf_core/lint/merge_markers.py ]]; then + sed -i 's/>>>>>>> or <<<<<<>>>>>>`` or ``<<<<<<<``/g' nf_core/lint/merge_markers.py + fi # remove markdown files if --force is set if [[ "$force" = true ]]; then echo -e "\n\e[31mRemoving $output_dir/$release because of '--force'\e[0m" @@ -75,7 +81,7 @@ for release in "${releases[@]}"; do # undo all changes git restore . - git checkout - + git checkout "$original_ref" # replace :::{seealso} with :::tip in the markdown files find "$output_dir/$release" -name "*.md" -exec sed -i 's/:::{seealso}/:::tip/g' {} \; i=1 @@ -89,6 +95,6 @@ for release in "${releases[@]}"; do find "$output_dir/$release" -name "*.md" -size 0 -delete # remove `.doctrees` directory rm -rf "$output_dir/$release/.doctrees" - # run pre-commit to fix any formatting issues on the generated markdown files - pre-commit run --files "$output_dir/$release" + # run prek to fix any formatting issues on the generated markdown files + prek run --files "$output_dir/$release" done diff --git a/nf_core/.pre-commit-prettier-config.yaml b/nf_core/.pre-commit-prettier-config.yaml index ef414cb69b..d7235f440d 100644 --- a/nf_core/.pre-commit-prettier-config.yaml +++ b/nf_core/.pre-commit-prettier-config.yaml @@ -4,4 +4,4 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.8.1 + - prettier@3.8.3 diff --git a/nf_core/__main__.py b/nf_core/__main__.py index 46c5035423..51aa4239b2 100644 --- a/nf_core/__main__.py +++ b/nf_core/__main__.py @@ -6,7 +6,7 @@ import sys from pathlib import Path -import rich +import requests import rich.console import rich.logging import rich.traceback @@ -60,12 +60,7 @@ from nf_core.components.constants import NF_CORE_MODULES_REMOTE from nf_core.pipelines.download.download import DownloadError from nf_core.pipelines.list import autocomplete_pipelines -from nf_core.utils import ( - check_if_outdated, - nfcore_logo, - rich_force_colors, - setup_nfcore_dir, -) +from nf_core.utils import check_if_outdated, nfcore_logo, rich_force_colors, setup_nfcore_dir # Set up logging as the root logger # Submodules should all traverse back to this @@ -79,6 +74,7 @@ rc.USE_RICH_MARKUP = True rc.COMMANDS_BEFORE_OPTIONS = True + # Set up rich stderr console stderr = rich.console.Console(stderr=True, force_terminal=rich_force_colors()) stdout = rich.console.Console(force_terminal=rich_force_colors()) @@ -127,7 +123,7 @@ def run_nf_core(): f"[bold bright_yellow] There is a new version of nf-core/tools available! ({remote_vers})", highlight=False, ) - except Exception as e: + except requests.exceptions.RequestException as e: log.debug(f"Could not check latest version: {e}") stderr.print("\n") # Launch the click cli @@ -135,7 +131,7 @@ def run_nf_core(): @tui(command="interface", help="Launch the nf-core interface") -@click.group(context_settings=dict(help_option_names=["-h", "--help"])) +@click.group(context_settings={"help_option_names": ["-h", "--help"]}) @click.version_option(__version__) @click.option( "-v", @@ -222,13 +218,7 @@ def pipelines(ctx): @click.option("-d", "--description", type=str, help="A short description of your pipeline") @click.option("-a", "--author", type=str, help="Name of the main author(s)") @click.option("--version", type=str, default="1.0.0dev", help="The initial version number to use") -@click.option( - "-f", - "--force", - is_flag=True, - default=False, - help="Overwrite output directory if it already exists", -) +@click.option("-f", "--force", is_flag=True, default=False, help="Overwrite output directory if it already exists") @click.option("-o", "--outdir", help="Output directory for new pipeline (default: pipeline name)") @click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template") @click.option( @@ -259,7 +249,7 @@ def command_pipelines_create(ctx, name, description, author, version, force, out is_flag=True, default=Path(os.environ.get("GITHUB_REF", "").strip(" '\"")).parent.name in ["master", "main"] and os.environ.get("GITHUB_REPOSITORY", "").startswith("nf-core/") - and not os.environ.get("GITHUB_REPOSITORY", "") == "nf-core/tools", + and os.environ.get("GITHUB_REPOSITORY", "") != "nf-core/tools", help="Execute additional checks for release-ready workflows.", ) @click.option( @@ -466,12 +456,13 @@ def command_pipelines_download( default=False, help="Show hidden params which don't normally need changing", ) +@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input") @click.pass_context -def command_pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden): +def command_pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden, no_prompts): """ Build a parameter file for a pipeline. """ - pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden) + pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden, no_prompts) # nf-core pipelines launch @@ -483,7 +474,7 @@ def command_pipelines_create_params_file(ctx, pipeline, revision, output, force, shell_complete=autocomplete_pipelines, ) @click.option("-r", "--revision", help="Release/branch/SHA of the project to run (if remote)") -@click.option("-i", "--id", help="ID for web-gui launch parameter set") +@click.option("-i", "--id", "launch_id", help="ID for web-gui launch parameter set") @click.option( "-c", "--command-only", @@ -495,7 +486,7 @@ def command_pipelines_create_params_file(ctx, pipeline, revision, output, force, "-o", "--params-out", type=click.Path(), - default=os.path.join(os.getcwd(), "nf-params.json"), + default=str(Path.cwd() / "nf-params.json"), help="Path to save run parameters file", ) @click.option( @@ -525,11 +516,12 @@ def command_pipelines_create_params_file(ctx, pipeline, revision, output, force, default="https://nf-co.re/launch", help="Customise the builder URL (for development work)", ) +@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input") @click.pass_context def command_pipelines_launch( ctx, pipeline, - id, + launch_id, revision, command_only, params_in, @@ -537,11 +529,14 @@ def command_pipelines_launch( save_all, show_hidden, url, + no_prompts, ): """ Launch a pipeline using a web GUI or command line prompts. """ - pipelines_launch(ctx, pipeline, id, revision, command_only, params_in, params_out, save_all, show_hidden, url) + pipelines_launch( + ctx, pipeline, launch_id, revision, command_only, params_in, params_out, save_all, show_hidden, url, no_prompts + ) # nf-core pipelines list @@ -636,14 +631,33 @@ def rocrate( @click.option("-u", "--username", type=str, help="GitHub PR: auth username.") @click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template") @click.option("-b", "--blog-post", type=str, help="Link to the blog post") +@click.option("-n", "--no-prompts", is_flag=True, default=False, help="Run without prompting for user input") def command_pipelines_sync( - ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post + ctx, + directory, + from_branch, + pull_request, + github_repository, + username, + template_yaml, + force_pr, + blog_post, + no_prompts, ): """ Sync a pipeline [cyan i]TEMPLATE[/] branch with the nf-core template. """ pipelines_sync( - ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post + ctx, + directory, + from_branch, + pull_request, + github_repository, + username, + template_yaml, + force_pr, + blog_post, + no_prompts, ) @@ -699,6 +713,7 @@ def command_pipelines_bump_version(ctx, new_version, directory, nextflow): ) @click.option( "--format", + "img_format", type=click.Choice(["png", "svg"]), default="png", help="Image format of the logo, either PNG or SVG.", @@ -711,11 +726,11 @@ def command_pipelines_bump_version(ctx, new_version, directory, nextflow): default=False, help="Overwrite any files if they already exist", ) -def command_pipelines_create_logo(logo_text, directory, name, theme, width, format, force): +def command_pipelines_create_logo(logo_text, directory, name, theme, width, img_format, force): """ Generate a logo with the nf-core logo template. """ - pipelines_create_logo(logo_text, directory, name, theme, width, format, force) + pipelines_create_logo(logo_text, directory, name, theme, width, img_format, force) # nf-core pipelines schema subcommands @@ -841,6 +856,7 @@ def command_pipelines_schema_lint(directory, schema_file): @click.option( "-x", "--format", + "output_format", type=click.Choice(["markdown", "html"]), default="markdown", help="Format to output docs in.", @@ -854,11 +870,42 @@ def command_pipelines_schema_lint(directory, schema_file): help="CSV list of columns to include in the parameter tables (parameter,description,type,default,required,hidden)", default="parameter,description,type,default,required,hidden", ) -def command_pipelines_schema_docs(directory, schema_file, output, format, force, columns): +def command_pipelines_schema_docs(directory, schema_file, output, output_format, force, columns): """ Outputs parameter documentation for a pipeline schema. """ - pipelines_schema_docs(Path(directory, schema_file), output, format, force, columns) + pipelines_schema_docs(Path(directory, schema_file), output, output_format, force, columns) + + +# nf-core configs subcommands +@nf_core_cli.group(aliases=["c", "config"]) +@click.pass_context +def configs(ctx): + """ + Commands to manage nf-core configs. + """ + # ensure that ctx.obj exists and is a dict (in case `cli()` is called + # by means other than the `if` block below) + ctx.ensure_object(dict) + + +# nf-core configs create +@configs.command("create") +@click.pass_context +def create_configs(ctx): + """ + Command to interactively create a nextflow or nf-core config + """ + from nf_core.configs.create import ConfigsCreateApp + + try: + log.info("Launching interactive nf-core configs creation tool.") + app = ConfigsCreateApp() + app.run() + sys.exit(app.return_code or 0) + except UserWarning as e: + log.error(e) + sys.exit(1) # nf-core modules subcommands @@ -1049,6 +1096,12 @@ def command_modules_install(ctx, tool, directory, prompt, force, sha): default=False, help="Automatically update all linked modules and subworkflows without asking for confirmation", ) +@click.option( + "--skip-deps", + is_flag=True, + default=False, + help="Skip the cascade to linked components. Conflicts with --update-deps and --all.", +) def command_modules_update( ctx, tool, @@ -1061,11 +1114,14 @@ def command_modules_update( save_diff, update_deps, limit_output, + skip_deps, ): """ Update DSL2 modules within a pipeline. """ - modules_update(ctx, tool, directory, force, prompt, sha, install_all, preview, save_diff, update_deps, limit_output) + modules_update( + ctx, tool, directory, force, prompt, sha, install_all, preview, save_diff, update_deps, limit_output, skip_deps + ) # nf-core modules patch @@ -1114,11 +1170,18 @@ def command_modules_patch(ctx, tool, directory, remove): default=".", help=r"Pipeline directory. [dim]\[default: current working directory][/]", ) -def command_modules_remove(ctx, directory, tool): +@click.option( + "-f", + "--force", + is_flag=True, + default=False, + help="Force removal of the module, even if it is included in the pipeline.", +) +def command_modules_remove(ctx, directory, tool, force): """ Remove a module from a pipeline. """ - modules_remove(ctx, directory, tool) + modules_remove(ctx, directory, tool, force) # nf-core modules create @@ -1182,12 +1245,6 @@ def command_modules_remove(ctx, directory, tool): default=False, help="Create a module from the template without TODOs or examples", ) -@click.option( - "--migrate-pytest", - is_flag=True, - default=False, - help="Migrate a module with pytest tests to nf-test", -) def command_modules_create( ctx, tool, @@ -1200,7 +1257,6 @@ def command_modules_create( conda_name, conda_package_version, empty_template, - migrate_pytest, ): """ Create a new DSL2 module from the nf-core template. @@ -1217,7 +1273,6 @@ def command_modules_create( conda_name, conda_package_version, empty_template, - migrate_pytest, ) @@ -1268,19 +1323,13 @@ def command_modules_create( default=None, help="Run tests with a specific profile", ) -@click.option( - "--migrate-pytest", - is_flag=True, - default=False, - help="Migrate a module with pytest tests to nf-test", -) -def command_modules_test(ctx, tool, directory, no_prompts, update, once, profile, migrate_pytest, verbose): +def command_modules_test(ctx, tool, directory, no_prompts, update, once, profile, verbose): """ Run nf-test for a module. """ if verbose: ctx.obj["verbose"] = verbose - modules_test(ctx, tool, directory, no_prompts, update, once, profile, migrate_pytest) + modules_test(ctx, tool, directory, no_prompts, update, once, profile) # nf-core modules lint @@ -1318,7 +1367,7 @@ def command_modules_test(ctx, tool, directory, no_prompts, update, once, profile multiple=True, help="Run only these lint tests", ) -@click.option("-a", "--all", is_flag=True, help="Run on all modules") +@click.option("-a", "--all", "all_modules", is_flag=True, help="Run on all modules") @click.option("-w", "--fail-warned", is_flag=True, help="Convert warn tests to failures") @click.option("--local", is_flag=True, help="Run additional lint tests for local modules") @click.option("--passed", is_flag=True, help="Show passed tests") @@ -1342,13 +1391,25 @@ def command_modules_test(ctx, tool, directory, no_prompts, update, once, profile help="Print results in plain text format without Rich formatting (easier to copy). Can also be enabled with env var NF_CORE_LINT_OUTPUT.", ) def command_modules_lint( - ctx, tool, directory, registry, key, all, fail_warned, local, passed, sort_by, fix_version, fix, plain_text + ctx, tool, directory, registry, key, all_modules, fail_warned, local, passed, sort_by, fix_version, fix, plain_text ): """ Lint one or more modules in a directory. """ modules_lint( - ctx, tool, directory, registry, key, all, fail_warned, local, passed, sort_by, fix_version, fix, plain_text + ctx, + tool, + directory, + registry, + key, + all_modules, + fail_warned, + local, + passed, + sort_by, + fix_version, + fix, + plain_text, ) @@ -1397,15 +1458,15 @@ def command_modules_info(ctx, tool, directory): default=".", metavar="", ) -@click.option("-a", "--all", is_flag=True, help="Run on all modules") +@click.option("-a", "--all", "all_modules", is_flag=True, help="Run on all modules") @click.option("-s", "--show-all", is_flag=True, help="Show up-to-date modules in results too") @click.option("-r", "--dry-run", is_flag=True, help="Dry run the command") -def command_modules_bump_versions(ctx, tool, directory, all, show_all, dry_run): +def command_modules_bump_versions(ctx, tool, directory, all_modules, show_all, dry_run): """ Bump versions for one or more modules in a clone of the nf-core/modules repo. """ - modules_bump_versions(ctx, tool, directory, all, show_all, dry_run) + modules_bump_versions(ctx, tool, directory, all_modules, show_all, dry_run) # nf-core subworkflows click command @@ -1467,17 +1528,11 @@ def subworkflows(ctx, git_remote, branch, no_pull): default=False, help="Overwrite any files if they already exist", ) -@click.option( - "--migrate-pytest", - is_flag=True, - default=False, - help="Migrate a module with pytest tests to nf-test", -) -def command_subworkflows_create(ctx, subworkflow, directory, author, force, migrate_pytest): +def command_subworkflows_create(ctx, subworkflow, directory, author, force): """ Create a new subworkflow from the nf-core template. """ - subworkflows_create(ctx, subworkflow, directory, author, force, migrate_pytest) + subworkflows_create(ctx, subworkflow, directory, author, force) # nf-core subworkflows test @@ -1520,17 +1575,11 @@ def command_subworkflows_create(ctx, subworkflow, directory, author, force, migr default=None, help="Run tests with a specific profile", ) -@click.option( - "--migrate-pytest", - is_flag=True, - default=False, - help="Migrate a subworkflow with pytest tests to nf-test", -) -def command_subworkflows_test(ctx, subworkflow, directory, no_prompts, update, once, profile, migrate_pytest): +def command_subworkflows_test(ctx, subworkflow, directory, no_prompts, update, once, profile): """ Run nf-test for a subworkflow. """ - subworkflows_test(ctx, subworkflow, directory, no_prompts, update, once, profile, migrate_pytest) + subworkflows_test(ctx, subworkflow, directory, no_prompts, update, once, profile) # nf-core subworkflows list subcommands @@ -1610,7 +1659,7 @@ def command_subworkflows_list_local(ctx, keywords, json, directory): # pylint: multiple=True, help="Run only these lint tests", ) -@click.option("-a", "--all", is_flag=True, help="Run on all subworkflows") +@click.option("-a", "--all", "all_subworkflows", is_flag=True, help="Run on all subworkflows") @click.option("-w", "--fail-warned", is_flag=True, help="Convert warn tests to failures") @click.option("--local", is_flag=True, help="Run additional lint tests for local subworkflows") @click.option("--passed", is_flag=True, help="Show passed tests") @@ -1629,13 +1678,24 @@ def command_subworkflows_list_local(ctx, keywords, json, directory): # pylint: help="Print results in plain text format without Rich formatting (easier to copy). Can also be enabled with env var NF_CORE_LINT_OUTPUT.", ) def command_subworkflows_lint( - ctx, subworkflow, directory, registry, key, all, fail_warned, local, passed, sort_by, fix, plain_text + ctx, subworkflow, directory, registry, key, all_subworkflows, fail_warned, local, passed, sort_by, fix, plain_text ): """ Lint one or more subworkflows in a directory. """ subworkflows_lint( - ctx, subworkflow, directory, registry, key, all, fail_warned, local, passed, sort_by, fix, plain_text + ctx, + subworkflow, + directory, + registry, + key, + all_subworkflows, + fail_warned, + local, + passed, + sort_by, + fix, + plain_text, ) @@ -1705,11 +1765,17 @@ def command_subworkflows_info(ctx, subworkflow, directory): metavar="", help="Install subworkflow at commit SHA", ) -def command_subworkflows_install(ctx, subworkflow, directory, prompt, force, sha): +@click.option( + "--skip-deps", + is_flag=True, + default=False, + help="With --force, refresh only the named subworkflow; leave transitive deps' files and SHAs pinned.", +) +def command_subworkflows_install(ctx, subworkflow, directory, prompt, force, sha, skip_deps): """ Install DSL2 subworkflow within a pipeline. """ - subworkflows_install(ctx, subworkflow, directory, prompt, force, sha) + subworkflows_install(ctx, subworkflow, directory, prompt, force, sha, skip_deps) # nf-core subworkflows patch @@ -1726,12 +1792,13 @@ def command_subworkflows_install(ctx, subworkflow, directory, prompt, force, sha @click.option( "-d", "--dir", + "directory", type=click.Path(exists=True), default=".", help=r"Pipeline directory. [dim]\[default: current working directory][/]", ) @click.option("-r", "--remove", is_flag=True, default=False, help="Remove an existent patch file and regenerate it.") -def subworkflows_patch(ctx, subworkflow, dir, remove): +def subworkflows_patch(ctx, subworkflow, directory, remove): """ Create a patch file for minor changes in a subworkflow @@ -1742,7 +1809,7 @@ def subworkflows_patch(ctx, subworkflow, dir, remove): try: subworkflow_patch = SubworkflowPatch( - dir, + directory, ctx.obj["modules_repo_url"], ctx.obj["modules_repo_branch"], ctx.obj["modules_repo_no_pull"], @@ -1775,11 +1842,18 @@ def subworkflows_patch(ctx, subworkflow, dir, remove): default=".", help=r"Pipeline directory. [dim]\[default: current working directory][/]", ) -def command_subworkflows_remove(ctx, directory, subworkflow): +@click.option( + "-f", + "--force", + is_flag=True, + default=False, + help="Force removal of the subworkflow, even if it is included in the pipeline.", +) +def command_subworkflows_remove(ctx, directory, subworkflow, force): """ Remove a subworkflow from a pipeline. """ - subworkflows_remove(ctx, directory, subworkflow) + subworkflows_remove(ctx, directory, subworkflow, force) # nf-core subworkflows update @@ -1854,6 +1928,12 @@ def command_subworkflows_remove(ctx, directory, subworkflow): default=False, help="Automatically update all linked modules and subworkflows without asking for confirmation", ) +@click.option( + "--skip-deps", + is_flag=True, + default=False, + help="Skip the cascade to linked components. Conflicts with --update-deps and --all.", +) def command_subworkflows_update( ctx, subworkflow, @@ -1866,46 +1946,27 @@ def command_subworkflows_update( save_diff, update_deps, limit_output, + skip_deps, ): """ Update DSL2 subworkflow within a pipeline. """ subworkflows_update( - ctx, subworkflow, directory, force, prompt, sha, install_all, preview, save_diff, update_deps, limit_output + ctx, + subworkflow, + directory, + force, + prompt, + sha, + install_all, + preview, + save_diff, + update_deps, + limit_output, + skip_deps, ) -# nf-core configs subcommands -@nf_core_cli.group(aliases=["c", "config"]) -@click.pass_context -def configs(ctx): - """ - Commands to manage nf-core configs. - """ - # ensure that ctx.obj exists and is a dict (in case `cli()` is called - # by means other than the `if` block below) - ctx.ensure_object(dict) - - -# nf-core configs create -@configs.command("create") -@click.pass_context -def create_configs(ctx): - """ - Command to interactively create a nextflow or nf-core config - """ - from nf_core.configs.create import ConfigsCreateApp - - try: - log.info("Launching interactive nf-core configs creation tool.") - app = ConfigsCreateApp() - app.run() - sys.exit(app.return_code or 0) - except UserWarning as e: - log.error(e) - sys.exit(1) - - # nf-core test-dataset subcommands @nf_core_cli.group(aliases=["t", "td", "tds", "test-datasets"]) @click.pass_context @@ -1980,596 +2041,6 @@ def command_test_datasets_list_branches(ctx): test_datasets_list_branches(ctx) -## DEPRECATED commands since v3.0.0 - - -# nf-core schema subcommands (deprecated) -@nf_core_cli.group(deprecated=True, hidden=True) -def schema(): - """ - Use `nf-core pipelines schema ` instead. - """ - pass - - -# nf-core schema validate (deprecated) -@schema.command("validate", deprecated=True) -@click.argument("pipeline", required=True, metavar="") -@click.argument("params", type=click.Path(exists=True), required=True, metavar="") -def command_schema_validate(pipeline, params): - """ - Use `nf-core pipelines schema validate` instead. - """ - log.warning( - "The `[magenta]nf-core schema validate[/]` command is deprecated. Use `[magenta]nf-core pipelines schema validate[/]` instead." - ) - pipelines_schema_validate(pipeline, params) - - -# nf-core schema build (deprecated) -@schema.command("build", deprecated=True) -@click.option( - "-d", - "--dir", - "directory", - type=click.Path(exists=True), - default=".", - help=r"Pipeline directory. [dim]\[default: current working directory][/]", -) -@click.option( - "--no-prompts", - is_flag=True, - help="Do not confirm changes, just update parameters and exit", -) -@click.option( - "--web-only", - is_flag=True, - help="Skip building using Nextflow config, just launch the web tool", -) -@click.option( - "--url", - type=str, - default="https://oldsite.nf-co.re/pipeline_schema_builder", - help="Customise the builder URL (for development work)", -) -def command_schema_build(directory, no_prompts, web_only, url): - """ - Use `nf-core pipelines schema build` instead. - """ - log.warning( - "The `[magenta]nf-core schema build[/]` command is deprecated. Use `[magenta]nf-core pipelines schema build[/]` instead." - ) - pipelines_schema_build(directory, no_prompts, web_only, url) - - -# nf-core schema lint (deprecated) -@schema.command("lint", deprecated=True) -@click.argument( - "schema_path", - type=click.Path(exists=True), - default="nextflow_schema.json", - metavar="", -) -def command_schema_lint(schema_path): - """ - Use `nf-core pipelines schema lint` instead. - """ - log.warning( - "The `[magenta]nf-core schema lint[/]` command is deprecated. Use `[magenta]nf-core pipelines schema lint[/]` instead." - ) - pipelines_schema_lint(schema_path) - - -# nf-core schema docs (deprecated) -@schema.command("docs", deprecated=True) -@click.argument( - "schema_path", - type=click.Path(exists=True), - default="nextflow_schema.json", - required=False, - metavar="", -) -@click.option( - "-o", - "--output", - type=str, - metavar="", - help="Output filename. Defaults to standard out.", -) -@click.option( - "-x", - "--format", - type=click.Choice(["markdown", "html"]), - default="markdown", - help="Format to output docs in.", -) -@click.option("-f", "--force", is_flag=True, default=False, help="Overwrite existing files") -@click.option( - "-c", - "--columns", - type=str, - metavar="", - help="CSV list of columns to include in the parameter tables (parameter,description,type,default,required,hidden)", - default="parameter,description,type,default,required,hidden", -) -def command_schema_docs(schema_path, output, format, force, columns): - """ - Use `nf-core pipelines schema docs` instead. - """ - log.warning( - "The `[magenta]nf-core schema docs[/]` command is deprecated. Use `[magenta]nf-core pipelines schema docs[/]` instead." - ) - pipelines_schema_docs(schema_path, output, format, force, columns) - - -# nf-core create-logo (deprecated) -@nf_core_cli.command("create-logo", deprecated=True, hidden=True) -@click.argument("logo-text", metavar="") -@click.option("-d", "--dir", "directory", type=click.Path(), default=".", help="Directory to save the logo in.") -@click.option( - "-n", - "--name", - type=str, - help="Name of the output file (with or without '.png' suffix).", -) -@click.option( - "--theme", - type=click.Choice(["light", "dark"]), - default="light", - help="Theme for the logo.", - show_default=True, -) -@click.option( - "--width", - type=int, - default=2300, - help="Width of the logo in pixels.", - show_default=True, -) -@click.option( - "--format", - type=click.Choice(["png", "svg"]), - default="png", - help="Image format of the logo, either PNG or SVG.", - show_default=True, -) -@click.option( - "-f", - "--force", - is_flag=True, - default=False, - help="Overwrite any files if they already exist", -) -def command_create_logo(logo_text, directory, name, theme, width, format, force): - """ - Use `nf-core pipelines create-logo` instead. - """ - log.warning( - "The `[magenta]nf-core create-logo[/]` command is deprecated. Use `[magenta]nf-core pipeliness create-logo[/]` instead." - ) - pipelines_create_logo(logo_text, directory, name, theme, width, format, force) - - -# nf-core sync (deprecated) -@nf_core_cli.command("sync", hidden=True, deprecated=True) -@click.pass_context -@click.option( - "-d", - "--dir", - "directory", - type=click.Path(exists=True), - default=".", - help=r"Pipeline directory. [dim]\[default: current working directory][/]", -) -@click.option( - "-b", - "--from-branch", - type=str, - help="The git branch to use to fetch workflow variables.", -) -@click.option( - "-p", - "--pull-request", - is_flag=True, - default=False, - help="Make a GitHub pull-request with the changes.", -) -@click.option( - "--force_pr", - is_flag=True, - default=False, - help="Force the creation of a pull-request, even if there are no changes.", -) -@click.option("-g", "--github-repository", type=str, help="GitHub PR: target repository.") -@click.option("-u", "--username", type=str, help="GitHub PR: auth username.") -@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template") -def command_sync(ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr): - """ - Use `nf-core pipelines sync` instead. - """ - log.warning( - "The `[magenta]nf-core sync[/]` command is deprecated. Use `[magenta]nf-core pipelines sync[/]` instead." - ) - pipelines_sync(ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr) - - -# nf-core bump-version (deprecated) -@nf_core_cli.command("bump-version", hidden=True, deprecated=True) -@click.pass_context -@click.argument("new_version", default="") -@click.option( - "-d", - "--dir", - "directory", - type=click.Path(exists=True), - default=".", - help=r"Pipeline directory. [dim]\[default: current working directory][/]", -) -@click.option( - "-n", - "--nextflow", - is_flag=True, - default=False, - help="Bump required nextflow version instead of pipeline version", -) -def command_bump_version(ctx, new_version, directory, nextflow): - """ - Use `nf-core pipelines bump-version` instead. - """ - log.warning( - "The `[magenta]nf-core bump-version[/]` command is deprecated. Use `[magenta]nf-core pipelines bump-version[/]` instead." - ) - pipelines_bump_version(ctx, new_version, directory, nextflow) - - -# nf-core list (deprecated) -@nf_core_cli.command("list", deprecated=True, hidden=True) -@click.argument("keywords", required=False, nargs=-1, metavar="") -@click.option( - "-s", - "--sort", - type=click.Choice(["release", "pulled", "name", "stars"]), - default="release", - help="How to sort listed pipelines", -) -@click.option("--json", is_flag=True, default=False, help="Print full output as JSON") -@click.option("--show-archived", is_flag=True, default=False, help="Print archived workflows") -@click.pass_context -def command_list(ctx, keywords, sort, json, show_archived): - """ - Use `nf-core pipelines list` instead. - """ - log.warning( - "The `[magenta]nf-core list[/]` command is deprecated. Use `[magenta]nf-core pipelines list[/]` instead." - ) - pipelines_list(ctx, keywords, sort, json, show_archived) - - -# nf-core launch (deprecated) -@nf_core_cli.command("launch", deprecated=True, hidden=True) -@click.argument("pipeline", required=False, metavar="") -@click.option("-r", "--revision", help="Release/branch/SHA of the project to run (if remote)") -@click.option("-i", "--id", help="ID for web-gui launch parameter set") -@click.option( - "-c", - "--command-only", - is_flag=True, - default=False, - help="Create Nextflow command with params (no params file)", -) -@click.option( - "-o", - "--params-out", - type=click.Path(), - default=os.path.join(os.getcwd(), "nf-params.json"), - help="Path to save run parameters file", -) -@click.option( - "-p", - "--params-in", - type=click.Path(exists=True), - help="Set of input run params to use from a previous run", -) -@click.option( - "-a", - "--save-all", - is_flag=True, - default=False, - help="Save all parameters, even if unchanged from default", -) -@click.option( - "-x", - "--show-hidden", - is_flag=True, - default=False, - help="Show hidden params which don't normally need changing", -) -@click.option( - "-u", - "--url", - type=str, - default="https://nf-co.re/launch", - help="Customise the builder URL (for development work)", -) -@click.pass_context -def command_launch( - ctx, - pipeline, - id, - revision, - command_only, - params_in, - params_out, - save_all, - show_hidden, - url, -): - """ - Use `nf-core pipelines launch` instead. - """ - log.warning( - "The `[magenta]nf-core launch[/]` command is deprecated. Use `[magenta]nf-core pipelines launch[/]` instead." - ) - pipelines_launch(ctx, pipeline, id, revision, command_only, params_in, params_out, save_all, show_hidden, url) - - -# nf-core create-params-file (deprecated) -@nf_core_cli.command("create-params-file", deprecated=True, hidden=True) -@click.argument("pipeline", required=False, metavar="") -@click.option("-r", "--revision", help="Release/branch/SHA of the pipeline (if remote)") -@click.option( - "-o", - "--output", - type=str, - default="nf-params.yml", - metavar="", - help="Output filename. Defaults to `nf-params.yml`.", -) -@click.option("-f", "--force", is_flag=True, default=False, help="Overwrite existing files") -@click.option( - "-x", - "--show-hidden", - is_flag=True, - default=False, - help="Show hidden params which don't normally need changing", -) -def command_create_params_file(pipeline, revision, output, force, show_hidden): - """ - Use `nf-core pipelines create-params-file` instead. - """ - log.warning( - "The `[magenta]nf-core create-params-file[/]` command is deprecated. Use `[magenta]nf-core pipelines create-params-file[/]` instead." - ) - pipelines_create_params_file(pipeline, revision, output, force, show_hidden) - - -# nf-core download (deprecated) -@nf_core_cli.command("download", deprecated=True, hidden=True) -@click.argument("pipeline", required=False, metavar="") -@click.option( - "-r", - "--revision", - multiple=True, - help="Pipeline release to download. Multiple invocations are possible, e.g. `-r 1.1 -r 1.2`", -) -@click.option("-o", "--outdir", type=str, help="Output directory") -@click.option( - "-x", - "--compress", - type=click.Choice(["tar.gz", "tar.bz2", "zip", "none"]), - help="Archive compression type", -) -@click.option("-f", "--force", is_flag=True, default=False, help="Overwrite existing files") -@click.option( - "-t", - "--tower", - is_flag=True, - default=False, - hidden=True, - help="Download for Seqera Platform. DEPRECATED: Please use `--platform` instead.", -) -@click.option( - "--platform", - is_flag=True, - default=False, - help="Download for Seqera Platform (formerly Nextflow Tower)", -) -@click.option( - "-d", - "--download-configuration", - is_flag=True, - default=False, - help="Include configuration profiles in download. Not available with `--platform`", -) -@click.option( - "--tag", - multiple=True, - help="Add custom alias tags to `--platform` downloads. For example, `--tag \"3.10=validated\"` adds the custom 'validated' tag to the 3.10 release.", -) -@click.option( - "-s", - "--container-system", - type=click.Choice(["none", "singularity", "docker"]), - help="Download container images of required software.", -) -@click.option( - "-l", - "--container-library", - multiple=True, - help="Container registry/library or mirror to pull images from.", -) -@click.option( - "-u", - "--container-cache-utilisation", - type=click.Choice(["amend", "copy", "remote"]), - help="Utilise a `singularity.cacheDir` in the download process, if applicable.", -) -@click.option( - "-i", - "--container-cache-index", - type=str, - help="List of images already available in a remote `singularity.cacheDir`.", -) -@click.option( - "-p", - "--parallel-downloads", - type=int, - default=4, - help="Number of parallel image downloads", -) -@click.pass_context -def command_download( - ctx, - pipeline, - revision, - outdir, - compress, - force, - tower, - platform, - download_configuration, - tag, - container_system, - container_library, - container_cache_utilisation, - container_cache_index, - parallel_downloads, -): - """ - Use `nf-core pipelines download` instead. - """ - log.warning( - "The `[magenta]nf-core download[/]` command is deprecated. Use `[magenta]nf-core pipelines download[/]` instead." - ) - pipelines_download( - ctx, - pipeline, - revision, - outdir, - compress, - force, - platform or tower, - download_configuration, - tag, - container_system, - container_library, - container_cache_utilisation, - container_cache_index, - parallel_downloads, - ) - - -# nf-core lint (deprecated) -@nf_core_cli.command("lint", hidden=True, deprecated=True) -@click.option( - "-d", - "--dir", - "directory", - type=click.Path(exists=True), - default=".", - help=r"Pipeline directory [dim]\[default: current working directory][/]", -) -@click.option( - "--release", - is_flag=True, - default=Path(os.environ.get("GITHUB_REF", "").strip(" '\"")).parent.name in ["master", "main"] - and os.environ.get("GITHUB_REPOSITORY", "").startswith("nf-core/") - and not os.environ.get("GITHUB_REPOSITORY", "") == "nf-core/tools", - help="Execute additional checks for release-ready workflows.", -) -@click.option( - "-f", - "--fix", - type=str, - metavar="", - multiple=True, - help="Attempt to automatically fix specified lint test", -) -@click.option( - "-k", - "--key", - type=str, - metavar="", - multiple=True, - help="Run only these lint tests", -) -@click.option("-p", "--show-passed", is_flag=True, help="Show passing tests on the command line") -@click.option("-i", "--fail-ignored", is_flag=True, help="Convert ignored tests to failures") -@click.option("-w", "--fail-warned", is_flag=True, help="Convert warn tests to failures") -@click.option( - "--markdown", - type=str, - metavar="", - help="File to write linting results to (Markdown)", -) -@click.option( - "--json", - type=str, - metavar="", - help="File to write linting results to (JSON)", -) -@click.option( - "--sort-by", - type=click.Choice(["module", "test"]), - default="test", - help="Sort lint output by module or test name.", - show_default=True, -) -@click.pass_context -def command_lint( - ctx, - directory, - release, - fix, - key, - show_passed, - fail_ignored, - fail_warned, - markdown, - json, - sort_by, -): - """ - Use `nf-core pipelines lint` instead. - """ - log.warning( - "The `[magenta]nf-core lint[/]` command is deprecated. Use `[magenta]nf-core pipelines lint[/]` instead." - ) - pipelines_lint(ctx, directory, release, fix, key, show_passed, fail_ignored, fail_warned, markdown, json, sort_by) - - -# nf-core create (deprecated) -@nf_core_cli.command("create", hidden=True, deprecated=True) -@click.option( - "-n", - "--name", - type=str, - help="The name of your new pipeline", -) -@click.option("-d", "--description", type=str, help="A short description of your pipeline") -@click.option("-a", "--author", type=str, help="Name of the main author(s)") -@click.option("--version", type=str, default="1.0.0dev", help="The initial version number to use") -@click.option("-f", "--force", is_flag=True, default=False, help="Overwrite output directory if it already exists") -@click.option("-o", "--outdir", help="Output directory for new pipeline (default: pipeline name)") -@click.option("-t", "--template-yaml", help="Pass a YAML file to customize the template") -@click.option("--plain", is_flag=True, help="Use the standard nf-core template") -@click.option( - "--organisation", - type=str, - default="nf-core", - help="The name of the GitHub organisation where the pipeline will be hosted (default: nf-core)", -) -@click.pass_context -def command_create(ctx, name, description, author, version, force, outdir, template_yaml, plain, organisation): - """ - Use `nf-core pipelines create` instead. - """ - log.warning( - "The `[magenta]nf-core create[/]` command is deprecated. Use `[magenta]nf-core pipelines create[/]` instead." - ) - pipelines_create(ctx, name, description, author, version, force, outdir, template_yaml, organisation) - - # Main script is being run - launch the CLI if __name__ == "__main__": run_nf_core() diff --git a/nf_core/commands_modules.py b/nf_core/commands_modules.py index fa3e3c688b..525121726b 100644 --- a/nf_core/commands_modules.py +++ b/nf_core/commands_modules.py @@ -87,6 +87,7 @@ def modules_update( save_diff, update_deps, limit_output, + skip_deps, ): """ Update DSL2 modules within a pipeline. @@ -109,11 +110,12 @@ def modules_update( ctx.obj["modules_repo_branch"], ctx.obj["modules_repo_no_pull"], limit_output, + skip_deps, ) exit_status = module_install.update(tool) if not exit_status and install_all: sys.exit(1) - except (UserWarning, LookupError) as e: + except (UserWarning, LookupError, AssertionError) as e: log.error(e) sys.exit(1) @@ -143,7 +145,7 @@ def modules_patch(ctx, tool, directory, remove): sys.exit(1) -def modules_remove(ctx, directory, tool): +def modules_remove(ctx, directory, tool, force): """ Remove a module from a pipeline. """ @@ -156,25 +158,14 @@ def modules_remove(ctx, directory, tool): ctx.obj["modules_repo_branch"], ctx.obj["modules_repo_no_pull"], ) - module_remove.remove(tool) + module_remove.remove(tool, force=force) except (UserWarning, LookupError) as e: log.critical(e) sys.exit(1) def modules_create( - ctx, - tool, - directory, - author, - label, - meta, - no_meta, - force, - conda_name, - conda_package_version, - empty_template, - migrate_pytest, + ctx, tool, directory, author, label, meta, no_meta, force, conda_name, conda_package_version, empty_template ): """ Create a new DSL2 module from the nf-core template. @@ -208,7 +199,6 @@ def modules_create( conda_name, conda_package_version, empty_template, - migrate_pytest, ) module_create.create() except UserWarning as e: @@ -219,7 +209,7 @@ def modules_create( sys.exit(1) -def modules_test(ctx, tool, directory, no_prompts, update, once, profile, migrate_pytest): +def modules_test(ctx, tool, directory, no_prompts, update, once, profile): """ Run nf-test for a module. @@ -227,21 +217,6 @@ def modules_test(ctx, tool, directory, no_prompts, update, once, profile, migrat """ from nf_core.components.components_test import ComponentsTest - if migrate_pytest: - modules_create( - ctx, - tool, - directory, - author="", - label="", - meta=True, - no_meta=False, - force=False, - conda_name=None, - conda_package_version=None, - empty_template=False, - migrate_pytest=migrate_pytest, - ) try: module_tester = ComponentsTest( component_type="modules", @@ -262,7 +237,7 @@ def modules_test(ctx, tool, directory, no_prompts, update, once, profile, migrat def modules_lint( - ctx, tool, directory, registry, key, all, fail_warned, local, passed, sort_by, fix_version, fix, plain_text + ctx, tool, directory, registry, key, all_modules, fail_warned, local, passed, sort_by, fix_version, fix, plain_text ): """ Lint one or more modules in a directory. @@ -291,7 +266,7 @@ def modules_lint( module=tool, registry=registry, key=key, - all_modules=all, + all_modules=all_modules, print_results=True, local=local, show_passed=passed, @@ -337,7 +312,7 @@ def modules_info(ctx, tool, directory): sys.exit(1) -def modules_bump_versions(ctx, tool, directory, all, show_all, dry_run): +def modules_bump_versions(ctx, tool, directory, all_modules, show_all, dry_run): """ Bump versions for one or more modules in a clone of the nf-core/modules repo. @@ -352,7 +327,7 @@ def modules_bump_versions(ctx, tool, directory, all, show_all, dry_run): ctx.obj["modules_repo_branch"], ctx.obj["modules_repo_no_pull"], ) - version_bumper.bump_versions(module=tool, all_modules=all, show_up_to_date=show_all, dry_run=dry_run) + version_bumper.bump_versions(module=tool, all_modules=all_modules, show_up_to_date=show_all, dry_run=dry_run) except ModuleExceptionError as e: log.error(e) sys.exit(1) diff --git a/nf_core/commands_pipelines.py b/nf_core/commands_pipelines.py index f49d021fee..273097c474 100644 --- a/nf_core/commands_pipelines.py +++ b/nf_core/commands_pipelines.py @@ -1,10 +1,10 @@ import logging -import os import sys from pathlib import Path import rich +import nf_core.utils from nf_core.pipelines.params_file import ParamsFileBuilder from nf_core.utils import rich_force_colors @@ -53,6 +53,12 @@ def pipelines_create(ctx, name, description, author, version, force, outdir, tem ) sys.exit(1) else: + if not nf_core.utils.is_interactive(): + log.error( + "No pipeline arguments provided and session is not interactive (no TTY detected). " + "Please provide at least --name, --description, and --author to run non-interactively." + ) + sys.exit(1) log.info("Launching interactive nf-core pipeline creation tool.") app = PipelineCreateApp() app.run() @@ -206,7 +212,7 @@ def pipelines_download( # nf-core pipelines create-params-file -def pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden): +def pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hidden, no_prompts=False): """ Build a parameter file for a pipeline. @@ -218,7 +224,7 @@ def pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hi Run using a remote pipeline name (such as GitHub `user/repo` or a URL), a local pipeline directory. """ - builder = ParamsFileBuilder(pipeline, revision) + builder = ParamsFileBuilder(pipeline, revision, no_prompts) if not builder.write_params_file(Path(output), show_hidden=show_hidden, force=force): sys.exit(1) @@ -228,7 +234,7 @@ def pipelines_create_params_file(ctx, pipeline, revision, output, force, show_hi def pipelines_launch( ctx, pipeline, - id, + launch_id, revision, command_only, params_in, @@ -236,6 +242,7 @@ def pipelines_launch( save_all, show_hidden, url, + no_prompts=False, ): """ Launch a pipeline using a web GUI or command line prompts. @@ -261,7 +268,8 @@ def pipelines_launch( save_all, show_hidden, url, - id, + launch_id, + no_prompts, ) if not launcher.launch_pipeline(): sys.exit(1) @@ -309,7 +317,16 @@ def pipelines_rocrate( # nf-core pipelines sync def pipelines_sync( - ctx, directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post + ctx, + directory, + from_branch, + pull_request, + github_repository, + username, + template_yaml, + force_pr, + blog_post, + no_prompts=False, ): """ Sync a pipeline [cyan i]TEMPLATE[/] branch with the nf-core template. @@ -331,7 +348,15 @@ def pipelines_sync( is_pipeline_directory(directory) # Sync the given pipeline dir sync_obj = PipelineSync( - directory, from_branch, pull_request, github_repository, username, template_yaml, force_pr, blog_post + directory, + from_branch, + pull_request, + github_repository, + username, + template_yaml, + force_pr, + blog_post, + no_prompts, ) sync_obj.sync() except (SyncExceptionError, PullRequestExceptionError) as e: @@ -340,7 +365,7 @@ def pipelines_sync( # nf-core pipelines create-logo -def pipelines_create_logo(logo_text, directory, name, theme, width, format, force): +def pipelines_create_logo(logo_text, directory, name, theme, width, img_format, force): """ Generate a logo with the nf-core logo template. @@ -351,7 +376,7 @@ def pipelines_create_logo(logo_text, directory, name, theme, width, format, forc try: if directory == ".": directory = Path.cwd() - logo_path = create_logo(logo_text, directory, name, theme, width, format, force) + logo_path = create_logo(logo_text, directory, name, theme, width, img_format, force) # Print path to logo relative to current working directory try: logo_path = Path(logo_path).relative_to(Path.cwd()) @@ -444,11 +469,11 @@ def pipelines_schema_lint(schema_path): # nf-core pipelines schema docs -def pipelines_schema_docs(schema_path, output, format, force, columns): +def pipelines_schema_docs(schema_path, output, output_format, force, columns): """ Outputs parameter documentation for a pipeline schema. """ - if not os.path.exists(schema_path): + if not schema_path.exists(): log.error("Could not find 'nextflow_schema.json' in current directory. Please specify a path.") sys.exit(1) @@ -458,4 +483,4 @@ def pipelines_schema_docs(schema_path, output, format, force, columns): # Assume we're in a pipeline dir root if schema path not set schema_obj.get_schema_path(schema_path) schema_obj.load_schema() - schema_obj.print_documentation(output, format, force, columns.split(",")) + schema_obj.print_documentation(output, output_format, force, columns.split(",")) diff --git a/nf_core/commands_subworkflows.py b/nf_core/commands_subworkflows.py index 5e1b1e8be0..4ac3c321a9 100644 --- a/nf_core/commands_subworkflows.py +++ b/nf_core/commands_subworkflows.py @@ -10,7 +10,7 @@ stdout = rich.console.Console(force_terminal=rich_force_colors()) -def subworkflows_create(ctx, subworkflow, directory, author, force, migrate_pytest): +def subworkflows_create(ctx, subworkflow, directory, author, force): """ Create a new subworkflow from the nf-core template. @@ -24,7 +24,7 @@ def subworkflows_create(ctx, subworkflow, directory, author, force, migrate_pyte # Run function try: - subworkflow_create = SubworkflowCreate(directory, subworkflow, author, force, migrate_pytest) + subworkflow_create = SubworkflowCreate(directory, subworkflow, author, force) subworkflow_create.create() except UserWarning as e: log.critical(e) @@ -34,7 +34,7 @@ def subworkflows_create(ctx, subworkflow, directory, author, force, migrate_pyte sys.exit(1) -def subworkflows_test(ctx, subworkflow, directory, no_prompts, update, once, profile, migrate_pytest): +def subworkflows_test(ctx, subworkflow, directory, no_prompts, update, once, profile): """ Run nf-test for a subworkflow. @@ -42,8 +42,6 @@ def subworkflows_test(ctx, subworkflow, directory, no_prompts, update, once, pro """ from nf_core.components.components_test import ComponentsTest - if migrate_pytest: - subworkflows_create(ctx, subworkflow, directory, None, False, True) try: sw_tester = ComponentsTest( component_type="subworkflows", @@ -105,7 +103,7 @@ def subworkflows_list_local(ctx, keywords, json, directory): # pylint: disable= def subworkflows_lint( - ctx, subworkflow, directory, registry, key, all, fail_warned, local, passed, sort_by, fix, plain_text + ctx, subworkflow, directory, registry, key, all_subworkflows, fail_warned, local, passed, sort_by, fix, plain_text ): """ Lint one or more subworkflows in a directory. @@ -134,7 +132,7 @@ def subworkflows_lint( subworkflow=subworkflow, registry=registry, key=key, - all_subworkflows=all, + all_subworkflows=all_subworkflows, print_results=True, local=local, show_passed=passed, @@ -179,7 +177,7 @@ def subworkflows_info(ctx, subworkflow, directory): sys.exit(1) -def subworkflows_install(ctx, subworkflow, directory, prompt, force, sha): +def subworkflows_install(ctx, subworkflow, directory, prompt, force, sha, skip_deps=False): """ Install DSL2 subworkflow within a pipeline. @@ -196,6 +194,7 @@ def subworkflows_install(ctx, subworkflow, directory, prompt, force, sha): ctx.obj["modules_repo_url"], ctx.obj["modules_repo_branch"], ctx.obj["modules_repo_no_pull"], + skip_deps=skip_deps, ) exit_status = subworkflow_install.install(subworkflow) if not exit_status: @@ -205,7 +204,7 @@ def subworkflows_install(ctx, subworkflow, directory, prompt, force, sha): sys.exit(1) -def subworkflows_remove(ctx, directory, subworkflow): +def subworkflows_remove(ctx, directory, subworkflow, force): """ Remove a subworkflow from a pipeline. """ @@ -218,7 +217,7 @@ def subworkflows_remove(ctx, directory, subworkflow): ctx.obj["modules_repo_branch"], ctx.obj["modules_repo_no_pull"], ) - module_remove.remove(subworkflow) + module_remove.remove(subworkflow, force=force) except (UserWarning, LookupError) as e: log.critical(e) sys.exit(1) @@ -236,6 +235,7 @@ def subworkflows_update( save_diff, update_deps, limit_output, + skip_deps, ): """ Update DSL2 subworkflow within a pipeline. @@ -258,6 +258,7 @@ def subworkflows_update( ctx.obj["modules_repo_branch"], ctx.obj["modules_repo_no_pull"], limit_output, + skip_deps, ) exit_status = subworkflow_install.update(subworkflow) if not exit_status and install_all: diff --git a/nf_core/components/components_command.py b/nf_core/components/components_command.py index 375a711c99..2e01c245d3 100644 --- a/nf_core/components/components_command.py +++ b/nf_core/components/components_command.py @@ -35,11 +35,16 @@ def __init__( self.directory: Path = Path(directory) self.modules_repo = ModulesRepo(remote_url, branch, no_pull, hide_progress) self.hide_progress: bool = hide_progress - self.no_prompts: bool = no_prompts + self.no_prompts: bool = no_prompts or not nf_core.utils.is_interactive() self.repo_type: str | None = None self.org: str = "" self._configure_repo_and_paths() + def require_prompts(self, msg: str) -> None: + """Raise UserWarning if prompts are disabled (via --no-prompts or non-interactive session).""" + if self.no_prompts: + raise UserWarning(f"{msg} and prompts are disabled.") + def _configure_repo_and_paths(self, nf_dir_req: bool = True) -> None: """ Determine the repo type and set some default paths. @@ -50,7 +55,7 @@ def _configure_repo_and_paths(self, nf_dir_req: bool = True) -> None: """ try: if self.directory: - if self.directory == Path(".") and not nf_dir_req: + if self.directory == Path() and not nf_dir_req: self.no_prompts = True self.directory, self.repo_type, self.org = get_repo_info(self.directory, use_prompt=not self.no_prompts) except UserWarning: @@ -69,6 +74,7 @@ def get_local_components(self) -> list[str]: Get the local modules/subworkflows in a pipeline """ local_component_dir = Path(self.directory, self.component_type, "local") + # TODO: Return list of Path objects instead of strings to avoid unnecessary conversion return [ str(Path(directory).relative_to(local_component_dir)) for directory, _, files in os.walk(local_component_dir) @@ -85,6 +91,7 @@ def get_components_clone_modules(self) -> list[str]: component_base_path = Path(self.directory, self.default_modules_path) elif self.component_type == "subworkflows": component_base_path = Path(self.directory, self.default_subworkflows_path) + # TODO: Return list of Path objects instead of strings to avoid unnecessary conversion return [ str(Path(directory).relative_to(component_base_path)) for directory, _, files in os.walk(component_base_path) @@ -155,6 +162,7 @@ def components_from_repo(self, install_dir: str) -> list[str]: if not repo_dir.exists(): raise LookupError(f"Nothing installed from {install_dir} in pipeline") + # TODO: Return list of Path objects instead of strings to avoid unnecessary conversion return [ str(Path(dir_path).relative_to(repo_dir)) for dir_path, _, files in os.walk(repo_dir) if "main.nf" in files ] @@ -257,6 +265,7 @@ def check_patch_paths(self, patch_path: Path, module_name: str) -> None: and self.modules_repo.repo_path is not None and modules_json.modules_json is not None ): + # TODO: Consider if this needs to be a string or if Path object would work modules_json.modules_json["repos"][self.modules_repo.remote_url]["modules"][ self.modules_repo.repo_path ][module_name]["patch"] = str(patch_path.relative_to(self.directory.resolve())) @@ -273,20 +282,17 @@ def check_if_in_include_stmts(self, component_path: str) -> dict[str, list[dict[ """ include_stmts: dict[str, list[dict[str, int | str]]] = {} if self.repo_type == "pipeline": - workflow_files = Path(self.directory, "workflows").glob("*.nf") + workflow_files = Path(self.directory, "workflows").rglob("*.nf") for workflow_file in workflow_files: - with open(workflow_file) as fh: + with open(workflow_file) as fh, mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ) as s: # Check if component path is in the file using mmap - with mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ) as s: - if s.find(component_path.encode()) != -1: - # If the component path is in the file, check for include statements - for i, line in enumerate(fh): - if line.startswith("include") and component_path in line: - if str(workflow_file) not in include_stmts: - include_stmts[str(workflow_file)] = [] - include_stmts[str(workflow_file)].append( - {"line_number": i + 1, "line": line.rstrip()} - ) + if s.find(component_path.encode()) != -1: + # If the component path is in the file, check for include statements + for i, line in enumerate(fh): + if line.startswith("include") and component_path in line: + if str(workflow_file) not in include_stmts: + include_stmts[str(workflow_file)] = [] + include_stmts[str(workflow_file)].append({"line_number": i + 1, "line": line.rstrip()}) return include_stmts else: diff --git a/nf_core/components/components_completion.py b/nf_core/components/components_completion.py index 191fa32e21..8abecb4649 100644 --- a/nf_core/components/components_completion.py +++ b/nf_core/components/components_completion.py @@ -1,5 +1,6 @@ import sys +import git from click.shell_completion import CompletionItem from nf_core.modules.list import ModuleList @@ -24,7 +25,7 @@ def autocomplete_components(ctx, param, incomplete: str, component_type: str, li available_components = components_list.modules_repo.get_avail_components(component_type) return [CompletionItem(comp) for comp in available_components if comp.startswith(incomplete)] - except Exception as e: + except (git.exc.GitError, LookupError, OSError) as e: print(f"[ERROR] Autocomplete failed: {e}", file=sys.stderr) return [] diff --git a/nf_core/components/components_differ.py b/nf_core/components/components_differ.py index c92480a50d..b028b55626 100644 --- a/nf_core/components/components_differ.py +++ b/nf_core/components/components_differ.py @@ -187,7 +187,7 @@ def write_diff_file( elif diff_status == ComponentsDiffer.DiffEnum.REMOVED: # The file was removed between the commits fh.write(f"'{Path(dsp_from_dir, file)}' was removed\n") - elif limit_output and not file.suffix == ".nf": + elif limit_output and file.suffix != ".nf": # Skip printing the diff for files other than main.nf fh.write(f"Changes in '{Path(component, file)}' but not shown\n") else: @@ -286,7 +286,7 @@ def print_diff( elif diff_status == ComponentsDiffer.DiffEnum.REMOVED: # The file was removed between the commits log.info(f"'{Path(dsp_from_dir, file)}' was removed") - elif limit_output and not file.suffix == ".nf": + elif limit_output and file.suffix != ".nf": # Skip printing the diff for files other than main.nf log.info(f"Changes in '{Path(component, file)}' but not shown") else: diff --git a/nf_core/components/components_test.py b/nf_core/components/components_test.py index 46fcf2960f..587c724384 100644 --- a/nf_core/components/components_test.py +++ b/nf_core/components/components_test.py @@ -8,7 +8,7 @@ from pathlib import Path import questionary -from rich import print +from rich import print # noqa: A004 from rich.panel import Panel from rich.prompt import Confirm from rich.syntax import Syntax @@ -122,6 +122,7 @@ def check_inputs(self) -> None: except LookupError: raise + assert self.component_name is not None # Set above by user input, prompt, or guard self.component_dir = Path(self.component_type, self.modules_repo.repo_path, *self.component_name.split("/")) # First, sanity check that the module directory exists @@ -156,6 +157,9 @@ def display_nftest_output(self, nftest_out: bytes, nftest_err: bytes) -> None: log.debug("nf-test output:\n%s", nftest_out.decode()) if nftest_err: log.debug("nf-test error:\n%s", nftest_err.decode()) + if "Different Snapshot:" in nftest_err.decode() and self.update: + log.info("Updating snapshot") + self.generate_snapshot() else: # Interactive mode: use Rich formatting print("Displaying nf-test output") @@ -170,11 +174,7 @@ def display_nftest_output(self, nftest_out: bytes, nftest_err: bytes) -> None: print("Displaying nf-test error") if "Different Snapshot:" in nftest_err.decode(): log.error("nf-test failed due to differences in the snapshots") - # prompt to update snapshot - if self.no_prompts: - log.info("Updating snapshot") - self.update = True - elif self.update is None: + if self.update is None: answer = Confirm.ask( "[bold][blue]?[/] nf-test found differences in the snapshot. Do you want to update it?", default=True, @@ -201,7 +201,8 @@ def generate_snapshot(self) -> bool: # set verbose flag if self.verbose is True verbose = "--verbose --debug" if self.verbose else "" - update = "--update-snapshot" if self.update else "" + update_snapshot = self.update + update = "--update-snapshot" if update_snapshot else "" self.update = False # reset self.update to False to test if the new snapshot is stable tag = f"subworkflows/{self.component_name}" if self.component_type == "subworkflows" else self.component_name profile = self.profile if self.profile else os.environ["PROFILE"] @@ -221,6 +222,9 @@ def generate_snapshot(self) -> bool: self.obsolete_snapshots = True # check if nf-test was successful if "Assertion failed:" in nftest_out.decode(): + if "Different Snapshot:" in nftest_err.decode(): + return update_snapshot # snapshot was updated return False only if we don't want to update the snapshot + self.errors.append("Assertion failed.") return False elif "No tests to execute." in nftest_out.decode(): log.error("Nothing to execute. Is the file 'main.nf.test' missing?") diff --git a/nf_core/components/components_utils.py b/nf_core/components/components_utils.py index 989c1be8d9..db33ad8b80 100644 --- a/nf_core/components/components_utils.py +++ b/nf_core/components/components_utils.py @@ -70,7 +70,7 @@ def get_repo_info(directory: Path, use_prompt: bool | None = True) -> tuple[Path # Check for org if modules repo if repo_type == "modules": org = getattr(tools_config, "org_path", "") or "" - if org == "": + if org == "" and use_prompt: log.warning("Organisation path not defined in %s [key: org_path]", config_fn.name) org = questionary.text( "What is the organisation path under which modules and subworkflows are stored?", @@ -107,6 +107,8 @@ def prompt_component_version_sha( Returns: git_sha (str): The selected version of the module/subworkflow """ + if not nf_core.utils.is_interactive(): + raise UserWarning("Cannot interactively select a version and session is not interactive (no TTY detected).") older_commits_choice = questionary.Choice( title=[("fg:ansiyellow", "older commits"), ("class:choice-default", "")], value="" ) @@ -252,22 +254,22 @@ def get_channel_info_from_biotools( inputs = {} outputs = {} - def _iterate_input_output(type) -> DictWithStrAndTuple: + def _iterate_input_output(funct_data, io_type) -> DictWithStrAndTuple: type_info = {} - if type in funct: - for element in funct[type]: + if io_type in funct_data: + for element in funct_data[io_type]: if "data" in element: element_name = "_".join(element["data"]["term"].lower().split(" ")) uris = [element["data"]["uri"]] terms = [element["data"]["term"]] patterns = [] if "format" in element: - for format in element["format"]: + for fmt in element["format"]: # Append the EDAM URI - uris.append(format["uri"]) + uris.append(fmt["uri"]) # Append the EDAM term, getting the first word in case of complicated strings. i.e. "FASTA format" - patterns.append(format["term"].lower().split(" ")[0]) - terms.append(format["term"]) + patterns.append(fmt["term"].lower().split(" ")[0]) + terms.append(fmt["term"]) type_info[element_name] = (uris, terms, patterns) return type_info @@ -277,8 +279,8 @@ def _iterate_input_output(type) -> DictWithStrAndTuple: if "function" in tool: # Parse all tool functions for funct in tool["function"]: - inputs.update(_iterate_input_output("input")) - outputs.update(_iterate_input_output("output")) + inputs.update(_iterate_input_output(funct, "input")) + outputs.update(_iterate_input_output(funct, "output")) return inputs, outputs # If the tool name was not found in the response diff --git a/nf_core/components/create.py b/nf_core/components/create.py index 9fc973f77d..270cf1355a 100644 --- a/nf_core/components/create.py +++ b/nf_core/components/create.py @@ -2,17 +2,14 @@ The ComponentCreate class handles generating of module and subworkflow templates """ -import glob import json import logging import re -import shutil import subprocess from pathlib import Path import jinja2 import questionary -import rich import rich.prompt import ruamel.yaml from packaging.version import parse as parse_version @@ -38,7 +35,7 @@ class ComponentCreate(ComponentCommand): def __init__( self, component_type: str, - directory: Path = Path("."), + directory: Path = Path(), component: str = "", author: str | None = None, process_label: str | None = None, @@ -47,7 +44,6 @@ def __init__( conda_name: str | None = None, conda_version: str | None = None, empty_template: bool = False, - migrate_pytest: bool = False, # TODO: Deprecate this flag in the future ): super().__init__(component_type, directory) self.directory = directory @@ -68,7 +64,6 @@ def __init__( self.docker_container = None self.file_paths: dict[str, Path] = {} self.not_empty_template = not empty_template - self.migrate_pytest = migrate_pytest self.tool_identifier = "" def create(self) -> bool: @@ -146,33 +141,24 @@ def create(self) -> bool: # Check existence of directories early for fast-fail self.file_paths = self._get_component_dirs() - if self.migrate_pytest: - # Rename the component directory to old - component_old_dir = Path(str(self.component_dir) + "_old") - component_parent_path = Path(self.directory, self.component_type, self.org) - component_old_path = component_parent_path / component_old_dir - component_path = component_parent_path / self.component_dir - - component_path.rename(component_old_path) - else: - if self.component_type == "modules": - # Try to find a bioconda package for 'component' - self._get_bioconda_tool() - name = self.tool_conda_name if self.tool_conda_name else self.component - # Try to find a biotools entry for 'component' - biotools_data = get_biotools_response(name) - if biotools_data: - self.tool_identifier = get_biotools_id(biotools_data, name) - # Obtain EDAM ontologies for inputs and outputs - channel_info = get_channel_info_from_biotools(biotools_data, name) - if channel_info: - self.inputs, self.outputs = channel_info - - # Prompt for GitHub username - self._get_username() + if self.component_type == "modules": + # Try to find a bioconda package for 'component' + self._get_bioconda_tool() + name = self.tool_conda_name if self.tool_conda_name else self.component + # Try to find a biotools entry for 'component' + biotools_data = get_biotools_response(name) + if biotools_data: + self.tool_identifier = get_biotools_id(biotools_data, name) + # Obtain EDAM ontologies for inputs and outputs + channel_info = get_channel_info_from_biotools(biotools_data, name) + if channel_info: + self.inputs, self.outputs = channel_info + + # Prompt for GitHub username + self._get_username() - if self.component_type == "modules": - self._get_module_structure_components() + if self.component_type == "modules": + self._get_module_structure_components() # Add a valid organization name for nf-test tags not_alphabet = re.compile(r"[^a-zA-Z]") @@ -186,12 +172,6 @@ def create(self) -> bool: # Generate meta.yml inputs and outputs self.generate_meta_yml_file() - if self.migrate_pytest: - self._copy_old_files(component_old_path) - log.info("Migrate pytest tests: Copied original module files to new module") - shutil.rmtree(component_old_path) - self._print_and_delete_pytest_files() - new_files = [str(path) for path in self.file_paths.values()] run_prettier_on_file(new_files) @@ -213,7 +193,7 @@ def _get_bioconda_tool(self): if not self.tool_conda_version: version = anaconda_response.get("latest_version") if not version: - version = str(max([parse_version(v) for v in anaconda_response["versions"]])) + version = str(max(parse_version(v) for v in anaconda_response["versions"])) else: version = self.tool_conda_version @@ -231,6 +211,12 @@ def _get_bioconda_tool(self): log.warning( f"Could not find Conda dependency using the Anaconda API: '{self.tool_conda_name if self.tool_conda_name else self.component}'" ) + if self.no_prompts: + log.warning( + f"{e}\nBioconda package not found and prompts are disabled. " + "Building module without tool software and meta." + ) + break if rich.prompt.Confirm.ask("[violet]Do you want to enter a different Bioconda package name?"): self.tool_conda_name = rich.prompt.Prompt.ask("[violet]Name of Bioconda package").strip() continue @@ -263,6 +249,7 @@ def _get_module_structure_components(self): "process_medium", "process_high", "process_long", + "process_low_memory", "process_high_memory", ] if self.process_label is None: @@ -272,6 +259,7 @@ def _get_module_structure_components(self): "For example: {}".format(", ".join(process_label_defaults)) ) while self.process_label is None: + self.require_prompts("Process label not provided.\nPlease provide the `--process-label` option") self.process_label = questionary.autocomplete( "Process resource label:", choices=process_label_defaults, @@ -287,6 +275,9 @@ def _get_module_structure_components(self): "[link=https://github.com/nf-core/modules/blob/master/modules/nf-core/bwa/index/main.nf]indexing reference genome files[/link]." ) while self.has_meta is None: + self.require_prompts( + "Meta map requirement not specified.\nPlease provide the `--has-meta` or `--no-meta` option" + ) self.has_meta = rich.prompt.Confirm.ask( "[violet]Will the module require a meta map of sample information?", default=True, @@ -346,7 +337,7 @@ def _collect_name_prompt(self): elif self.component_type == "subworkflows": log.warning("Subworkflow name must be lower-case letters only, with no punctuation") name_clean = re.sub(r"[^a-z\d/]", "", self.component.lower()) - if rich.prompt.Confirm.ask(f"[violet]Change '{self.component}' to '{name_clean}'?"): + if self.no_prompts or rich.prompt.Confirm.ask(f"[violet]Change '{self.component}' to '{name_clean}'?"): self.component = name_clean else: self.component = "" @@ -363,6 +354,10 @@ def _collect_name_prompt(self): # Prompt for new entry if we reset if self.component == "": + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument" + ) if self.component_type == "modules": self.component = rich.prompt.Prompt.ask("[violet]Name of tool/subtool").strip() elif self.component_type == "subworkflows": @@ -383,7 +378,7 @@ def _get_component_dirs(self) -> dict[str, Path]: raise ValueError("`repo_type` not set correctly") # Check if module/subworkflow directories exist already - if component_dir.exists() and not self.force_overwrite and not self.migrate_pytest: + if component_dir.exists() and not self.force_overwrite: raise UserWarning( f"{self.component_type[:-1]} directory exists: '{component_dir}'. Use '--force' to overwrite" ) @@ -397,14 +392,14 @@ def _get_component_dirs(self) -> dict[str, Path]: self.component, "main.nf", ) - if self.subtool and parent_tool_main_nf.exists() and not self.migrate_pytest: + if self.subtool and parent_tool_main_nf.exists(): raise UserWarning( f"Module '{parent_tool_main_nf}' exists already, cannot make subtool '{self.component_name}'" ) # If no subtool, check that there isn't already a tool/subtool - tool_glob = glob.glob(f"{Path(self.directory, self.component_type, self.org, self.component)}/*/main.nf") - if not self.subtool and tool_glob and not self.migrate_pytest: + tool_glob = list(Path(self.directory, self.component_type, self.org, self.component).glob("*/main.nf")) + if not self.subtool and tool_glob: raise UserWarning( f"Module subtool '{tool_glob[0]}' exists already, cannot make tool '{self.component_name}'" ) @@ -427,7 +422,7 @@ def _get_username(self): try: gh_auth_user = json.loads(subprocess.check_output(["gh", "api", "/user"], stderr=subprocess.DEVNULL)) author_default = f"@{gh_auth_user['login']}" - except Exception as e: + except (subprocess.CalledProcessError, FileNotFoundError) as e: log.debug(f"Could not find GitHub username using 'gh' cli command: [red]{e}") # Regex to valid GitHub username: https://github.com/shinnn/github-username-regex @@ -435,91 +430,12 @@ def _get_username(self): while self.author is None or not github_username_regex.match(self.author): if self.author is not None and not github_username_regex.match(self.author): log.warning("Does not look like a valid GitHub username (must start with an '@')!") + self.require_prompts("GitHub username not provided.\nPlease provide the `--author` option") self.author = rich.prompt.Prompt.ask( f"[violet]GitHub Username:[/]{' (@author)' if author_default is None else ''}", default=author_default, ) - def _copy_old_files(self, component_old_path): - """Copy files from old module to new module""" - log.debug("Copying original main.nf file") - shutil.copyfile(component_old_path / "main.nf", self.file_paths["main.nf"]) - log.debug("Copying original meta.yml file") - shutil.copyfile(component_old_path / "meta.yml", self.file_paths["meta.yml"]) - if self.component_type == "modules": - log.debug("Copying original environment.yml file") - shutil.copyfile( - component_old_path / "environment.yml", - self.file_paths["environment.yml"], - ) - if (component_old_path / "templates").is_dir(): - log.debug("Copying original templates directory") - shutil.copytree( - component_old_path / "templates", - self.file_paths["environment.yml"].parent / "templates", - ) - # Create a nextflow.config file if it contains information other than publishDir - pytest_dir = Path(self.directory, "tests", self.component_type, self.org, self.component_dir) - nextflow_config = pytest_dir / "nextflow.config" - if nextflow_config.is_file(): - with open(nextflow_config) as fh: - config_lines = "" - for line in fh: - if "publishDir" not in line and line.strip() != "": - config_lines += line - # if the nextflow.config file only contained publishDir, non_publish_dir_lines will be 11 characters long (`process {\n}`) - if len(config_lines) > 11: - log.debug("Copying nextflow.config file from pytest tests") - with open( - Path( - self.directory, - self.component_type, - self.org, - self.component_dir, - "tests", - "nextflow.config", - ), - "w+", - ) as ofh: - ofh.write(config_lines) - - def _print_and_delete_pytest_files(self): - """Prompt if pytest files should be deleted and printed to stdout""" - pytest_dir = Path(self.directory, "tests", self.component_type, self.org, self.component_dir) - if rich.prompt.Confirm.ask( - "[violet]Do you want to delete the pytest files?[/]\nPytest file 'main.nf' will be printed to standard output to allow migrating the tests manually to 'main.nf.test'.", - default=False, - ): - with open(pytest_dir / "main.nf") as fh: - log.info(fh.read()) - if pytest_dir.is_symlink(): - resolved_dir = pytest_dir.resolve() - log.debug(f"Removing symlink: {resolved_dir}") - shutil.rmtree(resolved_dir) - pytest_dir.unlink() - else: - shutil.rmtree(pytest_dir) - log.info( - "[yellow]Please convert the pytest tests to nf-test in 'main.nf.test'.[/]\n" - "You can find more information about nf-test [link=https://nf-co.re/docs/contributing/modules#migrating-from-pytest-to-nf-test]at the nf-core web[/link]. " - ) - else: - log.info( - "[yellow]Please migrate the pytest tests to nf-test in 'main.nf.test'.[/]\n" - "You can find more information about nf-test [link=https://nf-co.re/docs/contributing/modules#migrating-from-pytest-to-nf-test]at the nf-core web[/link].\n" - f"Once done, make sure to delete the module pytest files to avoid linting errors: {pytest_dir}" - ) - # Delete tags from pytest_modules.yml - modules_yml = Path(self.directory, "tests", "config", "pytest_modules.yml") - with open(modules_yml) as fh: - yml_file = yaml.load(fh) - yml_key = str(self.component_dir) if self.component_type == "modules" else f"subworkflows/{self.component_dir}" - if yml_key in yml_file: - del yml_file[yml_key] - with open(modules_yml, "w") as fh: - yaml.dump(yml_file, fh) - run_prettier_on_file(modules_yml) - def generate_meta_yml_file(self) -> None: """ Generate the meta.yml file. @@ -626,7 +542,7 @@ def generate_meta_yml_file(self) -> None: if hasattr(self, "outputs") and len(self.outputs) > 0: outputs_dict: dict[str, list | dict] = {} - for i, (output_name, ontologies) in enumerate(self.outputs.items()): + for _i, (output_name, ontologies) in enumerate(self.outputs.items()): channel_contents: list[list[dict] | dict] = [] if self.has_meta: channel_contents.append( diff --git a/nf_core/components/info.py b/nf_core/components/info.py index 69a30929e0..cc30f8dec9 100644 --- a/nf_core/components/info.py +++ b/nf_core/components/info.py @@ -1,5 +1,4 @@ import logging -import os from pathlib import Path import questionary @@ -97,6 +96,10 @@ def init_mod_name(self, component: str | None) -> str: module: str: Module name to check """ if component is None: + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument" + ) self.local = questionary.confirm( f"Is the {self.component_type[:-1]} locally installed?", style=nf_core.utils.nfcore_question_style ).unsafe_ask() @@ -200,9 +203,9 @@ def get_local_yaml(self) -> dict | None: log.debug(f"{self.component_type[:-1].title()} '{self.component}' meta.yml not found locally") else: component_base_path = Path(self.directory, self.component_type, self.org) - if self.component in os.listdir(component_base_path): - comp_dir = Path(component_base_path, self.component) - meta_fn = Path(comp_dir, "meta.yml") + comp_dir = component_base_path / self.component + if comp_dir.is_dir(): + meta_fn = comp_dir / "meta.yml" if meta_fn.exists(): log.debug(f"Found local file: {meta_fn}") with open(meta_fn) as fh: @@ -228,10 +231,10 @@ def get_remote_yaml(self) -> dict | None: self.remote_location = self.modules_repo.remote_url return yaml.safe_load(file_contents) - def generate_params_table(self, type) -> Table: + def generate_params_table(self, io_type) -> Table: "Generate a rich table for inputs and outputs" table = Table(expand=True, show_lines=True, box=box.MINIMAL_HEAVY_HEAD, padding=0) - table.add_column(f":inbox_tray: {type}") + table.add_column(f":inbox_tray: {io_type}") table.add_column("Description") if self.component_type == "modules": table.add_column("Pattern", justify="right", style="green") @@ -296,10 +299,10 @@ def generate_component_info_help(self): # Inputs if self.meta.get("input"): inputs_table = self.generate_params_table("Inputs") - for i, input in enumerate(self.meta["input"]): + for i, input_channel in enumerate(self.meta["input"]): inputs_table.add_row(f"[italic]input[{i}][/]", "", "") if self.component_type == "modules": - for element in input: + for element in input_channel: for key, info in element.items(): inputs_table.add_row( f"[orange1 on black] {key} [/][dim i] ({info['type']})", @@ -307,7 +310,7 @@ def generate_component_info_help(self): info.get("pattern", ""), ) elif self.component_type == "subworkflows": - for key, info in input.items(): + for key, info in input_channel.items(): inputs_table.add_row( f"[orange1 on black] {key} [/][dim i]", Markdown(info["description"] if info["description"] else ""), @@ -375,7 +378,7 @@ def generate_component_info_help(self): ) if self.component_type == "subworkflows": subworkflow_config = Path(install_folder, self.component, "nextflow.config").relative_to(self.directory) - if os.path.isfile(subworkflow_config): + if subworkflow_config.exists(): renderables.append( Text.from_markup("\n [blue]Add the following config statement to use this subworkflow:") ) diff --git a/nf_core/components/install.py b/nf_core/components/install.py index 4128301811..f71cf4feef 100644 --- a/nf_core/components/install.py +++ b/nf_core/components/install.py @@ -1,15 +1,13 @@ import logging -import os from pathlib import Path import questionary -from rich import print +from rich import print # noqa: A004 from rich.console import Console from rich.markdown import Markdown from rich.panel import Panel from rich.syntax import Syntax -import nf_core.components import nf_core.modules.modules_utils import nf_core.utils from nf_core.components.components_command import ComponentCommand @@ -22,6 +20,7 @@ ) from nf_core.modules.modules_json import ModulesJson from nf_core.modules.modules_repo import ModulesRepo +from nf_core.pipelines.containers_utils import try_generate_container_configs log = logging.getLogger(__name__) @@ -38,6 +37,7 @@ def __init__( branch: str | None = None, no_pull: bool = False, installed_by: list[str] | None = None, + skip_deps: bool = False, ): super().__init__(component_type, pipeline_dir, remote_url, branch, no_pull) self.current_remote = ModulesRepo(remote_url, branch) @@ -46,6 +46,7 @@ def __init__( self.prompt = prompt self.sha = sha self.current_sha = sha + self.skip_deps = skip_deps if installed_by is not None: self.installed_by = installed_by else: @@ -160,15 +161,29 @@ def install(self, component: str | dict[str, str], silent: bool = False) -> bool if not self.install_component_files(component, version, self.modules_repo, install_folder): return False - # Update module.json with newly installed subworkflow + # Update module.json with newly installed component modules_json.load() modules_json.update( self.component_type, self.modules_repo, component, version, self.installed_by, install_track ) if self.component_type == "subworkflows": - # Install included modules and subworkflows - self.install_included_components(component_dir) + # Under --skip-deps, don't propagate --force to transitive deps so + # already-installed ones keep their pinned SHAs and just have + # installed_by tracking refreshed. + if self.skip_deps: + original_force = self.force + self.force = False + try: + self.install_included_components(component_dir) + finally: + self.force = original_force + else: + self.install_included_components(component_dir) + + # Regenerate container configuration files for the pipeline when modules are installed + if self.component_type == "modules": + try_generate_container_configs(self.directory, component_dir, component) if not silent: modules_json.load() @@ -223,6 +238,11 @@ def collect_and_verify_name( Check that the supplied name is an available module/subworkflow. """ if component is None: + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument:\n" + f" `nf-core {self.component_type} install `" + ) component = questionary.autocomplete( f"{'Tool' if self.component_type == 'modules' else 'Subworkflow'} name:", choices=sorted(modules_repo.get_avail_components(self.component_type, commit=self.current_sha)), @@ -251,10 +271,11 @@ def collect_and_verify_name( raise ValueError - if self.current_remote.remote_url == modules_repo.remote_url: - if not modules_repo.component_exists(component, self.component_type, commit=self.current_sha): - warn_msg = f"{self.component_type[:-1].title()} '{component}' not found in remote '{modules_repo.remote_url}' ({modules_repo.branch})" - log.warning(warn_msg) + if self.current_remote.remote_url == modules_repo.remote_url and not modules_repo.component_exists( + component, self.component_type, commit=self.current_sha + ): + warn_msg = f"{self.component_type[:-1].title()} '{component}' not found in remote '{modules_repo.remote_url}' ({modules_repo.branch})" + log.warning(warn_msg) return component @@ -266,7 +287,7 @@ def check_component_installed(self, component, current_version, component_dir, m True: if the component is not installed False: if the component is installed """ - if (current_version is not None and os.path.exists(component_dir)) and not force: + if (current_version is not None and component_dir.exists()) and not force: # make sure included components are also installed if self.component_type == "subworkflows": self.install_included_components(component_dir) @@ -274,6 +295,10 @@ def check_component_installed(self, component, current_version, component_dir, m log.info(f"{self.component_type[:-1].title()} '{component}' is already installed.") if prompt: + self.require_prompts( + f"{self.component_type[:-1].title()} '{component}' is already installed.\n" + "Use '--force' to force reinstallation" + ) message = ( "?" if self.component_type == "modules" else " of this subworkflow and all it's imported modules?" ) @@ -304,16 +329,17 @@ def get_version(self, component, sha, prompt, current_version, modules_repo): if sha: version = sha elif prompt: - try: - version = prompt_component_version_sha( - component, - self.component_type, - installed_sha=current_version, - modules_repo=modules_repo, - ) - except SystemError as e: - log.error(e) - return False + self.require_prompts( + f"Cannot interactively select a version for '{component}'.\n" + "Please specify the version using the '--sha' option:\n" + f" nf-core {self.component_type} install --sha {component}" + ) + version = prompt_component_version_sha( + component, + self.component_type, + installed_sha=current_version, + modules_repo=modules_repo, + ) else: # Fetch the latest commit for the module version = modules_repo.get_latest_component_version(component, self.component_type) @@ -346,9 +372,9 @@ def check_alternate_remotes(self, modules_json): False: if problematic components are not found """ modules_json.load() - for repo_url, repo_content in modules_json.modules_json.get("repos", dict()).items(): + for repo_url, repo_content in modules_json.modules_json.get("repos", {}).items(): for component_type in repo_content: - for directory in repo_content.get(component_type, dict()).keys(): + for directory in repo_content.get(component_type, {}): if directory == self.modules_repo.repo_path and repo_url != self.modules_repo.remote_url: return True return False diff --git a/nf_core/components/lint/__init__.py b/nf_core/components/lint/__init__.py index 63c651a401..efcdc11d90 100644 --- a/nf_core/components/lint/__init__.py +++ b/nf_core/components/lint/__init__.py @@ -259,7 +259,7 @@ def _print_results(self, show_passed=False, sort_by="test", plain_text=False): try: for lint_result in tests: max_name_len = max(len(lint_result.component_name), max_name_len) - except Exception: + except (AttributeError, TypeError): pass # Helper function to format test links nicely @@ -285,8 +285,9 @@ def format_result(test_results: list[LintResult], table: Table) -> Table: module_name = lint_result.component_name # Make the filename clickable to open in VSCode - file_path = os.path.relpath(lint_result.file_path, self.directory) - file_path_link = f"[link=vscode://file/{os.path.abspath(file_path)}]{file_path}[/link]" + file_path_obj = Path(lint_result.file_path) + file_path_rel = file_path_obj.relative_to(self.directory) + file_path_link = f"[link=vscode://file/{file_path_obj.resolve()}]{file_path_rel}[/link]" # Add link to the test documentation tools_version = __version__ diff --git a/nf_core/components/nfcore_component.py b/nf_core/components/nfcore_component.py index fa468be970..37744bf291 100644 --- a/nf_core/components/nfcore_component.py +++ b/nf_core/components/nfcore_component.py @@ -295,11 +295,8 @@ def get_inputs_from_main_nf(self) -> None: return # get all lines between "take" and "main" or "emit" input_data = data.split("take:")[1].split("main:")[0].split("emit:")[0] - for line in input_data.split("\n"): - try: - inputs.append(line.split()[0]) - except IndexError: - pass # Empty lines + # Extract first word from non-empty lines + inputs = [line.split()[0] for line in input_data.split("\n") if line.split()] log.debug(f"Found {len(inputs)} inputs in {self.main_nf}") self.inputs = inputs @@ -341,12 +338,8 @@ def get_outputs_from_main_nf(self): log.debug(f"Could not find any outputs in {self.main_nf}") return outputs output_data = data.split("emit:")[1].split("}")[0] - for line in output_data.split("\n"): - try: - outputs.append(line.split("=")[0].split()[0]) - except IndexError: - # Empty lines - pass + # Extract first word before '=' from non-empty lines + outputs = [parts[0] for line in output_data.split("\n") if (parts := line.split("=")[0].split())] log.debug(f"Found {len(outputs)} outputs in {self.main_nf}") self.outputs = outputs diff --git a/nf_core/components/patch.py b/nf_core/components/patch.py index 59ec7a381b..f7a1a00b6f 100644 --- a/nf_core/components/patch.py +++ b/nf_core/components/patch.py @@ -1,5 +1,4 @@ import logging -import os import shutil import tempfile from pathlib import Path @@ -10,6 +9,7 @@ from nf_core.components.components_command import ComponentCommand from nf_core.components.components_differ import ComponentsDiffer from nf_core.modules.modules_json import ModulesJson +from nf_core.pipelines.containers_utils import try_generate_container_configs log = logging.getLogger(__name__) @@ -52,6 +52,10 @@ def patch(self, component=None): components = self.modules_json.get_all_components(self.component_type).get(self.modules_repo.remote_url) if component is None: + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument" + ) choices = [ component if directory == self.modules_repo.repo_path else f"{directory}/{component}" for directory, component in components @@ -61,7 +65,7 @@ def patch(self, component=None): choices=sorted(choices), style=nf_core.utils.nfcore_question_style, ).unsafe_ask() - component_dir = [dir for dir, m in components if m == component][0] + component_dir = [comp_dir for comp_dir, m in components if m == component][0] component_fullname = str(Path(self.component_type, self.modules_repo.repo_path, component)) # Verify that the component has an entry in the modules.json file @@ -94,12 +98,15 @@ def patch(self, component=None): patch_path = Path(self.directory, patch_relpath) if patch_path.exists(): + self.require_prompts( + f"Patch already exists for '{component_fullname}'.\nPlease remove the existing patch file first" + ) remove = questionary.confirm( f"Patch exists for {self.component_type[:-1]} '{component_fullname}'. Do you want to regenerate it?", style=nf_core.utils.nfcore_question_style, ).unsafe_ask() if remove: - os.remove(patch_path) + patch_path.unlink() else: return @@ -112,7 +119,8 @@ def patch(self, component=None): ) # Write the patch to a temporary location (otherwise it is printed to the screen later) - patch_temp_path = tempfile.mktemp() + with tempfile.NamedTemporaryFile(delete=False) as tmp: + patch_temp_path = tmp.name try: ComponentsDiffer.write_diff_file( patch_temp_path, @@ -126,7 +134,9 @@ def patch(self, component=None): ) log.debug(f"Patch file wrote to a temporary directory {patch_temp_path}") except UserWarning: - raise UserWarning(f"{self.component_type[:-1]} '{component_fullname}' is unchanged. No patch to compute") + raise UserWarning( + f"{self.component_type[:-1]} '{component_fullname}' is unchanged. No patch to compute" + ) from None # Write changes to modules.json self.modules_json.add_patch_entry( @@ -148,6 +158,10 @@ def patch(self, component=None): shutil.move(patch_temp_path, patch_path) log.info(f"Patch file of '{component_fullname}' written to '{patch_path}'") + # Regenerate container configuration files for the pipeline when modules are removed + if self.component_type == "modules": + try_generate_container_configs(self.directory) + def remove(self, component): # Check modules directory structure self.check_modules_structure() @@ -155,8 +169,14 @@ def remove(self, component): self.modules_json.check_up_to_date() self._parameter_checks(component) components = self.modules_json.get_all_components(self.component_type).get(self.modules_repo.remote_url) + if components is None: + raise ValueError(f"No components found for {self.component_type} in the pipeline") if component is None: + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument" + ) choices = [ component if directory == self.modules_repo.repo_path else f"{directory}/{component}" for directory, component in components @@ -166,7 +186,7 @@ def remove(self, component): choices, style=nf_core.utils.nfcore_question_style, ).unsafe_ask() - component_dir = [dir for dir, m in components if m == component][0] + component_dir = [comp_dir for comp_dir, m in components if m == component][0] component_fullname = str(Path(self.component_type, component_dir, component)) # Verify that the component has an entry in the modules.json file @@ -199,6 +219,9 @@ def remove(self, component): component_path = Path(self.directory, component_relpath) if patch_path.exists(): + self.require_prompts( + f"Patch exists for '{component_fullname}'.\nPlease remove the existing patch file first" + ) remove = questionary.confirm( f"Patch exists for {self.component_type[:-1]} '{component_fullname}'. Are you sure you want to remove?", style=nf_core.utils.nfcore_question_style, @@ -213,9 +236,9 @@ def remove(self, component): try: for file in Path(temp_component_dir).glob("*"): file.rename(component_path.joinpath(file.name)) - os.rmdir(temp_component_dir) + Path(temp_component_dir).rmdir() except Exception as err: - raise UserWarning(f"There was a problem reverting the patched file: {err}") + raise UserWarning(f"There was a problem reverting the patched file: {err}") from err log.info(f"Patch for {component} reverted!") # Remove patch file if we could revert the patch diff --git a/nf_core/components/remove.py b/nf_core/components/remove.py index 7d7d05d033..123f0c6b52 100644 --- a/nf_core/components/remove.py +++ b/nf_core/components/remove.py @@ -9,6 +9,7 @@ import nf_core.utils from nf_core.components.components_command import ComponentCommand from nf_core.modules.modules_json import ModulesJson +from nf_core.pipelines.containers_utils import try_generate_container_configs from .install import ComponentInstall @@ -53,6 +54,10 @@ def remove(self, component, repo_url=None, repo_path=None, removed_by=None, remo if repo_url is None: repo_url = self.modules_repo.remote_url if component is None: + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument" + ) component = questionary.autocomplete( f"{self.component_type[:-1]} name:", choices=self.components_from_repo(repo_path), @@ -130,6 +135,10 @@ def remove(self, component, repo_url=None, repo_path=None, removed_by=None, remo ) # ask the user if they still want to remove the component, add it back otherwise if not force: + self.require_prompts( + f"{self.component_type[:-1].title()} '{component}' is still included in workflow files.\n" + "Use '--force' to force removal" + ) if not questionary.confirm( f"Do you still want to remove the {self.component_type[:-1]} '{component}'?", style=nf_core.utils.nfcore_question_style, @@ -144,11 +153,7 @@ def remove(self, component, repo_url=None, repo_path=None, removed_by=None, remo removed_components.append(component) return removed # Remove the component files of all entries removed from modules.json - removed = ( - True - if self.clear_component_dir(component, Path(self.directory, removed_component_dir)) or removed - else False - ) + removed = bool(self.clear_component_dir(component, Path(self.directory, removed_component_dir)) or removed) removed_components.append(component) # Prettify modules.json file after all changes have been made @@ -176,9 +181,14 @@ def remove(self, component, repo_url=None, repo_path=None, removed_by=None, remo # remember removed dependencies if dependency_removed: removed_components.append(component_name.replace("/", "_")) + # Regenerate container configuration files for the pipeline when modules are removed + if self.component_type == "modules": + try_generate_container_configs(self.directory) + # print removed dependencies - if removed_components: - log.info(f"Removed files for '{component}' and its dependencies '{', '.join(removed_components)}'.") + dependencies = set(removed_components) - {component} + if dependencies: + log.info(f"Removed files for '{component}' and its dependencies '{', '.join(dependencies)}'.") else: log.info(f"Removed files for '{component}'.") else: diff --git a/nf_core/components/update.py b/nf_core/components/update.py index fcdb005e9f..1017643549 100644 --- a/nf_core/components/update.py +++ b/nf_core/components/update.py @@ -1,12 +1,10 @@ import logging -import os import shutil import tempfile from pathlib import Path import questionary -import nf_core.modules.modules_utils import nf_core.utils from nf_core.components.components_command import ComponentCommand from nf_core.components.components_differ import ComponentsDiffer @@ -18,6 +16,7 @@ from nf_core.components.remove import ComponentRemove from nf_core.modules.modules_json import ModulesJson from nf_core.modules.modules_repo import ModulesRepo +from nf_core.pipelines.containers_utils import try_generate_container_configs from nf_core.utils import plural_es, plural_s, plural_y log = logging.getLogger(__name__) @@ -39,6 +38,7 @@ def __init__( branch=None, no_pull=False, limit_output=False, + skip_deps=False, ): super().__init__(component_type, pipeline_dir, remote_url, branch, no_pull) self.current_remote = ModulesRepo(remote_url, branch) @@ -51,6 +51,7 @@ def __init__( self.save_diff_fn = save_diff_fn self.limit_output = limit_output self.update_deps = update_deps + self.skip_deps = skip_deps self.component = None self.update_config = None self.modules_json = ModulesJson(self.directory) @@ -69,6 +70,12 @@ def _parameter_checks(self): if self.update_all and self.component: raise UserWarning(f"Either a {self.component_type[:-1]} or the '--all' flag can be specified, not both.") + if self.skip_deps and self.update_deps: + raise UserWarning("`--skip-deps` and `--update-deps` are mutually exclusive.") + + if self.skip_deps and self.update_all: + raise UserWarning("`--skip-deps` and `--all` are mutually exclusive.") + if self.repo_type == "modules": raise UserWarning( f"{self.component_type.title()} can not be updated in clones of the nf-core/modules repository." @@ -117,6 +124,10 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr self.modules_json.check_up_to_date() if not self.update_all and component is None: + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument or use '--all'" + ) choices = [f"All {self.component_type}", f"Named {self.component_type[:-1]}"] self.update_all = ( questionary.select( @@ -141,6 +152,10 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr # Ask if we should show the diffs (unless a filename was already given on the command line) if not self.save_diff_fn and self.show_diff is None: + self.require_prompts( + "Diff display preference not specified.\n" + "Please use '--preview', '--save-diff', or neither to skip diff viewing" + ) diff_type = questionary.select( "Do you want to view diffs of the proposed changes?", choices=[ @@ -190,13 +205,12 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr else: version = modules_repo.get_latest_component_version(component, self.component_type) - if current_version is not None and not self.force: - if current_version == version: - if self.sha or self.prompt: - log.info(f"'{component_fullname}' is already installed at {version}") - else: - log.info(f"'{component_fullname}' is already up to date") - continue + if current_version is not None and not self.force and current_version == version: + if self.sha or self.prompt: + log.info(f"'{component_fullname}' is already installed at {version}") + else: + log.info(f"'{component_fullname}' is already up to date") + continue # Download component files if not self.install_component_files(component, version, modules_repo, str(install_tmp_dir)): @@ -252,13 +266,15 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr updated.append(component) recursive_update = True modules_to_update, subworkflows_to_update = self.get_components_to_update(component) - if not silent and len(modules_to_update + subworkflows_to_update) > 0: + if self.skip_deps: + recursive_update = False + elif not silent and len(modules_to_update + subworkflows_to_update) > 0: log.warning( f"All modules and subworkflows linked to the updated {self.component_type[:-1]} will be added to the same diff file.\n" "It is advised to keep all your modules and subworkflows up to date.\n" "It is not guaranteed that a subworkflow will continue working as expected if all modules/subworkflows used in it are not up to date.\n" ) - if self.update_deps: + if self.update_deps or self.no_prompts: recursive_update = True else: recursive_update = questionary.confirm( @@ -286,6 +302,10 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr limit_output=self.limit_output, ) # Ask the user if they want to install the component + self.require_prompts( + "Cannot interactively confirm updates.\n" + "Please run without '--preview' or use '--save-diff' instead" + ) dry_run = not questionary.confirm( f"Update {self.component_type[:-1]} '{component}'?", default=False, @@ -298,23 +318,29 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr # Update modules.json with newly installed component self.modules_json.update(self.component_type, modules_repo, component, version, installed_by=None) updated.append(component) + + # Regenerate container configuration files for the pipeline when modules are updated + if self.component_type == "modules": + try_generate_container_configs(self.directory) recursive_update = True modules_to_update, subworkflows_to_update = self.get_components_to_update(component) - if not silent and len(modules_to_update + subworkflows_to_update) > 0: - if not self.update_all: - log.warning( - f"All modules and subworkflows linked to the updated {self.component_type[:-1]} will be {'asked for update' if self.show_diff else 'automatically updated'}.\n" - "It is advised to keep all your modules and subworkflows up to date.\n" - "It is not guaranteed that a subworkflow will continue working as expected if all modules/subworkflows used in it are not up to date.\n" - ) - if self.update_deps: - recursive_update = True - else: - recursive_update = questionary.confirm( - "Would you like to continue updating all modules and subworkflows?", - default=True, - style=nf_core.utils.nfcore_question_style, - ).unsafe_ask() + if self.skip_deps: + # Caller asked for single-target update; skip linked components entirely. + recursive_update = False + elif not silent and len(modules_to_update + subworkflows_to_update) > 0 and not self.update_all: + log.warning( + f"All modules and subworkflows linked to the updated {self.component_type[:-1]} will be {'asked for update' if self.show_diff else 'automatically updated'}.\n" + "It is advised to keep all your modules and subworkflows up to date.\n" + "It is not guaranteed that a subworkflow will continue working as expected if all modules/subworkflows used in it are not up to date.\n" + ) + if self.update_deps or self.no_prompts: + recursive_update = True + else: + recursive_update = questionary.confirm( + "Would you like to continue updating all modules and subworkflows?", + default=True, + style=nf_core.utils.nfcore_question_style, + ).unsafe_ask() if recursive_update and len(modules_to_update + subworkflows_to_update) > 0: # Update linked components self.update_linked_components(modules_to_update, subworkflows_to_update, updated) @@ -374,6 +400,10 @@ def get_single_component_info(self, component): ] if component is None: + self.require_prompts( + f"No {self.component_type[:-1]} name provided.\n" + f"Please provide the {self.component_type[:-1]} name as a command-line argument" + ) component = questionary.autocomplete( f"{self.component_type[:-1].title()} name:", choices=sorted(choices), @@ -382,9 +412,9 @@ def get_single_component_info(self, component): # Get component installation directory try: - install_dir = [dir for dir, m in components if component == m][0] - except IndexError: - raise UserWarning(f"{self.component_type[:-1].title()} '{component}' not found in 'modules.json'.") + install_dir = [comp_dir for comp_dir, m in components if component == m][0] + except IndexError as e: + raise UserWarning(f"{self.component_type[:-1].title()} '{component}' not found in 'modules.json'.") from e # Check if component is installed before trying to update if component not in choices: @@ -403,12 +433,10 @@ def get_single_component_info(self, component): config_entry = None if self.update_config is not None: if any( - [ - entry.count("/") == 1 - and (entry.endswith("modules") or entry.endswith("subworkflows")) - and not (entry.endswith(".git") or entry.endswith(".git/")) - for entry in self.update_config.keys() - ] + entry.count("/") == 1 + and entry.endswith(("modules", "subworkflows")) + and not entry.endswith((".git", ".git/")) + for entry in self.update_config ): raise UserWarning( "Your '.nf-core.yml' file format is outdated. " @@ -423,7 +451,7 @@ def get_single_component_info(self, component): config_entry = self.update_config[self.modules_repo.remote_url][install_dir].get(component) if config_entry is not None and config_entry is not True: if config_entry is False: - log.warn( + log.warning( f"{self.component_type[:-1].title()}'s update entry in '.nf-core.yml' for '{component}' is set to False" ) return (self.modules_repo, None, None, None) @@ -452,6 +480,11 @@ def get_single_component_info(self, component): f"You are trying to update the '{Path(install_dir, component)}' {self.component_type[:-1]} from " f"the '{new_branch}' branch. This {self.component_type[:-1]} was installed from the '{current_branch}'" ) + self.require_prompts( + f"Branch mismatch for '{component}'.\n" + f"The {self.component_type[:-1]} was installed from '{current_branch}' but you are updating from '{new_branch}'.\n" + f"Please use '-b {current_branch}' to update from the original branch" + ) switch = questionary.confirm(f"Do you want to update using the '{current_branch}' instead?").unsafe_ask() if switch: # Change the branch @@ -517,13 +550,13 @@ def get_all_components_info(self, branch=None): ] elif isinstance(self.update_config, dict) and isinstance(self.update_config[repo_name], dict): # If it is a dict, then there are entries for individual components or component directories - for component_dir in set([dir for dir, _ in components]): + for component_dir in {comp_dir for comp_dir, _ in components}: if isinstance(self.update_config[repo_name][component_dir], str): # If a string is given it is the commit SHA to which we should update to custom_sha = self.update_config[repo_name][component_dir] components_info[repo_name] = {} - for dir, component in components: - if component_dir == dir: + for comp_dir, component in components: + if component_dir == comp_dir: try: components_info[repo_name][component_dir].append( ( @@ -547,7 +580,7 @@ def get_all_components_info(self, branch=None): if self.sha is not None: overridden_repos.append(repo_name) elif self.update_config[repo_name][component_dir] is False: - for directory, component in components: + for directory, _component in components: if directory == component_dir: skipped_components.append(f"{component_dir}/{components}") elif isinstance(self.update_config[repo_name][component_dir], dict): @@ -664,7 +697,7 @@ def get_all_components_info(self, branch=None): # Loop through components_info and create on ModulesRepo object per remote and branch repos_and_branches = {} for repo_name, repo_content in components_info.items(): - for component_dir, comps in repo_content.items(): + for _component_dir, comps in repo_content.items(): for comp, sha, comp_branch in comps: if branch is not None: comp_branch = branch @@ -725,6 +758,7 @@ def setup_diff_file(self, check_diff_exist=True): Then creates the file for saving the diff. """ if self.save_diff_fn is True: + self.require_prompts("No diff filename provided.\nPlease provide a filename with '--save-diff '") # From questionary - no filename yet self.save_diff_fn = questionary.path( "Enter the filename: ", style=nf_core.utils.nfcore_question_style @@ -740,8 +774,12 @@ def setup_diff_file(self, check_diff_exist=True): return # Check if filename already exists (questionary or cli) while self.save_diff_fn.exists(): + self.require_prompts( + f"Diff file '{self.save_diff_fn}' already exists.\n" + "Please remove the file or provide a different filename" + ) if questionary.confirm(f"'{self.save_diff_fn}' exists. Remove file?").unsafe_ask(): - os.remove(self.save_diff_fn) + self.save_diff_fn.unlink() break self.save_diff_fn = questionary.path( "Enter a new filename: ", @@ -768,6 +806,7 @@ def move_files_from_tmp_dir(self, component: str, install_folder: str, repo_path if pipeline_path.exists(): pipeline_files = [f.name for f in pipeline_path.iterdir() if f.is_file()] # check if any *.config file exists in the pipeline + # TODO: Use f.suffix == ".config" instead of str(f).endswith(".config") config_files = [f for f in pipeline_files if str(f).endswith(".config")] for config_file in config_files: log.debug(f"Moving '{component}/{config_file}' to updated component") @@ -905,16 +944,20 @@ def get_components_to_update(self, component): elif self.component_type == "subworkflows": for repo, repo_content in mods_json["repos"].items(): for component_type, dir_content in repo_content.items(): - for dir, components in dir_content.items(): + for install_dir, components in dir_content.items(): for comp, comp_content in components.items(): # If the updated subworkflow name appears in the installed_by section of the checked component # The checked component is used by the updated subworkflow # We need to update it too if component in comp_content["installed_by"]: if component_type == "modules": - modules_to_update.append({"name": comp, "git_remote": repo, "org_path": dir}) + modules_to_update.append( + {"name": comp, "git_remote": repo, "org_path": install_dir} + ) elif component_type == "subworkflows": - subworkflows_to_update.append({"name": comp, "git_remote": repo, "org_path": dir}) + subworkflows_to_update.append( + {"name": comp, "git_remote": repo, "org_path": install_dir} + ) return modules_to_update, subworkflows_to_update diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index dca542d2c1..b8767a5bc2 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -25,7 +25,7 @@ from nf_core.configs.create.welcome import WelcomeScreen ## General utilities -from nf_core.utils import LoggingConsole +from nf_core.configs.create.utils import LoggingConsole ## Logging logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ class ConfigsCreateApp(App[utils.ConfigsCreateConfig]): """A Textual app to create nf-core configs.""" - CSS_PATH = "../../textual.tcss" + CSS_PATH = "create.tcss" TITLE = "nf-core configs create" SUB_TITLE = "Create a new nextflow config with an interactive interface" BINDINGS = [ diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index d7991a4682..329c81615a 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -12,7 +12,7 @@ from textual.widgets import Button, Footer, Header, Input, Markdown, Select from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context -from nf_core.utils import add_hide_class, remove_hide_class +from nf_core.configs.create.utils import add_hide_class, remove_hide_class config_exists_warn = """ > ⚠️ **The config file you are trying to create already exists.** diff --git a/nf_core/textual.tcss b/nf_core/configs/create/create.tcss similarity index 100% rename from nf_core/textual.tcss rename to nf_core/configs/create/create.tcss diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index fd8111626a..e54f4c45c7 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -8,7 +8,7 @@ from textual.widgets import Button, Footer, Header, Input, Markdown, Select, Static, Switch from nf_core.configs.create.utils import SUPPORTED_CONTAINERS, ConfigsCreateConfig, TextInput, init_context -from nf_core.utils import add_hide_class, remove_hide_class +from nf_core.configs.create.utils import add_hide_class, remove_hide_class markdown_intro = """ # Configure the options for your infrastructure config diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 72f9f63929..52a1e079d2 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -13,7 +13,8 @@ from textual.containers import Grid from textual.suggester import SuggestFromList from textual.validation import ValidationResult, Validator -from textual.widgets import Input, Static +from textual.widget import Widget +from textual.widgets import Input, RichLog, Static # Use ContextVar to define a context on the model initialization _init_context_var: ContextVar = ContextVar("_init_context_var", default={}) @@ -677,3 +678,21 @@ def validate(self, value: str) -> ValidationResult: def generate_config_entry(self, key, value): parsed_entry = " " + key + ' = "' + value + '"\n' return parsed_entry + + +class LoggingConsole(RichLog): + file = False + console: Widget + + def print(self, content): + self.write(content) + + +def add_hide_class(app, widget_id: str) -> None: + """Add class 'hide' to a widget. Not display widget.""" + app.get_widget_by_id(widget_id).add_class("hide") + + +def remove_hide_class(app, widget_id: str) -> None: + """Remove class 'hide' to a widget. Display widget.""" + app.get_widget_by_id(widget_id).remove_class("hide") \ No newline at end of file diff --git a/nf_core/module-template/main.nf b/nf_core/module-template/main.nf index 49802b58c9..ac40bb511f 100644 --- a/nf_core/module-template/main.nf +++ b/nf_core/module-template/main.nf @@ -25,9 +25,9 @@ process {{ component_name_underscore|upper }} { // TODO nf-core: See section in main README for further information regarding finding and adding container addresses to the section below. {% endif -%} conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? '{{ singularity_container if singularity_container else 'https://depot.galaxyproject.org/singularity/YOUR-TOOL-HERE' }}': - '{{ docker_container if docker_container else 'biocontainers/YOUR-TOOL-HERE' }}' }" + '{{ 'quay.io/' + docker_container if docker_container else 'quay.io/biocontainers/YOUR-TOOL-HERE' }}' }" input: {%- if inputs %} @@ -126,8 +126,8 @@ process {{ component_name_underscore|upper }} { {% if not_empty_template -%} // TODO nf-core: A stub section should mimic the execution of the original module as best as possible // Have a look at the following examples: - // Simple example: https://github.com/nf-core/modules/blob/818474a292b4860ae8ff88e149fbcda68814114d/modules/nf-core/bcftools/annotate/main.nf#L47-L63 - // Complex example: https://github.com/nf-core/modules/blob/818474a292b4860ae8ff88e149fbcda68814114d/modules/nf-core/bedtools/split/main.nf#L38-L54 + // Simple example: https://github.com/nf-core/modules/blob/624977dfaf562211e68a8a868ca80acc8461f1ac/modules/nf-core/cutadapt/main.nf#L34-L46 + // Complex example: https://github.com/nf-core/modules/blob/88d43dad73a675e66bff49ebb57fe657a5909018/modules/nf-core/bedtools/split/main.nf#L32-L43 // TODO nf-core: If the module doesn't use arguments ($args), you SHOULD remove: // - The definition of args `def args = task.ext.args ?: ''` above. // - The use of the variable in the script `echo $args ` below. diff --git a/nf_core/module-template/tests/main.nf.test.j2 b/nf_core/module-template/tests/main.nf.test.j2 index c3eaf48697..a05f6a33a9 100644 --- a/nf_core/module-template/tests/main.nf.test.j2 +++ b/nf_core/module-template/tests/main.nf.test.j2 @@ -19,7 +19,7 @@ nextflow_process { // TODO nf-core: If you are created a test for a chained module // (the module requires running more than one process to generate the required output) // add the 'setup' method here. - // You can find more information about how to use a 'setup' method in the docs (https://nf-co.re/docs/contributing/modules#steps-for-creating-nf-test-for-chained-modules). + // You can find more information about how to use a 'setup' method in the nf-test docs (https://www.nf-test.com/docs/testcases/setup/). when { process { @@ -45,7 +45,7 @@ nextflow_process { process.out.findAll { key, val -> key.startsWith('versions') } ).match() } //TODO nf-core: Add all required assertions to verify the test output. - // See https://nf-co.re/docs/contributing/tutorials/nf-test_assertions for more information and examples. + // See https://nf-co.re/docs/developing/testing/assertions for more information and examples. ) } diff --git a/nf_core/modules/bump_versions.py b/nf_core/modules/bump_versions.py index d4f325376b..7ab5da5855 100644 --- a/nf_core/modules/bump_versions.py +++ b/nf_core/modules/bump_versions.py @@ -3,6 +3,7 @@ or for a single module """ +import contextlib import logging import os import re @@ -93,6 +94,9 @@ def bump_versions( # Prompt for module or all if module is None and not all_modules: + self.require_prompts( + "No module name provided.\nPlease provide the module name as a command-line argument or use '--all'" + ) question = { "type": "list", "name": "all_modules", @@ -313,10 +317,8 @@ def _print_results(self) -> None: # Find maximum module name length max_mod_name_len = 40 for m in [self.up_to_date, self.updated, self.failed]: - try: + with contextlib.suppress(Exception): max_mod_name_len = max(len(m[2]), max_mod_name_len) - except Exception: - pass def format_result(module_updates: list[tuple[str, str]], table: Table) -> Table: """ @@ -328,10 +330,7 @@ def format_result(module_updates: list[tuple[str, str]], table: Table) -> Table: row_style = None for module_update in module_updates: if last_modname and module_update[1] != last_modname: - if row_style: - row_style = None - else: - row_style = "magenta" + row_style = None if row_style else "magenta" last_modname = module_update[1] table.add_row( Markdown(f"{module_update[1]}"), diff --git a/nf_core/modules/create.py b/nf_core/modules/create.py index a5e0795a9f..b5368130ce 100644 --- a/nf_core/modules/create.py +++ b/nf_core/modules/create.py @@ -17,7 +17,6 @@ def __init__( conda_name=None, conda_version=None, empty_template=False, - migrate_pytest=False, ): super().__init__( "modules", @@ -30,5 +29,4 @@ def __init__( conda_name, conda_version, empty_template, - migrate_pytest, ) diff --git a/nf_core/modules/lint/__init__.py b/nf_core/modules/lint/__init__.py index d4bc1ef974..f2472db0d3 100644 --- a/nf_core/modules/lint/__init__.py +++ b/nf_core/modules/lint/__init__.py @@ -375,7 +375,7 @@ def _sort_meta_yml(meta_yml: dict) -> dict: schema = self.load_meta_schema() schema_keys = list(schema["properties"].keys()) except (LintExceptionError, KeyError) as e: - raise UserWarning("Failed to load meta schema", e) + raise UserWarning("Failed to load meta schema", e) from e result: dict = {} @@ -385,7 +385,7 @@ def _sort_meta_yml(meta_yml: dict) -> dict: result[key] = meta_yml[key] # Then add any keys that aren't in the schema (to preserve custom keys) - for key in meta_yml.keys(): + for key in meta_yml: if key not in result: result[key] = meta_yml[key] @@ -394,13 +394,10 @@ def _sort_meta_yml(meta_yml: dict) -> dict: # Obtain inputs, outputs and topics from main.nf and meta.yml # Used to compare only the structure of channels and elements # Do not compare features to allow for custom features in meta.yml (i.e. pattern) - if "input" in meta_yml: - correct_inputs = self.obtain_inputs(mod.inputs) - meta_inputs = self.obtain_inputs(meta_yml["input"]) - if "output" in meta_yml: - correct_outputs = self.obtain_outputs(mod.outputs) - meta_outputs = self.obtain_outputs(meta_yml["output"]) - + correct_inputs = self.obtain_inputs(mod.inputs) + meta_inputs = self.obtain_inputs(meta_yml.get("input", [])) + correct_outputs = self.obtain_outputs(mod.outputs) + meta_outputs = self.obtain_outputs(meta_yml.get("output", {})) correct_topics = self.obtain_topics(mod.topics) meta_topics = self.obtain_topics(meta_yml.get("topics", {})) @@ -413,7 +410,7 @@ def _sort_meta_yml(meta_yml: dict) -> dict: versions_entry = template_meta.get("topics", {}).get("versions", [[]])[0] if len(versions_entry) == 3: topic_metadata = [next(iter(item.values())) for item in versions_entry] - except Exception as e: + except (OSError, ruamel.yaml.YAMLError, IndexError, StopIteration) as e: log.debug(f"Could not load topic template metadata: {e}") def _populate_channel_elements(io_type, correct_value, meta_value, mod_io_data, meta_yml_io, check_exists=True): @@ -488,7 +485,7 @@ def _process_element(element, index, is_output=False): # Only update structure when it differs from main.nf corrected_data = meta_yml_io.copy() if meta_yml_io else mod_io_data.copy() - for ch_name in mod_io_data.keys(): + for ch_name in mod_io_data: # Ensure channel exists in corrected_data if ch_name not in corrected_data: corrected_data[ch_name] = [] @@ -603,7 +600,7 @@ def _add_edam_ontologies(section, edam_formats, desc): if extension in edam_formats: expected_ontologies.append((edam_formats[extension][0], extension)) # remove duplicated entries - expected_ontologies = list({k: v for k, v in expected_ontologies}.items()) + expected_ontologies = list(dict(expected_ontologies).items()) if "ontologies" in section: for ontology in section["ontologies"]: try: @@ -639,7 +636,7 @@ def _add_edam_ontologies(section, edam_formats, desc): ) if "output" in meta_yml: - for ch_name in corrected_meta_yml["output"].keys(): + for ch_name in corrected_meta_yml["output"]: ch_content = corrected_meta_yml["output"][ch_name][0] if isinstance(ch_content, list): for i, element in enumerate(ch_content): @@ -667,7 +664,7 @@ def _add_edam_ontologies(section, edam_formats, desc): # Create YAML anchors for versions_* keys in output that match "versions" in topics # Since we now populate metadata for both output and topics, set up anchors to reference output from topics if "output" in corrected_meta_yml and "topics" in corrected_meta_yml: - versions_keys = [key for key in corrected_meta_yml["output"].keys() if key.startswith("versions_")] + versions_keys = [key for key in corrected_meta_yml["output"] if key.startswith("versions_")] if versions_keys and "versions" in corrected_meta_yml["topics"]: # Set topics["versions"] to reference output versions (now with populated metadata) diff --git a/nf_core/modules/lint/environment_yml.py b/nf_core/modules/lint/environment_yml.py index 2642a84ac2..63b3aac752 100644 --- a/nf_core/modules/lint/environment_yml.py +++ b/nf_core/modules/lint/environment_yml.py @@ -22,6 +22,18 @@ def environment_yml(module_lint_object: ComponentLint, module: NFCoreComponent, The lint test checks that the ``dependencies`` section in the environment.yml file is valid YAML and that it is sorted alphabetically. + + The following checks are performed: + + * ``environment_yml_exists``: The ``environment.yml`` file must exist if it is + referenced in ``main.nf``. + + * ``environment_yml_valid``: The ``environment.yml`` must be valid according to + the JSON schema defined at https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json. + + * ``environment_yml_sorted``: The dependencies listed in ``environment.yml`` + must be sorted alphabetically. If they are not, they will be sorted + automatically. """ env_yml = None has_schema_header = False diff --git a/nf_core/modules/lint/main_nf.py b/nf_core/modules/lint/main_nf.py index 17bc8b038a..b589bc3f0c 100644 --- a/nf_core/modules/lint/main_nf.py +++ b/nf_core/modules/lint/main_nf.py @@ -13,7 +13,6 @@ from rich.progress import Progress import nf_core -import nf_core.modules.modules_utils from nf_core.components.components_differ import ComponentsDiffer from nf_core.components.nfcore_component import NFCoreComponent @@ -23,23 +22,42 @@ def main_nf( module_lint_object, module: NFCoreComponent, fix_version: bool, registry: str, progress_bar: Progress ) -> tuple[list[str], list[str]]: - """ - Lint a ``main.nf`` module file + """Lint a ``main.nf`` module file Can also be used to lint local module files, - in which case failures will be reported as - warnings. - - The test checks for the following: - - * Software versions and containers are valid - * The module has a process label and it is among - the standard ones. - * If a ``meta`` map is defined as one of the modules - inputs it should be defined as one of the emits, - and be correctly configured in the ``saveAs`` function. - * The module script section should contain definitions - of ``software`` and ``prefix`` + in which case failures will be reported as warnings. + + The following checks are performed: + + * ``main_nf_exists``: The ``main.nf`` file must exist. + + * ``deprecated_dsl2``: The file must not contain deprecated DSL2 identifiers + (``initOptions``, ``saveFiles``, ``getSoftwareName``, ``getProcessName``, + ``publishDir``). + + * ``main_nf_script_outputs``: The process must have an ``output:`` block. + + * ``main_nf_container``: Container tags across the ``singularity``, ``docker``, + and ``conda`` directives must reference the same software version. A warning + is issued if they do not match. + + * ``main_nf_script_shell``: Exactly one of ``script:``, ``shell:``, or ``exec:`` + blocks must be present. + + * ``main_nf_shell_template``: If a ``shell:`` block is used, it must call + a ``template``. + + * ``main_nf_meta_output``: If ``meta`` is present in the module inputs, it + must also appear in at least one output channel. + + * ``main_nf_version_topic``: The module should emit software versions using + a ``topic: versions`` output. A warning is issued if no such topic is found. + + * ``main_nf_version_emit``: The number of ``topic: versions`` outputs must + equal the number of ``emit:`` outputs whose name starts with ``versions``. + A warning is issued if a legacy YAML-based ``versions`` emit is used instead + of a topic output. + """ inputs: list[str] = [] @@ -65,15 +83,12 @@ def main_nf( with open(module.main_nf) as fh: lines = fh.readlines() module.passed.append(("main_nf", "main_nf_exists", "Module file exists", module.main_nf)) - except FileNotFoundError: + except FileNotFoundError as e: module.failed.append(("main_nf", "main_nf_exists", "Module file does not exist", module.main_nf)) - raise FileNotFoundError(f"Module file does not exist: {module.main_nf}") + raise FileNotFoundError(f"Module file does not exist: {module.main_nf}") from e deprecated_i = ["initOptions", "saveFiles", "getSoftwareName", "getProcessName", "publishDir"] - if len(lines) > 0: - lines_j = "\n".join(lines) - else: - lines_j = "" + lines_j = "\n".join(lines) if len(lines) > 0 else "" for i in deprecated_i: if i in lines_j: @@ -145,6 +160,10 @@ def main_nf( if state == "exec" and not _is_empty(line): exec_lines.append(line) + # Check meta naming + if inputs: + check_meta_input_names(module, inputs) + # Check that we have required sections if not len(emits): module.failed.append(("main_nf", "main_nf_script_outputs", "No process 'output' block found", module.main_nf)) @@ -191,28 +210,27 @@ def main_nf( ) # Check whether 'meta' is emitted when given as input - if inputs: - if "meta" in inputs: - module.has_meta = True - if emits: - if "meta" in emits: - module.passed.append( - ( - "main_nf", - "main_nf_meta_output", - "'meta' map emitted in output channel(s)", - module.main_nf, - ) + if inputs and "meta" in inputs: + module.has_meta = True + if emits: + if "meta" in emits: + module.passed.append( + ( + "main_nf", + "main_nf_meta_output", + "'meta' map emitted in output channel(s)", + module.main_nf, ) - else: - module.failed.append( - ( - "main_nf", - "main_nf_meta_output", - "'meta' map not emitted in output channel(s)", - module.main_nf, - ) + ) + else: + module.failed.append( + ( + "main_nf", + "main_nf_meta_output", + "'meta' map not emitted in output channel(s)", + module.main_nf, ) + ) # Check that a software version is emitted if topics: @@ -221,7 +239,7 @@ def main_nf( ("main_nf", "main_nf_version_topic", "Module emits software versions as topic", module.main_nf) ) else: - module.warned.append( + module.failed.append( ("main_nf", "main_nf_version_topic", "Module does not emit software versions as topic", module.main_nf) ) @@ -286,7 +304,7 @@ def check_script_section(self, lines): permitted_meta_keys = {"id", "single_end"} invalid_meta_keys = [ f"{prefix}{key}" - for prefix, key in re.findall(r"\b(meta\d*\.)(\w+)\b(?!\()", script) + for prefix, key in re.findall(r"\b(meta\d*\??\.)(\w+)\b(?!\()", script) if key not in permitted_meta_keys ] if not invalid_meta_keys: @@ -302,7 +320,7 @@ def check_script_section(self, lines): ) # Validate ext keys - permitted_ext_keys = {"ext.args", "ext.prefix", "ext.use_gpu"} + permitted_ext_keys = {"ext.args", "ext.prefix", "ext.prefix2", "ext.use_gpu"} invalid_ext_keys = [ key for key in re.findall(r"\bext\.\w+", script) @@ -377,7 +395,7 @@ def check_process_section(self, lines, registry, fix_version, progress_bar): check_process_labels(self, lines) # Deprecated enable_conda - for i, raw_line in enumerate(lines): + for _i, raw_line in enumerate(lines): url = None line = raw_line.strip(" \n'\"}:?") @@ -432,18 +450,25 @@ def check_process_section(self, lines, registry, fix_version, progress_bar): else: self.failed.append(("main_nf", "docker_tag", "Unable to parse docker tag", self.main_nf)) docker_tag = None - if line.startswith(registry): + if line.startswith((registry, "community.wave.seqera.io/library/")): l_stripped = re.sub(r"\W+$", "", line) - self.failed.append( + self.passed.append( ( "main_nf", "container_links", - f"{l_stripped} container name found, please use just 'organisation/container:tag' instead.", + f"Container prefix is correct: {l_stripped}", self.main_nf, ) ) else: - self.passed.append(("main_nf", "container_links", "Container prefix is correct", self.main_nf)) + self.failed.append( + ( + "main_nf", + "container_links", + "Container prefix is not correct. Please add the registry prefix (e.g. 'quay.io/')", + self.main_nf, + ) + ) # Guess if container name is simple one (e.g. nfcore/ubuntu:20.04) # If so, add quay.io as default container prefix @@ -458,7 +483,7 @@ def check_process_section(self, lines, registry, fix_version, progress_bar): if url is None: continue try: - container_url = "https://" + urlunparse(url) if not url.scheme == "https" else urlunparse(url) + container_url = "https://" + urlunparse(url) if url.scheme != "https" else urlunparse(url) log.debug(f"Trying to connect to URL: {container_url}") response = requests.head( container_url, @@ -466,7 +491,7 @@ def check_process_section(self, lines, registry, fix_version, progress_bar): allow_redirects=True, ) log.debug( - f"Connected to URL: {'https://' + urlunparse(url) if not url.scheme == 'https' else urlunparse(url)}, " + f"Connected to URL: {'https://' + urlunparse(url) if url.scheme != 'https' else urlunparse(url)}, " f"status_code: {response.status_code}" ) except (requests.exceptions.RequestException, sqlite3.InterfaceError) as e: @@ -596,6 +621,7 @@ def check_process_labels(self, lines): "process_medium", "process_high", "process_long", + "process_low_memory", "process_high_memory", ] all_labels = [line.strip() for line in lines if line.lstrip().startswith("label ")] @@ -665,7 +691,7 @@ def check_container_link_line(self, raw_line, registry): ( "main_nf", "container_links", - f"Too many double quotes found when specifying container: {line.lstrip('container ')}", + f"Too many double quotes found when specifying container: {line.removeprefix('container ')}", self.main_nf, ) ) @@ -674,7 +700,7 @@ def check_container_link_line(self, raw_line, registry): ( "main_nf", "container_links", - f"Correct number of double quotes found when specifying container: {line.lstrip('container ')}", + f"Correct number of double quotes found when specifying container: {line.removeprefix('container ')}", self.main_nf, ) ) @@ -685,7 +711,7 @@ def check_container_link_line(self, raw_line, registry): # Look for container link as single item surrounded by quotes # (if there are multiple links, this will be warned in the next check) container_link = None - if len(single_quoted_items) == 3: + if len(single_quoted_items) == 3 or len(single_quoted_items) == 5 and " in [" in raw_line: container_link = single_quoted_items[1] elif len(double_quoted_items) == 3: container_link = double_quoted_items[1] @@ -723,6 +749,75 @@ def check_container_link_line(self, raw_line, registry): ) +def check_meta_input_names(self, inputs): + """ + Check ``meta_input_names``: The meta* variable names must follow the pattern `meta`, `meta2`, `meta3`, etc. + Args: + inputs (list): List of input variable names + """ + + meta_vars = [var for var in inputs if var.startswith("meta")] + + if not meta_vars: + return # No meta variables to check + + # Expected pattern: 'meta' or 'meta' followed by a number (meta2, meta3, etc.) + valid_pattern = re.compile(r"^meta(\d+)?$") + + invalid_meta_vars = [] + valid_numbers = [] + + for var in meta_vars: + if not valid_pattern.match(var): + invalid_meta_vars.append(var) + else: + # Extract number if present + match = re.match(r"^meta(\d+)?$", var) + if match.group(1): # Has a number + number_str = match.group(1) + number_int = int(number_str) + + if number_str != str(number_int) or number_int < 2: + # Check for leading zeros (e.g., meta02, meta003) or meta0 and meta1 + invalid_meta_vars.append(var) + else: + valid_numbers.append(number_int) + + # Check for invalid names + if invalid_meta_vars: + self.failed.append( + ( + "main_nf", + "meta_input_names", + f"Meta variables must be named 'meta', 'meta2', 'meta3', etc. Found: {', '.join(invalid_meta_vars)}", + self.main_nf, + ) + ) + + # Check for proper sequencing (2, 3, 4... not 2, 5, 3) + if valid_numbers: + expected = list(range(2, len(valid_numbers) + 2)) + if valid_numbers != expected: + self.warned.append( + ( + "main_nf", + "meta_input_names", + f"Meta variable numbers should be sequential starting at 2. Found: meta{', meta'.join(map(str, valid_numbers))}", + self.main_nf, + ) + ) + + if not invalid_meta_vars and (not valid_numbers or valid_numbers == list(range(2, len(valid_numbers) + 2))): + self.passed.append( + ( + "main_nf", + "meta_input_names", + f"Meta variable names follow correct pattern: {', '.join(sorted(meta_vars))}", + self.main_nf, + ) + ) + + def _parse_input(self, line_raw): """ Return list of input channel names from an input line. @@ -931,7 +1026,7 @@ def _container_type(line): """Returns the container type of a build.""" if line.startswith("conda"): return "conda" - if line.startswith("https://") or line.startswith("https://depot"): + if line.startswith("https://"): # Look for a http download URL. # Thanks Stack Overflow for the regex: https://stackoverflow.com/a/3809435/713980 url_regex = ( diff --git a/nf_core/modules/lint/meta_yml.py b/nf_core/modules/lint/meta_yml.py index c663b2defa..27bf1b9d6f 100644 --- a/nf_core/modules/lint/meta_yml.py +++ b/nf_core/modules/lint/meta_yml.py @@ -19,20 +19,46 @@ def meta_yml(module_lint_object: ModuleLint, module: NFCoreComponent, allow_missing: bool = False) -> None: - """ - Lint a ``meta.yml`` file + """Lint a ``meta.yml`` file + + Checks that the module has a ``meta.yml`` file, that it is valid according + to the nf-core JSON schema, and that its contents are consistent with + ``main.nf``. + + The following checks are performed: + + * ``meta_yml_exists``: The ``meta.yml`` file must exist. + + * ``meta_yml_valid``: The ``meta.yml`` must be valid according to the JSON + schema defined in ``modules/meta-schema.json`` in the nf-core/modules + repository. + + * ``meta_name``: The ``name`` field in ``meta.yml`` must match (case-insensitive) + the process name declared in ``main.nf``. + + * ``meta_input``: If ``main.nf`` declares inputs, they must be listed under + the ``input:`` key in ``meta.yml``. - The lint test checks that the module has - a ``meta.yml`` file and that it follows the - JSON schema defined in the ``modules/meta-schema.json`` - file in the nf-core/modules repository. + * ``correct_meta_inputs``: The inputs listed in ``meta.yml`` must exactly + match those parsed from ``main.nf``. Run ``nf-core modules lint --fix`` + to auto-correct. - In addition it checks that the module name - and module input is consistent between the - ``meta.yml`` and the ``main.nf``. + * ``meta_output``: If ``main.nf`` declares outputs, they must be listed under + the ``output:`` key in ``meta.yml``. - If the module has inputs or outputs, they are expected to be - formatted as: + * ``correct_meta_outputs``: The outputs listed in ``meta.yml`` must exactly + match those parsed from ``main.nf``. Run ``nf-core modules lint --fix`` + to auto-correct. + + * ``has_meta_topics``: If ``main.nf`` declares topics, ``meta.yml`` must + also contain a non-empty ``topics:`` block. Run + ``nf-core modules lint --fix`` to auto-correct. + + * ``correct_meta_topics``: The topics listed in ``meta.yml`` must exactly + match those parsed from ``main.nf``. Run ``nf-core modules lint --fix`` + to auto-correct. + + If the module has inputs or outputs, they are expected to be formatted as: .. code-block:: groovy @@ -42,10 +68,6 @@ def meta_yml(module_lint_object: ModuleLint, module: NFCoreComponent, allow_miss or permutations of the above. - Args: - module_lint_object (ComponentLint): The lint object for the module - module (NFCoreComponent): The module to lint - """ if module.meta_yml is None: if allow_missing: @@ -336,7 +358,7 @@ def obtain_outputs(_, outputs: dict | list) -> dict | list: if old_structure: outputs = {k: v for d in outputs for k, v in d.items()} assert isinstance(outputs, dict) # mypy - for channel_name in outputs.keys(): + for channel_name in outputs: output_channel = outputs[channel_name] channel_elements: list = [] for element in output_channel: @@ -367,7 +389,7 @@ def obtain_topics(_, topics: dict) -> dict: formatted_topics (dict): A dictionary containing the topics and their elements obtained from main.nf or meta.yml files. """ formatted_topics: dict = {} - for name in topics.keys(): + for name in topics: content = topics[name] t_elements: list = [] for element in content: diff --git a/nf_core/modules/lint/module_changes.py b/nf_core/modules/lint/module_changes.py index beb3855c28..de6f3d24c8 100644 --- a/nf_core/modules/lint/module_changes.py +++ b/nf_core/modules/lint/module_changes.py @@ -22,6 +22,11 @@ def module_changes(module_lint_object, module): compared against the files in the remote at this SHA. Only runs when linting a pipeline, not the modules repository + + The following checks are performed: + + * ``check_local_copy``: Each module file must be identical to the corresponding + file in the remote repository at the pinned commit SHA. """ if module.is_patched: # If the module is patched, we need to apply diff --git a/nf_core/modules/lint/module_deprecations.py b/nf_core/modules/lint/module_deprecations.py index dc232a81c3..e9d1070706 100644 --- a/nf_core/modules/lint/module_deprecations.py +++ b/nf_core/modules/lint/module_deprecations.py @@ -1,5 +1,4 @@ import logging -import os log = logging.getLogger(__name__) @@ -7,9 +6,14 @@ def module_deprecations(_, module): """ Check that the modules are up to the latest nf-core standard + + The following checks are performed: + + * ``module_deprecations``: Deprecated files (e.g. ``functions.nf``) must not + be present in the module directory. """ module.wf_path = module.component_dir - if "functions.nf" in os.listdir(module.component_dir): + if (module.component_dir / "functions.nf").exists(): module.failed.append( ( "module_deprecations", diff --git a/nf_core/modules/lint/module_tests.py b/nf_core/modules/lint/module_tests.py index 16939a767d..00031d8b62 100644 --- a/nf_core/modules/lint/module_tests.py +++ b/nf_core/modules/lint/module_tests.py @@ -14,11 +14,33 @@ def module_tests(_, module: NFCoreComponent, allow_missing: bool = False): - """ - Lint the tests of a module in ``nf-core/modules`` + """Lint the tests of a module in ``nf-core/modules`` + + Checks the ``tests/`` directory and ``main.nf.test`` file for correctness, + validates snapshot content, and verifies that nf-test tags follow guidelines. + + The following checks are performed: + + * ``test_dir_exists``: The nf-test directory ``tests/`` must exist. + + * ``test_main_nf_exists``: The file ``tests/main.nf.test`` must exist. + + * ``test_snapshot_exists``: If ``snapshot()`` is called in ``main.nf.test``, + the snapshot file ``tests/main.nf.test.snap`` must exist and be valid JSON. + + * ``test_snap_md5sum``: The snapshot must not contain md5sums for empty files + (``d41d8cd98f00b204e9800998ecf8427e``) or empty compressed files + (``7029066c27ac6f5ef18d660d5741979a``), unless the test name contains ``stub``. + + * ``test_snap_versions``: The snapshot must contain a ``versions`` key. - It verifies that the test directory exists - and contains a ``main.nf.test`` and a ``main.nf.test.snap`` + * ``test_main_tags``: The ``main.nf.test`` file must declare the required tags: + ``modules``, ``modules_``, the full component name, and (for ``tool/subtool`` + modules) the tool name alone. Tags for any chained components included via + ``include`` statements must also be present. + + * ``test_old_test_dir``: The legacy pytest directory + ``tests/modules//`` must not exist. """ if module.nftest_testdir is None: @@ -120,7 +142,7 @@ def module_tests(_, module: NFCoreComponent, allow_missing: bool = False): with open(snap_file) as snap_fh: try: snap_content = json.load(snap_fh) - for test_name in snap_content.keys(): + for test_name in snap_content: if "d41d8cd98f00b204e9800998ecf8427e" in str(snap_content[test_name]): if "stub" not in test_name: module.failed.append( @@ -213,6 +235,9 @@ def module_tests(_, module: NFCoreComponent, allow_missing: bool = False): snap_file, ) ) + # Check that stub blocks with gzip files use proper syntax + _check_stub_gzip_syntax(module) + # Verify that tags are correct. main_nf_tags = module._get_main_nf_tags(module.nftest_main_nf) not_alphabet = re.compile(r"[^a-zA-Z]") @@ -268,3 +293,65 @@ def module_tests(_, module: NFCoreComponent, allow_missing: bool = False): old_test_dir, ) ) + + +def _check_stub_gzip_syntax(module: NFCoreComponent): + """ + Linting Checks perfomed: + * ``test_stub_gzip_syntax``: + Check that stub blocks with gzip output files use the proper syntax. + Stub files ending in .gz must use: echo "" | gzip > file.gz + Simply touching or creating empty .gz files will break nf-test's gzip parser + """ + if not module.main_nf.is_file(): + return + + with open(module.main_nf) as fh: + content = fh.read() + + # Find all stub blocks (matches both """ and ''' style strings) + # Pattern matches: stub: followed by anything, then triple quotes, content, closing triple quotes + stub_pattern = re.compile(r'stub:.*?(?:"""|\'\'\')\s*(.*?)\s*(?:"""|\'\'\')', re.DOTALL) + stub_blocks = stub_pattern.findall(content) + + invalid_gz_patterns = [] + for stub_block in stub_blocks: + # Look for lines that create .gz files + # Only valid pattern is: echo "" | gzip > file.gz + # Each .gz creation is always on a single line + + # Find all lines where .gz is the final extension + gz_lines = re.findall(r"^.*\.gz\s*$", stub_block, re.MULTILINE) + + for line in gz_lines: + line = line.strip() + # Skip empty lines and comments + if not line or line.startswith(("//", "#")): + continue + + # The ONLY valid pattern is: echo "" | gzip > file.gz + valid_pattern = r'echo\s+""\s*\|\s*gzip\s*>\s*.*\.gz$' + + if not re.search(valid_pattern, line): + invalid_gz_patterns.append(line.strip()) + + if invalid_gz_patterns: + module.failed.append( + ( + "module_tests", + "test_stub_gzip_syntax", + f"Stub gzip files must use 'echo \"\" | gzip >' syntax. Invalid patterns found: {'; '.join(set(invalid_gz_patterns))}", + module.main_nf, + ) + ) + else: + # Only add passed if there are actually stub blocks with .gz files + if stub_blocks and any(".gz" in block for block in stub_blocks): + module.passed.append( + ( + "module_tests", + "test_stub_gzip_syntax", + "Stub gzip files use correct syntax", + module.main_nf, + ) + ) diff --git a/nf_core/modules/lint/module_todos.py b/nf_core/modules/lint/module_todos.py index ce8ec34976..e453e2ad6a 100644 --- a/nf_core/modules/lint/module_todos.py +++ b/nf_core/modules/lint/module_todos.py @@ -6,8 +6,7 @@ def module_todos(_, module): - """ - Look for TODO statements in the module files + """Look for TODO statements in the module files The nf-core module template contains a number of comment lines to help developers of new modules know where they need to edit files and add content. @@ -26,6 +25,9 @@ def module_todos(_, module): This lint test runs through all files in the module and searches for these lines. If any are found they will throw a warning. + * ``module_todo``: A warning is issued for each TODO comment found in the + module files. + .. tip:: Note that many GUI code editors have plugins to list all instances of *TODO* in a given project directory. This is a very quick and convenient way to get started on your pipeline! @@ -36,5 +38,5 @@ def module_todos(_, module): mod_results = pipeline_todos(None, root_dir=module.component_dir) for i, warning in enumerate(mod_results["warned"]): module.warned.append(("module_todos", "module_todo", warning, mod_results["file_paths"][i])) - for i, passed in enumerate(mod_results["passed"]): + for _i, passed in enumerate(mod_results["passed"]): module.passed.append(("module_todos", "module_todo", passed, module.component_dir)) diff --git a/nf_core/modules/lint/module_version.py b/nf_core/modules/lint/module_version.py index f8b975d5e9..5caef3f68d 100644 --- a/nf_core/modules/lint/module_version.py +++ b/nf_core/modules/lint/module_version.py @@ -5,10 +5,8 @@ import logging from pathlib import Path -import nf_core import nf_core.modules.lint import nf_core.modules.modules_repo -import nf_core.modules.modules_utils from nf_core.modules.modules_utils import NFCoreComponent log = logging.getLogger(__name__) @@ -21,6 +19,13 @@ def module_version(module_lint_object: "nf_core.modules.lint.ModuleLint", module It checks whether the module has an entry in the ``modules.json`` file containing a commit SHA. If that is true, it verifies that there are no newer version of the module available. + + The following checks are performed: + + * ``git_sha``: The module must have a ``git_sha`` entry in ``modules.json``. + + * ``module_version``: The module version must match the latest commit in the + remote repository. A warning is issued if a newer version is available. """ assert module_lint_object.modules_json is not None # mypy assert module.repo_url is not None # mypy diff --git a/nf_core/modules/modules_json.py b/nf_core/modules/modules_json.py index cf2834028f..716a40e8ea 100644 --- a/nf_core/modules/modules_json.py +++ b/nf_core/modules/modules_json.py @@ -42,12 +42,13 @@ class ModulesJson: An object for handling a 'modules.json' file in a pipeline """ - def __init__(self, pipeline_dir: str | Path) -> None: + def __init__(self, pipeline_dir: str | Path, no_prompts: bool = False) -> None: """ Initialise the object. Args: pipeline_dir (str): The pipeline directory + no_prompts (bool): Whether to disable interactive prompts """ self.directory = Path(pipeline_dir) self.modules_dir = self.directory / "modules" @@ -57,6 +58,12 @@ def __init__(self, pipeline_dir: str | Path) -> None: self.pipeline_modules = None self.pipeline_subworkflows = None self.pipeline_components: dict[str, list[tuple[str, str]]] | None = None + self.no_prompts: bool = no_prompts or not nf_core.utils.is_interactive() + + def require_prompts(self, msg: str) -> None: + """Raise UserWarning if prompts are disabled (via --no-prompts or non-interactive session).""" + if self.no_prompts: + raise UserWarning(f"{msg} and prompts are disabled.") def __str__(self): if self.modules_json is None: @@ -79,6 +86,7 @@ def create(self) -> None: new_modules_json = ModulesJsonType(name=pipeline_name, homePage=pipeline_url, repos={}) if not self.modules_dir.exists(): + self.require_prompts("Can't find a ./modules directory.\nPlease create the modules directory manually") if rich.prompt.Confirm.ask( "[bold][blue]?[/] Can't find a ./modules directory. Would you like me to create one?", default=True ): @@ -165,7 +173,7 @@ def get_pipeline_module_repositories( if repos is None: repos = {} # Check if there are any nf-core modules installed - if (directory / NF_CORE_MODULES_NAME).exists() and NF_CORE_MODULES_REMOTE not in repos.keys(): + if (directory / NF_CORE_MODULES_NAME).exists() and NF_CORE_MODULES_REMOTE not in repos: repos[NF_CORE_MODULES_REMOTE] = {} # The function might rename some directories, keep track of them renamed_dirs = {} @@ -175,12 +183,17 @@ def get_pipeline_module_repositories( if len(dirs_not_covered) > 0: log.info(f"Found custom {component_type[:-1]} repositories when creating 'modules.json'") # Loop until all directories in the base directory are covered by a remote + self.require_prompts( + f"Found untracked directories in '{component_type}'.\n" + f"Untracked: {', '.join(str(directory.relative_to(directory)) for directory in dirs_not_covered)}\n" + "Please configure these directories in your modules.json manually" + ) while len(dirs_not_covered) > 0: log.info( "The following director{s} in the {t} directory are untracked: '{l}'".format( s="ies" if len(dirs_not_covered) > 0 else "y", t=component_type, - l="', '".join(str(dir.relative_to(directory)) for dir in dirs_not_covered), + l="', '".join(str(d.relative_to(directory)) for d in dirs_not_covered), ) ) nrepo_remote = questionary.text( @@ -253,7 +266,7 @@ def dir_tree_uncovered(self, components_directory, repos): repos_at_level = {Path(*repo.parts[:depth]): len(repo.parts) for repo in repos} for directory in fifo: rel_dir = directory.relative_to(components_directory) - if rel_dir in repos_at_level.keys(): + if rel_dir in repos_at_level: # Go the next depth if this directory is not one of the repos if depth < repos_at_level[rel_dir]: temp_queue.extend(directory.iterdir()) @@ -327,6 +340,12 @@ def determine_branches_and_shas( log.info( f"Was unable to find matching {component_type[:-1]} files in the {modules_repo.branch} branch." ) + if self.no_prompts: + log.warning( + f"Unable to find matching {component_type[:-1]} files for '{component}' and prompts are disabled. Treating as local." + ) + sb_local.append(component) + break choices = [{"name": "No", "value": False}] + [ {"name": branch, "value": branch} for branch in (available_branches - tried_branches) ] @@ -449,6 +468,7 @@ def unsynced_components(self) -> tuple[list[str], list[str], dict]: # Add all modules from modules.json to missing_installation missing_installation = copy.deepcopy(self.modules_json["repos"]) # Obtain the path of all installed modules + # TODO: Use Path.parts instead of str().startswith() to avoid unnecessary string conversion module_dirs = [ Path(dir_name).relative_to(self.modules_dir) for dir_name, _, file_names in os.walk(self.modules_dir) @@ -457,6 +477,7 @@ def unsynced_components(self) -> tuple[list[str], list[str], dict]: untracked_dirs_modules, missing_installation = self.parse_dirs(module_dirs, missing_installation, "modules") # Obtain the path of all installed subworkflows + # TODO: Use Path.parts instead of str().startswith() to avoid unnecessary string conversion subworkflow_dirs = [ Path(dir_name).relative_to(self.subworkflows_dir) for dir_name, _, file_names in os.walk(self.subworkflows_dir) @@ -492,12 +513,14 @@ def parse_dirs(self, dirs: list[Path], missing_installation: dict, component_typ git_url = "" for repo in missing_installation: - if component_type in missing_installation[repo]: - if install_dir in missing_installation[repo][component_type]: - if component in missing_installation[repo][component_type][install_dir]: - component_in_file = True - git_url = repo - break + if ( + component_type in missing_installation[repo] + and install_dir in missing_installation[repo][component_type] + and component in missing_installation[repo][component_type][install_dir] + ): + component_in_file = True + git_url = repo + break if not component_in_file: # If it is not, add it to the list of missing components untracked_dirs.append(component) @@ -539,12 +562,7 @@ def has_git_url_and_modules(self) -> bool: elif ( not isinstance(repo_url, str) or repo_url == "" - or not ( - repo_url.startswith("http") - or repo_url.startswith("ftp") - or repo_url.startswith("ssh") - or repo_url.startswith("git") - ) + or not repo_url.startswith(("http", "ftp", "ssh", "git")) or not isinstance(repo_entry["modules"], dict) or repo_entry["modules"] == {} ): @@ -618,7 +636,7 @@ def check_up_to_date(self): for _, repo_entry in self.modules_json.get("repos", {}).items(): for component_type in ["modules", "subworkflows"]: if component_type in repo_entry: - for install_dir, install_dir_entry in repo_entry[component_type].items(): + for _install_dir, install_dir_entry in repo_entry[component_type].items(): for _, component in install_dir_entry.items(): if "installed_by" in component and isinstance(component["installed_by"], str): log.debug(f"Updating {component} in modules.json") @@ -640,12 +658,10 @@ def check_up_to_date(self): # we try to reinstall them if len(missing_installation) > 0: if "subworkflows" in [ - c_type for _, repo_content in missing_installation.items() for c_type in repo_content.keys() + c_type for _, repo_content in missing_installation.items() for c_type in repo_content ]: self.resolve_missing_installation(missing_installation, "subworkflows") - if "modules" in [ - c_type for _, repo_content in missing_installation.items() for c_type in repo_content.keys() - ]: + if "modules" in [c_type for _, repo_content in missing_installation.items() for c_type in repo_content]: self.resolve_missing_installation(missing_installation, "modules") # If some modules/subworkflows didn't have an entry in the 'modules.json' file @@ -697,10 +713,10 @@ def load(self) -> None: try: self.modules_json = json.load(fh) except json.JSONDecodeError as e: - raise UserWarning(f"Unable to load JSON file '{self.modules_json_path}' due to error {e}") + raise UserWarning(f"Unable to load JSON file '{self.modules_json_path}' due to error {e}") from e - except FileNotFoundError: - raise UserWarning("File 'modules.json' is missing") + except FileNotFoundError as e: + raise UserWarning("File 'modules.json' is missing") from e def update( self, @@ -889,7 +905,7 @@ def try_apply_patch_reverse(self, component_type, component, repo_name, patch_re except LookupError as e: raise LookupError( f"Failed to apply patch in reverse for {component_type[:-1]} '{component_fullname}' due to: {e}" - ) + ) from e # Write the patched files to a temporary directory log.debug("Writing patched files to tmpdir") @@ -1060,17 +1076,19 @@ def get_dependent_components( assert self.modules_json is not None # mypy component_types = ["modules"] if component_type == "modules" else ["modules", "subworkflows"] # Find all components that have an entry of install by of a given component, recursively call this function for subworkflows - for type in component_types: - for repo_url in self.modules_json["repos"].keys(): + for comp_type in component_types: + for repo_url in self.modules_json["repos"]: modules_repo = ModulesRepo(repo_url) install_dir = modules_repo.repo_path try: - for comp in self.modules_json["repos"][repo_url][type][install_dir]: - if name in self.modules_json["repos"][repo_url][type][install_dir][comp]["installed_by"]: - dependent_components[comp] = (repo_url, install_dir, type) + for comp in self.modules_json["repos"][repo_url][comp_type][install_dir]: + if name in self.modules_json["repos"][repo_url][comp_type][install_dir][comp]["installed_by"]: + dependent_components[comp] = (repo_url, install_dir, comp_type) except KeyError as e: # This exception will raise when there are only modules installed - log.debug(f"Trying to retrieve all {type}. There aren't {type} installed. Failed with error {e}") + log.debug( + f"Trying to retrieve all {comp_type}. There aren't {comp_type} installed. Failed with error {e}" + ) continue return dependent_components @@ -1178,7 +1196,7 @@ def resolve_missing_installation(self, missing_installation: dict, component_typ self.modules_json["repos"].pop(repo_url) def resolve_missing_from_modules_json(self, missing_from_modules_json, component_type): - format_missing = [f"'{dir}'" for dir in missing_from_modules_json] + format_missing = [f"'{d}'" for d in missing_from_modules_json] if len(format_missing) == 1: log.info( f"Recomputing commit SHA for {component_type[:-1]} {format_missing[0]} which was missing from 'modules.json'" @@ -1189,7 +1207,7 @@ def resolve_missing_from_modules_json(self, missing_from_modules_json, component ) assert self.modules_json is not None # mypy # Get the remotes we are missing - tracked_repos = {repo_url: (repo_entry) for repo_url, repo_entry in self.modules_json["repos"].items()} + tracked_repos = dict(self.modules_json["repos"].items()) repos, _ = self.get_pipeline_module_repositories(component_type, self.modules_dir, tracked_repos) # Get tuples of components that miss installation and their install directory @@ -1204,10 +1222,11 @@ def components_with_repos(): modules_repo.repo_path, ) for dir_name, _, _ in os.walk(repo_url_path): - if component_type == "modules": - if len(Path(directory).parts) > 1: # The module name is TOOL/SUBTOOL - paths_in_directory.append(str(Path(*Path(dir_name).parts[-2:]))) - pass + if ( + component_type == "modules" and len(Path(directory).parts) > 1 + ): # The module name is TOOL/SUBTOOL + paths_in_directory.append(str(Path(*Path(dir_name).parts[-2:]))) + pass paths_in_directory.append(Path(dir_name).parts[-1]) if directory in paths_in_directory: yield (modules_repo.repo_path, directory) @@ -1252,7 +1271,7 @@ def components_with_repos(): } ) - def recreate_dependencies(self, repo, org, subworkflow): + def recreate_dependencies(self, repo: str, org: str, subworkflow: dict[str, str]) -> None: """ Try to recreate the installed_by entries for subworkflows. Remove self installation entry from dependencies, assuming that the modules.json has been freshly created, @@ -1267,6 +1286,7 @@ def recreate_dependencies(self, repo, org, subworkflow): name = dep_mod["name"] current_repo = dep_mod.get("git_remote", repo) current_org = dep_mod.get("org_path", org) + assert current_repo is not None and current_org is not None installed_by = self.modules_json["repos"][current_repo]["modules"][current_org][name]["installed_by"] if installed_by == ["modules"]: self.modules_json["repos"][repo]["modules"][org][name]["installed_by"] = [] @@ -1277,6 +1297,7 @@ def recreate_dependencies(self, repo, org, subworkflow): name = dep_subwf["name"] current_repo = dep_subwf.get("git_remote", repo) current_org = dep_subwf.get("org_path", org) + assert current_repo is not None and current_org is not None installed_by = self.modules_json["repos"][current_repo]["subworkflows"][current_org][name]["installed_by"] if installed_by == ["subworkflows"]: self.modules_json["repos"][repo]["subworkflows"][org][name]["installed_by"] = [] diff --git a/nf_core/modules/modules_repo.py b/nf_core/modules/modules_repo.py index ec310c0001..b9ac2c815b 100644 --- a/nf_core/modules/modules_repo.py +++ b/nf_core/modules/modules_repo.py @@ -4,7 +4,6 @@ from pathlib import Path import git -import rich import rich.progress import rich.prompt from git.exc import GitCommandError, InvalidGitRepositoryError @@ -61,8 +60,8 @@ def __init__( raise UserWarning(f"Could not find a configuration file in {self.local_repo_dir}") try: self.repo_path = repo_config.org_path - except KeyError: - raise UserWarning(f"'org_path' key not present in {config_fn.name}") + except KeyError as e: + raise UserWarning(f"'org_path' key not present in {config_fn.name}") from e # Verify that the repo seems to be correctly configured if self.repo_path != NF_CORE_MODULES_NAME or self.branch: @@ -94,7 +93,7 @@ def setup_local_repo(self, remote, branch, hide_progress=True, in_cache=False): """ self.local_repo_dir = Path(NFCORE_DIR if not in_cache else NFCORE_CACHE_DIR, self.fullname) try: - if not os.path.exists(self.local_repo_dir): + if not self.local_repo_dir.exists(): try: pbar = rich.progress.Progress( "[bold blue]{task.description}", @@ -110,8 +109,8 @@ def setup_local_repo(self, remote, branch, hide_progress=True, in_cache=False): progress=RemoteProgressbar(pbar, self.fullname, self.remote_url, "Cloning"), ) ModulesRepo.update_local_repo_status(self.fullname, True) - except GitCommandError: - raise LookupError(f"Failed to clone from the remote: `{remote}`") + except GitCommandError as e: + raise LookupError(f"Failed to clone from the remote: `{remote}`") from e # Verify that the requested branch exists by checking it out self.setup_branch(branch) else: @@ -150,4 +149,4 @@ def setup_local_repo(self, remote, branch, hide_progress=True, in_cache=False): shutil.rmtree(self.local_repo_dir) self.setup_local_repo(remote, branch, hide_progress) else: - raise LookupError("Exiting due to error with local modules git repo") + raise LookupError("Exiting due to error with local modules git repo") from e diff --git a/nf_core/modules/modules_utils.py b/nf_core/modules/modules_utils.py index 105b5af681..7b38369e8c 100644 --- a/nf_core/modules/modules_utils.py +++ b/nf_core/modules/modules_utils.py @@ -1,5 +1,4 @@ import logging -import os from pathlib import Path from urllib.parse import urlparse @@ -32,9 +31,7 @@ def repo_full_name_from_remote(remote_url: str) -> str: path = urlparse(remote_url).path # Remove the file extension from the path - path, _ = os.path.splitext(path) - - return path + return str(Path(path).with_suffix("")) def get_installed_modules(directory: Path, repo_type="modules") -> tuple[list[str], list[NFCoreComponent]]: @@ -62,12 +59,11 @@ def get_installed_modules(directory: Path, repo_type="modules") -> tuple[list[st # Filter local modules if local_modules_dir.exists(): - local_modules = os.listdir(local_modules_dir) - local_modules = sorted([x for x in local_modules if x.endswith(".nf")]) + local_modules = sorted([x.name for x in local_modules_dir.iterdir() if x.suffix == ".nf"]) # Get nf-core modules if nfcore_modules_dir.exists(): - for m in sorted([m for m in nfcore_modules_dir.iterdir() if not m == "lib"]): + for m in sorted([m for m in nfcore_modules_dir.iterdir() if m != "lib"]): if not m.is_dir(): raise ModuleExceptionError( f"File found in '{nfcore_modules_dir}': '{m}'! This directory should only contain module directories." @@ -83,7 +79,7 @@ def get_installed_modules(directory: Path, repo_type="modules") -> tuple[list[st # Make full (relative) file paths and create NFCoreComponent objects if local_modules_dir: - local_modules = [os.path.join(local_modules_dir, m) for m in local_modules] + local_modules = [str(local_modules_dir / m) for m in local_modules] nfcore_modules = [ NFCoreComponent( @@ -110,13 +106,11 @@ def load_edam(): return edam_formats for line in response.content.splitlines(): fields = line.decode("utf-8").split("\t") - if fields[0].split("/")[-1].startswith("format"): - # We choose an already provided extension - if fields[14]: - extensions = fields[14].split("|") - for extension in extensions: - if extension not in edam_formats: - edam_formats[extension] = (fields[0], fields[1]) # URL, name + if fields[0].split("/")[-1].startswith("format") and fields[14]: # We choose an already provided extension + extensions = fields[14].split("|") + for extension in extensions: + if extension not in edam_formats: + edam_formats[extension] = (fields[0], fields[1]) # URL, name return edam_formats diff --git a/nf_core/modules/update.py b/nf_core/modules/update.py index f6cf5235a4..ed34b3e41c 100644 --- a/nf_core/modules/update.py +++ b/nf_core/modules/update.py @@ -16,6 +16,7 @@ def __init__( branch=None, no_pull=False, limit_output=False, + skip_deps=False, ): super().__init__( pipeline_dir, @@ -31,4 +32,5 @@ def __init__( branch, no_pull, limit_output, + skip_deps, ) diff --git a/nf_core/pipeline-template/.github/actions/get-shards/action.yml b/nf_core/pipeline-template/.github/actions/get-shards/action.yml index d2f98f85fd..c04bd28ad7 100644 --- a/nf_core/pipeline-template/.github/actions/get-shards/action.yml +++ b/nf_core/pipeline-template/.github/actions/get-shards/action.yml @@ -21,7 +21,7 @@ runs: using: "composite" steps: - name: Install nf-test - uses: nf-core/setup-nf-test@v1 + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 with: version: ${{ env.NFT_VER }} - name: Get number of shards diff --git a/nf_core/pipeline-template/.github/actions/nf-test/action.yml b/nf_core/pipeline-template/.github/actions/nf-test/action.yml index a9f33909e6..17137be3ab 100644 --- a/nf_core/pipeline-template/.github/actions/nf-test/action.yml +++ b/nf_core/pipeline-template/.github/actions/nf-test/action.yml @@ -20,7 +20,7 @@ runs: using: "composite" steps: - name: Setup Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 with: version: "{% raw %}${{ env.NXF_VERSION }}" @@ -30,7 +30,7 @@ runs: python-version: "3.14" - name: Install nf-test - uses: nf-core/setup-nf-test@v1 + uses: nf-core/setup-nf-test@4069fbbaabe94c08faba4ad261bfa88225ba133f # v2 with: version: "${{ env.NFT_VER }}" install-pdiff: true @@ -48,7 +48,7 @@ runs: - name: Conda setup if: contains(inputs.profile, 'conda') - uses: conda-incubator/setup-miniconda@fc2d68f6413eb2d87b895e92f8584b5b94a10167 # v3 + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4 with: auto-update-conda: true conda-solver: libmamba diff --git a/nf_core/pipeline-template/.github/workflows/awsfulltest.yml b/nf_core/pipeline-template/.github/workflows/awsfulltest.yml index cf076f5435..69d1baee71 100644 --- a/nf_core/pipeline-template/.github/workflows/awsfulltest.yml +++ b/nf_core/pipeline-template/.github/workflows/awsfulltest.yml @@ -23,7 +23,7 @@ jobs: echo "revision={%- raw -%}${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'release') && github.sha || 'dev' }}" >> "$GITHUB_OUTPUT" - name: Launch workflow via Seqera Platform - uses: seqeralabs/action-tower-launch@v2 + uses: seqeralabs/action-tower-launch@51565b514bff1827cf34620de25d0055759f1fc9 # v2 # TODO nf-core: You can customise AWS full pipeline tests as required # Add full size test data (but still relatively small datasets for few samples) # on the `test_full.config` test runs with only one set of parameters @@ -41,17 +41,16 @@ jobs: enabled = true bot { token = '{% raw %}${{ secrets.NFSLACK_BOT_TOKEN }}{% endraw %}' - # TODO nf-core: Set the Slack channel for CI notifications channel = '{{ short_name }}' } onStart { enabled = false } onComplete { - message = ':white_check_mark: *{{ short_name }}/{% raw %}${{ matrix.profile }}{% endraw %}* completed successfully! :tada:' + message = ':white_check_mark: *{{ short_name }}/test_full* completed successfully! :tada:' } onError { - message = ':x: *{{ short_name }}/{% raw %}${{ matrix.profile }}{% endraw %}* failed :crying_cat_face:' + message = ':x: *{{ short_name }}/test_full* failed :crying_cat_face:' } } parameters: | @@ -60,7 +59,7 @@ jobs: } profiles: test_full - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: Seqera Platform debug log file path: | diff --git a/nf_core/pipeline-template/.github/workflows/awstest.yml b/nf_core/pipeline-template/.github/workflows/awstest.yml index 3c8b4d8311..1c3f82f407 100644 --- a/nf_core/pipeline-template/.github/workflows/awstest.yml +++ b/nf_core/pipeline-template/.github/workflows/awstest.yml @@ -12,7 +12,7 @@ jobs: steps: # Launch workflow using Seqera Platform CLI tool action {%- raw %} - name: Launch workflow via Seqera Platform - uses: seqeralabs/action-tower-launch@v2 + uses: seqeralabs/action-tower-launch@51565b514bff1827cf34620de25d0055759f1fc9 # v2 with: workspace_id: ${{ vars.TOWER_WORKSPACE_ID }} access_token: ${{ secrets.TOWER_ACCESS_TOKEN }} @@ -25,7 +25,7 @@ jobs: } profiles: test - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: Seqera Platform debug log file path: | diff --git a/nf_core/pipeline-template/.github/workflows/branch.yml b/nf_core/pipeline-template/.github/workflows/branch.yml index 3cf6e66abb..27468bc072 100644 --- a/nf_core/pipeline-template/.github/workflows/branch.yml +++ b/nf_core/pipeline-template/.github/workflows/branch.yml @@ -21,7 +21,7 @@ jobs: # NOTE - this doesn't currently work if the PR is coming from a fork, due to limitations in GitHub actions secrets - name: Post PR comment if: failure() - uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3 + uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3 with: message: | ## This PR is against the `${{github.event.pull_request.base.ref}}` branch :x: diff --git a/nf_core/pipeline-template/.github/workflows/download_pipeline.yml b/nf_core/pipeline-template/.github/workflows/download_pipeline.yml index 110d2769a4..65354ea1d9 100644 --- a/nf_core/pipeline-template/.github/workflows/download_pipeline.yml +++ b/nf_core/pipeline-template/.github/workflows/download_pipeline.yml @@ -38,8 +38,11 @@ jobs: runs-on: ubuntu-latest needs: configure steps: + - name: Check out pipeline code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 - name: Disk space cleanup uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 @@ -52,13 +55,12 @@ jobs: - name: Setup Apptainer uses: eWaterCycle/setup-apptainer@4bb22c52d4f63406c49e94c804632975787312b3 # v2.0.0 with: - apptainer-version: 1.3.4 + apptainer-version: 1.3.4{% raw %} - name: Read .nf-core.yml - uses: pietrobolcato/action-read-yaml@9f13718d61111b69f30ab4ac683e67a56d254e1d # 1.1.0 id: read_yml - with: - config: ${{ github.workspace }}/.nf-core.yml + run: | + echo "nf_core_version=$(yq '.nf_core_version' ${{ github.workspace }}/.nf-core.yml)" >> "$GITHUB_OUTPUT" - name: Install dependencies run: | @@ -71,7 +73,7 @@ jobs: - name: Download the pipeline env: - NXF_SINGULARITY_CACHEDIR: ./singularity_container_images{% raw %} + NXF_SINGULARITY_CACHEDIR: ./singularity_container_images run: | nf-core pipelines download ${{ needs.configure.outputs.REPO_LOWERCASE }} \ --revision ${{ needs.configure.outputs.REPO_BRANCH }} \ @@ -133,7 +135,7 @@ jobs: fi{% endraw %} - name: Upload Nextflow logfile for debugging purposes - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: nextflow_logfile.txt path: .nextflow.log* diff --git a/nf_core/pipeline-template/.github/workflows/fix_linting.yml b/nf_core/pipeline-template/.github/workflows/fix_linting.yml index b39fbf49eb..383a24494c 100644 --- a/nf_core/pipeline-template/.github/workflows/fix_linting.yml +++ b/nf_core/pipeline-template/.github/workflows/fix_linting.yml @@ -31,22 +31,18 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.nf_core_bot_auth_token }} - # Install and run pre-commit - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 - with: - python-version: "3.14" - - - name: Install pre-commit - run: pip install pre-commit + - name: Install Nextflow + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 - - name: Run pre-commit - id: pre-commit - run: pre-commit run --all-files + # Install and run prek + - name: Run prek + id: prek + uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2 continue-on-error: true # indication that the linting has finished - name: react if linting finished succesfully - if: steps.pre-commit.outcome == 'success' + if: steps.prek.outcome == 'success' uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 with: comment-id: ${{ github.event.comment.id }} @@ -54,7 +50,7 @@ jobs: - name: Commit & push changes id: commit-and-push - if: steps.pre-commit.outcome == 'failure' + if: steps.prek.outcome == 'failure' run: | git config user.email "core@nf-co.re" git config user.name "nf-core-bot" diff --git a/nf_core/pipeline-template/.github/workflows/linting.yml b/nf_core/pipeline-template/.github/workflows/linting.yml index f54e06c5f5..8e5261f2fd 100644 --- a/nf_core/pipeline-template/.github/workflows/linting.yml +++ b/nf_core/pipeline-template/.github/workflows/linting.yml @@ -13,8 +13,11 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install Nextflow + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 + - name: Run prek - uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0 + uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2 nf-core: runs-on: ubuntu-latest @@ -23,7 +26,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Nextflow - uses: nf-core/setup-nextflow@v2 + uses: nf-core/setup-nextflow@b4ec1bc7c16a94435159de94a05253542fddf6ef # v3 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: @@ -31,7 +34,7 @@ jobs: architecture: "x64" - name: Setup uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: read .nf-core.yml uses: pietrobolcato/action-read-yaml@9f13718d61111b69f30ab4ac683e67a56d254e1d # 1.1.0 @@ -64,7 +67,7 @@ jobs: - name: Upload linting log file artifact if: ${{ always() }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: linting-logs path: | diff --git a/nf_core/pipeline-template/.github/workflows/linting_comment.yml b/nf_core/pipeline-template/.github/workflows/linting_comment.yml index 7951d28f42..c2923041b3 100644 --- a/nf_core/pipeline-template/.github/workflows/linting_comment.yml +++ b/nf_core/pipeline-template/.github/workflows/linting_comment.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download lint results - uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19 + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 with: workflow: linting.yml workflow_conclusion: completed diff --git a/nf_core/pipeline-template/.github/workflows/nf-test.yml b/nf_core/pipeline-template/.github/workflows/nf-test.yml index 28471884ad..a75dd68913 100644 --- a/nf_core/pipeline-template/.github/workflows/nf-test.yml +++ b/nf_core/pipeline-template/.github/workflows/nf-test.yml @@ -80,7 +80,7 @@ jobs: - isMain: false profile: "singularity" NXF_VER: - - "25.04.0" + - "25.10.4" - "latest-everything" env: NXF_ANSI_LOG: false diff --git a/nf_core/pipeline-template/.github/workflows/template-version-comment.yml b/nf_core/pipeline-template/.github/workflows/template-version-comment.yml index 08ca7817a6..cca374be84 100644 --- a/nf_core/pipeline-template/.github/workflows/template-version-comment.yml +++ b/nf_core/pipeline-template/.github/workflows/template-version-comment.yml @@ -29,7 +29,7 @@ jobs: run: echo "OUTPUT=$(pip list --outdated | grep nf-core)" >> ${GITHUB_ENV} - name: Post nf-core template version comment - uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3 + uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3 if: | contains(env.OUTPUT, 'nf-core') with: @@ -42,5 +42,5 @@ jobs: > Your pipeline is using an old version of the nf-core template: ${{ steps.read_yml.outputs['nf_core_version'] }}. > Please update your pipeline to the latest version. > - > For more documentation on how to update your pipeline, please see the [nf-core documentation](https://github.com/nf-core/tools?tab=readme-ov-file#sync-a-pipeline-with-the-template) and [Synchronisation documentation](https://nf-co.re/docs/contributing/sync). + > For more documentation on how to update your pipeline, please see the [Synchronisation documentation](https://nf-co.re/docs/developing/template-syncs/overview). #{%- endraw %} diff --git a/nf_core/pipeline-template/.pre-commit-config.yaml b/nf_core/pipeline-template/.pre-commit-config.yaml index b1e5f80e51..f51e1a28da 100644 --- a/nf_core/pipeline-template/.pre-commit-config.yaml +++ b/nf_core/pipeline-template/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.8.1 + - prettier@3.8.3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: diff --git a/nf_core/pipeline-template/README.md b/nf_core/pipeline-template/README.md index 584eebaba4..57908aa578 100644 --- a/nf_core/pipeline-template/README.md +++ b/nf_core/pipeline-template/README.md @@ -21,7 +21,7 @@ [![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX) [![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com) -[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) +[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.10.4-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) [![nf-core template version](https://img.shields.io/badge/nf--core_template-{{ nf_core_version }}-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/{{ nf_core_version }}) [![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/) [![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/) @@ -47,7 +47,7 @@ --> + workflows use the "tube map" design for that. See https://nf-co.re/docs/community/brand/workflow-schematics#examples for examples. --> {%- if fastqc %}1. Read QC ([`FastQC`](https://www.bioinformatics.babraham.ac.uk/projects/fastqc/)){% endif %} @@ -56,7 +56,7 @@ ## Usage > [!NOTE] -> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. {% if test_config %}Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data.{% endif %} +> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/get_started/environment_setup/overview) on how to set-up Nextflow. {% if test_config %}Make sure to [test your setup](https://nf-co.re/docs/get_started/run-your-first-pipeline) with `-profile test` before running the workflow on actual data.{% endif %} try to fix it before schema validation try: wf_json["on"] = wf_json.pop(True) - except Exception: + except KeyError: failed.append(f"Missing 'on' keyword in {wf}") # Validate the workflow try: jsonschema.validate(wf_json, schema) passed.append(f"Workflow validation passed: {wf}") - except Exception as e: + except jsonschema.ValidationError as e: failed.append(f"Workflow validation failed for {wf}: {e}") return {"passed": passed, "failed": failed, "warned": warned} diff --git a/nf_core/pipelines/lint/configs.py b/nf_core/pipelines/lint/configs.py index c90b5faec1..05b4bde59e 100644 --- a/nf_core/pipelines/lint/configs.py +++ b/nf_core/pipelines/lint/configs.py @@ -30,7 +30,7 @@ def lint_file(self, lint_name: str, file_path: Path) -> dict[str, list[str]]: try: with open(fn) as fh: config = fh.read() - except Exception as e: + except OSError as e: return {"failed": [f"Could not parse file: {fn}, {e}"]} # find sections with a withName: prefix diff --git a/nf_core/pipelines/lint/container_configs.py b/nf_core/pipelines/lint/container_configs.py new file mode 100644 index 0000000000..b5143dba82 --- /dev/null +++ b/nf_core/pipelines/lint/container_configs.py @@ -0,0 +1,78 @@ +import logging +from pathlib import Path + +import git + +from nf_core.pipelines.containers_utils import ContainerConfigs + +log = logging.getLogger(__name__) + + +def container_configs(self): + """Check that the container configuration files in ``conf/`` are up to date. + + Runs ``nextflow inspect`` to regenerate container configuration files directly + in ``conf/`` and uses ``git diff`` to detect changes. If not in ``--fix`` mode + the working tree is restored to its original state afterwards. + + Can be skipped by adding the following to the ``.nf-core.yml`` file: + + .. code-block:: yaml + + lint: + container_configs: False + """ + passed = [] + failed = [] + warned = [] + fixed = [] + could_fix = False + + conf_dir = Path(self.wf_path) / "conf" + repo = git.Repo(self.wf_path) + + try: + generated = ContainerConfigs(self.wf_path).generate_container_configs() + except UserWarning as e: + warned.append(f"Could not generate container configuration files: {e}") + return {"passed": passed, "failed": failed, "warned": warned} + + # Files modified in the working tree (tracked and changed by generation) + modified = { + Path(d.a_path).name + for d in repo.index.diff(None) + if d.a_path and Path(d.a_path).parent.name == "conf" and Path(d.a_path).name.startswith("containers_") + } + # Newly created files (generated but not previously tracked) + new = { + Path(f).name + for f in repo.untracked_files + if Path(f).parent.name == "conf" and Path(f).name.startswith("containers_") + } + # Already-correct files: generated, tracked, and unchanged + correct = generated - modified - new + + fixing = "container_configs" in self.fix + + for name in sorted(correct): + passed.append(f"`conf/{name}` is up to date") + + for name in sorted(modified | new): + if fixing: + passed.append(f"`conf/{name}` is up to date") + fixed.append(f"`conf/{name}` overwritten with regenerated container configuration.") + else: + if name in new: + failed.append(f"`conf/{name}` is missing – please regenerate the container configuration files.") + else: + failed.append(f"`conf/{name}` is out of date – please regenerate the container configuration files.") + could_fix = True + + if not fixing: + # Restore working tree: reset modified tracked files and delete new untracked ones + for name in modified: + repo.git.restore(str(conf_dir / name)) + for name in new: + (conf_dir / name).unlink(missing_ok=True) + + return {"passed": passed, "failed": failed, "warned": warned, "fixed": fixed, "could_fix": could_fix} diff --git a/nf_core/pipelines/lint/files_exist.py b/nf_core/pipelines/lint/files_exist.py index e99b97c246..202906c184 100644 --- a/nf_core/pipelines/lint/files_exist.py +++ b/nf_core/pipelines/lint/files_exist.py @@ -224,18 +224,18 @@ def pf(file_path: str | Path) -> Path: # Files that cause an error if they don't exist for files in files_fail: - if any([str(f) in ignore_files for f in files]): + if any(str(f) in ignore_files for f in files): continue - if any([pf(f).is_file() for f in files]): + if any(pf(f).is_file() for f in files): passed.append(f"File found: {self._wrap_quotes(files)}") else: failed.append(f"File not found: {self._wrap_quotes(files)}") # Files that cause a warning if they don't exist for files in files_warn: - if any([str(f) in ignore_files for f in files]): + if any(str(f) in ignore_files for f in files): continue - if any([pf(f).is_file() for f in files]): + if any(pf(f).is_file() for f in files): passed.append(f"File found: {self._wrap_quotes(files)}") else: hint = "" diff --git a/nf_core/pipelines/lint/files_unchanged.py b/nf_core/pipelines/lint/files_unchanged.py index 8ff61171d3..4f9fe3fcd2 100644 --- a/nf_core/pipelines/lint/files_unchanged.py +++ b/nf_core/pipelines/lint/files_unchanged.py @@ -1,6 +1,5 @@ import filecmp import logging -import os import re import shutil import tempfile @@ -156,11 +155,11 @@ def _tf(file_path: str | Path) -> Path: # Files that must be completely unchanged from template for files in files_exact: # Ignore if file specified in linting config - if any([str(f) in ignore_files for f in files]): + if any(str(f) in ignore_files for f in files): ignored.append(f"File ignored due to lint config: {self._wrap_quotes(files)}") # Ignore if we can't find the file - elif not any([_pf(f).is_file() for f in files]): + elif not any(_pf(f).is_file() for f in files): ignored.append(f"File does not exist: {self._wrap_quotes(files)}") # Check that the file has an identical match @@ -170,8 +169,8 @@ def _tf(file_path: str | Path) -> Path: if filecmp.cmp(_pf(f), _tf(f), shallow=True): passed.append(f"`{f}` matches the template") else: - if f.name.endswith(".png") and int(os.stat(_pf(f)).st_size / 500) == int( - os.stat(_tf(f)).st_size / 500 + if f.name.endswith(".png") and int(_pf(f).stat().st_size / 500) == int( + _tf(f).stat().st_size / 500 ): # almost the same file, good enough for the logo log.debug(f"Files are almost the same. Will pass: {f}") @@ -196,11 +195,11 @@ def _tf(file_path: str | Path) -> Path: # Files that can be added to, but that must contain the template contents for files in files_partial: # Ignore if file specified in linting config - if any([str(f) in ignore_files for f in files]): + if any(str(f) in ignore_files for f in files): ignored.append(f"File ignored due to lint config: {self._wrap_quotes(files)}") # Ignore if we can't find the file - elif not any([_pf(f).is_file() for f in files]): + elif not any(_pf(f).is_file() for f in files): ignored.append(f"File does not exist: {self._wrap_quotes(files)}") # Check that the file contains the template file contents diff --git a/nf_core/pipelines/lint/modules_json.py b/nf_core/pipelines/lint/modules_json.py index dc9faadf5d..f7f5a077e7 100644 --- a/nf_core/pipelines/lint/modules_json.py +++ b/nf_core/pipelines/lint/modules_json.py @@ -24,7 +24,7 @@ def modules_json(self) -> dict[str, list[str]]: if _modules_json and modules_json_dict is not None: all_modules_passed = True - for repo in modules_json_dict["repos"].keys(): + for repo in modules_json_dict["repos"]: # Check if the modules.json has been updated to keep the if "modules" not in modules_json_dict["repos"][repo] or not repo.startswith("http"): failed.append( @@ -33,18 +33,22 @@ def modules_json(self) -> dict[str, list[str]]: ) continue - for dir in modules_json_dict["repos"][repo]["modules"].keys(): - for module, module_entry in modules_json_dict["repos"][repo]["modules"][dir].items(): - if not Path(modules_dir, dir, module).exists(): + for install_dir in modules_json_dict["repos"][repo]["modules"]: + for module, module_entry in modules_json_dict["repos"][repo]["modules"][install_dir].items(): + if not Path(modules_dir, install_dir, module).exists(): failed.append( - f"Entry for `{Path(modules_dir, dir, module)}` found in `modules.json` but module is not installed in " + f"Entry for `{Path(modules_dir, install_dir, module)}` found in `modules.json` but module is not installed in " "pipeline." ) all_modules_passed = False if module_entry.get("branch") is None: - failed.append(f"Entry for `{Path(modules_dir, dir, module)}` is missing branch information.") + failed.append( + f"Entry for `{Path(modules_dir, install_dir, module)}` is missing branch information." + ) if module_entry.get("git_sha") is None: - failed.append(f"Entry for `{Path(modules_dir, dir, module)}` is missing version information.") + failed.append( + f"Entry for `{Path(modules_dir, install_dir, module)}` is missing version information." + ) if all_modules_passed: passed.append("Only installed modules found in `modules.json`") else: diff --git a/nf_core/pipelines/lint/multiqc_config.py b/nf_core/pipelines/lint/multiqc_config.py index 01b6b06dec..254caa925a 100644 --- a/nf_core/pipelines/lint/multiqc_config.py +++ b/nf_core/pipelines/lint/multiqc_config.py @@ -57,7 +57,7 @@ def multiqc_config(self) -> dict[str, list[str]]: try: with open(fn) as fh: mqc_yml = yaml.safe_load(fh) - except Exception as e: + except (OSError, yaml.YAMLError) as e: return {"failed": [f"Could not parse yaml file: {fn}, {e}"]} # check if required sections are present @@ -137,7 +137,7 @@ def multiqc_config(self) -> dict[str, list[str]]: # Check that export_plots is activated try: if not mqc_yml["export_plots"]: - raise AssertionError() + raise AssertionError except (AssertionError, KeyError, TypeError): failed.append("`assets/multiqc_config.yml` does not contain 'export_plots: true'.") else: diff --git a/nf_core/pipelines/lint/nextflow_config.py b/nf_core/pipelines/lint/nextflow_config.py index 0020c97749..f5913f5521 100644 --- a/nf_core/pipelines/lint/nextflow_config.py +++ b/nf_core/pipelines/lint/nextflow_config.py @@ -208,7 +208,7 @@ def nextflow_config(self) -> dict[str, list[str]]: if cf in ignore_configs: ignored.append(f"Config variable ignored: {self._wrap_quotes(cf)}") break - if cf in self.nf_config.keys(): + if cf in self.nf_config: passed.append(f"Config variable found: {self._wrap_quotes(cf)}") break else: @@ -218,7 +218,7 @@ def nextflow_config(self) -> dict[str, list[str]]: if cf in ignore_configs: ignored.append(f"Config variable ignored: {self._wrap_quotes(cf)}") break - if cf in self.nf_config.keys(): + if cf in self.nf_config: passed.append(f"Config variable found: {self._wrap_quotes(cf)}") break else: @@ -227,7 +227,7 @@ def nextflow_config(self) -> dict[str, list[str]]: if cf in ignore_configs: ignored.append(f"Config variable ignored: {self._wrap_quotes(cf)}") break - if cf not in self.nf_config.keys(): + if cf not in self.nf_config: passed.append(f"Config variable (correctly) not found: {self._wrap_quotes(cf)}") else: failed.append(f"Config variable (incorrectly) found: {self._wrap_quotes(cf)}") @@ -235,13 +235,7 @@ def nextflow_config(self) -> dict[str, list[str]]: # Check and warn if the process configuration is done with deprecated syntax process_with_deprecated_syntax = list( - set( - [ - match.group(1) - for ck in self.nf_config.keys() - if (match := re.match(r"^(process\.\$.*?)\.+.*$", ck)) is not None - ] - ) + {match.group(1) for ck in self.nf_config if (match := re.match(r"^(process\.\$.*?)\.+.*$", ck)) is not None} ) for pd in process_with_deprecated_syntax: warned.append(f"Process configuration is done with deprecated_syntax: {pd}") @@ -265,7 +259,7 @@ def nextflow_config(self) -> dict[str, list[str]]: try: manifest_name = self.nf_config.get("manifest.name", "").strip("'\"") if not manifest_name.startswith(f"{org_name}/"): - raise AssertionError() + raise AssertionError except (AssertionError, IndexError): failed.append(f"Config ``manifest.name`` did not begin with ``{org_name}/``:\n {manifest_name}") else: @@ -276,7 +270,7 @@ def nextflow_config(self) -> dict[str, list[str]]: try: manifest_homepage = self.nf_config.get("manifest.homePage", "").strip("'\"") if not manifest_homepage.startswith(f"https://github.com/{org_name}/"): - raise AssertionError() + raise AssertionError except (AssertionError, IndexError): failed.append( f"Config variable ``manifest.homePage`` did not begin with https://github.com/{org_name}/:\n {manifest_homepage}" @@ -415,11 +409,11 @@ def nextflow_config(self) -> dict[str, list[str]]: schema.load_schema() schema.get_schema_defaults() # Get default values from schema schema.get_schema_types() # Get types from schema - for param_name in schema.schema_defaults.keys(): + for param_name in schema.schema_defaults: param = "params." + param_name if param in ignore_defaults: ignored.append(f"Config default ignored: {param}") - elif param in self.nf_config.keys(): + elif param in self.nf_config: config_default: str | float | int | None = None schema_default: str | float | int | None = None if schema.schema_types[param_name] == "boolean": diff --git a/nf_core/pipelines/lint/nf_test_content.py b/nf_core/pipelines/lint/nf_test_content.py index 045c27f540..b36c36c8bf 100644 --- a/nf_core/pipelines/lint/nf_test_content.py +++ b/nf_core/pipelines/lint/nf_test_content.py @@ -106,7 +106,7 @@ def nf_test_content(self) -> dict[str, list[str]]: ignored.append(f"'{test_fn.relative_to(self.wf_path)}' checking ignored") continue - checks_passed = {check: False for check in test_checks} + checks_passed = dict.fromkeys(test_checks, False) with open(test_fn) as fh: for line in fh: for check_name, check_info in test_checks.items(): @@ -145,7 +145,7 @@ def nf_test_content(self) -> dict[str, list[str]]: failed.append(f"'{conf_fn.relative_to(self.wf_path)}' does not exist") else: if nf_test_content_conf is None or str(conf_fn.relative_to(self.wf_path)) not in nf_test_content_conf: - checks_passed = {check: False for check in config_checks} + checks_passed = dict.fromkeys(config_checks, False) with open(conf_fn) as fh: for line in fh: line = line.strip() @@ -167,19 +167,19 @@ def nf_test_content(self) -> dict[str, list[str]]: nf_test_conf_fn = Path(self.wf_path, "nf-test.config") nf_test_checks: dict[str, dict[str, str]] = { "testsDir": { - "pattern": r'testsDir "\."', + "pattern": r'testsDir\s*=?\s*"\."', "description": "sets a `testsDir`", - "failure_msg": 'does not set a `testsDir`, it should contain `testsDir "."`', + "failure_msg": 'does not set a `testsDir`, it should contain `testsDir = "."`.', }, "workDir": { - "pattern": r'workDir System\.getenv\("NFT_WORKDIR"\) \?: "\.nf-test"', + "pattern": r'workDir\s*=?\s*System\.getenv\("NFT_WORKDIR"\) \?: "\.nf-test"', "description": "sets a `workDir`", - "failure_msg": 'does not set a `workDir`, it should contain `workDir System.getenv("NFT_WORKDIR") ?: ".nf-test"`', + "failure_msg": 'does not set a `workDir`, it should contain `workDir = System.getenv("NFT_WORKDIR") ?: ".nf-test"`.', }, "configFile": { - "pattern": r'configFile "tests/nextflow\.config"', + "pattern": r'configFile\s*=?\s*"tests/nextflow\.config"', "description": "sets a `configFile`", - "failure_msg": 'does not set a `configFile`, it should contain `configFile "tests/nextflow.config"`', + "failure_msg": 'does not set a `configFile`, it should contain `configFile = "tests/nextflow.config"`.', }, } @@ -187,7 +187,7 @@ def nf_test_content(self) -> dict[str, list[str]]: failed.append(f"'{nf_test_conf_fn.relative_to(self.wf_path)}' does not exist") else: if nf_test_content_conf is None or str(nf_test_conf_fn.relative_to(self.wf_path)) not in nf_test_content_conf: - checks_passed = {check: False for check in nf_test_checks} + checks_passed = dict.fromkeys(nf_test_checks, False) with open(nf_test_conf_fn) as fh: for line in fh: line = line.strip() diff --git a/nf_core/pipelines/lint/plugin_includes.py b/nf_core/pipelines/lint/plugin_includes.py index a774192769..b787405169 100644 --- a/nf_core/pipelines/lint/plugin_includes.py +++ b/nf_core/pipelines/lint/plugin_includes.py @@ -1,7 +1,7 @@ import ast -import glob import logging import re +from pathlib import Path log = logging.getLogger(__name__) @@ -22,20 +22,19 @@ def plugin_includes(self) -> dict[str, list[str]]: plugin_include_pattern = re.compile(r"^include\s*{[^}]+}\s*from\s*[\"']plugin/([^\"']+)[\"']\s*$", re.MULTILINE) workflow_files = [ - file for file in glob.glob(f"{self.wf_path}/**/*.nf", recursive=True) if not file.startswith("./modules/") + file for file in Path(self.wf_path).rglob("*.nf") if file.relative_to(self.wf_path).parts[0] != "modules" ] test_passed = True for file in workflow_files: - with open(file) as of: - plugin_includes = re.findall(plugin_include_pattern, of.read()) - for include in plugin_includes: - if include not in ["nf-validation", "nf-schema"]: - continue - if include != validation_plugin: - test_passed = False - failed.append( - f"Found a `{include}` plugin import in `{file[2:]}`, but `{validation_plugin}` was used in `nextflow.config`" - ) + plugin_includes = re.findall(plugin_include_pattern, file.read_text()) + for include in plugin_includes: + if include not in ["nf-validation", "nf-schema"]: + continue + if include != validation_plugin: + test_passed = False + failed.append( + f"Found a `{include}` plugin import in `{file.relative_to(self.wf_path)}`, but `{validation_plugin}` was used in `nextflow.config`" + ) if test_passed: passed.append("No wrong validation plugin imports have been found") diff --git a/nf_core/pipelines/lint/readme.py b/nf_core/pipelines/lint/readme.py index d16125021c..57a3dfa3a2 100644 --- a/nf_core/pipelines/lint/readme.py +++ b/nf_core/pipelines/lint/readme.py @@ -38,7 +38,7 @@ def readme(self): readme: - nextflow_badge - nfcore_template_badge - - zenodo_release + - zenodo_doi """ passed = [] @@ -53,7 +53,7 @@ def readme(self): if "nextflow_badge" not in ignore_configs: # Check that there is a readme badge showing the minimum required version of Nextflow - # [![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) + # [![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.10.4-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) # and that it has the correct version nf_badge_re = r"\[!\[Nextflow\]\(https://img\.shields\.io/badge/version-!?(?:%E2%89%A5|%3E%3D)([\d\.]+)-green\?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow\.io\)\]\(https://www\.nextflow\.io/\)" match = re.search(nf_badge_re, content) @@ -61,7 +61,7 @@ def readme(self): nf_badge_version = match.group(1).strip("'\"") try: if nf_badge_version != self.minNextflowVersion: - raise AssertionError() + raise AssertionError except (AssertionError, KeyError): failed.append( f"README Nextflow minimum version badge does not match config. Badge: `{nf_badge_version}`, " diff --git a/nf_core/pipelines/lint/schema_description.py b/nf_core/pipelines/lint/schema_description.py index b586cc5242..8571f7832d 100644 --- a/nf_core/pipelines/lint/schema_description.py +++ b/nf_core/pipelines/lint/schema_description.py @@ -27,7 +27,7 @@ def schema_description(self): ignore_params = self.lint_config.get("schema_description", []) if self.lint_config is not None else [] # Get ungrouped params - if "properties" in self.schema_obj.schema.keys(): + if "properties" in self.schema_obj.schema: ungrouped_params = self.schema_obj.schema["properties"].keys() for up in ungrouped_params: if up in ignore_params: @@ -37,13 +37,13 @@ def schema_description(self): # Iterate over groups and add warning for parameters without a description defs_notation = self.schema_obj.defs_notation - for group_key in self.schema_obj.schema[defs_notation].keys(): + for group_key in self.schema_obj.schema[defs_notation]: group = self.schema_obj.schema[defs_notation][group_key] for param_key, param in group["properties"].items(): if param_key in ignore_params: ignored.append(f"Ignoring description check for param in schema: `{param_key}`") continue - if "description" not in param.keys(): + if "description" not in param: warned.append(f"No description provided in schema for parameter: `{param_key}`") for ip in ignore_params: diff --git a/nf_core/pipelines/lint/system_exit.py b/nf_core/pipelines/lint/system_exit.py index 435a2452d0..b891fea7a0 100644 --- a/nf_core/pipelines/lint/system_exit.py +++ b/nf_core/pipelines/lint/system_exit.py @@ -18,8 +18,8 @@ def system_exit(self): root_dir = Path(self.wf_path) # Get all groovy and nf files - groovy_files = [f for f in root_dir.rglob("*.groovy")] - nf_files = [f for f in root_dir.rglob("*.nf")] + groovy_files = list(root_dir.rglob("*.groovy")) + nf_files = list(root_dir.rglob("*.nf")) to_check = nf_files + groovy_files for file in to_check: diff --git a/nf_core/pipelines/lint/template_strings.py b/nf_core/pipelines/lint/template_strings.py index 0cb669e553..01f4ea5f2b 100644 --- a/nf_core/pipelines/lint/template_strings.py +++ b/nf_core/pipelines/lint/template_strings.py @@ -50,17 +50,16 @@ def template_strings(self): # Skip binary files binary_ftypes = ["image", "application/java-archive"] (ftype, encoding) = mimetypes.guess_type(fn) - if encoding is not None or (ftype is not None and any([ftype.startswith(ft) for ft in binary_ftypes])): + if encoding is not None or (ftype is not None and any(ftype.startswith(ft) for ft in binary_ftypes)): continue with open(fn, encoding="latin1") as fh: - lnum = 0 - for line in fh: - lnum += 1 + for i, line in enumerate(fh): + i += 1 cc_matches = re.findall(r"[^$]{{[^:}]*}}", line) if len(cc_matches) > 0: for cc_match in cc_matches: - failed.append(f"Found a Jinja template string in `{fn}` L{lnum}: {cc_match}") + failed.append(f"Found a Jinja template string in `{fn}` L{i}: {cc_match}") num_matches += 1 if num_matches == 0: passed.append(f"Did not find any Jinja template strings ({len(self.files)} files)") diff --git a/nf_core/pipelines/lint/version_consistency.py b/nf_core/pipelines/lint/version_consistency.py index e344d25e5d..c939938a46 100644 --- a/nf_core/pipelines/lint/version_consistency.py +++ b/nf_core/pipelines/lint/version_consistency.py @@ -47,7 +47,7 @@ def version_consistency(self): os.environ.get("GITHUB_REF", "").startswith("refs/tags/") and os.environ.get("GITHUB_REPOSITORY", "") != "nf-core/tools" ): - versions["GITHUB_REF"] = os.path.basename(os.environ["GITHUB_REF"].strip(" '\"")) + versions["GITHUB_REF"] = Path(os.environ["GITHUB_REF"].strip(" '\"")).name # Get version from the .nf-core.yml template yaml = YAML() diff --git a/nf_core/pipelines/lint_utils.py b/nf_core/pipelines/lint_utils.py index b9ff9533d3..e1980cf89d 100644 --- a/nf_core/pipelines/lint_utils.py +++ b/nf_core/pipelines/lint_utils.py @@ -1,8 +1,10 @@ import json import logging import subprocess +import sys from pathlib import Path +import git import rich import yaml from rich.console import Console @@ -132,9 +134,9 @@ def print_fixes(lint_obj, plain_text=False): def check_git_repo() -> bool: """Check if the current directory is a git repository.""" try: - subprocess.check_output(["git", "rev-parse", "--is-inside-work-tree"]) + git.Repo(search_parent_directories=True) return True - except subprocess.CalledProcessError: + except git.InvalidGitRepositoryError: return False @@ -151,7 +153,13 @@ def run_prettier_on_file(file: Path | str | list[str]) -> None: is_git = check_git_repo() nf_core_pre_commit_config = Path(nf_core.__file__).parent / ".pre-commit-prettier-config.yaml" - args = ["pre-commit", "run", "--config", str(nf_core_pre_commit_config), "prettier"] + # Resolve prek relative to the current interpreter so it works in isolated + # environments (uv tool, pipx, conda) where only the main entry point is on + # PATH. Fall back to the bare name for system-wide pip installs where the + # scripts directory differs from the interpreter location. + prek_bin = Path(sys.executable).parent / "prek" + prek = str(prek_bin) if prek_bin.exists() else "prek" + args = [prek, "run", "--config", str(nf_core_pre_commit_config), "prettier"] if isinstance(file, list): args.extend(["--files", *file]) else: @@ -165,7 +173,7 @@ def run_prettier_on_file(file: Path | str | list[str]) -> None: if ": SyntaxError: " in e.stdout.decode(): log.critical(f"Can't format {file} because it has a syntax error.\n{e.stdout.decode()}") elif "files were modified by this hook" in e.stdout.decode(): - all_lines = [line for line in e.stdout.decode().split("\n")] + all_lines = list(e.stdout.decode().split("\n")) files = "\n".join(all_lines[3:]) log.debug(f"The following files were modified by prettier:\n {files}") else: diff --git a/nf_core/pipelines/list.py b/nf_core/pipelines/list.py index 5822557d64..bc9bf30205 100644 --- a/nf_core/pipelines/list.py +++ b/nf_core/pipelines/list.py @@ -10,7 +10,6 @@ import git import requests -import rich.console import rich.table from click.shell_completion import CompletionItem @@ -22,6 +21,50 @@ nf_core.utils.setup_requests_cachedir() +def _get_nextflow_assets_dir() -> Path: + """Return the Nextflow assets directory used for local workflow caches.""" + nxf_assets = os.environ.get("NXF_ASSETS") + if nxf_assets: + base = Path(nxf_assets) + elif nxf_home := os.environ.get("NXF_HOME"): + base = Path(nxf_home, "assets") + else: + base = Path(os.environ.get("HOME", ""), ".nextflow", "assets") + + # Newer Nextflow versions store clones under assets/.repos/ + repos_dir = base / ".repos" + if repos_dir.is_dir(): + return repos_dir + return base + + +def _resolve_wf_path(path: Path) -> Path: + """Resolve the actual pipeline working tree for a given assets dir / workflow path. + + Nextflow 26.04+ uses a worktree layout under .repos: + //clones// ← working tree + //bare/ ← bare git repo + Prefers the clone matching the bare repo's HEAD; falls back to the most + recently modified clone if HEAD's clone is missing or the bare repo is unreadable. + Returns path unchanged for the old (pre-26.04) flat layout. + """ + clones_dir = path / "clones" + bare_dir = path / "bare" + if clones_dir.is_dir(): + if bare_dir.is_dir(): + try: + sha = git.Repo(bare_dir).head.commit.hexsha + clone = clones_dir / sha + if clone.is_dir(): + return clone + except git.GitCommandError: + pass + sha_dirs = sorted(clones_dir.iterdir(), key=lambda p: p.stat().st_mtime) + if sha_dirs: + return sha_dirs[-1] + return path + + def list_workflows(filter_by=None, sort_by="release", as_json=False, show_archived=False): """Prints out a list of all nf-core workflows. @@ -53,40 +96,50 @@ def autocomplete_pipelines(ctx, param, incomplete: str): matches = [CompletionItem(wor) for wor in available_workflows if wor.startswith(incomplete)] return matches - except Exception as e: + except (OSError, AttributeError) as e: print(f"[ERROR] Autocomplete failed: {e}", file=sys.stderr) return [] -def get_local_wf(workflow: str | Path, revision=None) -> str | None: +def get_local_wf(workflow: str | Path, revision=None) -> Path | None: """ Check if this workflow has a local copy and use nextflow to pull it if not """ # Assume nf-core if no org given if str(workflow).count("/") == 0: - workflow = f"nf-core/{workflow}" - - wfs = Workflows() - wfs.get_local_nf_workflows() - for wf in wfs.local_workflows: - if workflow == wf.full_name: - if revision is None or revision == wf.commit_sha or revision == wf.branch or revision == wf.active_tag: - if wf.active_tag: - print_revision = f"v{wf.active_tag}" - elif wf.branch: - print_revision = f"{wf.branch} - {wf.commit_sha[:7]}" - else: - print_revision = wf.commit_sha - log.info(f"Using local workflow: {workflow} ({print_revision})") - return wf.local_path + workflow = Path("nf-core", workflow) + + local_wf = LocalWorkflow(str(workflow)) + local_wf_path = _resolve_wf_path(_get_nextflow_assets_dir() / workflow) + if local_wf_path.is_dir(): + local_wf.local_path = local_wf_path + local_wf.get_local_nf_workflow_details() + if local_wf.commit_sha is not None and ( + revision is None + or revision == local_wf.commit_sha + or revision == local_wf.branch + or revision == local_wf.active_tag + ): + if local_wf.active_tag: + print_revision = f"v{local_wf.active_tag}" + elif local_wf.branch: + print_revision = f"{local_wf.branch} - {local_wf.commit_sha[:7]}" + else: + print_revision = local_wf.commit_sha + log.info(f"Using local workflow: {workflow} ({print_revision})") + return local_wf.local_path # Wasn't local, fetch it log.info(f"Downloading workflow: {workflow} ({revision})") pull_cmd = f"pull {workflow}" if revision is not None: pull_cmd += f" -r {revision}" - nf_core.utils.run_cmd("nextflow", pull_cmd) - local_wf = LocalWorkflow(workflow) + try: + nf_core.utils.run_cmd("nextflow", pull_cmd) + except RuntimeError as e: + log.warning(f"Could not pull workflow '{workflow}': {e}") + return None + local_wf = LocalWorkflow(str(workflow)) local_wf.get_local_nf_workflow_details() return local_wf.local_path @@ -131,17 +184,16 @@ def get_local_nf_workflows(self): Local workflows are stored in :attr:`self.local_workflows` list. """ # Try to guess the local cache directory (much faster than calling nextflow) - if len(os.environ.get("NXF_ASSETS", "")) > 0: - nextflow_wfdir = os.environ.get("NXF_ASSETS") - elif len(os.environ.get("NXF_HOME", "")) > 0: - nextflow_wfdir = os.path.join(os.environ.get("NXF_HOME"), "assets") - else: - nextflow_wfdir = os.path.join(os.getenv("HOME"), ".nextflow", "assets") - if os.path.isdir(nextflow_wfdir): + nextflow_wfdir = _get_nextflow_assets_dir() + if nextflow_wfdir.is_dir(): log.debug("Guessed nextflow assets directory - pulling pipeline dirnames") - for org_name in os.listdir(nextflow_wfdir): - for wf_name in os.listdir(os.path.join(nextflow_wfdir, org_name)): - self.local_workflows.append(LocalWorkflow(f"{org_name}/{wf_name}")) + self.local_workflows.extend( + LocalWorkflow(f"{org_dir.name}/{wf_dir.name}") + for org_dir in nextflow_wfdir.iterdir() + if org_dir.is_dir() + for wf_dir in org_dir.iterdir() + if wf_dir.is_dir() + ) # Fetch details about local cached pipelines with `nextflow list` else: @@ -174,7 +226,7 @@ def compare_remote_local(self): if rwf.full_name == lwf.full_name: rwf.local_wf = lwf if rwf.releases: - if rwf.releases[-1]["tag_sha"] == lwf.commit_sha: + if rwf.releases[0]["tag_sha"] == lwf.commit_sha: rwf.local_is_latest = True else: rwf.local_is_latest = False @@ -212,7 +264,7 @@ def print_summary(self): if not self.sort_workflows_by or self.sort_workflows_by == "release": filtered_workflows.sort( key=lambda wf: ( - (wf.releases[-1].get("published_at_timestamp", 0) if len(wf.releases) > 0 else 0) * -1, + (wf.releases[0].get("published_at_timestamp", 0) if len(wf.releases) > 0 else 0) * -1, wf.archived, wf.full_name.lower(), ) @@ -223,7 +275,8 @@ def print_summary(self): def sort_pulled_date(wf): try: return wf.local_wf.last_pull * -1 - except Exception: + except AttributeError: + # local_wf is None or doesn't have last_pull attribute return 0 filtered_workflows.sort(key=sort_pulled_date) @@ -258,10 +311,7 @@ def sort_pulled_date(wf): revision = f"{wf.local_wf.branch} - {wf.local_wf.commit_sha[:7]}" else: revision = wf.local_wf.commit_sha - if wf.local_is_latest: - is_latest = f"[green]Yes ({revision})" - else: - is_latest = f"[red]No ({revision})" + is_latest = f"[green]Yes ({revision})" if wf.local_is_latest else f"[red]No ({revision})" else: is_latest = "[dim]-" @@ -332,11 +382,11 @@ def __init__(self, data): class LocalWorkflow: """Class to handle local workflows pulled by nextflow""" - def __init__(self, name): + def __init__(self, name: str): """Initialise the LocalWorkflow object""" self.full_name = name self.repository = None - self.local_path = None + self.local_path: Path | None = None self.commit_sha = None self.remote_url = None self.branch = None @@ -350,13 +400,8 @@ def get_local_nf_workflow_details(self): if self.local_path is None: # Try to guess the local cache directory - if len(os.environ.get("NXF_ASSETS", "")) > 0: - nf_wfdir = os.path.join(os.environ.get("NXF_ASSETS"), self.full_name) - elif len(os.environ.get("NXF_HOME", "")) > 0: - nf_wfdir = os.path.join(os.environ.get("NXF_HOME"), "assets", self.full_name) - else: - nf_wfdir = os.path.join(os.getenv("HOME"), ".nextflow", "assets", self.full_name) - if os.path.isdir(nf_wfdir): + nf_wfdir = _resolve_wf_path(_get_nextflow_assets_dir() / self.full_name) + if nf_wfdir.is_dir(): log.debug(f"Guessed nextflow assets workflow directory: {nf_wfdir}") self.local_path = nf_wfdir @@ -369,7 +414,10 @@ def get_local_nf_workflow_details(self): for key, pattern in re_patterns.items(): m = re.search(pattern, str(nfinfo_raw)) if m: - setattr(self, key, m.group(1)) + value = Path(m.group(1)) if key == "local_path" else m.group(1) + if key == "local_path": + value = _resolve_wf_path(value) + setattr(self, key, value) # Pull information from the local git repository if self.local_path is not None: @@ -378,7 +426,8 @@ def get_local_nf_workflow_details(self): repo = git.Repo(self.local_path) self.commit_sha = str(repo.head.commit.hexsha) self.remote_url = str(repo.remotes.origin.url) - self.last_pull = os.stat(os.path.join(self.local_path, ".git", "FETCH_HEAD")).st_mtime + self.last_pull = Path(repo.common_dir) / "FETCH_HEAD" + self.last_pull = self.last_pull.stat().st_mtime self.last_pull_date = datetime.fromtimestamp(self.last_pull).strftime("%Y-%m-%d %H:%M:%S") self.last_pull_pretty = pretty_date(self.last_pull) @@ -395,12 +444,12 @@ def get_local_nf_workflow_details(self): self.active_tag = str(tag) # I'm not sure that we need this any more, it predated the self.branch catch above for detacted HEAD - except (TypeError, git.InvalidGitRepositoryError) as e: + except (OSError, TypeError, ValueError, git.InvalidGitRepositoryError) as e: log.error( f"Could not fetch status of local Nextflow copy of '{self.full_name}':" f"\n [red]{type(e).__name__}:[/] {str(e)}" "\n\nThis git repository looks broken. It's probably a good idea to delete this local copy and pull again:" - f"\n [magenta]rm -rf {self.local_path}" + f"\n [magenta]rm -rf {_get_nextflow_assets_dir() / self.full_name}" f"\n [magenta]nextflow pull {self.full_name}", ) @@ -415,10 +464,7 @@ def pretty_date(time): """ now = datetime.now() - if isinstance(time, datetime): - diff = now - time - else: - diff = now - datetime.fromtimestamp(time) + diff = now - time if isinstance(time, datetime) else now - datetime.fromtimestamp(time) second_diff = diff.seconds day_diff = diff.days diff --git a/nf_core/pipelines/params_file.py b/nf_core/pipelines/params_file.py index a4f7bff321..527a1e9913 100644 --- a/nf_core/pipelines/params_file.py +++ b/nf_core/pipelines/params_file.py @@ -85,6 +85,7 @@ def __init__( self, pipeline=None, revision=None, + no_prompts=False, ) -> None: """Initialise the ParamFileBuilder class @@ -95,6 +96,7 @@ def __init__( self.pipeline = pipeline self.pipeline_revision = revision self.schema_obj: PipelineSchema | None = None + self.no_prompts: bool = no_prompts or not nf_core.utils.is_interactive() # Fetch remote workflows self.wfs = nf_core.pipelines.list.Workflows() @@ -106,6 +108,12 @@ def get_pipeline(self) -> bool | None: """ # Prompt for pipeline if not supplied if self.pipeline is None: + if self.no_prompts: + log.error( + "No pipeline name provided and session is not interactive (no TTY detected).\n" + "Please provide the pipeline name as a command-line argument." + ) + return False launch_type = questionary.select( "Generate parameter file for local pipeline or remote GitHub pipeline?", choices=["Remote pipeline", "Local path"], @@ -171,7 +179,7 @@ def format_group(self, definition, show_hidden=False) -> str: return out def format_param( - self, name: str, properties: dict, required_properties: list[str] = [], show_hidden: bool = False + self, name: str, properties: dict, required_properties: list[str] | None = None, show_hidden: bool = False ) -> str | None: """ Format a single parameter of the schema as commented YAML @@ -186,6 +194,8 @@ def format_param( str: Section of a params-file.yml for given parameter None: If the parameter is skipped because it is hidden and show_hidden is not set """ + if required_properties is None: + required_properties = [] out = "" hidden = properties.get("hidden", False) @@ -198,7 +208,7 @@ def format_param( return "" self.schema_obj.get_schema_defaults() default = properties.get("default") - type = properties.get("type") + param_type = properties.get("type") required = name in required_properties out += _print_wrapped(name, "-", mode="both") @@ -206,8 +216,8 @@ def format_param( if description: out += _print_wrapped(description + "\n", mode="none", indent=4) - if type: - out += _print_wrapped(f"Type: {type}", mode="none", indent=4) + if param_type: + out += _print_wrapped(f"Type: {param_type}", mode="none", indent=4) if required: out += _print_wrapped("Required", mode="none", indent=4) diff --git a/nf_core/pipelines/refgenie.py b/nf_core/pipelines/refgenie.py index 46197e9cc8..882f432587 100644 --- a/nf_core/pipelines/refgenie.py +++ b/nf_core/pipelines/refgenie.py @@ -52,12 +52,12 @@ def _print_nf_config(rgc): for asset in asset_list: try: pth = rgc.seek(genome, asset) - # Catch general exception instead of refgencof exception --> no refgenconf import needed - except Exception: + # Catch refgenconf exceptions without importing refgenconf + except (OSError, RuntimeError): log.warning(f"{genome}/{asset} is incomplete, ignoring...") else: # Translate an alias name to the alias used in the pipeline - if asset in alias_translations.keys(): + if asset in alias_translations: log.info(f"Translating refgenie asset alias {asset} to {alias_translations[asset]}.") asset = alias_translations[asset] genomes_str += f' {asset.ljust(20, " ")} = "{pth}"\n' @@ -72,23 +72,24 @@ def _update_nextflow_home_config(refgenie_genomes_config_file, nxf_home): for the 'refgenie_genomes_config_file' if not already defined """ # Check if NXF_HOME/config exists and has a + refgenie_config_path = Path(refgenie_genomes_config_file).resolve() include_config_string = dedent( f""" ///// >>> nf-core + RefGenie >>> ///// // !! Contents within this block are managed by 'nf-core/tools' !! // Includes auto-generated config file with RefGenie genome assets - includeConfig '{os.path.abspath(refgenie_genomes_config_file)}' + includeConfig '{refgenie_config_path}' ///// <<< nf-core + RefGenie <<< ///// """ ) nxf_home_config = Path(nxf_home) / "config" - if os.path.exists(nxf_home_config): + if nxf_home_config.exists(): # look for include statement in config has_include_statement = False with open(nxf_home_config) as fh: lines = fh.readlines() for line in lines: - if re.match(rf"\s*includeConfig\s*'{os.path.abspath(refgenie_genomes_config_file)}'", line): + if re.match(rf"\s*includeConfig\s*'{re.escape(str(refgenie_config_path))}'", line): has_include_statement = True break @@ -164,9 +165,9 @@ def update_config(rgc): if not nxf_home: try: nxf_home = Path.home() / ".nextflow" - if not os.path.exists(nxf_home): + if not nxf_home.exists(): log.info(f"Creating NXF_HOME directory at {nxf_home}") - os.makedirs(nxf_home, exist_ok=True) + nxf_home.mkdir(parents=True, exist_ok=True) except RuntimeError: nxf_home = False diff --git a/nf_core/pipelines/rocrate.py b/nf_core/pipelines/rocrate.py index 199110e7a3..b4735c2127 100644 --- a/nf_core/pipelines/rocrate.py +++ b/nf_core/pipelines/rocrate.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """Code to deal with pipeline RO (Research Object) Crates""" +import json import logging import os import re @@ -21,6 +22,14 @@ log = logging.getLogger(__name__) +# To identify bots, we look for names that contain "[bot]" or end with "-bot" or "_bot", case-insensitive +BOT_PATTERNS = re.compile(r"\[bot\]|(-bot|_bot)$", re.IGNORECASE) + + +def _is_bot(name: str) -> bool: + return bool(BOT_PATTERNS.search(name)) + + class CustomNextflowCrateBuilder(NextflowCrateBuilder): DATA_ENTITIES = NextflowCrateBuilder.DATA_ENTITIES + [ ("docs/usage.md", "File", "Usage documentation"), @@ -94,23 +103,22 @@ def create_rocrate(self, json_path: None | Path = None, zip_path: None | Path = """ # Check that the checkout pipeline version is the same as the requested version - if self.version != "": - if self.version != self.pipeline_obj.nf_config.get("manifest.version"): - # using git checkout to get the requested version - log.info(f"Checking out pipeline version {self.version}") - if self.pipeline_obj.repo is None: - log.error(f"Pipeline repository not found in {self.pipeline_dir}") - sys.exit(1) - try: - self.pipeline_obj.repo.git.checkout(self.version) - self.pipeline_obj = Pipeline(self.pipeline_dir) - self.pipeline_obj._load() - except InvalidGitRepositoryError: - log.error(f"Could not find a git repository in {self.pipeline_dir}") - sys.exit(1) - except GitCommandError: - log.error(f"Could not checkout version {self.version}") - sys.exit(1) + if self.version != "" and self.version != self.pipeline_obj.nf_config.get("manifest.version"): + # using git checkout to get the requested version + log.info(f"Checking out pipeline version {self.version}") + if self.pipeline_obj.repo is None: + log.error(f"Pipeline repository not found in {self.pipeline_dir}") + sys.exit(1) + try: + self.pipeline_obj.repo.git.checkout(self.version) + self.pipeline_obj = Pipeline(self.pipeline_dir) + self.pipeline_obj._load() + except InvalidGitRepositoryError: + log.error(f"Could not find a git repository in {self.pipeline_dir}") + sys.exit(1) + except GitCommandError: + log.error(f"Could not checkout version {self.version}") + sys.exit(1) self.version = self.pipeline_obj.nf_config.get("manifest.version", "") self.make_workflow_rocrate() @@ -173,8 +181,8 @@ def make_workflow_rocrate(self) -> None: # get license from LICENSE file license_file = self.pipeline_dir / "LICENSE" try: - license = license_file.read_text() - if license.startswith("MIT"): + license_text = license_file.read_text() + if license_text.startswith("MIT"): self.crate.license = "MIT" else: # prompt for license @@ -206,10 +214,7 @@ def set_main_entity(self, main_entity_filename: str): "dateModified", str(datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")), compact=True ) self.crate.mainEntity.append_to("sdPublisher", {"@id": "https://nf-co.re/"}, compact=True) - if self.version.endswith("dev"): - url = "dev" - else: - url = self.version + url = "dev" if self.version.endswith("dev") else self.version self.crate.mainEntity.append_to( "url", f"https://nf-co.re/{self.crate.name.replace('nf-core/', '')}/{url}/", compact=True ) @@ -263,85 +268,195 @@ def add_main_authors(self, wf_file: rocrate.model.entity.Entity) -> None: """ Add workflow authors to the crate """ - # add author entity to crate - - try: - authors = [] - if "manifest.author" in self.pipeline_obj.nf_config: - authors.extend([a.strip() for a in self.pipeline_obj.nf_config["manifest.author"].split(",")]) - if "manifest.contributors" in self.pipeline_obj.nf_config: - contributors = self.pipeline_obj.nf_config["manifest.contributors"] - names = re.findall(r"name:'([^']+)'", contributors) - authors.extend(names) - if not authors: - raise KeyError("No authors found") - # add manifest authors as maintainer to crate - - except KeyError: - log.error("No author or contributors fields found in manifest of nextflow.config") + contributors = [] + if "manifest.contributors" in self.pipeline_obj.nf_config: + contributors = self.parse_manifest_contributors() + if not contributors and "manifest.author" in self.pipeline_obj.nf_config: + if self.pipeline_obj.repo: + contributors = self.parse_manifest_authors() + else: + log.debug("No git repository found. Cannot add contributors.") + return + if not contributors: + log.error("No authors found in pipeline manifest. Proceeding without adding authors to the RO-Crate.") return + + for author in contributors: + log.debug(f"Adding author: {author}") + + properties = { + k: v + for k, v in { + "name": author["name"], + "affiliation": author.get("affiliation"), + "url": author.get("github"), + "email": author.get("email") + or (self.pipeline_obj.repo and self._get_git_email_for_name(author["name"])), + }.items() + if v + } + + author_id = self._get_author_identifier(author) + author_entity = self.crate.add(Person(self.crate, author_id, properties=properties)) + for mode in author.get("contribution", ["contributor"]): + wf_file.append_to(mode, author_entity) + + def _get_author_identifier(self, author: dict) -> str | None: + if orcid := author.get("orcid") or get_orcid(author["name"]): + return orcid + if email := author.get("email"): + return f"#{email}" + return None + + def _get_git_email_for_name(self, name: str) -> str: + if self.pipeline_obj.repo is None: + return "" + + names_to_try: list[str] = [] + if "," in name: + # Support "First, Last" and "Last, First" + (one, two) = [n.strip() for n in name.split(",", 1)] + if one and two: + names_to_try = [f"{one} {two}", f"{two} {one}"] + elif one: + names_to_try = [one] + elif two: + names_to_try = [two] + elif name: + names_to_try = [name] + + for full_name in names_to_try: + try: + email = self.pipeline_obj.repo.git.log(f"--author={full_name}", "--pretty=format:%ae", "-1") + email = email.strip() + if email: + return email + except GitCommandError: + pass + return "" + + def _make_progress_bar(self): + return Progress( + "[bold blue]{task.description}", + BarColumn(bar_width=None), + "[magenta]{task.completed} of {task.total}[reset] » [bold yellow]{task.fields[name]}", + transient=True, + disable=os.environ.get("HIDE_PROGRESS", None) is not None, + ) + + def parse_manifest_authors(self) -> list: + # parse manifest.author" + authors = [a.strip() for a in self.pipeline_obj.nf_config["manifest.author"].split(",")] # remove duplicates authors = list(set(authors)) + log.debug(f"Authors: {authors}") + # look at git contributors for author names - try: - git_contributors: set[str] = set() - if self.pipeline_obj.repo is None: - log.debug("No git repository found. No git contributors will be added as authors.") - return + git_contributors: set[str] = set() + if self.pipeline_obj.repo is not None: commits_touching_path = list(self.pipeline_obj.repo.iter_commits(paths="main.nf")) for commit in commits_touching_path: - if commit.author.name is not None: - git_contributors.add(commit.author.name) - # exclude bots - contributors = {c for c in git_contributors if not c.endswith("bot") and c != "Travis CI User"} - - log.debug(f"Found {len(contributors)} git authors") - - progress_bar = Progress( - "[bold blue]{task.description}", - BarColumn(bar_width=None), - "[magenta]{task.completed} of {task.total}[reset] » [bold yellow]{task.fields[test_name]}", - transient=True, - disable=os.environ.get("HIDE_PROGRESS", None) is not None, - ) - with progress_bar: - bump_progress = progress_bar.add_task( - "Searching for author names on GitHub", total=len(contributors), test_name="" - ) - - for git_author in contributors: - progress_bar.update(bump_progress, advance=1, test_name=git_author) - git_author = ( - requests.get(f"https://api.github.com/users/{git_author}").json().get("name", git_author) - ) - if git_author is None: - log.debug(f"Could not find name for {git_author}") - continue - - except AttributeError: + name = commit.author.name + # exclude bots + if name and not _is_bot(name) and name != "Travis CI User": + git_contributors.add(name) + else: log.debug("Could not find git contributors") + log.debug(f"Found {len(git_contributors)} git authors") - # remove usernames (just keep names with spaces) - named_contributors = {c for c in contributors if " " in c} - - for author in named_contributors: - log.debug(f"Adding author: {author}") + git_authors = [] + with self._make_progress_bar() as progress_bar: + bump_progress = progress_bar.add_task( + "Searching for author names on GitHub", total=len(git_contributors), name="" + ) - if self.pipeline_obj.repo is None: - log.info("No git repository found. No git contributors will be added as authors.") - return - # get email from git log - email = self.pipeline_obj.repo.git.log(f"--author={author}", "--pretty=format:%ae", "-1") - orcid = get_orcid(author) - author_entitity = self.crate.add( - Person( - self.crate, orcid if orcid is not None else "#" + email, properties={"name": author, "email": email} - ) + for git_author in git_contributors: + progress_bar.update(bump_progress, advance=1, name=git_author) + github_name = requests.get(f"https://api.github.com/users/{git_author}").json().get("name") + if github_name: + # remove usernames (just keep names with spaces) + if " " in github_name and github_name not in authors: + git_authors.append(github_name) + else: + log.debug(f"Could not find name for {git_author}") + log.debug(f"Git authors: {git_authors}") + + contributors = [] + assert self.pipeline_obj.repo is not None # mypy + for name in authors + git_authors: + log.debug(name) + struct = { + "name": name, + "contribution": ["author" if name in authors else "contributor"], + } + contributors.append(struct) + + return contributors + + # Read and parse manifest.contributors. Normalise and fix its fields, + # and return as a list of dictionaries + def parse_manifest_contributors(self) -> list: + field_names = ["name", "affiliation", "github", "contribution", "orcid", "email"] + # Grab the contributor list and convert to JSON + # TODO: can be removed once we switch to `nextflow config -o json` + contributors_str = self.pipeline_obj.nf_config["manifest.contributors"] + log.debug(f"manifest.contributors: {contributors_str}") + # JSON uses double quotes, not single quotes + contributors_str = contributors_str.replace("'", '"') + for key in field_names: + # All dictionary keys need to be quoted + contributors_str = contributors_str.replace(f"{key}:", f'"{key}":') + # Use curly brackets for dictionaries + contributors_str = contributors_str.replace("], [", "}, {").replace("[[", "[{").replace("]]", "}]") + log.debug(f"manifest.contributors (normalised): {contributors_str}") + try: + contributors = json.loads(contributors_str) + except json.JSONDecodeError as exc: + log.error( + "Could not parse `manifest.contributors` from nextflow.config. " + "Expected a list of maps, for example: [[name: 'First Last', github: 'user']]. " + f"Normalised string passed to JSON parser was: {contributors_str!r}. " + f"JSON decoding error: {exc}" ) - wf_file.append_to("creator", author_entitity) - if author in authors: - wf_file.append_to("maintainer", author_entitity) + return [] + + # Using a progress bar because parsing the git log could be slow + with self._make_progress_bar() as progress_bar: + bump_progress = progress_bar.add_task("Searching for author emails", total=len(contributors), name="") + + for author in contributors: + progress_bar.update(bump_progress, advance=1, name=author.get("name")) + + # Normalise fields + for key in field_names: + if key in author: + if isinstance(author[key], str): + author[key] = author[key].strip() + elif isinstance(author[key], list): + author[key] = list(filter(lambda s: s, (s.strip() for s in author[key]))) + if not author[key]: + del author[key] + + # Name is required + if "name" not in author: + log.critical(f"No name field for author: {author}") + sys.exit(1) + + # Fix the ORCID URL + if "orcid" in author: + orcid = author["orcid"] + if not orcid.startswith("http"): + author["orcid"] = "https://orcid.org/" + orcid + + # Fix the GitHub URL + if "github" in author: + if author["github"].startswith("@"): + author["github"] = "https://github.com/" + author["github"][1:] + elif not author["github"].startswith("http"): + author["github"] = "https://github.com/" + author["github"] + + return contributors def update_rocrate(self) -> bool: """ diff --git a/nf_core/pipelines/schema.py b/nf_core/pipelines/schema.py index e9c1689e73..0c42b718dc 100644 --- a/nf_core/pipelines/schema.py +++ b/nf_core/pipelines/schema.py @@ -67,8 +67,10 @@ def _update_validation_plugin_from_config(self) -> None: plugin = "nf-schema" if self.schema_filename: conf = nf_core.utils.fetch_wf_config(Path(self.schema_filename).parent) - else: + elif self.pipeline_dir is not None: conf = nf_core.utils.fetch_wf_config(Path(self.pipeline_dir)) + else: + return plugins = str(conf.get("plugins", "")).strip("'\"").strip(" ").split(",") plugin_found = False @@ -137,7 +139,7 @@ def get_schema_path(self, path: str | Path, local_only: bool = False, revision: self.pipeline_dir = nf_core.pipelines.list.get_local_wf(path, revision=revision) self.schema_filename = Path(self.pipeline_dir or "", "nextflow_schema.json") # check if the schema file exists - if not self.schema_filename.exists(): + if self.schema_filename is not None and not self.schema_filename.exists(): self.schema_filename = None # Only looking for local paths, overwrite with None to be safe else: @@ -169,11 +171,11 @@ def load_lint_schema(self): except json.decoder.JSONDecodeError as e: error_msg = f"[bold red]Could not parse schema JSON:[/] {e}" log.error(error_msg) - raise AssertionError(error_msg) + raise AssertionError(error_msg) from e except AssertionError as e: error_msg = f"[red][✗] Pipeline schema does not follow nf-core specs:\n {e}" log.error(error_msg) - raise AssertionError(error_msg) + raise AssertionError(error_msg) from e def load_schema(self): """Load a pipeline schema from a file""" @@ -279,10 +281,10 @@ def load_input_params(self, params_path): try: params = json.load(fh) except json.JSONDecodeError as e: - raise UserWarning(f"Unable to load JSON file '{params_path}' due to error {e}") + raise UserWarning(f"Unable to load JSON file '{params_path}' due to error {e}") from e self.input_params.update(params) log.debug(f"Loaded JSON input params: {params_path}") - except Exception as json_e: + except (OSError, UserWarning) as json_e: log.debug(f"Could not load input params as JSON: {json_e}") # This failed, try to load as YAML try: @@ -293,7 +295,7 @@ def load_input_params(self, params_path): except Exception as yaml_e: error_msg = f"Could not load params file as either JSON or YAML:\n JSON: {json_e}\n YAML: {yaml_e}" log.error(error_msg) - raise AssertionError(error_msg) + raise AssertionError(error_msg) from yaml_e def validate_params(self): """Check given parameters against a schema and validate""" @@ -322,7 +324,7 @@ def validate_default_params(self): jsonschema.validate(self.schema_defaults, strip_required(self.schema)) except jsonschema.exceptions.ValidationError as e: log.debug(f"Complete error message:\n{e}") - raise AssertionError(f"Default parameters are invalid: {e.message}") + raise AssertionError(f"Default parameters are invalid: {e.message}") from e for param, default in self.schema_defaults.items(): if default in ("null", "", None, "None") or default is False: log.warning( @@ -335,7 +337,7 @@ def validate_default_params(self): self.get_wf_params() # Go over group keys - for group_key, group in self.schema.get(self.defs_notation, {}).items(): + for _group_key, group in self.schema.get(self.defs_notation, {}).items(): group_properties = group.get("properties") for param in group_properties: if param in self.ignored_params: @@ -369,7 +371,7 @@ def validate_config_default_parameter(self, param, schema_param, config_default) """ # If we have a default in the schema, check it matches the config - if "default" in schema_param and ( + if "default" in schema_param and ( # noqa SIM102 (schema_param["type"] == "boolean" and str(config_default).lower() != str(schema_param["default"]).lower()) and (str(schema_param["default"]) != str(config_default).strip("'\"")) ): @@ -385,16 +387,12 @@ def validate_config_default_parameter(self, param, schema_param, config_default) return # Check variable types in nextflow.config - if schema_param["type"] == "string": - if str(config_default) in ["false", "true", "''"]: - self.invalid_nextflow_config_default_parameters[param] = ( - f"String should not be set to `{config_default}`" - ) - if schema_param["type"] == "boolean": - if str(config_default) not in ["false", "true"]: - self.invalid_nextflow_config_default_parameters[param] = ( - f"Booleans should only be true or false, not `{config_default}`" - ) + if schema_param["type"] == "string" and str(config_default) in ["false", "true", "''"]: + self.invalid_nextflow_config_default_parameters[param] = f"String should not be set to `{config_default}`" + if schema_param["type"] == "boolean" and str(config_default) not in ["false", "true"]: + self.invalid_nextflow_config_default_parameters[param] = ( + f"Booleans should only be true or false, not `{config_default}`" + ) if schema_param["type"] == "integer": try: int(config_default) @@ -429,13 +427,13 @@ def validate_schema(self, schema=None): jsonschema.Draft7Validator.check_schema(schema) log.debug("JSON Schema Draft7 validated") except jsonschema.exceptions.SchemaError as e: - raise AssertionError(f"Schema does not validate as Draft 7 JSON Schema:\n {e}") + raise AssertionError(f"Schema does not validate as Draft 7 JSON Schema:\n {e}") from e elif self.schema_draft == "https://json-schema.org/draft/2020-12/schema": try: jsonschema.Draft202012Validator.check_schema(schema) log.debug("JSON Schema Draft2020-12 validated") except jsonschema.exceptions.SchemaError as e: - raise AssertionError(f"Schema does not validate as Draft 2020-12 JSON Schema:\n {e}") + raise AssertionError(f"Schema does not validate as Draft 2020-12 JSON Schema:\n {e}") from e else: raise AssertionError( f"Schema `$schema` should be `https://json-schema.org/draft/2020-12/schema` or `https://json-schema.org/draft-07/schema` \n Found `{schema_draft}`" @@ -560,7 +558,7 @@ def check_for_input_mimetype(self): def print_documentation( self, output_fn=None, - format="markdown", + output_format="markdown", force=False, columns=None, ): @@ -571,7 +569,7 @@ def print_documentation( columns = ["parameter", "description", "type,", "default", "required", "hidden"] output = self.schema_to_markdown(columns) - if format == "html": + if output_format == "html": output = self.markdown_to_html(output) with tempfile.NamedTemporaryFile(mode="w+") as fh: @@ -582,7 +580,7 @@ def print_documentation( if not output_fn: console = rich.console.Console() - console.print("\n", Syntax(prettified_docs, format, word_wrap=True), "\n") + console.print("\n", Syntax(prettified_docs, output_format, word_wrap=True), "\n") else: if Path(output_fn).exists() and not force: log.error(f"File '{output_fn}' exists! Please delete first, or use '--force'") @@ -727,24 +725,23 @@ def build_schema(self, pipeline_dir, no_prompts, web_only, url): self.save_schema() # If running interactively, send to the web for customisation - if not self.no_prompts: - if Confirm.ask(":rocket: Launch web builder for customisation and editing?"): - try: - self.launch_web_builder() - except AssertionError as e: - log.error(e.args[0]) - # Extra help for people running offline - if "Could not connect" in e.args[0]: - log.info( - f"If you're working offline, now copy your schema ({self.schema_filename}) and paste at https://nf-co.re/pipeline_schema_builder" - ) - log.info("When you're finished, you can paste the edited schema back into the same file") - if self.web_schema_build_web_url: - log.info( - "To save your work, open {}\n" - f"Click the blue 'Finished' button, copy the schema and paste into this file: {self.web_schema_build_web_url, self.schema_filename}" - ) - return False + if not self.no_prompts and Confirm.ask(":rocket: Launch web builder for customisation and editing?"): + try: + self.launch_web_builder() + except AssertionError as e: + log.error(e.args[0]) + # Extra help for people running offline + if "Could not connect" in e.args[0]: + log.info( + f"If you're working offline, now copy your schema ({self.schema_filename}) and paste at https://nf-co.re/pipeline_schema_builder" + ) + log.info("When you're finished, you can paste the edited schema back into the same file") + if self.web_schema_build_web_url: + log.info( + "To save your work, open {}\n" + f"Click the blue 'Finished' button, copy the schema and paste into this file: {self.web_schema_build_web_url, self.schema_filename}" + ) + return False def get_wf_params(self): """ @@ -833,7 +830,7 @@ def remove_schema_notfound_configs_single_schema(self, schema): schema = copy.deepcopy(schema) params_removed = [] # Use iterator so that we can delete the key whilst iterating - for p_key in [k for k in schema.get("properties", {}).keys()]: + for p_key in list(schema.get("properties", {})): if self.prompt_remove_schema_notfound_config(p_key): del schema["properties"][p_key] # Remove required flag if set @@ -908,14 +905,16 @@ def add_schema_found_configs(self): and (p_key not in self.schema_defaults) and (p_key not in self.ignored_params) and (p_def := self.build_schema_param(p_val).get("default")) - ): - if self.no_prompts or Confirm.ask( + ) and ( + self.no_prompts + or Confirm.ask( f":sparkles: Default for [bold]'params.{p_key}'[/] is not in schema (def='{p_def}'). " "[blue]Update pipeline schema?" - ): - s_key_def = s_key + ("default",) - nf_core.utils.nested_setitem(self.schema, s_key_def, p_def) - log.debug(f"Updating '{p_key}' default to '{p_def}' in pipeline schema") + ) + ): + s_key_def = s_key + ("default",) + nf_core.utils.nested_setitem(self.schema, s_key_def, p_def) + log.debug(f"Updating '{p_key}' default to '{p_def}' in pipeline schema") return params_added def build_schema_param(self, p_val): @@ -973,12 +972,12 @@ def launch_web_builder(self): raise AssertionError( f'web_response["status"] should be "recieved", but it is "{web_response["status"]}"' ) - except AssertionError: + except AssertionError as e: log.debug(f"Response content:\n{json.dumps(web_response, indent=4)}") raise AssertionError( f"Pipeline schema builder response not recognised: {self.web_schema_build_url}\n" " See verbose log for full response (nf-core -v schema)" - ) + ) from e else: self.web_schema_build_web_url = web_response["web_url"] self.web_schema_build_api_url = web_response["api_url"] @@ -1004,7 +1003,7 @@ def get_web_builder_response(self): self.remove_schema_empty_definitions() self.validate_schema() except AssertionError as e: - raise AssertionError(f"Response from schema builder did not pass validation:\n {e}") + raise AssertionError(f"Response from schema builder did not pass validation:\n {e}") from e else: self.save_schema() return True diff --git a/nf_core/pipelines/sync.py b/nf_core/pipelines/sync.py index f54dbe2d56..d5a167c6ba 100644 --- a/nf_core/pipelines/sync.py +++ b/nf_core/pipelines/sync.py @@ -9,7 +9,6 @@ import git import questionary -import requests import requests.auth import requests_cache import rich @@ -18,7 +17,6 @@ import nf_core import nf_core.pipelines.create.create -import nf_core.pipelines.list import nf_core.utils from nf_core.pipelines.lint_utils import dump_yaml_with_prettier @@ -70,6 +68,7 @@ def __init__( template_yaml_path: str | None = None, force_pr: bool = False, blog_post: str = "", + no_prompts: bool = False, ): """Initialise syncing object""" @@ -93,6 +92,7 @@ def __init__( self.gh_repo = gh_repo self.pr_url = "" self.blog_post = blog_post + self.no_prompts: bool = no_prompts or not nf_core.utils.is_interactive() self.config_yml_path, self.config_yml = nf_core.utils.load_tools_config(self.pipeline_dir) assert self.config_yml_path is not None and self.config_yml is not None # mypy @@ -102,6 +102,11 @@ def __init__( f"The `template_yaml_path` argument is deprecated. Saving pipeline creation settings in .nf-core.yml instead. Please remove {template_yaml_path} file." ) if getattr(self.config_yml, "template", None) is not None: + if self.no_prompts: + raise UserWarning( + f"A template section already exists in '{self.config_yml_path}' and session is not interactive.\n" + "Please resolve the template_yaml_path conflict manually." + ) overwrite_template = questionary.confirm( f"A template section already exists in '{self.config_yml_path}'. Do you want to overwrite?", style=nf_core.utils.nfcore_question_style, @@ -166,7 +171,7 @@ def sync(self) -> None: self.make_pull_request() except PullRequestExceptionError as e: self.reset_target_dir() - raise PullRequestExceptionError(e) + raise PullRequestExceptionError(e) from e self.reset_target_dir() @@ -184,8 +189,8 @@ def inspect_sync_dir(self): # Check that the pipeline_dir is a git repo try: self.repo = git.Repo(self.pipeline_dir) - except InvalidGitRepositoryError: - raise SyncExceptionError(f"'{self.pipeline_dir}' does not appear to be a git repository") + except InvalidGitRepositoryError as e: + raise SyncExceptionError(f"'{self.pipeline_dir}' does not appear to be a git repository") from e # get current branch so we can switch back later self.original_branch = self.repo.active_branch.name @@ -211,8 +216,8 @@ def get_wf_config(self) -> None: if self.from_branch and self.repo.active_branch.name != self.from_branch: log.info(f"Checking out workflow branch '{self.from_branch}'") self.repo.git.checkout(self.from_branch) - except GitCommandError: - raise SyncExceptionError(f"Branch `{self.from_branch}` not found!") + except GitCommandError as e: + raise SyncExceptionError(f"Branch `{self.from_branch}` not found!") from e # If not specified, get the name of the active branch if not self.from_branch: @@ -242,8 +247,8 @@ def checkout_template_branch(self): # Try to check out an existing local branch called TEMPLATE try: self.repo.git.checkout("TEMPLATE") - except GitCommandError: - raise SyncExceptionError("Could not check out branch 'origin/TEMPLATE' or 'TEMPLATE'") + except GitCommandError as e: + raise SyncExceptionError("Could not check out branch 'origin/TEMPLATE' or 'TEMPLATE'") from e def delete_tracked_template_branch_files(self): """ @@ -264,7 +269,7 @@ def _delete_tracked_files(self): try: file_path.unlink() except Exception as e: - raise SyncExceptionError(e) + raise SyncExceptionError(e) from e def _clean_up_empty_dirs(self): """ @@ -280,14 +285,14 @@ def _clean_up_empty_dirs(self): if curr_dir == str(self.pipeline_dir): continue - subdir_set = set(Path(curr_dir) / d for d in sub_dirs) + subdir_set = {Path(curr_dir) / d for d in sub_dirs} currdir_is_empty = (len(subdir_set - deleted) == 0) and (len(files) == 0) if currdir_is_empty: log.debug(f"Deleting empty directory {curr_dir}") try: Path(curr_dir).rmdir() except Exception as e: - raise SyncExceptionError(e) + raise SyncExceptionError(e) from e deleted.add(Path(curr_dir)) def make_template_pipeline(self) -> None: @@ -333,7 +338,7 @@ def make_template_pipeline(self) -> None: except Exception as err: # Reset to where you were to prevent git getting messed up. self.repo.git.reset("--hard") - raise SyncExceptionError(f"Failed to rebuild pipeline from template with error:\n{err}") + raise SyncExceptionError(f"Failed to rebuild pipeline from template with error:\n{err}") from err def commit_template_changes(self): """If we have any changes with the new template files, make a git commit""" @@ -351,7 +356,7 @@ def commit_template_changes(self): self.made_changes = True log.info("Committed changes to 'TEMPLATE' branch") except Exception as e: - raise SyncExceptionError(f"Could not commit changes to TEMPLATE:\n{e}") + raise SyncExceptionError(f"Could not commit changes to TEMPLATE:\n{e}") from e return True def push_template_branch(self): @@ -359,11 +364,11 @@ def push_template_branch(self): and try to make a PR. If we don't have the auth token, try to figure out a URL for the PR and print this to the console. """ - log.info(f"Pushing TEMPLATE branch to remote: '{os.path.basename(self.pipeline_dir)}'") + log.info(f"Pushing TEMPLATE branch to remote: '{self.pipeline_dir.name}'") try: self.repo.git.push() except GitCommandError as e: - raise PullRequestExceptionError(f"Could not push TEMPLATE branch:\n {e}") + raise PullRequestExceptionError(f"Could not push TEMPLATE branch:\n {e}") from e def create_merge_base_branch(self): """Create a new branch from the updated TEMPLATE branch @@ -390,7 +395,7 @@ def create_merge_base_branch(self): try: self.repo.create_head(self.merge_branch) except GitCommandError as e: - raise SyncExceptionError(f"Could not create new branch '{self.merge_branch}'\n{e}") + raise SyncExceptionError(f"Could not create new branch '{self.merge_branch}'\n{e}") from e def push_merge_branch(self): """Push the newly created merge branch to the remote repository""" @@ -399,7 +404,7 @@ def push_merge_branch(self): origin = self.repo.remote() origin.push(self.merge_branch) except GitCommandError as e: - raise PullRequestExceptionError(f"Could not push branch '{self.merge_branch}':\n {e}") + raise PullRequestExceptionError(f"Could not push branch '{self.merge_branch}':\n {e}") from e def make_pull_request(self): """Create a pull request to a base branch (default: dev), @@ -420,7 +425,7 @@ def make_pull_request(self): f"resolving any merge conflicts in the `{self.merge_branch}` branch (or your own fork, if you prefer). " "Once complete, make a new minor release of your pipeline.\n\n" "For instructions on how to merge this PR, please see " - "[https://nf-co.re/docs/contributing/sync/](https://nf-co.re/docs/contributing/sync/#merging-automated-prs).\n\n" + "[the template sync documentation](https://nf-co.re/docs/developing/template-syncs/merge-automated-pull-requests).\n\n" "For more information about this release of [nf-core/tools](https://github.com/nf-core/tools), " "please see the `v{tag}` [release page](https://github.com/nf-core/tools/releases/tag/{tag})." "\n\n> [!NOTE]\n" @@ -444,7 +449,7 @@ def make_pull_request(self): ) except Exception as e: stderr.print_exception() - raise PullRequestExceptionError(f"Something went badly wrong - {e}") + raise PullRequestExceptionError(f"Something went badly wrong - {e}") from e else: self.gh_pr_returned_data = r.json() self.pr_url = self.gh_pr_returned_data["html_url"] @@ -464,7 +469,8 @@ def _parse_json_response(response) -> tuple[Any, str]: try: json_data = json.loads(response.content) return json_data, json.dumps(json_data, indent=4) - except Exception: + except json.JSONDecodeError: + # Response content is not valid JSON return response.content, str(response.content) def reset_target_dir(self): @@ -475,7 +481,7 @@ def reset_target_dir(self): try: self.repo.git.checkout(self.original_branch) except GitCommandError as e: - raise SyncExceptionError(f"Could not reset to original branch `{self.original_branch}`:\n{e}") + raise SyncExceptionError(f"Could not reset to original branch `{self.original_branch}`:\n{e}") from e def _get_ignored_files(self) -> list[str]: """ diff --git a/nf_core/subworkflows/create.py b/nf_core/subworkflows/create.py index 93e9f271be..963076455e 100644 --- a/nf_core/subworkflows/create.py +++ b/nf_core/subworkflows/create.py @@ -12,7 +12,6 @@ def __init__( component="", author=None, force=False, - migrate_pytest=False, ): super().__init__( "subworkflows", @@ -20,5 +19,4 @@ def __init__( component, author, force=force, - migrate_pytest=migrate_pytest, ) diff --git a/nf_core/subworkflows/install.py b/nf_core/subworkflows/install.py index 70a6b0afa5..90bb504b80 100644 --- a/nf_core/subworkflows/install.py +++ b/nf_core/subworkflows/install.py @@ -12,6 +12,7 @@ def __init__( branch=None, no_pull=False, installed_by=None, + skip_deps=False, ): super().__init__( pipeline_dir, @@ -23,4 +24,5 @@ def __init__( branch=branch, no_pull=no_pull, installed_by=installed_by, + skip_deps=skip_deps, ) diff --git a/nf_core/subworkflows/lint/__init__.py b/nf_core/subworkflows/lint/__init__.py index ffa3e797c9..7d5fbccdae 100644 --- a/nf_core/subworkflows/lint/__init__.py +++ b/nf_core/subworkflows/lint/__init__.py @@ -262,14 +262,12 @@ def update_meta_yml_file(self, swf): # Compare inputs and add them if missing if "input" in meta_yaml: # Delete inputs from meta.yml which are not present in main.nf - meta_yaml_corrected["input"] = [ - input for input in meta_yaml["input"] if list(input.keys())[0] in swf.inputs - ] + meta_yaml_corrected["input"] = [inp for inp in meta_yaml["input"] if list(inp.keys())[0] in swf.inputs] # Obtain inputs from main.nf missing in meta.yml inputs_correct = [ - list(input.keys())[0] for input in meta_yaml_corrected["input"] if list(input.keys())[0] in swf.inputs + list(inp.keys())[0] for inp in meta_yaml_corrected["input"] if list(inp.keys())[0] in swf.inputs ] - inputs_missing = [input for input in swf.inputs if input not in inputs_correct] + inputs_missing = [inp for inp in swf.inputs if inp not in inputs_correct] # Add missing inputs to meta.yml for missing_input in inputs_missing: meta_yaml_corrected["input"].append({missing_input: {"description": ""}}) diff --git a/nf_core/subworkflows/lint/main_nf.py b/nf_core/subworkflows/lint/main_nf.py index c656a0c2b7..6d79a47396 100644 --- a/nf_core/subworkflows/lint/main_nf.py +++ b/nf_core/subworkflows/lint/main_nf.py @@ -24,6 +24,16 @@ def main_nf(_, subworkflow: NFCoreComponent) -> tuple[list[str], list[str]]: * All included modules or subworkflows are used and their names are used for `versions.yml` * The workflow name is all capital letters * The subworkflow emits a software version + + The following checks are performed: + + * ``main_nf_exists``: The ``main.nf`` file must exist. + + * ``main_nf_script_outputs``: The workflow must have an ``emit:`` block. + + * ``main_nf_version_emitted``: The subworkflow should emit a software version + channel. A warning is issued if no ``versions`` output is found (can be + ignored if the subworkflow uses topic channels). """ inputs: list[str] = [] @@ -96,22 +106,6 @@ def main_nf(_, subworkflow: NFCoreComponent) -> tuple[list[str], list[str]]: # Check the main definition check_main_section(subworkflow, main_lines, included_components) - # Check that a software version is emitted - if outputs: - if "versions" in outputs: - subworkflow.passed.append( - ("main_nf", "main_nf_version_emitted", "Subworkflow emits software version", subworkflow.main_nf) - ) - else: - subworkflow.warned.append( - ( - "main_nf", - "main_nf_version_emitted", - "Subworkflow does not emit software version. Can be ignored if the subworkflow is using topic channels", - subworkflow.main_nf, - ) - ) - return inputs, outputs @@ -157,24 +151,6 @@ def check_main_section(self, lines, included_components): self.main_nf, ) ) - if component + ".out.versions" in script: - self.passed.append( - ( - "main_nf", - "main_nf_include_versions", - f"Included component '{component}' versions are added in main.nf", - self.main_nf, - ) - ) - else: - self.warned.append( - ( - "main_nf", - "main_nf_include_versions", - f"Included component '{component}' versions are not added in main.nf. Can be ignored if the module is using topic channels", - self.main_nf, - ) - ) def check_subworkflow_section(self, lines: list[str]) -> list[str]: diff --git a/nf_core/subworkflows/lint/meta_yml.py b/nf_core/subworkflows/lint/meta_yml.py index 59d86584ac..bfc3330e9d 100644 --- a/nf_core/subworkflows/lint/meta_yml.py +++ b/nf_core/subworkflows/lint/meta_yml.py @@ -27,6 +27,27 @@ def meta_yml(subworkflow_lint_object, subworkflow, allow_missing: bool = False): Checks that all input and output channels are specified in ``meta.yml``. Checks that all included components in ``main.nf`` are specified in ``meta.yml``. + The following checks are performed: + + * ``meta_yml_exists``: The ``meta.yml`` file must exist. + + * ``meta_yml_valid``: The ``meta.yml`` must be valid according to the JSON + schema defined at https://raw.githubusercontent.com/nf-core/subworkflows/master/modules/environment-schema.json. + + * ``meta_input``: All input channels declared in ``main.nf`` must be listed + under the ``input:`` key in ``meta.yml``. + + * ``meta_output``: All output channels declared in ``main.nf`` must be listed + under the ``output:`` key in ``meta.yml``. + + * ``meta_name``: The ``name`` field in ``meta.yml`` must match the workflow + name declared in ``main.nf``. + + * ``meta_include``: All modules and subworkflows included in ``main.nf`` must + be listed under the ``components:`` key in ``meta.yml``. + + * ``meta_modules_deprecated``: The deprecated ``modules:`` section must not + be present in ``meta.yml``; use ``components:`` instead. """ # Read the meta.yml file if subworkflow.meta_yml is None: @@ -85,12 +106,12 @@ def meta_yml(subworkflow_lint_object, subworkflow, allow_missing: bool = False): if valid_meta_yml: if "input" in meta_yaml: meta_input = [list(x.keys())[0] for x in meta_yaml["input"]] - for input in subworkflow.inputs: - if input in meta_input: - subworkflow.passed.append(("meta_yml", "meta_input", f"`{input}` specified", subworkflow.meta_yml)) + for inp in subworkflow.inputs: + if inp in meta_input: + subworkflow.passed.append(("meta_yml", "meta_input", f"`{inp}` specified", subworkflow.meta_yml)) else: subworkflow.failed.append( - ("meta_yml", "meta_input", f"`{input}` missing in `meta.yml`", subworkflow.meta_yml) + ("meta_yml", "meta_input", f"`{inp}` missing in `meta.yml`", subworkflow.meta_yml) ) else: log.debug(f"No inputs specified in subworkflow `main.nf`: {subworkflow.component_name}") diff --git a/nf_core/subworkflows/lint/subworkflow_changes.py b/nf_core/subworkflows/lint/subworkflow_changes.py index bbdbfd344b..9152931594 100644 --- a/nf_core/subworkflows/lint/subworkflow_changes.py +++ b/nf_core/subworkflows/lint/subworkflow_changes.py @@ -22,6 +22,14 @@ def subworkflow_changes(subworkflow_lint_object, subworkflow): compared against the files in the remote at this SHA. Only runs when linting a pipeline, not the modules repository + + The following checks are performed: + + * ``subworkflow_patch``: If the subworkflow is patched, the patch must apply + cleanly in reverse against the remote version. + + * ``check_local_copy``: Each subworkflow file must be identical to the + corresponding file in the remote repository at the pinned commit SHA. """ if subworkflow.is_patched: # If the subworkflow is patched, we need to apply diff --git a/nf_core/subworkflows/lint/subworkflow_if_empty_null.py b/nf_core/subworkflows/lint/subworkflow_if_empty_null.py index 481e31e3ed..5709874329 100644 --- a/nf_core/subworkflows/lint/subworkflow_if_empty_null.py +++ b/nf_core/subworkflows/lint/subworkflow_if_empty_null.py @@ -14,6 +14,11 @@ def subworkflow_if_empty_null(_, subworkflow): There are multiple examples of workflows that inject null objects into channels using `ifEmpty(null)`, which can cause unhandled null pointer exceptions. This lint test throws warnings for those instances. + + The following checks are performed: + + * ``subworkflow_if_empty_null``: Warns if any ``ifEmpty(null)`` usage is found + in the subworkflow files. """ # Main subworkflow directory @@ -22,7 +27,7 @@ def subworkflow_if_empty_null(_, subworkflow): subworkflow.warned.append( ("subworkflow_if_empty_null", "subworkflow_if_empty_null", warning, swf_results["file_paths"][i]) ) - for i, passed in enumerate(swf_results["passed"]): + for _i, passed in enumerate(swf_results["passed"]): subworkflow.passed.append( ("subworkflow_if_empty_null", "subworkflow_if_empty_null", passed, subworkflow.component_dir) ) diff --git a/nf_core/subworkflows/lint/subworkflow_tests.py b/nf_core/subworkflows/lint/subworkflow_tests.py index 168fac9fc2..189f3c10f0 100644 --- a/nf_core/subworkflows/lint/subworkflow_tests.py +++ b/nf_core/subworkflows/lint/subworkflow_tests.py @@ -14,13 +14,36 @@ def subworkflow_tests(_, subworkflow: NFCoreComponent, allow_missing: bool = False): - """ - Lint the tests of a subworkflow in ``nf-core/modules`` + """Lint the tests of a subworkflow in ``nf-core/modules`` + + Checks the ``tests/`` directory and ``main.nf.test`` file for correctness, + validates snapshot content, and verifies that nf-test tags follow guidelines. + + The following checks are performed: + + * ``test_dir_exists``: The nf-test directory ``tests/`` must exist. + + * ``test_main_nf_exists``: The file ``tests/main.nf.test`` must exist. + + * ``test_snapshot_exists``: If ``snapshot()`` is called in ``main.nf.test``, + the snapshot file ``tests/main.nf.test.snap`` must exist and be valid JSON. + + * ``test_snap_md5sum``: The snapshot must not contain md5sums for empty files + (``d41d8cd98f00b204e9800998ecf8427e``) or empty compressed files + (``7029066c27ac6f5ef18d660d5741979a``), unless the test name contains ``stub``. - It verifies that the test directory exists - and contains a ``main.nf.test`` and a ``main.nf.test.snap`` + * ``test_snap_versions``: The snapshot should contain a ``versions`` key. + A warning (not a failure) is issued if it is absent, since subworkflows that + use topic channels may not emit versions directly. + + * ``test_main_tags``: The ``main.nf.test`` file must declare the required tags: + ``subworkflows``, ``subworkflows/``, ``subworkflows_``, + all components included in the subworkflow's ``main.nf``, and any chained + components referenced via ``include`` statements in the test file. + + * ``test_old_test_dir``: The legacy pytest directory + ``tests/subworkflows//`` must not exist. - Additionally, checks that all included components in test ``main.nf`` are specified in ``test.yml`` """ if subworkflow.nftest_testdir is None: if allow_missing: @@ -139,7 +162,7 @@ def subworkflow_tests(_, subworkflow: NFCoreComponent, allow_missing: bool = Fal with open(snap_file) as snap_fh: try: snap_content = json.load(snap_fh) - for test_name in snap_content.keys(): + for test_name in snap_content: if "d41d8cd98f00b204e9800998ecf8427e" in str(snap_content[test_name]): if "stub" not in test_name: subworkflow.failed.append( @@ -196,24 +219,6 @@ def subworkflow_tests(_, subworkflow: NFCoreComponent, allow_missing: bool = Fal snap_file, ) ) - if "versions" in str(snap_content[test_name]) or "versions" in str(snap_content.keys()): - subworkflow.passed.append( - ( - "subworkflow_tests", - "test_snap_versions", - "versions found in snapshot file", - snap_file, - ) - ) - else: - subworkflow.warned.append( - ( - "subworkflow_tests", - "test_snap_versions", - "versions not found in snapshot file. Can be ignored if the subworkflow is using topic channels", - snap_file, - ) - ) except json.decoder.JSONDecodeError as e: subworkflow.failed.append( ( diff --git a/nf_core/subworkflows/lint/subworkflow_todos.py b/nf_core/subworkflows/lint/subworkflow_todos.py index 05286bf11c..9483771a59 100644 --- a/nf_core/subworkflows/lint/subworkflow_todos.py +++ b/nf_core/subworkflows/lint/subworkflow_todos.py @@ -30,11 +30,15 @@ def subworkflow_todos(_, subworkflow): in a given project directory. This is a very quick and convenient way to get started on your pipeline! + The following checks are performed: + + * ``subworkflow_todo``: Warns if any ``TODO nf-core:`` comment lines are found + in the subworkflow files. """ # Main subworkflow directory swf_results = pipeline_todos(None, root_dir=subworkflow.component_dir) for i, warning in enumerate(swf_results["warned"]): subworkflow.warned.append(("subworkflow_todos", "subworkflow_todo", warning, swf_results["file_paths"][i])) - for i, passed in enumerate(swf_results["passed"]): + for _i, passed in enumerate(swf_results["passed"]): subworkflow.passed.append(("subworkflow_todos", "subworkflow_todo", passed, subworkflow.component_dir)) diff --git a/nf_core/subworkflows/lint/subworkflow_version.py b/nf_core/subworkflows/lint/subworkflow_version.py index b9712556d8..b5b3b1d0e0 100644 --- a/nf_core/subworkflows/lint/subworkflow_version.py +++ b/nf_core/subworkflows/lint/subworkflow_version.py @@ -5,9 +5,7 @@ import logging from pathlib import Path -import nf_core import nf_core.modules.modules_repo -import nf_core.modules.modules_utils log = logging.getLogger(__name__) @@ -19,6 +17,13 @@ def subworkflow_version(subworkflow_lint_object, subworkflow): It checks whether the subworkflow has an entry in the ``modules.json`` file containing a commit SHA. If that is true, it verifies that there are no newer version of the subworkflow available. + + The following checks are performed: + + * ``git_sha``: The subworkflow must have a ``git_sha`` entry in ``modules.json``. + + * ``subworkflow_version``: The subworkflow version must match the latest commit + in the remote repository. A warning is issued if a newer version is available. """ modules_json_path = Path(subworkflow_lint_object.directory, "modules.json") diff --git a/nf_core/subworkflows/update.py b/nf_core/subworkflows/update.py index 9b6bf16928..29c4dbcc02 100644 --- a/nf_core/subworkflows/update.py +++ b/nf_core/subworkflows/update.py @@ -16,6 +16,7 @@ def __init__( branch=None, no_pull=False, limit_output=False, + skip_deps=False, ): super().__init__( pipeline_dir, @@ -31,4 +32,5 @@ def __init__( branch, no_pull, limit_output, + skip_deps, ) diff --git a/nf_core/synced_repo.py b/nf_core/synced_repo.py index eb5e406bfa..72b5cf34b3 100644 --- a/nf_core/synced_repo.py +++ b/nf_core/synced_repo.py @@ -5,6 +5,7 @@ from collections.abc import Iterable from configparser import NoOptionError, NoSectionError from pathlib import Path +from typing import cast import git from git.exc import GitCommandError @@ -92,8 +93,8 @@ def get_remote_branches(remote_url): """ try: unparsed_branches = git.Git().ls_remote(remote_url) - except git.GitCommandError: - raise LookupError(f"Was unable to fetch branches from '{remote_url}'") + except git.GitCommandError as e: + raise LookupError(f"Was unable to fetch branches from '{remote_url}'") from e else: branches = {} for branch_info in unparsed_branches.split("\n"): @@ -133,8 +134,8 @@ def __init__(self, remote_url=None, branch=None, no_pull=False, hide_progress=Fa if config_fn is not None and repo_config is not None: try: self.repo_path = repo_config.org_path - except KeyError: - raise UserWarning(f"'org_path' key not present in {config_fn.name}") + except KeyError as e: + raise UserWarning(f"'org_path' key not present in {config_fn.name}") from e # Verify that the repo seems to be correctly configured if self.repo_path != NF_CORE_MODULES_NAME or self.branch: @@ -165,10 +166,9 @@ def verify_sha(self, prompt, sha): log.error("Cannot use '--sha' and '--prompt' at the same time!") return False - if sha: - if not self.sha_exists_on_branch(sha): - log.error(f"Commit SHA '{sha}' doesn't exist in '{self.remote_url}'") - return False + if sha and not self.sha_exists_on_branch(sha): + log.error(f"Commit SHA '{sha}' doesn't exist in '{self.remote_url}'") + return False return True @@ -206,17 +206,18 @@ def branch_exists(self): """ try: self.checkout_branch() - except GitCommandError: - raise LookupError(f"Branch '{self.branch}' not found in '{self.remote_url}'") + except GitCommandError as e: + raise LookupError(f"Branch '{self.branch}' not found in '{self.remote_url}'") from e def verify_branch(self): """ Verifies the active branch conforms to the correct directory structure """ - dir_names = os.listdir(self.local_repo_dir) - if "modules" not in dir_names: + local_repo_path = Path(cast(str, self.local_repo_dir)) + + if not Path(local_repo_path, "modules").exists(): err_str = f"Repository '{self.remote_url}' ({self.branch}) does not contain the 'modules/' directory" - if "software" in dir_names: + if Path(local_repo_path, "software").exists(): err_str += ( ".\nAs of nf-core/tools version 2.0, the 'software/' directory should be renamed to 'modules/'" ) @@ -336,7 +337,7 @@ def component_files_identical(self, component_name, base_path, commit, component else: self.checkout(commit) component_files = ["main.nf", "meta.yml"] - files_identical = {file: True for file in component_files} + files_identical = dict.fromkeys(component_files, True) component_dir = self.get_component_dir(component_name, component_type) for file in component_files: try: @@ -399,7 +400,7 @@ def get_component_git_log( "To solve this, you can try to remove the cloned rempository and run the command again.\n" f"This repository is typically found at `{self.local_repo_dir}`" ) - raise UserWarning + raise UserWarning from None commits = iter(commits_new + commits_old) return commits @@ -413,7 +414,7 @@ def get_latest_component_version(self, component_name, component_type): if not git_logs: return None return git_logs[0]["git_sha"] - except Exception as e: + except (git.exc.GitError, KeyError) as e: log.debug(f"Could not get latest version of {component_name}: {e}") return None diff --git a/nf_core/test_datasets/list.py b/nf_core/test_datasets/list.py index 3531eef1ce..f773598431 100644 --- a/nf_core/test_datasets/list.py +++ b/nf_core/test_datasets/list.py @@ -56,7 +56,7 @@ def list_datasets( tree = list_files_by_branch(branch, all_branches, ignored_file_prefixes) out = [] - for b in tree.keys(): + for b in tree: files = sorted(tree[b]) for f in files: if generate_nf_path: diff --git a/nf_core/test_datasets/test_datasets_utils.py b/nf_core/test_datasets/test_datasets_utils.py index 14719c6d8c..7ed782cba4 100644 --- a/nf_core/test_datasets/test_datasets_utils.py +++ b/nf_core/test_datasets/test_datasets_utils.py @@ -10,6 +10,7 @@ from nf_core.utils import ( determine_base_dir, fetch_wf_config, + is_interactive, load_tools_config, nfcore_question_style, rich_force_colors, @@ -82,11 +83,15 @@ def get_remote_branch_names() -> list[str]: return branches -def get_remote_tree_for_branch(branch: str, only_files: bool = True, ignored_prefixes: list[str] = []) -> list[str]: +def get_remote_tree_for_branch( + branch: str, only_files: bool = True, ignored_prefixes: list[str] | None = None +) -> list[str]: """ For a given branch name, return the file tree by querying the github API at the endpoint at `/repos/nf-core/test-datasets/git/trees/` """ + if ignored_prefixes is None: + ignored_prefixes = [] gh_filetree_file_value = "blob" # value in nodes used to refer to "files" gh_response_filetree_key = "tree" # key in response to refer to the filetree gh_filetree_type_key = "type" # key in filetree nodes used to refer to their type @@ -127,20 +132,18 @@ def get_remote_tree_for_branch(branch: str, only_files: bool = True, ignored_pre def list_files_by_branch( branch: str = "", - branches: list[str] = [], - ignored_file_prefixes: list[str] = [ - ".", - "CITATION", - "LICENSE", - "README", - "docs", - ], + branches: list[str] | None = None, + ignored_file_prefixes: list[str] | None = None, ) -> dict[str, list[str]]: """ Lists files for all branches in the test-datasets github repo. Returns dictionary with branchnames as keys and file-lists as values """ + if ignored_file_prefixes is None: + ignored_file_prefixes = [".", "CITATION", "LICENSE", "README", "docs"] + if branches is None: + branches = [] if len(branches) == 0: log.debug("Fetching list of remote branch names") branches = get_remote_branch_names() @@ -151,7 +154,7 @@ def list_files_by_branch( log.error(f"No branches matching '{branch}'") log.debug("Fetching remote trees") - tree = dict() + tree = {} for b in branches: tree[b] = get_remote_tree_for_branch(b, only_files=True, ignored_prefixes=ignored_file_prefixes) @@ -205,6 +208,8 @@ def get_or_prompt_branch(maybe_branch: str) -> tuple[str, list[str]]: if pipeline_name in all_branches: branch_prefill = pipeline_name + if not is_interactive(): + raise UserWarning("No branch name provided and session is not interactive (no TTY detected).") branch = questionary.autocomplete( "Branch name:", choices=sorted(all_branches), @@ -231,11 +236,13 @@ def get_or_prompt_file_selection(files: list[str], query: str | None) -> str: file_selected = True while not file_selected: + if not is_interactive(): + raise UserWarning("No file selected and session is not interactive (no TTY detected).") selection = questionary.autocomplete( "File:", choices=files, style=nfcore_question_style, default=query, qmark=AUTOCOMPLETION_HINT ).unsafe_ask() - file_selected = any([selection == file for file in files]) + file_selected = any(selection == file for file in files) if not file_selected: stdout.print("Please select a file.") diff --git a/nf_core/utils.py b/nf_core/utils.py index e4877667a3..d376559e53 100644 --- a/nf_core/utils.py +++ b/nf_core/utils.py @@ -20,7 +20,7 @@ import sys import time from collections.abc import Callable, Generator -from contextlib import contextmanager +from contextlib import contextmanager, suppress from pathlib import Path from typing import TYPE_CHECKING, Any, Literal @@ -36,9 +36,6 @@ from pydantic import BaseModel, ValidationError, field_validator from rich.live import Live from rich.spinner import Spinner -from textual.message import Message -from textual.widget import Widget -from textual.widgets import Markdown, RichLog import nf_core @@ -90,11 +87,20 @@ ] ) + +def is_interactive() -> bool: + """Check if the current session is interactive (has a TTY on stdin, stdout, and stderr).""" + return sys.stdin.isatty() and sys.stdout.isatty() and sys.stderr.isatty() + + NFCORE_CACHE_DIR = Path( os.environ.get("XDG_CACHE_HOME", Path(os.getenv("HOME") or "", ".cache")), "nfcore", ) -NFCORE_DIR = Path(os.environ.get("XDG_CONFIG_HOME", os.path.join(os.getenv("HOME") or "", ".config")), "nfcore") +NFCORE_DIR = Path( + os.environ.get("XDG_CONFIG_HOME") or Path(os.getenv("HOME") or "") / ".config", + "nfcore", +) def unquote(s: str) -> str: @@ -155,11 +161,10 @@ def check_if_outdated( with concurrent.futures.ThreadPoolExecutor() as executor: future = executor.submit(fetch_remote_version, source_url) remote_version = future.result() - except Exception as e: + except requests.exceptions.RequestException as e: log.debug(f"Could not check for nf-core updates: {e}") - if remote_version is not None: - if Version(remote_version) > Version(current_version): - is_outdated = True + if remote_version is not None and Version(remote_version) > Version(current_version): + is_outdated = True return (is_outdated, current_version, remote_version) @@ -207,7 +212,7 @@ def __init__(self, wf_path: Path) -> None: try: self.repo = git.Repo(self.wf_path) self.git_sha = self.repo.head.object.hexsha - except Exception as e: + except git.exc.GitError as e: log.debug(f"Could not find git hash for pipeline: {self.wf_path}. {e}") # Overwrite if we have the last commit from the PR - otherwise we get a merge commit hash @@ -288,8 +293,7 @@ def is_pipeline_directory(wf_path): UserWarning: If one of the files are missing """ for fn in ["main.nf", "nextflow.config"]: - path = os.path.join(wf_path, fn) - if not os.path.isfile(path): + if not Path(wf_path, fn).is_file(): if wf_path == ".": warning = f"Current directory is not a pipeline - '{fn}' is missing." else: @@ -335,7 +339,7 @@ def get_nf_version() -> tuple[int, int, int, bool] | None: is_edge, ) return parsed_version_tuple - except Exception as e: + except (subprocess.CalledProcessError, IndexError, ValueError) as e: log.warning(f"Error getting Nextflow version: {e}") return None @@ -420,10 +424,7 @@ def fetch_wf_config(wf_path: Path, cache_config: bool = True) -> dict: # Log warning but don't raise - just regenerate the cache log.warning(f"Unable to load cached JSON file '{cache_path}' due to error: {e}") log.debug("Removing corrupted cache file and regenerating...") - try: - cache_path.unlink() - except OSError: - pass # If we can't delete it, just continue + cache_path.unlink(missing_ok=True) log.debug("No config cache found") # Call `nextflow config` @@ -478,23 +479,21 @@ def run_cmd(executable: str, cmd: str) -> tuple[bytes, bytes] | None: full_cmd = f"{executable} {cmd}" log.debug(f"Running command: {full_cmd}") try: - proc = subprocess.run(shlex.split(full_cmd), capture_output=True, check=True) + proc = subprocess.run(shlex.split(full_cmd), capture_output=True, check=False) + if proc.returncode != 0: + if executable == "nf-test": + return (proc.stdout, proc.stderr) + raise subprocess.CalledProcessError(proc.returncode, proc.args, output=proc.stdout, stderr=proc.stderr) return (proc.stdout, proc.stderr) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Command '{full_cmd}' failed: {e}") from e except OSError as e: if e.errno == errno.ENOENT: raise RuntimeError( f"It looks like {executable} is not installed. Please ensure it is available in your PATH." - ) + ) from e else: return None - except subprocess.CalledProcessError as e: - log.debug(f"Command '{full_cmd}' returned non-zero error code '{e.returncode}':\n[red]> {e.stderr.decode()}") - if executable == "nf-test": - return (e.stdout, e.stderr) - else: - raise RuntimeError( - f"Command '{full_cmd}' returned non-zero error code '{e.returncode}':\n[red]> {e.stderr.decode()}{e.stdout.decode()}" - ) def setup_nfcore_dir() -> bool: @@ -537,7 +536,7 @@ def setup_nfcore_cachedir(cache_fn: str | Path) -> Path: if not Path(cachedir).exists(): Path(cachedir).mkdir(parents=True) except PermissionError: - log.warn(f"Could not create cache directory: {cachedir}") + log.warning(f"Could not create cache directory: {cachedir}") return cachedir @@ -562,8 +561,8 @@ def wait_cli_function(poll_func: Callable[[], bool], refresh_per_second: int = 2 if poll_func(): break time.sleep(2) - except KeyboardInterrupt: - raise AssertionError("Cancelled!") + except KeyboardInterrupt as e: + raise AssertionError("Cancelled!") from e def poll_nfcore_web_api(api_url: str, post_data: dict | None = None) -> dict: @@ -582,10 +581,10 @@ def poll_nfcore_web_api(api_url: str, post_data: dict | None = None) -> dict: else: log.debug(f"requesting {api_url} with {post_data}") response = requests.post(url=api_url, data=post_data) - except requests.exceptions.Timeout: - raise AssertionError(f"URL timed out: {api_url}") - except requests.exceptions.ConnectionError: - raise AssertionError(f"Could not connect to URL: {api_url}") + except requests.exceptions.Timeout as e: + raise AssertionError(f"URL timed out: {api_url}") from e + except requests.exceptions.ConnectionError as e: + raise AssertionError(f"Could not connect to URL: {api_url}") from e else: if response.status_code != 200 and response.status_code != 301: response_content = response.content @@ -601,8 +600,8 @@ def poll_nfcore_web_api(api_url: str, post_data: dict | None = None) -> dict: try: web_response = json.loads(response.content) if "status" not in web_response: - raise AssertionError() - except (json.decoder.JSONDecodeError, AssertionError, TypeError): + raise AssertionError + except (json.decoder.JSONDecodeError, AssertionError, TypeError) as e: response_content = response.content if isinstance(response_content, bytes): response_content = response_content.decode() @@ -610,7 +609,7 @@ def poll_nfcore_web_api(api_url: str, post_data: dict | None = None) -> dict: raise AssertionError( f"nf-core website API results response not recognised: {api_url}\n " "See verbose log for full response" - ) + ) from e else: return web_response @@ -660,8 +659,8 @@ def __call__(self, r): return r # Default auth if we're running and the gh CLI tool is installed - gh_cli_config_fn = os.path.expanduser("~/.config/gh/hosts.yml") - if self.auth is None and os.path.exists(gh_cli_config_fn): + gh_cli_config_fn = Path.home() / ".config" / "gh" / "hosts.yml" + if self.auth is None and gh_cli_config_fn.exists(): try: with open(gh_cli_config_fn) as fh: gh_cli_config = yaml.safe_load(fh) @@ -670,7 +669,7 @@ def __call__(self, r): gh_cli_config["github.com"]["oauth_token"], ) self.auth_mode = f"gh CLI config: {gh_cli_config['github.com']['user']}" - except Exception: + except (OSError, KeyError, yaml.YAMLError): ex_type, ex_value, _ = sys.exc_info() if ex_type is not None: output = rich.markup.escape(f"{ex_type.__name__}: {ex_value}") @@ -699,7 +698,7 @@ def log_content_headers(self, request, post_data=None): log.debug(json.dumps(dict(request.headers), indent=4)) log.debug(json.dumps(request.json(), indent=4)) log.debug(json.dumps(post_data, indent=4)) - except Exception as e: + except (json.JSONDecodeError, TypeError) as e: log.debug(f"Could not parse JSON response from GitHub API! {e}") log.debug(request.headers) log.debug(request.content) @@ -815,10 +814,10 @@ def anaconda_package(dep, dep_channels=None): anaconda_api_url = f"https://api.anaconda.org/package/{ch}/{depname}" try: response = requests.get(anaconda_api_url, timeout=10) - except requests.exceptions.Timeout: - raise LookupError(f"Anaconda API timed out: {anaconda_api_url}") - except requests.exceptions.ConnectionError: - raise LookupError("Could not connect to Anaconda API") + except requests.exceptions.Timeout as e: + raise LookupError(f"Anaconda API timed out: {anaconda_api_url}") from e + except requests.exceptions.ConnectionError as e: + raise LookupError("Could not connect to Anaconda API") from e else: if response.status_code == 200: return response.json() @@ -843,28 +842,26 @@ def parse_anaconda_licence(anaconda_response, version=None): # Licence for each version for f in anaconda_response["files"]: if not version or version == f.get("version"): - try: + with suppress(KeyError): licences.add(f["attrs"]["license"]) - except KeyError: - pass # Main licence field if len(list(licences)) == 0 and isinstance(anaconda_response["license"], str): licences.add(anaconda_response["license"]) # Clean up / standardise licence names clean_licences = [] - for license in licences: - license = re.sub(r"GNU General Public License v\d \(([^\)]+)\)", r"\1", license) - license = re.sub(r"GNU GENERAL PUBLIC LICENSE", "GPL", license, flags=re.IGNORECASE) - license = license.replace("GPL-", "GPLv") - license = re.sub(r"GPL\s*([\d\.]+)", r"GPL v\1", license) # Add v prefix to GPL version if none found - license = re.sub(r"GPL\s*v(\d).0", r"GPL v\1", license) # Remove superfluous .0 from GPL version - license = re.sub(r"GPL \(([^\)]+)\)", r"GPL \1", license) - license = re.sub(r"GPL\s*v", "GPL v", license) # Normalise whitespace to one space between GPL and v - license = re.sub(r"\s*(>=?)\s*(\d)", r" \1\2", license) # Normalise whitespace around >= GPL versions - license = license.replace("Clause", "clause") # BSD capitalisation - license = re.sub(r"-only$", "", license) # Remove superfluous GPL "only" version suffixes - clean_licences.append(license) + for lic in licences: + lic = re.sub(r"GNU General Public License v\d \(([^\)]+)\)", r"\1", lic) + lic = re.sub(r"GNU GENERAL PUBLIC LICENSE", "GPL", lic, flags=re.IGNORECASE) + lic = lic.replace("GPL-", "GPLv") + lic = re.sub(r"GPL\s*([\d\.]+)", r"GPL v\1", lic) # Add v prefix to GPL version if none found + lic = re.sub(r"GPL\s*v(\d).0", r"GPL v\1", lic) # Remove superfluous .0 from GPL version + lic = re.sub(r"GPL \(([^\)]+)\)", r"GPL \1", lic) + lic = re.sub(r"GPL\s*v", "GPL v", lic) # Normalise whitespace to one space between GPL and v + lic = re.sub(r"\s*(>=?)\s*(\d)", r" \1\2", lic) # Normalise whitespace around >= GPL versions + lic = lic.replace("Clause", "clause") # BSD capitalisation + lic = re.sub(r"-only$", "", lic) # Remove superfluous GPL "only" version suffixes + clean_licences.append(lic) return clean_licences @@ -884,10 +881,10 @@ def pip_package(dep): pip_api_url = f"https://pypi.python.org/pypi/{pip_depname}/json" try: response = requests.get(pip_api_url, timeout=10) - except requests.exceptions.Timeout: - raise LookupError(f"PyPI API timed out: {pip_api_url}") - except requests.exceptions.ConnectionError: - raise LookupError(f"PyPI API Connection error: {pip_api_url}") + except requests.exceptions.Timeout as e: + raise LookupError(f"PyPI API timed out: {pip_api_url}") from e + except requests.exceptions.ConnectionError as e: + raise LookupError(f"PyPI API Connection error: {pip_api_url}") from e else: if response.status_code == 200: return response.json() @@ -919,8 +916,8 @@ def get_tag_date(tag_date): try: response = requests.get(biocontainers_api_url) - except requests.exceptions.ConnectionError: - raise LookupError("Could not connect to biocontainers.pro API") + except requests.exceptions.ConnectionError as e: + raise LookupError("Could not connect to biocontainers.pro API") from e else: if response.status_code == 200: try: @@ -962,8 +959,8 @@ def get_tag_date(tag_date): if singularity_image is None: raise LookupError(f"Could not find singularity container for {package}") return docker_image_name, singularity_image["image_name"] - except TypeError: - raise LookupError(f"Could not find docker or singularity container for {package}") + except TypeError as e: + raise LookupError(f"Could not find docker or singularity container for {package}") from e elif response.status_code != 404: raise LookupError(f"Unexpected response code `{response.status_code}` for {biocontainers_api_url}") elif response.status_code == 404: @@ -1008,7 +1005,7 @@ def is_file_binary(path): binary_extensions = [".jpeg", ".jpg", ".png", ".zip", ".gz", ".jar", ".tar"] # Check common file extensions - _, file_extension = os.path.splitext(path) + file_extension = Path(path).suffix if file_extension in binary_extensions: return True @@ -1031,6 +1028,8 @@ def prompt_remote_pipeline_name(wfs): AssertionError, if pipeline cannot be found """ + if not is_interactive(): + raise UserWarning("No pipeline name provided and session is not interactive (no TTY detected).") pipeline = questionary.autocomplete( "Pipeline name:", choices=[wf.name for wf in wfs.remote_workflows], @@ -1046,7 +1045,7 @@ def prompt_remote_pipeline_name(wfs): if pipeline.count("/") == 1: try: gh_api.safe_get(f"https://api.github.com/repos/{pipeline}") - except Exception: + except requests.exceptions.RequestException: # No repo found - pass and raise error at the end pass else: @@ -1075,7 +1074,7 @@ def prompt_pipeline_release_branch( # Releases if len(wf_releases) > 0: - for tag in map(lambda release: release.get("tag_name"), wf_releases): + for tag in (release.get("tag_name") for release in wf_releases): tag_display = [ ("fg:ansiblue", f"{tag} "), ("class:choice-default", "[release]"), @@ -1084,7 +1083,7 @@ def prompt_pipeline_release_branch( tag_set.append(str(tag)) # Branches - for branch in wf_branches.keys(): + for branch in wf_branches: branch_display = [ ("fg:ansiyellow", f"{branch} "), ("class:choice-default", "[branch]"), @@ -1095,6 +1094,9 @@ def prompt_pipeline_release_branch( if len(choices) == 0: return [], [] + if not is_interactive(): + raise UserWarning("No release/branch specified and session is not interactive (no TTY detected).") + if multiple: return ( questionary.checkbox("Select release / branch:", choices=choices, style=nfcore_question_style).unsafe_ask(), @@ -1115,7 +1117,7 @@ class SingularityCacheFilePathValidator(questionary.Validator): def validate(self, value): if len(value.text): - if os.path.isfile(value.text): + if Path(value.text).is_file(): return True else: raise questionary.ValidationError( @@ -1150,12 +1152,10 @@ def get_repo_releases_branches(pipeline, wfs): pipeline = wf.full_name # Store releases and stop loop - wf_releases = list( - sorted( - wf.releases, - key=lambda k: k.get("published_at_timestamp", 0), - reverse=True, - ) + wf_releases = sorted( + wf.releases, + key=lambda k: k.get("published_at_timestamp", 0), + reverse=True, ) break @@ -1176,12 +1176,10 @@ def get_repo_releases_branches(pipeline, wfs): raise AssertionError(f"Not able to find pipeline '{pipeline}'") except AttributeError: # Success! We have a list, which doesn't work with .get() which is looking for a dict key - wf_releases = list( - sorted( - rel_r.json(), - key=lambda k: k.get("published_at_timestamp", 0), - reverse=True, - ) + wf_releases = sorted( + rel_r.json(), + key=lambda k: k.get("published_at_timestamp", 0), + reverse=True, ) # Get release tag commit hashes @@ -1370,6 +1368,8 @@ class NFCoreYamlLintConfig(BaseModel): """ Lint for included configs """ local_component_structure: bool | None = None """ Lint local components use correct structure mirroring remote""" + container_configs: bool | None = None + """ Lint that container configuration files in conf/ are up to date """ rocrate_readme_sync: bool | None = None """ Lint for README.md and rocrate.json sync """ @@ -1467,8 +1467,10 @@ def load_tools_config(directory: str | Path = ".") -> tuple[Path | None, NFCoreY except ValidationError as e: error_message = f"Config file '{config_fn}' is invalid" for error in e.errors(): - error_message += f"\n{error['loc'][0]}: {error['msg']}\ninput: {error['input']}" - raise AssertionError(error_message) + error_message += ( + f"\n{'.'.join(str(loc) for loc in error['loc'])}: {error['msg']}\nGot instead: {error['input']}" + ) + raise AssertionError(error_message) from e wf_config = fetch_wf_config(Path(directory)) if nf_core_yaml_config["repository_type"] == "pipeline" and wf_config: @@ -1698,46 +1700,3 @@ def get_wf_files(wf_path: Path): wf_files.append(str(path)) return wf_files - - -# General textual-related functions and objects - - -class HelpText(Markdown): - """A class to show a text box with help text.""" - - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - - def show(self) -> None: - """Method to show the help text box.""" - self.add_class("displayed") - - def hide(self) -> None: - """Method to hide the help text box.""" - self.remove_class("displayed") - - -class LoggingConsole(RichLog): - file = False - console: Widget - - def print(self, content): - self.write(content) - - -class ShowLogs(Message): - """Custom message to show the logging messages.""" - - pass - - -# Functions -def add_hide_class(app, widget_id: str) -> None: - """Add class 'hide' to a widget. Not display widget.""" - app.get_widget_by_id(widget_id).add_class("hide") - - -def remove_hide_class(app, widget_id: str) -> None: - """Remove class 'hide' to a widget. Display widget.""" - app.get_widget_by_id(widget_id).remove_class("hide") diff --git a/pyproject.toml b/pyproject.toml index c6d1231810..a9e78a29e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ nf_core = ["**/*"] [project] name = "nf-core" -version = "3.6.0dev" +version = "4.0.3" description = "Helper tools for use with nf-core Nextflow pipelines." readme = "README.md" license = "MIT" @@ -40,7 +40,7 @@ dependencies = [ "markdown>=3.3", "packaging", "pillow", - "pre-commit", + "prek", "prompt_toolkit<=3.0.52", "pydantic>=2.2.1", "pyyaml", @@ -54,7 +54,7 @@ dependencies = [ "repo2rocrate", "setuptools<81", "tabulate", - "textual==7.5.0", + "textual==8.2.4", "trogon", "pdiff", "ruamel.yaml", @@ -62,12 +62,11 @@ dependencies = [ [project.optional-dependencies] dev = [ - "mypy", + "mypy>=2.0.0", "myst_parser", "pytest-cov", "pytest-datafiles", "responses", - "prek", "ruff", "Sphinx", "sphinx-rtd-theme", @@ -121,7 +120,23 @@ target-version = "py310" cache-dir = "~/.cache/ruff" [tool.ruff.lint] -select = ["I", "E1", "E4", "E7", "E9", "F", "UP", "N"] +select = [ + "I", # isort + "E1", "E4", "E7", "E9","E902", # pycodestyle errors (partial) + "F", # pyflakes + "UP", # pyupgrade + "N", # pep8-naming + "B", # flake8-bugbear - catches common bugs and design problems + "RSE", # flake8-raise - unnecessary parentheses on raised exceptions + "PTH", # flake8-use-pathlib - prefer pathlib over os.path + "SIM", # flake8-simplify - simplifies code patterns + "C4", # flake8-comprehensions - better list/dict/set comprehensions + "BLE", # flake8-blind-except - catches blind except clauses + "W", # pycodestyle warnings + "A", # flake8-builtins - catches shadowing of Python builtins + "PIE", # flake8-pie - misc lints for common issues +] +ignore = ["PTH123", "PIE790"] [tool.ruff.lint.isort] known-first-party = ["nf_core"] diff --git a/tests/components/test_components_generate_snapshot.py b/tests/components/test_components_generate_snapshot.py index 39293ad1ea..b6ec4058f3 100644 --- a/tests/components/test_components_generate_snapshot.py +++ b/tests/components/test_components_generate_snapshot.py @@ -124,6 +124,26 @@ def test_update_snapshot_module(self): assert "Single-End" in snap_content assert snap_content["Single-End"]["timestamp"] != original_timestamp + def test_assertion_failure_no_prompts(self): + """Assertion failures in no_prompts mode should raise UserWarning, not silently pass""" + with set_wd(self.nfcore_modules): + test_file = Path("modules", "nf-core-test", "bwa", "mem", "tests", "main.nf.test") + original_content = test_file.read_text() + test_file.write_text(original_content.replace("then {", "then {\n assert false", 1)) + try: + snap_generator = ComponentsTest( + component_type="modules", + component_name="bwa/mem", + no_prompts=True, + remote_url=GITLAB_URL, + branch=GITLAB_NFTEST_BRANCH, + ) + with pytest.raises(UserWarning) as e: + snap_generator.run() + finally: + test_file.write_text(original_content) + assert "Assertion failed." in str(e.value) + def test_test_not_found(self): """Generate the snapshot for a module in nf-core/modules clone which doesn't contain tests""" with set_wd(self.nfcore_modules): diff --git a/tests/components/test_components_utils.py b/tests/components/test_components_utils.py index ca85639dd2..754612a9ea 100644 --- a/tests/components/test_components_utils.py +++ b/tests/components/test_components_utils.py @@ -62,8 +62,8 @@ def test_get_biotools_id(self): with responses.RequestsMock() as rsps: mock_biotools_api_calls(rsps, "bpipe") response = nf_core.components.components_utils.get_biotools_response("bpipe") - id = nf_core.components.components_utils.get_biotools_id(response, "bpipe") - assert id == "biotools:bpipe" + biotools_id = nf_core.components.components_utils.get_biotools_id(response, "bpipe") + assert biotools_id == "biotools:bpipe" def test_get_biotools_id_warn(self): """Test getting the bio.tools ID for a tool and failing""" @@ -117,11 +117,11 @@ def test_environment_variables_override(self): try: with mock.patch.dict(os.environ, mock_env): importlib.reload(nf_core.components.constants) - assert nf_core.components.constants.NF_CORE_MODULES_NAME == mock_env["NF_CORE_MODULES_NAME"] - assert nf_core.components.constants.NF_CORE_MODULES_REMOTE == mock_env["NF_CORE_MODULES_REMOTE"] + assert mock_env["NF_CORE_MODULES_NAME"] == nf_core.components.constants.NF_CORE_MODULES_NAME + assert mock_env["NF_CORE_MODULES_REMOTE"] == nf_core.components.constants.NF_CORE_MODULES_REMOTE assert ( - nf_core.components.constants.NF_CORE_MODULES_DEFAULT_BRANCH - == mock_env["NF_CORE_MODULES_DEFAULT_BRANCH"] + mock_env["NF_CORE_MODULES_DEFAULT_BRANCH"] + == nf_core.components.constants.NF_CORE_MODULES_DEFAULT_BRANCH ) finally: importlib.reload(nf_core.components.constants) diff --git a/tests/conftest.py b/tests/conftest.py index fbaf10dfbd..00731ed215 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,30 @@ """Global pytest configuration for nf-core tests setting up worker-specific cache directories to avoid git lock issues.""" +import contextlib import os import shutil +import sys import tempfile +from unittest import mock + +import pytest + + +@pytest.fixture(autouse=True) +def _mock_interactive_session(): + """Mock TTY detection so tests behave as if running in an interactive session. + + Tests run in CI without a TTY, which causes is_interactive() to return False + and auto-sets no_prompts=True. This fixture ensures consistent interactive + behavior by default. Tests that specifically need non-interactive behavior + can override with their own mock. + """ + with ( + mock.patch.object(sys.stdin, "isatty", return_value=True), + mock.patch.object(sys.stdout, "isatty", return_value=True), + mock.patch.object(sys.stderr, "isatty", return_value=True), + ): + yield def pytest_configure(config): @@ -42,12 +64,8 @@ def pytest_unconfigure(config): # Clean up temporary directories if hasattr(config, "_temp_cache_dir"): - try: + with contextlib.suppress(OSError, FileNotFoundError): shutil.rmtree(config._temp_cache_dir) - except (OSError, FileNotFoundError): - pass if hasattr(config, "_temp_config_dir"): - try: + with contextlib.suppress(OSError, FileNotFoundError): shutil.rmtree(config._temp_config_dir) - except (OSError, FileNotFoundError): - pass diff --git a/tests/data/mock_pipeline_containers/conf/base.config b/tests/data/mock_pipeline_containers/conf/base.config index 07aff59b98..a87a26956d 100644 --- a/tests/data/mock_pipeline_containers/conf/base.config +++ b/tests/data/mock_pipeline_containers/conf/base.config @@ -15,7 +15,7 @@ process { memory = { 6.GB * task.attempt } time = { 4.h * task.attempt } - errorStrategy = { task.exitStatus in ((130..145) + 104 + 175) ? 'retry' : 'finish' } + errorStrategy = { task.exitStatus in ((130..145) + 104 + (175..177)) ? 'retry' : 'finish' } maxRetries = 1 maxErrors = '-1' diff --git a/tests/data/mock_pipeline_containers/modules/nf-core/rmarkdownnotebook/main.nf b/tests/data/mock_pipeline_containers/modules/nf-core/rmarkdownnotebook/main.nf index 43eac5bf30..4cea759bbc 100644 --- a/tests/data/mock_pipeline_containers/modules/nf-core/rmarkdownnotebook/main.nf +++ b/tests/data/mock_pipeline_containers/modules/nf-core/rmarkdownnotebook/main.nf @@ -8,7 +8,7 @@ process RMARKDOWNNOTEBOOK { //dependencies for your analysis. The container at least needs to contain the //yaml and rmarkdown R packages. conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/mulled-v2-31ad840d814d356e5f98030a4ee308a16db64ec5:0e852a1e4063fdcbe3f254ac2c7469747a60e361-0' : 'biocontainers/mulled-v2-31ad840d814d356e5f98030a4ee308a16db64ec5:0e852a1e4063fdcbe3f254ac2c7469747a60e361-0' }" diff --git a/tests/data/mock_pipeline_containers/nextflow.config b/tests/data/mock_pipeline_containers/nextflow.config index a031baae31..d07c04a405 100644 --- a/tests/data/mock_pipeline_containers/nextflow.config +++ b/tests/data/mock_pipeline_containers/nextflow.config @@ -57,9 +57,7 @@ process { memory = { 6.GB * task.attempt } time = { 4.h * task.attempt } - // 175 signals that the Pipeline had an unrecoverable error while - // restoring a Snapshot via Fusion Snapshots. - errorStrategy = { task.exitStatus in ((130..145) + 104 + 175) ? 'retry' : 'finish' } + errorStrategy = { task.exitStatus in ((130..145) + 104 + (175..177)) ? 'retry' : 'finish' } maxRetries = 1 maxErrors = '-1' } diff --git a/tests/data/mock_pipeline_containers/subworkflows/local/utils_nfcore_mock-pipeline_pipeline/main.nf b/tests/data/mock_pipeline_containers/subworkflows/local/utils_nfcore_mock-pipeline_pipeline/main.nf index c2f8a26bea..497a033c04 100644 --- a/tests/data/mock_pipeline_containers/subworkflows/local/utils_nfcore_mock-pipeline_pipeline/main.nf +++ b/tests/data/mock_pipeline_containers/subworkflows/local/utils_nfcore_mock-pipeline_pipeline/main.nf @@ -9,9 +9,9 @@ */ -include { completionSummary } from '../../nf-core/utils_nfcore_pipeline' -include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' -include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' +include { completionSummary } from '../../nf-core/utils_nfcore_pipeline' +include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' +include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -20,14 +20,13 @@ include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipelin */ workflow PIPELINE_INITIALISATION { - take: - version // boolean: Display version and exit - validate_params // boolean: Boolean whether to validate parameters against the schema at runtime - monochrome_logs // boolean: Do not use coloured log outputs + version // boolean: Display version and exit + validate_params // boolean: Boolean whether to validate parameters against the schema at runtime + monochrome_logs // boolean: Do not use coloured log outputs nextflow_cli_args // array: List of positional nextflow CLI args - outdir // string: The output directory where the results will be saved - input // string: Path to input samplesheet + outdir // string: The output directory where the results will be saved + input // string: Path to input samplesheet main: @@ -36,17 +35,17 @@ workflow PIPELINE_INITIALISATION { // // Print version and exit if required and dump pipeline parameters to JSON file // - UTILS_NEXTFLOW_PIPELINE ( + UTILS_NEXTFLOW_PIPELINE( version, true, outdir, - workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1 + workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1, ) // // Check config provided to the pipeline // - UTILS_NFCORE_PIPELINE ( + UTILS_NFCORE_PIPELINE( nextflow_cli_args ) @@ -58,23 +57,22 @@ workflow PIPELINE_INITIALISATION { .fromPath(params.input) .splitCsv(header: true, strip: true) .map { row -> - [[id:row.sample], row.fastq_1, row.fastq_2] + [[id: row.sample], row.fastq_1, row.fastq_2] } - .map { - meta, fastq_1, fastq_2 -> - if (!fastq_2) { - return [ meta.id, meta + [ single_end:true ], [ fastq_1 ] ] - } else { - return [ meta.id, meta + [ single_end:false ], [ fastq_1, fastq_2 ] ] - } + .map { meta, fastq_1, fastq_2 -> + if (!fastq_2) { + return [meta.id, meta + [single_end: true], [fastq_1]] + } + else { + return [meta.id, meta + [single_end: false], [fastq_1, fastq_2]] + } } .groupTuple() .map { samplesheet -> validateInputSamplesheet(samplesheet) } - .map { - meta, fastqs -> - return [ meta, fastqs.flatten() ] + .map { meta, fastqs -> + return [meta, fastqs.flatten()] } .set { ch_samplesheet } @@ -90,11 +88,10 @@ workflow PIPELINE_INITIALISATION { */ workflow PIPELINE_COMPLETION { - take: - outdir // path: Path to output directory where results will be published + outdir // path: Path to output directory where results will be published monochrome_logs // boolean: Disable ANSI colour codes in log output - multiqc_report // string: Path to MultiQC report + multiqc_report // string: Path to MultiQC report main: summary_params = [:] @@ -109,7 +106,8 @@ workflow PIPELINE_COMPLETION { } workflow.onError { - log.error "Pipeline failed. Please refer to troubleshooting docs: https://nf-co.re/docs/usage/troubleshooting" + // TODO docs-v2 where has this page gone?! + log.error("Pipeline failed. Please refer to troubleshooting docs: https://nf-co.re/docs/usage/troubleshooting") } } @@ -126,12 +124,12 @@ def validateInputSamplesheet(input) { def (metas, fastqs) = input[1..2] // Check that multiple runs of the same sample are of the same datatype i.e. single-end / paired-end - def endedness_ok = metas.collect{ meta -> meta.single_end }.unique().size == 1 + def endedness_ok = metas.collect { meta -> meta.single_end }.unique().size == 1 if (!endedness_ok) { error("Please check input samplesheet -> Multiple runs of a sample must be of the same datatype i.e. single-end or paired-end: ${metas[0].id}") } - return [ metas[0], fastqs ] + return [metas[0], fastqs] } // // Generate methods description for MultiQC @@ -141,11 +139,11 @@ def toolCitationText() { // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "Tool (Foo et al. 2023)" : "", // Uncomment function in methodsDescriptionText to render in MultiQC report def citation_text = [ - "Tools used in the workflow included:", - "FastQC (Andrews 2010),", - "MultiQC (Ewels et al. 2016)", - "." - ].join(' ').trim() + "Tools used in the workflow included:", + "FastQC (Andrews 2010),", + "MultiQC (Ewels et al. 2016)", + ".", + ].join(' ').trim() return citation_text } @@ -155,9 +153,9 @@ def toolBibliographyText() { // Can use ternary operators to dynamically construct based conditions, e.g. params["run_xyz"] ? "
  • Author (2023) Pub name, Journal, DOI
  • " : "", // Uncomment function in methodsDescriptionText to render in MultiQC report def reference_text = [ - "
  • Andrews S, (2010) FastQC, URL: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/).
  • ", - "
  • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: /10.1093/bioinformatics/btw354
  • " - ].join(' ').trim() + "
  • Andrews S, (2010) FastQC, URL: https://www.bioinformatics.babraham.ac.uk/projects/fastqc/).
  • ", + "
  • Ewels, P., Magnusson, M., Lundin, S., & Käller, M. (2016). MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics , 32(19), 3047–3048. doi: /10.1093/bioinformatics/btw354
  • ", + ].join(' ').trim() return reference_text } @@ -179,7 +177,10 @@ def methodsDescriptionText(mqc_methods_yaml) { temp_doi_ref += "(doi: ${doi_ref.replace("https://doi.org/", "").replace(" ", "")}), " } meta["doi_text"] = temp_doi_ref.substring(0, temp_doi_ref.length() - 2) - } else meta["doi_text"] = "" + } + else { + meta["doi_text"] = "" + } meta["nodoi_text"] = meta.manifest_map.doi ? "" : "
  • If available, make sure to update the text to include the Zenodo DOI of version of the pipeline used.
  • " // Tool references @@ -193,7 +194,7 @@ def methodsDescriptionText(mqc_methods_yaml) { def methods_text = mqc_methods_yaml.text - def engine = new groovy.text.SimpleTemplateEngine() + def engine = new groovy.text.SimpleTemplateEngine() def description_html = engine.createTemplate(methods_text).make(meta) return description_html.toString() diff --git a/tests/modules/lint/test_environment_yml.py b/tests/modules/lint/test_environment_yml.py index f6886d2ea8..21a44226b3 100644 --- a/tests/modules/lint/test_environment_yml.py +++ b/tests/modules/lint/test_environment_yml.py @@ -179,7 +179,7 @@ def test_environment_yml_invalid_files(tmp_path, invalid_content, filename): """Test that invalid YAML files raise exceptions""" test_file, module, lint = setup_test_environment(tmp_path, invalid_content, filename) - with pytest.raises(Exception): + with pytest.raises(ruamel.yaml.YAMLError): environment_yml(lint, module) diff --git a/tests/modules/lint/test_main_nf.py b/tests/modules/lint/test_main_nf.py index 3725d5f20f..f6c3848f71 100644 --- a/tests/modules/lint/test_main_nf.py +++ b/tests/modules/lint/test_main_nf.py @@ -10,6 +10,7 @@ ) from ...test_modules import TestModules +from ...utils import GITLAB_NFTEST_BRANCH, GITLAB_URL from .test_lint_utils import MockModuleLint @@ -51,7 +52,7 @@ def test_process_labels(content, passed, warned, failed): ('container "quay.io/nf-core/gatk:4.4.0.0" //Biocontainers is missing a package', 2, 0, 0), # Multi-line container definition should pass ( - '''container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + '''container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/gatk4:4.4.0.0--py36hdfd78af_0': 'biocontainers/gatk4:4.4.0.0--py36hdfd78af_0' }"''', 6, @@ -60,7 +61,7 @@ def test_process_labels(content, passed, warned, failed): ), # Space in container URL should fail ( - '''container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + '''container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/gatk4:4.4.0.0--py36hdfd78af_0 ': 'biocontainers/gatk4:4.4.0.0--py36hdfd78af_0' }"''', 5, @@ -69,7 +70,7 @@ def test_process_labels(content, passed, warned, failed): ), # Incorrect quoting of container string should fail ( - '''container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + '''container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/gatk4:4.4.0.0--py36hdfd78af_0 ': "biocontainers/gatk4:4.4.0.0--py36hdfd78af_0" }"''', 4, @@ -124,7 +125,7 @@ def test_main_nf_lint_with_alternative_registry(self): """Test main.nf linting with alternative container registry""" # Test with alternative registry - should warn/fail when containers don't match the registry module_lint = nf_core.modules.lint.ModuleLint(directory=self.pipeline_dir, registry="public.ecr.aws") - module_lint.lint(print_results=False, module="samtools/sort") + module_lint.lint(print_results=True, module="samtools/sort") # Alternative registry should produce warnings or failures for container mismatches # since samtools/sort module likely uses biocontainers/quay.io, not public.ecr.aws @@ -144,11 +145,12 @@ def test_topics_and_emits_version_check(self): self.mods_install_gitlab_nftest.install("fastqc") # Lint a module installed from the gitlab test branch; gitlab test modules that is known to have versions YAML in main.nf - module_lint = nf_core.modules.lint.ModuleLint(directory=self.pipeline_dir) + module_lint = nf_core.modules.lint.ModuleLint( + directory=self.pipeline_dir, remote_url=GITLAB_URL, branch=GITLAB_NFTEST_BRANCH + ) module_lint.lint(print_results=False, module="fastqc") - assert len(module_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}" - assert any(w.lint_test in ("main_nf_version_emit", "main_nf_version_topic") for w in module_lint.warned), ( - f"Expected warning about missing version topic, got {[w.message for w in module_lint.warned]}" + assert any(f.lint_test in ("main_nf_version_emit", "main_nf_version_topic") for f in module_lint.failed), ( + f"Expected failure about missing version topic, got {[f.message for f in module_lint.failed]}" ) assert len(module_lint.passed) > 0 @@ -423,13 +425,13 @@ def test_get_outputs_with_hidden_attribute(tmp_path): # Check that the path pattern doesn't include "hidden: true" path_key = list(prof_output[0][1].keys())[0] - assert '"*.{prof,pidx}*"' == path_key, f"Expected '\"*.{{prof,pidx}}*\"', got '{path_key}'" + assert path_key == '"*.{prof,pidx}*"', f"Expected '\"*.{{prof,pidx}}*\"', got '{path_key}'" assert "hidden" not in path_key, f"Pattern should not contain 'hidden': {path_key}" # Check the data output also doesn't include "hidden: true" data_output = component.outputs["data"] data_path_key = list(data_output[0].keys())[0] - assert '"data.csv"' == data_path_key, f"Expected '\"data.csv\"', got '{data_path_key}'" + assert data_path_key == '"data.csv"', f"Expected '\"data.csv\"', got '{data_path_key}'" assert "hidden" not in data_path_key, f"Pattern should not contain 'hidden': {data_path_key}" @@ -524,6 +526,425 @@ def test_get_topics_version_yml_path_no_parens(tmp_path): ) +def test_meta_input_names_valid_sequential(tmp_path): + """Test that valid sequential meta input names (meta, meta2, meta3, meta4) pass validation""" + main_nf_content = """ +process TEST_PROCESS { + input: + tuple val(meta), path(reads) + tuple val(meta2), path(index) + tuple val(meta3), path(database) + tuple val(meta4), path(reference) + + output: + tuple val(meta), path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + assert any("meta_input_names" in str(p) for p in mock_lint.passed), ( + f"Expected meta_input_names in passed, got: {mock_lint.passed}" + ) + assert len(mock_lint.failed) == 0, f"Expected no failures, got: {mock_lint.failed}" + assert len(mock_lint.warned) == 0, f"Expected no warnings, got: {mock_lint.warned}" + + +def test_meta_input_names_invalid_underscore(tmp_path): + """Test that invalid meta input names with underscores (meta_vcf, meta_gex) fail validation""" + main_nf_content = """ +process TEST_PROCESS { + input: + tuple val(meta_vcf), path(reads) + tuple val(meta_gex), path(index) + val(meta_ab) + + output: + tuple val(meta_vcf), path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + assert any("meta_input_names" in str(f) for f in mock_lint.failed), ( + f"Expected meta_input_names in failed, got: {mock_lint.failed}" + ) + # Check that the error message mentions the invalid names + failed_msg = str(mock_lint.failed[0]) + assert "meta_vcf" in failed_msg, f"Expected 'meta_vcf' in error message, got: {failed_msg}" + assert "meta_gex" in failed_msg, f"Expected 'meta_gex' in error message, got: {failed_msg}" + assert "meta_ab" in failed_msg, f"Expected 'meta_ab' in error message, got: {failed_msg}" + + +def test_meta_input_names_invalid_meta1(tmp_path): + """Test that meta0 and meta1 fail validation (only meta, meta2, meta3... are allowed)""" + main_nf_content = """ +process TEST_PROCESS { + input: + tuple val(meta), path(reads) + tuple val(meta0), path(index) + tuple val(meta1), path(database) + + output: + tuple val(meta), path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + assert any("meta_input_names" in str(f) for f in mock_lint.failed), ( + f"Expected meta_input_names in failed, got: {mock_lint.failed}" + ) + failed_msg = str(mock_lint.failed[0]) + assert "meta0" in failed_msg, f"Expected 'meta0' in error message, got: {failed_msg}" + assert "meta1" in failed_msg, f"Expected 'meta1' in error message, got: {failed_msg}" + + +def test_meta_input_names_invalid_leading_zeros(tmp_path): + """Test that meta variables with leading zeros (meta01, meta02, meta003) fail validation""" + main_nf_content = """ +process TEST_PROCESS { + input: + tuple val(meta), path(reads) + tuple val(meta01), path(index) + tuple val(meta02), path(database) + tuple val(meta003), path(reference) + + output: + tuple val(meta), path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + assert any("meta_input_names" in str(f) for f in mock_lint.failed), ( + f"Expected meta_input_names in failed, got: {mock_lint.failed}" + ) + failed_msg = str(mock_lint.failed[0]) + assert "meta01" in failed_msg, f"Expected 'meta01' in error message, got: {failed_msg}" + assert "meta02" in failed_msg, f"Expected 'meta02' in error message, got: {failed_msg}" + assert "meta003" in failed_msg, f"Expected 'meta003' in error message, got: {failed_msg}" + + +def test_meta_input_names_non_sequential_order(tmp_path): + """Test that non-sequential meta numbering (meta, meta3, meta2) produces a warning""" + main_nf_content = """ +process TEST_PROCESS { + input: + tuple val(meta), path(reads) + tuple val(meta3), path(database) + tuple val(meta2), path(index) + + output: + tuple val(meta), path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + assert any("meta_input_names" in str(w) for w in mock_lint.warned), ( + f"Expected meta_input_names in warned, got: {mock_lint.warned}" + ) + warned_msg = str(mock_lint.warned[0]) + assert "sequential" in warned_msg.lower(), f"Expected 'sequential' in warning message, got: {warned_msg}" + + +def test_meta_input_names_gap_in_sequence(tmp_path): + """Test that meta numbering with gaps (meta, meta2, meta5) produces a warning""" + main_nf_content = """ +process TEST_PROCESS { + input: + tuple val(meta), path(reads) + tuple val(meta2), path(index) + tuple val(meta5), path(database) + + output: + tuple val(meta), path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + assert any("meta_input_names" in str(w) for w in mock_lint.warned), ( + f"Expected meta_input_names in warned, got: {mock_lint.warned}" + ) + warned_msg = str(mock_lint.warned[0]) + assert "sequential" in warned_msg.lower(), f"Expected 'sequential' in warning message, got: {warned_msg}" + + +def test_meta_input_names_no_meta_variables(tmp_path): + """Test that modules without meta inputs don't trigger validation""" + main_nf_content = """ +process TEST_PROCESS { + input: + path(reads) + val(sample_id) + tuple val(condition), path(reference) + + output: + path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + # Should have no passed/failed/warned for meta_input_names since there are no meta inputs + assert not any("meta_input_names" in str(p) for p in mock_lint.passed), ( + f"Should not have meta_input_names in passed when no meta vars, got: {mock_lint.passed}" + ) + assert not any("meta_input_names" in str(f) for f in mock_lint.failed), ( + f"Should not have meta_input_names in failed when no meta vars, got: {mock_lint.failed}" + ) + assert not any("meta_input_names" in str(w) for w in mock_lint.warned), ( + f"Should not have meta_input_names in warned when no meta vars, got: {mock_lint.warned}" + ) + + +def test_meta_input_names_only_meta(tmp_path): + """Test that a single 'meta' input passes validation""" + main_nf_content = """ +process TEST_PROCESS { + input: + tuple val(meta), path(reads) + + output: + tuple val(meta), path("*.bam"), emit: bam + + script: + "echo test" +} +""" + main_nf_path = tmp_path / "main.nf" + main_nf_path.write_text(main_nf_content) + + mock_lint = MockModuleLint() + mock_lint.main_nf = main_nf_path + + component = NFCoreComponent( + component_name="test", + repo_url=None, + component_dir=tmp_path, + repo_type="modules", + base_dir=tmp_path, + component_type="modules", + remote_component=False, + ) + + component.get_inputs_from_main_nf() + flattened_inputs = [] + for inputs in component.inputs: + if isinstance(inputs, list): + flattened_inputs.extend([list(i.keys())[0] for i in inputs]) + else: + flattened_inputs.append(list(inputs.keys())[0]) + + from nf_core.modules.lint.main_nf import check_meta_input_names + + check_meta_input_names(mock_lint, flattened_inputs) + + assert any("meta_input_names" in str(p) for p in mock_lint.passed), ( + f"Expected meta_input_names in passed, got: {mock_lint.passed}" + ) + assert len(mock_lint.failed) == 0, f"Expected no failures, got: {mock_lint.failed}" + assert len(mock_lint.warned) == 0, f"Expected no warnings, got: {mock_lint.warned}" + + def test_validate_meta_keys(): """Test validation of meta keys in script""" mock_lint = MockModuleLint() @@ -536,6 +957,7 @@ def test_validate_meta_keys(): def prefix = "${meta.id}" def se = meta.single_end def id = meta.subMap(['id']) + def m2id = meta2?.id """ ], ) @@ -549,12 +971,14 @@ def id = meta.subMap(['id']) """ def sample = meta.sample def strand = meta.strandedness + def m2opts = meta2?.options """ ], ) assert len(mock_lint.failed) == 1 assert "meta.sample" in mock_lint.failed[0][2] assert "meta.strandedness" in mock_lint.failed[0][2] + assert "meta2?.options" in mock_lint.failed[0][2] # meta2/meta3 with valid keys mock_lint.passed, mock_lint.failed = [], [] @@ -562,7 +986,7 @@ def strand = meta.strandedness mock_lint, [ """ - def id1 = meta.id + def id1 = meta?.id def id2 = meta2.id def se = meta3.single_end """ @@ -601,6 +1025,7 @@ def args = task.ext.args ?: '' def args2 = task.ext.args2 ?: '' def args3 = task.ext.args3 ?: '' def prefix = task.ext.prefix ?: "${meta.id}" + def prefix2 = task.ext.prefix2 ?: '' def use_gpu = task.ext.use_gpu ? '--gpu' : '' """ ], @@ -616,6 +1041,9 @@ def use_gpu = task.ext.use_gpu ? '--gpu' : '' def args1 = task.ext.args1 ?: '' def custom = task.ext.custom ?: '' def suffix = task.ext.suffix ?: '.bam' + def prefix1 = task.ext.prefix1 ?: '' + def prefix3 = task.ext.prefix3 ?: '' + def prefix22 = task.ext.prefix22 ?: '' """ ], ) @@ -623,6 +1051,9 @@ def suffix = task.ext.suffix ?: '.bam' assert "ext.args1" in mock_lint.failed[0][2] assert "ext.custom" in mock_lint.failed[0][2] assert "ext.suffix" in mock_lint.failed[0][2] + assert "ext.prefix1" in mock_lint.failed[0][2] + assert "ext.prefix3" in mock_lint.failed[0][2] + assert "ext.prefix22" in mock_lint.failed[0][2] # ext.argsN where N >= 2 should be valid mock_lint.passed, mock_lint.failed = [], [] diff --git a/tests/modules/lint/test_meta_yml.py b/tests/modules/lint/test_meta_yml.py index f26c310399..4bfe262981 100644 --- a/tests/modules/lint/test_meta_yml.py +++ b/tests/modules/lint/test_meta_yml.py @@ -86,32 +86,26 @@ def test_modules_meta_yml_multiple_versions_channels(self): with open(main_nf_path, "w") as fh: fh.write(main_nf_modified) - try: - # Run lint with fix to update meta.yml - module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=True) - module_lint.lint(print_results=False, module="bpipe/test") - - # Read the updated meta.yml - meta_yml_path = Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "meta.yml") - with open(meta_yml_path) as fh: - meta_yml = yaml.safe_load(fh) - - # Check that topics.versions is now a list with multiple entries - assert "topics" in meta_yml, "topics should be present in meta.yml" - assert "versions" in meta_yml["topics"], "versions should be present in topics" - - # For multiple versions channels, topics.versions should be a list - versions_topic = meta_yml["topics"]["versions"] - assert isinstance(versions_topic, list) and all(isinstance(item, list) for item in versions_topic) - # Two versions channels: should have 2 entries - assert len(versions_topic) == 2, ( - f"Expected 2 entries in versions topic, got {len(versions_topic)}: {versions_topic}" - ) - - finally: - # Restore original main.nf - with open(main_nf_path, "w") as fh: - fh.write(main_nf_original) + # Run lint with fix to update meta.yml + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=True) + module_lint.lint(print_results=False, module="bpipe/test") + + # Read the updated meta.yml + meta_yml_path = Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "meta.yml") + with open(meta_yml_path) as fh: + meta_yml = yaml.safe_load(fh) + + # Check that topics.versions is now a list with multiple entries + assert "topics" in meta_yml, "topics should be present in meta.yml" + assert "versions" in meta_yml["topics"], "versions should be present in topics" + + # For multiple versions channels, topics.versions should be a list + versions_topic = meta_yml["topics"]["versions"] + assert isinstance(versions_topic, list) and all(isinstance(item, list) for item in versions_topic) + # Two versions channels: should have 2 entries + assert len(versions_topic) == 2, ( + f"Expected 2 entries in versions topic, got {len(versions_topic)}: {versions_topic}" + ) def test_modules_meta_yml_missing_topic_fixed(self): """Test that a missing topic in meta.yml is added when running lint --fix""" @@ -122,8 +116,6 @@ def test_modules_meta_yml_missing_topic_fixed(self): # Read the original meta.yml meta_yml_path = Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "meta.yml") - with open(meta_yml_path) as fh: - meta_yml_original = fh.read() # Add a topic to main.nf main_nf_modified = main_nf_original.replace("emit: sequence_report", "emit: sequence_report, topic: versions") @@ -141,40 +133,32 @@ def test_modules_meta_yml_missing_topic_fixed(self): with open(meta_yml_path, "w") as fh: fh.write(yaml.dump(meta_yml)) - try: - # First lint without fix should fail - module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=False) - module_lint.lint(print_results=False, module="bpipe/test") - - # Should have failures for missing topics - has_topic_failure = any( - result.lint_test in ["has_meta_topics", "correct_meta_topics"] for result in module_lint.failed - ) - assert has_topic_failure, "Expected failure for missing topics in meta.yml" - - # Now lint with fix to add the missing topic - module_lint_fix = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=True) - module_lint_fix.lint(print_results=False, module="bpipe/test") - - # Read the updated meta.yml - with open(meta_yml_path) as fh: - meta_yml_updated = yaml.safe_load(fh) - - # Check that topics section was added - assert "topics" in meta_yml_updated, "topics should be added to meta.yml after fix" - assert "versions" in meta_yml_updated["topics"], "reports topic should be present in meta.yml" - - # Check that the versions topic has the correct structure - versions_topic = meta_yml_updated["topics"]["versions"] - assert isinstance(versions_topic, list), "versions topic should be a list" - assert len(versions_topic[0]) == 3, "versions topic should have 1 entrie with 3 elements" - - finally: - # Restore original files - with open(main_nf_path, "w") as fh: - fh.write(main_nf_original) - with open(meta_yml_path, "w") as fh: - fh.write(meta_yml_original) + # First lint without fix should fail + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=False) + module_lint.lint(print_results=False, module="bpipe/test") + + # Should have failures for missing topics + has_topic_failure = any( + result.lint_test in ["has_meta_topics", "correct_meta_topics"] for result in module_lint.failed + ) + assert has_topic_failure, "Expected failure for missing topics in meta.yml" + + # Now lint with fix to add the missing topic + module_lint_fix = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=True) + module_lint_fix.lint(print_results=False, module="bpipe/test") + + # Read the updated meta.yml + with open(meta_yml_path) as fh: + meta_yml_updated = yaml.safe_load(fh) + + # Check that topics section was added + assert "topics" in meta_yml_updated, "topics should be added to meta.yml after fix" + assert "versions" in meta_yml_updated["topics"], "reports topic should be present in meta.yml" + + # Check that the versions topic has the correct structure + versions_topic = meta_yml_updated["topics"]["versions"] + assert isinstance(versions_topic, list), "versions topic should be a list" + assert len(versions_topic[0]) == 3, "versions topic should have 1 entrie with 3 elements" def test_modules_meta_yml_val_topic_gets_string_type(self): """Test that val() keywords in topics get type: string, not type: eval""" @@ -185,8 +169,6 @@ def test_modules_meta_yml_val_topic_gets_string_type(self): # Read the original meta.yml meta_yml_path = Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "meta.yml") - with open(meta_yml_path) as fh: - meta_yml_original = fh.read() # Modify main.nf to use val() for version instead of eval() # This simulates a module like fastk that has hardcoded version @@ -206,39 +188,49 @@ def test_modules_meta_yml_val_topic_gets_string_type(self): with open(main_nf_path, "w") as fh: fh.write(main_nf_modified) - try: - # Run lint with fix to update meta.yml - module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=True) - module_lint.lint(print_results=False, module="bpipe/test") - - assert len(module_lint.failed) == 0, f"Expected 0 failed tests, got {len(module_lint.failed)}" - - # Read the updated meta.yml - with open(meta_yml_path) as fh: - meta_yml = yaml.safe_load(fh) - - # Check that topics.versions exists - assert "topics" in meta_yml, "topics should be present in meta.yml" - assert "versions" in meta_yml["topics"], "versions should be present in topics" - - # Check the structure - should be [[{process: {}}, {tool: {}}, {version: {}}]] - versions_topic = meta_yml["topics"]["versions"] - assert isinstance(versions_topic, list), "versions topic should be a list" - assert len(versions_topic) == 1, f"Expected 1 entry in versions topic, got {len(versions_topic)}" - assert len(versions_topic[0]) == 3, f"Expected 3 elements in versions entry, got {len(versions_topic[0])}" - - # Check that the third element (version) has type: string (since it's val(), not eval()) - version_element = versions_topic[0][2] # Third element is the version - version_key = list(version_element.keys())[0] - version_meta = version_element[version_key] - assert "type" in version_meta, f"Version element should have a type field: {version_meta}" - assert version_meta["type"] == "string", ( - f"Version element should have type: string (since it's val()), but got type: {version_meta['type']}" - ) - - finally: - # Restore original files - with open(main_nf_path, "w") as fh: - fh.write(main_nf_original) - with open(meta_yml_path, "w") as fh: - fh.write(meta_yml_original) + # Run lint with fix to update meta.yml + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=True) + module_lint.lint(print_results=False, module="bpipe/test") + + assert len(module_lint.failed) == 0, f"Expected 0 failed tests, got {len(module_lint.failed)}" + + # Read the updated meta.yml + with open(meta_yml_path) as fh: + meta_yml = yaml.safe_load(fh) + + # Check that topics.versions exists + assert "topics" in meta_yml, "topics should be present in meta.yml" + assert "versions" in meta_yml["topics"], "versions should be present in topics" + + # Check the structure - should be [[{process: {}}, {tool: {}}, {version: {}}]] + versions_topic = meta_yml["topics"]["versions"] + assert isinstance(versions_topic, list), "versions topic should be a list" + assert len(versions_topic) == 1, f"Expected 1 entry in versions topic, got {len(versions_topic)}" + assert len(versions_topic[0]) == 3, f"Expected 3 elements in versions entry, got {len(versions_topic[0])}" + + # Check that the third element (version) has type: string (since it's val(), not eval()) + version_element = versions_topic[0][2] # Third element is the version + version_key = list(version_element.keys())[0] + version_meta = version_element[version_key] + assert "type" in version_meta, f"Version element should have a type field: {version_meta}" + assert version_meta["type"] == "string", ( + f"Version element should have type: string (since it's val()), but got type: {version_meta['type']}" + ) + + def test_modules_meta_yml_no_input_section(self): + """Test that lint --fix does not crash when meta.yml has no input section (e.g. parameter-only modules)""" + meta_yml_path = self.bpipe_test_module_path / "meta.yml" + + with open(meta_yml_path) as fh: + meta_yml = yaml.safe_load(fh) + + del meta_yml["input"] + + with open(meta_yml_path, "w") as fh: + fh.write(yaml.dump(meta_yml)) + + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules, fix=True) + module_lint.lint(print_results=True, module="bpipe/test") + # Should not raise UnboundLocalError — just complete without crashing + + assert len(module_lint.failed) == 1, "meta.yml linting failed" diff --git a/tests/modules/lint/test_module_lint_remotes.py b/tests/modules/lint/test_module_lint_remotes.py index 57cb797440..6b8429d8c7 100644 --- a/tests/modules/lint/test_module_lint_remotes.py +++ b/tests/modules/lint/test_module_lint_remotes.py @@ -23,19 +23,19 @@ def test_modules_lint_no_gitlab(self): def test_modules_lint_gitlab_modules(self): """Lint modules from a different remote""" - self.mods_install_gitlab.install("fastqc") - self.mods_install_gitlab.install("multiqc") + self.mods_install_gitlab_nftest.install("fastqc") + self.mods_install_gitlab_nftest.install("multiqc") module_lint = nf_core.modules.lint.ModuleLint(directory=self.pipeline_dir, remote_url=GITLAB_URL) module_lint.lint(print_results=False, all_modules=True) - assert len(module_lint.failed) == 2 + assert len(module_lint.failed) == 3 assert len(module_lint.passed) > 0 assert len(module_lint.warned) >= 0 def test_modules_lint_multiple_remotes(self): """Lint modules from a different remote""" - self.mods_install_gitlab.install("multiqc") + self.mods_install_gitlab_nftest.install("multiqc") module_lint = nf_core.modules.lint.ModuleLint(directory=self.pipeline_dir, remote_url=GITLAB_URL) - module_lint.lint(print_results=False, all_modules=True) - assert len(module_lint.failed) == 1 + module_lint.lint(print_results=True, all_modules=True) + assert len(module_lint.failed) == 2 assert len(module_lint.passed) > 0 assert len(module_lint.warned) >= 0 diff --git a/tests/modules/lint/test_module_tests.py b/tests/modules/lint/test_module_tests.py index b5f361c7d8..49fc3607f4 100644 --- a/tests/modules/lint/test_module_tests.py +++ b/tests/modules/lint/test_module_tests.py @@ -4,7 +4,6 @@ from git.repo import Repo import nf_core.modules.lint -import nf_core.modules.patch from ...test_modules import TestModules from ...utils import GITLAB_NFTEST_BRANCH, GITLAB_URL @@ -132,14 +131,15 @@ def test_nftest_failing_linting(self): Repo.clone_from(GITLAB_URL, self.nfcore_modules, branch=GITLAB_NFTEST_BRANCH) module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules) - module_lint.lint(print_results=False, module="kallisto/quant") + module_lint.lint(print_results=True, module="kallisto/quant") - assert len(module_lint.failed) == 2, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}" + assert len(module_lint.failed) == 3, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}" assert len(module_lint.passed) >= 0 assert len(module_lint.warned) >= 0 - assert module_lint.failed[0].lint_test == "meta_yml_valid" - assert module_lint.failed[1].lint_test == "test_main_tags" - assert "kallisto/index" in module_lint.failed[1].message + assert module_lint.failed[0].lint_test == "main_nf_version_topic" + assert module_lint.failed[1].lint_test == "meta_yml_valid" + assert module_lint.failed[2].lint_test == "test_main_tags" + assert "kallisto/index" in module_lint.failed[2].message def test_modules_absent_version(self): """Test linting a nf-test module if the versions is absent in the snapshot file `""" @@ -179,6 +179,162 @@ def test_modules_empty_file_in_snapshot(self): with open(snap_file, "w") as fh: fh.write(content) + def test_modules_stub_gzip_valid_syntax(self): + """Test linting a module with valid stub gzip syntax: echo "" | gzip > file.gz""" + main_nf = self.bpipe_test_module_path / "main.nf" + original_content = main_nf.read_text() + + # Replace the existing stub block with one that has valid gzip syntax + import re + + new_stub = ''' stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + echo "" | gzip > ${prefix}.txt.gz + touch ${prefix}.txt.gz.stats + touch ${prefix}.txt + """ +}''' + # Replace the existing stub block + new_content = re.sub(r"stub:.*?^}", new_stub, original_content, flags=re.DOTALL | re.MULTILINE) + + main_nf.write_text(new_content) + + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules) + module_lint.lint(print_results=False, module="bpipe/test") + + # Reset the file + main_nf.write_text(original_content) + + # Check that stub gzip syntax check passed + passed_tests = [x.lint_test if hasattr(x, "lint_test") else x[1] for x in module_lint.passed] + assert "test_stub_gzip_syntax" in passed_tests, "test_stub_gzip_syntax not found in passed tests" + assert len(module_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}" + + def test_modules_stub_gzip_invalid_syntax_touch(self): + """Test linting a module with invalid stub gzip syntax using touch""" + main_nf = self.bpipe_test_module_path / "main.nf" + original_content = main_nf.read_text() + + # Replace the existing stub block with one that uses touch (invalid) + import re + + new_stub = ''' stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + touch ${prefix}.txt.gz + touch ${prefix}.txt + """ +}''' + new_content = re.sub(r"stub:.*?^}", new_stub, original_content, flags=re.DOTALL | re.MULTILINE) + + main_nf.write_text(new_content) + + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules) + module_lint.lint(print_results=False, module="bpipe/test") + + # Reset the file + main_nf.write_text(original_content) + + # Check that stub gzip syntax check failed + assert len(module_lint.failed) >= 1, "Expected linting to fail" + failed_tests = [x.lint_test if hasattr(x, "lint_test") else x[1] for x in module_lint.failed] + assert "test_stub_gzip_syntax" in failed_tests, "test_stub_gzip_syntax not found in failed tests" + + def test_modules_stub_gzip_invalid_syntax_wrong_echo(self): + """Test linting a module with invalid stub gzip syntax using echo with non-empty string""" + main_nf = self.bpipe_test_module_path / "main.nf" + original_content = main_nf.read_text() + + # Replace the existing stub block with one that uses echo "stub" instead of echo "" + import re + + new_stub = ''' stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + echo "stub" | gzip > ${prefix}.txt.gz + touch ${prefix}.txt + """ +}''' + new_content = re.sub(r"stub:.*?^}", new_stub, original_content, flags=re.DOTALL | re.MULTILINE) + + main_nf.write_text(new_content) + + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules) + module_lint.lint(print_results=False, module="bpipe/test") + + # Reset the file + main_nf.write_text(original_content) + + # Check that stub gzip syntax check failed + assert len(module_lint.failed) >= 1, "Expected linting to fail" + failed_tests = [x.lint_test if hasattr(x, "lint_test") else x[1] for x in module_lint.failed] + assert "test_stub_gzip_syntax" in failed_tests, "test_stub_gzip_syntax not found in failed tests" + + def test_modules_stub_gzip_multiple_valid(self): + """Test linting a module with multiple valid stub gzip files""" + main_nf = self.bpipe_test_module_path / "main.nf" + original_content = main_nf.read_text() + + # Replace the existing stub block with one that has multiple valid gzip files + import re + + new_stub = ''' stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + echo "" | gzip > ${prefix}.txt.gz + echo "" | gzip > ${prefix}.vcf.gz + echo "" | gzip > ${prefix}.bam.gz + touch ${prefix}.txt + """ +}''' + new_content = re.sub(r"stub:.*?^}", new_stub, original_content, flags=re.DOTALL | re.MULTILINE) + + main_nf.write_text(new_content) + + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules) + module_lint.lint(print_results=False, module="bpipe/test") + + # Reset the file + main_nf.write_text(original_content) + + # Check that stub gzip syntax check passed + passed_tests = [x.lint_test if hasattr(x, "lint_test") else x[1] for x in module_lint.passed] + assert "test_stub_gzip_syntax" in passed_tests, "test_stub_gzip_syntax not found in passed tests" + assert len(module_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}" + + def test_modules_stub_gzip_multiple_valid_and_invalid(self): + """Test linting a module with multiple valid and invalid stub gzip files""" + main_nf = self.bpipe_test_module_path / "main.nf" + original_content = main_nf.read_text() + + # Replace the existing stub block with one that has both valid and invalid gzip patterns + import re + + new_stub = ''' stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + echo "" | gzip > ${prefix}.txt.gz + echo | gzip > ${prefix}.vcf.gz + echo "" | gzip > ${prefix}.bam.gz + touch ${prefix}.txt + """ +}''' + new_content = re.sub(r"stub:.*?^}", new_stub, original_content, flags=re.DOTALL | re.MULTILINE) + + main_nf.write_text(new_content) + + module_lint = nf_core.modules.lint.ModuleLint(directory=self.nfcore_modules) + module_lint.lint(print_results=False, module="bpipe/test") + + # Reset the file + main_nf.write_text(original_content) + + # Check that stub gzip syntax check failed (because one pattern is invalid) + assert len(module_lint.failed) >= 1, "Expected linting to fail" + failed_tests = [x.lint_test if hasattr(x, "lint_test") else x[1] for x in module_lint.failed] + assert "test_stub_gzip_syntax" in failed_tests, "test_stub_gzip_syntax not found in failed tests" + def test_modules_empty_file_in_stub_snapshot(self): """Test linting a nf-test module with an empty file sha sum in the stub test snapshot, which should make it not fail""" snap_file = self.bpipe_test_module_path / "tests" / "main.nf.test.snap" diff --git a/tests/modules/lint/test_patch.py b/tests/modules/lint/test_patch.py index 2c93f70b24..e42b0b84a6 100644 --- a/tests/modules/lint/test_patch.py +++ b/tests/modules/lint/test_patch.py @@ -55,6 +55,6 @@ def test_modules_lint_patched_modules(self): all_modules=True, ) - assert len(module_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}" + assert len(module_lint.failed) == 3, f"Linting failed with {[x.__dict__ for x in module_lint.failed]}" assert len(module_lint.passed) > 0 assert len(module_lint.warned) >= 0 diff --git a/tests/modules/test_bump_versions.py b/tests/modules/test_bump_versions.py index f3b377be33..63e6eb9c2c 100644 --- a/tests/modules/test_bump_versions.py +++ b/tests/modules/test_bump_versions.py @@ -1,4 +1,3 @@ -import os import re import tempfile from pathlib import Path @@ -18,7 +17,7 @@ class TestModulesBumpVersions(TestModules): def test_modules_bump_versions_single_module(self): """Test updating a single module""" # Change the bpipe/test version to an older version - env_yml_path = os.path.join(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "environment.yml") + env_yml_path = Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "environment.yml") with open(env_yml_path) as fh: content = fh.read() new_content = re.sub(r"bioconda::star=\d.\d.\d\D?", r"bioconda::star=2.6.1d", content) @@ -98,7 +97,7 @@ def test_modules_bump_versions_fail(self): def test_modules_bump_versions_fail_unknown_version(self): """Fail because of an unknown version""" # Change the bpipe/test version to an older version - env_yml_path = os.path.join(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "environment.yml") + env_yml_path = Path(self.nfcore_modules, "modules", "nf-core", "bpipe", "test", "environment.yml") with open(env_yml_path) as fh: content = fh.read() new_content = re.sub(r"bioconda::bpipe=\d.\d.\d\D?", r"bioconda::bpipe=xxx", content) diff --git a/tests/modules/test_create.py b/tests/modules/test_create.py index cf0efc6b04..88789b51f2 100644 --- a/tests/modules/test_create.py +++ b/tests/modules/test_create.py @@ -1,5 +1,3 @@ -import os -import shutil from pathlib import Path from unittest import mock @@ -7,12 +5,9 @@ import requests_cache import responses import yaml -from git.repo import Repo import nf_core.modules.create from tests.utils import ( - GITLAB_SUBWORKFLOWS_ORG_PATH_BRANCH, - GITLAB_URL, mock_anaconda_api_calls, mock_biocontainers_api_calls, mock_biotools_api_calls, @@ -32,7 +27,7 @@ def test_modules_create_succeed(self): ) with requests_cache.disabled(): module_create.create() - assert os.path.exists(os.path.join(self.pipeline_dir, "modules", "local", "trimgalore/main.nf")) + assert Path(self.pipeline_dir, "modules", "local", "trimgalore/main.nf").exists() def test_modules_create_fail_exists(self): """Fail at creating the same module twice""" @@ -44,9 +39,8 @@ def test_modules_create_fail_exists(self): ) with requests_cache.disabled(): module_create.create() - with pytest.raises(UserWarning) as excinfo: - with requests_cache.disabled(): - module_create.create() + with pytest.raises(UserWarning) as excinfo, requests_cache.disabled(): + module_create.create() assert "module directory exists:" in str(excinfo.value) def test_modules_create_nfcore_modules(self): @@ -59,10 +53,8 @@ def test_modules_create_nfcore_modules(self): ) with requests_cache.disabled(): module_create.create() - assert os.path.exists(os.path.join(self.nfcore_modules, "modules", "nf-core", "fastqc", "main.nf")) - assert os.path.exists( - os.path.join(self.nfcore_modules, "modules", "nf-core", "fastqc", "tests", "main.nf.test") - ) + assert Path(self.nfcore_modules, "modules", "nf-core", "fastqc", "main.nf").exists() + assert Path(self.nfcore_modules, "modules", "nf-core", "fastqc", "tests", "main.nf.test").exists() def test_modules_create_nfcore_modules_subtool(self): """Create a tool/subtool module in a nf-core/modules clone""" @@ -74,95 +66,8 @@ def test_modules_create_nfcore_modules_subtool(self): ) with requests_cache.disabled(): module_create.create() - assert os.path.exists(os.path.join(self.nfcore_modules, "modules", "nf-core", "star", "index", "main.nf")) - assert os.path.exists( - os.path.join(self.nfcore_modules, "modules", "nf-core", "star", "index", "tests", "main.nf.test") - ) - - @mock.patch("rich.prompt.Confirm.ask") - def test_modules_migrate(self, mock_rich_ask): - """Create a module with the --migrate-pytest option to convert pytest to nf-test""" - pytest_dir = Path(self.nfcore_modules, "tests", "modules", "nf-core", "samtools", "sort") - module_dir = Path(self.nfcore_modules, "modules", "nf-core", "samtools", "sort") - - # Clone modules repo with pytests - shutil.rmtree(self.nfcore_modules) - Repo.clone_from(GITLAB_URL, self.nfcore_modules, branch=GITLAB_SUBWORKFLOWS_ORG_PATH_BRANCH) - with open(module_dir / "main.nf") as fh: - old_main_nf = fh.read() - with open(module_dir / "meta.yml") as fh: - old_meta_yml = fh.read() - - # Create a module with --migrate-pytest - mock_rich_ask.return_value = True - module_create = nf_core.modules.create.ModuleCreate(self.nfcore_modules, "samtools/sort", migrate_pytest=True) - module_create.create() - - with open(module_dir / "main.nf") as fh: - new_main_nf = fh.read() - with open(module_dir / "meta.yml") as fh: - new_meta_yml = fh.read() - nextflow_config = module_dir / "tests" / "nextflow.config" - - # Check that old files have been copied to the new module - assert old_main_nf == new_main_nf - assert old_meta_yml == new_meta_yml - assert nextflow_config.is_file() - - # Check that pytest folder is deleted - assert not pytest_dir.is_dir() - - # Check that pytest_modules.yml is updated - with open(Path(self.nfcore_modules, "tests", "config", "pytest_modules.yml")) as fh: - modules_yml = yaml.safe_load(fh) - assert "samtools/sort" not in modules_yml.keys() - - @mock.patch("rich.prompt.Confirm.ask") - def test_modules_migrate_no_delete(self, mock_rich_ask): - """Create a module with the --migrate-pytest option to convert pytest to nf-test. - Test that pytest directory is not deleted.""" - pytest_dir = Path(self.nfcore_modules, "tests", "modules", "nf-core", "samtools", "sort") - - # Clone modules repo with pytests - shutil.rmtree(self.nfcore_modules) - Repo.clone_from(GITLAB_URL, self.nfcore_modules, branch=GITLAB_SUBWORKFLOWS_ORG_PATH_BRANCH) - - # Create a module with --migrate-pytest - mock_rich_ask.return_value = False - module_create = nf_core.modules.create.ModuleCreate(self.nfcore_modules, "samtools/sort", migrate_pytest=True) - module_create.create() - - # Check that pytest folder is not deleted - assert pytest_dir.is_dir() - - # Check that pytest_modules.yml is updated - with open(Path(self.nfcore_modules, "tests", "config", "pytest_modules.yml")) as fh: - modules_yml = yaml.safe_load(fh) - assert "samtools/sort" not in modules_yml.keys() - - @mock.patch("rich.prompt.Confirm.ask") - def test_modules_migrate_symlink(self, mock_rich_ask): - """Create a module with the --migrate-pytest option to convert pytest with symlinks to nf-test. - Test that the symlink is deleted and the file is copied.""" - - pytest_dir = Path(self.nfcore_modules, "tests", "modules", "nf-core", "samtools", "sort") - module_dir = Path(self.nfcore_modules, "modules", "nf-core", "samtools", "sort") - - # Clone modules repo with pytests - shutil.rmtree(self.nfcore_modules) - Repo.clone_from(GITLAB_URL, self.nfcore_modules, branch=GITLAB_SUBWORKFLOWS_ORG_PATH_BRANCH) - - # Create a symlinked file in the pytest directory - symlink_file = pytest_dir / "symlink_file.txt" - symlink_file.symlink_to(module_dir / "main.nf") - - # Create a module with --migrate-pytest - mock_rich_ask.return_value = True - module_create = nf_core.modules.create.ModuleCreate(self.nfcore_modules, "samtools/sort", migrate_pytest=True) - module_create.create() - - # Check that symlink is deleted - assert not symlink_file.is_symlink() + assert Path(self.nfcore_modules, "modules", "nf-core", "star", "index", "main.nf").exists() + assert Path(self.nfcore_modules, "modules", "nf-core", "star", "index", "tests", "main.nf.test").exists() def test_modules_meta_yml_structure_biotools_meta(self): """Test the structure of the module meta.yml file when it was generated with INFORMATION from bio.tools and WITH a meta.""" diff --git a/tests/modules/test_modules_json.py b/tests/modules/test_modules_json.py index 029eb32ccd..db0e522dba 100644 --- a/tests/modules/test_modules_json.py +++ b/tests/modules/test_modules_json.py @@ -23,7 +23,7 @@ def test_get_modules_json(self): try: mod_json_sb = json.load(fh) except json.JSONDecodeError as e: - raise UserWarning(f"Unable to load JSON file '{mod_json_path}' due to error {e}") + raise UserWarning(f"Unable to load JSON file '{mod_json_path}' due to error {e}") from e mod_json_obj = ModulesJson(self.pipeline_dir) mod_json = mod_json_obj.get_modules_json() @@ -40,10 +40,10 @@ def test_mod_json_update(self): mod_json = mod_json_obj.get_modules_json() assert "MODULE_NAME" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"] assert "git_sha" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"] - assert "GIT_SHA" == mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["git_sha"] + assert mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["git_sha"] == "GIT_SHA" assert ( - NF_CORE_MODULES_DEFAULT_BRANCH - == mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["branch"] + mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["branch"] + == NF_CORE_MODULES_DEFAULT_BRANCH ) assert ( "modules" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["installed_by"] @@ -214,7 +214,7 @@ def test_mod_json_dump(self): try: mod_json_new = json.load(f) except json.JSONDecodeError as e: - raise UserWarning(f"Unable to load JSON file '{mod_json_path}' due to error {e}") + raise UserWarning(f"Unable to load JSON file '{mod_json_path}' due to error {e}") from e assert mod_json == mod_json_new def test_mod_json_with_empty_modules_value(self): diff --git a/tests/modules/test_modules_utils.py b/tests/modules/test_modules_utils.py index 9a19f06159..8ef8d9d404 100644 --- a/tests/modules/test_modules_utils.py +++ b/tests/modules/test_modules_utils.py @@ -45,7 +45,7 @@ def test_filter_modules_by_name_tool_family(self): # Test filtering by tool family (super-tool) filtered = nf_core.modules.modules_utils.filter_modules_by_name(nfcore_modules, "samtools") - assert set(m.component_name for m in filtered) == {"samtools/view", "samtools/sort", "samtools/index"} + assert {m.component_name for m in filtered} == {"samtools/view", "samtools/sort", "samtools/index"} def test_filter_modules_by_name_exact_match_preferred(self): """Test that exact matches are preferred over prefix matches""" diff --git a/tests/modules/test_patch.py b/tests/modules/test_patch.py index f608278618..a77701b8ca 100644 --- a/tests/modules/test_patch.py +++ b/tests/modules/test_patch.py @@ -1,4 +1,3 @@ -import os import tempfile from pathlib import Path from unittest import mock @@ -324,11 +323,13 @@ def test_create_patch_update_fail(self): ).install_component_files(BISMARK_ALIGN, FAIL_SHA, update_obj.modules_repo, temp_dir) temp_module_dir = temp_dir / BISMARK_ALIGN - for file in os.listdir(temp_module_dir): - assert file in os.listdir(module_path) - with open(module_path / file) as fh: + temp_files = {f.name for f in temp_module_dir.iterdir()} + module_files = {f.name for f in module_path.iterdir()} + for file_name in temp_files: + assert file_name in module_files + with open(module_path / file_name) as fh: installed = fh.read() - with open(temp_module_dir / file) as fh: + with open(temp_module_dir / file_name) as fh: shouldbe = fh.read() assert installed == shouldbe @@ -359,6 +360,7 @@ def test_remove_patch(self): with mock.patch.object(nf_core.components.patch.questionary, "confirm") as mock_questionary: mock_questionary.unsafe_ask.return_value = True + patch_obj.no_prompts = False patch_obj.remove(BISMARK_ALIGN) # Check that the diff file has been removed assert not (module_path / patch_fn).exists() diff --git a/tests/modules/test_remove.py b/tests/modules/test_remove.py index 2caece7ce5..c63b3d4377 100644 --- a/tests/modules/test_remove.py +++ b/tests/modules/test_remove.py @@ -1,4 +1,3 @@ -import os from pathlib import Path from ..test_modules import TestModules @@ -11,7 +10,7 @@ def test_modules_remove_trimgalore(self): assert self.mods_install.directory is not None module_path = Path(self.mods_install.directory, "modules", "nf-core", "modules", "trimgalore") assert self.mods_remove.remove("trimgalore") - assert os.path.exists(module_path) is False + assert not module_path.exists() def test_modules_remove_trimgalore_uninstalled(self): """Test removing TrimGalore! module without installing it""" @@ -23,4 +22,4 @@ def test_modules_remove_multiqc_from_gitlab(self): assert self.mods_install.directory is not None module_path = Path(self.mods_install_gitlab.directory, "modules", "nf-core-test", "multiqc") assert self.mods_remove_gitlab.remove("multiqc", force=True) - assert os.path.exists(module_path) is False + assert not module_path.exists() diff --git a/tests/modules/test_update.py b/tests/modules/test_update.py index 807f67cb81..b061fe185a 100644 --- a/tests/modules/test_update.py +++ b/tests/modules/test_update.py @@ -43,6 +43,20 @@ def test_install_and_update(self): assert update_obj.update("trimgalore") is True assert cmp_component(trimgalore_tmpdir, trimgalore_path) is True + @mock.patch("nf_core.components.update.ComponentUpdate.update_linked_components") + @mock.patch( + "nf_core.components.update.ComponentUpdate.get_components_to_update", + return_value=([], [{"name": "fake_sw", "git_remote": "x"}]), + ) + def test_module_update_skip_deps_does_not_cascade(self, mock_get_linked, mock_cascade): + """`module update --skip-deps` skips cascade even when linked subworkflows exist.""" + assert self.mods_install_old.install("trimgalore") + update_obj = ModuleUpdate( + self.pipeline_dir, show_diff=False, skip_deps=True, remote_url=GITLAB_URL, branch=OLD_TRIMGALORE_BRANCH + ) + assert update_obj.update("trimgalore") is True + assert not mock_cascade.called + def test_install_at_hash_and_update(self): """Installs an old version of a module in the pipeline and updates it""" assert self.mods_install_old.install("trimgalore") @@ -167,7 +181,7 @@ def test_update_with_config_fixed_version(self): # Fix the trimgalore version in the .nf-core.yml to an old version update_config = {GITLAB_URL: {GITLAB_REPO: {"trimgalore": OLD_TRIMGALORE_SHA}}} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) @@ -192,7 +206,7 @@ def test_update_with_config_dont_update(self): # Set the trimgalore field to no update in the .nf-core.yml update_config = {GITLAB_URL: {GITLAB_REPO: {"trimgalore": False}}} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) @@ -221,7 +235,7 @@ def test_update_with_config_fix_all(self): # Fix the version of all nf-core modules in the .nf-core.yml to an old version update_config = {GITLAB_URL: OLD_TRIMGALORE_SHA} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) @@ -245,7 +259,7 @@ def test_update_with_config_no_updates(self): # Fix the version of all nf-core modules in the .nf-core.yml to an old version update_config = {GITLAB_URL: False} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) diff --git a/tests/pipelines/__snapshots__/test_create_app/test_welcome.svg b/tests/pipelines/__snapshots__/test_create_app/test_welcome.svg index 20f852d0d3..02187399bb 100644 --- a/tests/pipelines/__snapshots__/test_create_app/test_welcome.svg +++ b/tests/pipelines/__snapshots__/test_create_app/test_welcome.svg @@ -211,7 +211,7 @@ - + nf-core pipelines create — Create a new pipeline with the nf-core pipeline templa… @@ -232,12 +232,12 @@ 💡 If you want to add a pipeline to nf-core, please join on Slack and discuss your plans with the community as early as possible; ideally before you start on your pipeline! See the -nf-core guidelines and the #new-pipelines Slack channel for more information. - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Let's go!  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - +nf-core pipeline contribution documentation and the #new-pipelines Slack channel for more +information. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Let's go!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/pipelines/download/test_container.py b/tests/pipelines/download/test_container.py index 63fa7b9c07..b472128d9f 100644 --- a/tests/pipelines/download/test_container.py +++ b/tests/pipelines/download/test_container.py @@ -5,7 +5,6 @@ import pytest import rich.progress_bar import rich.table -import rich.text from nf_core.pipelines.download.container_fetcher import ContainerProgress @@ -60,13 +59,12 @@ def test_download_progress_sub_task(self): assert progress.tasks == [] # Add another sub-task, this time that raises an exception - with pytest.raises(ValueError): - with progress.sub_task("Sub-task", total=28) as sub_task_id: - assert sub_task_id == 1 - assert len(progress.tasks) == 1 - assert progress.task_ids[0] == sub_task_id - assert progress.tasks[0].total == 28 - raise ValueError("This is a test error") + with pytest.raises(ValueError), progress.sub_task("Sub-task", total=28) as sub_task_id: + assert sub_task_id == 1 + assert len(progress.tasks) == 1 + assert progress.task_ids[0] == sub_task_id + assert progress.tasks[0].total == 28 + raise ValueError("This is a test error") # The sub-task should also be gone now assert progress.tasks == [] diff --git a/tests/pipelines/download/test_docker.py b/tests/pipelines/download/test_docker.py index 183d1591f1..ffd0b3b1cf 100644 --- a/tests/pipelines/download/test_docker.py +++ b/tests/pipelines/download/test_docker.py @@ -11,7 +11,6 @@ import pytest import rich.progress_bar import rich.table -import rich.text from nf_core.pipelines.download import DownloadWorkflow from nf_core.pipelines.download.docker import ( diff --git a/tests/pipelines/download/test_download.py b/tests/pipelines/download/test_download.py index 5ecaa5e47a..40228994cc 100644 --- a/tests/pipelines/download/test_download.py +++ b/tests/pipelines/download/test_download.py @@ -1,10 +1,12 @@ """Tests for the download subcommand of nf-core tools""" +import contextlib import json import logging import os import re import shutil +import subprocess import tempfile import unittest from pathlib import Path @@ -17,17 +19,25 @@ import nf_core.pipelines.list import nf_core.utils from nf_core.pipelines.download import DownloadWorkflow +from nf_core.pipelines.download.utils import DownloadError from nf_core.pipelines.download.workflow_repo import WorkflowRepo from nf_core.synced_repo import SyncedRepo -from nf_core.utils import ( - NF_INSPECT_MIN_NF_VERSION, - check_nextflow_version, -) +from nf_core.utils import NF_INSPECT_MIN_NF_VERSION, check_nextflow_version, get_nf_version from ...utils import TEST_DATA_DIR, with_temporary_folder +NF_STRICT_SYNTAX_VERSION = (26, 4, 0, False) + class DownloadTest(unittest.TestCase): + def setUp(self): + nf_ver = get_nf_version() + self.nf_strict_syntax = nf_ver is not None and nf_ver >= NF_STRICT_SYNTAX_VERSION + + def _strict_syntax_ctx(self, match: str = "nextflow inspect"): + """Return a context manager that expects a DownloadError on NF >= 26.04, or a no-op otherwise.""" + return pytest.raises(DownloadError, match=match) if self.nf_strict_syntax else contextlib.nullcontext() + @pytest.fixture(autouse=True) def use_caplog(self, caplog): self._caplog = caplog @@ -255,22 +265,20 @@ def test_containers_pipeline_singularity(self, tmp_path, mock_fetch_wf_config): mock_fetch_wf_config.return_value = {} # Run get containers with `nextflow inspect` + # NF >= 26.04 rejects old-style if/else container directives (mock_dsl2_old uses them) entrypoint = "main_passing_test.nf" - download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) - - # Store the containers found by the new method - found_containers = set(download_obj.containers) - - # Load the reference containers - with open(refererence_json_dir / f"{container_system}_containers.json") as fh: - ref_containers = json.load(fh) - ref_container_strs = set(ref_containers.values()) - - # Now check that they contain the same containers - assert found_containers == ref_container_strs, ( - f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" - f"Containers that should've been found: {ref_container_strs}" - ) + with self._strict_syntax_ctx(match="downgrade to Nextflow"): + download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) + + if not self.nf_strict_syntax: + found_containers = set(download_obj.containers) + with open(refererence_json_dir / f"{container_system}_containers.json") as fh: + ref_containers = json.load(fh) + ref_container_strs = set(ref_containers.values()) + assert found_containers == ref_container_strs, ( + f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" + f"Containers that should've been found: {ref_container_strs}" + ) # # Test that `find_container_images` (uses `nextflow inspect`) fetches the correct Docker images @@ -289,27 +297,91 @@ def test_containers_pipeline_docker(self, tmp_path, mock_fetch_wf_config): container_system = "docker" mock_pipeline_dir = TEST_DATA_DIR / "mock_pipeline_containers" refererence_json_dir = mock_pipeline_dir / "per_profile_output" - # First check that `-profile singularity` produces the same output as the reference + # First check that `-profile docker` produces the same output as the reference download_obj = DownloadWorkflow(pipeline="dummy", outdir=tmp_path, container_system=container_system) mock_fetch_wf_config.return_value = {} - # Run get containers with `nextflow inspect` + # NF >= 26.04 rejects old-style if/else container directives (mock_dsl2_old uses them) entrypoint = "main_passing_test.nf" - download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) - - # Store the containers found by the new method - found_containers = set(download_obj.containers) - - # Load the reference containers - with open(refererence_json_dir / f"{container_system}_containers.json") as fh: - ref_containers = json.load(fh) - ref_container_strs = set(ref_containers.values()) + with self._strict_syntax_ctx(match="downgrade to Nextflow"): + download_obj.find_container_images(mock_pipeline_dir, "dummy-revision", entrypoint=entrypoint) + + if not self.nf_strict_syntax: + found_containers = set(download_obj.containers) + with open(refererence_json_dir / f"{container_system}_containers.json") as fh: + ref_containers = json.load(fh) + ref_container_strs = set(ref_containers.values()) + assert found_containers == ref_container_strs, ( + f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" + f"Containers that should've been found: {ref_container_strs}" + ) - # Now check that they contain the same containers - assert found_containers == ref_container_strs, ( - f"Containers found in pipeline by `nextflow inspect`: {found_containers}\n" - f"Containers that should've been found: {ref_container_strs}" - ) + @mock.patch("nf_core.pipelines.download.download.run_cmd") + @mock.patch("nf_core.pipelines.list.Workflows.get_remote_workflows") + def test_find_container_images_retries_with_outdir_on_missing_param_error( + self, mock_get_remote_workflows, mock_run_cmd + ): + download_obj = DownloadWorkflow(container_system="docker") + workflow_directory = Path("workflow") + inspect_payload = json.dumps( + {"processes": [{"name": "NFCORE_TEST:STEP", "container": "quay.io/nf-core/test:1.0"}]} + ).encode() + has_retried = False + + def _run_cmd_side_effect(_executable, cmd_params): + nonlocal has_retried + if not has_retried: + has_retried = True + nf_output = ( + b"The following invalid input values have been detected:\n * Missing required parameter: --outdir\n" + ) + cause = subprocess.CalledProcessError(1, ["nextflow", "inspect"], output=nf_output) + exc = RuntimeError("Command failed") + exc.__cause__ = cause + raise exc + + assert "-params-file" in cmd_params + params_file_match = re.search(r'-params-file\s+"([^"]+)"', cmd_params) + assert params_file_match is not None + assert Path(params_file_match.group(1)).read_text() == 'outdir: "nf-core-tools-inspect"\n' + assert "--outdir" not in cmd_params + return (inspect_payload, b"") + + mock_run_cmd.side_effect = _run_cmd_side_effect + + download_obj.find_container_images(workflow_directory, "dummy-revision") + + assert len(mock_run_cmd.call_args_list) == 2 + assert set(download_obj.containers) == {"quay.io/nf-core/test:1.0"} + mock_get_remote_workflows.assert_called_once() + + @mock.patch("nf_core.pipelines.download.download.run_cmd") + @mock.patch("nf_core.pipelines.list.Workflows.get_remote_workflows") + def test_find_container_images_raises_on_unexpected_runtime_error(self, mock_get_remote_workflows, mock_run_cmd): + download_obj = DownloadWorkflow(container_system="docker") + mock_run_cmd.side_effect = RuntimeError("unexpected inspect failure") + + with pytest.raises(DownloadError, match="unexpected inspect failure"): + download_obj.find_container_images(Path("workflow"), "dummy-revision") + + assert mock_run_cmd.call_count == 1 + mock_get_remote_workflows.assert_called_once() + + @mock.patch("nf_core.pipelines.download.download.run_cmd") + @mock.patch("nf_core.pipelines.list.Workflows.get_remote_workflows") + def test_find_container_images_raises_on_strict_syntax_error(self, mock_get_remote_workflows, mock_run_cmd): + """Nextflow >= 26.04 rejects old if/else container directives; tool should give actionable guidance.""" + download_obj = DownloadWorkflow(container_system="docker") + cause = subprocess.CalledProcessError(1, ["nextflow", "inspect"], output=b"Error: Invalid process directive\n") + exc = RuntimeError("Command failed") + exc.__cause__ = cause + mock_run_cmd.side_effect = exc + + with pytest.raises(DownloadError, match="downgrade to Nextflow"): + download_obj.find_container_images(Path("workflow"), "dummy-revision") + + assert mock_run_cmd.call_count == 1 + mock_get_remote_workflows.assert_called_once() # # Tests for the main entry method 'download_workflow' @@ -339,7 +411,9 @@ def test_download_workflow_with_success(self, tmp_dir, mock_check_and_set_implem ) download_obj.include_configs = True # suppress prompt, because stderr.is_interactive doesn't. - download_obj.download_workflow() + # NF >= 26.04 fails on old-style container directives used in bamtofastq 2.2.0 + with self._strict_syntax_ctx(): + download_obj.download_workflow() # # Test Download for Seqera Platform @@ -388,9 +462,12 @@ def test_download_workflow_for_platform( assert isinstance(download_obj.outdir, Path) assert bool(re.search(r"nf-core-rnaseq_\d{4}-\d{2}-\d{1,2}_\d{1,2}-\d{1,2}", str(download_obj.outdir), re.S)) - download_obj.output_filename = download_obj.outdir.with_suffix(".git") - download_obj.download_workflow_platform(location=tmp_dir) + download_obj.output_filename = download_obj.outdir / "rnaseq.git" + # NF >= 26.04 fails on old-style container directives used in rnaseq 3.17.0 / 3.19.0 + with self._strict_syntax_ctx(): + download_obj.download_workflow_platform(location=tmp_dir) + # workflow_repo is populated before find_container_images is called, so it's accessible in both paths assert download_obj.workflow_repo assert isinstance(download_obj.workflow_repo, WorkflowRepo) assert issubclass(type(download_obj.workflow_repo), SyncedRepo) @@ -403,12 +480,13 @@ def test_download_workflow_for_platform( # assert that the download has a "latest" branch. assert "latest" in all_heads - # download_obj.download_workflow_platform(location=tmp_dir) will run `nextflow inspect` for each revision - # This means that the containers in download_obj.containers are the containers the last specified revision i.e. 3.17 - assert isinstance(download_obj.containers, list) and len(download_obj.containers) == 39 - assert ( - "https://depot.galaxyproject.org/singularity/bbmap:39.10--h92535d8_0" in download_obj.containers - ) # direct definition + if not self.nf_strict_syntax: + # download_obj.download_workflow_platform(location=tmp_dir) will run `nextflow inspect` for each revision + # This means that the containers in download_obj.containers are the containers the last specified revision i.e. 3.17 + assert isinstance(download_obj.containers, list) and len(download_obj.containers) == 39 + assert ( + "https://depot.galaxyproject.org/singularity/bbmap:39.10--h92535d8_0" in download_obj.containers + ) # direct definition # clean-up # remove "nf-core-rnaseq*" directories @@ -478,7 +556,7 @@ def test_download_workflow_for_platform_with_custom_tags(self, _, tmp_dir): ) = nf_core.utils.get_repo_releases_branches(download_obj.pipeline, wfs) download_obj.get_revision_hash() - download_obj.output_filename = f"{download_obj.outdir}.git" + download_obj.output_filename = download_obj.outdir / "rnaseq.git" download_obj.download_workflow_platform(location=tmp_dir) assert download_obj.workflow_repo diff --git a/tests/pipelines/download/test_singularity.py b/tests/pipelines/download/test_singularity.py index c7a05dbca3..eb6bfe1bbb 100644 --- a/tests/pipelines/download/test_singularity.py +++ b/tests/pipelines/download/test_singularity.py @@ -658,7 +658,7 @@ def test_file_download(self, outdir): output_path = outdir / Path(src_url).name downloader.download_file(src_url, output_path) assert (output_path).exists() - assert os.path.getsize(output_path) == 27 + assert output_path.stat().st_size == 27 assert ( "nf_core.pipelines.download.singularity", logging.DEBUG, @@ -704,7 +704,7 @@ def test_file_download(self, outdir): # Fire in the hole ! The download will be aborted and no output file will be created src_url = "https://github.com/nf-core/test-datasets/raw/refs/heads/modules/data/genomics/sarscov2/genome/genome.fasta.fai" output_path = outdir / Path(src_url).name - os.unlink(output_path) + output_path.unlink() downloader.kill_with_fire = True with pytest.raises(KeyboardInterrupt): downloader.download_file(src_url, output_path) diff --git a/tests/pipelines/download/test_utils.py b/tests/pipelines/download/test_utils.py index f3d35b333f..a04a65de15 100644 --- a/tests/pipelines/download/test_utils.py +++ b/tests/pipelines/download/test_utils.py @@ -1,4 +1,3 @@ -import os import subprocess import unittest from pathlib import Path @@ -33,7 +32,7 @@ def test_intermediate_file(self, outdir): tmp.write(b"Hello, World!") assert output_path.exists() - assert os.path.getsize(output_path) == 13 + assert output_path.stat().st_size == 13 assert not tmp_path.exists() # Run an external command as in pull_image @@ -43,40 +42,36 @@ def test_intermediate_file(self, outdir): subprocess.check_call([f"echo 'Hello, World!' > {tmp_path}"], shell=True) assert (output_path).exists() - assert os.path.getsize(output_path) == 14 # Extra \n ! + assert output_path.stat().st_size == 14 # Extra \n ! assert not (tmp_path).exists() # Code that fails. The file shall not exist # Directly write to the file and raise an exception output_path = outdir / "testfile3" - with pytest.raises(ValueError): - with intermediate_file(output_path) as tmp: - tmp_path = Path(tmp.name) - tmp.write(b"Hello, World!") - raise ValueError("This is a test error") + with pytest.raises(ValueError), intermediate_file(output_path) as tmp: + tmp_path = Path(tmp.name) + tmp.write(b"Hello, World!") + raise ValueError("This is a test error") assert not (output_path).exists() assert not (tmp_path).exists() # Run an external command and raise an exception output_path = outdir / "testfile4" - with pytest.raises(subprocess.CalledProcessError): - with intermediate_file(output_path) as tmp: - tmp_path = Path(tmp.name) - subprocess.check_call([f"echo 'Hello, World!' > {tmp_path}"], shell=True) - subprocess.check_call(["ls", "/dummy"]) + with pytest.raises(subprocess.CalledProcessError), intermediate_file(output_path) as tmp: + tmp_path = Path(tmp.name) + subprocess.check_call([f"echo 'Hello, World!' > {tmp_path}"], shell=True) + subprocess.check_call(["ls", "/dummy"]) assert not (output_path).exists() assert not (tmp_path).exists() # Test for invalid output paths - with pytest.raises(DownloadError): - with intermediate_file(outdir) as tmp: - pass + with pytest.raises(DownloadError), intermediate_file(outdir) as tmp: + pass output_path = outdir / "testfile5" - os.symlink("/dummy", output_path) - with pytest.raises(DownloadError): - with intermediate_file(output_path) as tmp: - pass + output_path.symlink_to("/dummy") + with pytest.raises(DownloadError), intermediate_file(output_path) as tmp: + pass diff --git a/tests/pipelines/lint/test_configs.py b/tests/pipelines/lint/test_configs.py index 7bb6329b5b..1c6fece3e3 100644 --- a/tests/pipelines/lint/test_configs.py +++ b/tests/pipelines/lint/test_configs.py @@ -2,7 +2,6 @@ import yaml -import nf_core.pipelines.create import nf_core.pipelines.lint from ..test_lint import TestLint @@ -21,7 +20,7 @@ def test_withname_in_modules_config(self): result = lint_obj.modules_config() assert len(result["failed"]) == 0 assert any( - ["`FASTQC` found in `conf/modules.config` and Nextflow scripts." in passed for passed in result["passed"]] + "`FASTQC` found in `conf/modules.config` and Nextflow scripts." in passed for passed in result["passed"] ) def test_superfluous_withname_in_modules_config_fails(self): diff --git a/tests/pipelines/lint/test_container_configs.py b/tests/pipelines/lint/test_container_configs.py new file mode 100644 index 0000000000..4610980466 --- /dev/null +++ b/tests/pipelines/lint/test_container_configs.py @@ -0,0 +1,134 @@ +from pathlib import Path +from unittest.mock import patch + +import git + +import nf_core.pipelines.lint + +from ..test_lint import TestLint + + +class TestLintContainerConfigs(TestLint): + def setUp(self) -> None: + super().setUp() + self.new_pipeline = self._make_pipeline_copy() + + # ------------------------------------------------------------------ + # helpers + # ------------------------------------------------------------------ + + def _lint(self): + lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline) + lint_obj._load() + return lint_obj.container_configs() + + def _write_container_cfg(self, name: str, content: str) -> Path: + path = Path(self.new_pipeline) / "conf" / name + path.write_text(content) + return path + + def _commit_container_cfg(self, name: str, content: str) -> Path: + """Write and git-commit a container config so it shows up in ``git ls-files``.""" + path = self._write_container_cfg(name, content) + repo = git.Repo(self.new_pipeline) + repo.index.add([str(path)]) + repo.index.commit(f"Add {name} for testing") + return path + + # ------------------------------------------------------------------ + # tests + # ------------------------------------------------------------------ + + def test_container_configs_up_to_date(self): + """Linting passes when generated configs match what is on disk.""" + content = "process { withName: 'FASTQC' { container = 'docker.io/biocontainers/fastqc:0.12.1' } }\n" + self._commit_container_cfg("containers_docker_amd64.config", content) + + def generate(cc_self): + (cc_self.workflow_directory / "conf" / "containers_docker_amd64.config").write_text(content) + return {"containers_docker_amd64.config"} + + with patch( + "nf_core.pipelines.containers_utils.ContainerConfigs.generate_container_configs", + autospec=True, + side_effect=generate, + ): + result = self._lint() + + assert len(result["failed"]) == 0 + assert any("up to date" in p for p in result["passed"]) + + def test_container_configs_out_of_date(self): + """Linting fails when generated configs differ from what is on disk.""" + old = "process { withName: 'FASTQC' { container = 'old_image' } }\n" + new = "process { withName: 'FASTQC' { container = 'new_image' } }\n" + self._commit_container_cfg("containers_docker_amd64.config", old) + + def generate(cc_self): + (cc_self.workflow_directory / "conf" / "containers_docker_amd64.config").write_text(new) + return {"containers_docker_amd64.config"} + + with patch( + "nf_core.pipelines.containers_utils.ContainerConfigs.generate_container_configs", + autospec=True, + side_effect=generate, + ): + result = self._lint() + + assert any("out of date" in f for f in result["failed"]) + + def test_container_configs_missing_file(self): + """Linting fails when generate produces a config that has never been committed to the repo.""" + content = "process { withName: 'FASTQC' { container = 'docker.io/biocontainers/fastqc:0.12.1' } }\n" + + repo = git.Repo(self.new_pipeline) + repo.index.remove(["conf/containers_docker_amd64.config"], working_tree=True) + repo.index.commit("remove container config to simulate missing file") + + def generate(cc_self): + (cc_self.workflow_directory / "conf" / "containers_docker_amd64.config").write_text(content) + return {"containers_docker_amd64.config"} + + with patch( + "nf_core.pipelines.containers_utils.ContainerConfigs.generate_container_configs", + autospec=True, + side_effect=generate, + ): + result = self._lint() + + assert any("missing" in f for f in result["failed"]) + + def test_container_configs_fix_overwrites_files(self): + """--fix overwrites out-of-date container config files and reports them as fixed.""" + old = "process { withName: 'FASTQC' { container = 'old_image' } }\n" + new = "process { withName: 'FASTQC' { container = 'new_image' } }\n" + cfg = self._commit_container_cfg("containers_docker_amd64.config", old) + + def generate(cc_self): + (cc_self.workflow_directory / "conf" / "containers_docker_amd64.config").write_text(new) + return {"containers_docker_amd64.config"} + + with patch( + "nf_core.pipelines.containers_utils.ContainerConfigs.generate_container_configs", + autospec=True, + side_effect=generate, + ): + lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline, fix=("container_configs",)) + lint_obj._load() + result = lint_obj.container_configs() + + assert len(result["failed"]) == 0 + assert any("overwritten" in f for f in result["fixed"]) + assert cfg.read_text() == new + + def test_container_configs_user_warning_warns(self): + """A UserWarning from ContainerConfigs (e.g. low NF version) produces a lint warning.""" + with patch( + "nf_core.pipelines.containers_utils.ContainerConfigs.generate_container_configs", + autospec=True, + side_effect=UserWarning("Nextflow version too low"), + ): + result = self._lint() + + assert len(result["failed"]) == 0 + assert any("Nextflow version too low" in w for w in result["warned"]) diff --git a/tests/pipelines/lint/test_if_empty_null.py b/tests/pipelines/lint/test_if_empty_null.py index a813a06569..3a324fcba8 100644 --- a/tests/pipelines/lint/test_if_empty_null.py +++ b/tests/pipelines/lint/test_if_empty_null.py @@ -2,7 +2,6 @@ import yaml -import nf_core.pipelines.create import nf_core.pipelines.lint from ..test_lint import TestLint diff --git a/tests/pipelines/lint/test_merge_markers.py b/tests/pipelines/lint/test_merge_markers.py index 3094d8f8d1..d724eb4e43 100644 --- a/tests/pipelines/lint/test_merge_markers.py +++ b/tests/pipelines/lint/test_merge_markers.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import nf_core.pipelines.lint @@ -10,10 +10,10 @@ def test_merge_markers_found(self): """Missing 'jobs' field should result in failure""" new_pipeline = self._make_pipeline_copy() - with open(os.path.join(new_pipeline, "main.nf")) as fh: + with open(Path(new_pipeline, "main.nf")) as fh: main_nf_content = fh.read() main_nf_content = ">>>>>>>\n" + main_nf_content - with open(os.path.join(new_pipeline, "main.nf"), "w") as fh: + with open(Path(new_pipeline, "main.nf"), "w") as fh: fh.write(main_nf_content) lint_obj = nf_core.pipelines.lint.PipelineLint(new_pipeline) diff --git a/tests/pipelines/lint/test_nextflow_config.py b/tests/pipelines/lint/test_nextflow_config.py index f8c3c1f31f..63c58b149b 100644 --- a/tests/pipelines/lint/test_nextflow_config.py +++ b/tests/pipelines/lint/test_nextflow_config.py @@ -1,10 +1,8 @@ -import os import re from pathlib import Path import yaml -import nf_core.pipelines.create.create import nf_core.pipelines.lint from ..test_lint import TestLint @@ -55,12 +53,13 @@ def test_nextflow_config_dev_in_release_mode_failed(self): def test_nextflow_config_missing_test_profile_failed(self): """Test failure if config file does not contain `test` profile.""" # Change the name of the test profile so there is no such profile - nf_conf_file = os.path.join(self.new_pipeline, "nextflow.config") + nf_conf_file = Path(self.new_pipeline, "nextflow.config") with open(nf_conf_file) as f: content = f.read() fail_content = re.sub(r"\btest\b", "testfail", content) with open(nf_conf_file, "w") as f: f.write(fail_content) + Path(self.new_pipeline, "conf", "testfail.config").touch() lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline) lint_obj.load_pipeline_config() result = lint_obj.nextflow_config() @@ -157,7 +156,7 @@ def test_default_values_float(self): content = f.read() fail_content = re.sub( r"validate_params\s*=\s*true", - "params.validate_params = true\ndummy = 0.000000001", + "validate_params = true\ndummy = 0.000000001", content, ) with open(nf_conf_file, "w") as f: @@ -189,7 +188,7 @@ def test_default_values_float_fail(self): content = f.read() fail_content = re.sub( r"validate_params\s*=\s*true", - "params.validate_params = true\ndummy = 0.000000001", + "validate_params = true\ndummy = 0.000000001", content, ) with open(nf_conf_file, "w") as f: diff --git a/tests/pipelines/lint/test_nf_test_content.py b/tests/pipelines/lint/test_nf_test_content.py index 8c623272a7..7009425a51 100644 --- a/tests/pipelines/lint/test_nf_test_content.py +++ b/tests/pipelines/lint/test_nf_test_content.py @@ -48,13 +48,13 @@ def test_nf_test_content_missing_nf_test_config_file(self): lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline) result = lint_obj.nf_test_content() assert len(result["failed"]) > 0 - assert "'nf-test.config' does not set a `testsDir`, it should contain `testsDir \".\"`" in result["failed"] + assert "'nf-test.config' does not set a `testsDir`, it should contain `testsDir = \".\"`." in result["failed"] assert ( - '\'nf-test.config\' does not set a `workDir`, it should contain `workDir System.getenv("NFT_WORKDIR") ?: ".nf-test"`' + '\'nf-test.config\' does not set a `workDir`, it should contain `workDir = System.getenv("NFT_WORKDIR") ?: ".nf-test"`.' in result["failed"] ) assert ( - "'nf-test.config' does not set a `configFile`, it should contain `configFile \"tests/nextflow.config\"`" + "'nf-test.config' does not set a `configFile`, it should contain `configFile = \"tests/nextflow.config\"`." in result["failed"] ) @@ -73,7 +73,6 @@ def test_nf_test_content_ignored(self): lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline) lint_obj._load() result = lint_obj.nf_test_content() - print(result) assert len(result["ignored"]) == 3 assert "'tests/default.nf.test' checking ignored" in result["ignored"] assert "'tests/nextflow.config' checking ignored" in result["ignored"] diff --git a/tests/pipelines/lint/test_rocrate_readme_sync.py b/tests/pipelines/lint/test_rocrate_readme_sync.py index cd600481e2..6ac0068e9c 100644 --- a/tests/pipelines/lint/test_rocrate_readme_sync.py +++ b/tests/pipelines/lint/test_rocrate_readme_sync.py @@ -19,7 +19,7 @@ def test_rocrate_readme_sync_fixed(self): try: rocrate = json.load(f) except json.JSONDecodeError as e: - raise UserWarning(f"Unable to load JSON file '{json_path}' due to error {e}") + raise UserWarning(f"Unable to load JSON file '{json_path}' due to error {e}") from e rocrate["@graph"][0]["description"] = "This is a test script" with open(json_path, "w") as f: json.dump(rocrate, f, indent=4) diff --git a/tests/pipelines/lint/test_template_strings.py b/tests/pipelines/lint/test_template_strings.py index 37b7604806..bc3e520792 100644 --- a/tests/pipelines/lint/test_template_strings.py +++ b/tests/pipelines/lint/test_template_strings.py @@ -3,7 +3,6 @@ import yaml -import nf_core.pipelines.create import nf_core.pipelines.lint from ..test_lint import TestLint diff --git a/tests/pipelines/lint/test_version_consistency.py b/tests/pipelines/lint/test_version_consistency.py index 32f24fa2d0..c7f7f28919 100644 --- a/tests/pipelines/lint/test_version_consistency.py +++ b/tests/pipelines/lint/test_version_consistency.py @@ -3,7 +3,6 @@ from ruamel.yaml import YAML -import nf_core.pipelines.create.create import nf_core.pipelines.lint from nf_core.utils import NFCoreYamlConfig diff --git a/tests/pipelines/test_container_configs.py b/tests/pipelines/test_container_configs.py new file mode 100644 index 0000000000..c7f5067afa --- /dev/null +++ b/tests/pipelines/test_container_configs.py @@ -0,0 +1,121 @@ +"""Tests for the ContainerConfigs helper used by pipelines.""" + +from pathlib import Path +from unittest.mock import patch + +import pytest +import ruamel.yaml + +from nf_core.modules.install import ModuleInstall +from nf_core.pipelines.containers_utils import PLATFORMS, ContainerConfigs +from nf_core.utils import NF_INSPECT_MIN_NF_VERSION, pretty_nf_version + +from ..test_pipelines import TestPipelines + +yaml = ruamel.yaml.YAML() + + +class TestContainerConfigs(TestPipelines): + """Tests for ContainerConfigs using a test pipeline.""" + + def setUp(self) -> None: + super().setUp() + self.container_configs = ContainerConfigs(self.pipeline_dir) + + def test_check_nextflow_version_sufficient_ok(self) -> None: + """check_nextflow_version should return silently when version is sufficient.""" + with patch( + "nf_core.pipelines.containers_utils.check_nextflow_version", + return_value=True, + ) as mocked_check: + self.container_configs.check_nextflow_version_sufficient() + + mocked_check.assert_called_once_with(NF_INSPECT_MIN_NF_VERSION, silent=True) + + def test_check_nextflow_version_sufficient_too_low(self) -> None: + """check_nextflow_version should raise UserWarning when version is too low.""" + with ( + patch( + "nf_core.pipelines.containers_utils.check_nextflow_version", + return_value=False, + ), + pytest.raises(UserWarning) as excinfo, + ): + self.container_configs.check_nextflow_version_sufficient() + + # Error message should mention the minimal required version + assert pretty_nf_version(NF_INSPECT_MIN_NF_VERSION) in str(excinfo.value) + + def test_generate_all_container_configs(self) -> None: + """Run generate_all_container_configs in a pipeline.""" + # Install fastqc and multiqc + mods_install = ModuleInstall( + self.pipeline_dir, prompt=False, force=False, sha="79b36b51048048374b642289bfe9e591ef56fe05" + ) + mods_install.install("fastqc") + mods_install.install("multiqc") + + self.container_configs.generate_container_configs() + + conf_dir = self.pipeline_dir / "conf" + with open(self.pipeline_dir / "modules" / "nf-core" / "fastqc" / "meta.yml") as fh: + fastqc_meta_yml = yaml.load(fh) + + for p_name, (runtime, arch, protocol) in PLATFORMS.items(): + cfg_path = conf_dir / f"containers_{p_name}.config" + assert cfg_path.exists() + with cfg_path.open("r") as fh: + content = fh.readlines() + value = fastqc_meta_yml["containers"][runtime][arch][protocol] + key = "conda" if p_name.startswith("conda_lock_") else "container" + assert f"process {{ withName: 'FASTQC' {{ {key} = '{value}' }} }}\n" in content + + def test_generate_container_configs_new_module_injected(self) -> None: + """new_module_name/path are used when nextflow inspect doesn't yet know about the module.""" + mods_install = ModuleInstall( + self.pipeline_dir, prompt=False, force=False, sha="79b36b51048048374b642289bfe9e591ef56fe05" + ) + mods_install.install("fastqc") + + with open(self.pipeline_dir / "modules" / "nf-core" / "fastqc" / "meta.yml") as fh: + fastqc_meta_yml = yaml.load(fh) + + with ( + patch("nf_core.pipelines.containers_utils.check_nextflow_version", return_value=True), + patch( + "nf_core.pipelines.containers_utils.run_cmd", + return_value=(b'{"processes": []}', b""), + ), + ): + self.container_configs.generate_container_configs( + new_module_path=Path("modules/nf-core/fastqc"), + new_module_name="fastqc", + ) + + conf_dir = self.pipeline_dir / "conf" + for p_name, (runtime, arch, protocol) in PLATFORMS.items(): + cfg_path = conf_dir / f"containers_{p_name}.config" + assert cfg_path.exists() + value = fastqc_meta_yml["containers"][runtime][arch][protocol] + key = "conda" if p_name.startswith("conda_lock_") else "container" + assert f"process {{ withName: 'FASTQC' {{ {key} = '{value}' }} }}\n" in cfg_path.read_text() + + def test_generate_container_configs_removes_stale_entries(self) -> None: + """Stale config files are deleted when all their modules have been removed.""" + conf_dir = self.pipeline_dir / "conf" + stale_line = "process { withName: 'REMOVED_MODULE' { container = 'stale/image:latest' } }\n" + for p_name in PLATFORMS: + (conf_dir / f"containers_{p_name}.config").write_text(stale_line) + + with ( + patch("nf_core.pipelines.containers_utils.check_nextflow_version", return_value=True), + patch( + "nf_core.pipelines.containers_utils.run_cmd", + return_value=(b'{"processes": []}', b""), + ), + ): + self.container_configs.generate_container_configs() + + for p_name in PLATFORMS: + cfg_path = conf_dir / f"containers_{p_name}.config" + assert not cfg_path.exists(), f"{cfg_path.name} should be deleted when all modules are removed" diff --git a/tests/pipelines/test_create.py b/tests/pipelines/test_create.py index d2ee4dd246..ccd125d17e 100644 --- a/tests/pipelines/test_create.py +++ b/tests/pipelines/test_create.py @@ -72,13 +72,13 @@ def test_pipeline_creation_initiation_with_yml(self, tmp_path): default_branch=self.default_branch, ) pipeline.init_pipeline() - assert os.path.isdir(os.path.join(pipeline.outdir, ".git")) + assert Path(pipeline.outdir, ".git").is_dir() assert f" {self.default_branch}\n" in git.Repo.init(pipeline.outdir).git.branch() # Check pipeline template yml has been dumped to `.nf-core.yml` and matches input - assert not os.path.exists(os.path.join(pipeline.outdir, "pipeline_template.yml")) - assert os.path.exists(os.path.join(pipeline.outdir, ".nf-core.yml")) - with open(os.path.join(pipeline.outdir, ".nf-core.yml")) as fh: + assert not Path(pipeline.outdir, "pipeline_template.yml").exists() + assert Path(pipeline.outdir, ".nf-core.yml").exists() + with open(Path(pipeline.outdir, ".nf-core.yml")) as fh: nfcore_yml = yaml.safe_load(fh) assert "template" in nfcore_yml assert yaml.safe_load(PIPELINE_TEMPLATE_YML.read_text()).items() <= nfcore_yml["template"].items() @@ -89,13 +89,13 @@ def test_pipeline_creation_initiation_customize_template(self, tmp_path): outdir=tmp_path, template_config=PIPELINE_TEMPLATE_YML, default_branch=self.default_branch ) pipeline.init_pipeline() - assert os.path.isdir(os.path.join(pipeline.outdir, ".git")) + assert Path(pipeline.outdir, ".git").is_dir() assert f" {self.default_branch}\n" in git.Repo.init(pipeline.outdir).git.branch() # Check pipeline template yml has been dumped to `.nf-core.yml` and matches input - assert not os.path.exists(os.path.join(pipeline.outdir, "pipeline_template.yml")) - assert os.path.exists(os.path.join(pipeline.outdir, ".nf-core.yml")) - with open(os.path.join(pipeline.outdir, ".nf-core.yml")) as fh: + assert not Path(pipeline.outdir, "pipeline_template.yml").exists() + assert Path(pipeline.outdir, ".nf-core.yml").exists() + with open(Path(pipeline.outdir, ".nf-core.yml")) as fh: nfcore_yml = yaml.safe_load(fh) assert "template" in nfcore_yml assert yaml.safe_load(PIPELINE_TEMPLATE_YML.read_text()).items() <= nfcore_yml["template"].items() @@ -151,7 +151,7 @@ def test_template_customisation_all_files_grouping(self): ] all_skipped_files = [] for section in template_features_yml.values(): - for feature in section["features"].keys(): + for feature in section["features"]: if section["features"][feature]["skippable_paths"]: all_skipped_files.extend(section["features"][feature]["skippable_paths"]) diff --git a/tests/pipelines/test_create_logo.py b/tests/pipelines/test_create_logo.py index 9ff9fce562..dcd80bddf0 100644 --- a/tests/pipelines/test_create_logo.py +++ b/tests/pipelines/test_create_logo.py @@ -88,7 +88,7 @@ def test_create_logo_svg(self): """Test that the create-logo command works for SVGs""" # Create a logo - logo_fn = nf_core.pipelines.create_logo.create_logo("pipes", self.pipeline_dir, format="svg") + logo_fn = nf_core.pipelines.create_logo.create_logo("pipes", self.pipeline_dir, img_format="svg") # Check that the file exists self.assertTrue(logo_fn.is_file()) # Check that the file is a SVG @@ -104,7 +104,7 @@ def test_create_logo_svg_dark(self): """Test that the create-logo command works for svgs and dark theme""" # Create a logo - logo_fn = nf_core.pipelines.create_logo.create_logo("pipes", self.pipeline_dir, format="svg", theme="dark") + logo_fn = nf_core.pipelines.create_logo.create_logo("pipes", self.pipeline_dir, img_format="svg", theme="dark") # Check that the file exists self.assertTrue(logo_fn.is_file()) # Check that the file is a SVG diff --git a/tests/pipelines/test_launch.py b/tests/pipelines/test_launch.py index ed23872f66..e97f2f10b6 100644 --- a/tests/pipelines/test_launch.py +++ b/tests/pipelines/test_launch.py @@ -325,7 +325,7 @@ def test_build_command_params(self): try: saved_json = json.load(fh) except json.JSONDecodeError as e: - raise UserWarning(f"Unable to load JSON file '{self.nf_params_fn}' due to error {e}") + raise UserWarning(f"Unable to load JSON file '{self.nf_params_fn}' due to error {e}") from e assert saved_json == {"input": "custom_input"} diff --git a/tests/pipelines/test_lint.py b/tests/pipelines/test_lint.py index 6a4a0243fc..0fccc69d07 100644 --- a/tests/pipelines/test_lint.py +++ b/tests/pipelines/test_lint.py @@ -5,7 +5,6 @@ import yaml -import nf_core.pipelines.create.create import nf_core.pipelines.lint import nf_core.utils @@ -60,14 +59,14 @@ def test_load_lint_config_ignore_all_tests(self): lint_obj = nf_core.pipelines.lint.PipelineLint(new_pipeline) # Make a config file listing all test names - config_dict = {"repository_type": "pipeline", "lint": {test_name: False for test_name in lint_obj.lint_tests}} + config_dict = {"repository_type": "pipeline", "lint": dict.fromkeys(lint_obj.lint_tests, False)} with open(Path(new_pipeline, ".nf-core.yml"), "w") as fh: yaml.dump(config_dict, fh) # Load the new lint config file and check lint_obj._load_lint_config() assert lint_obj.lint_config is not None - assert sorted(list(lint_obj.lint_config.model_dump(exclude_none=True))) == sorted(lint_obj.lint_tests) + assert sorted(lint_obj.lint_config.model_dump(exclude_none=True)) == sorted(lint_obj.lint_tests) # Try running linting and make sure that all tests are ignored lint_obj._lint_pipeline() @@ -114,7 +113,7 @@ def test_json_output(self, tmp_dir): try: saved_json = json.load(fh) except json.JSONDecodeError as e: - raise UserWarning(f"Unable to load JSON file '{json_fn}' due to error {e}") + raise UserWarning(f"Unable to load JSON file '{json_fn}' due to error {e}") from e assert saved_json["num_tests_pass"] > 0 assert saved_json["num_tests_warned"] > 0 assert saved_json["num_tests_ignored"] == 0 @@ -124,6 +123,28 @@ def test_json_output(self, tmp_dir): assert not saved_json["has_tests_ignored"] assert not saved_json["has_tests_failed"] + def test_load_tools_config_malformed_lint(self): + """load_tools_config raises AssertionError for invalid lint config + + pipeline_todos is typed bool | None but given a list value, + which should fail pydantic validation. + """ + new_pipeline = self._make_pipeline_copy() + nf_core_yml_path = Path(new_pipeline, ".nf-core.yml") + with open(nf_core_yml_path) as fh: + config = yaml.safe_load(fh) + if "lint" not in config or config["lint"] is None: + config["lint"] = {} + config["lint"]["pipeline_todos"] = ["README.md", "main.nf"] + with open(nf_core_yml_path, "w") as fh: + yaml.dump(config, fh) + + with self.assertRaises(AssertionError) as cm: + nf_core.utils.load_tools_config(new_pipeline) + assert "is invalid" in str(cm.exception) + assert "lint.pipeline_todos" in str(cm.exception) + assert "Got instead" in str(cm.exception) + def test_wrap_quotes(self): md = self.lint_obj._wrap_quotes(["one", "two", "three"]) assert md == "`one` or `two` or `three`" diff --git a/tests/pipelines/test_list.py b/tests/pipelines/test_list.py index aacc3805e8..8d32436393 100644 --- a/tests/pipelines/test_list.py +++ b/tests/pipelines/test_list.py @@ -106,14 +106,14 @@ def test_local_workflows_compare_and_fail_silently(self): rwf_ex.releases = None - @mock.patch("nf_core.pipelines.list.LocalWorkflow") - def test_parse_local_workflow_and_succeed(self, mock_local_wf): + def test_parse_local_workflow_and_succeed(self): test_path = self.tmp_nxf / "nf-core" - if not os.path.isdir(test_path): - os.makedirs(test_path) + if not test_path.is_dir(): + test_path.mkdir(parents=True) + # Create a dummy workflow directory (not just a file) + dummy_wf_path = self.tmp_nxf / ".repos" / "nf-core" / "dummy-wf" + dummy_wf_path.mkdir(parents=True, exist_ok=True) assert os.environ["NXF_ASSETS"] == self.tmp_nxf_str - with open(self.tmp_nxf / "nf-core/dummy-wf", "w") as f: - f.write("dummy") workflows_obj = nf_core.pipelines.list.Workflows() workflows_obj.get_local_nf_workflows() assert len(workflows_obj.local_workflows) == 1 @@ -122,23 +122,68 @@ def test_parse_local_workflow_and_succeed(self, mock_local_wf): @mock.patch("subprocess.check_output") def test_parse_local_workflow_home(self, mock_local_wf, mock_subprocess): test_path = self.tmp_nxf / "nf-core" - if not os.path.isdir(test_path): - os.makedirs(test_path) + if not test_path.is_dir(): + test_path.mkdir(parents=True) assert os.environ["NXF_ASSETS"] == self.tmp_nxf_str with open(self.tmp_nxf / "nf-core/dummy-wf", "w") as f: f.write("dummy") workflows_obj = nf_core.pipelines.list.Workflows() workflows_obj.get_local_nf_workflows() - @mock.patch("os.stat") @mock.patch("git.Repo") - def test_local_workflow_investigation(self, mock_repo, mock_stat): + def test_local_workflow_investigation(self, mock_repo): local_wf = nf_core.pipelines.list.LocalWorkflow("dummy") local_wf.local_path = self.tmp_nxf.parent - mock_repo.head.commit.hexsha = "h00r4y" - mock_stat.st_mode = 1 + # Create the .git/FETCH_HEAD file that the code expects + git_dir = self.tmp_nxf.parent / ".git" + git_dir.mkdir(parents=True, exist_ok=True) + (git_dir / "FETCH_HEAD").touch() + mock_repo.return_value.head.commit.hexsha = "h00r4y" + mock_repo.return_value.remotes.origin.url = "https://github.com/nf-core/dummy" + mock_repo.return_value.common_dir = str(git_dir) local_wf.get_local_nf_workflow_details() + @mock.patch("git.Repo") + def test_local_workflow_broken_ref(self, mock_repo): + local_wf = nf_core.pipelines.list.LocalWorkflow("dummy") + local_wf.local_path = self.tmp_nxf.parent + + class BrokenCommit: + @property + def hexsha(self): + raise ValueError("Reference at 'refs/heads/master' does not exist") + + repo = mock.Mock() + repo.remotes.origin.url = "https://github.com/nf-core/dummy" + repo.tags = [] + repo.head.commit = BrokenCommit() + mock_repo.return_value = repo + + local_wf.get_local_nf_workflow_details() + + assert local_wf.commit_sha is None + + @mock.patch.object(nf_core.pipelines.list.LocalWorkflow, "get_local_nf_workflow_details", autospec=True) + def test_get_local_wf_does_not_scan_unrelated_repos(self, mock_get_local_details): + atacseq_path = self.tmp_nxf / "nf-core" / "atacseq" + exoseq_path = self.tmp_nxf / "nf-core" / "exoseq" + atacseq_path.mkdir(parents=True) + exoseq_path.mkdir(parents=True) + + def set_local_workflow_details(local_wf): + if local_wf.full_name != "nf-core/atacseq": + raise AssertionError(f"Unexpected workflow lookup: {local_wf.full_name}") + local_wf.commit_sha = "abc1234" + local_wf.branch = "main" + local_wf.active_tag = None + + mock_get_local_details.side_effect = set_local_workflow_details + + local_path = nf_core.pipelines.list.get_local_wf(Path("atacseq")) + + assert local_path == atacseq_path + assert mock_get_local_details.call_count == 1 + def test_worflow_filter(self): workflows_obj = nf_core.pipelines.list.Workflows(["rna", "myWF"]) diff --git a/tests/pipelines/test_refgenie.py b/tests/pipelines/test_refgenie.py index 734a2368bd..2b824f599e 100644 --- a/tests/pipelines/test_refgenie.py +++ b/tests/pipelines/test_refgenie.py @@ -24,7 +24,7 @@ def setUp(self): # avoids adding includeConfig statement to config file outside the current tmpdir try: self.NXF_HOME_ORIGINAL = os.environ["NXF_HOME"] - except Exception: + except KeyError: self.NXF_HOME_ORIGINAL = None os.environ["NXF_HOME"] = str(self.NXF_HOME) diff --git a/tests/pipelines/test_rocrate.py b/tests/pipelines/test_rocrate.py index a073beae31..30df70f821 100644 --- a/tests/pipelines/test_rocrate.py +++ b/tests/pipelines/test_rocrate.py @@ -1,23 +1,32 @@ """Test the nf-core pipelines rocrate command""" import json +import re import shutil import tempfile from pathlib import Path +from unittest import mock import git import rocrate.rocrate from git import Repo -import nf_core.pipelines.create -import nf_core.pipelines.create.create import nf_core.pipelines.rocrate -import nf_core.utils from nf_core.pipelines.bump_version import bump_pipeline_version from ..test_pipelines import TestPipelines +class MockResponse: + def __init__(self, payload, status_code=200, url=""): + self.payload = payload + self.status_code = status_code + self.url = url + + def json(self): + return self.payload + + class TestROCrate(TestPipelines): """Class for lint tests""" @@ -37,11 +46,89 @@ def tearDown(self): if self.tmp_dir.exists(): shutil.rmtree(self.tmp_dir) + def _set_manifest_identity(self, manifest_body: str) -> None: + config_path = Path(self.pipeline_dir, "nextflow.config") + config_text = config_path.read_text() + updated_text, replacements = re.subn( + r"(?ms)^ contributors\s*=\s*\[\n.*?^ \]\n", + manifest_body, + config_text, + count=1, + ) + self.assertEqual(replacements, 1) + config_path.write_text(updated_text) + + self.pipeline_obj = nf_core.utils.Pipeline(self.pipeline_dir) + self.pipeline_obj._load() + self.rocrate_obj = nf_core.pipelines.rocrate.ROCrate(self.pipeline_dir) + + def _commit_main_nf_change(self, author_name: str, email: str, message: str) -> None: + repo = Repo(self.pipeline_dir) + main_nf = Path(self.pipeline_dir, "main.nf") + main_nf.write_text(main_nf.read_text() + f"\n// {message}\n") + repo.index.add([str(main_nf.relative_to(self.pipeline_dir))]) + actor = git.Actor(author_name, email) + repo.index.commit(message, author=actor, committer=actor) + + def _mock_requests_get(self, topics=None, github_names=None, orcid_lookup=None): + topics = topics or ["rna-seq"] + github_names = github_names or {} + orcid_lookup = orcid_lookup or {} + + def mocked_requests_get(url, params=None, headers=None, **kwargs): + if url == "https://nf-co.re/pipelines.json": + return MockResponse( + {"remote_workflows": [{"name": self.pipeline_obj.pipeline_name, "topics": topics}]}, + url=url, + ) + if url.startswith("https://api.github.com/users/"): + username = url.rsplit("/", 1)[-1] + return MockResponse({"name": github_names.get(username)}, url=url) + if url == "https://pub.orcid.org/v3.0/search/": + queried_name = None + if params is not None and "q" in params: + match = re.search(r'family-name:"([^"]+)" AND given-names:"([^"]+)"', params["q"]) + if match: + queried_name = f"{match.group(2)} {match.group(1)}" + orcid_uri = orcid_lookup.get(queried_name) + if orcid_uri is None: + return MockResponse({"num-found": 0, "result": []}, url=url) + return MockResponse( + {"num-found": 1, "result": [{"orcid-identifier": {"uri": orcid_uri}}]}, + url=url, + ) + raise AssertionError(f"Unexpected request during test: {url}") + + return mocked_requests_get + + def _read_crate_graph(self): + with open(Path(self.pipeline_dir, "ro-crate-metadata.json")) as f: + return json.load(f)["@graph"] + + def _graph_entity(self, graph, entity_id: str): + candidate_ids = {entity_id, entity_id.removeprefix("#")} + for entity in graph: + if entity.get("@id") in candidate_ids: + return entity + self.fail(f"Could not find entity {entity_id}") + + def _graph_person(self, graph, name: str): + for entity in graph: + if entity.get("@type") == "Person" and entity.get("name") == name: + return entity + self.fail(f"Could not find person {name}") + + def _create_rocrate_with_mocked_requests(self, topics=None, github_names=None, orcid_lookup=None) -> None: + with mock.patch( + "nf_core.pipelines.rocrate.requests.get", + side_effect=self._mock_requests_get(topics=topics, github_names=github_names, orcid_lookup=orcid_lookup), + ): + self.assertTrue(self.rocrate_obj.create_rocrate(json_path=self.pipeline_dir)) + def test_rocrate_creation(self): """Run the nf-core rocrate command""" # Run the command - self.rocrate_obj assert self.rocrate_obj.create_rocrate(self.pipeline_dir, self.pipeline_dir) # Check that the crate was created @@ -109,14 +196,197 @@ def test_rocrate_creation_to_zip(self): # Check that the crate was created self.assertTrue(Path(self.pipeline_dir, "ro-crate.crate.zip").exists()) - def test_rocrate_creation_for_fetchngs(self): - """Run the nf-core rocrate command with nf-core/fetchngs""" + def test_rocrate_creation_uses_manifest_author_when_contributors_missing(self): + """Use manifest.author when manifest.contributors is not defined""" + + self._set_manifest_identity("author = 'Ada Lovelace, Grace Hopper, Ada Lovelace'\n") + self._create_rocrate_with_mocked_requests() + + graph = self._read_crate_graph() + workflow = self._graph_entity(graph, "#main.nf") + person_names = {entity["name"] for entity in graph if entity.get("@type") == "Person"} + + self.assertEqual(person_names, {"Ada Lovelace", "Grace Hopper"}) + self.assertEqual(len(workflow["author"]), 2) + + def test_parse_manifest_authors_adds_git_contributors(self): + """Enrich manifest.author metadata with contributor names discovered from git history""" + + self._set_manifest_identity("author = 'Ada Lovelace'\n") + self._commit_main_nf_change("octocat", "octocat@example.com", "Add git contributor") + + with mock.patch( + "nf_core.pipelines.rocrate.requests.get", + side_effect=self._mock_requests_get(github_names={"octocat": "Mona Octocat"}), + ): + contributors = self.rocrate_obj.parse_manifest_authors() + + contributions_by_name = {contributor["name"]: contributor["contribution"] for contributor in contributors} + self.assertEqual(contributions_by_name["Ada Lovelace"], ["author"]) + self.assertEqual(contributions_by_name["Mona Octocat"], ["contributor"]) + + def test_parse_manifest_contributors_normalises_fields(self): + """Normalise contributor metadata from Nextflow config and backfill missing email addresses""" + + self._set_manifest_identity( + """contributors = [ + [ + name: 'Alice Example', + affiliation: ' Example Lab ', + email: '', + github: '@alice', + contribution: ['author', '', 'maintainer'], + orcid: '0000-0001-2345-6789' + ], + [ + name: 'Bob Example', + affiliation: '', + email: '', + github: 'bobdev', + contribution: ['contributor', ''], + orcid: '' + ] + ] + """ + ) + + contributors = self.rocrate_obj.parse_manifest_contributors() + contributors_by_name = {contributor["name"]: contributor for contributor in contributors} + + self.assertEqual(contributors_by_name["Alice Example"]["affiliation"], "Example Lab") + self.assertEqual(contributors_by_name["Alice Example"]["github"], "https://github.com/alice") + self.assertEqual(contributors_by_name["Alice Example"]["orcid"], "https://orcid.org/0000-0001-2345-6789") + self.assertEqual(contributors_by_name["Alice Example"]["contribution"], ["author", "maintainer"]) + self.assertNotIn("affiliation", contributors_by_name["Bob Example"]) + self.assertEqual(contributors_by_name["Bob Example"]["github"], "https://github.com/bobdev") + self.assertEqual(contributors_by_name["Bob Example"]["contribution"], ["contributor"]) + + def test_get_git_email_for_name(self): + """Match git author email using the full contributor name, not just the first token""" + + self._commit_main_nf_change("Alex Example", "alex.correct@example.com", "Commit by the right Alex") + self._commit_main_nf_change("Alex Wrong", "alex.wrong@example.com", "Commit by a different Alex") + + email = self.rocrate_obj._get_git_email_for_name("Alex Example") + self.assertEqual(email, "alex.correct@example.com") + + email = self.rocrate_obj._get_git_email_for_name("Alex Wrong") + self.assertEqual(email, "alex.wrong@example.com") + + def test_parse_manifest_contributors_logs_parse_errors(self): + """Emit a clear error when manifest.contributors cannot be normalised into valid JSON""" + + # Set nf_config directly to avoid running nextflow config -flat on invalid Groovy syntax + # (unquoted `alice` is valid Groovy but references an undefined variable, causing nextflow to fail) + self.rocrate_obj.pipeline_obj.nf_config["manifest.contributors"] = ( + "[[\n name: 'Alice Example',\n github: alice\n]]" + ) + + with self.assertLogs("nf_core.pipelines.rocrate", level="ERROR") as logs: + assert self.rocrate_obj.parse_manifest_contributors() == [] + + self.assertIn("Could not parse `manifest.contributors`", "\n".join(logs.output)) + + def test_rocrate_creation_prefers_manifest_contributors_over_author(self): + """Prefer manifest.contributors metadata when both contributor fields are present""" + + self._set_manifest_identity( + """author = 'Ignored Author' + contributors = [ + [ + name: 'Preferred Person', + affiliation: '', + email: 'preferred@example.com', + github: '', + contribution: ['author'], + orcid: '' + ] + ] + """ + ) + self._create_rocrate_with_mocked_requests() + + graph = self._read_crate_graph() + person_names = {entity["name"] for entity in graph if entity.get("@type") == "Person"} + preferred_person = self._graph_person(graph, "Preferred Person") + + self.assertEqual(person_names, {"Preferred Person"}) + self.assertEqual(preferred_person["@id"], "#preferred@example.com") + + def test_rocrate_creation_maps_manifest_contributor_roles_and_identifiers(self): + """Map contributor roles onto RO-Crate properties and keep deterministic identifiers""" + + self._set_manifest_identity( + """contributors = [ + [ + name: 'Alice Example', + affiliation: 'Example Lab', + email: '', + github: '@alice', + contribution: ['author', 'maintainer'], + orcid: '0000-0001-2345-6789' + ], + [ + name: 'Charlie Brown', + affiliation: '', + email: '', + github: '', + contribution: ['contributor'], + orcid: '' + ] + ] + """ + ) + self._create_rocrate_with_mocked_requests( + orcid_lookup={"Charlie Brown": "https://orcid.org/0000-0002-2222-2222"} + ) + + graph = self._read_crate_graph() + workflow = self._graph_entity(graph, "#main.nf") + alice = self._graph_person(graph, "Alice Example") + charlie = self._graph_person(graph, "Charlie Brown") + + self.assertEqual(alice["@id"], "https://orcid.org/0000-0001-2345-6789") + self.assertEqual(charlie["@id"], "https://orcid.org/0000-0002-2222-2222") + self.assertEqual(workflow["author"], [{"@id": alice["@id"]}]) + self.assertEqual(workflow["maintainer"], [{"@id": alice["@id"]}]) + self.assertEqual(workflow["contributor"], [{"@id": charlie["@id"]}]) + + def test_get_author_identifier_allows_missing_identifier(self): + """Allow contributors without ORCID or email to keep a None identifier""" + + author = {"name": "Charlie Brown"} + + with mock.patch("nf_core.pipelines.rocrate.get_orcid", return_value=None): + self.assertIsNone(self.rocrate_obj._get_author_identifier(author)) + + def test_parse_manifest_contributors_requires_names(self): + """Abort when a contributor entry is missing a name""" + + self._set_manifest_identity( + """contributors = [ + [ + affiliation: 'Example Lab', + email: '', + github: '', + contribution: ['author'], + orcid: '' + ] + ] + """ + ) + + with self.assertRaises(SystemExit): + self.rocrate_obj.parse_manifest_contributors() + + def test_rocrate_creation_for_demo(self): + """Run the nf-core rocrate command with nf-core/demo""" tmp_dir = Path(tempfile.mkdtemp()) - # git clone nf-core/fetchngs - git.Repo.clone_from("https://github.com/nf-core/fetchngs", tmp_dir / "fetchngs") + # git clone nf-core/demo + git.Repo.clone_from("https://github.com/nf-core/demo", tmp_dir / "demo") # Run the command - self.rocrate_obj = nf_core.pipelines.rocrate.ROCrate(tmp_dir / "fetchngs", version="1.12.0") - assert self.rocrate_obj.create_rocrate(tmp_dir / "fetchngs", self.pipeline_dir) + self.rocrate_obj = nf_core.pipelines.rocrate.ROCrate(tmp_dir / "demo", version="1.1.0") + assert self.rocrate_obj.create_rocrate(tmp_dir / "demo", self.pipeline_dir) # Check that Sateesh Peri is mentioned in creator field diff --git a/tests/pipelines/test_schema.py b/tests/pipelines/test_schema.py index efc2798969..5965a5c011 100644 --- a/tests/pipelines/test_schema.py +++ b/tests/pipelines/test_schema.py @@ -27,20 +27,38 @@ def setUp(self): self.schema_obj.schema_draft = "https://json-schema.org/draft/2020-12/schema" self.schema_obj.defs_notation = "$defs" self.schema_obj.validation_plugin = "nf-schema" - self.root_repo_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + self.root_repo_dir = Path(__file__).resolve().parent.parent # Create a test pipeline in temp directory self.tmp_dir = tempfile.mkdtemp() - self.template_dir = os.path.join(self.tmp_dir, "wf") + self.original_nxf_home = os.environ.get("NXF_HOME") + self.original_nxf_assets = os.environ.get("NXF_ASSETS") + self.nxf_home = Path(self.tmp_dir, ".nextflow") + self.nxf_assets = Path(self.nxf_home, "assets") + self.nxf_home.mkdir(exist_ok=True) + self.nxf_assets.mkdir(exist_ok=True) + os.environ["NXF_HOME"] = str(self.nxf_home) + os.environ["NXF_ASSETS"] = str(self.nxf_assets) + self.template_dir = Path(self.tmp_dir, "wf") create_obj = nf_core.pipelines.create.create.PipelineCreate( "testpipeline", "a description", "Me", outdir=self.template_dir, no_git=True ) create_obj.init_pipeline() - self.template_schema = os.path.join(self.template_dir, "nextflow_schema.json") + self.template_schema = self.template_dir / "nextflow_schema.json" def tearDown(self): - if os.path.exists(self.tmp_dir): + if self.original_nxf_home is None: + os.environ.pop("NXF_HOME", None) + else: + os.environ["NXF_HOME"] = self.original_nxf_home + + if self.original_nxf_assets is None: + os.environ.pop("NXF_ASSETS", None) + else: + os.environ["NXF_ASSETS"] = self.original_nxf_assets + + if Path(self.tmp_dir).exists(): shutil.rmtree(self.tmp_dir) def test_load_lint_schema(self): @@ -50,12 +68,12 @@ def test_load_lint_schema(self): def test_load_lint_schema_nofile(self): """Check that linting raises properly if a non-existent file is given""" - with pytest.raises(RuntimeError): + with pytest.raises(AssertionError): self.schema_obj.get_schema_path("fake_file") def test_load_lint_schema_notjson(self): """Check that linting raises properly if a non-JSON file is given""" - self.schema_obj.get_schema_path(os.path.join(self.template_dir, "nextflow.config")) + self.schema_obj.get_schema_path(self.template_dir / "nextflow.config") with pytest.raises(AssertionError): self.schema_obj.load_lint_schema() @@ -86,7 +104,7 @@ def test_get_schema_path_path_notexist(self): def test_get_schema_path_name(self): """Get schema file from the name of a remote pipeline""" - self.schema_obj.get_schema_path("atacseq") + self.schema_obj.get_schema_path("proteinfamilies") def test_get_schema_path_name_notexist(self): """ diff --git a/tests/pipelines/test_sync.py b/tests/pipelines/test_sync.py index 746b2db76a..aa27170f26 100644 --- a/tests/pipelines/test_sync.py +++ b/tests/pipelines/test_sync.py @@ -9,7 +9,6 @@ import pytest import yaml -import nf_core.pipelines.create.create import nf_core.pipelines.sync from nf_core.utils import NFCoreYamlConfig @@ -118,7 +117,7 @@ def test_inspect_sync_dir_dirty(self): psync.inspect_sync_dir() assert exc_info.value.args[0].startswith("Uncommitted changes found in pipeline directory!") finally: - os.remove(test_fn) + test_fn.unlink() def test_inspect_sync_ignored_files(self): """ @@ -180,7 +179,8 @@ def test_delete_tracked_template_branch_files(self): psync.checkout_template_branch() psync.delete_tracked_template_branch_files() top_level_ignored = self._get_top_level_ignored(psync) - assert set(os.listdir(self.pipeline_dir)) == set([".git"]).union(top_level_ignored) + pipeline_contents = {f.name for f in Path(self.pipeline_dir).iterdir()} + assert pipeline_contents == {".git"}.union(top_level_ignored) def test_delete_tracked_template_branch_files_unlink_throws_error(self): """Test that SyncExceptionError is raised when Path.unlink throws an exception""" @@ -299,11 +299,13 @@ def test_create_template_pipeline(self): psync.checkout_template_branch() psync.delete_tracked_template_branch_files() top_level_ignored = self._get_top_level_ignored(psync) - assert set(os.listdir(self.pipeline_dir)) == set([".git"]).union(top_level_ignored) + pipeline_contents = {f.name for f in Path(self.pipeline_dir).iterdir()} + assert pipeline_contents == {".git"}.union(top_level_ignored) # Now create the new template psync.make_template_pipeline() - assert "main.nf" in os.listdir(self.pipeline_dir) - assert "nextflow.config" in os.listdir(self.pipeline_dir) + pipeline_path = Path(self.pipeline_dir) + assert (pipeline_path / "main.nf").exists() + assert (pipeline_path / "nextflow.config").exists() def test_commit_template_changes_nochanges(self): """Try to commit the TEMPLATE branch, but no changes were made""" diff --git a/tests/subworkflows/lint/__init__.py b/tests/subworkflows/lint/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/subworkflows/lint/test_integration.py b/tests/subworkflows/lint/test_integration.py new file mode 100644 index 0000000000..05eb1dd46f --- /dev/null +++ b/tests/subworkflows/lint/test_integration.py @@ -0,0 +1,69 @@ +import nf_core.subworkflows + +from ...test_subworkflows import TestSubworkflows +from ...utils import GITLAB_SUBWORKFLOWS_BRANCH, GITLAB_URL + + +class TestSubworkflowsLintIntegration(TestSubworkflows): + """Test general subworkflow linting functionality""" + + def test_subworkflows_lint(self): + """Test linting the fastq_align_bowtie2 subworkflow""" + self.subworkflow_install.install("fastq_align_bowtie2") + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, subworkflow="fastq_align_bowtie2") + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_lint_empty(self): + """Test linting a pipeline with no subworkflows installed""" + self.subworkflow_remove.remove("utils_nextflow_pipeline", force=True) + self.subworkflow_remove.remove("utils_nfcore_pipeline", force=True) + self.subworkflow_remove.remove("utils_nfschema_plugin", force=True) + nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + assert "No subworkflows from https://github.com/nf-core/modules.git installed in pipeline" in self.caplog.text + + def test_subworkflows_lint_new_subworkflow(self): + """lint a new subworkflow""" + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) + subworkflow_lint.lint(print_results=True, all_subworkflows=True) + assert len(subworkflow_lint.failed) == 0 + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_lint_no_gitlab(self): + """Test linting a pipeline with no subworkflows installed""" + nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir, remote_url=GITLAB_URL) + assert f"No subworkflows from {GITLAB_URL} installed in pipeline" in self.caplog.text + + def test_subworkflows_lint_gitlab_subworkflows(self): + """Lint subworkflows from a different remote""" + self.subworkflow_install_gitlab.install("bam_stats_samtools") + subworkflow_lint = nf_core.subworkflows.SubworkflowLint( + directory=self.pipeline_dir, remote_url=GITLAB_URL, branch=GITLAB_SUBWORKFLOWS_BRANCH + ) + subworkflow_lint.lint(print_results=False, all_subworkflows=True) + assert len(subworkflow_lint.failed) == 0 + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_lint_multiple_remotes(self): + """Lint subworkflows from a different remote""" + self.subworkflow_install_gitlab.install("bam_stats_samtools") + self.subworkflow_install.install("fastq_align_bowtie2") + subworkflow_lint = nf_core.subworkflows.SubworkflowLint( + directory=self.pipeline_dir, remote_url=GITLAB_URL, branch=GITLAB_SUBWORKFLOWS_BRANCH + ) + subworkflow_lint.lint(print_results=False, all_subworkflows=True) + assert len(subworkflow_lint.failed) == 0 + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_lint_fix(self): + """update the meta.yml of a subworkflow""" + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules, fix=True) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 diff --git a/tests/subworkflows/lint/test_local.py b/tests/subworkflows/lint/test_local.py new file mode 100644 index 0000000000..dc34595762 --- /dev/null +++ b/tests/subworkflows/lint/test_local.py @@ -0,0 +1,46 @@ +import shutil +from pathlib import Path + +import nf_core.subworkflows + +from ...test_subworkflows import TestSubworkflows + + +class TestSubworkflowsLintLocal(TestSubworkflows): + """Test linting local subworkflows""" + + def setUp(self) -> None: + super().setUp() + assert self.subworkflow_install.install("fastq_align_bowtie2") + self.install_path = Path(self.pipeline_dir, "subworkflows", "nf-core", "fastq_align_bowtie2") + self.local_path = Path(self.pipeline_dir, "subworkflows", "local", "fastq_align_bowtie2") + + def test_subworkflows_lint_local(self): + shutil.move(self.install_path, self.local_path) + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, local=True) + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_lint_local_missing_files(self): + shutil.move(self.install_path, self.local_path) + Path(self.local_path, "meta.yml").unlink() + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, local=True) + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + warnings = [x.message for x in subworkflow_lint.warned] + assert "Subworkflow `meta.yml` does not exist" in warnings + + def test_subworkflows_lint_local_old_format(self): + Path(self.pipeline_dir, "subworkflows", "local").mkdir(exist_ok=True) + local = Path(self.pipeline_dir, "subworkflows", "local", "fastq_align_bowtie2.nf") + shutil.copy(Path(self.install_path, "main.nf"), local) + self.subworkflow_remove.remove("fastq_align_bowtie2", force=True) + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, local=True) + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 diff --git a/tests/subworkflows/lint/test_main_nf.py b/tests/subworkflows/lint/test_main_nf.py new file mode 100644 index 0000000000..7b79869b0c --- /dev/null +++ b/tests/subworkflows/lint/test_main_nf.py @@ -0,0 +1,114 @@ +from pathlib import Path + +import nf_core.subworkflows + +from ...test_subworkflows import TestSubworkflows + + +class TestMainNf(TestSubworkflows): + """Test main.nf functionality in subworkflows""" + + def setUp(self) -> None: + super().setUp() + + self.subworkflow_install.install("bam_stats_samtools") + self.main_nf = Path( + self.pipeline_dir, + "subworkflows", + "nf-core", + "bam_stats_samtools", + "main.nf", + ) + + def test_subworkflows_lint_less_than_two_modules_warning(self): + """Test linting a subworkflow with less than two modules""" + # Remove two modules + with open(self.main_nf) as fh: + content = fh.read() + new_content = content.replace( + "include { SAMTOOLS_IDXSTATS } from '../../../modules/nf-core/samtools/idxstats/main'", + "", + ) + new_content = new_content.replace( + "include { SAMTOOLS_FLAGSTAT } from '../../../modules/nf-core/samtools/flagstat/main'", + "", + ) + with open( + self.main_nf, + "w", + ) as fh: + fh.write(new_content) + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") + assert len(subworkflow_lint.failed) >= 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) > 0 + assert subworkflow_lint.warned[0].lint_test == "main_nf_include" + + def test_subworkflows_lint_include_multiple_alias(self): + """Test linting a subworkflow with multiple include methods""" + with open(self.main_nf) as fh: + content = fh.read() + new_content = content.replace("SAMTOOLS_STATS", "SAMTOOLS_STATS_1") + new_content = new_content.replace( + "include { SAMTOOLS_STATS_1 ", + "include { SAMTOOLS_STATS as SAMTOOLS_STATS_1; SAMTOOLS_STATS as SAMTOOLS_STATS_2 ", + ) + with open( + self.main_nf, + "w", + ) as fh: + fh.write(new_content) + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") + assert len(subworkflow_lint.failed) >= 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) == 1 + assert any( + x.message == "Included component 'SAMTOOLS_STATS_1' used in main.nf" for x in subworkflow_lint.passed + ) + assert any( + x.message == "Included component 'SAMTOOLS_STATS_2' not used in main.nf" for x in subworkflow_lint.warned + ) + + # cleanup + self.subworkflow_remove.remove("bam_stats_samtools", force=True) + + def test_skip_keyword_in_comment(self): + """Test linting a subworkflow where a comment contains a keyword (workflow, subworkflow, take, main, emit)""" + with open(self.main_nf) as fh: + content = fh.read() + new_content = content.replace( + " SAMTOOLS_IDXSTATS ( ch_bam_bai )", + " // This comment contains the word emit:\n SAMTOOLS_IDXSTATS ( ch_bam_bai )", + ) + with open(self.main_nf, "w") as fh: + fh.write(new_content) + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") + assert "Included component 'SAMTOOLS_IDXSTATS' not used in main.nf" not in [ + warning.message for warning in subworkflow_lint.warned + ] + + def test_subworkflows_lint_capitalization_fail(self): + """Test linting a subworkflow with a capitalization fail""" + # change workflow name to lowercase + with open(self.main_nf) as fh: + content = fh.read() + new_content = content.replace("workflow BAM_STATS_SAMTOOLS {", "workflow bam_stats_samtools {") + with open( + self.main_nf, + "w", + ) as fh: + fh.write(new_content) + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") + assert len(subworkflow_lint.failed) >= 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + assert any(x.lint_test == "workflow_capitals" for x in subworkflow_lint.failed) + + # cleanup + self.subworkflow_remove.remove("bam_stats_samtools", force=True) diff --git a/tests/subworkflows/lint/test_nf_test.py b/tests/subworkflows/lint/test_nf_test.py new file mode 100644 index 0000000000..3c863868cf --- /dev/null +++ b/tests/subworkflows/lint/test_nf_test.py @@ -0,0 +1,141 @@ +import json +import shutil +from pathlib import Path + +import nf_core.subworkflows + +from ...test_subworkflows import TestSubworkflows + + +class TestSubworkflowsNfTest(TestSubworkflows): + """Test subworkflow nf-test and snapshot functionality""" + + def setUp(self): + super().setUp() + self.snap_file = Path( + self.nfcore_modules, + "subworkflows", + "nf-core", + "test_subworkflow", + "tests", + "main.nf.test.snap", + ) + + def test_subworkflows_lint_snapshot_file(self): + """Test linting a subworkflow with a snapshot file""" + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_lint_snapshot_file_missing_fail(self): + """Test linting a subworkflow with a snapshot file missing, which should fail""" + self.snap_file.unlink() + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + self.snap_file.touch() + assert len(subworkflow_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_lint_snapshot_file_not_needed(self): + """Test linting a subworkflow which doesn't need a snapshot file by removing the snapshot keyword in the main.nf.test file""" + with open( + Path( + self.nfcore_modules, + "subworkflows", + "nf-core", + "test_subworkflow", + "tests", + "main.nf.test", + ) + ) as fh: + content = fh.read() + new_content = content.replace("snapshot(", "snap (") + with open( + Path( + self.nfcore_modules, + "subworkflows", + "nf-core", + "test_subworkflow", + "tests", + "main.nf.test", + ), + "w", + ) as fh: + fh.write(new_content) + + self.snap_file.unlink() + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + Path( + self.nfcore_modules, + "subworkflows", + "nf-core", + "test_subworkflow", + "tests", + "main.nf.test.snap", + ).touch() + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + + def test_subworkflows_absent_version(self): + """Test linting a nf-test subworkflow if the versions is absent in the snapshot file `""" + + with open(self.snap_file) as fh: + content = fh.read() + new_content = content.replace("versions", "foo") + with open(self.snap_file, "w") as fh: + fh.write(new_content) + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + assert len(subworkflow_lint.failed) == 0 + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" + + def test_subworkflows_empty_file_in_snapshot(self): + """Test linting a nf-test subworkflow with an empty file sha sum in the test snapshot, which should make it fail (if it is not a stub)""" + + snap = json.load(self.snap_file.open()) + snap["my test"]["content"][0]["0"] = "test:md5,d41d8cd98f00b204e9800998ecf8427e" + + with open(self.snap_file, "w") as fh: + json.dump(snap, fh) + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + assert len(subworkflow_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + assert subworkflow_lint.failed[0].lint_test == "test_snap_md5sum" + + def test_subworkflows_empty_file_in_stub_snapshot(self): + """Test linting a nf-test subworkflow with an empty file sha sum in the stub test snapshot, which should make it not fail""" + + content = json.load(self.snap_file.open()) + content["my_test_stub"] = {"content": [{"0": "test:md5,d41d8cd98f00b204e9800998ecf8427e", "versions": {}}]} + + with open(self.snap_file, "w") as fh: + json.dump(content, fh) + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0 + assert any(x.lint_test == "test_snap_md5sum" for x in subworkflow_lint.passed) + + def test_subworkflows_missing_test_dir(self): + """Test linting a nf-test subworkflow if the tests directory is missing""" + test_dir = self.snap_file.parent + shutil.rmtree(test_dir) + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(self.nfcore_modules) + subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") + assert len(subworkflow_lint.failed) == 1 + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) >= 0, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" + assert any(x.lint_test == "test_dir_exists" for x in subworkflow_lint.failed) diff --git a/tests/subworkflows/lint/test_patch.py b/tests/subworkflows/lint/test_patch.py new file mode 100644 index 0000000000..91efb94342 --- /dev/null +++ b/tests/subworkflows/lint/test_patch.py @@ -0,0 +1,61 @@ +import subprocess +from pathlib import Path + +import nf_core.subworkflows + +from ...test_subworkflows import TestSubworkflows + + +class TestSubworkflowsLintPatch(TestSubworkflows): + """Test linting patched subworkflows""" + + def setUp(self) -> None: + super().setUp() + + # Install the subworkflow bam_sort_stats_samtools + self.subworkflow_install.install("bam_sort_stats_samtools") + + # Modify the subworkflow by inserting a new input channel + new_line = " ch_dummy // channel: [ path ]\n" + + subworkflow_path = Path(self.pipeline_dir, "subworkflows", "nf-core", "bam_sort_stats_samtools", "main.nf") + + with open(subworkflow_path) as fh: + lines = fh.readlines() + for line_index in range(len(lines)): + if "take:" in lines[line_index]: + lines.insert(line_index + 1, new_line) + with open(subworkflow_path, "w") as fh: + fh.writelines(lines) + + # Create a patch file + self.patch_obj = nf_core.subworkflows.SubworkflowPatch(self.pipeline_dir) + self.patch_obj.patch("bam_sort_stats_samtools") + + def test_lint_clean_patch(self): + """Test linting a patched subworkflow""" + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, subworkflow="bam_sort_stats_samtools") + + assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) == 0, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" + + def test_lint_broken_patch(self): + """Test linting a patched subworkflow when the patch is broken""" + + # Now modify the diff + diff_file = Path( + self.pipeline_dir, "subworkflows", "nf-core", "bam_sort_stats_samtools", "bam_sort_stats_samtools.diff" + ) + subprocess.check_call(["sed", "-i''", "s/...$//", str(diff_file)]) + + subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) + subworkflow_lint.lint(print_results=False, subworkflow="bam_sort_stats_samtools") + + assert len(subworkflow_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" + errors = [x.message for x in subworkflow_lint.failed] + assert "Subworkflow patch cannot be cleanly applied" in errors + assert len(subworkflow_lint.passed) > 0 + assert len(subworkflow_lint.warned) == 0, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" diff --git a/tests/subworkflows/test_create.py b/tests/subworkflows/test_create.py index 704a23772e..a101e119bc 100644 --- a/tests/subworkflows/test_create.py +++ b/tests/subworkflows/test_create.py @@ -1,13 +1,8 @@ -import shutil from pathlib import Path -from unittest import mock import pytest -import yaml -from git.repo import Repo import nf_core.subworkflows -from tests.utils import GITLAB_SUBWORKFLOWS_ORG_PATH_BRANCH, GITLAB_URL from ..test_subworkflows import TestSubworkflows @@ -42,68 +37,3 @@ def test_subworkflows_create_nfcore_modules(self): assert Path( self.nfcore_modules, "subworkflows", "nf-core", "test_subworkflow", "tests", "main.nf.test" ).exists() - - @mock.patch("rich.prompt.Confirm.ask") - def test_subworkflows_migrate(self, mock_rich_ask): - """Create a subworkflow with the --migrate-pytest option to convert pytest to nf-test""" - pytest_dir = Path(self.nfcore_modules, "tests", "subworkflows", "nf-core", "bam_stats_samtools") - subworkflow_dir = Path(self.nfcore_modules, "subworkflows", "nf-core", "bam_stats_samtools") - - # Clone modules repo with pytests - shutil.rmtree(self.nfcore_modules) - Repo.clone_from(GITLAB_URL, self.nfcore_modules, branch=GITLAB_SUBWORKFLOWS_ORG_PATH_BRANCH) - with open(subworkflow_dir / "main.nf") as fh: - old_main_nf = fh.read() - with open(subworkflow_dir / "meta.yml") as fh: - old_meta_yml = fh.read() - - # Create a subworkflow with --migrate-pytest - mock_rich_ask.return_value = True - subworkflow_create = nf_core.subworkflows.SubworkflowCreate( - self.nfcore_modules, "bam_stats_samtools", migrate_pytest=True - ) - subworkflow_create.create() - - with open(subworkflow_dir / "main.nf") as fh: - new_main_nf = fh.read() - with open(subworkflow_dir / "meta.yml") as fh: - new_meta_yml = fh.read() - nextflow_config = subworkflow_dir / "tests" / "nextflow.config" - - # Check that old files have been copied to the new module - assert old_main_nf == new_main_nf - assert old_meta_yml == new_meta_yml - assert nextflow_config.is_file() - - # Check that pytest folder is deleted - assert not pytest_dir.is_dir() - - # Check that pytest_modules.yml is updated - with open(Path(self.nfcore_modules, "tests", "config", "pytest_modules.yml")) as fh: - modules_yml = yaml.safe_load(fh) - assert "subworkflows/bam_stats_samtools" not in modules_yml.keys() - - @mock.patch("rich.prompt.Confirm.ask") - def test_subworkflows_migrate_no_delete(self, mock_rich_ask): - """Create a subworkflow with the --migrate-pytest option to convert pytest to nf-test. - Test that pytest directory is not deleted.""" - pytest_dir = Path(self.nfcore_modules, "tests", "subworkflows", "nf-core", "bam_stats_samtools") - - # Clone modules repo with pytests - shutil.rmtree(self.nfcore_modules) - Repo.clone_from(GITLAB_URL, self.nfcore_modules, branch=GITLAB_SUBWORKFLOWS_ORG_PATH_BRANCH) - - # Create a module with --migrate-pytest - mock_rich_ask.return_value = False - module_create = nf_core.subworkflows.SubworkflowCreate( - self.nfcore_modules, "bam_stats_samtools", migrate_pytest=True - ) - module_create.create() - - # Check that pytest folder is not deleted - assert pytest_dir.is_dir() - - # Check that pytest_modules.yml is updated - with open(Path(self.nfcore_modules, "tests", "config", "pytest_modules.yml")) as fh: - modules_yml = yaml.safe_load(fh) - assert "subworkflows/bam_stats_samtools" not in modules_yml.keys() diff --git a/tests/subworkflows/test_install.py b/tests/subworkflows/test_install.py index 91263d2847..67b007f9d9 100644 --- a/tests/subworkflows/test_install.py +++ b/tests/subworkflows/test_install.py @@ -18,6 +18,33 @@ class TestSubworkflowsInstall(TestSubworkflows): + def test_subworkflow_install_force_skip_deps_does_not_reinstall_deps(self): + """`install --force --skip-deps` leaves transitive deps' SHAs pinned.""" + assert self.subworkflow_install.install("bam_sort_stats_samtools") is not False + samtools_index_path = Path(self.pipeline_dir, "modules", "nf-core", "samtools", "index") + assert samtools_index_path.exists() + before_sha = ModulesJson(self.pipeline_dir).get_modules_json()["repos"][ + "https://github.com/nf-core/modules.git" + ]["modules"]["nf-core"]["samtools/index"]["git_sha"] + + force_skip = SubworkflowInstall(self.pipeline_dir, prompt=False, force=True, skip_deps=True) + assert force_skip.install("bam_sort_stats_samtools") is not False + after_sha = ModulesJson(self.pipeline_dir).get_modules_json()["repos"][ + "https://github.com/nf-core/modules.git" + ]["modules"]["nf-core"]["samtools/index"]["git_sha"] + assert before_sha == after_sha + + def test_subworkflow_install_skip_deps_preserves_installed_by_tracking(self): + """`install --skip-deps` of a parent records the new dependent in shared subcomponents' installed_by.""" + # Install the sub-subworkflow first so it's already on disk before we --skip-deps the parent. + assert self.subworkflow_install.install("bam_stats_samtools") is not False + skip_install = SubworkflowInstall(self.pipeline_dir, prompt=False, force=False, skip_deps=True) + assert skip_install.install("bam_sort_stats_samtools") is not False + installed_by = ModulesJson(self.pipeline_dir).get_modules_json()["repos"][ + "https://github.com/nf-core/modules.git" + ]["subworkflows"]["nf-core"]["bam_stats_samtools"]["installed_by"] + assert sorted(installed_by) == sorted(["subworkflows", "bam_sort_stats_samtools"]) + def test_subworkflows_install_bam_sort_stats_samtools(self): """Test installing a subworkflow - bam_sort_stats_samtools""" assert self.subworkflow_install.install("bam_sort_stats_samtools") is not False diff --git a/tests/subworkflows/test_lint.py b/tests/subworkflows/test_lint.py deleted file mode 100644 index 80aeb1d113..0000000000 --- a/tests/subworkflows/test_lint.py +++ /dev/null @@ -1,539 +0,0 @@ -import json -import shutil -import subprocess -from pathlib import Path - -import nf_core.subworkflows - -from ..test_subworkflows import TestSubworkflows -from ..utils import GITLAB_SUBWORKFLOWS_BRANCH, GITLAB_URL - - -class TestSubworkflowsLint(TestSubworkflows): - def test_subworkflows_lint(self): - """Test linting the fastq_align_bowtie2 subworkflow""" - self.subworkflow_install.install("fastq_align_bowtie2") - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, subworkflow="fastq_align_bowtie2") - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_empty(self): - """Test linting a pipeline with no subworkflows installed""" - self.subworkflow_remove.remove("utils_nextflow_pipeline", force=True) - self.subworkflow_remove.remove("utils_nfcore_pipeline", force=True) - self.subworkflow_remove.remove("utils_nfschema_plugin", force=True) - nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - assert "No subworkflows from https://github.com/nf-core/modules.git installed in pipeline" in self.caplog.text - - def test_subworkflows_lint_new_subworkflow(self): - """lint a new subworkflow""" - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - subworkflow_lint.lint(print_results=True, all_subworkflows=True) - assert len(subworkflow_lint.failed) == 0 - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_no_gitlab(self): - """Test linting a pipeline with no subworkflows installed""" - nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir, remote_url=GITLAB_URL) - assert f"No subworkflows from {GITLAB_URL} installed in pipeline" in self.caplog.text - - def test_subworkflows_lint_gitlab_subworkflows(self): - """Lint subworkflows from a different remote""" - self.subworkflow_install_gitlab.install("bam_stats_samtools") - subworkflow_lint = nf_core.subworkflows.SubworkflowLint( - directory=self.pipeline_dir, remote_url=GITLAB_URL, branch=GITLAB_SUBWORKFLOWS_BRANCH - ) - subworkflow_lint.lint(print_results=False, all_subworkflows=True) - assert len(subworkflow_lint.failed) == 0 - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_multiple_remotes(self): - """Lint subworkflows from a different remote""" - self.subworkflow_install_gitlab.install("bam_stats_samtools") - self.subworkflow_install.install("fastq_align_bowtie2") - subworkflow_lint = nf_core.subworkflows.SubworkflowLint( - directory=self.pipeline_dir, remote_url=GITLAB_URL, branch=GITLAB_SUBWORKFLOWS_BRANCH - ) - subworkflow_lint.lint(print_results=False, all_subworkflows=True) - assert len(subworkflow_lint.failed) == 0 - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_update_meta_yml(self): - """update the meta.yml of a subworkflow""" - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules, fix=True) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_snapshot_file(self): - """Test linting a subworkflow with a snapshot file""" - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_snapshot_file_missing_fail(self): - """Test linting a subworkflow with a snapshot file missing, which should fail""" - Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test.snap", - ).unlink() - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test.snap", - ).touch() - assert len(subworkflow_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_snapshot_file_not_needed(self): - """Test linting a subworkflow which doesn't need a snapshot file by removing the snapshot keyword in the main.nf.test file""" - with open( - Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test", - ) - ) as fh: - content = fh.read() - new_content = content.replace("snapshot(", "snap (") - with open( - Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test", - ), - "w", - ) as fh: - fh.write(new_content) - - Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test.snap", - ).unlink() - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test.snap", - ).touch() - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_less_than_two_modules_warning(self): - """Test linting a subworkflow with less than two modules""" - self.subworkflow_install.install("bam_stats_samtools") - # Remove two modules - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ) - ) as fh: - content = fh.read() - new_content = content.replace( - "include { SAMTOOLS_IDXSTATS } from '../../../modules/nf-core/samtools/idxstats/main'", - "", - ) - new_content = new_content.replace( - "include { SAMTOOLS_FLAGSTAT } from '../../../modules/nf-core/samtools/flagstat/main'", - "", - ) - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ), - "w", - ) as fh: - fh.write(new_content) - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") - assert len(subworkflow_lint.failed) >= 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) > 0 - assert subworkflow_lint.warned[0].lint_test == "main_nf_include" - # cleanup - self.subworkflow_remove.remove("bam_stats_samtools", force=True) - - def test_subworkflows_lint_include_multiple_alias(self): - """Test linting a subworkflow with multiple include methods""" - self.subworkflow_install.install("bam_stats_samtools") - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ) - ) as fh: - content = fh.read() - new_content = content.replace("SAMTOOLS_STATS", "SAMTOOLS_STATS_1") - new_content = new_content.replace( - "include { SAMTOOLS_STATS_1 ", - "include { SAMTOOLS_STATS as SAMTOOLS_STATS_1; SAMTOOLS_STATS as SAMTOOLS_STATS_2 ", - ) - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ), - "w", - ) as fh: - fh.write(new_content) - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") - assert len(subworkflow_lint.failed) >= 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) == 6 - assert any( - [x.message == "Included component 'SAMTOOLS_STATS_1' used in main.nf" for x in subworkflow_lint.passed] - ) - assert any( - [x.message == "Included component 'SAMTOOLS_STATS_2' not used in main.nf" for x in subworkflow_lint.warned] - ) - assert any( - [ - x.message.endswith("Can be ignored if the module is using topic channels") - for x in subworkflow_lint.warned - ] - ) - - # cleanup - self.subworkflow_remove.remove("bam_stats_samtools", force=True) - - def test_subworkflows_lint_capitalization_fail(self): - """Test linting a subworkflow with a capitalization fail""" - self.subworkflow_install.install("bam_stats_samtools") - # change workflow name to lowercase - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ) - ) as fh: - content = fh.read() - new_content = content.replace("workflow BAM_STATS_SAMTOOLS {", "workflow bam_stats_samtools {") - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ), - "w", - ) as fh: - fh.write(new_content) - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") - assert len(subworkflow_lint.failed) >= 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - assert any([x.lint_test == "workflow_capitals" for x in subworkflow_lint.failed]) - - # cleanup - self.subworkflow_remove.remove("bam_stats_samtools", force=True) - - def test_subworkflows_absent_version(self): - """Test linting a nf-test subworkflow if the versions is absent in the snapshot file `""" - snap_file = Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test.snap", - ) - with open(snap_file) as fh: - content = fh.read() - new_content = content.replace("versions", "foo") - with open(snap_file, "w") as fh: - fh.write(new_content) - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - assert len(subworkflow_lint.failed) == 0 - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" - assert any([x.lint_test == "test_snap_versions" for x in subworkflow_lint.warned]) - - # cleanup - with open(snap_file, "w") as fh: - fh.write(content) - - def test_subworkflows_missing_test_dir(self): - """Test linting a nf-test subworkflow if the tests directory is missing""" - test_dir = Path(self.nfcore_modules, "subworkflows", "nf-core", "test_subworkflow", "tests") - test_dir_copy = shutil.copytree(test_dir, test_dir.parent / "tests_copy") - shutil.rmtree(test_dir) - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(self.nfcore_modules) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - assert len(subworkflow_lint.failed) == 1 - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" - assert any([x.lint_test == "test_dir_exists" for x in subworkflow_lint.failed]) - - # cleanup - shutil.copytree(test_dir_copy, test_dir) - - # There are many steps before the actual main_nf linting where we rely on the main_nf file to exist, so this test is not possible for now - # def test_subworkflows_missing_main_nf(self): - # """Test linting a nf-test subworkflow if the main.nf file is missing""" - - # subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - # main_nf = Path(self.nfcore_modules, "subworkflows", "nf-core", "test_subworkflow", "main.nf") - # main_nf_copy = shutil.copy(main_nf, main_nf.parent / "main_nf_copy") - # main_nf.unlink() - # subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - # assert len(subworkflow_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - # assert len(subworkflow_lint.passed) > 0 - # assert len(subworkflow_lint.warned) >= 0 - # assert subworkflow_lint.failed[0].lint_test == "main_nf_exists" - - # # cleanup - # shutil.copy(main_nf_copy, main_nf) - # shutil.rmtree(Path(self.nfcore_modules, "subworkflows", "nf-core", "test_subworkflow_backup")) - - def test_subworkflows_empty_file_in_snapshot(self): - """Test linting a nf-test subworkflow with an empty file sha sum in the test snapshot, which should make it fail (if it is not a stub)""" - snap_file = Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test.snap", - ) - snap = json.load(snap_file.open()) - content = snap_file.read_text() - snap["my test"]["content"][0]["0"] = "test:md5,d41d8cd98f00b204e9800998ecf8427e" - - with open(snap_file, "w") as fh: - json.dump(snap, fh) - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - assert len(subworkflow_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - assert subworkflow_lint.failed[0].lint_test == "test_snap_md5sum" - - # reset the file - with open(snap_file, "w") as fh: - fh.write(content) - - def test_subworkflows_empty_file_in_stub_snapshot(self): - """Test linting a nf-test subworkflow with an empty file sha sum in the stub test snapshot, which should make it not fail""" - snap_file = Path( - self.nfcore_modules, - "subworkflows", - "nf-core", - "test_subworkflow", - "tests", - "main.nf.test.snap", - ) - snap = json.load(snap_file.open()) - content = snap_file.read_text() - snap["my_test_stub"] = {"content": [{"0": "test:md5,d41d8cd98f00b204e9800998ecf8427e", "versions": {}}]} - - with open(snap_file, "w") as fh: - json.dump(snap, fh) - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.nfcore_modules) - subworkflow_lint.lint(print_results=False, subworkflow="test_subworkflow") - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - assert any(x.lint_test == "test_snap_md5sum" for x in subworkflow_lint.passed) - - # reset the file - with open(snap_file, "w") as fh: - fh.write(content) - - def test_subworkflows_lint_local(self): - assert self.subworkflow_install.install("fastq_align_bowtie2") - installed = Path(self.pipeline_dir, "subworkflows", "nf-core", "fastq_align_bowtie2") - local = Path(self.pipeline_dir, "subworkflows", "local", "fastq_align_bowtie2") - shutil.move(installed, local) - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, local=True) - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_subworkflows_lint_local_missing_files(self): - assert self.subworkflow_install.install("fastq_align_bowtie2") - installed = Path(self.pipeline_dir, "subworkflows", "nf-core", "fastq_align_bowtie2") - local = Path(self.pipeline_dir, "subworkflows", "local", "fastq_align_bowtie2") - shutil.move(installed, local) - Path(self.pipeline_dir, "subworkflows", "local", "fastq_align_bowtie2", "meta.yml").unlink() - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, local=True) - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - warnings = [x.message for x in subworkflow_lint.warned] - assert "Subworkflow `meta.yml` does not exist" in warnings - - def test_subworkflows_lint_local_old_format(self): - assert self.subworkflow_install.install("fastq_align_bowtie2") - installed = Path(self.pipeline_dir, "subworkflows", "nf-core", "fastq_align_bowtie2", "main.nf") - Path(self.pipeline_dir, "subworkflows", "local").mkdir(exist_ok=True) - local = Path(self.pipeline_dir, "subworkflows", "local", "fastq_align_bowtie2.nf") - shutil.copy(installed, local) - self.subworkflow_remove.remove("fastq_align_bowtie2", force=True) - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, local=True) - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - assert len(subworkflow_lint.warned) >= 0 - - def test_skip_keyword_in_comment(self): - """Test linting a subworkflow where a comment contains a keyword (workflow, subworkflow, take, main, emit)""" - assert self.subworkflow_install.install("bam_stats_samtools") - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ) - ) as fh: - content = fh.read() - new_content = content.replace( - " SAMTOOLS_IDXSTATS ( ch_bam_bai )", - " // This comment contains the word emit:\n SAMTOOLS_IDXSTATS ( ch_bam_bai )", - ) - with open( - Path( - self.pipeline_dir, - "subworkflows", - "nf-core", - "bam_stats_samtools", - "main.nf", - ), - "w", - ) as fh: - fh.write(new_content) - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, subworkflow="bam_stats_samtools") - assert "Included component 'SAMTOOLS_IDXSTATS' not used in main.nf" not in [ - warning.message for warning in subworkflow_lint.warned - ] - - -class TestSubworkflowsLintPatch(TestSubworkflows): - def setUp(self) -> None: - super().setUp() - - # Install the subworkflow bam_sort_stats_samtools - self.subworkflow_install.install("bam_sort_stats_samtools") - - # Modify the subworkflow by inserting a new input channel - new_line = " ch_dummy // channel: [ path ]\n" - - subworkflow_path = Path(self.pipeline_dir, "subworkflows", "nf-core", "bam_sort_stats_samtools", "main.nf") - - with open(subworkflow_path) as fh: - lines = fh.readlines() - for line_index in range(len(lines)): - if "take:" in lines[line_index]: - lines.insert(line_index + 1, new_line) - with open(subworkflow_path, "w") as fh: - fh.writelines(lines) - - # Create a patch file - self.patch_obj = nf_core.subworkflows.SubworkflowPatch(self.pipeline_dir) - self.patch_obj.patch("bam_sort_stats_samtools") - - def test_lint_clean_patch(self): - """Test linting a patched subworkflow""" - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, subworkflow="bam_sort_stats_samtools") - - assert len(subworkflow_lint.failed) == 0, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - assert len(subworkflow_lint.passed) > 0 - # TODO: Add count test back, once we have finished topic conversion - # assert len(subworkflow_lint.warned) == 1, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" - assert any( - [ - x.message.endswith("Can be ignored if the module is using topic channels") - for x in subworkflow_lint.warned - ] - ) - - def test_lint_broken_patch(self): - """Test linting a patched subworkflow when the patch is broken""" - - # Now modify the diff - diff_file = Path( - self.pipeline_dir, "subworkflows", "nf-core", "bam_sort_stats_samtools", "bam_sort_stats_samtools.diff" - ) - subprocess.check_call(["sed", "-i''", "s/...$//", str(diff_file)]) - - subworkflow_lint = nf_core.subworkflows.SubworkflowLint(directory=self.pipeline_dir) - subworkflow_lint.lint(print_results=False, subworkflow="bam_sort_stats_samtools") - - assert len(subworkflow_lint.failed) == 1, f"Linting failed with {[x.__dict__ for x in subworkflow_lint.failed]}" - errors = [x.message for x in subworkflow_lint.failed] - assert "Subworkflow patch cannot be cleanly applied" in errors - assert len(subworkflow_lint.passed) > 0 - # TODO: Add count test back, once we have finished topic conversion - # assert len(subworkflow_lint.warned) == 1, f"Linting warned with {[x.__dict__ for x in subworkflow_lint.warned]}" - assert any( - [ - x.message.endswith("Can be ignored if the module is using topic channels") - for x in subworkflow_lint.warned - ] - ) diff --git a/tests/subworkflows/test_patch.py b/tests/subworkflows/test_patch.py index 659ae2b1c4..884af41e83 100644 --- a/tests/subworkflows/test_patch.py +++ b/tests/subworkflows/test_patch.py @@ -1,4 +1,3 @@ -import os import tempfile from pathlib import Path from unittest import mock @@ -274,11 +273,13 @@ def test_create_patch_update_fail(self): ).install_component_files("bam_sort_stats_samtools", FAIL_SHA, update_obj.modules_repo, temp_dir) temp_module_dir = temp_dir / "bam_sort_stats_samtools" - for file in os.listdir(temp_module_dir): - assert file in os.listdir(swf_path) - with open(swf_path / file) as fh: + temp_files = {f.name for f in temp_module_dir.iterdir()} + swf_files = {f.name for f in swf_path.iterdir()} + for file_name in temp_files: + assert file_name in swf_files + with open(swf_path / file_name) as fh: installed = fh.read() - with open(temp_module_dir / file) as fh: + with open(temp_module_dir / file_name) as fh: shouldbe = fh.read() assert installed == shouldbe @@ -302,6 +303,7 @@ def test_remove_patch(self): with mock.patch.object(nf_core.components.patch.questionary, "confirm") as mock_questionary: mock_questionary.unsafe_ask.return_value = True + patch_obj.no_prompts = False patch_obj.remove("bam_sort_stats_samtools") # Check that the diff file has been removed assert not (subworkflow_path / "bam_sort_stats_samtools.diff").exists() diff --git a/tests/subworkflows/test_remove.py b/tests/subworkflows/test_remove.py index 94cd64d0c1..88ea58acf8 100644 --- a/tests/subworkflows/test_remove.py +++ b/tests/subworkflows/test_remove.py @@ -30,11 +30,9 @@ def test_subworkflows_remove_subworkflow(self): # assert subworkflows key is removed from modules.json assert ( "bam_sort_stats_samtools" - not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["subworkflows"].keys() - ) - assert ( - "samtools/index" not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"].keys() + not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["subworkflows"] ) + assert "samtools/index" not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"] def test_subworkflows_remove_subworkflow_keep_installed_module(self): """Test removing subworkflow and all it's dependencies after installing it, except for a separately installed module""" @@ -57,11 +55,10 @@ def test_subworkflows_remove_subworkflow_keep_installed_module(self): # assert subworkflows key is removed from modules.json assert ( "bam_sort_stats_samtools" - not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["subworkflows"].keys() + not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["subworkflows"] ) assert ( - "samtools/index" - in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"]["nf-core"].keys() + "samtools/index" in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"]["nf-core"] ) def test_subworkflows_remove_one_of_two_subworkflow(self): @@ -120,11 +117,6 @@ def test_subworkflows_remove_subworkflow_keep_installed_cross_org_module(self): assert Path.exists(nfcore_fastqc_path) is True assert mod_json_before != mod_json_after # assert subworkflows key is removed from modules.json - assert CROSS_ORGANIZATION_URL not in mod_json_after["repos"].keys() - assert ( - "fastqc" in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"]["nf-core"].keys() - ) - assert ( - "fastp" - not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"]["nf-core"].keys() - ) + assert CROSS_ORGANIZATION_URL not in mod_json_after["repos"] + assert "fastqc" in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"]["nf-core"] + assert "fastp" not in mod_json_after["repos"]["https://github.com/nf-core/modules.git"]["modules"]["nf-core"] diff --git a/tests/subworkflows/test_update.py b/tests/subworkflows/test_update.py index 7d2f78f041..25a41a1e42 100644 --- a/tests/subworkflows/test_update.py +++ b/tests/subworkflows/test_update.py @@ -4,6 +4,7 @@ from pathlib import Path from unittest import mock +import pytest import questionary import yaml @@ -32,6 +33,66 @@ def test_install_and_update(self): assert update_obj.update("bam_stats_samtools") is True assert cmp_component(tmpdir, sw_path) is True + def test_subworkflow_update_skip_deps_skips_linked_components(self): + """`--skip-deps` updates the named subworkflow's SHA but leaves transitive deps untouched.""" + assert self.subworkflow_install_old.install("fastq_align_bowtie2") + old_mod_json = ModulesJson(self.pipeline_dir).get_modules_json() + old_dep_sha = old_mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"][NF_CORE_MODULES_NAME][ + "samtools/flagstat" + ]["git_sha"] + + update_obj = SubworkflowUpdate(self.pipeline_dir, show_diff=False, skip_deps=True) + assert update_obj.update("fastq_align_bowtie2") is True + + mod_json = ModulesJson(self.pipeline_dir).get_modules_json() + # The named subworkflow's SHA moved. + assert ( + mod_json["repos"][NF_CORE_MODULES_REMOTE]["subworkflows"][NF_CORE_MODULES_NAME]["fastq_align_bowtie2"][ + "git_sha" + ] + != old_mod_json["repos"][NF_CORE_MODULES_REMOTE]["subworkflows"][NF_CORE_MODULES_NAME][ + "fastq_align_bowtie2" + ]["git_sha"] + ) + # A transitively-linked module's SHA did not. + assert ( + mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"][NF_CORE_MODULES_NAME]["samtools/flagstat"]["git_sha"] + == old_dep_sha + ) + + def test_subworkflow_update_skip_deps_save_diff_only_named_component(self): + """`--skip-deps --save-diff` writes a diff for only the named subworkflow, no linked components.""" + assert self.subworkflow_install_old.install("fastq_align_bowtie2") + diff_path = Path(tempfile.mkdtemp()) / "skipdeps.diff" + update_obj = SubworkflowUpdate( + self.pipeline_dir, + show_diff=False, + save_diff_fn=diff_path, + skip_deps=True, + ) + assert update_obj.update("fastq_align_bowtie2") is True + + diff_text = diff_path.read_text() + # The named subworkflow's main.nf should be in the diff + assert "subworkflows/nf-core/fastq_align_bowtie2/main.nf" in diff_text + # Transitively-linked module files (e.g. samtools/*) must not be in the diff + for linked in ( + "modules/nf-core/samtools/flagstat/main.nf", + "modules/nf-core/samtools/index/main.nf", + "modules/nf-core/samtools/sort/main.nf", + ): + assert linked not in diff_text, f"unexpected linked component in --skip-deps diff: {linked}" + + def test_subworkflow_update_skip_deps_with_update_deps_errors(self): + """`--skip-deps` and `--update-deps` are mutually exclusive.""" + with pytest.raises(UserWarning, match="mutually exclusive"): + SubworkflowUpdate(self.pipeline_dir, skip_deps=True, update_deps=True, show_diff=False)._parameter_checks() + + def test_subworkflow_update_skip_deps_with_all_errors(self): + """`--skip-deps` and `--all` are mutually exclusive.""" + with pytest.raises(UserWarning, match="mutually exclusive"): + SubworkflowUpdate(self.pipeline_dir, skip_deps=True, update_all=True, show_diff=False)._parameter_checks() + def test_install_at_hash_and_update(self): """Installs an old version of a subworkflow in the pipeline and updates it""" assert self.subworkflow_install_old.install("fastq_align_bowtie2") @@ -157,7 +218,7 @@ def test_update_with_config_fixed_version(self): # Fix the subworkflow version in the .nf-core.yml to an old version update_config = {NF_CORE_MODULES_REMOTE: {NF_CORE_MODULES_NAME: {"fastq_align_bowtie2": OLD_SUBWORKFLOWS_SHA}}} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) @@ -188,7 +249,7 @@ def test_update_with_config_dont_update(self): # Set the fastq_align_bowtie2 field to no update in the .nf-core.yml update_config = {NF_CORE_MODULES_REMOTE: {NF_CORE_MODULES_NAME: {"fastq_align_bowtie2": False}}} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) @@ -219,7 +280,7 @@ def test_update_with_config_fix_all(self): # Fix the version of all nf-core subworkflows in the .nf-core.yml to an old version update_config = {NF_CORE_MODULES_REMOTE: OLD_SUBWORKFLOWS_SHA} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) @@ -250,7 +311,7 @@ def test_update_with_config_no_updates(self): # Set all repository updates to False update_config = {NF_CORE_MODULES_REMOTE: False} config_fn, tools_config = nf_core.utils.load_tools_config(self.pipeline_dir) - setattr(tools_config, "update", update_config) + tools_config.update = update_config assert config_fn is not None and tools_config is not None # mypy with open(Path(self.pipeline_dir, config_fn), "w") as f: yaml.dump(tools_config.model_dump(), f) diff --git a/tests/test_cli.py b/tests/test_cli.py index 10727c3828..60fd4606f8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -101,21 +101,21 @@ def test_cli_launch(self, mock_launcher): """Test nf-core pipeline is launched and cli parameters are passed on.""" mock_launcher.return_value.launch_pipeline.return_value = True - temp_params_in = tempfile.NamedTemporaryFile() - params = { - "revision": "abcdef", - "id": "idgui", - "command-only": None, - "params-out": "/path/params/out", - "params-in": temp_params_in.name, - "save-all": None, - "show-hidden": None, - "url": "builder_url", - } - cmd = ["pipelines", "launch"] + self.assemble_params(params) + ["pipeline_name"] - result = self.invoke_cli(cmd) + with tempfile.NamedTemporaryFile() as temp_params_in: + params = { + "revision": "abcdef", + "id": "idgui", + "command-only": None, + "params-out": "/path/params/out", + "params-in": temp_params_in.name, + "save-all": None, + "show-hidden": None, + "url": "builder_url", + } + cmd = ["pipelines", "launch"] + self.assemble_params(params) + ["pipeline_name"] + result = self.invoke_cli(cmd) - assert result.exit_code == 0 + assert result.exit_code == 0 mock_launcher.assert_called_once_with( cmd[-1], @@ -127,6 +127,7 @@ def test_cli_launch(self, mock_launcher): "show-hidden" in params, params["url"], params["id"], + "no-prompts" in params, ) mock_launcher.return_value.launch_pipeline.assert_called_once() @@ -246,7 +247,8 @@ def test_create_error(self, mock_create): assert "Partial arguments supplied." in result.output @mock.patch("nf_core.pipelines.create.PipelineCreateApp") - def test_create_app(self, mock_create): + @mock.patch("nf_core.utils.is_interactive", return_value=True) + def test_create_app(self, mock_interactive, mock_create): """Test `nf-core pipelines create` runs an App.""" cmd = ["pipelines", "create"] result = self.invoke_cli(cmd) @@ -267,37 +269,37 @@ def test_lint(self, mock_lint, mock_is_pipeline): mock_lint_results[2].failed = [] mock_lint.return_value = mock_lint_results - temp_pipeline_dir = tempfile.NamedTemporaryFile() - params = { - "dir": temp_pipeline_dir.name, - "release": None, - "fix": "fix test", - "key": "key test", - "show-passed": None, - "fail-ignored": None, - "fail-warned": None, - "markdown": "output_file.md", - "json": "output_file.json", - } - - cmd = ["pipelines", "lint"] + self.assemble_params(params) - result = self.invoke_cli(cmd) + with tempfile.NamedTemporaryFile() as temp_pipeline_dir: + params = { + "dir": temp_pipeline_dir.name, + "release": None, + "fix": "fix test", + "key": "key test", + "show-passed": None, + "fail-ignored": None, + "fail-warned": None, + "markdown": "output_file.md", + "json": "output_file.json", + } + + cmd = ["pipelines", "lint"] + self.assemble_params(params) + result = self.invoke_cli(cmd) - assert result.exit_code == 0 - mock_lint.assert_called_once_with( - params["dir"], - "release" in params, - (params["fix"],), - (params["key"],), - "show-passed" in params, - "fail-ignored" in params, - "fail-warned" in params, - "test", - params["markdown"], - params["json"], - "hide-progress" in params, - False, # plain_text - ) + assert result.exit_code == 0 + mock_lint.assert_called_once_with( + params["dir"], + "release" in params, + (params["fix"],), + (params["key"],), + "show-passed" in params, + "fail-ignored" in params, + "fail-warned" in params, + "test", + params["markdown"], + params["json"], + "hide-progress" in params, + False, # plain_text + ) def test_lint_no_dir(self): """Test nf-core pipelines lint fails if --dir does not exist""" diff --git a/tests/test_lint_utils.py b/tests/test_lint_utils.py index a4b7caf307..3bda1f83fd 100644 --- a/tests/test_lint_utils.py +++ b/tests/test_lint_utils.py @@ -1,5 +1,3 @@ -import shutil - import git import pytest @@ -9,8 +7,6 @@ JSON_MALFORMED = "{'a':1}" JSON_FORMATTED = '{ "a": 1 }\n' -WHICH_PRE_COMMIT = shutil.which("pre-commit") - @pytest.fixture() def temp_git_repo(tmp_path_factory): @@ -63,3 +59,10 @@ def test_run_prettier_on_syntax_error_file(syntax_error_json, caplog): nf_core.pipelines.lint_utils.run_prettier_on_file(syntax_error_json) expected_critical_log = "SyntaxError: Unexpected token (1:10)" assert expected_critical_log in caplog.text + + +def test_run_prettier_prek_not_in_path(formatted_json, monkeypatch, tmp_path): + """prek must be found via sys.executable when not on PATH (e.g. uv tool install).""" + monkeypatch.setenv("PATH", str(tmp_path)) + nf_core.pipelines.lint_utils.run_prettier_on_file(formatted_json) + assert formatted_json.read_text() == JSON_FORMATTED diff --git a/tests/test_modules.py b/tests/test_modules.py index 9c39abfd15..fce0c848ee 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -9,7 +9,6 @@ import responses import ruamel.yaml -import nf_core.modules import nf_core.modules.create import nf_core.modules.install import nf_core.modules.modules_repo diff --git a/tests/test_subworkflows.py b/tests/test_subworkflows.py index 5c66dd37b3..5bd9a9d170 100644 --- a/tests/test_subworkflows.py +++ b/tests/test_subworkflows.py @@ -6,9 +6,7 @@ import pytest -import nf_core.modules import nf_core.modules.install -import nf_core.pipelines.create.create import nf_core.subworkflows from .utils import ( diff --git a/tests/test_utils.py b/tests/test_utils.py index f72de515c1..fe5ff56d8e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,13 +1,13 @@ """Tests covering for utility functions.""" import os +import sys from pathlib import Path from unittest import mock import pytest import requests -import nf_core.pipelines.create.create import nf_core.pipelines.list import nf_core.utils @@ -17,6 +17,36 @@ TEST_DATA_DIR = Path(__file__).parent / "data" +def test_is_interactive_consults_all_streams(): + """Verify all three streams are checked — catches a forgotten stream or hardcoded return.""" + with ( + mock.patch.object(sys.stdin, "isatty", return_value=True) as m_in, + mock.patch.object(sys.stdout, "isatty", return_value=True) as m_out, + mock.patch.object(sys.stderr, "isatty", return_value=True) as m_err, + ): + assert nf_core.utils.is_interactive() is True + m_in.assert_called_once() + m_out.assert_called_once() + m_err.assert_called_once() + + +@pytest.mark.parametrize( + "stdin,stdout,stderr", + [ + (False, True, True), + (True, False, True), + (True, True, False), + ], +) +def test_is_interactive_false_if_any_non_tty(stdin, stdout, stderr): + with ( + mock.patch.object(sys.stdin, "isatty", return_value=stdin), + mock.patch.object(sys.stdout, "isatty", return_value=stdout), + mock.patch.object(sys.stderr, "isatty", return_value=stderr), + ): + assert nf_core.utils.is_interactive() is False + + def test_strip_ansi_codes(): """Check that we can make rich text strings plain @@ -144,7 +174,7 @@ def test_get_repo_releases_branches_nf_core(self): break else: raise AssertionError("Release 1.6 not found") - assert "dev" in wf_branches.keys() + assert "dev" in wf_branches def test_get_repo_releases_branches_not_nf_core(self): wfs = nf_core.pipelines.list.Workflows() @@ -155,7 +185,7 @@ def test_get_repo_releases_branches_not_nf_core(self): break else: raise AssertionError("MultiQC release v1.32 not found") - assert "main" in wf_branches.keys() + assert "main" in wf_branches def test_get_repo_releases_branches_not_exists(self): wfs = nf_core.pipelines.list.Workflows() @@ -212,9 +242,8 @@ def test_set_wd(self): def test_set_wd_revert_on_raise(self): wd_before_context = Path().resolve() - with pytest.raises(Exception): - with nf_core.utils.set_wd(self.tmp_dir): - raise Exception + with pytest.raises(RuntimeError), nf_core.utils.set_wd(self.tmp_dir): + raise RuntimeError assert wd_before_context == Path().resolve() @mock.patch("nf_core.utils.run_cmd") diff --git a/tests/utils.py b/tests/utils.py index 5145ff3fc7..02e2f4ad0b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -12,7 +12,6 @@ import responses import yaml -import nf_core.modules import nf_core.pipelines.create.create from nf_core import __version__ from nf_core.utils import NFCoreTemplateConfig, NFCoreYamlConfig, custom_yaml_dumper diff --git a/uv.lock b/uv.lock index bb27b14154..ac0b46a1df 100644 --- a/uv.lock +++ b/uv.lock @@ -2,15 +2,9 @@ version = 1 revision = 3 requires-python = ">=3.10, <4" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] @@ -25,7 +19,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -37,110 +31,110 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, - { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, - { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, - { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, - { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, - { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, - { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, - { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, + { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, + { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, + { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] [[package]] @@ -205,6 +199,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/df/32574bc8f1d440d40f4aaf3b455316b2b1536c7243c985a90f8516cf3074/arcp-0.2.1-py2.py3-none-any.whl", hash = "sha256:4e09b2d8a9fc3fda7ec112b553498ff032ea7de354e27dbeb1acc53667122444", size = 15838, upload-time = "2020-02-12T12:05:05.769Z" }, ] +[[package]] +name = "ast-serialize" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" }, + { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" }, + { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" }, + { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" }, + { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" }, + { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" }, + { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" }, + { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -214,25 +246,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] -[[package]] -name = "attmap" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ubiquerg" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1b/47/1b222d01989c248c4638e0fba281a12388f03dbc77a8746092a255ea6a21/attmap-0.13.2.tar.gz", hash = "sha256:fdffa45f8671c13428eb8c3a1702bfdd1123badb99f7af14d72ad53cc7e770de", size = 10886, upload-time = "2021-11-05T00:54:47.191Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/14/20b368acd5aacbd0f01004f7ac8b57ced1a961833795c053fd87774ce7e8/attmap-0.13.2-py3-none-any.whl", hash = "sha256:9c76af312c3678927a03ebb8fd2fa3a9cab37f7ce34f1dc574ea890c778f2f26", size = 12648, upload-time = "2021-11-05T00:54:46.031Z" }, -] - [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] @@ -255,25 +275,25 @@ wheels = [ [[package]] name = "cattrs" -version = "25.3.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" }, + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, ] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] @@ -358,114 +378,121 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -491,115 +518,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, - { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, - { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, - { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, - { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, - { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, - { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, - { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, - { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, - { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, - { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, - { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [package.optional-dependencies] @@ -609,71 +636,62 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, -] - -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, ] [[package]] @@ -693,15 +711,9 @@ name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ @@ -710,19 +722,18 @@ wheels = [ [[package]] name = "eido" -version = "0.2.4" +version = "0.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema" }, { name = "logmuse" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, { name = "peppy" }, { name = "ubiquerg" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/2a/050b310ef179c48cb81e4b4c66afa3f95839d1de153e34b2968e4ff8799c/eido-0.2.4.tar.gz", hash = "sha256:effb950a2150be80af4c109dc4ea9d93f63903059138b5178de9f081ce398d7c", size = 16472, upload-time = "2024-09-30T19:53:30.698Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/4a/97c650e694b8a1571d749eb809d4b1850d73524a2148b71ef09b636783cd/eido-0.2.5.tar.gz", hash = "sha256:38c21e8678a4e37e2b0680225dd4cabe68482b5156c252199838faa06d7adf47", size = 16603, upload-time = "2026-02-03T21:05:49.598Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/50/6c4bac12b4c0fc363ecfa28b8264698e1918cd92cbcc76b0daf759f6657d/eido-0.2.4-py3-none-any.whl", hash = "sha256:0a80adcce10dbcc72940415ff9fd59553d2f44f3e8cffecc5c0cde133fa57ea6", size = 16844, upload-time = "2024-09-30T19:53:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f6/54de5d115447a5c2d98a12c9adcf43452a58efe076003bdac459fce76f07/eido-0.2.5-py3-none-any.whl", hash = "sha256:2d14b4923c52bc3554c7d016c0f638fdd61a8e083c3917f44988e2e598c58491", size = 16910, upload-time = "2026-02-03T21:05:47.516Z" }, ] [[package]] @@ -746,15 +757,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] -[[package]] -name = "filelock" -version = "3.24.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/8a/b24ff2c2d7f20ce930b5efe91e7260247d185d8939707721168ad204e465/filelock-3.24.1.tar.gz", hash = "sha256:3440181dd03f8904c108c8e9f5b11d1663e9fc960f1c837586a11f1c5c041e54", size = 37452, upload-time = "2026-02-15T22:03:16.564Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/64/3613e89811e79aca8d0d4f2c984fc66336bc9d83529c1cbe02f5df010d0a/filelock-3.24.1-py3-none-any.whl", hash = "sha256:7c59f595e3cf4887dc95b403a896849da49ed183d7c9d7ee855646ca99f10698", size = 24153, upload-time = "2026-02-15T22:03:15.262Z" }, -] - [[package]] name = "filetype" version = "1.2.0" @@ -885,15 +887,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, -] - [[package]] name = "gitdb" version = "4.0.12" @@ -908,14 +901,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.46" +version = "3.1.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] [[package]] @@ -930,31 +923,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] -[[package]] -name = "identify" -version = "2.6.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, -] - [[package]] name = "idna" -version = "3.11" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] name = "imagesize" -version = "1.4.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] @@ -1057,111 +1041,108 @@ wheels = [ [[package]] name = "librt" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/e9/018cfd60629e0404e6917943789800aa2231defbea540a17b90cc4547b97/librt-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db63cf3586a24241e89ca1ce0b56baaec9d371a328bd186c529b27c914c9a1ef", size = 65690, upload-time = "2026-02-12T14:51:57.761Z" }, - { url = "https://files.pythonhosted.org/packages/b5/80/8d39980860e4d1c9497ee50e5cd7c4766d8cfd90d105578eae418e8ffcbc/librt-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba9d9e60651615bc614be5e21a82cdb7b1769a029369cf4b4d861e4f19686fb6", size = 68373, upload-time = "2026-02-12T14:51:59.013Z" }, - { url = "https://files.pythonhosted.org/packages/2d/76/6e6f7a443af63977e421bd542551fec4072d9eaba02e671b05b238fe73bc/librt-0.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb4b3ad543084ed79f186741470b251b9d269cd8b03556f15a8d1a99a64b7de5", size = 197091, upload-time = "2026-02-12T14:52:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/14/40/fa064181c231334c9f4cb69eb338132d39510c8928e84beba34b861d0a71/librt-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d2720335020219197380ccfa5c895f079ac364b4c429e96952cd6509934d8eb", size = 207350, upload-time = "2026-02-12T14:52:02.32Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/e7f8438dd226305e3e5955d495114ad01448e6a6ffc0303289b4153b5fc5/librt-0.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726305d3e53419d27fc8cdfcd3f9571f0ceae22fa6b5ea1b3662c2e538f833e", size = 219962, upload-time = "2026-02-12T14:52:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/1f/2c/74086fc5d52e77107a3cc80a9a3209be6ad1c9b6bc99969d8d9bbf9fdfe4/librt-0.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3d107f603b5ee7a79b6aa6f166551b99b32fb4a5303c4dfcb4222fc6a0335e", size = 212939, upload-time = "2026-02-12T14:52:05.537Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ae/d6917c0ebec9bc2e0293903d6a5ccc7cdb64c228e529e96520b277318f25/librt-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41064a0c07b4cc7a81355ccc305cb097d6027002209ffca51306e65ee8293630", size = 221393, upload-time = "2026-02-12T14:52:07.164Z" }, - { url = "https://files.pythonhosted.org/packages/04/97/15df8270f524ce09ad5c19cbbe0e8f95067582507149a6c90594e7795370/librt-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c6e4c10761ddbc0d67d2f6e2753daf99908db85d8b901729bf2bf5eaa60e0567", size = 216721, upload-time = "2026-02-12T14:52:08.857Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/17cbcf9b7a1bae5016d9d3561bc7169b32c3bd216c47d934d3f270602c0c/librt-0.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba581acad5ac8f33e2ff1746e8a57e001b47c6721873121bf8bbcf7ba8bd3aa4", size = 214790, upload-time = "2026-02-12T14:52:10.033Z" }, - { url = "https://files.pythonhosted.org/packages/2a/2d/010a236e8dc4d717dd545c46fd036dcced2c7ede71ef85cf55325809ff92/librt-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bdab762e2c0b48bab76f1a08acb3f4c77afd2123bedac59446aeaaeed3d086cf", size = 237384, upload-time = "2026-02-12T14:52:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/f1c0eff3df8760dee761029efb72991c554d9f3282f1048e8c3d0eb60997/librt-0.8.0-cp310-cp310-win32.whl", hash = "sha256:6a3146c63220d814c4a2c7d6a1eacc8d5c14aed0ff85115c1dfea868080cd18f", size = 54289, upload-time = "2026-02-12T14:52:12.798Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0b/2684d473e64890882729f91866ed97ccc0a751a0afc3b4bf1a7b57094dbb/librt-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:bbebd2bba5c6ae02907df49150e55870fdd7440d727b6192c46b6f754723dde9", size = 61347, upload-time = "2026-02-12T14:52:13.793Z" }, - { url = "https://files.pythonhosted.org/packages/51/e9/42af181c89b65abfd557c1b017cba5b82098eef7bf26d1649d82ce93ccc7/librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369", size = 65314, upload-time = "2026-02-12T14:52:14.778Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4a/15a847fca119dc0334a4b8012b1e15fdc5fc19d505b71e227eaf1bcdba09/librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6", size = 68015, upload-time = "2026-02-12T14:52:15.797Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/ffc8dbd6ab68dd91b736c88529411a6729649d2b74b887f91f3aaff8d992/librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa", size = 194508, upload-time = "2026-02-12T14:52:16.835Z" }, - { url = "https://files.pythonhosted.org/packages/89/92/a7355cea28d6c48ff6ff5083ac4a2a866fb9b07b786aa70d1f1116680cd5/librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f", size = 205630, upload-time = "2026-02-12T14:52:18.58Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5e/54509038d7ac527828db95b8ba1c8f5d2649bc32fd8f39b1718ec9957dce/librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef", size = 218289, upload-time = "2026-02-12T14:52:20.134Z" }, - { url = "https://files.pythonhosted.org/packages/6d/17/0ee0d13685cefee6d6f2d47bb643ddad3c62387e2882139794e6a5f1288a/librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9", size = 211508, upload-time = "2026-02-12T14:52:21.413Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/1714ef6e9325582e3727de3be27e4c1b2f428ea411d09f1396374180f130/librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4", size = 219129, upload-time = "2026-02-12T14:52:22.61Z" }, - { url = "https://files.pythonhosted.org/packages/89/d3/2d9fe353edff91cdc0ece179348054a6fa61f3de992c44b9477cb973509b/librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6", size = 213126, upload-time = "2026-02-12T14:52:23.819Z" }, - { url = "https://files.pythonhosted.org/packages/ad/8e/9f5c60444880f6ad50e3ff7475e5529e787797e7f3ad5432241633733b92/librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da", size = 212279, upload-time = "2026-02-12T14:52:25.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/eb/d4a2cfa647da3022ae977f50d7eda1d91f70d7d1883cf958a4b6ef689eab/librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1", size = 234654, upload-time = "2026-02-12T14:52:26.204Z" }, - { url = "https://files.pythonhosted.org/packages/6a/31/26b978861c7983b036a3aea08bdbb2ec32bbaab1ad1d57c5e022be59afc1/librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258", size = 54603, upload-time = "2026-02-12T14:52:27.342Z" }, - { url = "https://files.pythonhosted.org/packages/d0/78/f194ed7c48dacf875677e749c5d0d1d69a9daa7c994314a39466237fb1be/librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817", size = 61730, upload-time = "2026-02-12T14:52:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/97/ee/ad71095478d02137b6f49469dc808c595cfe89b50985f6b39c5345f0faab/librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4", size = 52274, upload-time = "2026-02-12T14:52:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, - { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, - { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, - { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, - { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, - { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, - { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, - { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, - { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, - { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, - { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, - { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, - { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, - { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, - { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, - { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, - { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, - { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, - { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, - { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, - { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, - { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, - { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, - { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/18/827e5c1262a88c2602e86f99aee0f288ffea3280dbd2ff448858ef9dc6e9/librt-0.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dc99f9642100b86e5f6bb14cdc9970009e31a9ef7d64df6704b7018451524a3", size = 76461, upload-time = "2026-05-05T16:29:00.422Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/54254e30287f5a5abec6fef22d976987476e966be5fdff51fe8c2d5d73d1/librt-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8298cedfcfaff3790000bd057aaaa3df1b0ab54cf7b48eeab16184cbb1bc66b9", size = 79740, upload-time = "2026-05-05T16:29:01.926Z" }, + { url = "https://files.pythonhosted.org/packages/4c/20/e93264b52113669d98d3b63ff94d4ce0c4dd49ae0503f1788440a884e5f0/librt-0.10.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7dbe312dbf76468255b79a7ba311236fde620f2f7055fc09d421e31340314e", size = 243472, upload-time = "2026-05-05T16:29:03.373Z" }, + { url = "https://files.pythonhosted.org/packages/35/ad/34a5141178e8b18a4cfa45d1a0d523c84397e2abd5d06fea2d846da687e8/librt-0.10.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:56ed90c48c19249012dadfd79a1bc13bd5168ea60a70722d330a3a600c0b1852", size = 232073, upload-time = "2026-05-05T16:29:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/97/1f/67240e910cd9f9ab1498c1470738345fc29dce5dc9719db1e0e09d1e861f/librt-0.10.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d74ca0f4b2b09c117f913d4df01f6b934dff8a271096b35167d5264a31649f0", size = 256956, upload-time = "2026-05-05T16:29:06.516Z" }, + { url = "https://files.pythonhosted.org/packages/22/50/3a2b3482c27d607f6e8216d913c6bc592b9a2141d96990309452340a78e3/librt-0.10.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8eb2daa9375f93c0e55ff5e44a4bbe98f39e5fe52e1abf9c97acb67743b61bf8", size = 250593, upload-time = "2026-05-05T16:29:08.324Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1c/07dba133d79f93322fa17514062f1a2a50d6bdfb7baec4acf78193d7fad1/librt-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7b09b90e634e6dff57978cd358070046071e2b120501f10787aeb35425f504f6", size = 263582, upload-time = "2026-05-05T16:29:09.866Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/033f2c6d6ab0b48f15f02e5bf065521b11a51922806017f8b6274df30d69/librt-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2cf22fd379d60c739b800d4295ed34045f8b04aa8df9c12bd2f8f43f7fe672b7", size = 259307, upload-time = "2026-05-05T16:29:11.675Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/679046cd75d5a52c0104c890d8f69574ef4e619c683e59c15584d03a2457/librt-0.10.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:74c798793fcf29a84d442278ebe0bb1fff79fe58ac4106eeff7019cbba861423", size = 257342, upload-time = "2026-05-05T16:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d5/dbaac9c0884f78a53dda22b9ec92bb788e1400e762ed7623fa96928c8da5/librt-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc4f1573401e8dbe6c26511fe027620b0fb30ae9a7ab814e02e510626b8b5f9c", size = 280141, upload-time = "2026-05-05T16:29:14.922Z" }, + { url = "https://files.pythonhosted.org/packages/cc/81/71f18cf8eb340d9fda011498870910f6a8697aeb50833005d3d8107653fd/librt-0.10.0-cp310-cp310-win32.whl", hash = "sha256:e1428275f5fe3d4db6822e58d8b005a5b28ffca55e8433ebc051247fbe46429f", size = 62257, upload-time = "2026-05-05T16:29:16.226Z" }, + { url = "https://files.pythonhosted.org/packages/df/52/6bcebc2f870c4836bcb372be885fae7f17a1d25037d3a8250ef79fbe0124/librt-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:0708e9408f585b0f065081680583a577652099680ccf820c7538904322b679c3", size = 70321, upload-time = "2026-05-05T16:29:17.41Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/1472717d2325adacc8d335ba2e4078015c09d75b599f3cf48e967b3d306e/librt-0.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01b4500ca3a625450c032a9142a8e843923ce263fa8a92ad1b38927cabe2fe72", size = 76045, upload-time = "2026-05-05T16:29:18.731Z" }, + { url = "https://files.pythonhosted.org/packages/a6/31/bfe32355d4b369aef3d7aa442df663bb5558c2ffa2de286cb2956346bc24/librt-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b7e42d1b3e300d20bfc87e72ffd62f0a92a2cb3c35f7bf90df90c9d2a49f74c", size = 79466, upload-time = "2026-05-05T16:29:20.052Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f1/83f8a2c715ba2cac9b7387a5a5cea25f717f7184320cfe48b36bed9c58e9/librt-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef7b8c61ce3a1b597cd3e15348ff1574325165c2e7ce09a718154cde2a7950", size = 242283, upload-time = "2026-05-05T16:29:21.596Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/c3a4ce94857f0004a542f86662806383611858f522722db58efaec0a1472/librt-0.10.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e73c84f72d1fa0d6eaa7a1930b436ba8d2c90c58d77bfabb09995a69ad35f6c0", size = 230735, upload-time = "2026-05-05T16:29:23.335Z" }, + { url = "https://files.pythonhosted.org/packages/d1/41/e962bb26c7728eb7b3a69e490d0c800fd9968a6970e390c1f18ddb56093d/librt-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9728cb98713bd862fb8f4fd6a642d1896c86058a41d77c70f3d5cee75e725275", size = 256606, upload-time = "2026-05-05T16:29:24.91Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/4e46a707b1ecc993fd691071623b9beab89703a63bd21cc7807e06c28209/librt-0.10.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:648b7e941d20acd72f9652115e0e53facd98156d61f9ebf7a812bdef8bdccea9", size = 249739, upload-time = "2026-05-05T16:29:26.648Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/dc5b7eb294656ad23d4ff4cf8514208d54fe1026b909d726a0dc026689c9/librt-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3e33747c068e86a9007c20fdb777eb5ba8d3d19136d7812f88e69a713041b6f", size = 261414, upload-time = "2026-05-05T16:29:28.702Z" }, + { url = "https://files.pythonhosted.org/packages/58/e4/990ed8d12c7f114ac8f8ccd47f7d9bd9704ef61acfcb1df4a05047da7710/librt-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d509c745bf7e77d1107cf05e6abb249dc03fad13eb39f2286a49deedaeb2bcd7", size = 256614, upload-time = "2026-05-05T16:29:30.357Z" }, + { url = "https://files.pythonhosted.org/packages/60/eb/52d2726c7fb22818507dc3cc166c8f36dd4a4b68a7be67f12006ac8777c1/librt-0.10.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:786ad5a15e99d0e0e74f3adbeecc198a5ac58f340be07e984723d1e0074838de", size = 255144, upload-time = "2026-05-05T16:29:32.106Z" }, + { url = "https://files.pythonhosted.org/packages/bc/df/bd5591a78f7531fce4b6eb9962aadc6adc9560a01570442a884b6e554abe/librt-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:075582d877a97ee3d8e77bda3689dbe617b14f6469224a2d80b4b6c38e3951aa", size = 279121, upload-time = "2026-05-05T16:29:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/fd/df/7c2b838dfc89a1762dd156d8b0c39848a7a2845d725a50be5a6e021fb8ba/librt-0.10.0-cp311-cp311-win32.whl", hash = "sha256:75ecdc3f5a90065aa2af2e574706c5495adc392520762dcf10b1aa716f0b8090", size = 62593, upload-time = "2026-05-05T16:29:35.152Z" }, + { url = "https://files.pythonhosted.org/packages/91/19/22ff572981049a9d436a083dbea1572d0f5dc068b7353637d2dd9977c8f1/librt-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6f6084884131d8a52cb9d7095ff2aa52c1e786d9fdaefab1fb4515415e9e083", size = 70914, upload-time = "2026-05-05T16:29:36.407Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/1697cc64f4a5c7e9bce55e99c6d234a346beaedaefcd1e2ca90dd285f98c/librt-0.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:0140bd62151160047e89b2730cb6f8506cdac5127baa1afb9231e4dd3fe7f681", size = 61176, upload-time = "2026-05-05T16:29:37.62Z" }, + { url = "https://files.pythonhosted.org/packages/12/8e/cbb5b6f6e45e65c10a42449a69eaccc44d73e6a081ea752fbc5221c6dc1c/librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba", size = 77327, upload-time = "2026-05-05T16:29:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3d/8233cbee8e99e6a8992f02bfc2dec8d787509566a511d1fde2574ee7473f/librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5", size = 79971, upload-time = "2026-05-05T16:29:40.96Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/5264b298cef2b72fc97d2dde56c66181eda35204bf5dcd1ed0c3d0a0a782/librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37", size = 246559, upload-time = "2026-05-05T16:29:42.701Z" }, + { url = "https://files.pythonhosted.org/packages/07/7b/19b1b859cc60d5f99276cc2b3144d91556c6d1b1e4ebb50359696bebf7a8/librt-0.10.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:70b955f091beac93e994a0b7ec616934f63b3ea5c3d6d7af847562f935aceca7", size = 235216, upload-time = "2026-05-05T16:29:44.193Z" }, + { url = "https://files.pythonhosted.org/packages/6e/56/a2f40717142a8af46289f57874ef914353d8faccd5e4f8e594ab1e16e8c7/librt-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:483e685e06b6163728ba6c85d74315176be7190f432ec2a41226e5e14355d5f0", size = 263108, upload-time = "2026-05-05T16:29:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/ca/15c625c3bdc0167c01e04ef8878317e9713f3bfa788438342f7a94c7b22c/librt-0.10.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ac53d946a009d1a38c44a60812708c9458fb2a239a5f630d8e625571386650f", size = 255280, upload-time = "2026-05-05T16:29:48.087Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/ba301d571d9e05844e2435b73aba30bee77bb75ce155c9affcfd2173dd03/librt-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc8771c9fcf0ea894ca41fdc2abd83572c2fbda221f232d86e718614e57ff513", size = 268829, upload-time = "2026-05-05T16:29:49.628Z" }, + { url = "https://files.pythonhosted.org/packages/8b/60/af70e135bc1f1fe15dd3894b1e4bbefc7ecdf911749a925a39eb86ceb2a1/librt-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70805dbc5257892ac572f86290a61e3c8d90224ecce1a8b2d1f7ed51965417f4", size = 262051, upload-time = "2026-05-05T16:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/c2/c8236eb8b421bac5a172ba208f965abaa89805da2a3fa112bdf1764caf8f/librt-0.10.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d3b4f300f7bcba6e2ff73fb8bef1898479e9772bfa2682998c636391633ec826", size = 264347, upload-time = "2026-05-05T16:29:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/15b6d32bc25dacd4a60886a683d8128d6219910c122202b995a40dd4f8d2/librt-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:943bc943f92f4fb3408fae62485c6a3ad68ce4f2ee205643a39641525c19a276", size = 286482, upload-time = "2026-05-05T16:29:54.675Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8e/b1b959bacd323eb4360579db992513e1406d1c6ef7edb57b5511fd0666fd/librt-0.10.0-cp312-cp312-win32.whl", hash = "sha256:6065c1a758fba1010b41401013903d3d5d2750eab425ddedd584abac31d0630e", size = 62955, upload-time = "2026-05-05T16:29:56.39Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4c/d4cd6e4b9fc24098e63cc85537d1b6689682aee96809c38f08072067cc2b/librt-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d788ecbe208ab352dab0e105cc06057bf9a2fc7e58cabb0d751ad9e30062b9e2", size = 71191, upload-time = "2026-05-05T16:29:57.682Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/8641da1f63d24b92354a492f893c022d6b3a0df44e70c8eff49364613983/librt-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:6003d1f295bdba02656dc81308208fc060d0a51d8c0d0a6db70f7f3c57b9ba0a", size = 61432, upload-time = "2026-05-05T16:29:58.971Z" }, + { url = "https://files.pythonhosted.org/packages/e5/29/681a75c82f4cc90d29e4b257a3299b79fe13fe927a04c57b8109d70b6957/librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596", size = 77299, upload-time = "2026-05-05T16:30:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/62/24/0c7ca445a55d04be79cac19819437fd094782347fa116f6681844fa6143e/librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e", size = 79930, upload-time = "2026-05-05T16:30:01.555Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/1e2b8f6443ef9e9a81e89486ca70e22f3684f93db003ce6eaefc3d0839b9/librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275", size = 246195, upload-time = "2026-05-05T16:30:03.261Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/9dc9e03de0439ad84c1c240aac8b747f12c90cb797ea6042f7bdb8d3410f/librt-0.10.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:723ba80594c49cdf0584196fc430752262605dc9449902fc9bd3d9b79976cb77", size = 234951, upload-time = "2026-05-05T16:30:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/635223117d7590875bca441275065a3bf491203ad4208bd1cc3ffd90c5a1/librt-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7292edaaca294a61a978c53a3c7d6130d099b0dfbc8f0a65916cdc6b891b9852", size = 262768, upload-time = "2026-05-05T16:30:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/b04152d0cd8b6ca2b428a8bd3230343230c35ed304a932f35b5375f2f828/librt-0.10.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fe9d539f2c10a1666633eeeac507ce95dd06d9ecc58de3c6390dba156a3d3a", size = 255075, upload-time = "2026-05-05T16:30:08.216Z" }, + { url = "https://files.pythonhosted.org/packages/35/1e/25bac4c7f2ca36f0e612cade186970683cf79153d96beccc3a11a9e19b97/librt-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4efa7b9587503fa5b67f40593302b9c8836d211d222ff9f7cafe67be5f8f0b10", size = 268559, upload-time = "2026-05-05T16:30:10.1Z" }, + { url = "https://files.pythonhosted.org/packages/18/54/4601faab35b6632a13200faa146ca62bfd111ffbe2568be430d65c89493a/librt-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22dc982ef59df0136df36092ccbdbb570ced8aafb33e49585739b2f1de1c13b6", size = 261753, upload-time = "2026-05-05T16:30:11.912Z" }, + { url = "https://files.pythonhosted.org/packages/1b/cf/39f4023509e94fade8b074666fa3292db9cb6b34ea5dcbe7af53df9fca1d/librt-0.10.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6f2e5f3606253a84cea719c94a3bb1c54487b5d617d0254d46e0920d8a06be3f", size = 264055, upload-time = "2026-05-05T16:30:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8e/00/40247209fc46a8e308a91412d5206aedf8efb667ee89eb625820106a5c2f/librt-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40884bfaa1e29f6b6a9be255007d8f359bfc9e61d68bdef8ed3158bfcbc95df9", size = 286190, upload-time = "2026-05-05T16:30:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/5566beb94431a985abe1787af5ef86e087750172ff9d0bbf20f93e88132d/librt-0.10.0-cp313-cp313-win32.whl", hash = "sha256:3cd34cd8254eba756660bff6c2da91278248184301054fe3e4feb073bdd49b14", size = 62949, upload-time = "2026-05-05T16:30:16.503Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c2/3ea3301d6c8dff51d39dbe8ed75db3dc92896947d4afb5eeadf821c1e67f/librt-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:7baac5313e2d8dce1386f97777a8d03ab28f5fe1e780b3b9ac2ee7544551fedc", size = 71152, upload-time = "2026-05-05T16:30:17.766Z" }, + { url = "https://files.pythonhosted.org/packages/3c/de/5d49cb92cadcbc77d3abc27b93fd6030ed8437487dde2eae38cab5e6704d/librt-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:afc5b4406c8e2515698d922a5c7823a009312835ea58196671fff40e35cb8166", size = 61336, upload-time = "2026-05-05T16:30:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/7165e08108cc185a13a9c069f0685e6ef92e70e07fddf7edf5e7348c6316/librt-0.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f09588a30e6a22ec624090d72a3ab1a6d4d5485c3ed739603e76aa3c16efa688", size = 76794, upload-time = "2026-05-05T16:30:20.392Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ef/bf8613febf651b90c5222ee79dea5ae58d4cc2b544df69d3033424448934/librt-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:131ade118d12bd7a0adc4e655474a553f1b76cf78385868885944d21d51e45e0", size = 79662, upload-time = "2026-05-05T16:30:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/9eddd165c1d8397bdf99b38bf12b5a55b3def5035b49eedb49f2775d1430/librt-0.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8b9ab28e40d011c373a189eae900c916e66d6fbecf7983e9e4883089ee085ef", size = 242390, upload-time = "2026-05-05T16:30:23.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/d1/d95da80334501866cd37004ab5d7483220d05862fab4b5405394f0264f0d/librt-0.10.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:67c39bb30da73bae1f293d1ed8bc2f8f6642649dd0928d3600aeff3041ac23d6", size = 232603, upload-time = "2026-05-05T16:30:25.198Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fa/e6d64d28718bc1be4e1736fcb037ca1c4dfca927e7167df75a7d5215665e/librt-0.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3273c6b774614f093c8927c2bf1b077d0fefde988fe98f46a333734e5597ab", size = 259187, upload-time = "2026-05-05T16:30:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/72/3f/3fdb77e7f937dad59cfd76b720be7e7643400ec76b2da35befab8d66ba30/librt-0.10.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9dd7c1b86a4baa583ab5db977484b93a2c474e69e96ef3e9538387ea54229cb9", size = 251846, upload-time = "2026-05-05T16:30:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/18/ca/f4d49133dd86a6f55d79eca30bf412fa722f511a9abe67f62f57aa64e66a/librt-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a77385c5a202e831149f7ad03be9e67cf80e957e52c614e83dcb822c95222eb8", size = 264936, upload-time = "2026-05-05T16:30:30.491Z" }, + { url = "https://files.pythonhosted.org/packages/de/66/a8df2fbadc1f6c1827a096d11c40175bd526133480bd3bc88ec64a03d257/librt-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6a5eafa74b5655bad59886138ed68426f098a6beb8cb95a71f2cc3cd8bb33fe", size = 258699, upload-time = "2026-05-05T16:30:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/1e3c83613fe05451bb969e27b68a573d177f08d5f63533cc29fec0989658/librt-0.10.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1fc93d0439204c50ab4d1512611ce2c206f1b369b419f69c7c27c761561e3291", size = 259825, upload-time = "2026-05-05T16:30:35.077Z" }, + { url = "https://files.pythonhosted.org/packages/09/24/5e2f926ee9d3ef348d9339526d7062abb5c44d8419e3179528c01d78c102/librt-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:79e713c178bc7a744adfbee6b4619a288eecc0c914da2a9313a20255abe2f0cf", size = 282548, upload-time = "2026-05-05T16:30:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7d/3e89ed6ad0162561fa8bef9df3195e24263104c955713cd0237d3711fad2/librt-0.10.0-cp314-cp314-win32.whl", hash = "sha256:2eba9d955a68c41d9f326be3da42f163ec3518b7ab20f1c826224e7bed71e0bf", size = 58970, upload-time = "2026-05-05T16:30:38.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/579e731c94a7086a268bfa3e7a4945cd47836bebd3cbf3faeafd2e7eaef9/librt-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbfaf7f5145e9917f5d18bffa298eff6a19d74e7b8b11dabdca95785befe8dbf", size = 67260, upload-time = "2026-05-05T16:30:39.804Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f8/235822b7ae0b2334f12ee18bcf2476d07924077a5efeea57dbe927704be2/librt-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:8d6d385d1969849a6b1397114df22714b6ded917bada98668e3e974dc663477e", size = 57156, upload-time = "2026-05-05T16:30:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e3/9b919cbf1e8eb770bf91bb7df28125e0f1daf4587169afefd95402636e9a/librt-0.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6c3a82d3bd32631ef5c79922dfc028520c9ad840255979ab4d908271818039ee", size = 79150, upload-time = "2026-05-05T16:30:42.761Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f5/72a944aa3bc3498169a168087eff58ca48b58bf1b704e59d091fd30739f3/librt-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d64cc66005dc324c9bb1fa3fc2841f529002f6eb15966d55e46d430f56955a6a", size = 82304, upload-time = "2026-05-05T16:30:44.082Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e3/fcc290a33e295019759472dfa794d204e43504b276ac65eab7fd9da20ea3/librt-0.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb562cd28c88cd2c6a9a6c78f99dc39348d6b16c94adc25de0e574acf1176e9", size = 272556, upload-time = "2026-05-05T16:30:45.497Z" }, + { url = "https://files.pythonhosted.org/packages/fd/54/546975e4c997573885e7f040a05012f8838e06fb12b0c3c1fbb76254e9d7/librt-0.10.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b809aa2854d019c28773b03605df22adc675ee4f3f4402d673581313e8906119", size = 256941, upload-time = "2026-05-05T16:30:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f1d03401571b331653acddbd4e8cd955c06d945241dd08b25192fac0d04b/librt-0.10.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc15acabdd519bd4176fdadc2119e5e3093485d86f89138daf47e5b4cedb983a", size = 285855, upload-time = "2026-05-05T16:30:48.86Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/62cf80ff046c339faf56718b3a940244d4beb70f1c6407289b5830ec11e9/librt-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1b2d835307d08ddadd94568e2369648ec9173bd3eea6d7f52a1abe717c81f98", size = 275321, upload-time = "2026-05-05T16:30:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ea/da5918d4070362e9a4d2ee9cd34f9dc84902daad8fd4275f8504a727ff4e/librt-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d261c6a2f93335a5167887fb0223e8b98ffce20ee3fde242e8e58a37ece6d0e5", size = 293993, upload-time = "2026-05-05T16:30:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8d/68b6086bed1fcdc314c640ea04e31e52d18052e08059fa595409d66a51a9/librt-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e2ffd44963f8e7f68995504d90f9881d64e94dc1d8e310039b9526108fc0c0f7", size = 284254, upload-time = "2026-05-05T16:30:55.086Z" }, + { url = "https://files.pythonhosted.org/packages/06/c8/b810f1d84ec34a5a7ed93d7b510ab04164d75fbdf23088d5c3fbe6b08357/librt-0.10.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f285f6455ed495791c4d8630e5af732960adea93cac4c893d15619f2eae53e8", size = 284925, upload-time = "2026-05-05T16:30:56.728Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/3c82d4158c5a2c62528b8fccce65a8c9ad700e480e86f9389387435089a5/librt-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6034ff52e663d34c7b82ef2aa2f94ad7c1d939e2368e63b06844bc4d127d2e1", size = 307830, upload-time = "2026-05-05T16:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/99/3a/9c635ac3e8a00383ff689161d3eac8a30b3b2ddc711b40471e6b8983ea29/librt-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:657860fd877fba6a241ea088ef99f63ca819945d3c715265da670bad56c37ebe", size = 60147, upload-time = "2026-05-05T16:31:00.293Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e8/6f65f3e565d4ac212cddddd552eacc8035ffdf941ca0ad6fe945a211d41f/librt-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56ded2d66010203a0cb5af063b609e3f079531a0e5e576d618dece859fd2e1af", size = 68649, upload-time = "2026-05-05T16:31:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/51/78/a0705a67cacd81e5fa01a5035b3adbdfbb43a7b8d4bd27e2b282ae61baf2/librt-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1ee63f30abf18ed4830fdbaf87b2b6f4bba1e198d46085c314edde4045e56715", size = 58247, upload-time = "2026-05-05T16:31:03.191Z" }, ] [[package]] name = "linkify-it-py" -version = "2.0.3" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uc-micro-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] [[package]] name = "logmuse" -version = "0.2.8" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/8d/8436f8bae1dd15fb29f7ffca7d8e649bc061979a16d323f1c568ec16a732/logmuse-0.2.8.tar.gz", hash = "sha256:a639d795f33d6876766033dea3c4ceb51617029e5f6e0aa390e7c4bc3012624d", size = 12543, upload-time = "2024-04-04T15:51:58.622Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/3d/d1ed45e1488b6e7aaee971f6d72f7a5f95d7b389dfe872dc08dd83ed2cf0/logmuse-0.3.0.tar.gz", hash = "sha256:298c3336cbca049de2f3d6d4bc1ea4ca5d9d6154c748f34cfc86e89d31e3fc9f", size = 13046, upload-time = "2026-03-06T03:05:04.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/8b/3089babd989b967e96f73a8da89b792e36d15dd2d2d0a062f414c4ef1c48/logmuse-0.2.8-py3-none-any.whl", hash = "sha256:f8db8f8d4ebc450cd866f67000c81c84e5644c0f1238057579fa94f3d0ca2693", size = 7956, upload-time = "2024-04-04T15:51:57.282Z" }, + { url = "https://files.pythonhosted.org/packages/af/56/dce8413a48934065e6dcb814a58aba3f592b6109d149c2bdb4214aab4bca/logmuse-0.3.0-py3-none-any.whl", hash = "sha256:810ba709895bc2e08867d557d992e44a16895dfb4e477e61418d445e12be8a01", size = 8180, upload-time = "2026-03-06T03:05:02.662Z" }, ] [[package]] @@ -1195,25 +1176,19 @@ linkify = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.11.*'", ] dependencies = [ { name = "mdurl", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5c/f3aedc83549aae71cd52b9e9687fe896e3dc6e966ba20eba04718605d198/markdown_it_py-4.1.0.tar.gz", hash = "sha256:760e3f87b2787c044c5138a5ba107b7c2be26c03b13cc7f8fe42756b65b1df6c", size = 81613, upload-time = "2026-05-06T16:32:13.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl", hash = "sha256:d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d", size = 90955, upload-time = "2026-05-06T16:32:12.184Z" }, ] [package.optional-dependencies] @@ -1312,7 +1287,7 @@ version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ @@ -1529,48 +1504,61 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cf/dc/7e6d49f04fca40b9dd5c752a51a432ffe67fb45200702bc9eee0cb4bbb26/mypy-2.0.0.tar.gz", hash = "sha256:1a9e3900ac5c40f1fe813506c7739da6e6f0eab2729067ebd94bfb0bbba53532", size = 3869036, upload-time = "2026-05-06T19:26:43.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/1e/9983d2d5b5d2dc3677177bcf0fa6b25185ecf750cc0559e02199625a31c5/mypy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:65d6f22d643bccaeb182d41d2a9f0990a05a871673c4ae3f97d4931eca0d2294", size = 14663140, upload-time = "2026-05-06T19:25:59.474Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/b4009c91d3ced13c8f406acf47bbe56365025cd21bf6585cd1e87375a708/mypy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:106650bce72114f43019bf72197296f51c2cd47adfa9d073ea2976c247a404c5", size = 13526733, upload-time = "2026-05-06T19:22:56.425Z" }, + { url = "https://files.pythonhosted.org/packages/f0/99/2403cb0ceeb1552f70e70e779e3d0713b24f84c7ca0e9e14b2b7bc684cf0/mypy-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c734b7eb89a4cc4ec347f8187ffa730e2b59693407bc93dcb878183037f80a17", size = 13951940, upload-time = "2026-05-06T19:24:43.45Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f7/4848a14c2667b6eb62841c9aeb7e1f6479613b1ef9a65564fe1f5518a35b/mypy-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd9e60388944d0f1432a2419ab938a78d5658df1d143a7172cfe1a197276cf49", size = 14833983, upload-time = "2026-05-06T19:23:16.827Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/c51831f9f1c6e46cbce765bd0a18981b84696e40bd1eea14e0a08494af44/mypy-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95e3890666c3be41af7a7179f4872341c08e90c161ba8e7a08a21f9be92c131", size = 15135591, upload-time = "2026-05-06T19:24:32.96Z" }, + { url = "https://files.pythonhosted.org/packages/40/7f/3c25e503a94f9ec18352464551bc6c506dee2bca93c6d0e0b5568eefc269/mypy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8e8709ce1b1046b8aad77a506dd01491157102dd727128c0b374b5025c7d769", size = 10983019, upload-time = "2026-05-06T19:20:30.942Z" }, + { url = "https://files.pythonhosted.org/packages/75/da/5cf833fd3b53fd4b5797e55dc16fb7efab16fddbc7205d49ff65b15d554e/mypy-2.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:0165968759c99ab79dc1a9f8aaec18e93a1bedcf7c13edd70e68dd3d5faf17cb", size = 9914165, upload-time = "2026-05-06T19:21:49.165Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1e/268b81393b81d64683f670680215553e70ae92c55805915b3440080e05e4/mypy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17b7222e9fdfd352e61fb3131da117e55cc465f701ff232f1bd97a02bbad91f", size = 14580849, upload-time = "2026-05-06T19:23:06.567Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/d159a8002d9e5c44e59ece9d641a26956c89be5b6827f819d9a9dc678c65/mypy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0a61adea1a5ffc2d47a4dc4bb180d8103f477fc2a90a1cdcbb168c2cc6caff", size = 13444955, upload-time = "2026-05-06T19:25:11.982Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5d/3b28d5a2799591da0ee5490418e94497eaf5d701e42d8b001b5e17a9b3d6/mypy-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8578f857b519993d065e5805290b71467ebfae772407a5f57e823755e4fdb850", size = 13873124, upload-time = "2026-05-06T19:20:39.684Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/f40f723955617b814d5ddc1154d8938b77aaf6926c2dbf72846e8943a0b7/mypy-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33f668a37a650df60f7b825c1ac61e6baadd4ac3c89519e929badde58d28edf5", size = 14748822, upload-time = "2026-05-06T19:25:30.972Z" }, + { url = "https://files.pythonhosted.org/packages/d6/16/eded971224a483e422a141ffd580c00e1b919df8e529f06d03a4a987878c/mypy-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29ea6da86c8c5e9addd48fa6e624f467341b3814f54ded871b28980468686dea", size = 14992675, upload-time = "2026-05-06T19:23:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6a/1cbd7290f00b4dbaa4c4502e53ac05645ea635e4d1e3dcd42687c2fc39cd/mypy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:904baa0124ebbccf0c7ba94f722cf9186ee30478f5e5b11432ffc8929248ee55", size = 10983628, upload-time = "2026-05-06T19:26:39.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/3f/8caa9bcc2636cd512642050747466b695fa2540d7040544fd7ddb721d671/mypy-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:440165501295e523bf1e5d3e411b62b367b901c65610938e75f0e56ba0462461", size = 9906041, upload-time = "2026-05-06T19:24:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4b/f6cd12ef1eb63be1c342da3e8ca811d2280276177f6de4ef20cb2366d79b/mypy-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:660790551c988e69d8bf7a35c8b4149edeb22f4a339165702be843532e9dcdb5", size = 14756610, upload-time = "2026-05-06T19:26:19.221Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/67d09ca28bee21feaca264b2a680cf2d300bcc2071136ad064928324c843/mypy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a15bf92cd8781f8e72f69ffa7e30d1f434402d065ee1ecd5223ef2ef100f914", size = 13554270, upload-time = "2026-05-06T19:26:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/44718b5c6b1b5a27440ff2effe6a1be0fa2a190c0f4e2e21a83728416f95/mypy-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ff370b43d7def05bbcd2f5267f0bcda72dd6a552ef2ea9375b02d6fe06da270", size = 13924663, upload-time = "2026-05-06T19:21:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2b/bbb9cc5773f946846a7c340097e59bcf84095437dda0d56bb4f6cf1f6541/mypy-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37bd246590a018e5a11703b7b09c39d47ede3df5ba3fa863c5b8590b465beb01", size = 14946862, upload-time = "2026-05-06T19:24:23.023Z" }, + { url = "https://files.pythonhosted.org/packages/43/25/e9318566f443a5130b4ff0ad3367ee6c4c4c49ff083fe5214a7318c18282/mypy-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cce87e92214fac8bf8feb8a680d0c1b6fb748d50e9b57fbb13e4b1d83a3ed19b", size = 15175090, upload-time = "2026-05-06T19:26:28.794Z" }, + { url = "https://files.pythonhosted.org/packages/67/65/2ec28c834f21e164c33bc296a7db538ad50c74f83e517c7a0be95ff6de86/mypy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e19e9cb69b66a4141009d24898259914fa2b71d026de0b46edf9fafdbf4fd46e", size = 11052899, upload-time = "2026-05-06T19:25:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/9e/72/d1ec625cfc9bd101c07a6834ef1f94e820296f8fdbad2eb03f50e0983f8c/mypy-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:b021614cb08d44785b025982163ec3c39c94bff766ead071fa9e82b4ef6f62cd", size = 9972935, upload-time = "2026-05-06T19:23:24.204Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c6/996a1e535e5d0d597c3b1460fc962733091f885f312e749350eb2ac10965/mypy-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ef5f581b61240d1cc629b12f8df6565ed6ffac0d82ed745eef7833222ab50b9", size = 14737259, upload-time = "2026-05-06T19:20:23.081Z" }, + { url = "https://files.pythonhosted.org/packages/94/c5/0f9460e26b77f434bd53f47d1ce32a3cd4580c92a5331fa5dfc059f9421a/mypy-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20e3470a165dbc249bdfbe8d1c5172727ef22688cffc279f8c3aa264ab9d4d9a", size = 13538377, upload-time = "2026-05-06T19:21:08.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/3e/8ea2f8dd1e5c9c279fb3c28193bdb850adf4d3d8172880abad829eced609/mypy-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:224ba142eee8b4d65d4db657cb1fc22abec30b135ded6ab297302ba1f62e505d", size = 13914264, upload-time = "2026-05-06T19:24:12.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/78bd3b8520f676acee9dab48ea71473e68f6d5cf14b59fbd800bea50a92b/mypy-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e879ad8a03908ff74d15e8a9b42bf049918e6798d52c011011f1873d0b5877e", size = 14926761, upload-time = "2026-05-06T19:20:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/61/ef/b52fa340522da3d22e669117c3b83155c2660f7cdc035856958fbfffb224/mypy-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5c15bcbd18d6fe927cc55c459597a3517d69cc3123f067be3b020010e115e", size = 15157014, upload-time = "2026-05-06T19:25:49.78Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/dde7614250c6d017936c7aa3bb63b9b52c7cfd298d3f1be9be45f307870b/mypy-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1a068acd7c9fb77e9f8923f1556f2f49d6d7895821121b8d97fa5642b9c52f5", size = 11067049, upload-time = "2026-05-06T19:21:16.116Z" }, + { url = "https://files.pythonhosted.org/packages/27/ec/1d6af4830a94a285442db19caa02f160cc1a255e4f324eec5458e6c2bafb/mypy-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:ef9d96da1ddffbc21f27d3939319b6846d12393baa17c4d2f3e81e040e73ce2c", size = 9967903, upload-time = "2026-05-06T19:22:15.52Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/6fefe954207860aed6eeb91776795e64a257d3ce0360862288984ce121f5/mypy-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c918c64e8ce36557851b0347f84eb12f1965d3a06813c36df253eb0c0afd1d82", size = 14729633, upload-time = "2026-05-06T19:24:53.383Z" }, + { url = "https://files.pythonhosted.org/packages/23/d6/d336f5b820af189eb0390cce21de62d264c0a4e64713dfbe81bfc4fc7739/mypy-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:301f1a8ccc7d79b542ee218b28bb49443a83e194eb3d10da63ff1649e5aa5d34", size = 13559524, upload-time = "2026-05-06T19:22:24.906Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/d7bb54fde1770f0484e5fbdbdce37a41e95ed0a1cd493ec60ead111e356c/mypy-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdf4ef489d44ce350bac3fd699907834e551d4c934e9cc862ef201215ab1558d", size = 13936018, upload-time = "2026-05-06T19:25:02.992Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ba/5be51316b91e6a6bf6e3a8adb3de500e7e1fb5bf9491743b8cbc81a34a2c/mypy-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cde2d0989f912fc850890f727d0d76495e7a6c5bdd9912a1efdb64952b4398d", size = 14910712, upload-time = "2026-05-06T19:25:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/e2c8c3b373e20ebfb66e6c83a99027fd67df4ec43b08879f74e822d2dc4c/mypy-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdf05693c231a14fe37dbfce192a3a1372c26a833af4a80f550547742952e719", size = 15141499, upload-time = "2026-05-06T19:20:50.924Z" }, + { url = "https://files.pythonhosted.org/packages/12/36/07756f933e00416d912e35878cfcf89a593a3350a885691c0bb85ae0226a/mypy-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:73aee2da33a2237e66cbe84a94780e53599847e86bb3aa7b93e405e8cd9905f2", size = 11240511, upload-time = "2026-05-06T19:21:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/70/05/79ac1f20f2397353f3845f7b8bb5d8006cda7c8ef9092f04f9de3c6135f2/mypy-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:1f6dcd8f39971f41edab2728c877c4ac8b50ad3c387ff2770423b79a05d23910", size = 10149336, upload-time = "2026-05-06T19:22:08.383Z" }, + { url = "https://files.pythonhosted.org/packages/53/e0/0db84e0ebbad6e99e566c68e4b465784f2a2294f7719e8db9d509ef23087/mypy-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a04e980b9275c76159da66c6e1723c7798306f9802b31bdaf9358d0c84030ce8", size = 15797362, upload-time = "2026-05-06T19:22:00.835Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a4/14cc0768164dd53bec48aa41a20270b18df9bf72aa5054278bf133608315/mypy-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:33f9cf4825469b2bc73c53ba55f6d9a9b4cdb60f9e6e228745581520f29b8771", size = 14635914, upload-time = "2026-05-06T19:23:43.675Z" }, + { url = "https://files.pythonhosted.org/packages/08/48/d866a3e23b4dc5974c77d9cf65a435bf22de01a84dd4620917950e233960/mypy-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191675c3c7dc2a5c7722a035a6909c277f14046c5e4e02aa5fbf65f8524f08ad", size = 15270866, upload-time = "2026-05-06T19:22:34.756Z" }, + { url = "https://files.pythonhosted.org/packages/71/eb/de9ef94958eb2078a6b908ceb247757dc384d3a238d3bd6ed7d81de5eaf8/mypy-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d26c4321a3b06fc9f04c741e0733af693f82d823f8e64e47b2e63b7f19fa84", size = 16093131, upload-time = "2026-05-06T19:23:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/ad/07/0ab2c1a9d26e90942612724cbd5788f16b7810c5dd39bfcf79286c6c4524/mypy-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bbcbc4d5917ca6ce12de70e051de7f533e3bf92d548b41a38a2232a6fe356525", size = 16330685, upload-time = "2026-05-06T19:21:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/46f85d1371a5be642dad263828118ae1efd536d91d8bd2000c68acff3920/mypy-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:dbc6ba6d40572ae49268531565793a8f07eac7fc65ad76d482c9b4c8765b6043", size = 12752017, upload-time = "2026-05-06T19:22:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e6/94ca48800cac19eb28a58188a768aaec0d16cac0f373915f073058ab0855/mypy-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:77926029dfcb7e1a3ecb0acb2ddbb24ca36be03f7d623e1759ad5376be8f6c01", size = 10527097, upload-time = "2026-05-06T19:20:58.973Z" }, + { url = "https://files.pythonhosted.org/packages/5c/14/fd0694aa594d6e9f9fd16ce821be2eff295197a273262ef56ddcc1388d68/mypy-2.0.0-py3-none-any.whl", hash = "sha256:8a92b2be3146b4fa1f062af7eb05574cbf3e6eb8e1f14704af1075423144e4e5", size = 2673434, upload-time = "2026-05-06T19:26:32.856Z" }, ] [[package]] @@ -1607,20 +1595,14 @@ name = "myst-parser" version = "5.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.11.*'", ] dependencies = [ { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, { name = "pyyaml", marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, @@ -1633,7 +1615,7 @@ wheels = [ [[package]] name = "nf-core" -version = "3.6.0.dev0" +version = "4.0.3" source = { editable = "." } dependencies = [ { name = "click" }, @@ -1645,7 +1627,7 @@ dependencies = [ { name = "packaging" }, { name = "pdiff" }, { name = "pillow" }, - { name = "pre-commit" }, + { name = "prek" }, { name = "prompt-toolkit" }, { name = "pydantic" }, { name = "pygithub" }, @@ -1670,7 +1652,6 @@ dev = [ { name = "mypy" }, { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "prek" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -1701,13 +1682,12 @@ requires-dist = [ { name = "jinja2" }, { name = "jsonschema", specifier = ">=4.0" }, { name = "markdown", specifier = ">=3.3" }, - { name = "mypy", marker = "extra == 'dev'" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "myst-parser", marker = "extra == 'dev'" }, { name = "packaging" }, { name = "pdiff" }, { name = "pillow" }, - { name = "pre-commit" }, - { name = "prek", marker = "extra == 'dev'" }, + { name = "prek" }, { name = "prompt-toolkit", specifier = "<=3.0.52" }, { name = "pydantic", specifier = ">=2.2.1" }, { name = "pygithub" }, @@ -1734,7 +1714,7 @@ requires-dist = [ { name = "sphinx", marker = "extra == 'dev'" }, { name = "sphinx-rtd-theme", marker = "extra == 'dev'" }, { name = "tabulate" }, - { name = "textual", specifier = "==7.5.0" }, + { name = "textual", specifier = "==8.2.4" }, { name = "textual-dev", marker = "extra == 'dev'", specifier = "==1.8.0" }, { name = "trogon" }, { name = "types-jsonschema", marker = "extra == 'dev'" }, @@ -1746,15 +1726,6 @@ requires-dist = [ ] provides-extras = ["dev"] -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - [[package]] name = "numpy" version = "2.2.6" @@ -1822,127 +1793,107 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.2" +version = "2.4.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, - { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, - { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, - { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, - { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, - { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, - { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, -] - -[[package]] -name = "oyaml" -version = "1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/71/c721b9a524f6fe6f73469c90ec44784f0b2b1b23c438da7cc7daac1ede76/oyaml-1.0.tar.gz", hash = "sha256:ed8fc096811f4763e1907dce29c35895d6d5936c4d0400fe843a91133d4744ed", size = 2914, upload-time = "2020-08-18T04:37:43.397Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/aa/111610d8bf5b1bb7a295a048fc648cec346347a8b0be5881defd2d1b4a52/oyaml-1.0-py2.py3-none-any.whl", hash = "sha256:3a378747b7fb2425533d1ce41962d6921cda075d46bb480a158d45242d156323", size = 2992, upload-time = "2020-08-18T04:37:38.263Z" }, + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, ] [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1995,84 +1946,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] -[[package]] -name = "pandas" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, - { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, - { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, - { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, - { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, - { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, - { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, - { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, - { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, - { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, - { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, - { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, - { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, - { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, - { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, - { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, - { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, - { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, - { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, - { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, - { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, -] - [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -2089,21 +1969,20 @@ wheels = [ [[package]] name = "pephubclient" -version = "0.5.0" +version = "0.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coloredlogs" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, { name = "peppy" }, { name = "pydantic" }, { name = "requests" }, { name = "typer" }, { name = "ubiquerg" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/09/f650658cab89df6082726b552f23bef23ee28c84471b637c83ecf4ecfaf0/pephubclient-0.5.0.tar.gz", hash = "sha256:1cd58b08a9b416d1cecebe2da879d3e305e92c09b6e8fb3e228dfabb9c2975e1", size = 25422, upload-time = "2026-01-27T02:07:38.367Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/dc/91e9444c87c7124a612d7d942fcfc7c57daef2d3b2017275e9ec231c474a/pephubclient-0.5.1.tar.gz", hash = "sha256:41a8b7af90477c027f0f9205383178381a2bd042d2da18d9e047be81d9597b7a", size = 25512, upload-time = "2026-03-18T18:47:37.569Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/02/6aadc5bed8aab107f7f1f12956da8bb49703ee88697aa1f6e453b378ef48/pephubclient-0.5.0-py3-none-any.whl", hash = "sha256:a73ca50fa779f810e63204d66037c3e92df26c222865a67e3822300a7054bde2", size = 27018, upload-time = "2026-01-27T02:07:37.176Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3d/b47e0d16d6db6c51a3953b1c31f7c8356ebd783b8bb137dc4a20803818bb/pephubclient-0.5.1-py3-none-any.whl", hash = "sha256:9b5a4ab4ae4fb8e4f3fa80681a274f758f7c0678e1caa41aed4067a495ff0653", size = 27103, upload-time = "2026-03-18T18:47:36.024Z" }, ] [[package]] @@ -2112,9 +1991,8 @@ version = "0.40.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, { name = "pephubclient" }, { name = "pyyaml" }, { name = "rich" }, @@ -2127,147 +2005,143 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, - { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, - { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, - { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, - { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, - { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, - { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, - { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, - { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, - { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, - { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, - { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, - { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, - { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] name = "piper" -version = "0.14.5" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "logmuse" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pipestat" }, { name = "psutil" }, { name = "ubiquerg" }, { name = "yacman" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/34/5d893efb399aa908a7b00f3d8c075e8e738bd2f728904d057c7f9c587f10/piper-0.14.5.tar.gz", hash = "sha256:f866533c0bb8b997094bb4c647bb32c7de57053d77cf255c4a1b68586d8d9fde", size = 74636, upload-time = "2025-09-22T18:24:29.807Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/85/8f9c96d3fedc52783cfae3345fe76d7380341732c2691be747b3ad51ff06/piper-0.15.1.tar.gz", hash = "sha256:0fd2d4d4483ddac9c6fefd005b95ea528a7159c9e7f72a085bb4d36daf6fff9d", size = 117363, upload-time = "2026-03-06T03:13:12.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e3/d818110971ba314fdf04022e8b6b01a2fcefb81f0ad9b223dca980c6dc7b/piper-0.14.5-py3-none-any.whl", hash = "sha256:cfa38caa62b7e6aa751a1c806f098f9b4e9942cef94a4fc837ae48cd9c26cb3b", size = 72839, upload-time = "2025-09-22T18:24:28.127Z" }, + { url = "https://files.pythonhosted.org/packages/13/97/42968a400c60a9f502ff2a964462fed5fb51612219004c6d8033ed88f454/piper-0.15.1-py3-none-any.whl", hash = "sha256:1aefcc17abfd98017513a3492f3a40d2f1c22a88c99d89bb009b4926e94ff327", size = 72647, upload-time = "2026-03-06T03:13:10.717Z" }, ] [[package]] name = "pipestat" -version = "0.12.2" +version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eido" }, { name = "jinja2" }, { name = "jsonschema" }, { name = "logmuse" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml" }, { name = "ubiquerg" }, { name = "yacman" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/b9/bda18c48a3242a49905849185d43b472d7b569fde16bf7d88b9948043f19/pipestat-0.12.2.tar.gz", hash = "sha256:1b8fccb9dd12073460f058c219d0f8afd8b0251176cc5216610baccc3f479fbb", size = 89297, upload-time = "2025-09-25T22:49:31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/0b/f3fadd34b2b341a695052de36f4fc15255d9580579f46f198d8fa06e9855/pipestat-0.13.1.tar.gz", hash = "sha256:87cf69404511e434e8e9a20abdfd9c8123ad6d2f7818a182a29db772a8d78d47", size = 139939, upload-time = "2026-03-06T03:06:37.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/8f/548eb35894231eeba1d7e5ce4f0689de49e7ed81d3337bed738c649048d2/pipestat-0.12.2-py3-none-any.whl", hash = "sha256:6084a990d6729b58f268a0547c1ea33c291ccd9d3009b79cd28a09a0be58759c", size = 87984, upload-time = "2025-09-25T22:49:29.489Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cb/500e0283bbe51c4fa63433238496c6f77bc3eec75ffc998a3aae7ac67503/pipestat-0.13.1-py3-none-any.whl", hash = "sha256:b51b97d1a68b1f0f553a785fc66574ddbe162e24b57e13c36190f7fe4b5c6153", size = 104657, upload-time = "2026-03-06T03:06:34.951Z" }, ] [[package]] name = "platformdirs" -version = "4.9.1" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/d5/763666321efaded11112de8b7a7f2273dd8d1e205168e73c334e54b0ab9a/platformdirs-4.9.1.tar.gz", hash = "sha256:f310f16e89c4e29117805d8328f7c10876eeff36c94eac879532812110f7d39f", size = 28392, upload-time = "2026-02-14T21:02:44.973Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/77/e8c95e95f1d4cdd88c90a96e31980df7e709e51059fac150046ad67fac63/platformdirs-4.9.1-py3-none-any.whl", hash = "sha256:61d8b967d34791c162d30d60737369cbbd77debad5b981c4bfda1842e71e0d66", size = 21307, upload-time = "2026-02-14T21:02:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] @@ -2279,44 +2153,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "pre-commit" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, -] - [[package]] name = "prek" -version = "0.3.3" +version = "0.3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/f1/7613dc8347a33e40fc5b79eec6bc7d458d8bbc339782333d8433b665f86f/prek-0.3.3.tar.gz", hash = "sha256:117bd46ebeb39def24298ce021ccc73edcf697b81856fcff36d762dd56093f6f", size = 343697, upload-time = "2026-02-15T13:33:28.723Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/59/0a279983f96bd5d538b4975f0a23121082aa3b8560b6649fdf61f8011b07/prek-0.3.13.tar.gz", hash = "sha256:c48586ee3708bfbf3df80121f55583e9a7d0fa166b08172c091fe5971e92a0ac", size = 444848, upload-time = "2026-05-05T18:07:09.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/8b/dce13d2a3065fd1e8ffce593a0e51c4a79c3cde9c9a15dc0acc8d9d1573d/prek-0.3.3-py3-none-linux_armv6l.whl", hash = "sha256:e8629cac4bdb131be8dc6e5a337f0f76073ad34a8305f3fe2bc1ab6201ede0a4", size = 4644636, upload-time = "2026-02-15T13:33:43.609Z" }, - { url = "https://files.pythonhosted.org/packages/01/30/06ab4dbe7ce02a8ce833e92deb1d9a8e85ae9d40e33d1959a2070b7494c6/prek-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4b9e819b9e4118e1e785047b1c8bd9aec7e4d836ed034cb58b7db5bcaaf49437", size = 4651410, upload-time = "2026-02-15T13:33:34.277Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fc/da3bc5cb38471e7192eda06b7a26b7c24ef83e82da2c1dbc145f2bf33640/prek-0.3.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bf29db3b5657c083eb8444c25aadeeec5167dc492e9019e188f87932f01ea50a", size = 4273163, upload-time = "2026-02-15T13:33:42.106Z" }, - { url = "https://files.pythonhosted.org/packages/b4/74/47839395091e2937beced81a5dd2f8ea9c8239c853da8611aaf78ee21a8b/prek-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ae09736149815b26e64a9d350ca05692bab32c2afdf2939114d3211aaad68a3e", size = 4631808, upload-time = "2026-02-15T13:33:20.076Z" }, - { url = "https://files.pythonhosted.org/packages/e2/89/3f5ef6f7c928c017cb63b029349d6bc03598ab7f6979d4a770ce02575f82/prek-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:856c2b55c51703c366bb4ce81c6a91102b70573a9fc8637db2ac61c66e4565f9", size = 4548959, upload-time = "2026-02-15T13:33:36.325Z" }, - { url = "https://files.pythonhosted.org/packages/b2/18/80002c4c4475f90ca025f27739a016927a0e5d905c60612fc95da1c56ab7/prek-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3acdf13a018f685beaff0a71d4b0d2ccbab4eaa1aced6d08fd471c1a654183eb", size = 4862256, upload-time = "2026-02-15T13:33:37.754Z" }, - { url = "https://files.pythonhosted.org/packages/c5/25/648bf084c2468fa7cfcdbbe9e59956bbb31b81f36e113bc9107d80af26a7/prek-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f035667a8bd0a77b2bfa2b2e125da8cb1793949e9eeef0d8daab7f8ac8b57fe", size = 5404486, upload-time = "2026-02-15T13:33:39.239Z" }, - { url = "https://files.pythonhosted.org/packages/8b/43/261fb60a11712a327da345912bd8b338dc5a050199de800faafa278a6133/prek-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d09b2ad14332eede441d977de08eb57fb3f61226ed5fd2ceb7aadf5afcdb6794", size = 4887513, upload-time = "2026-02-15T13:33:40.702Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2c/581e757ee57ec6046b32e0ee25660fc734bc2622c319f57119c49c0cab58/prek-0.3.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c0c3ffac16e37a9daba43a7e8316778f5809b70254be138761a8b5b9ef0df28e", size = 4632336, upload-time = "2026-02-15T13:33:25.867Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d8/aa276ce5d11b77882da4102ca0cb7161095831105043ae7979bbfdcc3dc4/prek-0.3.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a3dc7720b580c07c0386e17af2486a5b4bc2f6cc57034a288a614dcbc4abe555", size = 4679370, upload-time = "2026-02-15T13:33:22.247Z" }, - { url = "https://files.pythonhosted.org/packages/70/19/9d4fa7bde428e58d9f48a74290c08736d42aeb5690dcdccc7a713e34a449/prek-0.3.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60e0fa15da5020a03df2ee40268145ec5b88267ec2141a205317ad4df8c992d6", size = 4540316, upload-time = "2026-02-15T13:33:24.088Z" }, - { url = "https://files.pythonhosted.org/packages/25/b5/973cce29257e0b47b16cc9b4c162772ea01dbb7c080791ea0c068e106e05/prek-0.3.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:553515da9586d9624dc42db32b744fdb91cf62b053753037a0cadb3c2d8d82a2", size = 4724566, upload-time = "2026-02-15T13:33:29.832Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/ad8b2658895a8ed2b0bc630bf38686fe38b7ff2c619c58953a80e4de3048/prek-0.3.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9512cf370e0d1496503463a4a65621480efb41b487841a9e9ff1661edf14b238", size = 4995072, upload-time = "2026-02-15T13:33:27.417Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b7/0540c101c00882adb9d30319d22d8f879413598269ecc60235e41875efd4/prek-0.3.3-py3-none-win32.whl", hash = "sha256:b2b328c7c6dc14ccdc79785348589aa39850f47baff33d8f199f2dee80ff774c", size = 4293144, upload-time = "2026-02-15T13:33:46.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/c7/e4f11da653093040efba2d835aa0995d78940aea30887287aeaebe34a545/prek-0.3.3-py3-none-win_amd64.whl", hash = "sha256:3d7d7acf7ca8db65ba0943c52326c898f84bab0b1c26a35c87e0d177f574ca5f", size = 4652761, upload-time = "2026-02-15T13:33:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/11/e4/d99dec54c6a5fb2763488bff6078166383169a93f3af27d2edae88379a39/prek-0.3.3-py3-none-win_arm64.whl", hash = "sha256:8aa87ee7628cd74482c0dd6537a3def1f162b25cd642d78b1b35dd3e81817f60", size = 4367520, upload-time = "2026-02-15T13:33:31.664Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6a/9baa2bda21dccc2927e952416f6cc23a75eb99c9ed18837164ac2e4a5640/prek-0.3.13-py3-none-linux_armv6l.whl", hash = "sha256:b00d38f01235073c35aa5f48df57fefef45a6cec2ae0884d750345a2c7220370", size = 5506622, upload-time = "2026-05-05T18:06:53.091Z" }, + { url = "https://files.pythonhosted.org/packages/56/77/d44b5d9bdca0879b865f8e47bf84cf5dc9e8b358d029e6d9b83d8809c116/prek-0.3.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0d89ac712c60e34d1550a606ad5fdfb8ad71d44ced8afa2fa5cbc106be4abd9e", size = 5878743, upload-time = "2026-05-05T18:07:23.164Z" }, + { url = "https://files.pythonhosted.org/packages/08/cf/19e8525cde8b3aa12858aca434d1fa653ef3b152da5af11eafc857634dc2/prek-0.3.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f9b5265863d18b5be4ea094fdce4fd6ca61a8c89a70ee3d8ee153b3e0ed6b272", size = 5434909, upload-time = "2026-05-05T18:07:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9a/e5f97194782de4dab622ce09dafb3ebdd2ee4d354a83ac4def7ebeee236c/prek-0.3.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:64b59a1550780af2bba37297c704b17f81d8e9df6288af1fab4017938e33b1db", size = 5697536, upload-time = "2026-05-05T18:07:05.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8c/e1f548ffc4b227e4c2b5a9b30f5978a7e0e6dad51305b97a2ba5b2a923e7/prek-0.3.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ce6cd8f114ba9bbdbe97422103fd886101949b1c42e588a7543c4436ead2020", size = 5428160, upload-time = "2026-05-05T18:07:01.489Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/abd919b00905a32d21dca2cec32c707860cf217da2431b62dd52684b310e/prek-0.3.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc03e924a24d8d961f56195853c8b206cb196be6db4ad8312125dae847d718ac", size = 5827275, upload-time = "2026-05-05T18:07:17.437Z" }, + { url = "https://files.pythonhosted.org/packages/af/ed/cafd2b80d58a83faf8371c6543bd1475a2224242a3294da7f8582f6aa551/prek-0.3.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ca8c526a23873177fb3b92013500b08ef5f8bedc7263f9f3a44dd2f49645a26", size = 6710293, upload-time = "2026-05-05T18:07:10.663Z" }, + { url = "https://files.pythonhosted.org/packages/35/09/52a4a27596b764173a34d74db09356b30faaacb4a1075b75adbc036a0008/prek-0.3.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5bbb175478438a871e3281d2c3c3f067288af73ad81707a9bdebfd769766c7d", size = 6096556, upload-time = "2026-05-05T18:07:19.46Z" }, + { url = "https://files.pythonhosted.org/packages/63/60/80f61729ce6498815d46d5580cf76da2c157c9b6494046183682441a0ea3/prek-0.3.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a65327a014d838341af757dfc05a706d10e8e33f039bc32bb3dbe2fa21c440c0", size = 5693267, upload-time = "2026-05-05T18:07:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/c7a663fe70676ffab2e0c6c9a71997a3ccd002ed5bc60b7422a937911af0/prek-0.3.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b6a200843a36a5b0c41764ce7639ccb3471d48b097f1c5e3fc8f034219b42626", size = 5532865, upload-time = "2026-05-05T18:07:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/32/68/506ef5a235536030e16f61e7210474554f6e05f845f27df5877d2dbb1a06/prek-0.3.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bdacaad8f35f343e063d251211fe34db1de9e5cc591795361ad69a6485202258", size = 5395951, upload-time = "2026-05-05T18:06:55.183Z" }, + { url = "https://files.pythonhosted.org/packages/3e/00/22d7c6db7f43b58f7d015913c12660c9bbc82751cff6cfd8c31993cf30eb/prek-0.3.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f00328f1c520d8fefb910ab0d3c6764ee330d227952baa19b7e3de7242bd8b3b", size = 5681195, upload-time = "2026-05-05T18:07:12.804Z" }, + { url = "https://files.pythonhosted.org/packages/10/e3/fdf9882238796914ddaf11381a9083b374980156200a953324f6c795f34d/prek-0.3.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e5530a867bcf5b172b7513a64e71b06a337d1d184696227ae953845867376b8d", size = 6212085, upload-time = "2026-05-05T18:07:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/528759931344b5c7103085798f5fa2e86d27d9410b753a6bcbe7726aa8ba/prek-0.3.13-py3-none-win32.whl", hash = "sha256:326fac2bdce00074ce6c5046b861d310638aee2b9de1ed241ba7eb32bdc83898", size = 5199566, upload-time = "2026-05-05T18:07:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d0/8715ee837c73314a02767d20652cc312d1b6ff6733fa00f52de2b648bc3a/prek-0.3.13-py3-none-win_amd64.whl", hash = "sha256:841049f89f5ec9f4035299283d11e566ac5a068e3742ead1055ea04f886831fc", size = 5589599, upload-time = "2026-05-05T18:06:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/ff/cf/0af0b15be0ebd82f7e50adee149b05a73533d78cb1b97cb889f0647ebffe/prek-0.3.13-py3-none-win_arm64.whl", hash = "sha256:a9fd74e0aec550c6b8d41076fdcdd6ff121cd7d94d743c1338bd794784e3c775", size = 5419029, upload-time = "2026-05-05T18:06:59.645Z" }, ] [[package]] @@ -2484,7 +2342,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2492,144 +2350,142 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] name = "pyfaidx" -version = "0.9.0.3" +version = "0.9.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/44/070fd3b8e7042d03c3e800f25e44ed3d95fb31a3297d784588c03f238c1a/pyfaidx-0.9.0.3.tar.gz", hash = "sha256:648b4b28d98dcb06f9a62d88a956ebb998297f4bd2f04e05609d935a2c314a79", size = 71549, upload-time = "2025-09-03T15:05:52.978Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/af/da148aaa8e75c9890a41dbe5516f8006def2b74831e66acf79f81bb7bb5b/pyfaidx-0.9.0.4.tar.gz", hash = "sha256:801bdd208b12bff6f4fb2da10a16834158dbb1c6f146e4015c9ff45e509acd65", size = 71759, upload-time = "2026-03-19T19:02:58.561Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/0b/fadfd7d0088efa3ba0cdcdbb51d093e18d3700f6f88634aa8387106394a4/pyfaidx-0.9.0.3-py3-none-any.whl", hash = "sha256:e494edffd644dbfa141ec63196c274cbb1f029acfc5977108c4fe51fdf5dc3ae", size = 29370, upload-time = "2025-09-03T15:05:51.469Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/44d47df76193b31fa4a7c662531076e6ec9a0f2f3017171f47c466211537/pyfaidx-0.9.0.4-py3-none-any.whl", hash = "sha256:18b223b7ba8c66b988a8ce00011500961cb2aeb7fa97d05a293cf472acdd0009", size = 29469, upload-time = "2026-03-19T19:02:57.213Z" }, ] [[package]] name = "pygithub" -version = "2.8.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyjwt", extra = ["crypto"] }, @@ -2638,27 +2494,30 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/74/e560bdeffea72ecb26cff27f0fad548bbff5ecc51d6a155311ea7f9e4c4c/pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9", size = 2246994, upload-time = "2025-09-02T17:41:54.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/c3/8465a311197e16cf5ab68789fe689535e90f6b61ab524cc32a39e67237ae/pygithub-2.9.1.tar.gz", hash = "sha256:59771d7ff63d54d427be2e7d0dad2208dfffc2b0a045fec959263787739b611c", size = 2594989, upload-time = "2026-04-14T07:26:13.622Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/ba/7049ce39f653f6140aac4beb53a5aaf08b4407b6a3019aae394c1c5244ff/pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0", size = 432709, upload-time = "2025-09-02T17:41:52.947Z" }, + { url = "https://files.pythonhosted.org/packages/77/aa/81a5506f089a26338bff17535e4339b3b22049ebd1bcdeff756c4d7a7559/pygithub-2.9.1-py3-none-any.whl", hash = "sha256:2ec78fca30092d51a42d76f4ddb02131b6f0c666a35dfdf364cf302cdda115b9", size = 449710, upload-time = "2026-04-14T07:26:12.382Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyjwt" -version = "2.11.0" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] [package.optional-dependencies] @@ -2744,16 +2603,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -2827,11 +2686,11 @@ wheels = [ [[package]] name = "pytz" -version = "2025.2" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] @@ -2926,37 +2785,38 @@ wheels = [ [[package]] name = "refgenconf" -version = "0.12.2" +version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future" }, - { name = "jsonschema" }, { name = "pyfaidx" }, { name = "pyyaml" }, { name = "requests" }, { name = "rich" }, { name = "tqdm" }, + { name = "ubiquerg" }, { name = "yacman" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/ef/cc3e8746b27f8fcfa7bf07ce11fcac50301e856cb25a9e772430cf79ca96/refgenconf-0.12.2.tar.gz", hash = "sha256:6c9f9ecd8b91b4f75a535cfbdbdfb136f2dc9e9864142d07aa0352c61cf0cf78", size = 67815, upload-time = "2021-11-04T16:36:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/42/efc7f18e7991527b2395ff604719f9267204af8827e2c184915f71bcdc98/refgenconf-0.13.1.tar.gz", hash = "sha256:4653d5ada163a2716ef61f834207c55761d0ad681c8204a1a4873ef3708eb027", size = 77318, upload-time = "2026-04-01T13:31:52.717Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/df/43109af627154773c5475e1031d1adf86bb117e8b72410c7b9dc00993c1d/refgenconf-0.12.2-py3-none-any.whl", hash = "sha256:43be0120821b84a6480eb8bf23763c005df0a02f375aa2c1d318040711d26bb1", size = 72025, upload-time = "2021-11-04T16:36:46.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4a/141c6348e36a51bda59015affd99de8148de3cd9ea173bef6d86f2490120/refgenconf-0.13.1-py3-none-any.whl", hash = "sha256:1084a70720a872535c749616634e33e717784f9e140bfac5eed546539d7434e3", size = 66660, upload-time = "2026-04-01T13:31:51.382Z" }, ] [[package]] name = "refgenie" -version = "0.12.1" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "logmuse" }, { name = "piper" }, { name = "pyfaidx" }, { name = "refgenconf" }, + { name = "rich" }, + { name = "ubiquerg" }, { name = "yacman" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/45/34ae2aae013a07b1fce4f2c87c2f8d5abf3b6d390687fac9bd0dac60de4e/refgenie-0.12.1.tar.gz", hash = "sha256:cfd007ed0981e00d019deb49aaea896952341096494165cb8378488850eec451", size = 34272, upload-time = "2021-11-04T16:58:07.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/20/e631ace8f96bbf3317446a49fd97766edee0f2d9115a3c397881af9c29dd/refgenie-0.13.0.tar.gz", hash = "sha256:dc1a132b492feb8950f59737a87ea9626c8d400f728041ca3dd93ad860504404", size = 33042, upload-time = "2026-02-25T20:51:42.865Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/7f/024700960ef5238f5af119efdc422e14c0be3c0beafa0ea99f705c63f930/refgenie-0.12.1-py3-none-any.whl", hash = "sha256:cb831e99dcaedfc1cfbcfd8a6579b764a5b52912fdf3190adc030727fab8a126", size = 33425, upload-time = "2021-11-04T16:58:06.24Z" }, + { url = "https://files.pythonhosted.org/packages/f5/50/cd8613060f653e9735b3daab5930c57735a3d8d1f9af268c5e0188c3d646/refgenie-0.13.0-py3-none-any.whl", hash = "sha256:107e2b0fafb3e51158be134d6904f27fd288d7901eb93a44eb76b9e6232a20e2", size = 32970, upload-time = "2026-02-25T20:51:41.077Z" }, ] [[package]] @@ -2975,7 +2835,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2983,14 +2843,14 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] name = "requests-cache" -version = "1.3.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -3000,37 +2860,37 @@ dependencies = [ { name = "url-normalize" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/6c/deaf1a9462ce8b6a9ac0ee3603d9ba32917be8e48c8f6799770d5418c3cb/requests_cache-1.3.0.tar.gz", hash = "sha256:070e357ccef11a300ccef4294a85de1ab265833c5d9c9538b26cd7ba4085d54a", size = 97720, upload-time = "2026-02-02T23:17:33.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/41/cca56fdb48cbca8feb68770bce70cb315de2a1f7d6bf7ca62c437ac279f0/requests_cache-1.3.1.tar.gz", hash = "sha256:784e9d07f72db4fe234830a065230c59eb446489528f271ba288c640897e47c4", size = 99416, upload-time = "2026-03-04T18:05:50.918Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/3f/dfa42bb16be96d53351aa151cb1e39fcaafe6cda01389c530a2ec809ef8a/requests_cache-1.3.0-py3-none-any.whl", hash = "sha256:f09f27bbf100c250886acf13a9db35b53cf2852fddd71977b47c71ea7d90dbba", size = 69626, upload-time = "2026-02-02T23:17:31.718Z" }, + { url = "https://files.pythonhosted.org/packages/5a/08/48fa499c5c4667d5ceda6bd658dabf327b49997a99ac8674c61f4ba557a2/requests_cache-1.3.1-py3-none-any.whl", hash = "sha256:43a67448c3b2964c631ac7027b84607f2f63438e28104b68ad2211f32d9f606c", size = 70211, upload-time = "2026-03-04T18:05:49.628Z" }, ] [[package]] name = "responses" -version = "0.25.8" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303, upload-time = "2026-02-19T14:38:05.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099, upload-time = "2026-02-19T14:38:03.847Z" }, ] [[package]] name = "rich" -version = "14.3.2" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -3050,7 +2910,7 @@ wheels = [ [[package]] name = "rocrate" -version = "0.14.2" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arcp" }, @@ -3059,9 +2919,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/30/68e6a46f01c643d5af5e151afd69608dde4f9009191436db4c94432dcc92/rocrate-0.14.2.tar.gz", hash = "sha256:22263f149b17edeb0bb55cccbcc307917e93f62fe64cfcd14de53bbf1e561d10", size = 309959, upload-time = "2025-10-13T15:14:04.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/4f/bd59926cd240ed55309f8ea71b94c43802d85ef3e0d778ad3c78cfa872fc/rocrate-0.15.0.tar.gz", hash = "sha256:20f0b518dcb1efbf9d25833ec5c24586dee4c39201ed1024e2277a3cd171df3e", size = 320997, upload-time = "2026-04-13T09:19:32.177Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/4c/d74018818d4cc53e113d6df178b4b8894b304b10abfa9f44eb89e3648a0d/rocrate-0.14.2-py3-none-any.whl", hash = "sha256:2fcb1452e81e2f54e4456a7d69c74b37a0a56e55a43428292f593d9deebc6de2", size = 340572, upload-time = "2025-10-13T15:14:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/38/81/c7fd0f15eb155fb74891fe9cd07f5c4838c509dc3e1b5da9da7d390b75a0/rocrate-0.15.0-py3-none-any.whl", hash = "sha256:faa7b8fe41f84a7a8085cf4e31a973565a391cc25a850e847cdab5c2c2e52cee", size = 349120, upload-time = "2026-04-13T09:19:30.744Z" }, ] [[package]] @@ -3206,27 +3066,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, - { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, - { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, - { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, - { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, - { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] @@ -3258,11 +3118,11 @@ wheels = [ [[package]] name = "smmap" -version = "5.0.2" +version = "5.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] [[package]] @@ -3310,9 +3170,7 @@ name = "sphinx" version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*'", ] dependencies = [ { name = "alabaster", marker = "python_full_version == '3.11.*'" }, @@ -3343,12 +3201,8 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" }, @@ -3473,29 +3327,29 @@ wheels = [ [[package]] name = "tabulate" -version = "0.9.0" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] [[package]] name = "textual" -version = "7.5.0" +version = "8.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "python_full_version < '3.11'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "python_full_version >= '3.11'" }, { name = "mdit-py-plugins" }, { name = "platformdirs" }, { name = "pygments" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/38/7d169a765993efde5095c70a668bf4f5831bb7ac099e932f2783e9b71abf/textual-7.5.0.tar.gz", hash = "sha256:c730cba1e3d704e8f1ca915b6a3af01451e3bca380114baacf6abf87e9dac8b6", size = 1592319, upload-time = "2026-01-30T13:46:39.881Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/89/bec5709fb759f9c784bbcb30b2e3497df3f901691d13c2b864dbf6694a17/textual-8.2.4.tar.gz", hash = "sha256:d4e2b2ddd7157191d00b228592b7c739ea080b7d792fd410f23ca75f05ea76c4", size = 1848933, upload-time = "2026-04-19T04:20:45.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/78/96ddb99933e11d91bc6e05edae23d2687e44213066bcbaca338898c73c47/textual-7.5.0-py3-none-any.whl", hash = "sha256:849dfee9d705eab3b2d07b33152b7bd74fb1f5056e002873cc448bce500c6374", size = 718164, upload-time = "2026-01-30T13:46:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/02932f0d597cdbb34e34bf24266ff0f2cf292ccb3aafc37dd9efcb0cc416/textual-8.2.4-py3-none-any.whl", hash = "sha256:a83bd3f0cc7125ca203845af753f9d6b6be030025ecd1b05cc75ebe645b9c4ba", size = 724390, upload-time = "2026-04-19T04:20:49.968Z" }, ] [[package]] @@ -3533,56 +3387,56 @@ wheels = [ [[package]] name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] @@ -3612,7 +3466,7 @@ wheels = [ [[package]] name = "typer" -version = "0.23.1" +version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -3620,60 +3474,60 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, ] [[package]] name = "types-jsonschema" -version = "4.26.0.20260202" +version = "4.26.0.20260408" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/07/68f63e715eb327ed2f5292e29e8be99785db0f72c7664d2c63bd4dbdc29d/types_jsonschema-4.26.0.20260202.tar.gz", hash = "sha256:29831baa4308865a9aec547a61797a06fc152b0dac8dddd531e002f32265cb07", size = 16168, upload-time = "2026-02-02T04:11:22.585Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4c/2ef5483ef81e7b6e1b901eb2994fd280cb19860134a20ff99e72748f3ccb/types_jsonschema-4.26.0.20260408.tar.gz", hash = "sha256:82b75a976ed1507c473b8dee2d4841bd65926758c6d672bb93d08bf5e16f1b3f", size = 16553, upload-time = "2026-04-08T04:36:00.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/06/962d4f364f779d7389cd31a1bb581907b057f52f0ace2c119a8dd8409db6/types_jsonschema-4.26.0.20260202-py3-none-any.whl", hash = "sha256:41c95343abc4de9264e333a55e95dfb4d401e463856d0164eec9cb182e8746da", size = 15914, upload-time = "2026-02-02T04:11:21.61Z" }, + { url = "https://files.pythonhosted.org/packages/12/3c/724ad6ecaea06e8c6c8a8c9949a0979e6a2149b8aec4fe160135c64dd5ac/types_jsonschema-4.26.0.20260408-py3-none-any.whl", hash = "sha256:1ab0058c2612b0b81fc4e3c88ec3f9ad9b0af3fd31a176030d78b6d6cefb6e7b", size = 16081, upload-time = "2026-04-08T04:35:59.678Z" }, ] [[package]] name = "types-markdown" -version = "3.10.2.20260211" +version = "3.10.2.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/2e/35b30a09f6ee8a69142408d3ceb248c4454aa638c0a414d8704a3ef79563/types_markdown-3.10.2.20260211.tar.gz", hash = "sha256:66164310f88c11a58c6c706094c6f8c537c418e3525d33b76276a5fbd66b01ce", size = 19768, upload-time = "2026-02-11T04:19:29.497Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/0e/a690840934c459aa50e0470e7550d7f151632eafa4a8e3c21d18009ad15c/types_markdown-3.10.2.20260408.tar.gz", hash = "sha256:d5cba15ed65a1420e80e31c17e3d4a2ad7208a3f3a4da97fd2c5f093caf523cd", size = 19784, upload-time = "2026-04-08T04:33:07.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/c9/659fa2df04b232b0bfcd05d2418e683080e91ec68f636f3c0a5a267350e7/types_markdown-3.10.2.20260211-py3-none-any.whl", hash = "sha256:2d94d08587e3738203b3c4479c449845112b171abe8b5cadc9b0c12fcf3e99da", size = 25854, upload-time = "2026-02-11T04:19:28.647Z" }, + { url = "https://files.pythonhosted.org/packages/75/7e/265a8df257c8dced6ea89295f793a19f0a49ccbfeae1ed562368b2caf7a3/types_markdown-3.10.2.20260408-py3-none-any.whl", hash = "sha256:b0bbe8b7a8174db732067b86e391262898f5f536589ea81efec6d35ceb829331", size = 25857, upload-time = "2026-04-08T04:33:06.769Z" }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20250915" +version = "6.0.12.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" }, ] [[package]] name = "types-requests" -version = "2.32.4.20260107" +version = "2.33.0.20260503" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/b8/57e94268c0d82ac3eaa2fc35aa8ca7bbc2542f726b67dcf90b0b00a3b14d/types_requests-2.33.0.20260503.tar.gz", hash = "sha256:9721b2d9dbee7131f2fb39f20f0ebb1999c18cef4b512c9a7932f3722de7c5f4", size = 23931, upload-time = "2026-05-03T05:20:08.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, + { url = "https://files.pythonhosted.org/packages/c3/82/959113a6351f3ca046cd0a8cd2cee071d7ea47473560557a01eeae9a6fe2/types_requests-2.33.0.20260503-py3-none-any.whl", hash = "sha256:02aaa7e3577a13471715bb1bddb693cc985ea514f754b503bf033e6a09a3e528", size = 20736, upload-time = "2026-05-03T05:20:07.858Z" }, ] [[package]] name = "types-setuptools" -version = "82.0.0.20260210" +version = "82.0.0.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/90/796ac8c774a7f535084aacbaa6b7053d16fff5c630eff87c3ecff7896c37/types_setuptools-82.0.0.20260210.tar.gz", hash = "sha256:d9719fbbeb185254480ade1f25327c4654f8c00efda3fec36823379cebcdee58", size = 44768, upload-time = "2026-02-10T04:22:02.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/12/3464b410c50420dd4674fa5fe9d3880711c1dbe1a06f5fe4960ee9067b9e/types_setuptools-82.0.0.20260408.tar.gz", hash = "sha256:036c68caf7e672a699f5ebbf914708d40644c14e05298bc49f7272be91cf43d3", size = 44861, upload-time = "2026-04-08T04:29:33.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/54/3489432b1d9bc713c9d8aa810296b8f5b0088403662959fb63a8acdbd4fc/types_setuptools-82.0.0.20260210-py3-none-any.whl", hash = "sha256:5124a7daf67f195c6054e0f00f1d97c69caad12fdcf9113eba33eff0bce8cd2b", size = 68433, upload-time = "2026-02-10T04:22:00.876Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/46a4fc3ef03aabf5d18bac9df5cf37c6b02c3bddf3e05c3533f4b4588331/types_setuptools-82.0.0.20260408-py3-none-any.whl", hash = "sha256:ece0a215cdfa6463a65fd6f68bd940f39e455729300ddfe61cab1147ed1d2462", size = 68428, upload-time = "2026-04-08T04:29:32.175Z" }, ] [[package]] @@ -3699,41 +3553,41 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] name = "ubiquerg" -version = "0.9.0" +version = "0.9.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/4b/e23a8cd383b2c14e63acdd145bde5c9bb53c3565e39639ec6488d610e440/ubiquerg-0.9.0.tar.gz", hash = "sha256:d8a2e0a6f2e3c9326e7f5f33c074801a0ae359ee81bf65db9a4be502e8d3368f", size = 29907, upload-time = "2026-02-11T02:23:50.865Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/b3/d5117824d2b520cce2a6ae391ac6a2d69d9941f737d69c1b06aaffbdfa74/ubiquerg-0.9.3.tar.gz", hash = "sha256:3b4094377061fd5425cfc9f10865437f2c01d90a0d933d08682d789dc3e4ece8", size = 31309, upload-time = "2026-04-02T14:38:38.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/1b/cb3bcc63f3e00f75657dfcf82eb2637f56bc742ac1aff1cebe16f2c0be68/ubiquerg-0.9.0-py3-none-any.whl", hash = "sha256:df6e517902da4a07bcb7a0998a6cf4021f3fbf0d04c0841b45033e3b2e3958c4", size = 18256, upload-time = "2026-02-11T02:23:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6f/ab3741724827389a66a3fda618954272051cbe1f51153c457148762941b1/ubiquerg-0.9.3-py3-none-any.whl", hash = "sha256:ffe7589df85cf3ba7379b23e6d6060557b5dcd4eab3f70befcf90b6bf812f08a", size = 19249, upload-time = "2026-04-02T14:38:36.942Z" }, ] [[package]] name = "uc-micro-py" -version = "1.0.3" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ] [[package]] name = "url-normalize" -version = "2.2.1" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/31/febb777441e5fcdaacb4522316bf2a527c44551430a4873b052d545e3279/url_normalize-2.2.1.tar.gz", hash = "sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37", size = 18846, upload-time = "2025-04-26T20:37:58.553Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/cd/846d87d6d49d963b04ef4429b73d71d3c17468059956bab360866a9b0aec/url_normalize-3.0.0.tar.gz", hash = "sha256:0552cbf2831a32a28994a13d29bca58a60e10ff6c0380e343ec6d1c2a0d232d8", size = 21777, upload-time = "2026-04-25T00:31:59.514Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/d9/5ec15501b675f7bc07c5d16aa70d8d778b12375686b6efd47656efdc67cd/url_normalize-2.2.1-py3-none-any.whl", hash = "sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b", size = 14728, upload-time = "2025-04-26T20:37:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/f72344eab18674fd7b174f35abbce41ed88fea72927f111726732d0ca779/url_normalize-3.0.0-py3-none-any.whl", hash = "sha256:95234bd359f86831c1fd87c248877f2a6887db2f3b5087120083f2fffcba4889", size = 16854, upload-time = "2026-04-25T00:31:58.271Z" }, ] [[package]] @@ -3745,28 +3599,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -[[package]] -name = "virtualenv" -version = "20.36.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, -] - [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, ] [[package]] @@ -3784,144 +3623,155 @@ wheels = [ [[package]] name = "yacman" -version = "0.9.4" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attmap" }, - { name = "jsonschema" }, - { name = "oyaml" }, { name = "pyyaml" }, { name = "ubiquerg" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/b1/129c08d94d57efe63143f505b739c3ee631e915e2e83c4e93151e59464fc/yacman-0.9.4.tar.gz", hash = "sha256:e10babf0faa1e77ce35d8d00e6be1aa762aef5db6666db09015ba013a65e5f45", size = 23451, upload-time = "2025-11-05T19:28:40.088Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9b/b0eab5c362281e796ca84c3610298d6d0e7e98dbad0ec9fb72986b061bcf/yacman-1.0.0.tar.gz", hash = "sha256:203d8159709c6f2ba7cb2c99c96f0386de3a2d30fc1d26c38226f16d1f8c1b55", size = 51292, upload-time = "2026-03-06T01:38:30.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/d0/df1427ac21ef5c1ce6fe8f1bfe396e23ee8a025ae078893204a5dd7d4c7d/yacman-0.9.4-py3-none-any.whl", hash = "sha256:8acec597a8ede98562066611cd1d11ad1f4076fe7ae700d070afe8570c034eed", size = 27605, upload-time = "2025-11-05T19:28:38.604Z" }, + { url = "https://files.pythonhosted.org/packages/55/37/f4defbef12cf6ab09ff5ed8a942ac4567779d895723e3ed96e60eecbf92a/yacman-1.0.0-py3-none-any.whl", hash = "sha256:a63f65b53597d147472925833ffc158e534902f69bb1fe68b731e975ac8c1755", size = 15483, upload-time = "2026-03-06T01:38:29.11Z" }, ] [[package]] name = "yarl" -version = "1.22.0" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, - { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, - { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, - { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, - { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, - { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, - { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, - { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, - { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, - { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, - { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, - { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, - { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, - { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, - { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, - { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, - { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, - { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, - { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, - { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, - { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] [[package]] From 579cf362b38212d8b148adbc8101e6075efdb7bf Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 19 May 2026 11:55:12 +1000 Subject: [PATCH 110/121] update manifest with new tcss path --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index ba36475db3..84baedbeeb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,5 +9,5 @@ include nf_core/assets/logo/nf-core-repo-logo-base-darkbg.png include nf_core/assets/logo/placeholder_logo.svg include nf_core/assets/logo/MavenPro-Bold.ttf include nf_core/pipelines/create/create.tcss -include nf_core/textual.tcss +include nf_core/configs/create/create.tcss include nf_core/pipelines/create/template_features.yml From 1835494d96abde2f53cd76a4a7dbb55cc4877f66 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Tue, 19 May 2026 14:47:49 +1000 Subject: [PATCH 111/121] WIP: add unit tests for configs create --- tests/configs/test_create.py | 209 +++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 tests/configs/test_create.py diff --git a/tests/configs/test_create.py b/tests/configs/test_create.py new file mode 100644 index 0000000000..150302019f --- /dev/null +++ b/tests/configs/test_create.py @@ -0,0 +1,209 @@ +from nf_core.configs.create.utils import ConfigsCreateConfig, init_context + +from pydantic_core._pydantic_core import ValidationError + +from enum import Enum + + +class Context(Enum): + + NF_INFRA_HPC = { + "is_nfcore": True, + "is_infrastructure": True, + "is_hpc": True, + } + NF_INFRA_LOCAL = { + "is_nfcore": True, + "is_infrastructure": True, + "is_hpc": False, + } + NF_PIPE_HPC = { + "is_nfcore": True, + "is_infrastructure": False, + "is_hpc": True, + } + NF_PIPE_LOCAL = { + "is_nfcore": True, + "is_infrastructure": False, + "is_hpc": False, + } + CUSTOM_INFRA_HPC = { + "is_nfcore": False, + "is_infrastructure": True, + "is_hpc": True, + } + CUSTOM_INFRA_LOCAL = { + "is_nfcore": False, + "is_infrastructure": True, + "is_hpc": False, + } + CUSTOM_PIPE_HPC = { + "is_nfcore": False, + "is_infrastructure": False, + "is_hpc": True, + } + CUSTOM_PIPE_LOCAL = { + "is_nfcore": False, + "is_infrastructure": False, + "is_hpc": False, + } + + +def test_pipe_name(): + cfg_valid = {"config_pipeline_name": "Some pipeline name."} + cfg_invalid = {"config_pipeline_name": ""} + + # Test nf-core pipeline context + with init_context(Context.NF_PIPE_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_invalid) + except ValidationError: + failed = True + assert failed + + # Test custom pipeline context + with init_context(Context.CUSTOM_PIPE_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + ConfigsCreateConfig(**cfg_invalid) + + # Test custom infra context + with init_context(Context.CUSTOM_INFRA_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + ConfigsCreateConfig(**cfg_invalid) + + # Test nf-core infra context + with init_context(Context.NF_INFRA_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + ConfigsCreateConfig(**cfg_invalid) + + +def test_pipe_path(): + cfg_valid = {"config_pipeline_path": "."} + cfg_invalid_1 = {"config_pipeline_path": ""} + cfg_invalid_2 = {"config_pipeline_path": "./_this_path_doesnt_exist_"} + + # Test custom pipeline context + with init_context(Context.CUSTOM_PIPE_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_invalid_1) + except ValidationError: + failed = True + assert failed + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_invalid_2) + except ValidationError: + failed = True + assert failed + + # Test nf-core pipeline context + with init_context(Context.NF_PIPE_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + ConfigsCreateConfig(**cfg_invalid_1) + ConfigsCreateConfig(**cfg_invalid_2) + + # Test custom infra context + with init_context(Context.CUSTOM_INFRA_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + ConfigsCreateConfig(**cfg_invalid_1) + ConfigsCreateConfig(**cfg_invalid_2) + + # Test nf-core infra context + with init_context(Context.NF_INFRA_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid) + ConfigsCreateConfig(**cfg_invalid_1) + ConfigsCreateConfig(**cfg_invalid_2) + + +def test_config_name(): + cfg_valid_custom = {"general_config_name": "A valid custom config Name"} + cfg_valid_nfcore = {"general_config_name": "a valid nfcore config name"} + cfg_invalid = {"general_config_name": ""} + + # Test custom config context + with init_context(Context.CUSTOM_INFRA_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid_nfcore) + ConfigsCreateConfig(**cfg_valid_custom) + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_invalid) + except ValidationError: + failed = True + assert failed + + # Test nfcore config context + with init_context(Context.NF_INFRA_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid_nfcore) + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_valid_custom) + except ValidationError: + failed = True + assert failed + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_invalid) + except ValidationError: + failed = True + assert failed + + # Test custom pipeline context + with init_context(Context.CUSTOM_PIPE_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid_nfcore) + ConfigsCreateConfig(**cfg_valid_custom) + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_invalid) + except ValidationError: + failed = True + assert failed + + # Test nf-core pipeline context + with init_context(Context.NF_PIPE_LOCAL.value): + # Should succeed + ConfigsCreateConfig(**cfg_valid_nfcore) + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_valid_custom) + except ValidationError: + failed = True + assert failed + + # Should fail + failed = False + try: + ConfigsCreateConfig(**cfg_invalid) + except ValidationError: + failed = True + assert failed From aa3b24f48936e8f17bb8fe9235dee4738dad8d35 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Wed, 20 May 2026 10:18:51 +1000 Subject: [PATCH 112/121] WIP: more unit tests for validation code --- nf_core/configs/create/utils.py | 22 +- tests/configs/test_create.py | 496 ++++++++++++++++++++++++-------- 2 files changed, 391 insertions(+), 127 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 52a1e079d2..05f71a20a2 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -304,9 +304,9 @@ def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: @field_validator("config_profile_contact") @classmethod def notempty_contact(cls, v: str, info: ValidationInfo) -> str: - """Check that contact values are not empty when the config is infrastructure.""" + """Check that contact values are not empty when the config is nf-core.""" context = info.context - if context and context["is_infrastructure"] and context["is_nfcore"]: + if context and context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") return v @@ -319,12 +319,12 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: """Check that GitHub handles start with '@'. Make providing a handle mandatory for nf-core configs""" context = info.context - if context and context["is_infrastructure"] and context["is_nfcore"]: + if context and context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") - if not v.strip() == "" and not re.match(r"^@[aA-zZ\d](?:[aA-zZ\d]|-(?=[aA-zZ\d])){0,38}$", v): - ## Regex adapted from: https://github.com/shinnn/github-username-regex - raise ValueError("Handle must start with '@'.") + if not re.match(r"^@[aA-zZ\d](?:[aA-zZ\d]|-(?=[aA-zZ\d])){0,38}$", v): + ## Regex adapted from: https://github.com/shinnn/github-username-regex + raise ValueError("Handle must start with '@'.") return v @field_validator( @@ -334,7 +334,7 @@ def handle_prefix(cls, v: str, info: ValidationInfo) -> str: def url_prefix(cls, v: str, info: ValidationInfo) -> str: """Check that institutional web links start with valid URL prefix.""" context = info.context - if context and context["is_infrastructure"] and context["is_nfcore"]: + if context and context["is_nfcore"]: if v.strip() == "": raise ValueError("Cannot be left empty.") elif not re.match( @@ -344,14 +344,6 @@ def url_prefix(cls, v: str, info: ValidationInfo) -> str: raise ValueError( "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." ) - else: - if not v.strip() == "" and not re.match( - r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", - v, - ): ## Regex from: https://stackoverflow.com/a/3809435 - raise ValueError( - "Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com)." - ) return v @field_validator("custom_process_name_id") diff --git a/tests/configs/test_create.py b/tests/configs/test_create.py index 150302019f..11077a7f3e 100644 --- a/tests/configs/test_create.py +++ b/tests/configs/test_create.py @@ -49,40 +49,47 @@ class Context(Enum): } +def check_config(cfg: dict, fail: bool, ctx: dict = {}) -> None: + assert isinstance(cfg, dict) + assert isinstance(fail, bool) + assert isinstance(ctx, dict) + + with init_context(ctx): + if fail: + failed = False + try: + ConfigsCreateConfig(**cfg) + except ValidationError: + failed = True + assert failed + else: + ConfigsCreateConfig(**cfg) + + def test_pipe_name(): cfg_valid = {"config_pipeline_name": "Some pipeline name."} cfg_invalid = {"config_pipeline_name": ""} # Test nf-core pipeline context - with init_context(Context.NF_PIPE_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - - # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_invalid) - except ValidationError: - failed = True - assert failed + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_PIPE_LOCAL.value) # Test custom pipeline context - with init_context(Context.CUSTOM_PIPE_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - ConfigsCreateConfig(**cfg_invalid) + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) # Test custom infra context - with init_context(Context.CUSTOM_INFRA_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - ConfigsCreateConfig(**cfg_invalid) + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) # Test nf-core infra context - with init_context(Context.NF_INFRA_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - ConfigsCreateConfig(**cfg_invalid) + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) def test_pipe_path(): @@ -91,46 +98,29 @@ def test_pipe_path(): cfg_invalid_2 = {"config_pipeline_path": "./_this_path_doesnt_exist_"} # Test custom pipeline context - with init_context(Context.CUSTOM_PIPE_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - - # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_invalid_1) - except ValidationError: - failed = True - assert failed - - # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_invalid_2) - except ValidationError: - failed = True - assert failed + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid_1, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid_2, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) # Test nf-core pipeline context - with init_context(Context.NF_PIPE_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - ConfigsCreateConfig(**cfg_invalid_1) - ConfigsCreateConfig(**cfg_invalid_2) + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_invalid_1, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_invalid_2, fail=False, ctx=Context.NF_PIPE_LOCAL.value) # Test custom infra context - with init_context(Context.CUSTOM_INFRA_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - ConfigsCreateConfig(**cfg_invalid_1) - ConfigsCreateConfig(**cfg_invalid_2) + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_1, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_2, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) # Test nf-core infra context - with init_context(Context.NF_INFRA_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid) - ConfigsCreateConfig(**cfg_invalid_1) - ConfigsCreateConfig(**cfg_invalid_2) + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_1, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_2, fail=False, ctx=Context.NF_INFRA_LOCAL.value) def test_config_name(): @@ -138,72 +128,354 @@ def test_config_name(): cfg_valid_nfcore = {"general_config_name": "a valid nfcore config name"} cfg_invalid = {"general_config_name": ""} - # Test custom config context - with init_context(Context.CUSTOM_INFRA_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid_nfcore) - ConfigsCreateConfig(**cfg_valid_custom) + # Test custom infra config context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + # Test nfcore infra config context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + # Should fail + check_config(cfg_valid_custom, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + + # Test custom pipeline context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_valid_custom, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_invalid, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + +def test_contact_name(): + cfg_valid = {"config_profile_contact": "some name"} + cfg_invalid = {"config_profile_contact": ""} + + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + + # Test custom pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + + # Test custom infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + # Test nf-core infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + + +def test_handle(): + cfg_valid = {"config_profile_handle": "@someHandle"} + cfg_invalid = {"config_profile_handle": "BadHandle"} + cfg_empty = {"config_profile_handle": ""} + + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_empty, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + + # Test custom pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + + # Test custom infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + # Test nf-core infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_empty, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + + +def test_description(): + cfg_valid = {"config_profile_description": "A good description"} + cfg_invalid = {"config_profile_description": ""} + + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + + # Test custom pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + + # Test custom infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + # Test nf-core infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + + +def test_url(): + cfg_valid = {"config_profile_url": "http://some.url"} + cfg_invalid = {"config_profile_url": "a.bad.url"} + cfg_empty = {"config_profile_url": ""} + + # Test nf-core infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_empty, fail=True, ctx=Context.NF_INFRA_LOCAL.value) + + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_empty, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + + # Test custom pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + + # Test custom infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + +def test_proc_cpu_mem(): + for field in [ + "default_process_ncpus", + "default_process_memgb", + "custom_process_ncpus", + "custom_process_memgb", + ]: + cfg_valid = {field: "2"} + cfg_invalid_str = {field: "hello"} + cfg_invalid_float = {field: "1.2"} + cfg_invalid_zero = {field: "0"} + cfg_invalid_neg = {field: "-2"} + cfg_empty = {field: ""} + + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.NF_PIPE_LOCAL.value) # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_invalid) - except ValidationError: - failed = True - assert failed - - # Test nfcore config context - with init_context(Context.NF_INFRA_LOCAL.value): + check_config(cfg_invalid_str, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_invalid_float, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_invalid_zero, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_invalid_neg, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + + # Test custom pipeline context # Should succeed - ConfigsCreateConfig(**cfg_valid_nfcore) + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid_str, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid_float, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid_zero, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid_neg, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Test custom infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_str, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_float, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_zero, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_neg, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + # Test nf-core infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_str, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_float, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_zero, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_neg, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + + +def test_proc_hours(): + for field in [ + "default_process_hours", + "custom_process_hours", + ]: + cfg_valid = {field: "2"} + cfg_valid_float = {field: "1.2"} + cfg_valid_zero = {field: "0"} + cfg_invalid_str = {field: "hello"} + cfg_invalid_neg = {field: "-2"} + cfg_empty = {field: ""} + + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_valid_float, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_valid_zero, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.NF_PIPE_LOCAL.value) # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_valid_custom) - except ValidationError: - failed = True - assert failed + check_config(cfg_invalid_str, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_invalid_neg, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + # Test custom pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_valid_float, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_valid_zero, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_invalid) - except ValidationError: - failed = True - assert failed + check_config(cfg_invalid_str, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_invalid_neg, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) - # Test custom pipeline context - with init_context(Context.CUSTOM_PIPE_LOCAL.value): + # Test custom infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_valid_float, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_valid_zero, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_str, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_neg, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + # Test nf-core infra context # Should succeed - ConfigsCreateConfig(**cfg_valid_nfcore) - ConfigsCreateConfig(**cfg_valid_custom) + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_valid_float, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_valid_zero, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_str, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_neg, fail=False, ctx=Context.NF_INFRA_LOCAL.value) - # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_invalid) - except ValidationError: - failed = True - assert failed + +def test_proc_name(): + cfg_valid_nfcore = {"custom_process_name_id": "SOME.PROC_ESS:NAME*"} + cfg_valid_custom = {"custom_process_name_id": "a_process_name"} + cfg_empty = {"custom_process_name_id": ""} # Test nf-core pipeline context - with init_context(Context.NF_PIPE_LOCAL.value): - # Should succeed - ConfigsCreateConfig(**cfg_valid_nfcore) + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_valid_custom, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_empty, fail=True, ctx=Context.NF_PIPE_LOCAL.value) - # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_valid_custom) - except ValidationError: - failed = True - assert failed + # Test custom pipeline context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Should fail + check_config(cfg_empty, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) - # Should fail - failed = False - try: - ConfigsCreateConfig(**cfg_invalid) - except ValidationError: - failed = True - assert failed + # Test nf-core infra context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + + # Test custom infra context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + + +def test_proc_label(): + cfg_valid_nfcore = {"custom_process_label_id": "some_label"} + cfg_valid_custom = {"custom_process_label_id": "ANOTHER_LABEL"} + cfg_empty = {"custom_process_label_id": ""} + + +def test_proc_queue(): + cfg_valid = {} + + +def test_save_loc(): + cfg_valid = {} + + +def test_scheduler(): + cfg_valid = {} + + +def test_default_queue(): + cfg_valid = {} + + +def test_modules(): + cfg_valid = {} + + +def test_container(): + cfg_valid = {} + + +def test_cpu_mem(): + cfg_valid = {} + + +def test_hours(): + cfg_valid = {} + + +def test_cachedir(): + cfg_valid = {} + + +def test_igenomes(): + cfg_valid = {} + + +def test_scratch(): + cfg_valid = {} + + +def test_retires(): + cfg_valid = {} + + +def test_queue_stat_int(): + cfg_valid = {} + + +def test_queue_size(): + cfg_valid = {} + + +def test_poll_int(): + cfg_valid = {} + + +def test_submit_rate(): + cfg_valid = {} From 75b480c4ec3a366d42c4b736ff6607024830b17a Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 22 May 2026 12:23:36 +1000 Subject: [PATCH 113/121] finish validation unit tests --- tests/configs/test_create.py | 384 ++++++++++++++++++++++++++++++++--- 1 file changed, 351 insertions(+), 33 deletions(-) diff --git a/tests/configs/test_create.py b/tests/configs/test_create.py index 11077a7f3e..fad5e11eda 100644 --- a/tests/configs/test_create.py +++ b/tests/configs/test_create.py @@ -1,4 +1,4 @@ -from nf_core.configs.create.utils import ConfigsCreateConfig, init_context +from nf_core.configs.create.utils import ConfigsCreateConfig, init_context, SUPPORTED_SCHEDULERS, SUPPORTED_CONTAINERS from pydantic_core._pydantic_core import ValidationError @@ -418,64 +418,382 @@ def test_proc_label(): def test_proc_queue(): - cfg_valid = {} + cfg_valid = {"custom_process_queue": "somequeue"} + cfg_valid_empty = {"custom_process_queue": ""} + cfg_invalid_spaces = {"custom_process_queue": "a bad queue"} + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_valid_empty, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid_spaces, fail=True, ctx=Context.NF_PIPE_LOCAL.value) -def test_save_loc(): - cfg_valid = {} + # Test custom pipeline context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_valid_empty, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Should fail + check_config(cfg_invalid_spaces, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Test nf-core infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_valid_empty, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_invalid_spaces, fail=False, ctx=Context.NF_INFRA_LOCAL.value) -def test_scheduler(): - cfg_valid = {} + # Test custom infra context + # Should succeed + check_config(cfg_valid, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_valid_empty, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_invalid_spaces, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) -def test_default_queue(): - cfg_valid = {} +def test_save_loc(): + cfg_valid = {"savelocation": "."} + cfg_invalid_empty = {"savelocation": ""} + cfg_invalid_missing = {"savelocation": "./_this_path_doesnt_exist_"} + + # Test all contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + # Should fail + check_config(cfg_invalid_empty, fail=True, ctx=ctx) + check_config(cfg_invalid_missing, fail=True, ctx=ctx) + +def test_scheduler(): + cfg_empty = {"scheduler": ""} + cfg_unsupported = {"scheduler": "nonexistant_scheduler"} + + # Test all contexts other than INFRA_HPC + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.NF_INFRA_LOCAL.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + ]: + # Should succeed + check_config(cfg_empty, fail=False, ctx=ctx) + check_config(cfg_unsupported, fail=False, ctx=ctx) -def test_modules(): - cfg_valid = {} + # Test INFRA_HPC contexts + for ctx in [ + Context.NF_INFRA_HPC.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + for sch in SUPPORTED_SCHEDULERS: + cfg_valid = {"scheduler": sch} + check_config(cfg_valid, fail=False, ctx=ctx) + # Should fail + check_config(cfg_empty, fail=True, ctx=ctx) + check_config(cfg_unsupported, fail=True, ctx=ctx) def test_container(): - cfg_valid = {} + valid_cfgs = [ + {"container_system": ct} + for ct in SUPPORTED_CONTAINERS + ] + invalid_cfg = {"container_system": "imaginary_container_system"} + empty_cfg = {"container_system": ""} + + # Test all contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + for cfg_valid in valid_cfgs: + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(empty_cfg, fail=False, ctx=ctx) + # Shoudl fail + check_config(invalid_cfg, fail=True, ctx=ctx) -def test_cpu_mem(): - cfg_valid = {} +def test_cpu_mem_retries(): + for field in [ + "cpus", + "memory", + "retries", + ]: + cfg_valid = {field: "2"} + cfg_invalid_str = {field: "hello"} + cfg_invalid_float = {field: "1.2"} + cfg_invalid_zero = {field: "0"} + cfg_invalid_neg = {field: "-2"} + cfg_empty = {field: ""} + + # Test all INFRA contexts + for ctx in [ + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + # Shoudl fail + check_config(cfg_invalid_str, fail=True, ctx=ctx) + check_config(cfg_invalid_float, fail=True, ctx=ctx) + check_config(cfg_invalid_zero, fail=True, ctx=ctx) + check_config(cfg_invalid_neg, fail=True, ctx=ctx) + check_config(cfg_empty, fail=True, ctx=ctx) + + # Test all PIPE contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_invalid_str, fail=False, ctx=ctx) + check_config(cfg_invalid_float, fail=False, ctx=ctx) + check_config(cfg_invalid_zero, fail=False, ctx=ctx) + check_config(cfg_invalid_neg, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) def test_hours(): - cfg_valid = {} + cfg_valid = {"time": "2"} + cfg_valid_float = {"time": "1.2"} + cfg_valid_zero = {"time": "0"} + cfg_invalid_str = {"time": "hello"} + cfg_invalid_neg = {"time": "-2"} + cfg_empty = {"time": ""} + + # Test all INFRA contexts + for ctx in [ + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_valid_float, fail=False, ctx=ctx) + check_config(cfg_valid_zero, fail=False, ctx=ctx) + # Shoudl fail + check_config(cfg_invalid_str, fail=True, ctx=ctx) + check_config(cfg_invalid_neg, fail=True, ctx=ctx) + check_config(cfg_empty, fail=True, ctx=ctx) + + # Test all PIPE contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_valid_float, fail=False, ctx=ctx) + check_config(cfg_valid_zero, fail=False, ctx=ctx) + check_config(cfg_invalid_str, fail=False, ctx=ctx) + check_config(cfg_invalid_neg, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) -def test_cachedir(): - cfg_valid = {} +def test_cache_scratch_dirs(): + for field in [ + "cachedir", + "scratch_dir", + ]: + cfg_valid_abs = {field: "/an/absolute/path"} + cfg_valid_home = {field: "~/a/path/in/home"} + cfg_valid_env = {field: "${SOME}/env/prefixed/path"} + cfg_empty = {field: ""} + cfg_invalid_rel = {field: "a/relative/path"} + + # Test all contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid_abs, fail=False, ctx=ctx) + check_config(cfg_valid_home, fail=False, ctx=ctx) + check_config(cfg_valid_env, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) + # Should fail + check_config(cfg_invalid_rel, fail=True, ctx=ctx) def test_igenomes(): - cfg_valid = {} - - -def test_scratch(): - cfg_valid = {} - - -def test_retires(): - cfg_valid = {} + cfg_valid_abs = {"igenomes_cachedir": "/an/absolute/path"} + cfg_valid_home = {"igenomes_cachedir": "~/a/path/in/home"} + cfg_valid_env = {"igenomes_cachedir": "${SOME}/env/prefixed/path"} + cfg_valid_s3 = {"igenomes_cachedir": "s3://some/s3/bucket"} + cfg_empty = {"igenomes_cachedir": ""} + cfg_invalid_rel = {"igenomes_cachedir": "a/relative/path"} + + # Test all contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid_abs, fail=False, ctx=ctx) + check_config(cfg_valid_home, fail=False, ctx=ctx) + check_config(cfg_valid_env, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) + check_config(cfg_valid_s3, fail=False, ctx=ctx) + # Should fail + check_config(cfg_invalid_rel, fail=True, ctx=ctx) def test_queue_stat_int(): - cfg_valid = {} - + cfg_valid = {"queue_stat_interval": "2"} + cfg_valid_float = {"queue_stat_interval": "1.2"} + cfg_invalid_zero = {"queue_stat_interval": "0"} + cfg_invalid_str = {"queue_stat_interval": "hello"} + cfg_invalid_neg = {"queue_stat_interval": "-2"} + cfg_empty = {"queue_stat_interval": ""} + + # Test all INFRA_HPC contexts + for ctx in [ + Context.NF_INFRA_HPC.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_valid_float, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) + # Shoudl fail + check_config(cfg_invalid_zero, fail=True, ctx=ctx) + check_config(cfg_invalid_str, fail=True, ctx=ctx) + check_config(cfg_invalid_neg, fail=True, ctx=ctx) + + # Test all PIPE and INFRA_LOCAL contexts + for ctx in [ + Context.NF_INFRA_LOCAL.value, + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_valid_float, fail=False, ctx=ctx) + check_config(cfg_invalid_zero, fail=False, ctx=ctx) + check_config(cfg_invalid_str, fail=False, ctx=ctx) + check_config(cfg_invalid_neg, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) -def test_queue_size(): - cfg_valid = {} +def test_queue_size_submit_rate(): + for field in [ + "queue_size", + "submit_rate", + ]: + cfg_valid = {field: "2"} + cfg_invalid_str = {field: "hello"} + cfg_invalid_float = {field: "1.2"} + cfg_invalid_zero = {field: "0"} + cfg_invalid_neg = {field: "-2"} + cfg_empty = {field: ""} -def test_poll_int(): - cfg_valid = {} + # Test all INFRA contexts + for ctx in [ + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) + # Shoudl fail + check_config(cfg_invalid_str, fail=True, ctx=ctx) + check_config(cfg_invalid_float, fail=True, ctx=ctx) + check_config(cfg_invalid_zero, fail=True, ctx=ctx) + check_config(cfg_invalid_neg, fail=True, ctx=ctx) + + # Test all PIPE contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_invalid_str, fail=False, ctx=ctx) + check_config(cfg_invalid_float, fail=False, ctx=ctx) + check_config(cfg_invalid_zero, fail=False, ctx=ctx) + check_config(cfg_invalid_neg, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) -def test_submit_rate(): - cfg_valid = {} +def test_poll_int(): + cfg_valid = {"poll_interval": "2"} + cfg_valid_float = {"poll_interval": "1.2"} + cfg_invalid_zero = {"poll_interval": "0"} + cfg_invalid_str = {"poll_interval": "hello"} + cfg_invalid_neg = {"poll_interval": "-2"} + cfg_empty = {"poll_interval": ""} + + # Test all INFRA contexts + for ctx in [ + Context.NF_INFRA_LOCAL.value, + Context.NF_INFRA_HPC.value, + Context.CUSTOM_INFRA_LOCAL.value, + Context.CUSTOM_INFRA_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_valid_float, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) + # Shoudl fail + check_config(cfg_invalid_zero, fail=True, ctx=ctx) + check_config(cfg_invalid_str, fail=True, ctx=ctx) + check_config(cfg_invalid_neg, fail=True, ctx=ctx) + + # Test all PIPE contexts + for ctx in [ + Context.NF_PIPE_LOCAL.value, + Context.NF_PIPE_HPC.value, + Context.CUSTOM_PIPE_LOCAL.value, + Context.CUSTOM_PIPE_HPC.value, + ]: + # Should succeed + check_config(cfg_valid, fail=False, ctx=ctx) + check_config(cfg_valid_float, fail=False, ctx=ctx) + check_config(cfg_invalid_zero, fail=False, ctx=ctx) + check_config(cfg_invalid_str, fail=False, ctx=ctx) + check_config(cfg_invalid_neg, fail=False, ctx=ctx) + check_config(cfg_empty, fail=False, ctx=ctx) From 9a0c69941df8808085cef8068d6f7f3986a352fe Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 22 May 2026 15:37:12 +1000 Subject: [PATCH 114/121] finish unit tests, reformat, fix failing app tests --- nf_core/configs/create/basicdetails.py | 4 +- nf_core/configs/create/finalinfradetails.py | 4 +- nf_core/configs/create/hpccustomisation.py | 3 +- nf_core/configs/create/utils.py | 6 +- tests/configs/test_create_app.py | 15 +- tests/configs/test_serial.py | 733 ++++++++++++++++++ .../{test_create.py => test_validation.py} | 6 +- tests/test_configs.py | 43 +- 8 files changed, 802 insertions(+), 12 deletions(-) create mode 100644 tests/configs/test_serial.py rename tests/configs/{test_create.py => test_validation.py} (99%) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 329c81615a..2582e64032 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -63,7 +63,9 @@ def compose(self) -> ComposeResult: yield Markdown( "Pipeline name.", id="config_pipeline_name_text", - classes="hide" if self.parent.CONFIG_TYPE == "infrastructure" or not self.parent.NFCORE_CONFIG else "field_help", + classes="hide" + if self.parent.CONFIG_TYPE == "infrastructure" or not self.parent.NFCORE_CONFIG + else "field_help", ) yield Select( [(c, c) for c in self.nf_core_pipelines], diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index e54f4c45c7..ad17cb5977 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -162,7 +162,9 @@ def _get_container_system(self) -> str: # If not available natively, check if module exists if module_system_used: try: - output = subprocess.check_output(["module", "avail", "|", "grep", system], stderr=subprocess.STDOUT).decode("utf-8") + output = subprocess.check_output( + ["module", "avail", "|", "grep", system], stderr=subprocess.STDOUT + ).decode("utf-8") if output: return system except subprocess.CalledProcessError: diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index ca90e295e0..a9cf38a185 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -45,6 +45,7 @@ default to 1 minute if not provided. """ + class HpcCustomisation(Screen): """Customise the options to create a config for an HPC.""" @@ -68,7 +69,7 @@ def compose(self) -> ComposeResult: prompt="Select your HPC's scheduler.", value=scheduler if scheduler is not None else "local", classes="column", - id="scheduler" + id="scheduler", ) yield Markdown(markdown_queue) yield TextInput( diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 05f71a20a2..a2a05f0e09 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -182,7 +182,9 @@ def serial_hpc(self): }, self.container_system: { "enabled": True, - "cacheDir": self.cachedir if self.container_system in ["singularity", "apptainer", "charliecloud", "conda"] else None, + "cacheDir": self.cachedir + if self.container_system in ["singularity", "apptainer", "charliecloud", "conda"] + else None, "autoMounts": True if self.container_system in ["singularity", "apptainer"] else None, }, "cleanup": self.delete_work_dir, @@ -687,4 +689,4 @@ def add_hide_class(app, widget_id: str) -> None: def remove_hide_class(app, widget_id: str) -> None: """Remove class 'hide' to a widget. Display widget.""" - app.get_widget_by_id(widget_id).remove_class("hide") \ No newline at end of file + app.get_widget_by_id(widget_id).remove_class("hide") diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py index f42796b54d..ed8ed836fd 100644 --- a/tests/configs/test_create_app.py +++ b/tests/configs/test_create_app.py @@ -6,7 +6,12 @@ from nf_core.configs.create import ConfigsCreateApp from nf_core.configs.create.utils import SUPPORTED_CONTAINERS -from ..test_configs import INFRA_SINGULARITY_CONFIG, INFRA_CUSTOM_SINGULARITY_CONFIG, PIPE_NFCORE_CONFIG, PIPE_CUSTOM_CONFIG +from ..test_configs import ( + INFRA_SINGULARITY_CONFIG, + INFRA_CUSTOM_SINGULARITY_CONFIG, + PIPE_NFCORE_CONFIG, + PIPE_CUSTOM_CONFIG, +) INIT_FILE = "../../nf_core/configs/create/__init__.py" @@ -201,6 +206,10 @@ def test_submitting_infra_nfcore_hpc_details(snap_compare): async def run_before(pilot) -> None: await submit_infra_nfcore_hpc_details(pilot) + # Select singularity from the drop down to ensure consistency + await pilot.click("#container_system") + await pilot.press(*list("singularity")) + await pilot.press("enter") assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) @@ -483,6 +492,10 @@ def test_submitting_infra_custom_hpc_details(snap_compare): async def run_before(pilot) -> None: await submit_infra_custom_hpc_details(pilot) + # Select singularity from the drop down to ensure consistency + await pilot.click("#container_system") + await pilot.press(*list("singularity")) + await pilot.press("enter") assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) diff --git a/tests/configs/test_serial.py b/tests/configs/test_serial.py new file mode 100644 index 0000000000..8119a5d312 --- /dev/null +++ b/tests/configs/test_serial.py @@ -0,0 +1,733 @@ +from nf_core.configs.create.serial import NextflowSerial +from nf_core.configs.create.utils import ConfigsCreateConfig, init_context +from nf_core.configs.create.create import ConfigCreate + +from ..test_configs import ( + INFRA_SINGULARITY_CONFIG, + INFRA_CUSTOM_SINGULARITY_CONFIG, + PIPE_NFCORE_CONFIG, + PIPE_CUSTOM_CONFIG, + INFRA_NFCORE_SINGULARITY_LOCAL_CONFIG, + INFRA_CUSTOM_SINGULARITY_LOCAL_CONFIG, +) + +from pydantic import ValidationError +from pathlib import Path +from tempfile import TemporaryDirectory + +# Valid configs + +VALID_NFCORE_INFRA_HPC_CONFIG = { + "general_config_name": "myconfig", + "is_infrastructure": True, + "is_nfcore": True, + "config_profile_contact": "my name", + "config_profile_handle": "@myhandle", + "config_profile_description": "A cool description", + "config_profile_url": "https://example.com", + "scheduler": "pbspro", + "queue": "myqueue", + "module": False, + "module_system": "", + "container_system": "singularity", + "cpus": "48", + "memory": "128", + "time": "9.5", + "cachedir": "/singularity/cachedir", + "scratch_dir": "/tmp/scratch", + "igenomes_cachedir": "/data/igenomes/cache", + "retries": "2", + "delete_work_dir": True, + "queue_stat_interval": "0.75", + "poll_interval": "0.25", + "queue_size": "200", + "submit_rate": "25", + "savelocation": ".", +} + + +VALID_CUSTOM_INFRA_HPC_CONFIG = { + "general_config_name": "myconfig", + "is_infrastructure": True, + "is_nfcore": False, + "config_profile_description": "A cool description", + "scheduler": "pbspro", + "queue": "myqueue", + "module": False, + "module_system": "", + "container_system": "singularity", + "cpus": "48", + "memory": "128", + "time": "9.5", + "cachedir": "/singularity/cachedir", + "scratch_dir": "/tmp/scratch", + "retries": "2", + "delete_work_dir": True, + "queue_stat_interval": "0.75", + "poll_interval": "0.25", + "queue_size": "200", + "submit_rate": "25", + "savelocation": ".", +} + + +VALID_NFCORE_INFRA_LOCAL_CONFIG = { + "general_config_name": "myconfig", + "is_infrastructure": True, + "is_nfcore": True, + "config_profile_contact": "my name", + "config_profile_handle": "@myhandle", + "config_profile_description": "A cool description", + "config_profile_url": "https://example.com", + "container_system": "singularity", + "cpus": "8", + "memory": "32", + "time": "19.5", + "cachedir": "/singularity/cachedir", + "scratch_dir": "/tmp/scratch", + "igenomes_cachedir": "/data/igenomes/cache", + "retries": "2", + "delete_work_dir": True, + "savelocation": ".", +} + + +VALID_CUSTOM_INFRA_LOCAL_CONFIG = { + "general_config_name": "myconfig", + "is_infrastructure": True, + "is_nfcore": False, + "config_profile_description": "A cool description", + "container_system": "singularity", + "cpus": "8", + "memory": "32", + "time": "19.5", + "cachedir": "/singularity/cachedir", + "scratch_dir": "/tmp/scratch", + "retries": "2", + "delete_work_dir": True, + "savelocation": ".", +} + + +VALID_NFCORE_PIPE_HPC_CONFIG = { + "general_config_name": "myconfig", + "is_infrastructure": False, + "is_nfcore": True, + "config_pipeline_name": "rnaseq", + "config_profile_contact": "my name", + "config_profile_handle": "@myhandle", + "config_profile_description": "A cool description", + "config_profile_url": "https://example.com", + "default_process_ncpus": "2", + "default_process_memgb": "8", + "default_process_hours": "9.5", + "named_process_resources": { + "SOME:PROC.NAME*": { + "custom_process_name_id": "SOME:PROC.NAME*", + "custom_process_ncpus": "3", + "custom_process_memgb": "4", + "custom_process_hours": "5.5", + "custom_process_queue": "", + }, + "ANOTHER:PROC_NAME": { + "custom_process_name_id": "ANOTHER:PROC_NAME", + "custom_process_ncpus": "2", + "custom_process_memgb": "3", + "custom_process_hours": "4.4", + "custom_process_queue": "customqueue", + }, + }, + "labelled_process_resources": { + "some_proc_label": { + "custom_process_label_id": "some_proc_label", + "custom_process_ncpus": "1", + "custom_process_memgb": "1", + "custom_process_hours": "1.1", + "custom_process_queue": "", + }, + "another_label": { + "custom_process_label_id": "another_label", + "custom_process_ncpus": "2", + "custom_process_memgb": "2", + "custom_process_hours": "2.2", + "custom_process_queue": "customqueue", + }, + "a_third_label": { + "custom_process_label_id": "a_third_label", + "custom_process_ncpus": "", + "custom_process_memgb": "", + "custom_process_hours": "", + "custom_process_queue": "", + }, + "fourth_label": { + "custom_process_label_id": "fourth_label", + "custom_process_ncpus": "4", + "custom_process_memgb": "", + "custom_process_hours": "4.4", + "custom_process_queue": "anotherqueue", + }, + }, + "savelocation": ".", +} + + +VALID_CUSTOM_PIPE_HPC_CONFIG = { + "general_config_name": "myconfig", + "is_infrastructure": False, + "is_nfcore": False, + "config_profile_description": "A cool description", + "config_pipeline_path": ".", + "default_process_ncpus": "2", + "default_process_memgb": "8", + "default_process_hours": "9.5", + "named_process_resources": { + "some_proc": { + "custom_process_name_id": "some_proc", + "custom_process_ncpus": "3", + "custom_process_memgb": "4", + "custom_process_hours": "5.5", + "custom_process_queue": "", + }, + "another_proc": { + "custom_process_name_id": "another_proc", + "custom_process_ncpus": "2", + "custom_process_memgb": "3", + "custom_process_hours": "4.4", + "custom_process_queue": "customqueue", + }, + }, + "labelled_process_resources": { + "some_proc_label": { + "custom_process_label_id": "some_proc_label", + "custom_process_ncpus": "1", + "custom_process_memgb": "1", + "custom_process_hours": "1.1", + "custom_process_queue": "", + }, + "another_label": { + "custom_process_label_id": "another_label", + "custom_process_ncpus": "2", + "custom_process_memgb": "2", + "custom_process_hours": "2.2", + "custom_process_queue": "customqueue", + }, + "a_third_label": { + "custom_process_label_id": "a_third_label", + "custom_process_ncpus": "", + "custom_process_memgb": "", + "custom_process_hours": "", + "custom_process_queue": "", + }, + "fourth_label": { + "custom_process_label_id": "fourth_label", + "custom_process_ncpus": "4", + "custom_process_memgb": "", + "custom_process_hours": "4.4", + "custom_process_queue": "anotherqueue", + }, + }, + "savelocation": ".", +} + + +# Invalid configs + +INVALID_NFCORE_INFRA_HPC_CONFIG = { + "is_infrastructure": True, + "is_nfcore": True, + "config_profile_contact": "my name", + "config_profile_handle": "bad handle", + "config_profile_description": "A cool description", + "scheduler": "unsupported", + "queue": "myqueue", + "module": False, + "module_system": "", + "container_system": "noncontainer", + "cpus": "1.1", + "memory": "128.9", + "time": "9.5a", + "cachedir": "/singularity/cachedir", + "scratch_dir": "/tmp/scratch", + "igenomes_cachedir": "/data/igenomes/cache", + "retries": "2", + "delete_work_dir": True, + "queue_stat_interval": "0.75", + "poll_interval": "0.25", + "queue_size": "200", + "submit_rate": "25", + "savelocation": ".", +} + + +INVALID_CUSTOM_INFRA_HPC_CONFIG = { + "is_infrastructure": True, + "is_nfcore": False, + "config_profile_description": "A cool description", + "scheduler": "pbsprose", + "queue": "", + "module": False, + "module_system": "", + "container_system": "singularitay", + "cachedir": "singularity/cachedir", + "scratch_dir": "tmp/scratch", + "retries": "2", + "delete_work_dir": True, + "queue_stat_interval": "0.75", + "poll_interval": "0.25", + "queue_size": "200", + "submit_rate": "25", + "savelocation": ".", +} + + +INVALID_NFCORE_INFRA_LOCAL_CONFIG = { + "is_infrastructure": True, + "is_nfcore": True, + "config_profile_contact": "my name", + "config_profile_handle": "@myhandle", + "config_profile_description": "A cool description", + "config_profile_url": "bad_url", + "container_system": "", + "cpus": "8", + "memory": "32", + "time": "19.5", + "cachedir": "/singularity/cachedir", + "scratch_dir": "/tmp/scratch", + "igenomes_cachedir": "/data/igenomes/cache", + "retries": "2", + "delete_work_dir": True, + "savelocation": "_path_doesnt_exist_", +} + + +INVALID_CUSTOM_INFRA_LOCAL_CONFIG = { + "is_infrastructure": True, + "is_nfcore": False, + "config_profile_description": "A cool description", + "container_system": "apptianer", + "cpus": "8.0", + "memory": "32 GB", + "time": "19.5.1", + "cachedir": "singularity/cachedir", + "scratch_dir": "tmp/scratch", + "retries": "2", + "delete_work_dir": True, + "savelocation": ".", +} + + +INVALID_NFCORE_PIPE_HPC_CONFIG = { + "is_infrastructure": False, + "is_nfcore": True, + "config_pipeline_name": "", + "config_profile_contact": "my name", + "config_profile_handle": "@myhandle", + "config_profile_description": "A cool description", + "config_profile_url": "example.com", + "default_process_ncpus": "2", + "default_process_memgb": "8", + "default_process_hours": "9.5", + "named_process_resources": { + "SOME:PROC.NAME*": { + "custom_process_name_id": "SOME:PROC.NAME*", + "custom_process_ncpus": "3", + "custom_process_memgb": "4", + "custom_process_hours": "5.5", + "custom_process_queue": "", + }, + "ANOTHER:PROC_NAME": { + "custom_process_name_id": "ANOTHER:PROC_NAME", + "custom_process_ncpus": "2", + "custom_process_memgb": "3", + "custom_process_hours": "4.4", + "custom_process_queue": "customqueue", + }, + }, + "labelled_process_resources": { + "some_proc_label": { + "custom_process_label_id": "some_proc_label", + "custom_process_ncpus": "1", + "custom_process_memgb": "1", + "custom_process_hours": "1.1", + "custom_process_queue": "", + }, + "another_label": { + "custom_process_label_id": "another_label", + "custom_process_ncpus": "2", + "custom_process_memgb": "2", + "custom_process_hours": "2.2", + "custom_process_queue": "custom queue", + }, + "a_third_label": { + "custom_process_label_id": "a_third_label", + "custom_process_ncpus": "", + "custom_process_memgb": "", + "custom_process_hours": "", + "custom_process_queue": "", + }, + "fourth_label": { + "custom_process_label_id": "fourth_label", + "custom_process_ncpus": "4", + "custom_process_memgb": "", + "custom_process_hours": "4.4", + "custom_process_queue": "anotherqueue", + }, + }, + "savelocation": ".", +} + + +INVALID_CUSTOM_PIPE_HPC_CONFIG = { + "is_infrastructure": False, + "is_nfcore": False, + "config_profile_description": "A cool description", + "config_pipeline_path": "_this_path_doesnt_exist_", + "default_process_ncpus": "2", + "default_process_memgb": "8", + "default_process_hours": ".", + "named_process_resources": { + "some_proc": { + "custom_process_name_id": "some_proc", + "custom_process_ncpus": "3", + "custom_process_memgb": "4", + "custom_process_hours": "5.5", + "custom_process_queue": "", + }, + "another_proc": { + "custom_process_name_id": "another_proc", + "custom_process_ncpus": "2", + "custom_process_memgb": "3", + "custom_process_hours": "4.4", + "custom_process_queue": "customqueue", + }, + }, + "labelled_process_resources": { + "some_proc_label": { + "custom_process_label_id": "some_proc_label", + "custom_process_ncpus": "1", + "custom_process_memgb": "1", + "custom_process_hours": "1.1", + "custom_process_queue": "", + }, + "another_label": { + "custom_process_label_id": "another_label", + "custom_process_ncpus": "2", + "custom_process_memgb": "2", + "custom_process_hours": "2.2", + "custom_process_queue": "customqueue", + }, + "a_third_label": { + "custom_process_label_id": "a_third_label", + "custom_process_ncpus": "", + "custom_process_memgb": "", + "custom_process_hours": "", + "custom_process_queue": "", + }, + "fourth_label": { + "custom_process_label_id": "fourth_label", + "custom_process_ncpus": "4", + "custom_process_memgb": "", + "custom_process_hours": "4.4", + "custom_process_queue": "anotherqueue", + }, + }, + "savelocation": "_this_path_also_doesnt_exist_", +} + + +def test_serial_valid_nfcore_infra_hpc(): + with init_context( + { + "is_nfcore": True, + "is_infrastructure": True, + "is_hpc": True, + } + ): + c = ConfigsCreateConfig(**VALID_NFCORE_INFRA_HPC_CONFIG) + s = NextflowSerial.dumps(data_dict=c.serial(), drop_null=True) + assert s == INFRA_SINGULARITY_CONFIG + + # Test writing to file + tmpdir = TemporaryDirectory() + cf = ConfigCreate(template_config=c, config_type="infrastructure", config_dir=Path(tmpdir.name)) + cf.write_to_file() + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + tmpdir.cleanup() + assert config == INFRA_SINGULARITY_CONFIG + + +def test_serial_valid_custom_infra_hpc(): + with init_context( + { + "is_nfcore": False, + "is_infrastructure": True, + "is_hpc": True, + } + ): + c = ConfigsCreateConfig(**VALID_CUSTOM_INFRA_HPC_CONFIG) + s = NextflowSerial.dumps(data_dict=c.serial(), drop_null=True) + assert s == INFRA_CUSTOM_SINGULARITY_CONFIG + + # Test writing to file + tmpdir = TemporaryDirectory() + cf = ConfigCreate(template_config=c, config_type="infrastructure", config_dir=Path(tmpdir.name)) + cf.write_to_file() + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + tmpdir.cleanup() + assert config == INFRA_CUSTOM_SINGULARITY_CONFIG + + +def test_serial_valid_nfcore_pipe_hpc(): + with init_context( + { + "is_nfcore": True, + "is_infrastructure": False, + "is_hpc": True, + } + ): + c = ConfigsCreateConfig(**VALID_NFCORE_PIPE_HPC_CONFIG) + s = NextflowSerial.dumps(data_dict=c.serial(), drop_null=True) + assert s == PIPE_NFCORE_CONFIG + + # Test writing to file + tmpdir = TemporaryDirectory() + cf = ConfigCreate(template_config=c, config_type="pipeline", config_dir=Path(tmpdir.name)) + cf.write_to_file() + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + tmpdir.cleanup() + assert config == PIPE_NFCORE_CONFIG + + +def test_serial_valid_custom_pipe_hpc(): + with init_context( + { + "is_nfcore": False, + "is_infrastructure": False, + "is_hpc": True, + } + ): + c = ConfigsCreateConfig(**VALID_CUSTOM_PIPE_HPC_CONFIG) + s = NextflowSerial.dumps(data_dict=c.serial(), drop_null=True) + assert s == PIPE_CUSTOM_CONFIG + + # Test writing to file + tmpdir = TemporaryDirectory() + cf = ConfigCreate(template_config=c, config_type="pipeline", config_dir=Path(tmpdir.name)) + cf.write_to_file() + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + tmpdir.cleanup() + assert config == PIPE_CUSTOM_CONFIG + + +def test_serial_valid_nfcore_infra_local(): + with init_context( + { + "is_nfcore": True, + "is_infrastructure": True, + "is_hpc": False, + } + ): + c = ConfigsCreateConfig(**VALID_NFCORE_INFRA_LOCAL_CONFIG) + s = NextflowSerial.dumps(data_dict=c.serial(), drop_null=True) + assert s == INFRA_NFCORE_SINGULARITY_LOCAL_CONFIG + + # Test writing to file + tmpdir = TemporaryDirectory() + cf = ConfigCreate(template_config=c, config_type="infrastructure", config_dir=Path(tmpdir.name)) + cf.write_to_file() + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + tmpdir.cleanup() + assert config == INFRA_NFCORE_SINGULARITY_LOCAL_CONFIG + + +def test_serial_valid_custom_infra_local(): + with init_context( + { + "is_nfcore": False, + "is_infrastructure": True, + "is_hpc": False, + } + ): + c = ConfigsCreateConfig(**VALID_CUSTOM_INFRA_LOCAL_CONFIG) + s = NextflowSerial.dumps(data_dict=c.serial(), drop_null=True) + assert s == INFRA_CUSTOM_SINGULARITY_LOCAL_CONFIG + + # Test writing to file + tmpdir = TemporaryDirectory() + cf = ConfigCreate(template_config=c, config_type="infrastructure", config_dir=Path(tmpdir.name)) + cf.write_to_file() + config_file = Path(tmpdir.name) / "myconfig.conf" + with open(config_file) as f: + config = f.read() + tmpdir.cleanup() + assert config == INFRA_CUSTOM_SINGULARITY_LOCAL_CONFIG + + +# Invalid tests + + +def test_serial_invalid_nfcore_infra_hpc(): + with init_context( + { + "is_nfcore": True, + "is_infrastructure": True, + "is_hpc": True, + } + ): + expected_error = None + + try: + c = ConfigsCreateConfig(**INVALID_NFCORE_INFRA_HPC_CONFIG) + except ValidationError as e: + expected_error = e + + assert expected_error is not None + errors = expected_error.errors() + assert len(errors) == 6 + expected_error_msgs = { + "Value error, Must be a number.", + "Value error, Handle must start with '@'.", + "Value error, Must be one of: singularity, docker, apptainer, charliecloud, podman, sarus, shifter, conda", + "Value error, Must be one of: local, pbs, pbspro, slurm, sge", + "Value error, Must be an integer.", + } + assert set([e["msg"] for e in errors]) == expected_error_msgs + + +def test_serial_invalid_custom_infra_hpc(): + with init_context( + { + "is_nfcore": False, + "is_infrastructure": True, + "is_hpc": True, + } + ): + expected_error = None + + try: + c = ConfigsCreateConfig(**INVALID_CUSTOM_INFRA_HPC_CONFIG) + except ValidationError as e: + expected_error = e + + assert expected_error is not None + errors = expected_error.errors() + assert len(errors) == 4 + expected_error_msgs = { + "Value error, Must be one of: singularity, docker, apptainer, charliecloud, podman, sarus, shifter, conda", + "Value error, Must be one of: local, pbs, pbspro, slurm, sge", + "Value error, Must be an absolute path (/data/scratch), a path relative to home (~/scratch), or a path with an environmental variable (e.g. ${DIR}/scratch)", + } + assert set([e["msg"] for e in errors]) == expected_error_msgs + + +def test_serial_invalid_nfcore_pipe_hpc(): + with init_context( + { + "is_nfcore": True, + "is_infrastructure": False, + "is_hpc": True, + } + ): + expected_error = None + + try: + c = ConfigsCreateConfig(**INVALID_NFCORE_PIPE_HPC_CONFIG) + except ValidationError as e: + expected_error = e + + assert expected_error is not None + errors = expected_error.errors() + assert len(errors) == 2 + expected_error_msgs = { + "Value error, Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com).", + "Value error, Cannot be left empty.", + } + assert set([e["msg"] for e in errors]) == expected_error_msgs + + +def test_serial_invalid_custom_pipe_hpc(): + with init_context( + { + "is_nfcore": False, + "is_infrastructure": False, + "is_hpc": True, + } + ): + expected_error = None + + try: + c = ConfigsCreateConfig(**INVALID_CUSTOM_PIPE_HPC_CONFIG) + except ValidationError as e: + expected_error = e + + assert expected_error is not None + errors = expected_error.errors() + assert len(errors) == 3 + expected_error_msgs = { + "Value error, Must be a valid path.", + "Value error, Must be a number.", + "Value error, Must be a valid path to a directory.", + } + assert set([e["msg"] for e in errors]) == expected_error_msgs + + +def test_serial_invalid_nfcore_infra_local(): + with init_context( + { + "is_nfcore": True, + "is_infrastructure": True, + "is_hpc": False, + } + ): + expected_error = None + + try: + c = ConfigsCreateConfig(**INVALID_NFCORE_INFRA_LOCAL_CONFIG) + except ValidationError as e: + expected_error = e + + assert expected_error is not None + errors = expected_error.errors() + assert len(errors) == 2 + expected_error_msgs = { + "Value error, Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com).", + "Value error, Must be a valid path to a directory.", + } + assert set([e["msg"] for e in errors]) == expected_error_msgs + + +def test_serial_invalid_custom_infra_local(): + with init_context( + { + "is_nfcore": False, + "is_infrastructure": True, + "is_hpc": False, + } + ): + expected_error = None + + try: + c = ConfigsCreateConfig(**INVALID_CUSTOM_INFRA_LOCAL_CONFIG) + except ValidationError as e: + expected_error = e + + assert expected_error is not None + errors = expected_error.errors() + assert len(errors) == 6 + expected_error_msgs = { + "Value error, Must be a number.", + "Value error, Must be an integer.", + "Value error, Must be one of: singularity, docker, apptainer, charliecloud, podman, sarus, shifter, conda", + "Value error, Must be an absolute path (/data/scratch), a path relative to home (~/scratch), or a path with an environmental variable (e.g. ${DIR}/scratch)", + } + assert set([e["msg"] for e in errors]) == expected_error_msgs diff --git a/tests/configs/test_create.py b/tests/configs/test_validation.py similarity index 99% rename from tests/configs/test_create.py rename to tests/configs/test_validation.py index fad5e11eda..68719fd5e8 100644 --- a/tests/configs/test_create.py +++ b/tests/configs/test_validation.py @@ -6,7 +6,6 @@ class Context(Enum): - NF_INFRA_HPC = { "is_nfcore": True, "is_infrastructure": True, @@ -504,10 +503,7 @@ def test_scheduler(): def test_container(): - valid_cfgs = [ - {"container_system": ct} - for ct in SUPPORTED_CONTAINERS - ] + valid_cfgs = [{"container_system": ct} for ct in SUPPORTED_CONTAINERS] invalid_cfg = {"container_system": "imaginary_container_system"} empty_cfg = {"container_system": ""} diff --git a/tests/test_configs.py b/tests/test_configs.py index 10d79a8847..6bca8e487a 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -131,4 +131,45 @@ queue = 'anotherqueue' } } -""" \ No newline at end of file +""" + +INFRA_NFCORE_SINGULARITY_LOCAL_CONFIG = """\ +params { + config_profile_contact = 'my name (@myhandle)' + config_profile_description = 'A cool description' + config_profile_url = 'https://example.com' + igenomes_base = '/data/igenomes/cache' +} +executor { +} +process { + resourceLimits = [cpus: 8, memory: '32.0GB', time: '19.5h'] + scratch = '/tmp/scratch' + maxRetries = 2 +} +singularity { + enabled = true + cacheDir = '/singularity/cachedir' + autoMounts = true +} +cleanup = true +""" + +INFRA_CUSTOM_SINGULARITY_LOCAL_CONFIG = """\ +params { + config_profile_description = 'A cool description' +} +executor { +} +process { + resourceLimits = [cpus: 8, memory: '32.0GB', time: '19.5h'] + scratch = '/tmp/scratch' + maxRetries = 2 +} +singularity { + enabled = true + cacheDir = '/singularity/cachedir' + autoMounts = true +} +cleanup = true +""" From 967ae26a768935c111cfa80b660af49122c6e954 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 22 May 2026 16:17:54 +1000 Subject: [PATCH 115/121] reformat again and fix ruff check errors --- nf_core/configs/create/__init__.py | 2 +- nf_core/configs/create/basicdetails.py | 12 +++--- nf_core/configs/create/create.py | 2 +- nf_core/configs/create/create.tcss | 2 +- nf_core/configs/create/finalinfradetails.py | 14 ++++--- nf_core/configs/create/hpccustomisation.py | 6 +-- nf_core/configs/create/multiprocessres.py | 10 ++--- nf_core/configs/create/utils.py | 41 +++++++++----------- tests/configs/test_create_app.py | 4 +- tests/configs/test_serial.py | 43 +++++++++++---------- tests/configs/test_validation.py | 32 +++++++++++++-- 11 files changed, 98 insertions(+), 70 deletions(-) diff --git a/nf_core/configs/create/__init__.py b/nf_core/configs/create/__init__.py index b8767a5bc2..01e78dad34 100644 --- a/nf_core/configs/create/__init__.py +++ b/nf_core/configs/create/__init__.py @@ -22,10 +22,10 @@ from nf_core.configs.create.multiprocessres import MultiLabelledProcessConfig, MultiNamedProcessConfig from nf_core.configs.create.nfcorequestion import ChooseNfcoreConfig from nf_core.configs.create.pipelineconfigquestion import PipelineConfigQuestion -from nf_core.configs.create.welcome import WelcomeScreen ## General utilities from nf_core.configs.create.utils import LoggingConsole +from nf_core.configs.create.welcome import WelcomeScreen ## Logging logger = logging.getLogger(__name__) diff --git a/nf_core/configs/create/basicdetails.py b/nf_core/configs/create/basicdetails.py index 2582e64032..d9bd686c6e 100644 --- a/nf_core/configs/create/basicdetails.py +++ b/nf_core/configs/create/basicdetails.py @@ -2,8 +2,9 @@ displaying such info in the pipeline run header on run execution""" from textwrap import dedent -from requests import get +from requests import get +from requests.exceptions import RequestException from textual import on from textual.app import ComposeResult from textual.containers import Center, Horizontal @@ -11,8 +12,7 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown, Select -from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context -from nf_core.configs.create.utils import add_hide_class, remove_hide_class +from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, add_hide_class, init_context, remove_hide_class config_exists_warn = """ > ⚠️ **The config file you are trying to create already exists.** @@ -104,21 +104,21 @@ def get_valid_nfcore_pipelines(self) -> list[str]: url = "https://raw.githubusercontent.com/nf-core/website/refs/heads/main/public/pipeline_names.json" try: response = get(url) - except: + except RequestException: return [] if response.status_code != 200: return [] data = response.json() if not isinstance(data, dict): return [] - if not "pipeline" in data: + if "pipeline" not in data: return [] pipelines = data["pipeline"] if not isinstance(pipelines, list): return [] if not len(pipelines) > 0: return [] - if not all([isinstance(p, str) for p in pipelines]): + if not all(isinstance(p, str) for p in pipelines): return [] return pipelines diff --git a/nf_core/configs/create/create.py b/nf_core/configs/create/create.py index 5f1d5c26f9..6b227f0d2c 100644 --- a/nf_core/configs/create/create.py +++ b/nf_core/configs/create/create.py @@ -10,7 +10,7 @@ class ConfigCreate: - def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir: Path | str = Path(".")): + def __init__(self, template_config: ConfigsCreateConfig, config_type: str, config_dir: Path | str = Path()): self.template_config = template_config self.config_type = config_type config_dir_path = config_dir if isinstance(config_dir, Path) else Path(config_dir) diff --git a/nf_core/configs/create/create.tcss b/nf_core/configs/create/create.tcss index 812d3d72f9..437130a799 100644 --- a/nf_core/configs/create/create.tcss +++ b/nf_core/configs/create/create.tcss @@ -166,4 +166,4 @@ Vertical{ } .ghrepo-cols Button { margin-top: 2; -} \ No newline at end of file +} diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index ad17cb5977..aa51956937 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -7,8 +7,12 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown, Select, Static, Switch -from nf_core.configs.create.utils import SUPPORTED_CONTAINERS, ConfigsCreateConfig, TextInput, init_context -from nf_core.configs.create.utils import add_hide_class, remove_hide_class +from nf_core.configs.create.utils import ( + SUPPORTED_CONTAINERS, + ConfigsCreateConfig, + TextInput, + init_context, +) markdown_intro = """ # Configure the options for your infrastructure config @@ -182,10 +186,10 @@ def _detect_module_system(self) -> bool: return False return True - def _get_set_directory(self, dir: str) -> str | None: + def _get_set_directory(self, directory: str) -> str | None: """Get the available cache directories""" - if dir: - set_dir = os.environ.get(dir) + if directory: + set_dir = os.environ.get(directory) if set_dir: return set_dir return None diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index a9cf38a185..6d007c471c 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -4,7 +4,7 @@ from textual import on from textual.app import ComposeResult -from textual.containers import Center, Horizontal +from textual.containers import Center from textual.screen import Screen from textual.widgets import Button, Footer, Header, Input, Markdown, Select @@ -65,7 +65,7 @@ def compose(self) -> ComposeResult: yield Markdown(markdown_intro) yield Markdown(markdown_scheduler) yield Select( - [(name, keyword) for name, keyword in supported_schedulers.items()], + list(supported_schedulers.items()), prompt="Select your HPC's scheduler.", value=scheduler if scheduler is not None else "local", classes="column", @@ -169,7 +169,7 @@ def _slurm_get_default_queue(self) -> str: config = self._parse_slurm_config(fp) for conf in config: - if (conf["Default"] if "Default" in conf.keys() else "NO") == "YES": + if conf.get("Default", "NO") == "YES": return conf["PartitionName"] # If no default is set, use the first option diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index 95d1942575..ea47611ed2 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -2,10 +2,10 @@ from textual import on from textual.app import ComposeResult -from textual.containers import Center, Horizontal, HorizontalGroup, Vertical, VerticalScroll +from textual.containers import Center, HorizontalGroup, Vertical, VerticalScroll from textual.events import Mount, ScreenResume from textual.screen import Screen -from textual.widgets import Button, Footer, Header, Input, Label, Markdown, Static, Switch +from textual.widgets import Button, Footer, Header, Input, Label, Markdown, Static from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context @@ -66,11 +66,11 @@ def remove_widget(self) -> None: def update_hpc_status(self, hpc: bool) -> None: self.hpc = hpc - id = "custom_process_queue" + field_id = "custom_process_queue" if self.hpc: - self.get_widget_by_id(id).remove_class("hide") + self.get_widget_by_id(field_id).remove_class("hide") else: - self.get_widget_by_id(id).add_class("hide") + self.get_widget_by_id(field_id).add_class("hide") class MultiProcessConfig(Screen): diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index a2a05f0e09..8192d2d5f9 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -17,7 +17,8 @@ from textual.widgets import Input, RichLog, Static # Use ContextVar to define a context on the model initialization -_init_context_var: ContextVar = ContextVar("_init_context_var", default={}) +_init_context_var: ContextVar = ContextVar("_init_context_var", default=None) +_init_context_var.set({}) @contextmanager @@ -298,9 +299,8 @@ def final_path_valid(cls, v: str, info: ValidationInfo) -> str: def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: """Check that an nf-core pipeline name is valid.""" context = info.context - if context and not context["is_infrastructure"] and context["is_nfcore"]: - if v.strip() == "": - raise ValueError("Cannot be left empty.") + if context and not context["is_infrastructure"] and context["is_nfcore"] and v.strip() == "": + raise ValueError("Cannot be left empty.") return v @field_validator("config_profile_contact") @@ -308,9 +308,8 @@ def nfcore_name_valid(cls, v: str, info: ValidationInfo) -> str: def notempty_contact(cls, v: str, info: ValidationInfo) -> str: """Check that contact values are not empty when the config is nf-core.""" context = info.context - if context and context["is_nfcore"]: - if v.strip() == "": - raise ValueError("Cannot be left empty.") + if context and context["is_nfcore"] and v.strip() == "": + raise ValueError("Cannot be left empty.") return v @field_validator( @@ -394,7 +393,7 @@ def pos_integer_valid(cls, v: str, info: ValidationInfo) -> str: try: v_int = int(v.strip()) except ValueError: - raise ValueError("Must be an integer.") + raise ValueError("Must be an integer.") from None if not v_int > 0: raise ValueError("Must be a positive integer.") return v @@ -410,7 +409,7 @@ def non_neg_float_valid(cls, v: str, info: ValidationInfo) -> str: try: vf = float(v.strip()) except ValueError: - raise ValueError("Must be a number.") + raise ValueError("Must be a number.") from None if not vf >= 0: raise ValueError("Must be a non-negative number.") return v @@ -445,7 +444,7 @@ def pos_integer_valid_infra(cls, v: str, info: ValidationInfo) -> str: try: v_int = int(v.strip()) except ValueError: - raise ValueError("Must be an integer.") + raise ValueError("Must be an integer.") from None if not v_int > 0: raise ValueError("Must be a positive integer.") return v @@ -466,7 +465,7 @@ def pos_integer_optional_valid_infra(cls, v: str, info: ValidationInfo) -> str: try: v_int = int(v.strip()) except ValueError: - raise ValueError("Must be an integer.") + raise ValueError("Must be an integer.") from None if not v_int > 0: raise ValueError("Must be a positive integer.") return v @@ -482,7 +481,7 @@ def non_neg_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: try: vf = float(v.strip()) except ValueError: - raise ValueError("Must be a number.") + raise ValueError("Must be a number.") from None if not vf >= 0: raise ValueError("Must be a non-negative number.") return v @@ -498,7 +497,7 @@ def pos_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: try: vf = float(v.strip()) except ValueError: - raise ValueError("Must be a number.") + raise ValueError("Must be a number.") from None if not vf > 0: raise ValueError("Must be a positive number.") return v @@ -508,9 +507,8 @@ def pos_float_valid_infra(cls, v: str, info: ValidationInfo) -> str: def nonemtpy_hpc_details(cls, v: str, info: ValidationInfo) -> str: """Check that HPC infrastructure details are non-empty""" context = info.context - if context and context["is_infrastructure"] and context["is_hpc"]: - if v.strip() == "": - raise ValueError("Cannot be left empty.") + if context and context["is_infrastructure"] and context["is_hpc"] and v.strip() == "": + raise ValueError("Cannot be left empty.") return v @field_validator("scheduler") @@ -518,9 +516,8 @@ def nonemtpy_hpc_details(cls, v: str, info: ValidationInfo) -> str: def valid_scheduler(cls, v: str, info: ValidationInfo) -> str: """Check that the HPC scheduler is supported""" context = info.context - if context and context["is_infrastructure"] and context["is_hpc"]: - if v.strip() not in SUPPORTED_SCHEDULERS: - raise ValueError(f"Must be one of: {', '.join(SUPPORTED_SCHEDULERS)}") + if context and context["is_infrastructure"] and context["is_hpc"] and v.strip() not in SUPPORTED_SCHEDULERS: + raise ValueError(f"Must be one of: {', '.join(SUPPORTED_SCHEDULERS)}") return v @field_validator("cachedir", "scratch_dir") @@ -583,7 +580,7 @@ def pos_hpc_interval_valid(cls, v: str, info: ValidationInfo) -> str: try: vf = float(v.strip()) except ValueError: - raise ValueError("Must be a number.") + raise ValueError("Must be a number.") from None if not vf > 0: raise ValueError("Must be a positive number.") return v @@ -598,7 +595,7 @@ class TextInput(Static): """ def __init__( - self, field_id, placeholder, description, default=None, password=None, suggestions=[], **kwargs + self, field_id, placeholder, description, default=None, password=None, suggestions=None, **kwargs ) -> None: """Initialise the widget with our values. @@ -610,7 +607,7 @@ def __init__( self.description: str = description self.default: str = default self.password: bool = password - self.suggestions: list[str] = suggestions + self.suggestions: list[str] = suggestions or [] def compose(self) -> ComposeResult: yield Grid( diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py index ed8ed836fd..d8820c701a 100644 --- a/tests/configs/test_create_app.py +++ b/tests/configs/test_create_app.py @@ -7,10 +7,10 @@ from nf_core.configs.create.utils import SUPPORTED_CONTAINERS from ..test_configs import ( - INFRA_SINGULARITY_CONFIG, INFRA_CUSTOM_SINGULARITY_CONFIG, - PIPE_NFCORE_CONFIG, + INFRA_SINGULARITY_CONFIG, PIPE_CUSTOM_CONFIG, + PIPE_NFCORE_CONFIG, ) INIT_FILE = "../../nf_core/configs/create/__init__.py" diff --git a/tests/configs/test_serial.py b/tests/configs/test_serial.py index 8119a5d312..7666bba541 100644 --- a/tests/configs/test_serial.py +++ b/tests/configs/test_serial.py @@ -1,20 +1,21 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + +from pydantic import ValidationError + +from nf_core.configs.create.create import ConfigCreate from nf_core.configs.create.serial import NextflowSerial from nf_core.configs.create.utils import ConfigsCreateConfig, init_context -from nf_core.configs.create.create import ConfigCreate from ..test_configs import ( - INFRA_SINGULARITY_CONFIG, INFRA_CUSTOM_SINGULARITY_CONFIG, - PIPE_NFCORE_CONFIG, - PIPE_CUSTOM_CONFIG, - INFRA_NFCORE_SINGULARITY_LOCAL_CONFIG, INFRA_CUSTOM_SINGULARITY_LOCAL_CONFIG, + INFRA_NFCORE_SINGULARITY_LOCAL_CONFIG, + INFRA_SINGULARITY_CONFIG, + PIPE_CUSTOM_CONFIG, + PIPE_NFCORE_CONFIG, ) -from pydantic import ValidationError -from pathlib import Path -from tempfile import TemporaryDirectory - # Valid configs VALID_NFCORE_INFRA_HPC_CONFIG = { @@ -587,7 +588,7 @@ def test_serial_invalid_nfcore_infra_hpc(): expected_error = None try: - c = ConfigsCreateConfig(**INVALID_NFCORE_INFRA_HPC_CONFIG) + ConfigsCreateConfig(**INVALID_NFCORE_INFRA_HPC_CONFIG) except ValidationError as e: expected_error = e @@ -601,7 +602,7 @@ def test_serial_invalid_nfcore_infra_hpc(): "Value error, Must be one of: local, pbs, pbspro, slurm, sge", "Value error, Must be an integer.", } - assert set([e["msg"] for e in errors]) == expected_error_msgs + assert {e["msg"] for e in errors} == expected_error_msgs def test_serial_invalid_custom_infra_hpc(): @@ -615,7 +616,7 @@ def test_serial_invalid_custom_infra_hpc(): expected_error = None try: - c = ConfigsCreateConfig(**INVALID_CUSTOM_INFRA_HPC_CONFIG) + ConfigsCreateConfig(**INVALID_CUSTOM_INFRA_HPC_CONFIG) except ValidationError as e: expected_error = e @@ -627,7 +628,7 @@ def test_serial_invalid_custom_infra_hpc(): "Value error, Must be one of: local, pbs, pbspro, slurm, sge", "Value error, Must be an absolute path (/data/scratch), a path relative to home (~/scratch), or a path with an environmental variable (e.g. ${DIR}/scratch)", } - assert set([e["msg"] for e in errors]) == expected_error_msgs + assert {e["msg"] for e in errors} == expected_error_msgs def test_serial_invalid_nfcore_pipe_hpc(): @@ -641,7 +642,7 @@ def test_serial_invalid_nfcore_pipe_hpc(): expected_error = None try: - c = ConfigsCreateConfig(**INVALID_NFCORE_PIPE_HPC_CONFIG) + ConfigsCreateConfig(**INVALID_NFCORE_PIPE_HPC_CONFIG) except ValidationError as e: expected_error = e @@ -652,7 +653,7 @@ def test_serial_invalid_nfcore_pipe_hpc(): "Value error, Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com).", "Value error, Cannot be left empty.", } - assert set([e["msg"] for e in errors]) == expected_error_msgs + assert {e["msg"] for e in errors} == expected_error_msgs def test_serial_invalid_custom_pipe_hpc(): @@ -666,7 +667,7 @@ def test_serial_invalid_custom_pipe_hpc(): expected_error = None try: - c = ConfigsCreateConfig(**INVALID_CUSTOM_PIPE_HPC_CONFIG) + ConfigsCreateConfig(**INVALID_CUSTOM_PIPE_HPC_CONFIG) except ValidationError as e: expected_error = e @@ -678,7 +679,7 @@ def test_serial_invalid_custom_pipe_hpc(): "Value error, Must be a number.", "Value error, Must be a valid path to a directory.", } - assert set([e["msg"] for e in errors]) == expected_error_msgs + assert {e["msg"] for e in errors} == expected_error_msgs def test_serial_invalid_nfcore_infra_local(): @@ -692,7 +693,7 @@ def test_serial_invalid_nfcore_infra_local(): expected_error = None try: - c = ConfigsCreateConfig(**INVALID_NFCORE_INFRA_LOCAL_CONFIG) + ConfigsCreateConfig(**INVALID_NFCORE_INFRA_LOCAL_CONFIG) except ValidationError as e: expected_error = e @@ -703,7 +704,7 @@ def test_serial_invalid_nfcore_infra_local(): "Value error, Handle must be a valid URL starting with 'https://' or 'http://' and include the domain (e.g. .com).", "Value error, Must be a valid path to a directory.", } - assert set([e["msg"] for e in errors]) == expected_error_msgs + assert {e["msg"] for e in errors} == expected_error_msgs def test_serial_invalid_custom_infra_local(): @@ -717,7 +718,7 @@ def test_serial_invalid_custom_infra_local(): expected_error = None try: - c = ConfigsCreateConfig(**INVALID_CUSTOM_INFRA_LOCAL_CONFIG) + ConfigsCreateConfig(**INVALID_CUSTOM_INFRA_LOCAL_CONFIG) except ValidationError as e: expected_error = e @@ -730,4 +731,4 @@ def test_serial_invalid_custom_infra_local(): "Value error, Must be one of: singularity, docker, apptainer, charliecloud, podman, sarus, shifter, conda", "Value error, Must be an absolute path (/data/scratch), a path relative to home (~/scratch), or a path with an environmental variable (e.g. ${DIR}/scratch)", } - assert set([e["msg"] for e in errors]) == expected_error_msgs + assert {e["msg"] for e in errors} == expected_error_msgs diff --git a/tests/configs/test_validation.py b/tests/configs/test_validation.py index 68719fd5e8..7798c64eac 100644 --- a/tests/configs/test_validation.py +++ b/tests/configs/test_validation.py @@ -1,8 +1,8 @@ -from nf_core.configs.create.utils import ConfigsCreateConfig, init_context, SUPPORTED_SCHEDULERS, SUPPORTED_CONTAINERS +from enum import Enum from pydantic_core._pydantic_core import ValidationError -from enum import Enum +from nf_core.configs.create.utils import SUPPORTED_CONTAINERS, SUPPORTED_SCHEDULERS, ConfigsCreateConfig, init_context class Context(Enum): @@ -48,7 +48,7 @@ class Context(Enum): } -def check_config(cfg: dict, fail: bool, ctx: dict = {}) -> None: +def check_config(cfg: dict, fail: bool, ctx: dict) -> None: assert isinstance(cfg, dict) assert isinstance(fail, bool) assert isinstance(ctx, dict) @@ -415,6 +415,32 @@ def test_proc_label(): cfg_valid_custom = {"custom_process_label_id": "ANOTHER_LABEL"} cfg_empty = {"custom_process_label_id": ""} + # Test nf-core pipeline context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.NF_PIPE_LOCAL.value) + # Should fail + check_config(cfg_valid_custom, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + check_config(cfg_empty, fail=True, ctx=Context.NF_PIPE_LOCAL.value) + + # Test custom pipeline context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.CUSTOM_PIPE_LOCAL.value) + # Should fail + check_config(cfg_empty, fail=True, ctx=Context.CUSTOM_PIPE_LOCAL.value) + + # Test nf-core infra context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.NF_INFRA_LOCAL.value) + + # Test custom infra context + # Should succeed + check_config(cfg_valid_nfcore, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_valid_custom, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + check_config(cfg_empty, fail=False, ctx=Context.CUSTOM_INFRA_LOCAL.value) + def test_proc_queue(): cfg_valid = {"custom_process_queue": "somequeue"} From 200d8627111a4a7af3ff8359d70833455f6a24a4 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 22 May 2026 16:30:07 +1000 Subject: [PATCH 116/121] resize infra final details screen in test --- tests/configs/test_create_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/configs/test_create_app.py b/tests/configs/test_create_app.py index d8820c701a..6f6e6d39f2 100644 --- a/tests/configs/test_create_app.py +++ b/tests/configs/test_create_app.py @@ -211,7 +211,7 @@ async def run_before(pilot) -> None: await pilot.press(*list("singularity")) await pilot.press("enter") - assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) def enter_infra_final_details(container): @@ -497,7 +497,7 @@ async def run_before(pilot) -> None: await pilot.press(*list("singularity")) await pilot.press("enter") - assert snap_compare(INIT_FILE, terminal_size=(100, 50), run_before=run_before) + assert snap_compare(INIT_FILE, terminal_size=(100, 100), run_before=run_before) def enter_infra_custom_final_details(container): From fb8d65a214602a8221225a2d3e1bbcd8ae5378f8 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 22 May 2026 16:33:16 +1000 Subject: [PATCH 117/121] update snapshots --- ...st_submitting_infra_custom_hpc_details.svg | 275 ++++++++++++++--- ...st_submitting_infra_nfcore_hpc_details.svg | 279 +++++++++++++++--- 2 files changed, 486 insertions(+), 68 deletions(-) diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg index f1be698f9e..99bb662059 100644 --- a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_custom_hpc_details.svg @@ -1,4 +1,4 @@ - + - + @@ -201,9 +210,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - nf-core configs create + nf-core configs create @@ -211,47 +370,47 @@ - + - nf-core configs create — Create a new nextflow config with an interactive inter… + nf-core configs create — Create a new nextflow config with an interactive interfa… -Configure the options for your infrastructure config +Configure the options for your infrastructure config Select the container/software management system available on your infrastructure. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -singularity -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Set maximum available resources The following fields let you set the maximum available resrouces on your infrastructure. -Memory, CPUs, and time must be filled out and all processes run with this configuration will -be capped at these values. +Memory, CPUs, and time must be filled out and all processes run with this configuration will be +capped at these values. -The queue size, poll interval, and submit rate fields are optional, since Nextflow has -built-in defaults for these values. Consult the Nextflow documentation for further details on -these default values. +The queue size, poll interval, and submit rate fields are optional, since Nextflow has built-in +defaults for these values. Consult the Nextflow documentation for further details on these +default values. -Maximum memory (GB)Maximum number of CPUsMaximum time (hours) -available on youravailable on youravailable to jobs on your -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -MemoryCPUsTime▄▄ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) available +on your infrastructureavailable on yourto jobs on your infrastructure +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +MemoryCPUsTime +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that -can be submittedcheck for successful processcan be submitted per minute -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Queue sizePoll intervalJobs per minutes -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Queue sizePoll intervalJobs per minutes +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -261,8 +420,58 @@ The following fields let you define global directories for a container/conda image cache, an iGenomes cache, and a scratch directory in which to run your jobs. -Each field is optional, but must contain the full (absolute) path to these directories if - d Toggle dark mode  q Quit ^p palette +Each field is optional, but must contain the full (absolute) path to these directories if +specified. They may also contain references to environment variables (e.g. $CACHEDIR) and may +use the ~ symbol to refer to your home directory. + + + +If you have a global container/conda cache directory, specify the **full path** here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/path/to/cache/dir +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you want your jobs to run within a scratch directory, specify the full path here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Scratch directory +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔Clean up work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run (prevents resume functionality). + + + +Specify the number of retries for a failed job. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + d Toggle dark mode  q Quit ^p palette diff --git a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg index fd119350c8..28e4c03270 100644 --- a/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg +++ b/tests/configs/__snapshots__/test_create_app/test_submitting_infra_nfcore_hpc_details.svg @@ -1,4 +1,4 @@ - + - + @@ -201,9 +210,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - nf-core configs create + nf-core configs create @@ -211,47 +370,47 @@ - + - nf-core configs create — Create a new nextflow config with an interactive inter… + nf-core configs create — Create a new nextflow config with an interactive interfa… -Configure the options for your infrastructure config +Configure the options for your infrastructure config Select the container/software management system available on your infrastructure. -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -singularity -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +singularity +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Set maximum available resources The following fields let you set the maximum available resrouces on your infrastructure. -Memory, CPUs, and time must be filled out and all processes run with this configuration will -be capped at these values. +Memory, CPUs, and time must be filled out and all processes run with this configuration will be +capped at these values. -The queue size, poll interval, and submit rate fields are optional, since Nextflow has -built-in defaults for these values. Consult the Nextflow documentation for further details on -these default values. +The queue size, poll interval, and submit rate fields are optional, since Nextflow has built-in +defaults for these values. Consult the Nextflow documentation for further details on these +default values. -Maximum memory (GB)Maximum number of CPUsMaximum time (hours)▁▁ -available on youravailable on youravailable to jobs on your -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -MemoryCPUsTime -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Maximum memory (GB) availableMaximum number of CPUsMaximum time (hours) available +on your infrastructureavailable on yourto jobs on your infrastructure +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +MemoryCPUsTime +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that -can be submittedcheck for successful processcan be submitted per minute -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Queue sizePoll intervalJobs per minutes -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Maximum number of jobs thatHow often (in minutes) toMaximum number of jobs that +can be submittedcheck for successful processcan be submitted per minute +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Queue sizePoll intervalJobs per minutes +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -261,8 +420,58 @@ The following fields let you define global directories for a container/conda image cache, an iGenomes cache, and a scratch directory in which to run your jobs. -Each field is optional, but must contain the full (absolute) path to these directories if - d Toggle dark mode  q Quit ^p palette +Each field is optional, but must contain the full (absolute) path to these directories if +specified. They may also contain references to environment variables (e.g. $CACHEDIR) and may +use the ~ symbol to refer to your home directory. + + + +If you have a global container/conda cache directory, specify the **full path** here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +/path/to/cache/dir +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you have an iGenomes cache directory, specify the **full path** here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +iGenomes cache directory +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +If you want your jobs to run within a scratch directory, specify the full path here. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Scratch directory +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔Clean up work directory +Select if you want to delete the files in the work/ directory on successful +▁▁▁▁▁▁▁▁completion of a run (prevents resume functionality). + + + +Specify the number of retries for a failed job. + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +1                                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Back  Finish  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + d Toggle dark mode  q Quit ^p palette From 3393082f89f5c5d2c72eb45e36d2664b0a293d29 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Thu, 9 Jul 2026 12:02:37 +1000 Subject: [PATCH 118/121] remove empty config sections, clean up resource request formatting --- nf_core/configs/create/utils.py | 66 +++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 8192d2d5f9..57991de69c 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -4,6 +4,7 @@ from collections.abc import Iterator from contextlib import contextmanager from contextvars import ContextVar +from math import ceil from pathlib import Path from typing import Any @@ -130,6 +131,47 @@ def __init__(self, /, **data: Any) -> None: context=_init_context_var.get(), ) + def _remove_empty_sections(self, config_dict: dict): + # Takes a config dict produced by serial_hpc() or serial_pipeline() + # and removes keys with null or empty values + ret = {} + for k, v in config_dict.items(): + if isinstance(v, dict): + v2 = self._remove_empty_sections(v) + else: + v2 = v + if v2: + ret[k] = v2 + return ret + + def _format_resource_request(self, value_str: str, unit_str: str) -> str: + # Format a resource request to an integer + # and update the units if required + # E.g. 1.0h -> 1h; 1.0GB -> 1GB + # and 0.5h -> 30min; 0.5GB -> 512MB + + # Make sure unit_str is valid + assert unit_str in ['h', 'min', 'GB'], f'Invalid unit: "{unit_str}"' + + # Turn the value string into a float + v = float(value_str) + u = unit_str + if v.is_integer(): + v = int(v) + if v == 0: + v = 1 + elif v < 1: + if unit_str == 'min': + v = ceil(v * 60) + u = 's' + elif unit_str == 'h': + v = ceil(v * 60) + u = 'min' + elif unit_str == 'GB': + v = ceil(v * 1024) + u = 'MB' + return f'{v} {u}' + def serial_params(self): # Determine contact info contact = "" @@ -149,7 +191,7 @@ def serial_params(self): "igenomes_base": self.igenomes_cachedir or None, }, } - return ret + return self._remove_empty_sections(ret) def serial_hpc(self): """Returns a dictionary of the config""" @@ -164,18 +206,18 @@ def serial_hpc(self): ret = { **params, "executor": { - "queueStatInterval": str(float(self.queue_stat_interval)) + "m" if self.queue_stat_interval else None, + "queueStatInterval": self._format_resource_request(self.queue_stat_interval, 'min') if self.queue_stat_interval else None, "queueSize": int(self.queue_size) if self.queue_size else None, - "pollInterval": str(float(self.poll_interval)) + "m" if self.poll_interval else None, - "submitRateLimit": str(int(self.submit_rate)) + "min" if self.submit_rate else None, + "pollInterval": self._format_resource_request(self.poll_interval, 'min') if self.poll_interval else None, + "submitRateLimit": self._format_resource_request(self.submit_rate, "min") if self.submit_rate else None, }, "process": { "executor": self.scheduler or None, "queue": self.queue or None, "resourceLimits": [ {"cpus": int(self.cpus) if self.cpus else None}, - {"memory": str(float(self.memory)) + "GB" if self.memory else None}, - {"time": str(float(self.time)) + "h" if self.time else None}, + {"memory": self._format_resource_request(self.memory, 'GB') if self.memory else None}, + {"time": self._format_resource_request(self.time, 'h') if self.time else None}, ], "scratch": self.scratch_dir or None, "maxRetries": int(self.retries) if self.retries else None, @@ -191,7 +233,7 @@ def serial_hpc(self): "cleanup": self.delete_work_dir, } - return ret + return self._remove_empty_sections(ret) def serial_pipeline(self): """Returns a dictionary of the pipeline config""" @@ -201,8 +243,8 @@ def serial_pipeline(self): **params, "process": { "cpus": int(self.default_process_ncpus) if self.default_process_ncpus else None, - "memory": str(float(self.default_process_memgb)) + "GB" if self.default_process_memgb else None, - "time": str(float(self.default_process_hours)) + "h" if self.default_process_hours else None, + "memory": self._format_resource_request(self.default_process_memgb, 'GB') if self.default_process_memgb else None, + "time": self._format_resource_request(self.default_process_hours, 'h') if self.default_process_hours else None, }, } # Get custom process resources @@ -220,12 +262,12 @@ def serial_pipeline(self): else None ), "memory": ( - str(float(process_resources["custom_process_memgb"])) + "GB" + self._format_resource_request(process_resources["custom_process_memgb"], 'GB') if process_resources["custom_process_memgb"] else None ), "time": ( - str(float(process_resources["custom_process_hours"])) + "h" + self._format_resource_request(process_resources["custom_process_hours"], 'h') if process_resources["custom_process_hours"] else None ), @@ -238,7 +280,7 @@ def serial_pipeline(self): ret["process"][f"{selector}: '{process_id}'"]["executor"] = ( process_resources["executor"] if process_resources["executor"] else None ) - return ret + return self._remove_empty_sections(ret) def serial(self): if self.is_infrastructure: From d67ba056ee95f384d0e4c91027d6724d2b2f2099 Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Thu, 9 Jul 2026 02:04:22 +0000 Subject: [PATCH 119/121] [automated] Fix code linting --- nf_core/configs/create/utils.py | 40 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 57991de69c..10c93ee916 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -151,7 +151,7 @@ def _format_resource_request(self, value_str: str, unit_str: str) -> str: # and 0.5h -> 30min; 0.5GB -> 512MB # Make sure unit_str is valid - assert unit_str in ['h', 'min', 'GB'], f'Invalid unit: "{unit_str}"' + assert unit_str in ["h", "min", "GB"], f'Invalid unit: "{unit_str}"' # Turn the value string into a float v = float(value_str) @@ -161,16 +161,16 @@ def _format_resource_request(self, value_str: str, unit_str: str) -> str: if v == 0: v = 1 elif v < 1: - if unit_str == 'min': + if unit_str == "min": v = ceil(v * 60) - u = 's' - elif unit_str == 'h': + u = "s" + elif unit_str == "h": v = ceil(v * 60) - u = 'min' - elif unit_str == 'GB': + u = "min" + elif unit_str == "GB": v = ceil(v * 1024) - u = 'MB' - return f'{v} {u}' + u = "MB" + return f"{v} {u}" def serial_params(self): # Determine contact info @@ -206,9 +206,13 @@ def serial_hpc(self): ret = { **params, "executor": { - "queueStatInterval": self._format_resource_request(self.queue_stat_interval, 'min') if self.queue_stat_interval else None, + "queueStatInterval": self._format_resource_request(self.queue_stat_interval, "min") + if self.queue_stat_interval + else None, "queueSize": int(self.queue_size) if self.queue_size else None, - "pollInterval": self._format_resource_request(self.poll_interval, 'min') if self.poll_interval else None, + "pollInterval": self._format_resource_request(self.poll_interval, "min") + if self.poll_interval + else None, "submitRateLimit": self._format_resource_request(self.submit_rate, "min") if self.submit_rate else None, }, "process": { @@ -216,8 +220,8 @@ def serial_hpc(self): "queue": self.queue or None, "resourceLimits": [ {"cpus": int(self.cpus) if self.cpus else None}, - {"memory": self._format_resource_request(self.memory, 'GB') if self.memory else None}, - {"time": self._format_resource_request(self.time, 'h') if self.time else None}, + {"memory": self._format_resource_request(self.memory, "GB") if self.memory else None}, + {"time": self._format_resource_request(self.time, "h") if self.time else None}, ], "scratch": self.scratch_dir or None, "maxRetries": int(self.retries) if self.retries else None, @@ -243,8 +247,12 @@ def serial_pipeline(self): **params, "process": { "cpus": int(self.default_process_ncpus) if self.default_process_ncpus else None, - "memory": self._format_resource_request(self.default_process_memgb, 'GB') if self.default_process_memgb else None, - "time": self._format_resource_request(self.default_process_hours, 'h') if self.default_process_hours else None, + "memory": self._format_resource_request(self.default_process_memgb, "GB") + if self.default_process_memgb + else None, + "time": self._format_resource_request(self.default_process_hours, "h") + if self.default_process_hours + else None, }, } # Get custom process resources @@ -262,12 +270,12 @@ def serial_pipeline(self): else None ), "memory": ( - self._format_resource_request(process_resources["custom_process_memgb"], 'GB') + self._format_resource_request(process_resources["custom_process_memgb"], "GB") if process_resources["custom_process_memgb"] else None ), "time": ( - self._format_resource_request(process_resources["custom_process_hours"], 'h') + self._format_resource_request(process_resources["custom_process_hours"], "h") if process_resources["custom_process_hours"] else None ), From b7d274249279d8294d5aafb3eae4939d491d8698 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Fri, 10 Jul 2026 17:25:07 +1000 Subject: [PATCH 120/121] update wording, WIP updating optional/required field markers in finalinfradetails --- nf_core/configs/create/configtype.py | 4 ++- nf_core/configs/create/finalinfradetails.py | 29 +++++++++++++++------ nf_core/configs/create/hpccustomisation.py | 8 +++--- nf_core/configs/create/hpcquestion.py | 6 +++++ nf_core/configs/create/nfcorequestion.py | 2 +- nf_core/configs/create/welcome.py | 4 +-- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/nf_core/configs/create/configtype.py b/nf_core/configs/create/configtype.py index c95ca0abfd..cc7f2c9e88 100644 --- a/nf_core/configs/create/configtype.py +++ b/nf_core/configs/create/configtype.py @@ -27,6 +27,7 @@ _Infrastructure_ configs: +- Are pipeline agnostic. - Describe the basic necessary information for any nf-core pipeline to execute. - Define things such as which container engine to use, if there is a scheduler and @@ -35,11 +36,12 @@ - Can be uploaded to [nf-core configs](https://github.com/nf-core/tools/configs) to be directly accessible in a nf-core pipeline with `-profile `. -- Are not used to tweak specific parts of a given pipeline (such as a process or +- Are not used to adjust scientific parameters of a given pipeline (such as a process or module). _Pipeline_ configs +- Are pipeline specific. - Are config files that target specific component of a particular pipeline or pipeline run. - Example: you have a particular step of the pipeline that often runs out. of memory using the pipeline's default settings. You would use this config to diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index aa51956937..7ae414e0f2 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -21,13 +21,25 @@ markdown_max_resources = """ ## Set maximum available resources +### REQUIRED + The following fields let you set the maximum available resrouces -on your infrastructure. +**across all compute nodes** on your infrastructure. Memory, CPUs, and time must be filled out and all processes run with this configuration will be capped at these values. +""" + +markdown_queue_info = """ +## Set job queue information + +### OPTIONAL + +The queue size, poll interval, and submit rate fields specify how many jobs +can be run at once, how frequently Nextflow checks for job termination, and +how quickly new jobs can be submitted. -The queue size, poll interval, and submit rate fields are optional, since +These fields are optional, since Nextflow has built-in defaults for these values. Consult the Nextflow documentation for further details on these default values. """ @@ -74,38 +86,39 @@ def compose(self) -> ComposeResult: yield TextInput( "memory", "Memory", - "Maximum memory (GB) available on your infrastructure (across all nodes).", + "Max. memory (GB) available on your infrastructure.", classes="column", ) yield TextInput( "cpus", "CPUs", - "Maximum number of CPUs available on your infrastructure (across all nodes).", + "Max. number of CPUs available on your infrastructure.", classes="column", ) yield TextInput( "time", "Time", - "Maximum time (hours) available to jobs on your infrastructure (across all nodes).", + "Max. time (hours) available to jobs on your infrastructure.", classes="column", ) + yield Markdown(markdown_queue_info) with Horizontal(): yield TextInput( "queue_size", "Queue size", - "Maximum number of jobs that can be submitted simultaneously on your infrastructure (optional).", + "Max. number of jobs that can be submitted simultaneously on your infrastructure.", classes="column", ) yield TextInput( "poll_interval", "Poll interval", - "How often (in minutes) to check for successful process completion (optional).", + "How often (in minutes) to check for job completion.", classes="column", ) yield TextInput( "submit_rate", "Jobs per minutes", - "Maximum number of jobs that can be submitted per minute (optional).", + "Max. number of jobs that can be submitted per minute.", classes="column", ) yield Markdown(markdown_global_dirs) diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 6d007c471c..52b467b3ca 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -74,16 +74,16 @@ def compose(self) -> ComposeResult: yield Markdown(markdown_queue) yield TextInput( "queue", - "Queue", - "The default queue in your HPC (if required).", + "Queue name", + "The default queue in your HPC (leave blank if not required).", default=default_queue if default_queue else "", classes="column", suggestions=queues, ) yield TextInput( "queue_stat_interval", - "Queue stat interval", - "How often to get the queue status from the scheduler (minutes).", + "Queue stat interval (minutes)", + "How often (in minutes) to get the queue status from the scheduler (optional).", classes="column", ) yield TextInput( diff --git a/nf_core/configs/create/hpcquestion.py b/nf_core/configs/create/hpcquestion.py index 6556c2d9cd..9e2b951fb2 100644 --- a/nf_core/configs/create/hpcquestion.py +++ b/nf_core/configs/create/hpcquestion.py @@ -29,6 +29,11 @@ * Select if you need to load other modules """ +cloud_note = """ +**Note:** Currently, only local and HPC executor configurations are supported. +Support for cloud executor configuration will be supported in a future release. +""" + class ChooseHpc(Screen): """Choose whether this will be a config for an HPC or not.""" @@ -49,4 +54,5 @@ def compose(self) -> ComposeResult: classes="col-2 pipeline-type-grid", ) yield Markdown(markdown_details) + yield Markdown(cloud_note) yield Center(Button("Back", id="back", variant="default"), classes="cta") diff --git a/nf_core/configs/create/nfcorequestion.py b/nf_core/configs/create/nfcorequestion.py index f93ad035a9..fa9eac382a 100644 --- a/nf_core/configs/create/nfcorequestion.py +++ b/nf_core/configs/create/nfcorequestion.py @@ -4,7 +4,7 @@ from textual.widgets import Button, Footer, Header, Markdown markdown_intro = """ -# Is this configuration file part of the nf-core organisation? +# Will this configuration file be part of the nf-core organisation? """ markdown_type_nfcore = """ diff --git a/nf_core/configs/create/welcome.py b/nf_core/configs/create/welcome.py index 7bca8100d0..aae51efde3 100644 --- a/nf_core/configs/create/welcome.py +++ b/nf_core/configs/create/welcome.py @@ -10,8 +10,8 @@ markdown = """ # Welcome to the nf-core config creation wizard -This app will help you create **Nextflow configuration files** -for both: +This app will help you create **nf-core compatible** Nextflow configuration +files for both: - **Infrastructure** configs for defining computing environment for all pipelines, and From e810969433c8ed8d0f0900716d91c664b9efefe7 Mon Sep 17 00:00:00 2001 From: mgeaghan Date: Mon, 13 Jul 2026 17:42:47 +1000 Subject: [PATCH 121/121] fix typos, improve explanatory text, make cachedir dynamic --- nf_core/configs/create/configtype.py | 2 +- nf_core/configs/create/defaultprocessres.py | 25 ++++++-- nf_core/configs/create/finalinfradetails.py | 31 +++++---- nf_core/configs/create/hpccustomisation.py | 6 +- nf_core/configs/create/multiprocessres.py | 63 +++++++++++++++---- .../configs/create/pipelineconfigquestion.py | 25 +++++++- nf_core/configs/create/utils.py | 3 +- 7 files changed, 120 insertions(+), 35 deletions(-) diff --git a/nf_core/configs/create/configtype.py b/nf_core/configs/create/configtype.py index cc7f2c9e88..ad1c0bdca4 100644 --- a/nf_core/configs/create/configtype.py +++ b/nf_core/configs/create/configtype.py @@ -43,7 +43,7 @@ - Are pipeline specific. - Are config files that target specific component of a particular pipeline or pipeline run. - - Example: you have a particular step of the pipeline that often runs out. + - Example: you have a particular step of the pipeline that often runs out of memory using the pipeline's default settings. You would use this config to increase the amount of memory Nextflow supplies that given task. - Are normally only used by a _single or small group_ of users. diff --git a/nf_core/configs/create/defaultprocessres.py b/nf_core/configs/create/defaultprocessres.py index 2d9001f2de..a1a1b51106 100644 --- a/nf_core/configs/create/defaultprocessres.py +++ b/nf_core/configs/create/defaultprocessres.py @@ -10,6 +10,20 @@ from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context +markdown_explain = """ +Use the following fields to specify the **default** number of +CPUs, memory (in GB) and walltime (in hours) for your pipeline. + +These values will be used by any process that haven't been specifically configured +to request a particular resource. For example, if a process is configured to +request 2 CPUs and 8GB of memory, but no walltime is specified, +the walltime value below will be used. + +All values are optional, but recommended. + +Use the `Skip` button to skip setting default resource values. +""" + class DefaultProcess(Screen): """Get default process resource requirements.""" @@ -24,25 +38,26 @@ def compose(self) -> ComposeResult: """ ) ) + yield Markdown(markdown_explain) yield TextInput( "default_process_ncpus", - "1", + "CPUs (OPTIONAL)", "Number of CPUs to use by default for all processes.", "1", classes="column", ) yield TextInput( "default_process_memgb", - "6", + "Memory (GB) (OPTIONAL)", "Amount of memory in GB to use by default for all processes.", - "6", + "2", classes="column", ) yield TextInput( "default_process_hours", - "4", + "Time (hours) (OPTIONAL)", "The default number of hours of walltime required for processes:", - "4", + "1", classes="column", ) yield Center( diff --git a/nf_core/configs/create/finalinfradetails.py b/nf_core/configs/create/finalinfradetails.py index 7ae414e0f2..073ea2b7b1 100644 --- a/nf_core/configs/create/finalinfradetails.py +++ b/nf_core/configs/create/finalinfradetails.py @@ -8,10 +8,13 @@ from textual.widgets import Button, Footer, Header, Input, Markdown, Select, Static, Switch from nf_core.configs.create.utils import ( + CACHED_CONTAINERS, SUPPORTED_CONTAINERS, ConfigsCreateConfig, TextInput, + add_hide_class, init_context, + remove_hide_class, ) markdown_intro = """ @@ -68,6 +71,7 @@ def __init__(self, *args, **kwargs): self.cache_dirs = { self.container_system: self._get_container_cache_directory(), } + self.cache_environment = self.container_system in CACHED_CONTAINERS def compose(self) -> ComposeResult: yield Header() @@ -85,19 +89,19 @@ def compose(self) -> ComposeResult: with Horizontal(): yield TextInput( "memory", - "Memory", + "Memory (REQUIRED)", "Max. memory (GB) available on your infrastructure.", classes="column", ) yield TextInput( "cpus", - "CPUs", + "CPUs (REQUIRED)", "Max. number of CPUs available on your infrastructure.", classes="column", ) yield TextInput( "time", - "Time", + "Time (REQUIRED)", "Max. time (hours) available to jobs on your infrastructure.", classes="column", ) @@ -105,39 +109,39 @@ def compose(self) -> ComposeResult: with Horizontal(): yield TextInput( "queue_size", - "Queue size", + "Queue size (OPTIONAL)", "Max. number of jobs that can be submitted simultaneously on your infrastructure.", classes="column", ) yield TextInput( "poll_interval", - "Poll interval", + "Poll interval (OPTIONAL)", "How often (in minutes) to check for job completion.", classes="column", ) yield TextInput( "submit_rate", - "Jobs per minutes", + "Jobs per minutes (OPTIONAL)", "Max. number of jobs that can be submitted per minute.", classes="column", ) yield Markdown(markdown_global_dirs) yield TextInput( "cachedir", - "/path/to/cache/dir", + "/path/to/cache/dir (OPTIONAL)", "If you have a global container/conda cache directory, specify the **full path** here.", - classes="", + classes="hide" if not self.cache_environment else "", default=self._get_container_cache_directory(), ) yield TextInput( "igenomes_cachedir", - "iGenomes cache directory", + "iGenomes cache directory (OPTIONAL)", "If you have an iGenomes cache directory, specify the **full path** here.", classes="hide" if not self.parent.NFCORE_CONFIG else "", ) yield TextInput( "scratch_dir", - "Scratch directory", + "Scratch directory (OPTIONAL)", "If you want your jobs to run within a scratch directory, specify the full path here.", classes="", ) @@ -152,7 +156,7 @@ def compose(self) -> ComposeResult: yield TextInput( "retries", "Number of retries", - "Specify the number of retries for a failed job.", + "Specify how many times Nextflow will retry a failed task before the pipeline fails.", default="1", classes="", ) @@ -242,6 +246,11 @@ def get_container_system(self, event: Select.Changed) -> None: cachedir_input = cachedir_text_input.query_one(Input) if self.container_system: cachedir_input.value = self.get_container_cache_directory() + self.cache_environment = self.container_system in CACHED_CONTAINERS + if self.cache_environment: + remove_hide_class(self.parent, "cachedir") + else: + add_hide_class(self.parent, "cachedir") @on(Button.Pressed, "#finish") def on_finish_button(self, event: Button.Pressed) -> None: diff --git a/nf_core/configs/create/hpccustomisation.py b/nf_core/configs/create/hpccustomisation.py index 52b467b3ca..339acecf8b 100644 --- a/nf_core/configs/create/hpccustomisation.py +++ b/nf_core/configs/create/hpccustomisation.py @@ -74,7 +74,7 @@ def compose(self) -> ComposeResult: yield Markdown(markdown_queue) yield TextInput( "queue", - "Queue name", + "Queue name (OPTIONAL)", "The default queue in your HPC (leave blank if not required).", default=default_queue if default_queue else "", classes="column", @@ -82,13 +82,13 @@ def compose(self) -> ComposeResult: ) yield TextInput( "queue_stat_interval", - "Queue stat interval (minutes)", + "Queue stat interval (minutes) (OPTIONAL)", "How often (in minutes) to get the queue status from the scheduler (optional).", classes="column", ) yield TextInput( "module_system", - "Other modules to load", + "Other modules to load (OPTIONAL)", "Do you need to load other software using the module system for your compute nodes? Separate multiple modules by spaces.", classes="hide" if not module_system_used else "", ) diff --git a/nf_core/configs/create/multiprocessres.py b/nf_core/configs/create/multiprocessres.py index ea47611ed2..071af78e7f 100644 --- a/nf_core/configs/create/multiprocessres.py +++ b/nf_core/configs/create/multiprocessres.py @@ -9,6 +9,42 @@ from nf_core.configs.create.utils import ConfigsCreateConfig, TextInput, init_context +markdown_process_name = """ +Use the following fields to configure the basic resource requirements +for your pipeline's processes. The only required field is the process name; +all other fields can be left blank to use the pipeline or infrastructure defaults. +""" + +markdown_process_label = """ +Use the following fields to configure the basic resource requirements +for your pipeline's labels. The only required field is the process label; +all other fields can be left blank to use the pipeline or infrastructure defaults. +""" + +markdown_process_common = """ +Use the `Add another process` button to configure additional processes. + +Use the read `—` buttons to remove a process configuration from the list. +""" + +markdown_skip_name = """ +Use the `Skip` button to skip setting process-specific resource configurations. +""" + +markdown_skip_label = """ +Use the `Skip` button to skip setting label-specific resource configurations. +""" + +markdown_warn_name = """ +**NOTE:** Process names are **not** checked for validity. +Please verify that the name is correct before continuing. +""" + +markdown_warn_label = """ +**NOTE:** Process labels are **not** checked for validity. +Please verify that the label is correct before continuing. +""" + class ProcessConfig(HorizontalGroup): """Get resource requirements for a single process.""" @@ -22,42 +58,42 @@ def __init__(self, selector: str, hpc: bool) -> None: def compose(self) -> ComposeResult: yield TextInput( f"custom_process_{self.selector}_id", - "", + f"Process {self.selector} (REQUIRED)", f"Process {self.selector}:", "", classes="custom-process-id", ) yield TextInput( "custom_process_ncpus", - "1", + "CPUs", "# CPUs:", - "1", + "", classes="custom-process-number", ) yield TextInput( "custom_process_memgb", - "6", + "Mem", "Memory (GB):", - "6", + "", classes="custom-process-number", ) yield TextInput( "custom_process_hours", - "4", + "Time", "Walltime (hours):", - "4", + "", classes="custom-process-number", ) yield TextInput( "custom_process_queue", - "queue name", + "queue name (OPTIONAL)", "HPC queue (optional):", "", classes=("custom-process-queue" + (" hide" if not self.hpc else "")), ) with Vertical(classes="remove-process-group"): yield Label("Remove", id="remove-process-config", classes="field_help") - yield Button("-", id="remove", variant="error", classes="remove-process-button") + yield Button("—", id="remove", variant="error", classes="remove-process-button") yield Static(classes="remove-process-group-filler") # Filler @on(Button.Pressed, "#remove") @@ -85,6 +121,9 @@ def __init__(self, selector_type: str, config_key: str, title: str) -> None: self.selector_type = selector_type assert isinstance(config_key, str) and config_key self.config_key = config_key + self.intro = markdown_process_name if self.selector_type == "name" else markdown_process_label + self.skip = markdown_skip_name if self.selector_type == "name" else markdown_skip_label + self.warning = markdown_warn_name if self.selector_type == "name" else markdown_warn_label def _set_next_screen(self, next_screen: str) -> None: assert isinstance(next_screen, str) @@ -94,9 +133,11 @@ def compose(self) -> ComposeResult: yield Header() yield Footer() yield Markdown(f"# {self.title}") + yield Markdown(self.intro) + yield Markdown(markdown_process_common) + yield Markdown(self.skip) + yield Markdown(self.warning) yield VerticalScroll( - ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), - ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), ProcessConfig(selector=self.selector_type, hpc=self.parent.PIPE_CONF_HPC), id="configs", ) diff --git a/nf_core/configs/create/pipelineconfigquestion.py b/nf_core/configs/create/pipelineconfigquestion.py index a3991acb58..9dc3ff396d 100644 --- a/nf_core/configs/create/pipelineconfigquestion.py +++ b/nf_core/configs/create/pipelineconfigquestion.py @@ -8,6 +8,24 @@ from textual.screen import Screen from textual.widgets import Button, Footer, Header, Label, Markdown, Switch +markdown_explain = """ +The following pages will let you configure the CPU, memory, and walltime +resources for your pipeline. Use the toggles below to specify whether +you want to configure these values for specific processes or lables, +and if you want to set the defaults for these values. + +- If you want to set the **default** CPU, memory, and walltime resources for **every process**, +toggle `Configure default resources for processes?`. +- If you want to configure the resources for individual processes by name, +toggle `Configure specific processes by name?`. +- If you want to configure groups of processes that share a particular **[label](https://docs.seqera.io/nextflow/reference/process#label)**, +toggle `Configure processes by label?`. + +Remember, process-specific resource configurations take precedence over +label configurations. If neither is set for a given process and resource type, the +default resource configuration will be used. +""" + class PipelineConfigQuestion(Screen): """Determine whether the user wants to configure the default resources and/or specific process names/labels.""" @@ -29,22 +47,23 @@ def compose(self) -> ComposeResult: """ ) ) + yield Markdown(markdown_explain) with Horizontal(): - yield Label("Configure default process resources?", id="toggle_configure_defaults_label") + yield Label("Configure default resources for processes?", id="toggle_configure_defaults_label") yield Switch( id="toggle_configure_defaults", value=self.config_defaults, ) yield Label("Yes" if self.config_defaults else "No", id="toggle_configure_defaults_state_label") with Horizontal(): - yield Label("Configure specific named processes?", id="toggle_configure_names_label") + yield Label("Configure specific processes by name?", id="toggle_configure_names_label") yield Switch( id="toggle_configure_names", value=self.config_named_processes, ) yield Label("Yes" if self.config_named_processes else "No", id="toggle_configure_names_state_label") with Horizontal(): - yield Label("Configure labels?", id="toggle_configure_labels_label") + yield Label("Configure processes by label?", id="toggle_configure_labels_label") yield Switch( id="toggle_configure_labels", value=self.config_labels, diff --git a/nf_core/configs/create/utils.py b/nf_core/configs/create/utils.py index 10c93ee916..089b0d7063 100644 --- a/nf_core/configs/create/utils.py +++ b/nf_core/configs/create/utils.py @@ -38,6 +38,7 @@ def init_context(value: dict[str, Any]) -> Iterator[None]: _PATH_PATTERN = re.compile(r"(\/|~\/|~$|\$\{?\w+\}?)(.*)") # Used by finalinfradetails as it already imports create.utils SUPPORTED_CONTAINERS = ["singularity", "docker", "apptainer", "charliecloud", "podman", "sarus", "shifter", "conda"] +CACHED_CONTAINERS = ["singularity", "apptainer", "charliecloud", "conda"] SUPPORTED_SCHEDULERS = ["local", "pbs", "pbspro", "slurm", "sge"] @@ -230,7 +231,7 @@ def serial_hpc(self): self.container_system: { "enabled": True, "cacheDir": self.cachedir - if self.container_system in ["singularity", "apptainer", "charliecloud", "conda"] + if self.container_system in CACHED_CONTAINERS else None, "autoMounts": True if self.container_system in ["singularity", "apptainer"] else None, },