Repository: PrefectHQ/prefect

Issue:
Marshmallow Deprecation Warning
## Opened from the [Prefect Public Slack Community](https://prefect.io/slack)

**elliot**: Hey folks is anyone else getting a deprecation warning on marshmallow? Something like:

```
...
/home/rof/.pyenv/versions/3.9.1/lib/python3.9/site-packages/marshmallow/fields.py:198
/home/rof/.pyenv/versions/3.9.1/lib/python3.9/site-packages/marshmallow/fields.py:198: RemovedInMarshmallow4Warning: Passing field metadata as a keyword arg is deprecated. Use the explicit `metadata=...` argument instead.
...
```

Tried to update to the latest version of prefect and still no joy!

**holm.noah**: Yes, I suppose some changes in the usage still needs to be performed

**kevin701**: Hi <@U01A5UMH7HT>! I’ll bring this up to the team.

**elliot**: Awesome thanks :+1:

**kevin701**: Do you have a simple flow to reproduce this and what is your Prefect version?

**kevin701**: Can you try with Python 3.8 as well because 3.9 is experimental?

**holm.noah**: Hey Kevin, I’ve seen this on Python 3.8 when using `prefect.utilities.debug.is_serializable` from pytest.

**kevin701**: Perfect thanks for the info.

**kevin701**: <@ULVA73B9P> open “Marshmallow Deprecation Warning”

Original thread can be found [here](https://prefect-community.slack.com/archives/CL09KU1K7/p1621242234189600?thread_ts=1621242234.189600&cid=CL09KU1K7).




Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -56,6 +56,7 @@ def run(self):
     "dask_cloudprovider": ["dask_cloudprovider[aws] >= 0.2.0"],
     "dev": dev_requires + test_requires,
     "dropbox": ["dropbox ~= 9.0"],
+    "firebolt": ["firebolt-sdk >= 0.2.1"],
     "ge": ["great_expectations >= 0.13.8, < 0.14", "mistune < 2"],
     "gcp": [
         "google-cloud-bigquery >= 1.6.0, < 3.0",
diff --git a/src/prefect/tasks/firebolt/__init__.py b/src/prefect/tasks/firebolt/__init__.py
new file mode 100644
--- /dev/null
+++ b/src/prefect/tasks/firebolt/__init__.py
@@ -0,0 +1,11 @@
+"""
+This module contains a collection of tasks for interacting with Firebolt databases via
+the firebolt-python-sdk library.
+"""
+
+try:
+    from prefect.tasks.firebolt.firebolt import FireboltQuery
+except ImportError as err:
+    raise ImportError(
+        'Using `prefect.tasks.firebolt` requires Prefect to be installed with the "firebolt" extra.'
+    ) from err
diff --git a/src/prefect/tasks/firebolt/firebolt.py b/src/prefect/tasks/firebolt/firebolt.py
new file mode 100644
--- /dev/null
+++ b/src/prefect/tasks/firebolt/firebolt.py
@@ -0,0 +1,91 @@
+import firebolt.db.connection as firebolt_conn
+from typing import List
+from prefect import Task
+from prefect.utilities.tasks import defaults_from_attrs
+
+
+class FireboltQuery(Task):
+    """
+    Task for executing a query against a Firebolt database.
+
+    Args:
+        - database (str): name of the database to use.
+        - username (str): username used to authenticate.
+        - password (str): password used to authenticate.
+        - engine_name (str): name of the engine to use.
+        - query (str): query to execute against database.
+        - **kwargs (dict, optional): additional keyword arguments to pass to the Task constructor.
+    """
+
+    def __init__(
+        self,
+        database: str = None,
+        username: str = None,
+        password: str = None,
+        engine_name: str = None,
+        query: str = None,
+        **kwargs
+    ):
+        self.database = database
+        self.username = username
+        self.password = password
+        self.engine_name = engine_name
+        self.query = query
+        super().__init__(**kwargs)
+
+    @defaults_from_attrs("database", "username", "password", "engine_name", "query")
+    def run(
+        self,
+        database: str = None,
+        username: str = None,
+        password: str = None,
+        engine_name: str = None,
+        query: str = None,
+    ) -> List[List]:
+        """
+        Task run method. Executes a query against Firebolt database.
+
+        Args:
+            - database (str): name of the database to use.
+            - username (str): username used to authenticate.
+            - password (str): password used to authenticate.
+            - engine_name (str): name of the engine to use.
+            - query (str): query to execute against database.
+
+        Returns:
+            - List[List]: output of 'cursor.fetchall()' if 'cursor.execute(query)' > 0,
+                else an empty list
+
+        Raises:
+            - ValueError: if a required parameter is not supplied.
+            - DatabaseError: if exception occurs when executing the query.
+        """
+        if not database:
+            raise ValueError("A database name must be provided")
+        if not username:
+            raise ValueError("User name must be provided")
+        if not password:
+            raise ValueError("A password must be provided")
+        if not engine_name:
+            raise ValueError("An engine name must be provided")
+        if not query:
+            raise ValueError("A query string must be provided")
+
+        # build the connection parameter dictionary
+        conn_config = {
+            "database": database,
+            "username": username,
+            "password": password,
+            "engine_name": engine_name,
+        }
+
+        # connect to database, return a connection object
+        conn = firebolt_conn.connect(**conn_config)
+        result = []
+
+        # execute query
+        with conn:
+            with conn.cursor() as cursor:
+                if cursor.execute(query) > 0:
+                    result = cursor.fetchall()
+        return result


Test patch:
diff --git a/tests/tasks/firebolt/__init__.py b/tests/tasks/firebolt/__init__.py
new file mode 100644
--- /dev/null
+++ b/tests/tasks/firebolt/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytest.importorskip("firebolt.db.connection")
diff --git a/tests/tasks/firebolt/test_firebolt.py b/tests/tasks/firebolt/test_firebolt.py
new file mode 100644
--- /dev/null
+++ b/tests/tasks/firebolt/test_firebolt.py
@@ -0,0 +1,154 @@
+from unittest.mock import MagicMock
+import pytest
+import firebolt.db.connection as fb_conn
+from prefect.tasks.firebolt.firebolt import FireboltQuery
+
+
+@pytest.fixture
+def mock_conn(monkeypatch):
+
+    # returns a mocked cursor for testing
+    firebolt_conn = MagicMock()
+    connection = MagicMock(spec=fb_conn.Connection)
+    cursor = MagicMock(spec=fb_conn.Cursor)
+
+    # link all the mocks together appropriately
+    firebolt_conn.return_value = connection
+    connection.cursor = cursor
+    firebolt_connection = MagicMock(connect=firebolt_conn)
+    monkeypatch.setattr(
+        "prefect.tasks.firebolt.firebolt.firebolt_conn", firebolt_connection
+    )
+
+    return firebolt_connection, cursor
+
+
+class TestFireboltQuery:
+    def test_construction(self):
+        """
+        Tests that all required params are present for FireboltQuery Task.
+        """
+        task = FireboltQuery(
+            database="test",
+            username="test",
+            password="test",
+            engine_name="test",
+            query="test",
+        )
+        assert task.database == "test"
+        assert task.username == "test"
+        assert task.password == "test"
+        assert task.engine_name == "test"
+        assert task.query == "test"
+
+    def test_required_params(self):
+        """
+        Tests to check if there are missing required parameters.
+        """
+
+        # raises Value error if engine name is not provided
+        with pytest.raises(ValueError, match="An engine name must be provided"):
+            FireboltQuery().run(
+                database="test",
+                username="test",
+                password="test",
+                query="test",
+            )
+
+        # raises Value error if database name is not provided
+        with pytest.raises(ValueError, match="A database name must be provided"):
+            FireboltQuery().run(
+                username="test",
+                password="test",
+                engine_name="test",
+                query="test",
+            )
+        # raises Value error if username is not provided
+        with pytest.raises(ValueError, match="User name must be provided"):
+            FireboltQuery().run(
+                database="test",
+                password="test",
+                engine_name="test",
+                query="test",
+            )
+
+        # raises Value error if password is not provided
+        with pytest.raises(ValueError, match="A password must be provided"):
+            FireboltQuery().run(
+                database="test",
+                username="test",
+                engine_name="test",
+                query="test",
+            )
+
+        # raises Value error if query is not provided
+        with pytest.raises(ValueError, match="A query string must be provided"):
+            FireboltQuery().run(
+                database="test",
+                username="test",
+                password="test",
+                engine_name="test",
+            )
+
+    # test to check if the ddl/dml query was executed
+    def test_execute_query(self, mock_conn):
+        """
+        Tests that the FireboltQuery Task calls the execute method on the cursor.
+        """
+        connection = mock_conn[0]
+        cursor = mock_conn[1]
+
+        # setting execute return
+        cursor().__enter__().execute.return_value = 0
+        query = "SHOW DATABASES"
+        output = FireboltQuery(
+            database="test",
+            username="test",
+            password="test",
+            engine_name="test",
+            query=query,
+        ).run()
+        conn_config = {
+            "database": "test",
+            "username": "test",
+            "password": "test",
+            "engine_name": "test",
+        }
+        connection.connect.assert_called_once_with(**conn_config)
+        cursor.assert_called_with()
+        cursor().__enter__().execute.assert_called_once_with(query)
+        cursor().__enter__().fetchall.assert_not_called()
+        assert output == []
+
+    # test to check if the query was executed and metadata was retrieved from database
+    def test_execute_fetchall(self, mock_conn):
+        """
+        Tests that the FireboltQuery Task calls the fetchall method on the cursor.
+        """
+        connection = mock_conn[0]
+        cursor = mock_conn[1]
+
+        # setting execute return
+        cursor().__enter__().execute.return_value = 1
+
+        # setting fetchall return
+        cursor().__enter__().fetchall.return_value = ["TESTDB"]
+        query = "SHOW DATABASES"
+        output = FireboltQuery(
+            database="test",
+            username="test",
+            password="test",
+            engine_name="test",
+            query=query,
+        ).run()
+        conn_config = {
+            "database": "test",
+            "username": "test",
+            "password": "test",
+            "engine_name": "test",
+        }
+        connection.connect.assert_called_once_with(**conn_config)
+        cursor.assert_called_with()
+        cursor().__enter__().execute.assert_called_once_with(query)
+        cursor().__enter__().fetchall.assert_called()
+        assert output == ["TESTDB"]