prefect-client 2.14.10__py3-none-any.whl → 2.14.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import contextlib
2
3
  import contextvars
3
4
  import importlib
@@ -16,11 +17,17 @@ from rich.console import Console
16
17
  from rich.panel import Panel
17
18
  from rich.progress import Progress, SpinnerColumn, TextColumn
18
19
  from rich.prompt import Confirm
20
+ from rich.syntax import Syntax
19
21
 
22
+ from prefect.cli._prompts import prompt
20
23
  from prefect.client.orchestration import PrefectClient
21
24
  from prefect.client.schemas.actions import BlockDocumentCreate
22
25
  from prefect.client.utilities import inject_client
23
26
  from prefect.exceptions import ObjectNotFound
27
+ from prefect.settings import (
28
+ PREFECT_DEFAULT_DOCKER_BUILD_NAMESPACE,
29
+ update_current_profile,
30
+ )
24
31
  from prefect.utilities.collections import get_from_dict
25
32
  from prefect.utilities.importtools import lazy_import
26
33
 
@@ -48,46 +55,10 @@ class IamPolicyResource:
48
55
 
49
56
  def __init__(
50
57
  self,
51
- policy_name: str = "prefect-ecs-policy",
58
+ policy_name: str,
52
59
  ):
53
60
  self._iam_client = boto3.client("iam")
54
61
  self._policy_name = policy_name
55
- self._policy_document = json.dumps(
56
- {
57
- "Version": "2012-10-17",
58
- "Statement": [
59
- {
60
- "Sid": "PrefectEcsPolicy",
61
- "Effect": "Allow",
62
- "Action": [
63
- "ec2:AuthorizeSecurityGroupIngress",
64
- "ec2:CreateSecurityGroup",
65
- "ec2:CreateTags",
66
- "ec2:DescribeNetworkInterfaces",
67
- "ec2:DescribeSecurityGroups",
68
- "ec2:DescribeSubnets",
69
- "ec2:DescribeVpcs",
70
- "ecs:CreateCluster",
71
- "ecs:DeregisterTaskDefinition",
72
- "ecs:DescribeClusters",
73
- "ecs:DescribeTaskDefinition",
74
- "ecs:DescribeTasks",
75
- "ecs:ListAccountSettings",
76
- "ecs:ListClusters",
77
- "ecs:ListTaskDefinitions",
78
- "ecs:RegisterTaskDefinition",
79
- "ecs:RunTask",
80
- "ecs:StopTask",
81
- "logs:CreateLogStream",
82
- "logs:PutLogEvents",
83
- "logs:DescribeLogGroups",
84
- "logs:GetLogEvents",
85
- ],
86
- "Resource": "*",
87
- }
88
- ],
89
- }
90
- )
91
62
 
92
63
  self._requires_provisioning = None
93
64
 
@@ -146,6 +117,7 @@ class IamPolicyResource:
146
117
 
147
118
  async def provision(
148
119
  self,
120
+ policy_document: Dict[str, Any],
149
121
  advance: Callable[[], None],
150
122
  ):
151
123
  """
@@ -164,13 +136,23 @@ class IamPolicyResource:
164
136
  partial(
165
137
  self._iam_client.create_policy,
166
138
  PolicyName=self._policy_name,
167
- PolicyDocument=self._policy_document,
139
+ PolicyDocument=json.dumps(policy_document),
168
140
  )
169
141
  )
170
142
  policy_arn = policy["Policy"]["Arn"]
171
143
  advance()
172
144
  return policy_arn
173
- # TODO: read and return policy arn
145
+ else:
146
+ policy = await anyio.to_thread.run_sync(
147
+ partial(self._get_policy_by_name, self._policy_name)
148
+ )
149
+ # This should never happen, but just in case
150
+ assert policy is not None, "Could not find expected policy"
151
+ return policy["Arn"]
152
+
153
+ @property
154
+ def next_steps(self):
155
+ return []
174
156
 
175
157
 
176
158
  class IamUserResource:
@@ -246,6 +228,10 @@ class IamUserResource:
246
228
  )
247
229
  advance()
248
230
 
231
+ @property
232
+ def next_steps(self):
233
+ return []
234
+
249
235
 
250
236
  class CredentialsBlockResource:
251
237
  def __init__(self, user_name: str, block_document_name: str):
@@ -368,6 +354,10 @@ class CredentialsBlockResource:
368
354
  "$ref": {"block_document_id": str(block_doc.id)}
369
355
  }
370
356
 
357
+ @property
358
+ def next_steps(self):
359
+ return []
360
+
371
361
 
372
362
  class AuthenticationResource:
373
363
  def __init__(
@@ -375,18 +365,58 @@ class AuthenticationResource:
375
365
  work_pool_name: str,
376
366
  user_name: str = "prefect-ecs-user",
377
367
  policy_name: str = "prefect-ecs-policy",
368
+ credentials_block_name: str = None,
378
369
  ):
379
370
  self._user_name = user_name
371
+ self._credentials_block_name = (
372
+ credentials_block_name or f"{work_pool_name}-aws-credentials"
373
+ )
380
374
  self._policy_name = policy_name
375
+ self._policy_document = {
376
+ "Version": "2012-10-17",
377
+ "Statement": [
378
+ {
379
+ "Sid": "PrefectEcsPolicy",
380
+ "Effect": "Allow",
381
+ "Action": [
382
+ "ec2:AuthorizeSecurityGroupIngress",
383
+ "ec2:CreateSecurityGroup",
384
+ "ec2:CreateTags",
385
+ "ec2:DescribeNetworkInterfaces",
386
+ "ec2:DescribeSecurityGroups",
387
+ "ec2:DescribeSubnets",
388
+ "ec2:DescribeVpcs",
389
+ "ecs:CreateCluster",
390
+ "ecs:DeregisterTaskDefinition",
391
+ "ecs:DescribeClusters",
392
+ "ecs:DescribeTaskDefinition",
393
+ "ecs:DescribeTasks",
394
+ "ecs:ListAccountSettings",
395
+ "ecs:ListClusters",
396
+ "ecs:ListTaskDefinitions",
397
+ "ecs:RegisterTaskDefinition",
398
+ "ecs:RunTask",
399
+ "ecs:StopTask",
400
+ "logs:CreateLogStream",
401
+ "logs:PutLogEvents",
402
+ "logs:DescribeLogGroups",
403
+ "logs:GetLogEvents",
404
+ ],
405
+ "Resource": "*",
406
+ }
407
+ ],
408
+ }
381
409
  self._iam_user_resource = IamUserResource(user_name=user_name)
382
410
  self._iam_policy_resource = IamPolicyResource(policy_name=policy_name)
383
411
  self._credentials_block_resource = CredentialsBlockResource(
384
- user_name=user_name, block_document_name=f"{work_pool_name}-aws-credentials"
412
+ user_name=user_name, block_document_name=self._credentials_block_name
385
413
  )
414
+ self._execution_role_resource = ExecutionRoleResource()
386
415
 
387
416
  @property
388
417
  def resources(self):
389
418
  return [
419
+ self._execution_role_resource,
390
420
  self._iam_user_resource,
391
421
  self._iam_policy_resource,
392
422
  self._credentials_block_resource,
@@ -439,10 +469,25 @@ class AuthenticationResource:
439
469
  infrastructure for.
440
470
  advance: A callback function to indicate progress.
441
471
  """
472
+ # Provision task execution role
473
+ role_arn = await self._execution_role_resource.provision(
474
+ base_job_template=base_job_template, advance=advance
475
+ )
476
+ # Update policy document with the role ARN
477
+ self._policy_document["Statement"].append(
478
+ {
479
+ "Sid": "AllowPassRoleForEcs",
480
+ "Effect": "Allow",
481
+ "Action": "iam:PassRole",
482
+ "Resource": role_arn,
483
+ }
484
+ )
442
485
  # Provision the IAM user
443
486
  await self._iam_user_resource.provision(advance=advance)
444
487
  # Provision the IAM policy
445
- policy_arn = await self._iam_policy_resource.provision(advance=advance)
488
+ policy_arn = await self._iam_policy_resource.provision(
489
+ policy_document=self._policy_document, advance=advance
490
+ )
446
491
  # Attach the policy to the user
447
492
  if policy_arn:
448
493
  iam_client = boto3.client("iam")
@@ -458,6 +503,14 @@ class AuthenticationResource:
458
503
  advance=advance,
459
504
  )
460
505
 
506
+ @property
507
+ def next_steps(self):
508
+ return [
509
+ next_step
510
+ for resource in self.resources
511
+ for next_step in resource.next_steps
512
+ ]
513
+
461
514
 
462
515
  class ClusterResource:
463
516
  def __init__(self, cluster_name: str = "prefect-ecs-cluster"):
@@ -535,13 +588,22 @@ class ClusterResource:
535
588
  "default"
536
589
  ] = self._cluster_name
537
590
 
591
+ @property
592
+ def next_steps(self):
593
+ return []
594
+
538
595
 
539
596
  class VpcResource:
540
- def __init__(self, vpc_name: str = "prefect-ecs-vpc"):
597
+ def __init__(
598
+ self,
599
+ vpc_name: str = "prefect-ecs-vpc",
600
+ ecs_security_group_name: str = "prefect-ecs-security-group",
601
+ ):
541
602
  self._ec2_client = boto3.client("ec2")
542
603
  self._ec2_resource = boto3.resource("ec2")
543
604
  self._vpc_name = vpc_name
544
605
  self._requires_provisioning = None
606
+ self._ecs_security_group_name = ecs_security_group_name
545
607
 
546
608
  async def get_task_count(self):
547
609
  """
@@ -746,7 +808,7 @@ class VpcResource:
746
808
  await anyio.to_thread.run_sync(
747
809
  partial(
748
810
  self._ec2_resource.create_security_group,
749
- GroupName="prefect-ecs-security-group",
811
+ GroupName=self._ecs_security_group_name,
750
812
  Description=(
751
813
  "Block all inbound traffic and allow all outbound traffic"
752
814
  ),
@@ -762,6 +824,269 @@ class VpcResource:
762
824
  vpc.id
763
825
  )
764
826
 
827
+ @property
828
+ def next_steps(self):
829
+ return []
830
+
831
+
832
+ class ContainerRepositoryResource:
833
+ def __init__(self, work_pool_name: str, repository_name: str = "prefect-flows"):
834
+ self._ecr_client = boto3.client("ecr")
835
+ self._repository_name = repository_name
836
+ self._requires_provisioning = None
837
+ self._work_pool_name = work_pool_name
838
+ self._next_steps = []
839
+
840
+ async def get_task_count(self):
841
+ """
842
+ Returns the number of tasks that will be executed to provision this resource.
843
+
844
+ Returns:
845
+ int: The number of tasks to be provisioned.
846
+ """
847
+ return 3 if await self.requires_provisioning() else 0
848
+
849
+ async def _get_prefect_created_registry(self):
850
+ try:
851
+ registries = await anyio.to_thread.run_sync(
852
+ partial(
853
+ self._ecr_client.describe_repositories,
854
+ repositoryNames=[self._repository_name],
855
+ )
856
+ )
857
+ return next(iter(registries), None)
858
+ except self._ecr_client.exceptions.RepositoryNotFoundException:
859
+ return None
860
+
861
+ async def requires_provisioning(self) -> bool:
862
+ """
863
+ Check if this resource requires provisioning.
864
+
865
+ Returns:
866
+ bool: True if provisioning is required, False otherwise.
867
+ """
868
+ if self._requires_provisioning is not None:
869
+ return self._requires_provisioning
870
+
871
+ if await self._get_prefect_created_registry() is not None:
872
+ self._requires_provisioning = False
873
+ return False
874
+
875
+ self._requires_provisioning = True
876
+ return True
877
+
878
+ async def get_planned_actions(self) -> List[str]:
879
+ """
880
+ Returns a description of the planned actions for provisioning this resource.
881
+
882
+ Returns:
883
+ Optional[str]: A description of the planned actions for provisioning the resource,
884
+ or None if provisioning is not required.
885
+ """
886
+ if await self.requires_provisioning():
887
+ return [
888
+ "Creating an ECR repository for storing Prefect images:"
889
+ f" [blue]{self._repository_name}[/]"
890
+ ]
891
+ return []
892
+
893
+ async def provision(
894
+ self,
895
+ base_job_template: Dict[str, Any],
896
+ advance: Callable[[], None],
897
+ ):
898
+ """
899
+ Provisions an ECR repository.
900
+
901
+ Args:
902
+ base_job_template: The base job template of the work pool to provision
903
+ infrastructure for.
904
+ advance: A callback function to indicate progress.
905
+ """
906
+ if await self.requires_provisioning():
907
+ console = current_console.get()
908
+ console.print("Provisioning ECR repository")
909
+ response = await anyio.to_thread.run_sync(
910
+ partial(
911
+ self._ecr_client.create_repository,
912
+ repositoryName=self._repository_name,
913
+ )
914
+ )
915
+ advance()
916
+ console.print("Authenticating with ECR")
917
+ auth_token = self._ecr_client.get_authorization_token()
918
+ user, passwd = (
919
+ base64.b64decode(
920
+ auth_token["authorizationData"][0]["authorizationToken"]
921
+ )
922
+ .decode()
923
+ .split(":")
924
+ )
925
+ proxy_endpoint = auth_token["authorizationData"][0]["proxyEndpoint"]
926
+ await run_process(f"docker login -u {user} -p {passwd} {proxy_endpoint}")
927
+ advance()
928
+ console.print("Setting default Docker build namespace")
929
+ namespace = response["repository"]["repositoryUri"].split("/")[0]
930
+ update_current_profile({PREFECT_DEFAULT_DOCKER_BUILD_NAMESPACE: namespace})
931
+ self._update_next_steps(namespace)
932
+ advance()
933
+
934
+ def _update_next_steps(self, repository_uri: str):
935
+ self._next_steps.extend(
936
+ [
937
+ dedent(
938
+ f"""\
939
+
940
+ Your default Docker build namespace has been set to [blue]{repository_uri!r}[/].
941
+
942
+ To build and push a Docker image to your newly created repository, use [blue]{self._repository_name!r}[/] as your image name:
943
+ """
944
+ ),
945
+ Panel(
946
+ Syntax(
947
+ dedent(
948
+ f"""\
949
+ from prefect import flow
950
+ from prefect.deployments import DeploymentImage
951
+
952
+
953
+ @flow(log_prints=True)
954
+ def my_flow(name: str = "world"):
955
+ print(f"Hello {{name}}! I'm a flow running on ECS!")
956
+
957
+
958
+ if __name__ == "__main__":
959
+ my_flow.deploy(
960
+ name="my-deployment",
961
+ work_pool_name="{self._work_pool_name}",
962
+ image=DeploymentImage(
963
+ name="{self._repository_name}:latest",
964
+ platform="linux/amd64",
965
+ )
966
+ )"""
967
+ ),
968
+ "python",
969
+ background_color="default",
970
+ ),
971
+ title="example_deploy_script.py",
972
+ expand=False,
973
+ ),
974
+ ]
975
+ )
976
+
977
+ @property
978
+ def next_steps(self):
979
+ return self._next_steps
980
+
981
+
982
+ class ExecutionRoleResource:
983
+ def __init__(self, execution_role_name: str = "PrefectEcsTaskExecutionRole"):
984
+ self._iam_client = boto3.client("iam")
985
+ self._execution_role_name = execution_role_name
986
+ self._trust_policy_document = json.dumps(
987
+ {
988
+ "Version": "2012-10-17",
989
+ "Statement": [
990
+ {
991
+ "Effect": "Allow",
992
+ "Principal": {"Service": "ecs-tasks.amazonaws.com"},
993
+ "Action": "sts:AssumeRole",
994
+ }
995
+ ],
996
+ }
997
+ )
998
+ self._requires_provisioning = None
999
+
1000
+ async def get_task_count(self):
1001
+ """
1002
+ Returns the number of tasks that will be executed to provision this resource.
1003
+
1004
+ Returns:
1005
+ int: The number of tasks to be provisioned.
1006
+ """
1007
+ return 1 if await self.requires_provisioning() else 0
1008
+
1009
+ async def requires_provisioning(self) -> bool:
1010
+ """
1011
+ Check if this resource requires provisioning.
1012
+
1013
+ Returns:
1014
+ bool: True if provisioning is required, False otherwise.
1015
+ """
1016
+ if self._requires_provisioning is None:
1017
+ try:
1018
+ await anyio.to_thread.run_sync(
1019
+ partial(
1020
+ self._iam_client.get_role, RoleName=self._execution_role_name
1021
+ )
1022
+ )
1023
+ self._requires_provisioning = False
1024
+ except self._iam_client.exceptions.NoSuchEntityException:
1025
+ self._requires_provisioning = True
1026
+
1027
+ return self._requires_provisioning
1028
+
1029
+ async def get_planned_actions(self) -> List[str]:
1030
+ """
1031
+ Returns a description of the planned actions for provisioning this resource.
1032
+
1033
+ Returns:
1034
+ Optional[str]: A description of the planned actions for provisioning the resource,
1035
+ or None if provisioning is not required.
1036
+ """
1037
+ if await self.requires_provisioning():
1038
+ return [
1039
+ "Creating an IAM role assigned to ECS tasks:"
1040
+ f" [blue]{self._execution_role_name}[/]"
1041
+ ]
1042
+ return []
1043
+
1044
+ async def provision(
1045
+ self,
1046
+ base_job_template: Dict[str, Any],
1047
+ advance: Callable[[], None],
1048
+ ):
1049
+ """
1050
+ Provisions an IAM role.
1051
+
1052
+ Args:
1053
+ base_job_template: The base job template of the work pool to provision
1054
+ infrastructure for.
1055
+ advance: A callback function to indicate progress.
1056
+ """
1057
+ if await self.requires_provisioning():
1058
+ console = current_console.get()
1059
+ console.print("Provisioning execution role")
1060
+ response = await anyio.to_thread.run_sync(
1061
+ partial(
1062
+ self._iam_client.create_role,
1063
+ RoleName=self._execution_role_name,
1064
+ Description="Role for ECS tasks to access ECR and other resources.",
1065
+ AssumeRolePolicyDocument=self._trust_policy_document,
1066
+ )
1067
+ )
1068
+ await anyio.to_thread.run_sync(
1069
+ partial(
1070
+ self._iam_client.attach_role_policy,
1071
+ RoleName=self._execution_role_name,
1072
+ PolicyArn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
1073
+ )
1074
+ )
1075
+ advance()
1076
+ else:
1077
+ response = await anyio.to_thread.run_sync(
1078
+ partial(self._iam_client.get_role, RoleName=self._execution_role_name)
1079
+ )
1080
+
1081
+ base_job_template["variables"]["properties"]["execution_role_arn"][
1082
+ "default"
1083
+ ] = response["Role"]["Arn"]
1084
+ return response["Role"]["Arn"]
1085
+
1086
+ @property
1087
+ def next_steps(self):
1088
+ return []
1089
+
765
1090
 
766
1091
  class ElasticContainerServicePushProvisioner:
767
1092
  """
@@ -797,11 +1122,33 @@ class ElasticContainerServicePushProvisioner:
797
1122
  except ModuleNotFoundError:
798
1123
  return False
799
1124
 
800
- def _generate_resources(self, work_pool_name: str):
1125
+ def _generate_resources(
1126
+ self,
1127
+ work_pool_name: str,
1128
+ user_name: str = "prefect-ecs-user",
1129
+ policy_name: str = "prefect-ecs-policy",
1130
+ credentials_block_name: str = None,
1131
+ cluster_name: str = "prefect-ecs-cluster",
1132
+ vpc_name: str = "prefect-ecs-vpc",
1133
+ ecs_security_group_name: str = "prefect-ecs-security-group",
1134
+ repository_name: str = "prefect-flows",
1135
+ ):
801
1136
  return [
802
- AuthenticationResource(work_pool_name=work_pool_name),
803
- ClusterResource(),
804
- VpcResource(),
1137
+ AuthenticationResource(
1138
+ work_pool_name=work_pool_name,
1139
+ user_name=user_name,
1140
+ policy_name=policy_name,
1141
+ credentials_block_name=credentials_block_name,
1142
+ ),
1143
+ ClusterResource(cluster_name=cluster_name),
1144
+ VpcResource(
1145
+ vpc_name=vpc_name,
1146
+ ecs_security_group_name=ecs_security_group_name,
1147
+ ),
1148
+ ContainerRepositoryResource(
1149
+ work_pool_name=work_pool_name,
1150
+ repository_name=repository_name,
1151
+ ),
805
1152
  ]
806
1153
 
807
1154
  async def provision(
@@ -833,7 +1180,88 @@ class ElasticContainerServicePushProvisioner:
833
1180
  )
834
1181
 
835
1182
  try:
836
- resources = self._generate_resources(work_pool_name=work_pool_name)
1183
+ if self.console.is_interactive and Confirm.ask(
1184
+ "Would you like to customize the resource names for your"
1185
+ " infrastructure? This includes an IAM user, IAM policy, ECS cluster,"
1186
+ " VPC, ECS security group, and ECR repository."
1187
+ ):
1188
+ user_name = prompt(
1189
+ "Enter a name for the IAM user (manages ECS tasks)",
1190
+ default="prefect-ecs-user",
1191
+ )
1192
+ policy_name = prompt(
1193
+ (
1194
+ "Enter a name for the IAM policy (defines ECS task execution"
1195
+ " and image management permissions)"
1196
+ ),
1197
+ default="prefect-ecs-policy",
1198
+ )
1199
+ cluster_name = prompt(
1200
+ "Enter a name for the ECS cluster (hosts ECS tasks)",
1201
+ default="prefect-ecs-cluster",
1202
+ )
1203
+ credentials_name = prompt(
1204
+ (
1205
+ "Enter a name for the AWS credentials block (stores AWS"
1206
+ " credentials for managing ECS tasks)"
1207
+ ),
1208
+ default=f"{work_pool_name}-aws-credentials",
1209
+ )
1210
+ vpc_name = prompt(
1211
+ (
1212
+ "Enter a name for the VPC (provides network isolation for ECS"
1213
+ " tasks)"
1214
+ ),
1215
+ default="prefect-ecs-vpc",
1216
+ )
1217
+ ecs_security_group_name = prompt(
1218
+ (
1219
+ "Enter a name for the ECS security group (controls task network"
1220
+ " traffic)"
1221
+ ),
1222
+ default="prefect-ecs-security-group",
1223
+ )
1224
+ repository_name = prompt(
1225
+ (
1226
+ "Enter a name for the ECR repository (stores Docker images for"
1227
+ " ECS tasks)"
1228
+ ),
1229
+ default="prefect-flows",
1230
+ )
1231
+
1232
+ provision_preview = Panel(
1233
+ dedent(
1234
+ f"""\
1235
+ Custom names for infrastructure resources for
1236
+ [blue]{work_pool_name}[/]:
1237
+
1238
+ - IAM user: [blue]{user_name}[/]
1239
+ - IAM policy: [blue]{policy_name}[/]
1240
+ - ECS cluster: [blue]{cluster_name}[/]
1241
+ - AWS credentials block: [blue]{credentials_name}[/]
1242
+ - VPC: [blue]{vpc_name}[/]
1243
+ - ECS security group: [blue]{ecs_security_group_name}[/]
1244
+ - ECR repository: [blue]{repository_name}[/]
1245
+ """
1246
+ ),
1247
+ expand=False,
1248
+ )
1249
+
1250
+ self.console.print(provision_preview)
1251
+
1252
+ resources = self._generate_resources(
1253
+ work_pool_name=work_pool_name,
1254
+ user_name=user_name,
1255
+ policy_name=policy_name,
1256
+ credentials_block_name=credentials_name,
1257
+ cluster_name=cluster_name,
1258
+ vpc_name=vpc_name,
1259
+ ecs_security_group_name=ecs_security_group_name,
1260
+ repository_name=repository_name,
1261
+ )
1262
+
1263
+ else:
1264
+ resources = self._generate_resources(work_pool_name=work_pool_name)
837
1265
 
838
1266
  with Progress(
839
1267
  SpinnerColumn(),
@@ -879,6 +1307,7 @@ class ElasticContainerServicePushProvisioner:
879
1307
  # provision calls will be no-ops, but update the base job template
880
1308
 
881
1309
  base_job_template_copy = deepcopy(base_job_template)
1310
+ next_steps = []
882
1311
  with Progress(console=self._console, disable=num_tasks == 0) as progress:
883
1312
  task = progress.add_task(
884
1313
  "Provisioning Infrastructure",
@@ -890,6 +1319,12 @@ class ElasticContainerServicePushProvisioner:
890
1319
  advance=partial(progress.advance, task),
891
1320
  base_job_template=base_job_template_copy,
892
1321
  )
1322
+ next_steps.append(resource.next_steps)
1323
+
1324
+ if next_steps:
1325
+ for step in next_steps:
1326
+ for item in step:
1327
+ self._console.print(item)
893
1328
 
894
1329
  if num_tasks > 0:
895
1330
  self._console.print(
@@ -0,0 +1,11 @@
1
+ from .actions import create_flow_run_input, delete_flow_run_input, read_flow_run_input
2
+ from .run_input import RunInput, keyset_from_base_key, keyset_from_paused_state
3
+
4
+ __all__ = [
5
+ "RunInput",
6
+ "create_flow_run_input",
7
+ "keyset_from_base_key",
8
+ "keyset_from_paused_state",
9
+ "delete_flow_run_input",
10
+ "read_flow_run_input",
11
+ ]