# AUTOGENERATED! DO NOT EDIT! File to edit: notebooks/API_DataBlob.ipynb (unless otherwise specified).

__all__ = ["DataBlob", "set_default_azure_blob_storage_region", "set_default_s3_region"]

# Cell

from typing import *

# Internal Cell

import os
import requests
from contextlib import contextmanager

import pandas as pd
from fastcore.foundation import patch
from datetime import datetime, timedelta
from pathlib import Path
from tqdm import tqdm

from ..logger import get_logger, set_level
from ..helper import (
    get_data,
    post_data,
    delete_data,
    add_ready_column,
    generate_df,
    get_values_from_item,
    get_attributes_from_instances,
)

from .client import Client
from .datasource import DataSource
from .progress_status import ProgressStatus
from ..constant import CLIENT_DB_USERNAME, CLIENT_DB_PASSWORD

# Internal Cell

logger = get_logger(__name__)

# Internal Cell

DEFAULT_AZURE_BLOB_STORAGE_REGION = "westeurope"
DEFAULT_S3_REGION = "eu-west-1"

# Cell


class DataBlob:
    """A class for importing and processing the data from sources like CSV files, databases, AWS S3 bucket or Azure Blob Storage.

    Currently, the only way to instantiate the DataBlob class is to call one of the following static methods
    `from_local`, `from_mysql`, `from_clickhouse`, `from_s3`, or `from_azure_blob_storage` which imports the data in
    the parquet file format from:

     - a local CSV file,

     - a MySql database,

     - a ClickHouse database

     - an AWS S3 bucket, and

     - an Azure Blob Storage respectively.

    We plan to support other databases and storage medium in the future.

    For uploading the data from a local CSV file, the `from_local` method can be used and only requires the relative/absolute
    path of the CSV file as input.

    For establishing the connection to the MySql database, the `from_mysql` method can be used and requires parameters like host,
    port, database name, table name, etc. If the access to the MySql database requires authentication, then the credentials can be passed as parameters to the `from_mysql`
    method or stored in the environment variables **AIRT_CLIENT_DB_USERNAME** and **AIRT_CLIENT_DB_PASSWORD**.

    For establishing the connection to the ClickHouse database, the `from_clickhouse` method can be used and requires parameters like host,
    port, database name, protocol, etc. If the access to the ClickHouse database requires authentication, then the credentials can be passed as parameters to the `from_clickhouse`
    method or stored in the environment variables **CLICKHOUSE_USERNAME** and **CLICKHOUSE_PASSWORD**.

    Similarly, for establishing a connection to the S3 bucket, the `from_s3` method can be used and requires parameters like s3 uri,
    aws_access_key, and aws_secret_key, etc. The AWS S3 access_key and the secret_key can be passed as parameters to the `from_s3` method or stored in the
    environment variables **AWS_ACCESS_KEY_ID** and **AWS_SECRET_ACCESS_KEY**.

    All the function calls to the library are asynchronous and they return immediately.

    To manage completion, all methods inside the returned object will return a status object and a method to display an interactive
    progress bar that can be called to check the progress.

    Below are code examples for accessing the above status methods:

    An example to check for the status of from_s3 connection:

        ```python
        data_blob = DataBlob.from_s3(
            uri="s3://bucket/events.parquet"
        )
        data_blob.is_ready()

        data_source = data_blob.from_csv()
        data_source.is_ready()
        ```

        An example to display an interactive progress bar:

        ```python
        data_blob = DataBlob.from_s3(
            uri="s3://bucket/events.parquet"
        )
        data_blob.progress_bar()

        data_source = data_blob.from_csv()
        data_source.progress_bar()
        ```
    """

    BASIC_DB_COLS = [
        "uuid",
        "datasources",
        "type",
        "source",
        "region",
        "tags",
        "pulled_on",
        "completed_steps",
        "total_steps",
        "folder_size",
    ]

    ALL_DB_COLS = BASIC_DB_COLS + ["user", "error", "disabled"]

    COLS_TO_RENAME = {
        "uuid": "datablob_uuid",
        "datasources": "datasource_uuids",
        "user": "user_uuid",
    }

    _default_azure_blob_storage_regions: List[str] = []
    _default_s3_regions: List[str] = []

    def __init__(
        self,
        uuid: str,
        type: Optional[str] = None,
        source: Optional[str] = None,
        region: Optional[str] = None,
        datasources: Optional[List[str]] = None,
        total_steps: Optional[int] = None,
        completed_steps: Optional[int] = None,
        folder_size: Optional[int] = None,
        disabled: Optional[bool] = None,
        pulled_on: Optional[str] = None,
        user: Optional[str] = None,
        tags: Optional[List] = None,
        error: Optional[str] = None,
    ):
        """Constructs a new DataBlob instance.

        Warning:
            Do not construct this object directly by calling the constructor, please use `from_s3`,
            `from_mysql`, `from_clickhouse` or `from_local` methods instead.

        Args:
            uuid: Datablob uuid in the server.
            type: Datablob type in the server.
            source: Datablob source.
            region: AWS bucket region.
            datasources: The list of datasources created using the datablob.
            total_steps: No of steps needed to upload the datablob to the server.
            completed_steps: No of steps completed while the uploading the datablob to the server.
            folder_size: The size of the uploaded datablob in bytes.
            disabled: Flag to indicate the active status of the datablob.
            pulled_on: Datablob last pulled date.
            user: The uuid of the user who created the datablob.
            tags: Tag names associated with the datablob.
            error: Error message while processing the datablob.
        """
        self.uuid = uuid
        self.type = type
        self.source = source
        self.region = region
        self.datasources = datasources
        self.total_steps = total_steps
        self.completed_steps = completed_steps
        self.folder_size = folder_size
        self.disabled = disabled
        self.pulled_on = pulled_on
        self.user = user
        self.tags = tags
        self.error = error

    @staticmethod
    def _get_tag_name_and_datasource_id(res: Dict[str, Any]) -> Dict[str, Any]:
        """Get tag name and datasource ids as string seperated by comma.

        Args:
            res: The response object.

        Returns:
            The modified response object with tag name and datasource ids as string seperated by comma.
        """
        res["tags"] = get_values_from_item(res["tags"], "name")
        res["datasources"] = get_values_from_item(res["datasources"])

        return res

    @staticmethod
    def from_s3(
        *,
        uri: str,
        access_key: Optional[str] = None,
        secret_key: Optional[str] = None,
        region: Optional[str] = None,
        tag: Optional[str] = None,
    ) -> "DataBlob":
        """Create and return a datablob that encapsulates the data from an AWS S3 bucket.

        Args:
            uri: AWS S3 bucket uri.
            access_key: Access key for the S3 bucket. If **None** (default value), then the value
                from **AWS_ACCESS_KEY_ID** environment variable will be used.
            secret_key: Secret key for the S3 bucket. If **None** (default value), then the value
                from **AWS_SECRET_ACCESS_KEY** environment variable will be used.
            region: The AWS region to save your datablob. If **None** (default value), then the **uri** bucket's region will be used.
            tag: A string to tag the datablob. If not passed, then the tag **latest** will be assigned to the datablob.

        Returns:
            An instance of the `DataBlob` class.

        Raises:
            ValueError: If parameters to the API are invalid.
            ConnectionError: If the server address is invalid or not reachable.

        An example to create a Datablob from AWS s3 bucket:

        ```python
            db = DataBlob.from_s3(
                uri="s3://bucket/events.parquet"
            )
        ```
        """
        access_key = (
            access_key if access_key is not None else os.environ["AWS_ACCESS_KEY_ID"]
        )
        secret_key = (
            secret_key
            if secret_key is not None
            else os.environ["AWS_SECRET_ACCESS_KEY"]
        )
        region = DataBlob._get_s3_region(region=region, set_source_s3_region=True)  # type: ignore

        response = Client._post_data(
            relative_url="/datablob/from_s3",
            data=dict(
                uri=uri,
                access_key=access_key,
                secret_key=secret_key,
                region=region,
                tag=tag,
            ),
        )

        return DataBlob(
            uuid=response["uuid"], type=response["type"], source=response["source"]
        )

    @staticmethod
    def from_azure_blob_storage(
        uri: str,
        credential: Optional[str] = None,
        region: Optional[str] = None,
        tag: Optional[str] = None,
    ) -> "DataBlob":
        """Create and return a datablob that encapsulates the data from an Azure Blob Storage.

        Args:
            uri: Azure Blob Storage uri.
            credential: Azure Blob Storage credential. If **None** (default value), then the value
                from **AZURE_STORAGE_CREDENTIAL** environment variable will be used.
            region: The Azure Blob Storage region to save your datablob. If **None** (default value), then **westeurope** will be used as region.
            tag: A string to tag the datablob. If not passed, then the tag **latest** will be assigned to the datablob.

        Returns:
            An instance of the `DataBlob` class.

        Raises:
            ValueError: If parameters to the API are invalid.
            ConnectionError: If the server address is invalid or not reachable.


        To create a Datablob from Azure Blob Storage, you must have a valid Azure Blob Storage credential.

        If you already have one, please set the credential in the **AZURE_STORAGE_CREDENTIAL** environment variable or pass it in the parameter.

        If you don't know how to get the Azure Blob Storage credential, you can follow the below python example. It's one of the ways to get the Azure
        Blob Storage credential.

        - If you don't already have it, please install the Azure Storage Management (azure-mgmt-storage) and Azure Resource Management (azure-mgmt-resource)
        python client libraries using pip.

        - Import the **DefaultAzureCredential** and **StorageManagementClient** class from the respective package

        - Ensure the following four environment variables are set into your current working environment with appropriate values.

            - AZURE_TENANT_ID

            - AZURE_CLIENT_ID

            - AZURE_CLIENT_SECRET

            - AZURE_SUBSCRIPTION_ID

        - Assign the resource group name in the **GROUP_NAME** variable and the storage account name in the **STORAGE_ACCOUNT_NAME** variable.

        - Finally, use the StorageManagementClient and DefaultAzureCredential classes to get the credential.

        - Below is a sample code. Please copy it and replace the placeholders with appropriate values to get the Azure Blob Storage credential:

            ```python
                # Importing necessary libraries
                import os
                from azure.identity import DefaultAzureCredential
                from azure.mgmt.storage import StorageManagementClient

                # Setting the required environment variables
                os.environ["AZURE_SUBSCRIPTION_ID"] = "{fill in here}"
                os.environ["AZURE_CLIENT_ID"] = "{fill in here}"
                os.environ["AZURE_CLIENT_SECRET"] = "{fill in here}"
                os.environ["AZURE_TENANT_ID"]= "{fill in here}"

                # Setting the resource group name and storage account name
                GROUP_NAME = "{fill in here}"
                STORAGE_ACCOUNT_NAME = "{fill in here}"

                # Retrieving the credential
                storage_client = StorageManagementClient(
                    DefaultAzureCredential(), os.environ["AZURE_SUBSCRIPTION_ID"]
                )
                storage_keys = storage_client.storage_accounts.list_keys(GROUP_NAME, STORAGE_ACCOUNT_NAME)
                storage_keys = {v.key_name: v.value for v in storage_keys.keys}
                credential = storage_keys['key1']
            ```

        - Once you have the credential, please set it in the **AZURE_STORAGE_CREDENTIAL** environment variable or pass it in the parameter
        to the `from_azure_blob_storage` method to create a DataBlob.

            ```python
                # Sample code to create a Datablob from Azure Blob Storage
                db = DataBlob.from_azure_blob_storage(
                    uri="{fill in here}",
                    credential=credential
                )
            ```
        """
        region = DataBlob._get_azure_blob_storage_region(region)  # type: ignore

        credential = (
            credential
            if credential is not None
            else os.environ["AZURE_STORAGE_CREDENTIAL"]
        )

        response = Client._post_data(
            relative_url="/datablob/from_azure_blob_storage",
            data=dict(
                uri=uri,
                credential=credential,
                region=region,
                tag=tag,
            ),
        )

        return DataBlob(
            uuid=response["uuid"], type=response["type"], source=response["source"]
        )

    @staticmethod
    def from_mysql(
        *,
        host: str,
        database: str,
        table: str,
        port: int = 3306,
        region: Optional[str] = None,
        username: Optional[str] = None,
        password: Optional[str] = None,
        tag: Optional[str] = None,
    ) -> "DataBlob":
        """Create and return a datablob that encapsulates the data from a mysql database.

        If the database requires authentication, pass the username/password as parameters or store it in
        the **AIRT_CLIENT_DB_USERNAME** and **AIRT_CLIENT_DB_PASSWORD** environment variables.

        Args:
            host: Remote database host name.
            database: Database name.
            table: Table name.
            port: Host port number. If not passed, then the default value **3306** will be used.
            region: The AWS region to save your datablob. If **None** (default value), then **eu-west-1** will be used as region.
            username: Database username. If not passed, then the value set in the environment variable
                **AIRT_CLIENT_DB_USERNAME** will be used else the default value "root" will be used.
            password: Database password. If not passed, then the value set in the environment variable
                **AIRT_CLIENT_DB_PASSWORD** will be used else the default value "" will be used.
            tag: A string to tag the datablob. If not passed, then the tag **latest** will be assigned to the datablob.

        Returns:
           An instance of the `DataBlob` class.

        Raises:
            ValueError: If parameters to the API are invalid.
            ConnectionError: If the server address is invalid or not reachable.

        An example to create a Datablob from a database:

        ```python
            db = DataBlob.from_mysql(
                host="host_name",
                database="database_name",
                table="table_name"
            )
        ```
        """
        username = (
            username
            if username is not None
            else os.environ.get(CLIENT_DB_USERNAME, "root")
        )

        password = (
            password if password is not None else os.environ.get(CLIENT_DB_PASSWORD, "")
        )

        region = DataBlob._get_s3_region(region)  # type: ignore

        _body = dict(
            host=host,
            port=port,
            username=username,
            password=password,
            database=database,
            table=table,
            region=region,
            tag=tag,
        )

        response = Client._post_data(relative_url=f"/datablob/from_mysql", data=_body)

        return DataBlob(
            uuid=response["uuid"], type=response["type"], source=response["source"]
        )

    @staticmethod
    def from_clickhouse(
        *,
        host: str,
        database: str,
        table: str,
        protocol: str,
        index_column: str,
        timestamp_column: str,
        port: int = 0,
        region: Optional[str] = None,
        username: Optional[str] = None,
        password: Optional[str] = None,
        filters: Optional[Dict[str, Any]] = None,
        tag: Optional[str] = None,
    ) -> "DataBlob":
        """Create and return a datablob that encapsulates the data from a ClickHouse database.

        If the database requires authentication, pass the username/password as parameters or store it in
        the **CLICKHOUSE_USERNAME** and **CLICKHOUSE_PASSWORD** environment variables.

        Args:
            host: Remote database host name.
            database: Database name.
            table: Table name.
            protocol: Protocol to use (native/http).
            index_column: The column to use as index (row labels).
            timestamp_column: Timestamp column column name.
            port: Host port number. If not passed, then the default value **0** will be used.
            region: The AWS region to save your datablob. If **None** (default value), then **eu-west-1** will be used as region.
            username: Database username. If not passed, then the value set in the environment variable
                **CLICKHOUSE_USERNAME** will be used else the default value "root" will be used.
            password: Database password. If not passed, then the value set in the environment variable
                **CLICKHOUSE_PASSWORD** will be used else the default value "" will be used.
            filters: Any additional parameters to be used while importing the data as a dict.
            tag: A string to tag the datablob. If not passed, then the tag **latest** will be assigned to the datablob.

        Returns:
           An instance of the `DataBlob` class.

        Raises:
            ValueError: If parameters to the API are invalid.
            ConnectionError: If the server address is invalid or not reachable.

        An example to create a Datablob from a database:

        ```python
            db = DataBlob.from_clickhouse(
                host="host_name",
                database="database_name",
                table="table_name"
                protocol="native",
                index_column="index_column_name",
                timestamp_column="timestamp_column_name"
            )
        ```
        """
        username = (
            username
            if username is not None
            else os.environ.get("CLICKHOUSE_USERNAME", "root")
        )

        password = (
            password
            if password is not None
            else os.environ.get("CLICKHOUSE_PASSWORD", "")
        )

        region = DataBlob._get_s3_region(region)  # type: ignore

        _body = dict(
            host=host,
            database=database,
            table=table,
            protocol=protocol,
            port=port,
            username=username,
            password=password,
            index_column=index_column,
            timestamp_column=timestamp_column,
            filters=filters,
            region=region,
            tag=tag,
        )

        response = Client._post_data(
            relative_url=f"/datablob/from_clickhouse", data=_body
        )

        return DataBlob(
            uuid=response["uuid"], type=response["type"], source=response["source"]
        )

    @staticmethod
    def _upload_to_s3_with_retry(
        file_to_upload: Path,
        presigned_url: str,
        presigned_fields: Dict[str, Any],
        max_retry: int = 3,
        curr_iteration: int = 1,
    ):
        """Upload local files to s3 using presigned url

        Args:
            file_to_upload: path of file to upload
            presigned_url: presigned url to upload to
            presigned_fields: presigned fields provided by boto3
            max_retry: maximum retry count
            curr_iteration: current iteration count for internal use
        """
        try:
            with open(file_to_upload, "rb") as f:
                files = {"file": (str(file_to_upload), f)}
                response = requests.post(
                    presigned_url, data=presigned_fields, files=files
                )
                if not response.status_code == 204:
                    raise ValueError(response.text)

        except requests.exceptions.ConnectionError as e:
            if curr_iteration == max_retry:
                raise e
            DataBlob._upload_to_s3_with_retry(
                file_to_upload,
                presigned_url,
                presigned_fields,
                max_retry,
                curr_iteration + 1,
            )

    @staticmethod
    def from_local(
        path: Union[str, Path],
        region: Optional[str] = None,
        tag: Optional[str] = None,
        show_progress: Optional[bool] = True,
    ) -> "DataBlob":
        """Create and return a datablob from local file.

        Args:
            path: The relative or absolute path to a local CSV file or to a directory containing the CSV files.
            region: The AWS region to save your datablob. If **None** (default value), then **eu-west-1** will be used as region.
            tag: A string to tag the datablob. If not passed, then the tag **latest** will be assigned to the datablob.
            show_progress: Flag to set the progressbar visibility. If not passed, then the default value **True** will be used.

        Returns:
           An instance of the `DataBlob` class.

        Raises:
            ValueError: If parameters to the API are invalid.
            ConnectionError: If the server address is invalid or not reachable.

        An example to create a Datablob from a local file:

        ```python
            db = DataBlob.from_local(path=Path('path-to-local-file'))
        ```
        """
        path = Path(path)
        region = DataBlob._get_s3_region(region)  # type: ignore

        # Step 1: get presigned URL
        _path = f"local:{str(path)}"

        response = Client._post_data(
            relative_url=f"/datablob/from_local/start",
            data=dict(path=_path, region=region, tag=tag),
        )

        # Step 2: download the csv to the s3 bucket
        files = list(path.glob("*")) if path.is_dir() else [path]

        # Initiate progress bar
        t = tqdm(total=len(files), disable=not show_progress)

        for file_to_upload in files:
            DataBlob._upload_to_s3_with_retry(
                file_to_upload=file_to_upload,
                presigned_url=response["presigned"]["url"],
                presigned_fields=response["presigned"]["fields"],
            )
            t.update()

        t.close()
        return DataBlob(uuid=response["uuid"], type=response["type"])

    @staticmethod
    def ls(
        offset: int = 0,
        limit: int = 100,
        disabled: bool = False,
        completed: bool = False,
    ) -> List["DataBlob"]:
        """Return the list of DataBlob instances available in the server.

        Args:
            offset: The number of datablobs to offset at the beginning. If **None**,
                then the default value **0** will be used.
            limit: The maximum number of datablobs to return from the server. If **None**,
                then the default value **100** will be used.
            disabled: If set to **True**, then only the deleted datablobs will be returned.
                Else, the default value **False** will be used to return only the list
                of active datablobs.
            completed: If set to **True**, then only the datablobs that are successfully downloaded
                to the server will be returned. Else, the default value **False** will be used to
                return all the datablobs.

        Returns:
            A list of DataBlob instances available in the server.

        Raises:
            ConnectionError: If the server address is invalid or not reachable.

        An example to list the available datablobs:

        ```python
        DataBlob.ls()
        ```
        """
        lists = Client._get_data(
            relative_url=f"/datablob/?disabled={disabled}&completed={completed}&offset={offset}&limit={limit}"
        )

        dbx = [
            DataBlob(
                uuid=db["uuid"],
                type=db["type"],
                source=db["source"],
                region=db["region"],
                datasources=db["datasources"],
                total_steps=db["total_steps"],
                completed_steps=db["completed_steps"],
                folder_size=db["folder_size"],
                disabled=db["disabled"],
                pulled_on=db["pulled_on"],
                user=db["user"],
                tags=db["tags"],
                error=db["error"],
            )
            for db in lists
        ]

        return dbx

    @staticmethod
    def as_df(dbx: List["DataBlob"]) -> pd.DataFrame:
        """Return the details of datablob instances as a pandas dataframe.

        Args:
            dbx: List of datablob instances.

        Returns:
            Details of all the datablobs in a dataframe.

        Raises:
            ConnectionError: If the server address is invalid or not reachable.

        An example get the details of available datablobs:

        ```python
        dbx = DataBlob.ls()
        Datablob.as_df(dbx)
        ```
        """
        db_lists = get_attributes_from_instances(dbx, DataBlob.ALL_DB_COLS)  # type: ignore

        for db in db_lists:
            db = DataBlob._get_tag_name_and_datasource_id(db)

        lists_df = generate_df(db_lists, DataBlob.BASIC_DB_COLS)
        df = add_ready_column(lists_df)

        df = df.rename(columns=DataBlob.COLS_TO_RENAME)

        return df

    def is_ready(self) -> bool:
        """Check if the method's progress is complete.

        !!! info

            This method will return `True` immediately and will not wait for the progress to finish
            if the datablob is created using the `from_local` method.

        Returns:
            **True** if the progress if completed, else **False**.

        ```python

        db.is_ready()
        ```
        """
        if self.type in ["local"]:
            return True

        progress_status = ProgressStatus(relative_url=f"/datablob/{self.uuid}")

        return progress_status.is_ready()

    def progress_bar(self, sleep_for: Union[int, float] = 5, timeout: int = 0):
        """Blocks the execution and displays a progress bar showing the remote action progress.

        !!! info

            This method will not check the progress if the datablob is created using the
            `from_local` method.

        Args:
            sleep_for: The time interval in seconds between successive API calls.
            timeout: The maximum time allowed in seconds for the asynchronous call to complete. If not the
                progressbar will be terminated.

        Raises:
            ConnectionError: If the server address is invalid or not reachable.
            TimeoutError: in case of connection timeout.

        ```python

        db.progress_bar()
        ```
        """
        if self.type not in ["local"]:
            progress_status = ProgressStatus(
                relative_url=f"/datablob/{self.uuid}",
                sleep_for=sleep_for,
                timeout=timeout,
            )

            progress_status.progress_bar()

    def wait(self, sleep_for: Union[int, float] = 1, timeout: int = 0):
        """Blocks execution while waiting for the remote action to complete.

        !!! info

            This method will not check the progress if the datablob is created using the
            `from_local` method.

        Args:
            sleep_for: The time interval in seconds between successive API calls.
            timeout: The maximum time allowed in seconds for the asynchronous call to complete. If not the
                progressbar will be terminated.

        Raises:
            ConnectionError: If the server address is invalid or not reachable.
            TimeoutError: in case of timeout.

        ```python

        db.wait()
        ```
        """
        if self.type not in ["local"]:
            progress_status = ProgressStatus(
                relative_url=f"/datablob/{self.uuid}",
                sleep_for=sleep_for,
                timeout=timeout,
            )

            progress_status.wait()

    def from_csv(
        self,
        *,
        index_column: str,
        sort_by: Union[str, List[str]],
        deduplicate_data: bool = False,
        blocksize: str = "256MB",
        **kwargs,
    ) -> DataSource:
        raise NotImplementedError()

    def from_parquet(
        self,
        *,
        index_column: str,
        sort_by: Union[str, List[str]],
        deduplicate_data: bool = False,
        blocksize: str = "256MB",
        **kwargs,
    ) -> DataSource:
        raise NotImplementedError()

    def details(self) -> pd.DataFrame:
        raise NotImplementedError()

    def tag(self, name: str) -> pd.DataFrame:
        raise NotImplementedError()

    def delete(self) -> pd.DataFrame:
        raise NotImplementedError()


# Cell


@patch(cls_method=True)
@contextmanager
def set_default_azure_blob_storage_region(cls: DataBlob, region: str) -> Iterator[None]:
    """Sets the Azure Blob Storage region for datablobs created in this context.

    For the list of available regions, please click [here](https://docs.microsoft.com/en-us/azure/availability-zones/az-overview)

    Args:
        region: The Azure Blob Storage region name to use in the context.

    Returns:
        A context manager that specifies the Azure Blob Storage region to use for newly
        created datablobs.

    For example:
        ```python

        # Importing necessary libraries
        import os

        from azure.identity import DefaultAzureCredential
        from azure.mgmt.storage import StorageManagementClient

        # Setting the required environment variables
        os.environ["AZURE_SUBSCRIPTION_ID"] = "{fill in here}"
        os.environ["AZURE_CLIENT_ID"] = "{fill in here}"
        os.environ["AZURE_CLIENT_SECRET"] = "{fill in here}"
        os.environ["AZURE_TENANT_ID"]= "{fill in here}"

        # Setting the resource group name and storage account name
        GROUP_NAME = "{fill in here}"
        STORAGE_ACCOUNT_NAME = "{fill in here}"

        # Retrieving the credential
        storage_client = StorageManagementClient(
            DefaultAzureCredential(), os.environ["AZURE_SUBSCRIPTION_ID"]
        )
        storage_keys = storage_client.storage_accounts.list_keys(GROUP_NAME, STORAGE_ACCOUNT_NAME)
        storage_keys = {v.key_name: v.value for v in storage_keys.keys}
        credential = storage_keys['key1']

        # Authenticate
        Client.get_token()

        with DataBlob.set_default_azure_blob_storage_region(region="northeurope"):
            # datablobs created here will have regions set to northeurope
            db = DataBlob.from_azure_blob_storage(
                uri="{fill in here}",
                credential=credential
            )
            db.progress_bar()

            with DataBlob.set_default_azure_blob_storage_region(region="westeurope"):
                # datablobs created here will have regions set to westeurope
                db = DataBlob.from_azure_blob_storage(
                    uri="{fill in here}",
                    credential=credential
                )
                db.progress_bar()
        ```
    """
    cls._default_azure_blob_storage_regions.append(region)

    yield

    cls._default_azure_blob_storage_regions.pop()


@patch(cls_method=True)
def _get_default_azure_blob_storage_region(cls: DataBlob) -> Optional[str]:
    if len(cls._default_azure_blob_storage_regions) == 0:
        return None
    return cls._default_azure_blob_storage_regions[-1]


@patch(cls_method=True)
def _get_azure_blob_storage_region(cls: DataBlob, region: Optional[str] = None) -> str:
    if region is None:
        region = cls._get_default_azure_blob_storage_region()  # type: ignore
        if region is None:
            region = DEFAULT_AZURE_BLOB_STORAGE_REGION
    return region


# Cell


@patch(cls_method=True)
@contextmanager
def set_default_s3_region(cls: DataBlob, region: str) -> Iterator[None]:
    """Sets the s3 region for datablobs created in this context.

    For the list of available regions, please click [here](https://docs.aws.amazon.com/general/latest/gr/s3.html)

    Args:
        region: The s3 region name to use in the context.

    Returns:
        A context manager that specifies the s3 region to use for newly
        created datablobs.

    For example:
    ```python
    # Importing necessary libraries
    import os

    # Setting the AWS specific environment variables
    os.environ["AWS_ACCESS_KEY_ID"] = "{fill in here}"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "{fill in here}"

    # Authenticate
    Client.get_token()

    with DataBlob.set_default_s3_region(region="eu-west-1"):
        # datablobs created here will have regions set to eu-west-1
        db = DataBlob.from_s3(
            uri="{fill in here}"
        )
        db.progress_bar()

        with DataBlob.set_default_s3_region(region="eu-west-3"):
            # datablobs created here will have regions set to eu-west-3
            db = DataBlob.from_s3(
                uri="{fill in here}"
            )
            db.progress_bar()

    ```
    """

    cls._default_s3_regions.append(region)

    yield

    cls._default_s3_regions.pop()


@patch(cls_method=True)
def _get_default_s3_region(cls: DataBlob) -> Optional[str]:
    if len(cls._default_s3_regions) == 0:
        return None
    return cls._default_s3_regions[-1]


@patch(cls_method=True)
def _get_s3_region(
    cls: DataBlob, region: Optional[str] = None, set_source_s3_region: bool = False
) -> Optional[str]:
    if region is None:
        region = cls._get_default_s3_region()  # type: ignore
        if region is None:
            region = None if set_source_s3_region else DEFAULT_S3_REGION
    return region


# Cell


@patch
def details(self: DataBlob) -> pd.DataFrame:
    """Return details of a datablob.

    Returns:
        The datablob details as a pandas dataframe.

    Raises:
        ConnectionError: If the server address is invalid or not reachable.

    An example to get details of a datablob from the server:

    ```python
    db.details()
    ```
    """

    details = Client._get_data(relative_url=f"/datablob/{self.uuid}")

    details = DataBlob._get_tag_name_and_datasource_id(details)

    details_df = pd.DataFrame([details])[DataBlob.ALL_DB_COLS]

    details_df = details_df.rename(columns=DataBlob.COLS_TO_RENAME)

    return add_ready_column(details_df)


# Internal Cell


DataBlob.details.__doc__ = DataBlob.details.__doc__ + f"\n    Columns in the resulting dataframe are: {', '.join(DataBlob.ALL_DB_COLS)}."  # type: ignore

# Cell


@patch
def from_csv(
    self: DataBlob,
    *,
    index_column: str,
    sort_by: Union[str, List[str]],
    deduplicate_data: bool = False,
    blocksize: str = "256MB",
    **kwargs,
) -> DataSource:
    """Process the CSV data and return a datasource object.

    Args:
        index_column: The column to use as index (row labels).
        sort_by: The column(s) to sort the data. Can either be a string or a sequence of strings.
        deduplicate_data: If set to **True** (default value **False**), then duplicate rows are removed while uploading.
        blocksize: Data split size in bytes. If None, then the split size is set to **256MB**.
        kwargs: Any additional parameters to be used while processing the data.

    Returns:
        An instance of the `DataSource` class.

    Raises:
        ValueError: If the CSV file processing fails.
        ConnectionError: If the server address is invalid or not reachable.

    An example for processing a csv datablob:

    ```python
        data_source_csv = DataBlob.from_csv(
            index_column="index_column_name",
            sort_by="sort_by_column_name"
        )
    ```
    """
    data = dict(
        deduplicate_data=deduplicate_data,
        index_column=index_column,
        sort_by=sort_by,
        blocksize=blocksize,
        kwargs=kwargs,
    )
    response = Client._post_data(
        relative_url=f"/datablob/{self.uuid}/from_csv", data=data
    )

    return DataSource(uuid=response["uuid"])


# Cell


@patch
def from_parquet(
    self: DataBlob,
    *,
    index_column: str,
    sort_by: Union[str, List[str]],
    deduplicate_data: bool = False,
    blocksize: str = "256MB",
    **kwargs,
) -> DataSource:
    """Process the parquet data and return a datasource object.

    Args:
        index_column: The column to use as index (row labels).
        sort_by: The column(s) to sort the data. Can either be a string or a sequence of strings.
        deduplicate_data: If set to **True** (default value **False**), then duplicate rows are removed while uploading.
        blocksize: Data split size in bytes. If None, then the split size is set to **256MB**.
        kwargs: Any additional parameters to be used while processing the data.

    Returns:
        An instance of the `DataSource` class.

    Raises:
        ValueError: If processing of the parquet file fails.
        ConnectionError: If the server address is invalid or not reachable.

    An example for processing a parquet datablob:

    ```python
        data_source_parquet = DataBlob.from_parquet(
            index_column="index_column_name",
            sort_by="sort_by_column_name"
        )
    ```
    """
    data = dict(
        deduplicate_data=deduplicate_data,
        index_column=index_column,
        sort_by=sort_by,
        blocksize=blocksize,
        kwargs=kwargs,
    )
    response = Client._post_data(
        relative_url=f"/datablob/{self.uuid}/from_parquet", data=data
    )

    return DataSource(uuid=response["uuid"])


# Cell


@patch
def delete(self: DataBlob) -> pd.DataFrame:
    """Delete a datablob from the server.

    Returns:
        A pandas DataFrame encapsulating the details of the deleted datablob.

    Raises:
        ConnectionError: If the server address is invalid or not reachable.

    An example to delete a datablob from the server:

    ```python
    db.delete()
    ```
    """

    response = Client._delete_data(relative_url=f"/datablob/{self.uuid}")

    response = DataBlob._get_tag_name_and_datasource_id(response)

    df = pd.DataFrame([response])[DataBlob.BASIC_DB_COLS]

    df = df.rename(columns=DataBlob.COLS_TO_RENAME)

    return add_ready_column(df)


# Cell


@patch
def tag(self: DataBlob, name: str) -> pd.DataFrame:
    """Tag an existing datablob in the server.

    Args:
        name: A string to tag the datablob.

    Returns:
        A pandas dataframe with the details of the tagged datablob.

    Raises:
        ConnectionError: If the server address is invalid or not reachable.

    An example to tag an existing datablob:

    ```python
    db.tag(name="v1.0")
    ```
    """
    response = Client._post_data(
        relative_url=f"/datablob/{self.uuid}/tag", data=dict(name=name)
    )

    response = DataBlob._get_tag_name_and_datasource_id(response)

    df = pd.DataFrame([response])[DataBlob.BASIC_DB_COLS]

    df = df.rename(columns=DataBlob.COLS_TO_RENAME)

    return add_ready_column(df)
