From a8633125d88e1f31e9954f03e047f4729f6412bd Mon Sep 17 00:00:00 2001 From: Macabe Wood Date: Thu, 11 Jun 2026 17:04:57 -0700 Subject: [PATCH 1/5] adding tool to redact metadata --- q2cli/builtin/tools.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/q2cli/builtin/tools.py b/q2cli/builtin/tools.py index 88b2d396..e5c7eac2 100644 --- a/q2cli/builtin/tools.py +++ b/q2cli/builtin/tools.py @@ -1749,3 +1749,36 @@ def signature_verify(input_path, name): click.echo(CONFIG.cfg_style('success', f'Signature {name} on Result ' f'{input_path} verified successfully.')) + + +@tools.command( + name='redact-metadata', + short_help='Remove metadata from an artifact.', + help='Remove all metadata from an artifact and return the artifact ' + 'otherwise unchanged.' +) +@click.option( + '--input-path', + required=True, + help='The path to the artifact where you wish to remove the metadata.' +) +@click.option( + '--output-path', + required=True, + help='Path to save artifact with redacted metadata to.' +) +def redact_metadata(input_path, output_path): + from qiime2 import Artifact + from q2cli.core.config import CONFIG + + artifact = Artifact.load(input_path) + artifact.redact_metadata() + artifact.save(output_path) + + click.echo( + CONFIG.cfg_style( + 'success', + f'Succesfully redacted metadata from {input_path}, and saved to ' + f'{output_path}.' + ) + ) From 1ccb5e74a53a207a93532245fc2abbe96fd4baae Mon Sep 17 00:00:00 2001 From: Macabe Wood Date: Mon, 15 Jun 2026 12:48:52 -0700 Subject: [PATCH 2/5] adding to archiver --- q2cli/builtin/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/q2cli/builtin/tools.py b/q2cli/builtin/tools.py index e5c7eac2..1b315193 100644 --- a/q2cli/builtin/tools.py +++ b/q2cli/builtin/tools.py @@ -1772,7 +1772,7 @@ def redact_metadata(input_path, output_path): from q2cli.core.config import CONFIG artifact = Artifact.load(input_path) - artifact.redact_metadata() + artifact._archiver.redact_metadata() artifact.save(output_path) click.echo( From 646e6e7c8e33c82ca31de48e4190d91dd071b9be Mon Sep 17 00:00:00 2001 From: Macabe Wood Date: Wed, 17 Jun 2026 12:27:58 -0700 Subject: [PATCH 3/5] CI From 6463d7c27049b78b7fcd12e0a70d8e27ad463d4b Mon Sep 17 00:00:00 2001 From: Macabe Wood Date: Tue, 23 Jun 2026 13:27:45 -0700 Subject: [PATCH 4/5] try except --- q2cli/builtin/tools.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/q2cli/builtin/tools.py b/q2cli/builtin/tools.py index 1b315193..8abd632e 100644 --- a/q2cli/builtin/tools.py +++ b/q2cli/builtin/tools.py @@ -1772,7 +1772,13 @@ def redact_metadata(input_path, output_path): from q2cli.core.config import CONFIG artifact = Artifact.load(input_path) - artifact._archiver.redact_metadata() + + try: + artifact._archiver.redact_metadata() + except ValueError as e: + click.echo(CONFIG.cfg_style('error', str(e)), err=True) + raise click.Abort() + artifact.save(output_path) click.echo( From 0cd5f7c1367d04974617b67f83f58ae5e3d5377f Mon Sep 17 00:00:00 2001 From: Macabe Wood Date: Mon, 29 Jun 2026 16:04:18 -0700 Subject: [PATCH 5/5] tests --- q2cli/builtin/tools.py | 6 ++-- q2cli/tests/test_tools.py | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/q2cli/builtin/tools.py b/q2cli/builtin/tools.py index 8abd632e..f8262d56 100644 --- a/q2cli/builtin/tools.py +++ b/q2cli/builtin/tools.py @@ -1768,13 +1768,13 @@ def signature_verify(input_path, name): help='Path to save artifact with redacted metadata to.' ) def redact_metadata(input_path, output_path): - from qiime2 import Artifact + from rachis.sdk.result import Result from q2cli.core.config import CONFIG - artifact = Artifact.load(input_path) + artifact = Result.load(input_path) try: - artifact._archiver.redact_metadata() + artifact.redact_metadata() except ValueError as e: click.echo(CONFIG.cfg_style('error', str(e)), err=True) raise click.Abort() diff --git a/q2cli/tests/test_tools.py b/q2cli/tests/test_tools.py index 2447caff..01cae411 100644 --- a/q2cli/tests/test_tools.py +++ b/q2cli/tests/test_tools.py @@ -31,6 +31,7 @@ from q2cli.builtin.tools import tools from q2cli.commands import RootCommand from q2cli.core.usage import ReplayCLIUsage +from rachis.sdk.result import Visualization class TestCastMetadata(unittest.TestCase): @@ -2013,5 +2014,67 @@ def test_make_report_duplicate_names(self): self.assertIn('Multiple files share the same name', result.output) +class TestRedactMetadata(unittest.TestCase): + def setUp(self): + datadir = datadir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'data' + ) + self.tax_fp = os.path.join(datadir, 'taxonomy.qza') + self.tax_bar_fp = os.path.join(datadir, 'taxa-bar-plots.qzv') + self.demux_fp = os.path.join(datadir, 'demux.qza') + self.runner = CliRunner() + self.tempdir = tempfile.mkdtemp(prefix='qiime2-q2cli-test-temp-') + self.redacted_fp = os.path.join(self.tempdir, 'redacted') + + def test_with_metadata(self): + redacted_fp = self.redacted_fp + '.qzv' + result = self.runner.invoke(tools, [ + 'redact-metadata', '--input-path', self.tax_bar_fp, + '--output-path', redacted_fp + ]) + + redacted_artifact = Visualization.load(redacted_fp) + abs_md_paths, _ = redacted_artifact.metadata_paths() + + for path in abs_md_paths: + self.assertEqual(os.path.getsize(path), 0) + + success = f'Succesfully redacted metadata from {self.tax_bar_fp}, ' \ + f'and saved to {redacted_fp}.\n' + + self.assertEqual(success, result.output) + + def test_fails_already_redacted(self): + ''' + Redacting metadata from an Artifact twice should fail. + ''' + redacted_fp = self.redacted_fp + '.qza' + self.runner.invoke(tools, [ + 'redact-metadata', '--input-path', self.tax_fp, + '--output-path', self.redacted_fp + ]) + + result = self.runner.invoke(tools, [ + 'redact-metadata', '--input-path', redacted_fp, + '--output-path', os.path.join(self.tempdir, 'bad') + ]) + + self.assertEqual(result.exit_code, 1) + self.assertIn('only redacted metadata', result.output) + + def test_fails_no_metadata(self): + ''' + Redacting metadata from an Artifact with no metadata should fail. + ''' + redacted_fp = self.redacted_fp + '.qza' + result = self.runner.invoke(tools, [ + 'redact-metadata', '--input-path', self.demux_fp, + '--output-path', redacted_fp + ]) + + self.assertEqual(result.exit_code, 1) + self.assertIn('Artifact without metadata', result.output) + + if __name__ == "__main__": unittest.main()