Skip to content

Commit e2f6b83

Browse files
committed
Add AZURE_REQUEST_OPTIONS setting (#1179)
This PR changes no behavior except to provide a settings "hook" that you can use to pass-through azure cli request options. There was already one such option being passed through, `timeout`, so I've refactored things slighly to avoid duplication.
1 parent f029e50 commit e2f6b83

File tree

3 files changed

+57
-6
lines changed

3 files changed

+57
-6
lines changed

docs/backends/azure.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ Settings
195195
Additionally, this setting can be used to configure the client retry settings. To see how follow the
196196
`Python retry docs <https://learn.microsoft.com/en-us/azure/storage/blobs/storage-retry-policy-python>`__.
197197

198+
``request_options`` or ``AZURE_REQUEST_OPTIONS``
199+
200+
Default: ``{}``
201+
202+
A dict of kwarg options to set on each request for the ``BlobServiceClient``. A partial list of options can be found
203+
`in the client docs <https://learn.microsoft.com/en-us/python/api/overview/azure/storage-blob-readme?view=azure-python#other-client--per-operation-configuration>`__.
204+
205+
A no-argument callable can be used to set the value at request time. For example, if you are using django-guid
206+
and want to pass through the request id::
207+
208+
from django_guid import get_guid
209+
210+
AZURE_REQUEST_OPTIONS = {
211+
"client_request_id": get_guid
212+
}
213+
198214
``api_version`` or ``AZURE_API_VERSION``
199215

200216
Default: ``None``

storages/backends/azure_storage.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def _get_file(self):
4747

4848
if "r" in self._mode or "a" in self._mode:
4949
download_stream = self._storage.client.download_blob(
50-
self._path, timeout=self._storage.timeout
50+
self._path, **self._storage._request_options()
5151
)
5252
download_stream.readinto(file)
5353
if "r" in self._mode:
@@ -132,6 +132,22 @@ def __init__(self, **settings):
132132
if not self.account_key and "AccountKey" in parsed:
133133
self.account_key = parsed["AccountKey"]
134134

135+
def _request_options(self):
136+
"""
137+
If callables were provided in request_options, evaluate them and return
138+
the concrete values. Include "timeout", which was a previously-supported
139+
request option before the introduction of the request_options setting.
140+
"""
141+
if not self.request_options:
142+
return {"timeout": self.timeout}
143+
callable_allowed = ("raw_response_hook", "raw_request_hook")
144+
options = self.request_options.copy()
145+
options["timeout"] = self.timeout
146+
for key, value in self.request_options.items():
147+
if key not in callable_allowed and callable(value):
148+
options[key] = value()
149+
return options
150+
135151
def get_default_settings(self):
136152
return {
137153
"account_name": setting("AZURE_ACCOUNT_NAME"),
@@ -154,6 +170,7 @@ def get_default_settings(self):
154170
"token_credential": setting("AZURE_TOKEN_CREDENTIAL"),
155171
"api_version": setting("AZURE_API_VERSION", None),
156172
"client_options": setting("AZURE_CLIENT_OPTIONS", {}),
173+
"request_options": setting("AZURE_REQUEST_OPTIONS", {}),
157174
}
158175

159176
def _get_service_client(self):
@@ -252,13 +269,13 @@ def exists(self, name):
252269

253270
def delete(self, name):
254271
try:
255-
self.client.delete_blob(self._get_valid_path(name), timeout=self.timeout)
272+
self.client.delete_blob(self._get_valid_path(name), **self._request_options())
256273
except ResourceNotFoundError:
257274
pass
258275

259276
def size(self, name):
260277
blob_client = self.client.get_blob_client(self._get_valid_path(name))
261-
properties = blob_client.get_blob_properties(timeout=self.timeout)
278+
properties = blob_client.get_blob_properties(**self._request_options())
262279
return properties.size
263280

264281
def _save(self, name, content):
@@ -276,8 +293,8 @@ def _save(self, name, content):
276293
content,
277294
content_settings=ContentSettings(**params),
278295
max_concurrency=self.upload_max_conn,
279-
timeout=self.timeout,
280296
overwrite=self.overwrite_files,
297+
**self._request_options(),
281298
)
282299
return cleaned_name
283300

@@ -350,7 +367,7 @@ def get_modified_time(self, name):
350367
USE_TZ is True, otherwise returns a naive datetime in the local timezone.
351368
"""
352369
blob_client = self.client.get_blob_client(self._get_valid_path(name))
353-
properties = blob_client.get_blob_properties(timeout=self.timeout)
370+
properties = blob_client.get_blob_properties(**self._request_options())
354371
if not setting("USE_TZ", False):
355372
return timezone.make_naive(properties.last_modified)
356373

@@ -372,7 +389,7 @@ def list_all(self, path=""):
372389
return [
373390
blob.name
374391
for blob in self.client.list_blobs(
375-
name_starts_with=path, timeout=self.timeout
392+
name_starts_with=path, **self._request_options()
376393
)
377394
]
378395

tests/test_azure.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,21 @@ def test_client_settings(self, bsc):
378378
bsc.assert_called_once_with(
379379
"https://test.blob.core.windows.net", credential=None, api_version="1.3"
380380
)
381+
382+
def test_lazy_evaluated_request_options(self):
383+
foo = mock.MagicMock()
384+
foo.side_effect = [1, 2] # return different values the two times it is called
385+
with override_settings(AZURE_REQUEST_OPTIONS={"key1": 5, "key2": foo}):
386+
storage = azure_storage.AzureStorage()
387+
client_mock = mock.MagicMock()
388+
storage._client = client_mock
389+
390+
_, _ = storage.listdir("")
391+
client_mock.list_blobs.assert_called_with(
392+
name_starts_with="", timeout=20, key1=5, key2=1
393+
)
394+
395+
_, _ = storage.listdir("")
396+
client_mock.list_blobs.assert_called_with(
397+
name_starts_with="", timeout=20, key1=5, key2=2
398+
)

0 commit comments

Comments
 (0)