You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by GitBox <gi...@apache.org> on 2021/02/28 20:38:44 UTC

[GitHub] [airflow] dstandish commented on a change in pull request #14521: Add Asana Provider

dstandish commented on a change in pull request #14521:
URL: https://github.com/apache/airflow/pull/14521#discussion_r584350856



##########
File path: airflow/providers/asana/operators/asana_tasks.py
##########
@@ -0,0 +1,195 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Dict, Optional
+
+from airflow.models import BaseOperator
+from airflow.providers.asana.hooks.asana import AsanaHook
+from airflow.utils.decorators import apply_defaults
+
+
+class AsanaCreateTaskOperator(BaseOperator):
+    """
+    This operator can be used to create Asana tasks. For more information on
+    Asana optional task parameters, see https://developers.asana.com/docs/create-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaCreateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param name: Name of the task.
+    :type name: str
+    :param optional_task_parameters: Any of the optional task creation parameters.
+        See https://developers.asana.com/docs/create-a-task for a complete list.
+        You must specify at least one of 'workspace', 'parent', or 'projects'.
+    :type optional_task_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        name: str,
+        optional_task_parameters: Optional[dict] = None,

Review comment:
       ```suggestion
           task_parameters: Optional[dict] = None,
   ```
   
   i'd suggest calling it `task_parameters`
   
   the fact that they are optional is clear enough from the type annotation and default
   

##########
File path: airflow/providers/asana/hooks/asana.py
##########
@@ -0,0 +1,60 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Connect to Asana."""
+
+from asana import Client
+
+from airflow.hooks.base import BaseHook
+
+
+class AsanaHook(BaseHook):
+    """
+    Wrapper around Asana Python client library.
+    """
+
+    conn_name_attr = "asana_conn_id"
+    default_conn_name = "asana_default"
+    conn_type = "asana"
+    hook_name = "Asana"
+
+    def __init__(self, conn_id: str = default_conn_name, *args, **kwargs) -> None:
+        super().__init__(*args, **kwargs)
+        self.asana_conn_id = conn_id
+        self.connection = kwargs.pop("connection", None)
+        self.client = None
+        self.extras = None
+        self.uri = None
+
+    def get_conn(self) -> Client:
+        """
+        Creates Asana Client
+        """
+        self.connection = self.get_connection(self.asana_conn_id)
+        self.extras = self.connection.extra_dejson.copy()
+
+        if self.client is not None:

Review comment:
       one way this can be made a little cleaner is to make `client` a cached property and let get_conn just return `self.client`
   
   this let's you clean up a few things... e.g. you don't need to inititalize `self.client = None`, don't need to check if it is none, don't need to assign it when creating ....... you just return the client object

##########
File path: airflow/providers/asana/operators/asana_tasks.py
##########
@@ -0,0 +1,195 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Dict, Optional
+
+from airflow.models import BaseOperator
+from airflow.providers.asana.hooks.asana import AsanaHook
+from airflow.utils.decorators import apply_defaults
+
+
+class AsanaCreateTaskOperator(BaseOperator):
+    """
+    This operator can be used to create Asana tasks. For more information on
+    Asana optional task parameters, see https://developers.asana.com/docs/create-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaCreateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param name: Name of the task.
+    :type name: str
+    :param optional_task_parameters: Any of the optional task creation parameters.
+        See https://developers.asana.com/docs/create-a-task for a complete list.
+        You must specify at least one of 'workspace', 'parent', or 'projects'.
+    :type optional_task_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        name: str,
+        optional_task_parameters: Optional[dict] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.asana_conn_id = asana_conn_id
+        self.name = name
+        self.optional_task_parameters = optional_task_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> str:
+        asana_client = self.hook.get_conn()
+
+        params = {"name": self.name}
+        if self.optional_task_parameters is not None:
+            params.update(self.optional_task_parameters)
+        response = asana_client.tasks.create(params=params)
+
+        self.log.info(response)
+        return response["gid"]
+
+
+class AsanaUpdateTaskOperator(BaseOperator):
+    """
+    This operator can be used to update Asana tasks.
+    For more information on Asana optional task parameters, see
+    https://developers.asana.com/docs/update-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaUpdateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana task ID to update
+    :type asana_task_gid: str
+    :param task_update_parameters: Any task parameters that should be updated.
+        See https://developers.asana.com/docs/update-a-task for a complete list.
+    :type task_update_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        task_update_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.task_update_parameters = task_update_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+
+        response = asana_client.tasks.update(task=self.asana_task_gid, params=self.task_update_parameters)
+        self.log.info(response)
+
+
+class AsanaDeleteTaskOperator(BaseOperator):
+    """
+    This operator can be used to delete Asana tasks.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaDeleteTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana Task ID to delete.
+    :type asana_task_gid: str
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+        response = asana_client.tasks.delete_task(self.asana_task_gid)
+        self.log.info(response)
+
+
+class AsanaFindTaskOperator(BaseOperator):
+    """
+    This operator can be used to retrieve Asana tasks that match various filters.
+    You must specify at least one of `project`, `section`, `tag`, `user_task_list`,
+    or both `assignee` and `workspace`.
+    For a complete list of filters, see
+    https://github.com/Asana/python-asana/blob/ec5f178606251e2776a72a82f660cc1521516988/asana/resources/tasks.py#L182
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaFindTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param search_parameters: The parameters used to find relevant tasks
+    :type search_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        search_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.search_parameters = search_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> list:
+        contains_needed_values = ("assignee" in self.search_parameters) and \
+                                 ("workspace" in self.search_parameters)

Review comment:
       ```suggestion
           contains_needed_values = {"assignee", "workspace"}.issubset(self.search_parameters)
   ```
   
   i think this actually works fine... (i.e. tighout

##########
File path: airflow/providers/asana/example_dags/example_asana.py
##########
@@ -0,0 +1,80 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Example showing how to use Asana CreateTaskOperator.
+"""
+
+from airflow import DAG
+from airflow.providers.asana.operators.asana_tasks import AsanaCreateTaskOperator, AsanaDeleteTaskOperator, \

Review comment:
       this also doesn't look like black code style did precommit run black / isort?

##########
File path: airflow/providers/asana/operators/asana_tasks.py
##########
@@ -0,0 +1,195 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Dict, Optional
+
+from airflow.models import BaseOperator
+from airflow.providers.asana.hooks.asana import AsanaHook
+from airflow.utils.decorators import apply_defaults
+
+
+class AsanaCreateTaskOperator(BaseOperator):
+    """
+    This operator can be used to create Asana tasks. For more information on
+    Asana optional task parameters, see https://developers.asana.com/docs/create-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaCreateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param name: Name of the task.
+    :type name: str
+    :param optional_task_parameters: Any of the optional task creation parameters.
+        See https://developers.asana.com/docs/create-a-task for a complete list.
+        You must specify at least one of 'workspace', 'parent', or 'projects'.
+    :type optional_task_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        name: str,
+        optional_task_parameters: Optional[dict] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.asana_conn_id = asana_conn_id
+        self.name = name
+        self.optional_task_parameters = optional_task_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> str:
+        asana_client = self.hook.get_conn()
+
+        params = {"name": self.name}
+        if self.optional_task_parameters is not None:
+            params.update(self.optional_task_parameters)
+        response = asana_client.tasks.create(params=params)
+
+        self.log.info(response)
+        return response["gid"]
+
+
+class AsanaUpdateTaskOperator(BaseOperator):
+    """
+    This operator can be used to update Asana tasks.
+    For more information on Asana optional task parameters, see
+    https://developers.asana.com/docs/update-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaUpdateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana task ID to update
+    :type asana_task_gid: str
+    :param task_update_parameters: Any task parameters that should be updated.
+        See https://developers.asana.com/docs/update-a-task for a complete list.
+    :type task_update_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        task_update_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.task_update_parameters = task_update_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+
+        response = asana_client.tasks.update(task=self.asana_task_gid, params=self.task_update_parameters)
+        self.log.info(response)
+
+
+class AsanaDeleteTaskOperator(BaseOperator):
+    """
+    This operator can be used to delete Asana tasks.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaDeleteTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana Task ID to delete.
+    :type asana_task_gid: str
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+        response = asana_client.tasks.delete_task(self.asana_task_gid)
+        self.log.info(response)
+
+
+class AsanaFindTaskOperator(BaseOperator):
+    """
+    This operator can be used to retrieve Asana tasks that match various filters.
+    You must specify at least one of `project`, `section`, `tag`, `user_task_list`,
+    or both `assignee` and `workspace`.
+    For a complete list of filters, see
+    https://github.com/Asana/python-asana/blob/ec5f178606251e2776a72a82f660cc1521516988/asana/resources/tasks.py#L182
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaFindTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param search_parameters: The parameters used to find relevant tasks
+    :type search_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        search_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.search_parameters = search_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> list:
+        contains_needed_values = ("assignee" in self.search_parameters) and \
+                                 ("workspace" in self.search_parameters)
+        for key in ["project", "section", "tag", "user_task_list"]:
+            contains_needed_values |= key in self.search_parameters
+        if not contains_needed_values:
+            raise ValueError("You must specify at least one of 'project', 'section', 'tag', 'user_task_list',"
+                             "or both 'assignee' and 'workspace' in the search_parameters.")
+
+        asana_client = self.hook.get_conn()
+        response = asana_client.tasks.find_all(params=self.search_parameters)
+
+        # Convert the python-asana collection to a list

Review comment:
       > `# Convert the python-asana collection to a list`
   
   don't think this needs clarifying

##########
File path: airflow/providers/asana/operators/asana_tasks.py
##########
@@ -0,0 +1,195 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Dict, Optional
+
+from airflow.models import BaseOperator
+from airflow.providers.asana.hooks.asana import AsanaHook
+from airflow.utils.decorators import apply_defaults
+
+
+class AsanaCreateTaskOperator(BaseOperator):
+    """
+    This operator can be used to create Asana tasks. For more information on
+    Asana optional task parameters, see https://developers.asana.com/docs/create-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaCreateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param name: Name of the task.
+    :type name: str
+    :param optional_task_parameters: Any of the optional task creation parameters.
+        See https://developers.asana.com/docs/create-a-task for a complete list.
+        You must specify at least one of 'workspace', 'parent', or 'projects'.
+    :type optional_task_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        name: str,
+        optional_task_parameters: Optional[dict] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.asana_conn_id = asana_conn_id
+        self.name = name
+        self.optional_task_parameters = optional_task_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> str:
+        asana_client = self.hook.get_conn()
+
+        params = {"name": self.name}
+        if self.optional_task_parameters is not None:
+            params.update(self.optional_task_parameters)
+        response = asana_client.tasks.create(params=params)
+
+        self.log.info(response)
+        return response["gid"]
+
+
+class AsanaUpdateTaskOperator(BaseOperator):
+    """
+    This operator can be used to update Asana tasks.
+    For more information on Asana optional task parameters, see
+    https://developers.asana.com/docs/update-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaUpdateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana task ID to update
+    :type asana_task_gid: str
+    :param task_update_parameters: Any task parameters that should be updated.
+        See https://developers.asana.com/docs/update-a-task for a complete list.
+    :type task_update_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        task_update_parameters: dict,

Review comment:
       ```suggestion
           task_parameters: dict,
   ```
   
   just want to suggest task_parameters as the name here... the fact that you are updating them is evident from the operator name.... and task parameters is better description of what they are.... 
   

##########
File path: airflow/providers/asana/operators/asana_tasks.py
##########
@@ -0,0 +1,195 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Dict, Optional
+
+from airflow.models import BaseOperator
+from airflow.providers.asana.hooks.asana import AsanaHook
+from airflow.utils.decorators import apply_defaults
+
+
+class AsanaCreateTaskOperator(BaseOperator):
+    """
+    This operator can be used to create Asana tasks. For more information on
+    Asana optional task parameters, see https://developers.asana.com/docs/create-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaCreateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param name: Name of the task.
+    :type name: str
+    :param optional_task_parameters: Any of the optional task creation parameters.
+        See https://developers.asana.com/docs/create-a-task for a complete list.
+        You must specify at least one of 'workspace', 'parent', or 'projects'.
+    :type optional_task_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        name: str,
+        optional_task_parameters: Optional[dict] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.asana_conn_id = asana_conn_id
+        self.name = name
+        self.optional_task_parameters = optional_task_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> str:
+        asana_client = self.hook.get_conn()
+
+        params = {"name": self.name}
+        if self.optional_task_parameters is not None:
+            params.update(self.optional_task_parameters)
+        response = asana_client.tasks.create(params=params)
+
+        self.log.info(response)
+        return response["gid"]
+
+
+class AsanaUpdateTaskOperator(BaseOperator):
+    """
+    This operator can be used to update Asana tasks.
+    For more information on Asana optional task parameters, see
+    https://developers.asana.com/docs/update-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaUpdateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana task ID to update
+    :type asana_task_gid: str
+    :param task_update_parameters: Any task parameters that should be updated.
+        See https://developers.asana.com/docs/update-a-task for a complete list.
+    :type task_update_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        task_update_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.task_update_parameters = task_update_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+
+        response = asana_client.tasks.update(task=self.asana_task_gid, params=self.task_update_parameters)
+        self.log.info(response)
+
+
+class AsanaDeleteTaskOperator(BaseOperator):
+    """
+    This operator can be used to delete Asana tasks.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaDeleteTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana Task ID to delete.
+    :type asana_task_gid: str
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+        response = asana_client.tasks.delete_task(self.asana_task_gid)
+        self.log.info(response)
+
+
+class AsanaFindTaskOperator(BaseOperator):
+    """
+    This operator can be used to retrieve Asana tasks that match various filters.
+    You must specify at least one of `project`, `section`, `tag`, `user_task_list`,
+    or both `assignee` and `workspace`.
+    For a complete list of filters, see
+    https://github.com/Asana/python-asana/blob/ec5f178606251e2776a72a82f660cc1521516988/asana/resources/tasks.py#L182
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaFindTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param search_parameters: The parameters used to find relevant tasks
+    :type search_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        search_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.search_parameters = search_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> list:
+        contains_needed_values = ("assignee" in self.search_parameters) and \
+                                 ("workspace" in self.search_parameters)

Review comment:
       this does not look like `black` formatting 🤔

##########
File path: airflow/providers/asana/operators/asana_tasks.py
##########
@@ -0,0 +1,195 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Dict, Optional
+
+from airflow.models import BaseOperator
+from airflow.providers.asana.hooks.asana import AsanaHook
+from airflow.utils.decorators import apply_defaults
+
+
+class AsanaCreateTaskOperator(BaseOperator):
+    """
+    This operator can be used to create Asana tasks. For more information on
+    Asana optional task parameters, see https://developers.asana.com/docs/create-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaCreateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param name: Name of the task.
+    :type name: str
+    :param optional_task_parameters: Any of the optional task creation parameters.
+        See https://developers.asana.com/docs/create-a-task for a complete list.
+        You must specify at least one of 'workspace', 'parent', or 'projects'.
+    :type optional_task_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        name: str,
+        optional_task_parameters: Optional[dict] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.asana_conn_id = asana_conn_id
+        self.name = name
+        self.optional_task_parameters = optional_task_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> str:
+        asana_client = self.hook.get_conn()
+
+        params = {"name": self.name}
+        if self.optional_task_parameters is not None:
+            params.update(self.optional_task_parameters)
+        response = asana_client.tasks.create(params=params)
+
+        self.log.info(response)
+        return response["gid"]
+
+
+class AsanaUpdateTaskOperator(BaseOperator):
+    """
+    This operator can be used to update Asana tasks.
+    For more information on Asana optional task parameters, see
+    https://developers.asana.com/docs/update-a-task
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaUpdateTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana task ID to update
+    :type asana_task_gid: str
+    :param task_update_parameters: Any task parameters that should be updated.
+        See https://developers.asana.com/docs/update-a-task for a complete list.
+    :type task_update_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        task_update_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.task_update_parameters = task_update_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+
+        response = asana_client.tasks.update(task=self.asana_task_gid, params=self.task_update_parameters)
+        self.log.info(response)
+
+
+class AsanaDeleteTaskOperator(BaseOperator):
+    """
+    This operator can be used to delete Asana tasks.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaDeleteTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param asana_task_gid: Asana Task ID to delete.
+    :type asana_task_gid: str
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        asana_task_gid: str,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.asana_task_gid = asana_task_gid
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> None:
+        asana_client = self.hook.get_conn()
+        response = asana_client.tasks.delete_task(self.asana_task_gid)
+        self.log.info(response)
+
+
+class AsanaFindTaskOperator(BaseOperator):
+    """
+    This operator can be used to retrieve Asana tasks that match various filters.
+    You must specify at least one of `project`, `section`, `tag`, `user_task_list`,
+    or both `assignee` and `workspace`.
+    For a complete list of filters, see
+    https://github.com/Asana/python-asana/blob/ec5f178606251e2776a72a82f660cc1521516988/asana/resources/tasks.py#L182
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the guide:
+        :ref:`howto/operator:AsanaFindTaskOperator`
+
+    :param asana_conn_id: The Asana connection to use.
+    :type asana_conn_id: str
+    :param search_parameters: The parameters used to find relevant tasks
+    :type search_parameters: dict
+    """
+
+    @apply_defaults
+    def __init__(
+        self,
+        *,
+        asana_conn_id: str,
+        search_parameters: dict,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+
+        self.asana_conn_id = asana_conn_id
+        self.search_parameters = search_parameters
+        self.hook = AsanaHook(conn_id=self.asana_conn_id)
+
+    def execute(self, context: Dict) -> list:
+        contains_needed_values = ("assignee" in self.search_parameters) and \
+                                 ("workspace" in self.search_parameters)
+        for key in ["project", "section", "tag", "user_task_list"]:
+            contains_needed_values |= key in self.search_parameters
+        if not contains_needed_values:
+            raise ValueError("You must specify at least one of 'project', 'section', 'tag', 'user_task_list',"

Review comment:
       this section overall is a bit confusing.
   
   i might move it to a `validate_params` method and validate params at init (rather than waiting til run time
   
   also i'd try to make it clearer with variable naming that there is a set of required params and there is a set of params for which at least _one_ of them must be present.  ultimately i figured out that's what you're doing here but it took me a minute.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org