Skip to content

Commit efc081e

Browse files
Finish dynamic yielding of launch plans (#92)
1 parent 190e166 commit efc081e

14 files changed

Lines changed: 389 additions & 95 deletions

File tree

flytekit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from __future__ import absolute_import
22
import flytekit.plugins
33

4-
__version__ = '0.6.2'
4+
__version__ = '0.7.0b1'

flytekit/common/component_nodes.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,24 @@ def __init__(self, sdk_workflow=None, sdk_launch_plan=None):
6565
:param flytekit.common.workflow.SdkWorkflow sdk_workflow:
6666
:param flytekit.common.launch_plan.SdkLaunchPlan sdk_launch_plan:
6767
"""
68+
if sdk_workflow and sdk_launch_plan:
69+
raise _system_exceptions.FlyteSystemException("SdkWorkflowNode cannot be called with both a workflow and "
70+
"a launchplan specified, please pick one. WF: {} LP: {}",
71+
sdk_workflow, sdk_launch_plan)
72+
6873
self._sdk_workflow = sdk_workflow
6974
self._sdk_launch_plan = sdk_launch_plan
70-
super(SdkWorkflowNode, self).__init__()
75+
sdk_wf_id = sdk_workflow.id if sdk_workflow else None
76+
sdk_lp_id = sdk_launch_plan.id if sdk_launch_plan else None
77+
super(SdkWorkflowNode, self).__init__(launchplan_ref=sdk_lp_id, sub_workflow_ref=sdk_wf_id)
78+
79+
def __repr__(self):
80+
"""
81+
:rtype: Text
82+
"""
83+
if self.sdk_workflow is not None:
84+
return "SdkWorkflowNode with workflow: {}".format(self.sdk_workflow)
85+
return "SdkWorkflowNode with launch plan: {}".format(self.sdk_launch_plan)
7186

7287
@property
7388
def launchplan_ref(self):

flytekit/common/launch_plan.py

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class SdkLaunchPlan(
2727
):
2828
def __init__(self, *args, **kwargs):
2929
super(SdkLaunchPlan, self).__init__(*args, **kwargs)
30+
# Set all the attributes we expect this class to have
3031
self._id = None
3132

3233
# The interface is not set explicitly unless fetched in an engine context
@@ -79,26 +80,6 @@ def fetch(cls, project, domain, name, version=None):
7980
sdk_lp._interface = lp_wf.interface
8081
return sdk_lp
8182

82-
@_exception_scopes.system_entry_point
83-
def register(self, project, domain, name, version):
84-
"""
85-
:param Text project:
86-
:param Text domain:
87-
:param Text name:
88-
:param Text version:
89-
"""
90-
self.validate()
91-
id_to_register = _identifier.Identifier(
92-
_identifier_model.ResourceType.LAUNCH_PLAN,
93-
project,
94-
domain,
95-
name,
96-
version
97-
)
98-
_engine_loader.get_engine().get_launch_plan(self).register(id_to_register)
99-
self._id = id_to_register
100-
return _six.text_type(self.id)
101-
10283
@property
10384
def id(self):
10485
"""
@@ -225,12 +206,43 @@ def execute_with_literals(self, project, domain, literal_inputs, name=None, noti
225206

226207
@_exception_scopes.system_entry_point
227208
def __call__(self, *args, **input_map):
228-
raise _user_exceptions.FlyteAssertion(
229-
"TODO: Implement adding of remote launch plans to workflows. Current workaround is to add remote "
230-
"workflows directly."
209+
"""
210+
:param list[T] args: Do not specify. Kwargs only are supported for this function.
211+
:param dict[Text,T] input_map: Map of inputs. Can be statically defined or OutputReference links.
212+
:rtype: flytekit.common.nodes.SdkNode
213+
"""
214+
if len(args) > 0:
215+
raise _user_exceptions.FlyteAssertion(
216+
"When adding a launchplan as a node in a workflow, all inputs must be specified with kwargs only. We "
217+
"detected {} positional args.".format(self, len(args))
218+
)
219+
220+
# Take the default values from the launch plan
221+
default_inputs = {
222+
k: v.sdk_default
223+
for k, v in _six.iteritems(self.default_inputs.parameters) if not v.required
224+
}
225+
default_inputs.update(input_map)
226+
227+
bindings, upstream_nodes = self.interface.create_bindings_for_inputs(default_inputs)
228+
229+
return _nodes.SdkNode(
230+
id=None,
231+
metadata=_workflow_models.NodeMetadata("", _datetime.timedelta(), _literal_models.RetryStrategy(0)),
232+
bindings=sorted(bindings, key=lambda b: b.var),
233+
upstream_nodes=upstream_nodes,
234+
sdk_launch_plan=self
231235
)
232236

237+
def __repr__(self):
238+
"""
239+
:rtype: Text
240+
"""
241+
return "SdkLaunchPlan(ID: {} Interface: {} WF ID: {})".format(self.id, self.interface, self.workflow_id)
242+
233243

244+
# The difference between this and the SdkLaunchPlan class is that this runnable class is supposed to only be used for
245+
# launch plans loaded alongside the current Python interpreter.
234246
class SdkRunnableLaunchPlan(
235247
_hash_mixin.HashOnReferenceMixin,
236248
SdkLaunchPlan,
@@ -272,14 +284,14 @@ def __init__(
272284
if role:
273285
auth = _launch_plan_models.Auth(assumable_iam_role=role)
274286

287+
# The constructor for SdkLaunchPlan sets the id to None anyways so we don't bother passing in an ID. The ID
288+
# should be set in one of three places,
289+
# 1) When the object is registered (in the code above)
290+
# 2) By the dynamic task code after this runnable object has already been __call__'ed. The SdkNode produced
291+
# maintains a link to this object and will set the ID according to the configuration variables present.
292+
# 3) When SdkLaunchPlan.fetch() is run
275293
super(SdkRunnableLaunchPlan, self).__init__(
276-
_identifier.Identifier(
277-
_identifier_model.ResourceType.WORKFLOW,
278-
_internal_config.PROJECT.get(),
279-
_internal_config.DOMAIN.get(),
280-
_uuid.uuid4().hex,
281-
_internal_config.VERSION.get()
282-
),
294+
None,
283295
_launch_plan_models.LaunchPlanMetadata(
284296
schedule=schedule or _schedule_model.Schedule(''),
285297
notifications=notifications or []
@@ -303,6 +315,26 @@ def __init__(
303315
self._upstream_entities = {sdk_workflow}
304316
self._sdk_workflow = sdk_workflow
305317

318+
@_exception_scopes.system_entry_point
319+
def register(self, project, domain, name, version):
320+
"""
321+
:param Text project:
322+
:param Text domain:
323+
:param Text name:
324+
:param Text version:
325+
"""
326+
self.validate()
327+
id_to_register = _identifier.Identifier(
328+
_identifier_model.ResourceType.LAUNCH_PLAN,
329+
project,
330+
domain,
331+
name,
332+
version
333+
)
334+
_engine_loader.get_engine().get_launch_plan(self).register(id_to_register)
335+
self._id = id_to_register
336+
return _six.text_type(self.id)
337+
306338
@classmethod
307339
def from_flyte_idl(cls, _):
308340
raise _user_exceptions.FlyteAssertion(
@@ -356,33 +388,8 @@ def workflow_id(self):
356388
"""
357389
return self._sdk_workflow.id
358390

359-
@_exception_scopes.system_entry_point
360-
def __call__(self, *args, **input_map):
391+
def __repr__(self):
361392
"""
362-
:param list[T] args: Do not specify. Kwargs only are supported for this function.
363-
:param dict[Text,T] input_map: Map of inputs. Can be statically defined or OutputReference links.
364-
:rtype: flytekit.common.nodes.SdkNode
393+
:rtype: Text
365394
"""
366-
if len(args) > 0:
367-
raise _user_exceptions.FlyteAssertion(
368-
"When adding a launchplan as a node in a workflow, all inputs must be specified with kwargs only. We "
369-
"detected {} positional args.".format(self, len(args))
370-
)
371-
372-
# Take the default values from the launch plan
373-
default_inputs = {
374-
k: v.sdk_default
375-
for k, v in _six.iteritems(self.default_inputs.parameters) if not v.required
376-
}
377-
default_inputs.update(input_map)
378-
379-
bindings, upstream_nodes = self.interface.create_bindings_for_inputs(default_inputs)
380-
381-
# TODO: Remove DEADBEEF
382-
return _nodes.SdkNode(
383-
id=None,
384-
metadata=_workflow_models.NodeMetadata("DEADBEEF", _datetime.timedelta(), _literal_models.RetryStrategy(0)),
385-
bindings=sorted(bindings, key=lambda b: b.var),
386-
upstream_nodes=upstream_nodes,
387-
sdk_launch_plan=self
388-
)
395+
return "SdkRunnableLaunchPlan(ID: {} Interface: {} WF ID: {})".format(self.id, self.interface, self.workflow_id)

flytekit/common/mixins/registerable.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import abc as _abc
33
import inspect as _inspect
44
import six as _six
5+
import importlib as _importlib
6+
import logging as _logging
7+
58
from flytekit.common import sdk_bases as _sdk_bases
9+
from flytekit.common.exceptions import system as _system_exceptions
10+
from flytekit.common import utils as _utils
611

712

813
class _InstanceTracker(_sdk_bases.ExtendedSdkType):
@@ -33,6 +38,10 @@ def __call__(cls, *args, **kwargs):
3338

3439
class RegisterableEntity(_six.with_metaclass(_InstanceTracker, object)):
3540

41+
def __init__(self, *args, **kwargs):
42+
self._platform_valid_name = None
43+
super(RegisterableEntity, self).__init__(*args, **kwargs)
44+
3645
@_abc.abstractmethod
3746
def register(self, project, domain, name, version):
3847
"""
@@ -73,3 +82,67 @@ def instantiated_in(self):
7382
:rtype: Optional[Text]
7483
"""
7584
return self._instantiated_in
85+
86+
@property
87+
def has_valid_name(self):
88+
"""
89+
:rtype: bool
90+
"""
91+
return self._platform_valid_name is not None and self._platform_valid_name != ""
92+
93+
@property
94+
def platform_valid_name(self):
95+
"""
96+
:rtype: Text
97+
"""
98+
return self._platform_valid_name
99+
100+
def auto_assign_name(self):
101+
"""
102+
This function is a bit of trickster Python code that goes hand in hand with the _InstanceTracker metaclass
103+
defined above. Thanks @matthewphsmith for this bit of ingenuity.
104+
105+
For instance, if a user has code that looks like this:
106+
107+
from some.other.module import wf
108+
my_launch_plan = wf.create_launch_plan()
109+
110+
@dynamic_task
111+
def sample_task(wf_params):
112+
yield my_launch_plan()
113+
114+
This code means that we should have a launch plan with a name ending in "my_launch_plan", since that is the
115+
name of the variable that the created launch plan gets assigned to. That is also the name that the launch plan
116+
would be registered with.
117+
118+
However, when the create_launch_plan() function runs, the Python interpreter has no idea where the created
119+
object will be assigned to. It has no idea that the output of the create_launch_plan call is to be paired up
120+
with a variable named "my_launch_plan". This function basically does this after the fact. Leveraging the
121+
_instantiated_in field provided by the _InstanceTracker class above, this code will re-import the
122+
module (ie Python file) that the object is in. Since it's already loaded, it's just retrieved from memory.
123+
It then scans all objects in the module, and when an object match is found, it knows it's found the right
124+
variable name.
125+
126+
Just to drive the point home, this function is mostly needed for Launch Plans. Assuming that user code has:
127+
128+
@python_task
129+
def some_task()
130+
131+
When Flytekit calls the module loader and loads the task, the name of the task is the name of the function
132+
itself. It's known at time of creation. In contrast, when
133+
134+
xyz = SomeWorflow.create_launch_plan()
135+
136+
is called, the name of the launch plan isn't known until after creation, it's not "SomeWorkflow", it's "xyz"
137+
"""
138+
_logging.debug("Running name auto assign")
139+
m = _importlib.import_module(self.instantiated_in)
140+
141+
for k in dir(m):
142+
if getattr(m, k) == self:
143+
self._platform_valid_name = _utils.fqdn(m.__name__, k, entity_type=self.resource_type)
144+
_logging.debug("Auto-assigning name to {}".format(self._platform_valid_name))
145+
return
146+
147+
_logging.error("Could not auto-assign name")
148+
raise _system_exceptions.FlyteSystemException("Error looking for object while auto-assigning name.")

flytekit/common/nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def __repr__(self):
292292
"""
293293
:rtype: Text
294294
"""
295-
return "Node({})".format(self._executable_sdk_object)
295+
return "Node(ID: {} Executable: {})".format(self.id, self._executable_sdk_object)
296296

297297

298298
class SdkNodeExecution(

0 commit comments

Comments
 (0)