Üllar Maasik

To convert an unhashable Python dictionary to a hashable one

February 22, 2023

We started being throttled by an API endpoint in Azure due to sending too many requests all at once, so I had to implement some sort of caching to alleviate the situation.

The function in question responsible for fetching SAS tokens for storage accounts is something like this:

def get_storage_account_sas(
    subscription_id: str,
    resource_group_name: str,
    account_name: str,
    permissions: dict[str, str],
) -> str:
    """Return storage account SAS token."""

    api_version = "2021-04-01"
    exit_code, result, logs = az(
        "rest --method post "
        f"--url https://management.azure.com/subscriptions/{subscription_id}/"
        f"resourceGroups/{resource_group_name}/providers/Microsoft.Storage/storageAccounts/"
        f"{account_name}/ListAccountSas?api-version={api_version} "
        f"--body '{json.dumps(permissions)}'"
    )
    if exit_code == 0:
        return result["accountSasToken"]
    logger.error(logs.rstrip())
    raise typer.Exit(1)

At first glance I wanted to just add the @functools.lru_cache decorator to the function, but I then got the following error when the above function was called:

...
File "utilities.py", line 361, in get_storage_account_details
    storage_account_sas = get_storage_account_sas(
TypeError: unhashable type: 'dict'

This lead me to the official documentation for the decorator where it says:

Since a dictionary is used to cache results, the positional and keyword arguments to the function must be hashable.

Dictionaries by default aren’t hashable, so this needed solving. Luckily, making sure the given permissions dictionary is hashable was just a matter of the following:

+ class HashableDict(dict):
+     def __hash__(self):
+         return hash(frozenset(self))


@functools.lru_cache
def get_storage_account_sas(
    subscription_id: str,
    resource_group_name: str,
    account_name: str,
-   permissions: dict[str, str],
+   permissions: HashableDict,
) -> str:  # pragma: no cover
    """Return storage account SAS token."""

    raise typer.Exit(1)

@@ -358,6 +365,7 @@ def get_storage_account_details(
    else:
        permissions["signedExpiry"] = generate_sas_token_expiration(token_expiry_hours)

+   permissions = HashableDict(permissions)
    storage_account_sas = get_storage_account_sas(
        subscription_id=subscription.details["id"],
        resource_group_name=generic_resource_group,
        account_name=storage_account_name,
        permissions=permissions,
    )

Now the @functools.lru_cache decorator applied without issues! Here are some helpful links relating to this:

« PreviousNext »