Skip to content
Open
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
7 changes: 5 additions & 2 deletions docs/command-line-interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -759,8 +759,8 @@ Optional arguments:

.. _cli_run:

`$ run PIPELINE_NAME [PIPELINE_NAME ...] input_location`
--------------------------------------------------------
`$ run PIPELINE_NAME [PIPELINE_NAME ...] [input_location]`
----------------------------------------------------------

A ``run`` command is available for executing pipelines and printing the results
without providing any configuration. This can be useful for running a pipeline to get
Expand All @@ -770,6 +770,9 @@ review the results.
.. tip:: You can run multiple pipelines by providing their names, space-separated,
such as `pipeline1 pipeline2`.

The ``input_location`` is optional, so pipelines that do not take any input can be
run without it.

Optional arguments:

- ``--project PROJECT_NAME``: Provide a project name; otherwise, a random value is
Expand Down
25 changes: 22 additions & 3 deletions scanpipe/management/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from collections import defaultdict
from pathlib import Path

from django.apps import apps
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.core.management.base import CommandError
Expand All @@ -31,6 +32,8 @@
from scanpipe.management.commands import extract_tag_from_input_file
from scanpipe.pipes.fetch import SCHEME_TO_FETCHER_MAPPING

scanpipe_app = apps.get_app_config("scanpipe")


class Command(BaseCommand):
help = "Run a pipeline and print the results."
Expand All @@ -50,9 +53,11 @@ def add_arguments(self, parser):
)
parser.add_argument(
"input_location",
nargs="?",
help=(
"Input location: file, directory, and URL supported."
'Multiple values can be provided using the "input1,input2" syntax.'
'Multiple values can be provided using the "input1,input2" syntax. '
"Optional, as some pipelines do not require any input."
),
)
parser.add_argument("--project", required=False, help="Project name.")
Expand All @@ -64,8 +69,14 @@ def add_arguments(self, parser):
)

def handle(self, *args, **options):
# The ``input_location`` positional is declared for the command usage, but
# argparse collects every positional value in ``pipelines``. The trailing
# value is the input location unless it is an available pipeline name.
pipelines = options["pipelines"]
input_location = options["input_location"]
input_location = None
if len(pipelines) > 1 and not self.is_pipeline_name(pipelines[-1]):
input_location = pipelines.pop()

output_format = options["format"]
# Generate a random name for the project if not provided
project_name = options["project"] or get_random_string(10)
Expand All @@ -74,8 +85,9 @@ def handle(self, *args, **options):
"pipeline": pipelines,
"execute": True,
"verbosity": 0,
**self.get_input_options(input_location),
}
if input_location:
create_project_options.update(self.get_input_options(input_location))

# Run the database migrations in case the database is not created or outdated.
call_command("migrate", verbosity=0, interactive=False)
Expand All @@ -84,6 +96,13 @@ def handle(self, *args, **options):
# Print the results for the specified format on stdout
call_command("output", project=project_name, format=[output_format], print=True)

@staticmethod
def is_pipeline_name(value):
"""Return True when the provided ``value`` is an available pipeline name."""
pipeline_name, _ = scanpipe_app.extract_group_from_pipeline(value)
pipeline_name = scanpipe_app.get_new_pipeline_name(pipeline_name)
return pipeline_name in scanpipe_app.pipelines

@staticmethod
def get_input_options(input_location):
"""
Expand Down
27 changes: 24 additions & 3 deletions scanpipe/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,9 +1007,7 @@ def test_scanpipe_management_command_create_user_admin_superuser(self):
self.assertTrue(user.is_superuser)

def test_scanpipe_management_command_run(self):
expected = (
"Error: the following arguments are required: PIPELINE_NAME, input_location"
)
expected = "Error: the following arguments are required: PIPELINE_NAME"
with self.assertRaisesMessage(CommandError, expected):
call_command("run")

Expand Down Expand Up @@ -1047,6 +1045,29 @@ def test_scanpipe_management_command_run(self):
self.assertEqual("do_nothing", runs[1]["pipeline_name"])
self.assertEqual(["Group1", "Group2"], runs[1]["selected_groups"])

def test_scanpipe_management_command_run_without_input_location(self):
out = StringIO()
with redirect_stdout(out):
call_command("run", "do_nothing")

json_data = json.loads(out.getvalue())
self.assertEqual([], json_data["files"])
runs = json_data["headers"][0]["runs"]
self.assertEqual(1, len(runs))
self.assertEqual("do_nothing", runs[0]["pipeline_name"])
self.assertEqual("success", runs[0]["status"])

# A trailing pipeline name is not mistaken for an input location
out = StringIO()
with redirect_stdout(out):
call_command("run", "do_nothing", "profile_step")

json_data = json.loads(out.getvalue())
runs = json_data["headers"][0]["runs"]
self.assertEqual(2, len(runs))
self.assertEqual("do_nothing", runs[0]["pipeline_name"])
self.assertEqual("profile_step", runs[1]["pipeline_name"])

@mock.patch("scanpipe.pipes.fetch.is_safe_url", return_value=True)
@mock.patch("scanpipe.pipes.fetch.check_url")
@mock.patch("requests.sessions.Session.get")
Expand Down