-
Notifications
You must be signed in to change notification settings - Fork 14.3k
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
[AIRFLOW-8057] [AIP-31] Add @task decorator #8962
Conversation
Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contribution Guide (https://github.com/apache/airflow/blob/master/CONTRIBUTING.rst)
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow what a first commit :). I think mine was a 1 line bug fix.
airflow/models/xcom_arg.py
Outdated
@@ -83,7 +83,7 @@ def __getitem__(self, item): | |||
""" | |||
Implements xcomresult['some_result_key'] | |||
""" | |||
return XComArg(operator=self.operator, key=item) | |||
return XComArg(operator=self.operator, key=str(item)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the cast? Seems being explicit about the type received here might be safer, and the cast can happen on the caller side, otherwise this weakens type safety of this arg for all XCOM use cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Key will always be a string as it's casted when pushing the field to the XCom DB. This is just to make things easier to use when doing multiple_outputs also to ensure consistency. This allows you to do:
res[1]
Which in reality is more transparent.
It was a change we missed in #8652
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any chance you could pull this (and the test for it) out to a separate PR please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
related discussion: databand-ai#5 (comment)
This small cast makes the multiple_output
piece work more reliably and more transparently to the user. Not actually a new feature, but mostly a fix of what already got merged in XComArg.
I can break it into a separate PR but note that XCom class already does this when saving the key (not when retrieving it)
airflow/operators/python.py
Outdated
) -> None: | ||
# Check if we need to generate a new task_id | ||
task_id = kwargs.get('task_id', None) | ||
dag = kwargs.get('dag', None) or DagContext.get_current_dag() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this function play nicely if neither side of the or returns true and task_id is None, e.g. if someone initializes a task and then adds it to a DAG later?
I guess it doesn't really make sense with this pattern so specify dag_id later, so I think raising an exception if DAG is not specified would be reasonable (+accompanying unit test).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DAG can be not specified when initialized outside of DAG context. And you want to make it to bind when you call it. If you manually assign it the DAG, then it wont work thats true. Not sure if I should check this here.
airflow/operators/python.py
Outdated
) -> None: | ||
# Check if we need to generate a new task_id | ||
task_id = kwargs.get('task_id', None) | ||
dag = kwargs.get('dag', None) or DagContext.get_current_dag() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why default 'dag'/task_id to None?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dag
-> because we may declare the task outside without an explicit dag (and we want to fallback to current_dag then.
task_id
-> not really any reason. can switch it.
airflow/operators/python.py
Outdated
super().__init__(*args, **kwargs) | ||
self.python_callable = python_callable | ||
self.multiple_outputs = multiple_outputs | ||
self._kwargs = kwargs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does the use of kwargs relate to their deprecation in Airflow 2.0:
e.g. in the BaseOperator code:
if args or kwargs:
# TODO remove *args and **kwargs in Airflow 2.0
warnings.warn(
'Invalid arguments were passed to {c} (task_id: {t}). '
'Support for passing such arguments will be dropped in '
'Airflow 2.0. Invalid arguments were:'
'\n*args: {a}\n**kwargs: {k}'.format(
c=self.__class__.__name__, a=args, k=kwargs, t=task_id),
category=PendingDeprecationWarning,
stacklevel=3
)
Does this need a deprecation warning too? Should we just not allow kwargs/args here in the first place, or is it needed for backwards compatibility?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This already calls BaseOperator.__init__
so guessing it will already give you a warning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just drop the kwargs/etc support in the new operator though since it's deprecated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think that would be better. +1, for dropping it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean here. How then is the operator supposed to pass down kwargs like owner
that is a BaseOperator valid kwarg? If I remove kwargs here, then I won't be able to set owner in this operator.
airflow/operators/python.py
Outdated
|
||
def __call__(self, *args, **kwargs): | ||
# If args/kwargs are set, then operator has been called. Raise exception | ||
if self._called: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why the functions can't be reused seems a bit annoying for users, wonder if we can fix this (e.g. late-binding the task_ids or something...) or add a TODO, might be worth adding a comment here. ._checking if it was already called feels a bit hacky as it kind of couples the task execution with global state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mainly is bc of 1 function == 1 operator. The alternative was the idea that @evgenyshulman proposed which is to use functions as operators generators. This simplifies a bit, but also then you can't use the change the operator later on (it's never accessible in the DAG file itself). I've seen both approaches take here (either 1to1 or 1tomany). For me I like better the 1to1, but mainly bc it allows you to use it as you would use any operator later and feels a bit more intuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a exeuction time error, or does this happen at parse time?
We don't appear to have any tests that cover this (or I missed it). Can you add some, and add a section about this to the docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I correctly understand that this will not work?
@task
def update_user(user_id: str):
...
with DAG(...):
# Fetch list of users
...
# Execute task for each user
for user_id in users_list:
update_user(user_id)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will fail at parse time.
Correct. 1 function == 1 operator. Airflow doesn't allow dynamic operators (execute 1 operator several times). You can still work around it.
This will work though:
@task
def update_user(user_id: str):
...
with DAG(...):
# Fetch list of users
...
# Execute task for each user
for user_id in users_list:
update_user.copy(f'update_{user_id}')(user_id)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I have mixed feelings:
copy
is not self-explanatory in this case imho- in such case, shouldn't we generate auto id? Or at least can we try to do
update_user(user_id, task_id=user_id)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with turbaszek. This is not self-explanatory at all. I think having update_user(user_id, task_id=...)
would be much better. We can access the function signature inside task
. This should work and is more self-explanatory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Main worry is that then what is update_user
. What you are describing here is using update_user
as an operator factory. It has it's value, but it also feels too magic to me atm. If update_user
is a factory, then you can't change the operator instance at all or use it to set non-data dependencies.
We could capture task_id kwarg and generate a new operator, but then what is update_user
the first operator, the latest one? What does update_user
represent?
You can either do (1) update_user(i) for i in range(20)
or (2) update_user >> other_operation
, but not both. I prefer to support 2nd option as it adapts more to what Airflow already does with operators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could capture task_id kwarg and generate a new operator, but then what is
update_user
the first operator, the latest one? What doesupdate_user
represent?
For me update_user
is a function and as a function it can be called many times with different input thus yielding different results (here creating new task). I have never meet "function as a singleton" pattern. If we don't want to generate task_id
for users then we may consider raising an exception on second invocation when no custom task_id
is passed.
My point is: this is a function, I expect to be able to call it as many time as I wish. I expect Airflow to treat each call of this function (in proper context) as creating a new task.
You can either do (1)
update_user(i) for i in range(20)
or (2)update_user >> other_operation
, but not both. I prefer to support 2nd option as it adapts more to what Airflow already does with operators.
Why should I not be able to do this? This is something that I saw many times.
first_task = BashOperator()
last_task = BashOperator()
for user_id in users_list:
first_task >> update_user(user_id) >> last_task
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this use case is exactly what we want to support.
The issue being we'd have to generate a new task for each one (doable, that isn't a problem, we just need to work out what task_id to call it).
Perhaps taking a leaf out of pytest or https://pypi.org/project/parameterized/ for how they name functions/test cases?
airflow/operators/python.py
Outdated
raise AirflowException('@task decorated functions can only be called once. If you need to reuse ' | ||
'it several times in a DAG, use the `copy` method.') | ||
|
||
# If we have no DAG, reinitialize class to capture DAGContext and DAG default args. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not great that this and potential parse-time errors that could occur here is moved to runtime instead of DAG parse time, another reason it might be worth thinking about a more parse-time friendly solution if possible (or what's stopping Airflow from supporting this at the current time).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to other options. This seemed the cleanest one. We are basically calling init again so that we capture default_args from the dag. The other option is to manually implement default_args here.
Also note that we will get parse errors in the declaration if there's any. So this will be for default_args itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What use case/code path is this enabling?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defining a task decorated operator without a DAG and adding it to the DAG on __call__
.
@taks
def add_2(num)
return num+2
with DAG(...):
add_2(2)
Otherwise this does not work. Also if we define default_args
in DAG
we wont be able to capture it either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh gotcha, that is a good workflow to enable.
Is this covered in tests? (Sorry, finding it hard to keep track of all the discussions at the moment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same tbh. I think given the discussion on calling a decorated function several times, I'll refactor to accomodate that. This should simplify code a bit as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, not sure if we should use the same approach from parametrized, as you may want to run an operation twice with the same args/kwargs. Also, if the arg/kwarg is a XComArg, what should we do?
airflow/operators/python.py
Outdated
|
||
def __call__(self, *args, **kwargs): | ||
# If args/kwargs are set, then operator has been called. Raise exception | ||
if self._called: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I correctly understand that this will not work?
@task
def update_user(user_id: str):
...
with DAG(...):
# Fetch list of users
...
# Execute task for each user
for user_id in users_list:
update_user(user_id)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be ace!
airflow/operators/python.py
Outdated
raise AirflowException('@task decorated functions can only be called once. If you need to reuse ' | ||
'it several times in a DAG, use the `copy` method.') | ||
|
||
# If we have no DAG, reinitialize class to capture DAGContext and DAG default args. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What use case/code path is this enabling?
airflow/operators/python.py
Outdated
|
||
def __call__(self, *args, **kwargs): | ||
# If args/kwargs are set, then operator has been called. Raise exception | ||
if self._called: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a exeuction time error, or does this happen at parse time?
We don't appear to have any tests that cover this (or I missed it). Can you add some, and add a section about this to the docs.
tests/operators/test_python.py
Outdated
do_run() | ||
assert ['do_run'] == self.dag.task_ids | ||
do_run_1 = do_run.copy() | ||
do_run_2 = do_run.copy() | ||
assert do_run_1.task_id == 'do_run__1' | ||
assert do_run_2.task_id == 'do_run__2' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do_run() | |
assert ['do_run'] == self.dag.task_ids | |
do_run_1 = do_run.copy() | |
do_run_2 = do_run.copy() | |
assert do_run_1.task_id == 'do_run__1' | |
assert do_run_2.task_id == 'do_run__2' | |
run = do_run() | |
do_run_1 = do_run.copy() | |
do_run_2 = do_run.copy() | |
assert ['do_run', 'do_run__1', 'do_run__2'] == self.dag.task_ids | |
assert run.task_id == 'do_run' | |
assert do_run_1.task_id == 'do_run__1' | |
assert do_run_2.task_id == 'do_run__2' |
(I think)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mostly, except:
assert do_run.task_id == 'do_run'
run
here is an XComArg representing the value returned in the do_run operator.
tests/operators/test_python.py
Outdated
def return_dict(number: int): | ||
return { | ||
'number': number + 1, | ||
43: 43 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should test (and work out what we want) when you do return { 43: 43, '43': 42 }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XCom automatically casts keys to strings. That's the reason for adding casting in XComArg/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I mean we should define, and test, what we want the behaviour to be in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh I see, sorry did not fully understand the issue. Not sure what the best path for this may be. We can raise an exception (not great) or log weird usage.
Do copy what I put in slack, Thanks to Stackoverflow I've got a way of having diff --git airflow/task/__init__.py airflow/task/__init__.py
index 114d189da..0b15c9a8c 100644
--- airflow/task/__init__.py
+++ airflow/task/__init__.py
@@ -16,3 +16,20 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+
+import sys
+import types
+
+
+class CallableModule(types.ModuleType):
+ def __init__(self):
+ types.ModuleType.__init__(self, __name__)
+ self.__dict__.update(sys.modules[__name__].__dict__)
+
+ __all__ = list(set(vars().keys()) - {'__qualname__'}) # for python 2 and 3
+
+ def __call__(self, *args, **kwargs):
+ from airlfow.decorators import task
+ return task(*args, **kwargs)
+
+sys.modules[__name__] = CallableModule() This works in Py 2.7, 3.7 and 3.8 (versions I have easy access to). The main question is: do we want to support that? Is this "hack" worth it, and do IDEs get massively confused by this? |
I think if IDEs support it, and we add tests for it I'd say, yes. I wonder if we can do this in STATICA_HACK = True
globals()['kcah_acitats'[::-1].upper()] = False
if STATICA_HACK: # pragma: no cover
from airflow.models.dag import DAG
from airflow.exceptions import AirflowException
from airflow.decorators import task Without confusing IDEs more. |
I prefer to avoid doing that. Seems to add quite a bit of code complexity and I doubt it will impact significantly the user experience. I think it should be fine from users to use |
airflow/operators/python.py
Outdated
super().__init__(*args, **kwargs) | ||
self.python_callable = python_callable | ||
self.multiple_outputs = multiple_outputs | ||
self._kwargs = kwargs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think that would be better. +1, for dropping it.
airflow/operators/python.py
Outdated
|
||
def __call__(self, *args, **kwargs): | ||
# If args/kwargs are set, then operator has been called. Raise exception | ||
if self._called: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with turbaszek. This is not self-explanatory at all. I think having update_user(user_id, task_id=...)
would be much better. We can access the function signature inside task
. This should work and is more self-explanatory.
airflow/operators/python.py
Outdated
@staticmethod | ||
def _get_unique_task_id(task_id: str, dag: Optional[DAG]) -> str: | ||
dag = dag or DagContext.get_current_dag() | ||
if not dag or task_id not in dag.task_ids: | ||
return task_id | ||
core = re.split(r'__\d+$', task_id)[0] | ||
suffixes = sorted( | ||
[int(re.split(r'^.+__', task_id)[1]) | ||
for task_id in dag.task_ids | ||
if re.match(rf'^{core}__\d+$', task_id)] | ||
) | ||
if not suffixes: | ||
return f'{core}__1' | ||
return f'{core}__{suffixes[-1] + 1}' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are using F-Strings here, too - not only in examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we also use type annotations which is py3 only. We should probs decide if we want to support Python2 or not at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use f-strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we can use f-strings here, and manually fix it up when backporting -- it's not too much work on the release-manager/who ever does the backport.
When it comes time to backport this I can run someone through the process (or better yet, I should document this process)
|
airflow/operators/python.py
Outdated
@staticmethod | ||
def _get_unique_task_id(task_id: str, dag: Optional[DAG]) -> str: | ||
dag = dag or DagContext.get_current_dag() | ||
if not dag or task_id not in dag.task_ids: | ||
return task_id | ||
core = re.split(r'__\d+$', task_id)[0] | ||
suffixes = sorted( | ||
[int(re.split(r'^.+__', task_id)[1]) | ||
for task_id in dag.task_ids | ||
if re.match(rf'^{core}__\d+$', task_id)] | ||
) | ||
if not suffixes: | ||
return f'{core}__1' | ||
return f'{core}__{suffixes[-1] + 1}' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use f-strings
airflow/operators/python.py
Outdated
""" | ||
Python operator decorator. Wraps a function into an Airflow operator. | ||
Accepts kwargs for operator kwarg. Will try to wrap operator into DAG at declaration or | ||
on function invocation. Use alias to reuse function in the DAG. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still valid?
airflow/operators/python.py
Outdated
kwargs['task_id'] = self._get_unique_task_id(kwargs['task_id'], kwargs.get('dag', None)) | ||
super().__init__(**kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kwargs['task_id'] = self._get_unique_task_id(kwargs['task_id'], kwargs.get('dag', None)) | |
super().__init__(**kwargs) | |
super().__init__(**kwargs) | |
self.task_id = self._get_unique_task_id(kwargs['task_id'], kwargs.get('dag', None)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this preferred?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No strong opinion here, I just think this is more explicit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nvm, seems this proposed change fails due to task_id being checked if repeated on super().__init__(**kwargs)
Resolving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see another issue here. Currently, if task_id
is not provided user will get KeyError: 'task_id'
instead of TypeError: __init__() missing 1 required positional argument: 'task_id'
Also, this seems to work as expected:
In [8]: class CustomOp(BaseOperator):
...: def __init__(self, a, b, *args, **kwargs):
...: super().__init__(*args, **kwargs)
...: self.task_id = "other task id"
...:
In [9]: op = CustomOp(a=1, b=2, task_id="task_id")
In [10]: op.task_id
Out[10]: 'other task id'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_PythonFunctionalOperator
is a private operator. Aka it should only be used with @task
which does always set the task_id.
Will make the field mandatory just in case.
Hi @casassg is there any way we can help you with moving on? 😉 |
Mostly adding more hours to my day. Sorry, been a bit busy this week. Will try to address most comments |
364243d
to
ca98528
Compare
@casassg flake8 is sad, if you wish I can fix it |
ca98528
to
078c5de
Compare
I think I fixed it w latest commit. feel free to push a commit fixing it if it does not. Ran pre-commit locally and it passed |
Two small comments, otherwise it looks really good! I hope we will be able to merge it soon 🚀 It would be good to have another look from someone else. @dimberman @feluelle @kaxil @mik-laj WDYT? |
Co-authored-by: Tomek Urbaszek <turbaszek@gmail.com>
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
Co-authored-by: Ash Berlin-Taylor <ash_github@firemirror.com>
Co-authored-by: Ash Berlin-Taylor <ash_github@firemirror.com>
Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
Co-authored-by: Felix Uellendall <feluelle@users.noreply.github.com>
1c98796
to
3dd4113
Compare
Rebased from latest master to see if integration tests are fixed. |
Awesome work, congrats on your first merged pull request! |
Great work @casassg 🎉 |
This is gonna be awesome! Thank you @casassg ! |
Yay! Thanks everyone for the patience and through review 🎉 |
Just in time for the Summit ! |
@casassg thanks for your work! 🚀 |
@casassg great initiative and awesome implementation! |
Closes apache#8057. Closes apache#8056.
Airflow AIP-31 task decorator implementation. This decorator should facilitate wrapping a function into an operator and use it as such in a DAG. Closes #8057. Closes #8056.
PythonFunctionalOperator
. This can be used to set task dependencies. Ex:@task
and functional style dags in concepts.rstWork done in collaboration from @turbaszek and @evgenyshulman
Make sure to mark the boxes below before creating PR: [x]
In case of fundamental code change, Airflow Improvement Proposal (AIP) is needed.
In case of a new dependency, check compliance with the ASF 3rd Party License Policy.
In case of backwards incompatible changes please leave a note in UPDATING.md.
Read the Pull Request Guidelines for more information.