Skip to content

Commit

Permalink
CLI: Add the verdi node list command (#6267)
Browse files Browse the repository at this point in the history
This is a generic purpose command that makes it easy to query for nodes.
It allows to filter on node types and order/limit the results.
  • Loading branch information
sphuber authored Feb 23, 2024
1 parent 4626b11 commit cf091e8
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/source/reference/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ Below is a list with all available subcommands.
extras Show the extras of one or more nodes.
graph Create visual representations of the provenance graph.
label View or set the label of one or more nodes.
list Query all nodes with optional filtering and ordering.
rehash Recompute the hash for nodes in the database.
repo Inspect the content of a node repository folder.
show Show generic information on one or more nodes.
Expand Down
41 changes: 41 additions & 0 deletions src/aiida/cmdline/commands/cmd_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# For further information please visit http://www.aiida.net #
###########################################################################
"""`verdi node` command."""
import datetime
import pathlib

import click
Expand All @@ -25,6 +26,46 @@ def verdi_node():
"""Inspect, create and manage nodes."""


@verdi_node.command('list')
@click.option('-e', '--entry-point', type=str, required=False)
@click.option(
'--subclassing/--no-subclassing',
default=True,
help='Pass `--no-subclassing` to disable matching subclasses of the specified `--entry-point`.',
)
@options.PROJECT(
type=click.Choice(
('id', 'uuid', 'node_type', 'process_type', 'label', 'description', 'ctime', 'mtime', 'attributes', 'extras')
),
default=('id', 'uuid', 'node_type'),
)
@options.PAST_DAYS()
@options.ORDER_BY()
@options.ORDER_DIRECTION()
@options.LIMIT()
@options.RAW()
def node_list(entry_point, subclassing, project, past_days, order_by, order_dir, limit, raw):
"""Query all nodes with optional filtering and ordering."""
from aiida.orm import Node
from aiida.plugins.factories import DataFactory

node_class = DataFactory(entry_point) if entry_point else Node

if past_days is not None:
filters = {'ctime': {'>': timezone.now() - datetime.timedelta(days=past_days)}}
else:
filters = {}

query = node_class.collection.query(
filters,
project=list(project),
limit=limit,
subclassing=subclassing,
order_by=[{order_by: order_dir}],
)
echo_tabulate(query.all(), headers=project if not raw else [], tablefmt='plain' if raw else None)


@verdi_node.group('repo')
def verdi_node_repo():
"""Inspect the content of a node repository folder."""
Expand Down
8 changes: 6 additions & 2 deletions src/aiida/orm/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import abc
from enum import Enum
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Generic, List, Optional, Type, TypeVar, cast
from typing import TYPE_CHECKING, Any, Generic, List, Optional, Type, TypeVar, Union, cast

from plumpy.base.utils import call_with_super_check, super_check

Expand Down Expand Up @@ -99,23 +99,27 @@ def query(
self,
filters: Optional['FilterType'] = None,
order_by: Optional['OrderByType'] = None,
project: Optional[Union[list[str], str]] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
subclassing: bool = True,
) -> 'QueryBuilder':
"""Get a query builder for the objects of this collection.
:param filters: the keyword value pair filters to match
:param order_by: a list of (key, direction) pairs specifying the sort order
:param project: Optional projections.
:param limit: the maximum number of results to return
:param offset: number of initial results to be skipped
:param subclassing: whether to match subclasses of the type as well.
"""
from . import querybuilder

filters = filters or {}
order_by = {self.entity_type: order_by} if order_by else {}

query = querybuilder.QueryBuilder(backend=self._backend, limit=limit, offset=offset)
query.append(self.entity_type, project='*', filters=filters)
query.append(self.entity_type, project=project, filters=filters, subclassing=subclassing)
query.order_by([order_by])
return query

Expand Down
45 changes: 45 additions & 0 deletions tests/cmdline/commands/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# For further information please visit http://www.aiida.net #
###########################################################################
"""Tests for verdi node"""
import datetime
import errno
import gzip
import io
Expand All @@ -17,6 +18,7 @@
from aiida import orm
from aiida.cmdline.commands import cmd_node
from aiida.cmdline.utils.echo import ExitCode
from aiida.common import timezone


def get_result_lines(result):
Expand Down Expand Up @@ -589,3 +591,46 @@ def test_node_delete_basics(run_cli_command, options):
def test_node_delete_missing_pk(run_cli_command):
"""Check that no exception is raised when a non-existent pk is given (just warns)."""
run_cli_command(cmd_node.node_delete, ['999'])


@pytest.fixture(scope='class')
def create_nodes_verdi_node_list(aiida_profile_clean_class):
return (
orm.Data().store(),
orm.Int(0).store(),
orm.Int(1).store(),
orm.Int(2).store(),
orm.ArrayData().store(),
orm.KpointsData().store(),
orm.WorkflowNode(ctime=timezone.now() - datetime.timedelta(days=3)).store(),
)


@pytest.mark.usefixtures('create_nodes_verdi_node_list')
class TestNodeList:
"""Tests for the ``verdi node rehash`` command."""

@pytest.mark.parametrize(
'options, expected_nodes',
(
([], [6, 0, 1, 2, 3, 4, 5]),
(['-e', 'core.int'], [1, 2, 3]),
(['-e', 'core.int', '--limit', '1'], [1]),
(['-e', 'core.int', '--order-direction', 'desc'], [3, 2, 1]),
(['-e', 'core.int', '--order-by', 'id'], [1, 2, 3]),
(['-e', 'core.array', '--no-subclassing'], [4]),
(['-e', 'core.int', '-P', 'uuid'], [1, 2, 3]),
(['-p', '1'], [0, 1, 2, 3, 4, 5]),
),
)
def test_node_list(self, run_cli_command, options, expected_nodes):
"""Test the ``verdi node list`` command."""
nodes = orm.QueryBuilder().append(orm.Node).order_by({orm.Node: ['id']}).all(flat=True)

if set(['-P', 'uuid']).issubset(set(options)):
expected_projections = [nodes[index].uuid for index in expected_nodes]
else:
expected_projections = [str(nodes[index].pk) for index in expected_nodes]

result = run_cli_command(cmd_node.node_list, ['--project', 'id', '--raw'] + options)
assert result.output.strip() == '\n'.join(expected_projections)
1 change: 1 addition & 0 deletions tests/cmdline/commands/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def test_list_non_raw(self, run_cli_command):
assert 'Total results:' in result.output
assert 'Last time an entry changed state' in result.output

@pytest.mark.usefixtures('aiida_profile_clean')
def test_list(self, run_cli_command):
"""Test the list command."""
calcs = []
Expand Down

0 comments on commit cf091e8

Please sign in to comment.