Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions src/aiida/cmdline/commands/cmd_computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
###########################################################################
"""`verdi computer` command."""

import json
import pathlib
import traceback
from copy import deepcopy
Expand Down Expand Up @@ -270,6 +271,19 @@
return value


# Helper function to set template vars in context
def set_template_vars_in_context(ctx, param, value):
"""Set template variables in the context for the config provider to use."""
if value:
try:
template_var_dict = json.loads(value)

Check warning on line 279 in src/aiida/cmdline/commands/cmd_computer.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_computer.py#L277-L279

Added lines #L277 - L279 were not covered by tests
# Store template vars in context for the config provider
ctx._template_vars = template_var_dict
except json.JSONDecodeError as e:
raise click.BadParameter(f'Invalid JSON in template-vars: {e}')
return value

Check warning on line 284 in src/aiida/cmdline/commands/cmd_computer.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_computer.py#L281-L284

Added lines #L281 - L284 were not covered by tests


@verdi_computer.command('setup')
@options_computer.LABEL()
@options_computer.HOSTNAME()
Expand All @@ -285,22 +299,32 @@
@options_computer.PREPEND_TEXT()
@options_computer.APPEND_TEXT()
@options.NON_INTERACTIVE()
@options.CONFIG_FILE()
@options.TEMPLATE_VARS() # This should come before TEMPLATE_FILE
@options.TEMPLATE_FILE() # This will process the template and set defaults
@click.pass_context
@with_dbenv()
def computer_setup(ctx, non_interactive, **kwargs):
"""Create a new computer."""
from aiida.orm.utils.builders.computer import ComputerBuilder

if kwargs['label'] in get_computer_names():
# Debug output
print(f'Debug: non_interactive = {non_interactive}')
print(f'Debug: kwargs keys = {list(kwargs.keys())}')
print(f'Debug: ctx.default_map = {ctx.default_map}')

Check warning on line 313 in src/aiida/cmdline/commands/cmd_computer.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_computer.py#L311-L313

Added lines #L311 - L313 were not covered by tests

# Check for existing computer
if kwargs.get('label') and kwargs['label'] in get_computer_names():

Check warning on line 316 in src/aiida/cmdline/commands/cmd_computer.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_computer.py#L316

Added line #L316 was not covered by tests
echo.echo_critical(
'A computer called {c} already exists. '
'Use "verdi computer duplicate {c}" to set up a new '
'computer starting from the settings of {c}.'.format(c=kwargs['label'])
)

kwargs['transport'] = kwargs['transport'].name
kwargs['scheduler'] = kwargs['scheduler'].name
# Convert entry points to their names
if kwargs.get('transport'):
kwargs['transport'] = kwargs['transport'].name
if kwargs.get('scheduler'):
kwargs['scheduler'] = kwargs['scheduler'].name

Check warning on line 327 in src/aiida/cmdline/commands/cmd_computer.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_computer.py#L324-L327

Added lines #L324 - L327 were not covered by tests

computer_builder = ComputerBuilder(**kwargs)
try:
Expand Down
75 changes: 75 additions & 0 deletions src/aiida/cmdline/params/options/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
###########################################################################
"""Module with pre-defined reusable commandline options that can be used as `click` decorators."""

import json
import pathlib

import click

from aiida.brokers.rabbitmq.defaults import BROKER_DEFAULTS
from aiida.cmdline.utils import echo
from aiida.cmdline.utils.template_config import load_and_process_template
from aiida.common.log import LOG_LEVELS, configure_logging
from aiida.manage.external.postgres import DEFAULT_DBINFO

Expand Down Expand Up @@ -113,6 +116,8 @@
'SORT',
'START_DATE',
'SYMLINK_CALCS',
'TEMPLATE_FILE',
'TEMPLATE_VARS',
'TIMEOUT',
'TRAJECTORY_INDEX',
'TRANSPORT',
Expand Down Expand Up @@ -910,3 +915,73 @@
show_default=True,
help='End date for node mtime range selection for node collection dumping.',
)

import click

from aiida.cmdline.utils import echo

from .overridable import OverridableOption


# Template processing callback
def process_template_callback(ctx, param, value):
"""Process template file and update context defaults."""
if not value:
return value

Check warning on line 930 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L929-L930

Added lines #L929 - L930 were not covered by tests

ctx.default_map = ctx.default_map or {}

Check warning on line 932 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L932

Added line #L932 was not covered by tests

# Get template vars from context if they were set by TEMPLATE_VARS option
template_vars = getattr(ctx, '_template_vars', None)

Check warning on line 935 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L935

Added line #L935 was not covered by tests

# Check if we're in non-interactive mode
non_interactive = ctx.params.get('non_interactive', False)

Check warning on line 938 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L938

Added line #L938 was not covered by tests

try:

Check warning on line 940 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L940

Added line #L940 was not covered by tests
# Load and process the template
config_data = load_and_process_template(value, interactive=not non_interactive, template_vars=template_vars)

Check warning on line 942 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L942

Added line #L942 was not covered by tests

# Update the default map with template values
for key, template_value in config_data.items():
if key not in ctx.default_map:
ctx.default_map[key] = template_value

Check warning on line 947 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L945-L947

Added lines #L945 - L947 were not covered by tests

except Exception as e:
echo.echo_critical(f'Error processing template: {e}')

Check warning on line 950 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L949-L950

Added lines #L949 - L950 were not covered by tests

return value

Check warning on line 952 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L952

Added line #L952 was not covered by tests


# Template vars callback
def set_template_vars_callback(ctx, param, value):
"""Set template variables in the context for the template option to use."""
if value:
try:
template_var_dict = json.loads(value)

Check warning on line 960 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L958-L960

Added lines #L958 - L960 were not covered by tests
# Store template vars in context for the template option to use
ctx._template_vars = template_var_dict
except json.JSONDecodeError as e:
raise click.BadParameter(f'Invalid JSON in template-vars: {e}')
return value

Check warning on line 965 in src/aiida/cmdline/params/options/main.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/params/options/main.py#L962-L965

Added lines #L962 - L965 were not covered by tests


# Template options using simple OverridableOption with callbacks
TEMPLATE_VARS = OverridableOption(
'--template-vars',
type=click.STRING,
is_eager=True, # Process before template option
callback=set_template_vars_callback,
expose_value=False, # Don't pass to command function
help='JSON string containing template variable values for non-interactive mode. '
'Example: \'{"label": "my-computer", "slurm_account": "my_account"}\'',
)

TEMPLATE_FILE = OverridableOption(
'--template',
type=click.STRING,
is_eager=True, # Process template before other options
callback=process_template_callback,
expose_value=False, # Don't pass template to the command function
help='Load computer setup from configuration file in YAML format (local path or URL). '
'Supports Jinja2 templates with interactive prompting.',
)
150 changes: 150 additions & 0 deletions src/aiida/cmdline/utils/template_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from typing import Any, Dict, List, Optional

import click
import requests
import yaml
from jinja2 import BaseLoader, Environment, meta

from aiida.cmdline.utils import echo


class StringTemplateLoader(BaseLoader):
"""Jinja2 loader that loads templates from strings."""

def __init__(self, template_string: str):
self.template_string = template_string

Check warning on line 15 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L15

Added line #L15 was not covered by tests

def get_source(self, environment, template):
return self.template_string, None, lambda: True

Check warning on line 18 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L18

Added line #L18 was not covered by tests


def prompt_for_template_variables(template_variables: Dict[str, Any]) -> Dict[str, Any]:
"""Prompt user for template variables based on metadata definitions."""
values = {}

Check warning on line 23 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L23

Added line #L23 was not covered by tests

echo.echo_report('Template variables detected. Please provide values:')
echo.echo('')

Check warning on line 26 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L25-L26

Added lines #L25 - L26 were not covered by tests

for var_name, var_config in template_variables.items():
key_display = var_config.get('key_display', var_name)
description = var_config.get('description', f'Value for {var_name}')
var_type = var_config.get('type', 'text')
default = var_config.get('default')
options = var_config.get('options', [])

Check warning on line 33 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L28-L33

Added lines #L28 - L33 were not covered by tests

# Display help text
echo.echo(f'{click.style(key_display, fg="yellow")}')
echo.echo(f' {description}')

Check warning on line 37 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L36-L37

Added lines #L36 - L37 were not covered by tests

if var_type == 'list' and options:
echo.echo(f' Options: {", ".join(options)}')

Check warning on line 40 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L39-L40

Added lines #L39 - L40 were not covered by tests
while True:
value = click.prompt(' Enter value', default=default, show_default=True if default else False)
if value in options:
values[var_name] = value
break

Check warning on line 45 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L42-L45

Added lines #L42 - L45 were not covered by tests
else:
echo.echo_error(f'Invalid option. Please choose from: {", ".join(options)}')

Check warning on line 47 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L47

Added line #L47 was not covered by tests
else:
value = click.prompt(' Enter value', default=default, show_default=True if default else False)
values[var_name] = value

Check warning on line 50 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L49-L50

Added lines #L49 - L50 were not covered by tests

echo.echo('')

Check warning on line 52 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L52

Added line #L52 was not covered by tests

return values

Check warning on line 54 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L54

Added line #L54 was not covered by tests


def detect_template_variables(template_content: str) -> List[str]:
"""Detect Jinja2 variables in template content."""
env = Environment(loader=StringTemplateLoader(template_content))
ast = env.parse(template_content)
return list(meta.find_undeclared_variables(ast))

Check warning on line 61 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L59-L61

Added lines #L59 - L61 were not covered by tests


def load_and_process_template(
file_path_or_url: str, interactive: bool = True, template_vars: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Load and process a template configuration file."""

# Load content
if file_path_or_url.startswith(('http://', 'https://')):
try:
response = requests.get(file_path_or_url, timeout=10)
response.raise_for_status()
content = response.text
except requests.RequestException as e:
raise click.BadParameter(f'Failed to fetch URL {file_path_or_url}: {e}')

Check warning on line 76 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L70-L76

Added lines #L70 - L76 were not covered by tests
else:
try:
with open(file_path_or_url, 'r', encoding='utf-8') as f:
content = f.read()
except IOError as e:
raise click.BadParameter(f'Failed to read file {file_path_or_url}: {e}')

Check warning on line 82 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L78-L82

Added lines #L78 - L82 were not covered by tests

# Parse YAML to get metadata
try:
full_config = yaml.safe_load(content)
except yaml.YAMLError as e:
raise click.BadParameter(f'Invalid YAML: {e}')

Check warning on line 88 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L85-L88

Added lines #L85 - L88 were not covered by tests

# Extract metadata and template variables (if they exist)
metadata = full_config.pop('metadata', {})
template_variables = metadata.get('template_variables', {})

Check warning on line 92 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L91-L92

Added lines #L91 - L92 were not covered by tests

# Detect variables that need values
detected_vars = detect_template_variables(content)

Check warning on line 95 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L95

Added line #L95 was not covered by tests

# If no template variables detected, just return the config
if not detected_vars:
return full_config

Check warning on line 99 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L98-L99

Added lines #L98 - L99 were not covered by tests

# Filter to only prompt for variables that are actually used and defined in metadata
vars_to_prompt = {var: config for var, config in template_variables.items() if var in detected_vars}

Check warning on line 102 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L102

Added line #L102 was not covered by tests

if vars_to_prompt:
if interactive:

Check warning on line 105 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L104-L105

Added lines #L104 - L105 were not covered by tests
# Interactive prompting for template variables
template_values = prompt_for_template_variables(vars_to_prompt)

Check warning on line 107 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L107

Added line #L107 was not covered by tests
else:
# Non-interactive mode
if not template_vars:
raise click.BadParameter(

Check warning on line 111 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L110-L111

Added lines #L110 - L111 were not covered by tests
f'Template variables detected ({", ".join(detected_vars)}) but no values provided. '
'Use --template-vars to provide values in JSON format.'
)
template_values = template_vars

Check warning on line 115 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L115

Added line #L115 was not covered by tests

# Render the template with provided values
env = Environment(loader=StringTemplateLoader(content))
template = env.from_string(content)
rendered_content = template.render(**template_values)

Check warning on line 120 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L118-L120

Added lines #L118 - L120 were not covered by tests

# Parse the rendered YAML
try:
config = yaml.safe_load(rendered_content)
except yaml.YAMLError as e:
raise click.BadParameter(f'Invalid YAML after template rendering: {e}')

Check warning on line 126 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L123-L126

Added lines #L123 - L126 were not covered by tests
else:
# Template variables detected but none defined in metadata
# This could happen with simple Jinja variables like {{ username }}
if interactive:
echo.echo_warning(f'Template variables detected ({", ".join(detected_vars)}) but no metadata found.')
echo.echo_warning('You may need to provide values manually or the template may not render correctly.')

Check warning on line 132 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L130-L132

Added lines #L130 - L132 were not covered by tests

if template_vars:

Check warning on line 134 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L134

Added line #L134 was not covered by tests
# Try to render with provided vars
env = Environment(loader=StringTemplateLoader(content))
template = env.from_string(content)
rendered_content = template.render(**template_vars)
try:
config = yaml.safe_load(rendered_content)
except yaml.YAMLError as e:
raise click.BadParameter(f'Invalid YAML after template rendering: {e}')

Check warning on line 142 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L136-L142

Added lines #L136 - L142 were not covered by tests
else:
# Return original config and hope for the best
config = full_config

Check warning on line 145 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L145

Added line #L145 was not covered by tests

# Remove metadata section if it exists
config.pop('metadata', None)

Check warning on line 148 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L148

Added line #L148 was not covered by tests

return config

Check warning on line 150 in src/aiida/cmdline/utils/template_config.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/utils/template_config.py#L150

Added line #L150 was not covered by tests
Loading