Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/user_guide/concepts_utils/handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ The :class:`~fourinsight.engineroom.utils.AzureBlobHandler` is used to store tex
from fourinsight.engineroom.utils import AzureBlobHandler


handler = AzureBlobHandler(<connection-string>, <container-name>, <blob-name>)
# Instantiate from a connection string
handler = AzureBlobHandler(conn_str, container_name, blob_name)

# Instantiate from a container-level SAS URL
handler = AzureBlobHandler.from_container_url(container_url, blob_name)

The handlers behave like 'streams', and provide all the normal stream capabilities. Downloading and uploading is done by a push/pull
strategy; content is retrieved from the source by a :meth:`~fourinsight.engineroom.utils.BaseHandler.pull()` request, and uploaded
Expand Down
33 changes: 31 additions & 2 deletions fourinsight/engineroom/utils/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pandas as pd
from azure.core.exceptions import ResourceNotFoundError
from azure.storage.blob import BlobClient
from azure.storage.blob import BlobClient, ContainerClient

from ._constants import API_BASE_URL

Expand Down Expand Up @@ -188,8 +188,37 @@ def __init__(
)
super().__init__(encoding=encoding, newline=newline)

@classmethod
def from_container_url(
cls, container_url, blob_name, encoding="utf-8", newline="\n"
):
"""
Instantiate from a container-level SAS URL.

Parameters
----------
container_url : str
Full SAS URL for the container, e.g.
``"https://<account>.blob.core.windows.net/<container>?sv=...&sig=..."``.
blob_name : str
The name of the blob with which to interact.
encoding : str
Defaults to 'utf-8'.
newline : str
Defaults to '\\n'.
"""

instance = cls.__new__(cls)
instance._blob_client = ContainerClient.from_container_url(
container_url
).get_blob_client(blob_name)
instance._blob_name = blob_name
BaseHandler.__init__(instance, encoding=encoding, newline=newline)
return instance

def __repr__(self):
return f"AzureBlobHandler {self._container_name}/{self._blob_name}"
# return f"AzureBlobHandler {self._container_name}/{self._blob_name}"
return f"AzureBlobHandler {self._blob_client.container_name}/{self._blob_client.blob_name}"

def _pull(self):
return self._blob_client.download_blob().readinto(self.buffer)
Expand Down
31 changes: 31 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def azure_blob_handler_mocked(mock_from_connection_string):
blob_name = "some_blob_name"
handler = AzureBlobHandler(connection_string, container_name, blob_name)

handler._blob_client.container_name = "some_container_name"
handler._blob_client.blob_name = "some_blob_name"

remote_content = open(REMOTE_FILE_PATH, mode="r").read()
handler._blob_client.download_blob.return_value.readinto.side_effect = (
lambda buffer: handler.write(remote_content)
Expand Down Expand Up @@ -271,6 +274,34 @@ def test_push(self, azure_blob_handler_mocked):
content, overwrite=True
)

@patch("fourinsight.engineroom.utils._core.ContainerClient")
def test_blob_client_is_constructed_from_url(self, mock_container_client_cls):
AzureBlobHandler.from_container_url("some container", "state/state.json")
mock_container_client_cls.from_container_url.assert_called_once_with(
"some container"
)

@patch("fourinsight.engineroom.utils._core.ContainerClient")
def test_blob_client_gets_correct_blob(self, mock_container_client_cls):
AzureBlobHandler.from_container_url("some container", "state/state.json")
mock_container_client_cls.from_container_url().get_blob_client.assert_called_once_with(
"state/state.json"
)

@patch("fourinsight.engineroom.utils._core.ContainerClient")
def test_repr(self, mock_container_client_cls):
mock_blob_client = MagicMock()
mock_blob_client.container_name = "my-project"
mock_blob_client.blob_name = "state/state.json"
mock_container_client_cls.from_container_url().get_blob_client.return_value = (
mock_blob_client
)

handler = AzureBlobHandler.from_container_url(
"some container", "state/state.json"
)
assert repr(handler) == "AzureBlobHandler my-project/state/state.json"


class Test_PersistentDict:
def test__init__(self, local_file_handler_empty):
Expand Down
Loading