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

Run remote Launchplan from pyflyte run #1785

Merged
merged 14 commits into from
Aug 29, 2023
234 changes: 202 additions & 32 deletions flytekit/clis/sdk_in_container/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import yaml
from dataclasses_json import DataClassJsonMixin
from pytimeparse import parse
from rich.progress import Progress

Check warning on line 19 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L19

Added line #L19 was not covered by tests
from typing_extensions import get_args

from flytekit import BlobType, Literal, Scalar
Expand All @@ -42,11 +43,11 @@
from flytekit.core.type_engine import TypeEngine
from flytekit.core.workflow import PythonFunctionWorkflow, WorkflowBase
from flytekit.models import literals
from flytekit.models.interface import Variable
from flytekit.models.interface import Parameter, Variable

Check warning on line 46 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L46

Added line #L46 was not covered by tests
from flytekit.models.literals import Blob, BlobMetadata, LiteralCollection, LiteralMap, Primitive, Union
from flytekit.models.types import LiteralType, SimpleType
from flytekit.remote import FlyteLaunchPlan, FlyteRemote, FlyteTask, FlyteWorkflow

Check warning on line 49 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L49

Added line #L49 was not covered by tests
from flytekit.remote.executions import FlyteWorkflowExecution
from flytekit.remote.remote import FlyteRemote
from flytekit.tools import module_loader, script_mode
from flytekit.tools.script_mode import _find_project_root
from flytekit.tools.translator import Options
Expand Down Expand Up @@ -115,15 +116,13 @@
def convert(
self, value: typing.Any, param: typing.Optional[click.Parameter], ctx: typing.Optional[click.Context]
) -> typing.Any:

uri = FlyteContextManager.current_context().file_access.get_random_local_path()
with open(uri, "w+b") as outfile:
cloudpickle.dump(value, outfile)
return FileParam(filepath=str(pathlib.Path(uri).resolve()))


class DateTimeType(click.DateTime):

_NOW_FMT = "now"
_ADDITONAL_FORMATS = [_NOW_FMT]

Expand Down Expand Up @@ -458,6 +457,7 @@
python_type: typing.Type,
default_val: typing.Any,
get_upload_url_fn: typing.Callable,
required: bool,
) -> click.Option:
"""
This handles converting workflow input types to supported click parameters with callbacks to initialize
Expand All @@ -470,21 +470,24 @@
if literal_converter.is_bool() and not default_val:
default_val = False

description_extra = ""

Check warning on line 473 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L473

Added line #L473 was not covered by tests
if literal_var.type.simple == SimpleType.STRUCT:
if default_val:
if type(default_val) == dict or type(default_val) == list:
default_val = json.dumps(default_val)
else:
default_val = cast(DataClassJsonMixin, default_val).to_json()
if literal_var.type.metadata:
description_extra = f": {json.dumps(literal_var.type.metadata)}"

Check warning on line 481 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L481

Added line #L481 was not covered by tests

return click.Option(
param_decls=[f"--{input_name}"],
type=literal_converter.click_type,
is_flag=literal_converter.is_bool(),
default=default_val,
show_default=True,
required=default_val is None,
help=literal_var.description,
required=required,
help=literal_var.description + description_extra,
callback=literal_converter.convert,
)

Expand Down Expand Up @@ -592,6 +595,13 @@
type=str,
help="Tags to set for the execution",
),
click.Option(
param_decls=["--limit", "limit"],
required=False,
type=int,
default=10,
help="Use this to limit number of launch plans retreived from the backend, if `from-server` option is used",
),
]


Expand Down Expand Up @@ -662,12 +672,59 @@
return Entities(workflows, tasks)


def run_remote(

Check warning on line 675 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L675

Added line #L675 was not covered by tests
ctx: click.Context,
remote: FlyteRemote,
entity: typing.Union[FlyteWorkflow, FlyteTask, FlyteLaunchPlan],
project: str,
domain: str,
inputs: typing.Dict[str, typing.Any],
run_level_params: typing.Dict[str, typing.Any],
type_hints: typing.Optional[typing.Dict[str, typing.Type]] = None,
):
"""
Helper method that executes the given remote FlyteLaunchplan, FlyteWorkflow or FlyteTask
"""
options = None
service_account = run_level_params.get("service_account")

Check warning on line 689 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L688-L689

Added lines #L688 - L689 were not covered by tests
if service_account:
# options are only passed for the execution. This is to prevent errors when registering a duplicate workflow
# It is assumed that the users expectations is to override the service account only for the execution
options = Options.default_from(k8s_service_account=service_account)

Check warning on line 693 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L693

Added line #L693 was not covered by tests

execution = remote.execute(

Check warning on line 695 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L695

Added line #L695 was not covered by tests
entity,
inputs=inputs,
project=project,
domain=domain,
name=run_level_params.get("name"),
wait=run_level_params.get("wait_execution"),
options=options,
type_hints=type_hints,
overwrite_cache=run_level_params.get("overwrite_cache"),
envs=run_level_params.get("envs"),
tags=run_level_params.get("tag"),
)

console_url = remote.generate_console_url(execution)
click.secho(f"Go to {console_url} to see execution in the console.")

Check warning on line 710 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L709-L710

Added lines #L709 - L710 were not covered by tests

if run_level_params.get("dump_snippet"):
dump_flyte_remote_snippet(execution, project, domain)

Check warning on line 713 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L713

Added line #L713 was not covered by tests

if ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_FILE_NAME):
os.remove(ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_FILE_NAME))

Check warning on line 716 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L716

Added line #L716 was not covered by tests


def run_command(ctx: click.Context, entity: typing.Union[PythonFunctionWorkflow, PythonTask]):
"""
Returns a function that is used to implement WorkflowCommand and execute a flyte workflow.
"""

def _run(*args, **kwargs):
"""
Click command function that is used to execute a flyte workflow from the given entity in the file.
"""
# By the time we get to this function, all the loading has already happened

run_level_params = ctx.obj[RUN_LEVEL_PARAMS_KEY]
Expand Down Expand Up @@ -703,37 +760,137 @@
copy_all=ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_COPY_ALL),
)

options = None
service_account = run_level_params.get("service_account")
if service_account:
# options are only passed for the execution. This is to prevent errors when registering a duplicate workflow
# It is assumed that the users expectations is to override the service account only for the execution
options = Options.default_from(k8s_service_account=service_account)

execution = remote.execute(
run_remote(

Check warning on line 763 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L763

Added line #L763 was not covered by tests
ctx,
remote,
remote_entity,
inputs=inputs,
project=project,
domain=domain,
name=run_level_params.get("name"),
wait=run_level_params.get("wait_execution"),
options=options,
project,
domain,
inputs,
run_level_params,
type_hints=entity.python_interface.inputs,
overwrite_cache=run_level_params.get("overwrite_cache"),
envs=run_level_params.get("envs"),
tags=run_level_params.get("tag"),
)

console_url = remote.generate_console_url(execution)
click.secho(f"Go to {console_url} to see execution in the console.")
return _run

Check warning on line 774 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L774

Added line #L774 was not covered by tests

if run_level_params.get("dump_snippet"):
dump_flyte_remote_snippet(execution, project, domain)

if ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_FILE_NAME):
os.remove(ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_FILE_NAME))
class DynamicLaunchPlanCommand(click.RichCommand):

Check warning on line 777 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L777

Added line #L777 was not covered by tests
"""
This is a dynamic command that is created for each launch plan. This is used to execute a launch plan.
It will fetch the launch plan from remote and create parameters from all the inputs of the launch plan.
"""

return _run
def __init__(self, name: str, h: str, lp_name: str, **kwargs):
super().__init__(name=name, help=h, **kwargs)
self._lp_name = lp_name
self._lp = None

Check warning on line 786 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L783-L786

Added lines #L783 - L786 were not covered by tests

def _fetch_launch_plan(self, ctx: click.Context) -> FlyteLaunchPlan:

Check warning on line 788 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L788

Added line #L788 was not covered by tests
if self._lp:
return self._lp
project = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_PROJECT)
domain = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_DOMAIN)
r = get_and_save_remote_with_click_context(ctx, project, domain)
self._lp = r.fetch_launch_plan(project, domain, self._lp_name)
return self._lp

Check warning on line 795 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L790-L795

Added lines #L790 - L795 were not covered by tests

def _get_params(

Check warning on line 797 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L797

Added line #L797 was not covered by tests
self,
ctx: click.Context,
inputs: typing.Dict[str, Variable],
native_inputs: typing.Dict[str, type],
fixed: typing.Dict[str, Literal],
defaults: typing.Dict[str, Parameter],
) -> typing.List["click.Parameter"]:
params = []
project = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_PROJECT)
domain = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_DOMAIN)
r = get_and_save_remote_with_click_context(ctx, project, domain)
get_upload_url_fn = functools.partial(r.client.get_upload_signed_url, project=project, domain=domain)
flyte_ctx = context_manager.FlyteContextManager.current_context()

Check warning on line 810 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L805-L810

Added lines #L805 - L810 were not covered by tests
for name, var in inputs.items():
if fixed and name in fixed:
continue
required = True

Check warning on line 814 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L813-L814

Added lines #L813 - L814 were not covered by tests
if defaults and name in defaults:
required = False
params.append(

Check warning on line 817 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L816-L817

Added lines #L816 - L817 were not covered by tests
to_click_option(ctx, flyte_ctx, name, var, native_inputs[name], None, get_upload_url_fn, required)
)
return params

Check warning on line 820 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L820

Added line #L820 was not covered by tests

def get_params(self, ctx: click.Context) -> typing.List["click.Parameter"]:

Check warning on line 822 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L822

Added line #L822 was not covered by tests
if not self.params:
self.params = []
lp = self._fetch_launch_plan(ctx)

Check warning on line 825 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L824-L825

Added lines #L824 - L825 were not covered by tests
if lp.interface:
if lp.interface.inputs:
types = TypeEngine.guess_python_types(lp.interface.inputs)
self.params = self._get_params(

Check warning on line 829 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L828-L829

Added lines #L828 - L829 were not covered by tests
ctx, lp.interface.inputs, types, lp.fixed_inputs.literals, lp.default_inputs.parameters
)

return super().get_params(ctx)

Check warning on line 833 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L833

Added line #L833 was not covered by tests

def invoke(self, ctx: click.Context) -> typing.Any:

Check warning on line 835 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L835

Added line #L835 was not covered by tests
"""
Default or None values should be ignored. Only values that are provided by the user should be passed to the
remote execution.
"""
project = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_PROJECT)
domain = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_DOMAIN)
r = get_and_save_remote_with_click_context(ctx, project, domain)
lp = self._fetch_launch_plan(ctx)
run_remote(

Check warning on line 844 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L840-L844

Added lines #L840 - L844 were not covered by tests
ctx,
r,
lp,
project,
domain,
ctx.params,
ctx.obj[RUN_LEVEL_PARAMS_KEY],
type_hints=lp.python_interface.inputs if lp.python_interface else None,
)


class RemoteLaunchPlanGroup(click.RichGroup):

Check warning on line 856 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L856

Added line #L856 was not covered by tests
"""
click multicommand that retrieves launchplans from a remote flyte instance and executes them.
"""

COMMAND_NAME = "remote-launchplan"

Check warning on line 861 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L861

Added line #L861 was not covered by tests

def __init__(self):
super().__init__(

Check warning on line 864 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L863-L864

Added lines #L863 - L864 were not covered by tests
name="from-server",
help="Retrieve launchplans from a remote flyte instance and execute them.",
params=[
click.Option(
["--limit"], help="Limit the number of launchplans to retrieve.", default=10, show_default=True
)
],
)
self._lps = []

Check warning on line 873 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L873

Added line #L873 was not covered by tests

def list_commands(self, ctx):

Check warning on line 875 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L875

Added line #L875 was not covered by tests
if self._lps:
return self._lps

Check warning on line 877 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L877

Added line #L877 was not covered by tests
if ctx.obj is None:
return self._lps
project = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_PROJECT)
domain = ctx.obj[RUN_LEVEL_PARAMS_KEY].get(CTX_DOMAIN)
l = ctx.obj[RUN_LEVEL_PARAMS_KEY].get("limit")
r = get_and_save_remote_with_click_context(ctx, project, domain)
progress = Progress(transient=True)
task = progress.add_task(f"[cyan]Gathering [{l}] remote LaunchPlans...", total=None)

Check warning on line 885 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L879-L885

Added lines #L879 - L885 were not covered by tests
with progress:
progress.start_task(task)
lps = r.client.list_launch_plan_ids_paginated(project=project, domain=domain, limit=l)

Check warning on line 888 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L887-L888

Added lines #L887 - L888 were not covered by tests
self._lps = [l.name for l in lps[0]]
return self._lps

Check warning on line 890 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L890

Added line #L890 was not covered by tests

def get_command(self, ctx, name):
return DynamicLaunchPlanCommand(name=name, h="Execute a launchplan from remote.", lp_name=name)

Check warning on line 893 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L892-L893

Added lines #L892 - L893 were not covered by tests


class WorkflowCommand(click.RichGroup):
Expand All @@ -756,6 +913,8 @@
self._entities = None

def list_commands(self, ctx):
if self._entities:
return self._entities.all()

Check warning on line 917 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L917

Added line #L917 was not covered by tests
entities = get_entities_in_file(self._filename, self._should_delete)
self._entities = entities
return entities.all()
Expand Down Expand Up @@ -806,8 +965,11 @@
for input_name, input_type_val in entity.python_interface.inputs_with_defaults.items():
literal_var = entity.interface.inputs.get(input_name)
python_type, default_val = input_type_val
required = default_val is None

Check warning on line 968 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L968

Added line #L968 was not covered by tests
params.append(
to_click_option(ctx, flyte_ctx, input_name, literal_var, python_type, default_val, get_upload_url_fn)
to_click_option(
ctx, flyte_ctx, input_name, literal_var, python_type, default_val, get_upload_url_fn, required
)
)

entity_type = "Workflow" if is_workflow else "Task"
Expand All @@ -831,13 +993,21 @@
def __init__(self, *args, **kwargs):
params = get_workflow_command_base_params()
super().__init__(*args, params=params, **kwargs)
self._files = []

Check warning on line 996 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L996

Added line #L996 was not covered by tests

def list_commands(self, ctx):
return [str(p) for p in pathlib.Path(".").glob("*.py") if str(p) != "__init__.py"]
if self._files:
return self._files

Check warning on line 1000 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L1000

Added line #L1000 was not covered by tests
self._files = [str(p) for p in pathlib.Path(".").glob("*.py") if str(p) != "__init__.py"] + [
RemoteLaunchPlanGroup.COMMAND_NAME
]
return self._files

Check warning on line 1004 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L1004

Added line #L1004 was not covered by tests

def get_command(self, ctx, filename):
if ctx.obj:
ctx.obj[RUN_LEVEL_PARAMS_KEY] = ctx.params
if filename == RemoteLaunchPlanGroup.COMMAND_NAME:
return RemoteLaunchPlanGroup()

Check warning on line 1010 in flytekit/clis/sdk_in_container/run.py

View check run for this annotation

Codecov / codecov/patch

flytekit/clis/sdk_in_container/run.py#L1010

Added line #L1010 was not covered by tests
return WorkflowCommand(filename, name=filename, help=f"Run a [workflow|task] from {filename}")


Expand Down
1 change: 0 additions & 1 deletion flytekit/core/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ def _candidate_name_matches(candidate) -> bool:
return k
except ValueError as err:
logger.warning(f"Caught ValueError {err} while attempting to auto-assign name")
pass

logger.error(f"Could not find LHS for {self} in {self._instantiated_in}")
raise _system_exceptions.FlyteSystemException(f"Error looking for LHS in {self._instantiated_in}")
Expand Down
5 changes: 5 additions & 0 deletions flytekit/core/type_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,11 @@
def to_python_value(self, ctx: FlyteContext, lv: Literal, expected_python_type: Type[T]) -> T:
return expected_python_type(lv.scalar.primitive.string_value) # type: ignore

def guess_python_type(self, literal_type: LiteralType) -> Type[enum.Enum]:
if literal_type.enum_type:
return enum.Enum("DynamicEnum", {f"{i}": i for i in literal_type.enum_type.values}) # type: ignore
raise ValueError(f"Enum transformer cannot reverse {literal_type}")

Check warning on line 1555 in flytekit/core/type_engine.py

View check run for this annotation

Codecov / codecov/patch

flytekit/core/type_engine.py#L1555

Added line #L1555 was not covered by tests


def convert_json_schema_to_python_class(schema: Dict[str, Any], schema_name: str) -> Type[Any]:
"""
Expand Down
4 changes: 2 additions & 2 deletions tests/flytekit/unit/cli/pyflyte/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ def build_image(self, img):
ImageBuildEngine.register("test", TestImageSpecBuilder())

@task
def a():
def tk():
...

mock_click_ctx = mock.MagicMock()
Expand Down Expand Up @@ -354,7 +354,7 @@ def check_image(*args, **kwargs):

mock_remote.register_script.side_effect = check_image

run_command(mock_click_ctx, a)()
run_command(mock_click_ctx, tk)()


def test_file_param():
Expand Down
Loading
Loading