Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add verdi group delete --delete-nodes #4578

Merged
merged 20 commits into from
Jan 7, 2021
Merged
Show file tree
Hide file tree
Changes from 18 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
25 changes: 18 additions & 7 deletions aiida/cmdline/commands/cmd_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
###########################################################################
"""`verdi code` command."""
from functools import partial
import logging

import click
import tabulate
Expand Down Expand Up @@ -192,16 +193,26 @@ def delete(codes, verbose, dry_run, force):

Note that codes are part of the data provenance, and deleting a code will delete all calculations using it.
"""
from aiida.manage.database.delete.nodes import delete_nodes
from aiida.common.log import override_log_formatter_context
from aiida.tools import delete_nodes, DELETE_LOGGER

verbosity = 1
if force:
verbosity = 0
elif verbose:
verbosity = 2
verbosity = logging.DEBUG if verbose else logging.INFO
DELETE_LOGGER.setLevel(verbosity)

node_pks_to_delete = [code.pk for code in codes]
delete_nodes(node_pks_to_delete, dry_run=dry_run, verbosity=verbosity, force=force)

def _dry_run_callback(pks):
if not pks or force:
return False
echo.echo_warning(f'YOU ARE ABOUT TO DELETE {len(pks)} NODES! THIS CANNOT BE UNDONE!')
return not click.confirm('Shall I continue?')
chrisjsewell marked this conversation as resolved.
Show resolved Hide resolved

DELETE_LOGGER.setLevel(verbosity)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't you already set the logger verbosity on line 200?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops copy pasta

with override_log_formatter_context('%(message)s'):
_, was_deleted = delete_nodes(node_pks_to_delete, dry_run=dry_run or _dry_run_callback)

if was_deleted:
echo.echo_success('Finished deletion.')


@verdi_code.command()
Expand Down
54 changes: 41 additions & 13 deletions aiida/cmdline/commands/cmd_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
###########################################################################
"""`verdi group` commands"""
import warnings
import logging
import click

from aiida.common.exceptions import UniquenessError
Expand All @@ -17,6 +18,7 @@
from aiida.cmdline.params import options, arguments
from aiida.cmdline.utils import echo
from aiida.cmdline.utils.decorators import with_dbenv
from aiida.common.links import GraphTraversalRules


@verdi.group('group')
Expand All @@ -30,7 +32,7 @@ def verdi_group():
@arguments.NODES()
@with_dbenv()
def group_add_nodes(group, force, nodes):
"""Add nodes to the a group."""
"""Add nodes to a group."""
if not force:
click.confirm(f'Do you really want to add {len(nodes)} nodes to Group<{group.label}>?', abort=True)

Expand Down Expand Up @@ -61,29 +63,55 @@ def group_remove_nodes(group, nodes, clear, force):

@verdi_group.command('delete')
@arguments.GROUP()
@options.FORCE()
@click.option(
'--delete-nodes', is_flag=True, default=False, help='Delete all nodes in the group along with the group itself.'
)
@options.graph_traversal_rules(GraphTraversalRules.DELETE.value)
@options.DRY_RUN()
@options.VERBOSE()
@options.GROUP_CLEAR(
help='Remove all nodes before deleting the group itself.' +
' [deprecated: No longer has any effect. Will be removed in 2.0.0]'
)
@options.FORCE()
@with_dbenv()
def group_delete(group, clear, force):
"""Delete a group.

Note that this command only deletes groups - nodes contained in the group will remain untouched.
"""
def group_delete(group, clear, delete_nodes, dry_run, force, verbose, **traversal_rules):
"""Delete a group and (optionally) the nodes it contains."""
from aiida.common.log import override_log_formatter_context
from aiida.tools import delete_group_nodes, DELETE_LOGGER
from aiida import orm

label = group.label

if clear:
warnings.warn('`--clear` is deprecated and no longer has any effect.', AiidaDeprecationWarning) # pylint: disable=no-member

if not force:
click.confirm(f'Are you sure to delete Group<{label}>?', abort=True)
label = group.label
klass = group.__class__.__name__

verbosity = logging.DEBUG if verbose else logging.INFO
DELETE_LOGGER.setLevel(verbosity)

if not (force or dry_run):
click.confirm(f'Are you sure to delete {klass}<{label}>?', abort=True)
elif dry_run:
echo.echo_info(f'Would have deleted {klass}<{label}>.')

if delete_nodes:

def _dry_run_callback(pks):
if not pks or force:
return False
echo.echo_warning(f'YOU ARE ABOUT TO DELETE {len(pks)} NODES! THIS CANNOT BE UNDONE!')
return not click.confirm('Shall I continue?')
chrisjsewell marked this conversation as resolved.
Show resolved Hide resolved

with override_log_formatter_context('%(message)s'):
_, nodes_deleted = delete_group_nodes([group.pk], dry_run=dry_run or _dry_run_callback, **traversal_rules)
if not nodes_deleted:
# don't delete the group if the nodes were not deleted
return

orm.Group.objects.delete(group.pk)
echo.echo_success(f'Group<{label}> deleted.')
if not dry_run:
orm.Group.objects.delete(group.pk)
echo.echo_success(f'{klass}<{label}> deleted.')


@verdi_group.command('relabel')
Expand Down
31 changes: 20 additions & 11 deletions aiida/cmdline/commands/cmd_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
###########################################################################
"""`verdi node` command."""

import logging
import shutil
import pathlib

Expand Down Expand Up @@ -302,32 +303,40 @@ def tree(nodes, depth):
@options.FORCE()
@options.graph_traversal_rules(GraphTraversalRules.DELETE.value)
@with_dbenv()
def node_delete(identifier, dry_run, verbose, force, **kwargs):
def node_delete(identifier, dry_run, verbose, force, **traversal_rules):
"""Delete nodes from the provenance graph.

This will not only delete the nodes explicitly provided via the command line, but will also include
the nodes necessary to keep a consistent graph, according to the rules outlined in the documentation.
You can modify some of those rules using options of this command.
"""
from aiida.common.log import override_log_formatter_context
from aiida.orm.utils.loaders import NodeEntityLoader
from aiida.manage.database.delete.nodes import delete_nodes
from aiida.tools import delete_nodes, DELETE_LOGGER

verbosity = 1
if force:
verbosity = 0
elif verbose:
verbosity = 2
verbosity = logging.DEBUG if verbose else logging.INFO
DELETE_LOGGER.setLevel(verbosity)

pks = []

for obj in identifier:
# we only load the node if we need to convert from a uuid/label
if isinstance(obj, int):
pks.append(obj)
else:
try:
pks.append(int(obj))
except ValueError:
pks.append(NodeEntityLoader.load_entity(obj).pk)

delete_nodes(pks, dry_run=dry_run, verbosity=verbosity, force=force, **kwargs)
def _dry_run_callback(pks):
if not pks or force:
return False
echo.echo_warning(f'YOU ARE ABOUT TO DELETE {len(pks)} NODES! THIS CANNOT BE UNDONE!')
return not click.confirm('Shall I continue?')
chrisjsewell marked this conversation as resolved.
Show resolved Hide resolved

with override_log_formatter_context('%(message)s'):
_, was_deleted = delete_nodes(pks, dry_run=dry_run or _dry_run_callback, **traversal_rules)

if was_deleted:
echo.echo_success('Finished deletion.')


@verdi_node.command('rehash')
Expand Down
129 changes: 21 additions & 108 deletions aiida/manage/database/delete/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,118 +7,31 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Function to delete nodes from the database."""
from typing import Iterable

import click
from aiida.cmdline.utils import echo
"""Functions to delete nodes from the database, preserving provenance integrity."""
from typing import Callable, Iterable, Optional, Set, Tuple, Union
import warnings


def delete_nodes(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to deprecate this function if it wasn't part of the public API?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well no, but I know that I've certainly used it before (since there's no other way with the API to delete nodes), so better safe than sorry

pks: Iterable[int], verbosity: int = 0, dry_run: bool = False, force: bool = False, **traversal_rules: bool
):
"""Delete nodes by a list of pks.

This command will delete not only the specified nodes, but also the ones that are
linked to these and should be also deleted in order to keep a consistent provenance
according to the rules explained in the concepts section of the documentation.
In summary:

1. If a DATA node is deleted, any process nodes linked to it will also be deleted.

2. If a CALC node is deleted, any incoming WORK node (callers) will be deleted as
well whereas any incoming DATA node (inputs) will be kept. Outgoing DATA nodes
(outputs) will be deleted by default but this can be disabled.

3. If a WORK node is deleted, any incoming WORK node (callers) will be deleted as
well, but all DATA nodes will be kept. Outgoing WORK or CALC nodes will be kept by
default, but deletion of either of both kind of connected nodes can be enabled.
pks: Iterable[int],
verbosity: Optional[int] = None,
dry_run: Union[bool, Callable[[Set[int]], bool]] = True,
force: Optional[bool] = None,
**traversal_rules: bool
) -> Tuple[Set[int], bool]:
"""Delete nodes given a list of "starting" PKs.

.. deprecated:: 1.6.0
This function has been moved and will be removed in `v2.0.0`.
It should now be imported using `from aiida.tools import delete_nodes`

These rules are 'recursive', so if a CALC node is deleted, then its output DATA
nodes will be deleted as well, and then any CALC node that may have those as
inputs, and so on.

:param pks: a list of the PKs of the nodes to delete
:param force: do not ask for confirmation to delete nodes.
:param verbosity: 0 prints nothing,
1 prints just sums and total,
2 prints individual nodes.

:param dry_run:
Just perform a dry run and do not delete anything.
Print statistics according to the verbosity level set.
:param force: Do not ask for confirmation to delete nodes

:param traversal_rules: graph traversal rules. See :const:`aiida.common.links.GraphTraversalRules` what rule names
are toggleable and what the defaults are.
"""
# pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements
from aiida.backends.utils import delete_nodes_and_connections
from aiida.orm import Node, QueryBuilder, load_node
from aiida.tools.graph.graph_traversers import get_nodes_delete

def _missing_callback(_pks: Iterable[int]):
for _pk in _pks:
echo.echo_warning(f'warning: node with pk<{_pk}> does not exist, skipping')

pks_set_to_delete = get_nodes_delete(pks, get_links=False, missing_callback=_missing_callback,
**traversal_rules)['nodes']

# An empty set might be problematic for the queries done below.
if not pks_set_to_delete:
if verbosity:
echo.echo('Nothing to delete')
return

if verbosity > 0:
echo.echo(
'I {} delete {} node{}'.format(
'would' if dry_run else 'will', len(pks_set_to_delete), 's' if len(pks_set_to_delete) > 1 else ''
)
)
if verbosity > 1:
builder = QueryBuilder().append(
Node, filters={'id': {
'in': pks_set_to_delete
}}, project=('uuid', 'id', 'node_type', 'label')
)
echo.echo(f"The nodes I {'would' if dry_run else 'will'} delete:")
for uuid, pk, type_string, label in builder.iterall():
try:
short_type_string = type_string.split('.')[-2]
except IndexError:
short_type_string = type_string
echo.echo(f' {uuid} {pk} {short_type_string} {label}')

if dry_run:
if verbosity > 0:
echo.echo('\nThis was a dry run, exiting without deleting anything')
return

# Asking for user confirmation here
if force:
pass
else:
echo.echo_warning(f'YOU ARE ABOUT TO DELETE {len(pks_set_to_delete)} NODES! THIS CANNOT BE UNDONE!')
if not click.confirm('Shall I continue?'):
echo.echo('Exiting without deleting')
return

# Recover the list of folders to delete before actually deleting the nodes. I will delete the folders only later,
# so that if there is a problem during the deletion of the nodes in the DB, I don't delete the folders
repositories = [load_node(pk)._repository for pk in pks_set_to_delete] # pylint: disable=protected-access

if verbosity > 0:
echo.echo('Starting node deletion...')
delete_nodes_and_connections(pks_set_to_delete)

if verbosity > 0:
echo.echo('Nodes deleted from database, deleting files from the repository now...')
from aiida.common.warnings import AiidaDeprecationWarning
from aiida.tools import delete_nodes as _delete

# If we are here, we managed to delete the entries from the DB.
# I can now delete the folders
for repository in repositories:
repository.erase(force=True)
warnings.warn(
'This function has been moved and will be removed in `v2.0.0`.'
'It should now be imported using `from aiida.tools import delete_nodes`', AiidaDeprecationWarning
) # pylint: disable=no-member

if verbosity > 0:
echo.echo('Deletion completed.')
return _delete(pks, verbosity, dry_run, force, **traversal_rules)
5 changes: 4 additions & 1 deletion aiida/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
from .data.array.kpoints import *
from .data.structure import *
from .dbimporters import *
from .graph import *

__all__ = (calculations.__all__ + data.array.kpoints.__all__ + data.structure.__all__ + dbimporters.__all__)
__all__ = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be sure to also update the public API in the Reference section:

https://aiida-core.readthedocs.io/en/stable/reference/api/public.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 (I've got plans anyhow to automate the creation of this: #4558 (comment))

calculations.__all__ + data.array.kpoints.__all__ + data.structure.__all__ + dbimporters.__all__ + graph.__all__
)
5 changes: 5 additions & 0 deletions aiida/tools/graph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
# pylint: disable=wildcard-import,undefined-variable
"""Provides tools for traversing the provenance graph."""
from .deletions import *

__all__ = deletions.__all__
Loading