Files
2026-07-13 13:22:34 +08:00

320 lines
11 KiB
Python

"""
This module provides utilities for performing Azure Blob Storage operations without requiring
the heavyweight azure-storage-blob library dependency
"""
import logging
import urllib
from copy import deepcopy
from mlflow.utils import rest_utils
from mlflow.utils.file_utils import read_chunk
_logger = logging.getLogger(__name__)
_PUT_BLOCK_HEADERS = {
"x-ms-blob-type": "BlockBlob",
}
def put_adls_file_creation(sas_url, headers):
"""Performs an ADLS Azure file create `Put` operation
(https://docs.microsoft.com/en-us/rest/api/storageservices/datalakestoragegen2/path/create)
Args:
sas_url: A shared access signature URL referring to the Azure ADLS server
to which the file creation command should be issued.
headers: Additional headers to include in the Put request body.
"""
request_url = _append_query_parameters(sas_url, {"resource": "file"})
request_headers = {}
for name, value in headers.items():
if _is_valid_adls_put_header(name):
request_headers[name] = value
else:
_logger.debug("Removed unsupported '%s' header for ADLS Gen2 Put operation", name)
with rest_utils.cloud_storage_http_request(
"put", request_url, headers=request_headers
) as response:
rest_utils.augmented_raise_for_status(response)
def patch_adls_file_upload(sas_url, local_file, start_byte, size, position, headers, is_single):
"""
Performs an ADLS Azure file create `Patch` operation
(https://docs.microsoft.com/en-us/rest/api/storageservices/datalakestoragegen2/path/update)
Args:
sas_url: A shared access signature URL referring to the Azure ADLS server
to which the file update command should be issued.
local_file: The local file to upload
start_byte: The starting byte of the local file to upload
size: The number of bytes to upload
position: Positional offset of the data in the Patch request
headers: Additional headers to include in the Patch request body
is_single: Whether this is the only patch operation for this file
"""
new_params = {"action": "append", "position": str(position)}
if is_single:
new_params["flush"] = "true"
request_url = _append_query_parameters(sas_url, new_params)
request_headers = {}
for name, value in headers.items():
if _is_valid_adls_patch_header(name):
request_headers[name] = value
else:
_logger.debug("Removed unsupported '%s' header for ADLS Gen2 Patch operation", name)
data = read_chunk(local_file, size, start_byte)
with rest_utils.cloud_storage_http_request(
"patch", request_url, data=data, headers=request_headers
) as response:
rest_utils.augmented_raise_for_status(response)
def patch_adls_flush(sas_url, position, headers):
"""Performs an ADLS Azure file flush `Patch` operation
(https://docs.microsoft.com/en-us/rest/api/storageservices/datalakestoragegen2/path/update)
Args:
sas_url: A shared access signature URL referring to the Azure ADLS server
to which the file update command should be issued.
position: The final size of the file to flush.
headers: Additional headers to include in the Patch request body.
"""
request_url = _append_query_parameters(sas_url, {"action": "flush", "position": str(position)})
request_headers = {}
for name, value in headers.items():
if _is_valid_adls_put_header(name):
request_headers[name] = value
else:
_logger.debug("Removed unsupported '%s' header for ADLS Gen2 Patch operation", name)
with rest_utils.cloud_storage_http_request(
"patch", request_url, headers=request_headers
) as response:
rest_utils.augmented_raise_for_status(response)
def put_block(sas_url, block_id, data, headers):
"""
Performs an Azure `Put Block` operation
(https://docs.microsoft.com/en-us/rest/api/storageservices/put-block)
Args:
sas_url: A shared access signature URL referring to the Azure Block Blob
to which the specified data should be staged.
block_id: A base64-encoded string identifying the block.
data: Data to include in the Put Block request body.
headers: Additional headers to include in the Put Block request body
(the `x-ms-blob-type` header is always included automatically).
"""
request_url = _append_query_parameters(sas_url, {"comp": "block", "blockid": block_id})
request_headers = deepcopy(_PUT_BLOCK_HEADERS)
for name, value in headers.items():
if _is_valid_put_block_header(name):
request_headers[name] = value
else:
_logger.debug("Removed unsupported '%s' header for Put Block operation", name)
with rest_utils.cloud_storage_http_request(
"put", request_url, data=data, headers=request_headers
) as response:
rest_utils.augmented_raise_for_status(response)
def put_block_list(sas_url, block_list, headers):
"""Performs an Azure `Put Block List` operation
(https://docs.microsoft.com/en-us/rest/api/storageservices/put-block-list)
Args:
sas_url: A shared access signature URL referring to the Azure Block Blob
to which the specified data should be staged.
block_list: A list of uncommitted base64-encoded string block IDs to commit. For
more information, see
https://docs.microsoft.com/en-us/rest/api/storageservices/put-block-list.
headers: Headers to include in the Put Block request body.
"""
request_url = _append_query_parameters(sas_url, {"comp": "blocklist"})
data = _build_block_list_xml(block_list)
request_headers = {}
for name, value in headers.items():
if _is_valid_put_block_list_header(name):
request_headers[name] = value
else:
_logger.debug("Removed unsupported '%s' header for Put Block List operation", name)
with rest_utils.cloud_storage_http_request(
"put", request_url, data=data, headers=request_headers
) as response:
rest_utils.augmented_raise_for_status(response)
def _append_query_parameters(url, parameters):
parsed_url = urllib.parse.urlparse(url)
query_dict = dict(urllib.parse.parse_qsl(parsed_url.query))
query_dict.update(parameters)
new_query = urllib.parse.urlencode(query_dict)
new_url_components = parsed_url._replace(query=new_query)
return urllib.parse.urlunparse(new_url_components)
def _build_block_list_xml(block_list):
xml = '<?xml version="1.0" encoding="utf-8"?>\n<BlockList>\n'
for block_id in block_list:
# Because block IDs are base64-encoded and base64 strings do not contain
# XML special characters, we can safely insert the block ID directly into
# the XML document
xml += f"<Uncommitted>{block_id}</Uncommitted>\n"
xml += "</BlockList>"
return xml
def _is_valid_put_block_list_header(header_name):
"""
Returns:
True if the specified header name is a valid header for the Put Block List operation,
False otherwise. For a list of valid headers, see https://docs.microsoft.com/en-us/
rest/api/storageservices/put-block-list#request-headers and https://docs.microsoft.com/
en-us/rest/api/storageservices/
specifying-conditional-headers-for-blob-service-operations#Subheading1.
"""
return header_name.startswith("x-ms-meta-") or header_name in {
"Authorization",
"Date",
"x-ms-date",
"x-ms-version",
"Content-Length",
"Content-MD5",
"x-ms-content-crc64",
"x-ms-blob-cache-control",
"x-ms-blob-content-type",
"x-ms-blob-content-encoding",
"x-ms-blob-content-language",
"x-ms-blob-content-md5",
"x-ms-encryption-scope",
"x-ms-tags",
"x-ms-lease-id",
"x-ms-client-request-id",
"x-ms-blob-content-disposition",
"x-ms-access-tier",
"If-Modified-Since",
"If-Unmodified-Since",
"If-Match",
"If-None-Match",
}
def _is_valid_put_block_header(header_name):
"""
Returns:
True if the specified header name is a valid header for the Put Block operation, False
otherwise. For a list of valid headers, see
https://docs.microsoft.com/en-us/rest/api/storageservices/put-block#request-headers and
https://docs.microsoft.com/en-us/rest/api/storageservices/put-block#
request-headers-customer-provided-encryption-keys.
"""
return header_name in {
"Authorization",
"x-ms-date",
"x-ms-version",
"Content-Length",
"Content-MD5",
"x-ms-content-crc64",
"x-ms-encryption-scope",
"x-ms-lease-id",
"x-ms-client-request-id",
"x-ms-encryption-key",
"x-ms-encryption-key-sha256",
"x-ms-encryption-algorithm",
}
def _is_valid_adls_put_header(header_name):
"""
Returns:
True if the specified header name is a valid header for the ADLS Put operation, False
otherwise. For a list of valid headers, see
https://docs.microsoft.com/en-us/rest/api/storageservices/datalakestoragegen2/path/create
"""
return header_name in {
"Cache-Control",
"Content-Encoding",
"Content-Language",
"Content-Disposition",
"x-ms-cache-control",
"x-ms-content-type",
"x-ms-content-encoding",
"x-ms-content-language",
"x-ms-content-disposition",
"x-ms-rename-source",
"x-ms-lease-id",
"x-ms-properties",
"x-ms-permissions",
"x-ms-umask",
"x-ms-owner",
"x-ms-group",
"x-ms-acl",
"x-ms-proposed-lease-id",
"x-ms-expiry-option",
"x-ms-expiry-time",
"If-Match",
"If-None-Match",
"If-Modified-Since",
"If-Unmodified-Since",
"x-ms-source-if-match",
"x-ms-source-if-none-match",
"x-ms-source-if-modified-since",
"x-ms-source-if-unmodified-since",
"x-ms-encryption-key",
"x-ms-encryption-key-sha256",
"x-ms-encryption-algorithm",
"x-ms-encryption-context",
"x-ms-client-request-id",
"x-ms-date",
"x-ms-version",
}
def _is_valid_adls_patch_header(header_name):
"""
Returns:
True if the specified header name is a valid header for the ADLS Patch operation, False
otherwise. For a list of valid headers, see
https://docs.microsoft.com/en-us/rest/api/storageservices/datalakestoragegen2/path/update
"""
return header_name in {
"Content-Length",
"Content-MD5",
"x-ms-lease-id",
"x-ms-cache-control",
"x-ms-content-type",
"x-ms-content-disposition",
"x-ms-content-encoding",
"x-ms-content-language",
"x-ms-content-md5",
"x-ms-properties",
"x-ms-owner",
"x-ms-group",
"x-ms-permissions",
"x-ms-acl",
"If-Match",
"If-None-Match",
"If-Modified-Since",
"If-Unmodified-Since",
"x-ms-encryption-key",
"x-ms-encryption-key-sha256",
"x-ms-encryption-algorithm",
"x-ms-encryption-context",
"x-ms-client-request-id",
"x-ms-date",
"x-ms-version",
}