Skip to content

Commit 239ff8e

Browse files
committed
Merge remote-tracking branch 'upstream/main' into stashing/calcjob
2 parents cc8536a + c535928 commit 239ff8e

File tree

5 files changed

+357
-50
lines changed

5 files changed

+357
-50
lines changed

.readthedocs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ build:
2525
- uv sync --extra docs --extra tests --extra rest --extra atomic_tools
2626
build:
2727
html:
28-
- uv run sphinx-build -T -W --keep-going -b html -d _build/doctrees -D language=en docs/source $READTHEDOCS_OUTPUT/html
28+
- uv run sphinx-build -T -W --keep-going -b html -d _build/doctrees -D language=en docs/source $READTHEDOCS_OUTPUT/html -w sphinx.log || (cat sphinx.log && exit 1)
2929

3030
search:
3131
ranking:

src/aiida/cmdline/commands/cmd_calcjob.py

+2-15
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,7 @@ def calcjob_cleanworkdir(calcjobs, past_days, older_than, computers, force, exit
259259
If both are specified, a logical AND is done between the two, i.e. the calcjobs that will be cleaned have been
260260
modified AFTER [-p option] days from now, but BEFORE [-o option] days from now.
261261
"""
262-
from aiida import orm
263-
from aiida.orm.utils.remote import get_calcjob_remote_paths
262+
from aiida.orm.utils.remote import clean_mapping_remote_paths, get_calcjob_remote_paths
264263

265264
if calcjobs:
266265
if past_days is not None and older_than is not None:
@@ -286,19 +285,7 @@ def calcjob_cleanworkdir(calcjobs, past_days, older_than, computers, force, exit
286285
warning = f'Are you sure you want to clean the work directory of {path_count} calcjobs?'
287286
click.confirm(warning, abort=True)
288287

289-
user = orm.User.collection.get_default()
290-
291-
for computer_uuid, paths in path_mapping.items():
292-
counter = 0
293-
computer = orm.load_computer(uuid=computer_uuid)
294-
transport = orm.AuthInfo.collection.get(dbcomputer_id=computer.pk, aiidauser_id=user.pk).get_transport()
295-
296-
with transport:
297-
for remote_folder in paths:
298-
remote_folder._clean(transport=transport)
299-
counter += 1
300-
301-
echo.echo_success(f'{counter} remote folders cleaned on {computer.label}')
288+
clean_mapping_remote_paths(path_mapping)
302289

303290

304291
def get_remote_and_path(calcjob, path=None):

src/aiida/cmdline/commands/cmd_node.py

+61-4
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,14 @@ def extras(nodes, keys, fmt, identifier, raw):
328328
@click.argument('identifier', nargs=-1, metavar='NODES')
329329
@options.DRY_RUN()
330330
@options.FORCE()
331+
@click.option(
332+
'--clean-workdir',
333+
is_flag=True,
334+
help='Also clean the remote work directory, if applicable.',
335+
)
331336
@options.graph_traversal_rules(GraphTraversalRules.DELETE.value)
332337
@with_dbenv()
333-
def node_delete(identifier, dry_run, force, **traversal_rules):
338+
def node_delete(identifier, dry_run, force, clean_workdir, **traversal_rules):
334339
"""Delete nodes from the provenance graph.
335340
336341
This will not only delete the nodes explicitly provided via the command line, but will also include
@@ -356,10 +361,62 @@ def _dry_run_callback(pks):
356361
echo.echo_info('The nodes with the following pks would be deleted: ' + ' '.join(map(str, pks)))
357362
return not click.confirm('Shall I continue?', abort=True)
358363

359-
_, was_deleted = delete_nodes(pks, dry_run=dry_run or _dry_run_callback, **traversal_rules)
364+
def _perform_delete():
365+
_, was_deleted = delete_nodes(pks, dry_run=dry_run or _dry_run_callback, **traversal_rules)
366+
if was_deleted:
367+
echo.echo_success('Finished deletion.')
368+
369+
if clean_workdir:
370+
from aiida.manage import get_manager
371+
from aiida.orm import CalcJobNode, QueryBuilder
372+
from aiida.orm.utils.remote import clean_mapping_remote_paths, get_calcjob_remote_paths
373+
from aiida.tools.graph.graph_traversers import get_nodes_delete
374+
375+
backend = get_manager().get_backend()
376+
# For here we ignore missing nodes will be raised via func:``delete_nodes`` in the next block
377+
pks_set_to_delete = get_nodes_delete(
378+
pks, get_links=False, missing_callback=lambda missing_pks: None, backend=backend, **traversal_rules
379+
)['nodes']
380+
381+
qb = QueryBuilder()
382+
qb.append(CalcJobNode, filters={'id': {'in': pks_set_to_delete}}, project='id')
383+
calcjobs_pks = [result[0] for result in qb.all()]
384+
385+
if not calcjobs_pks:
386+
echo.echo_report('--clean-workdir ignored. No CalcJobNode associated with the given node, found.')
387+
_perform_delete()
388+
return
389+
390+
path_mapping = get_calcjob_remote_paths(
391+
calcjobs_pks,
392+
only_not_cleaned=True,
393+
)
394+
395+
if not path_mapping:
396+
echo.echo_report('--clean-workdir ignored. CalcJobNode work directories are already cleaned.')
397+
_perform_delete()
398+
return
399+
400+
descendant_pks = [remote_folder.pk for paths in path_mapping.values() for remote_folder in paths]
401+
402+
if not force and not dry_run:
403+
echo.echo_warning(
404+
f'YOU ARE ABOUT TO CLEAN {len(descendant_pks)} REMOTE DIRECTORIES! ' 'THIS CANNOT BE UNDONE!'
405+
)
406+
echo.echo_info(
407+
'Remote directories of nodes with the following pks would be cleaned: '
408+
+ ' '.join(map(str, descendant_pks))
409+
)
410+
click.confirm('Shall I continue?', abort=True)
411+
412+
if dry_run:
413+
echo.echo_report(
414+
'Remote folders of these node are marked for deletion: ' + ' '.join(map(str, descendant_pks))
415+
)
416+
else:
417+
clean_mapping_remote_paths(path_mapping)
360418

361-
if was_deleted:
362-
echo.echo_success('Finished deletion.')
419+
_perform_delete()
363420

364421

365422
@verdi_node.command('rehash')

src/aiida/orm/utils/remote.py

+30-2
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
import os
1414
import typing as t
1515

16+
from aiida import orm
17+
from aiida.cmdline.utils import echo
1618
from aiida.orm.nodes.data.remote.base import RemoteData
1719

1820
if t.TYPE_CHECKING:
1921
from collections.abc import Sequence
2022

21-
from aiida import orm
2223
from aiida.orm.implementation import StorageBackend
2324
from aiida.transports import Transport
2425

@@ -45,6 +46,34 @@ def clean_remote(transport: Transport, path: str) -> None:
4546
pass
4647

4748

49+
def clean_mapping_remote_paths(path_mapping, silent=False):
50+
"""Clean the remote folders for a given mapping of computer UUIDs to a list of remote folders.
51+
52+
:param path_mapping: a dictionary where the keys are the computer UUIDs and the values are lists of remote folders
53+
It's designed to accept the output of `get_calcjob_remote_paths`
54+
:param transport: the transport to use to clean the remote folders
55+
:param silent: if True, the `echo` output will be suppressed
56+
"""
57+
58+
user = orm.User.collection.get_default()
59+
60+
if not user:
61+
raise ValueError('No default user found')
62+
63+
for computer_uuid, paths in path_mapping.items():
64+
counter = 0
65+
computer = orm.load_computer(uuid=computer_uuid)
66+
transport = orm.AuthInfo.collection.get(dbcomputer_id=computer.pk, aiidauser_id=user.pk).get_transport()
67+
68+
with transport:
69+
for remote_folder in paths:
70+
remote_folder._clean(transport=transport)
71+
counter += 1
72+
73+
if not silent:
74+
echo.echo_success(f'{counter} remote folders cleaned on {computer.label}')
75+
76+
4877
def get_calcjob_remote_paths(
4978
pks: list[int] | None = None,
5079
past_days: int | None = None,
@@ -70,7 +99,6 @@ def get_calcjob_remote_paths(
7099
"""
71100
from datetime import timedelta
72101

73-
from aiida import orm
74102
from aiida.common import timezone
75103
from aiida.orm import CalcJobNode
76104

0 commit comments

Comments
 (0)