From a5f067087c49fa4061e7ea0de66c70177b7660fd Mon Sep 17 00:00:00 2001 From: heidi-holm-4ss Date: Thu, 12 Mar 2026 14:13:54 +0100 Subject: [PATCH 1/3] feat: add class method from_container_url() --- fourinsight/engineroom/utils/_core.py | 33 +++++++++++++++++++++++++-- tests/test_core.py | 31 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/fourinsight/engineroom/utils/_core.py b/fourinsight/engineroom/utils/_core.py index ec4585ce..5e1fea52 100644 --- a/fourinsight/engineroom/utils/_core.py +++ b/fourinsight/engineroom/utils/_core.py @@ -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 @@ -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://.blob.core.windows.net/?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) diff --git a/tests/test_core.py b/tests/test_core.py index e4601821..e677db13 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -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) @@ -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): From 0d5726facd8a58a8c93f894548c77e5fc63429f6 Mon Sep 17 00:00:00 2001 From: heidi-holm-4ss Date: Thu, 12 Mar 2026 14:16:58 +0100 Subject: [PATCH 2/3] doc: add example of new method --- docs/user_guide/concepts_utils/handlers.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/user_guide/concepts_utils/handlers.rst b/docs/user_guide/concepts_utils/handlers.rst index f8fc0d0a..268b1b2b 100644 --- a/docs/user_guide/concepts_utils/handlers.rst +++ b/docs/user_guide/concepts_utils/handlers.rst @@ -26,7 +26,11 @@ The :class:`~fourinsight.engineroom.utils.AzureBlobHandler` is used to store tex from fourinsight.engineroom.utils import AzureBlobHandler - handler = AzureBlobHandler(, , ) + # 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 From d14a20685fbfe70ef3f30c4475a185833ee2d2af Mon Sep 17 00:00:00 2001 From: heidi-holm-4ss Date: Thu, 12 Mar 2026 14:18:04 +0100 Subject: [PATCH 3/3] typo --- docs/user_guide/concepts_utils/handlers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/concepts_utils/handlers.rst b/docs/user_guide/concepts_utils/handlers.rst index 268b1b2b..ef249802 100644 --- a/docs/user_guide/concepts_utils/handlers.rst +++ b/docs/user_guide/concepts_utils/handlers.rst @@ -26,7 +26,7 @@ The :class:`~fourinsight.engineroom.utils.AzureBlobHandler` is used to store tex from fourinsight.engineroom.utils import AzureBlobHandler - # Instantiate from a connection string + # Instantiate from a connection string handler = AzureBlobHandler(conn_str, container_name, blob_name) # Instantiate from a container-level SAS URL