diff --git a/poetry.lock b/poetry.lock index cfbd682..1864302 100644 --- a/poetry.lock +++ b/poetry.lock @@ -59,6 +59,23 @@ typing-extensions = ">=4.6.0" [package.extras] aio = ["aiohttp (>=3.0)"] +[[package]] +name = "azure-identity" +version = "1.15.0" +description = "Microsoft Azure Identity Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "azure-identity-1.15.0.tar.gz", hash = "sha256:4c28fc246b7f9265610eb5261d65931183d019a23d4b0e99357facb2e6c227c8"}, + {file = "azure_identity-1.15.0-py3-none-any.whl", hash = "sha256:a14b1f01c7036f11f148f22cd8c16e05035293d714458d6b44ddf534d93eb912"}, +] + +[package.dependencies] +azure-core = ">=1.23.0,<2.0.0" +cryptography = ">=2.5" +msal = ">=1.24.0,<2.0.0" +msal-extensions = ">=0.3.0,<2.0.0" + [[package]] name = "azure-storage-blob" version = "12.19.0" @@ -771,6 +788,43 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "msal" +version = "1.25.0" +description = "The Microsoft Authentication Library (MSAL) for Python library" +optional = false +python-versions = ">=2.7" +files = [ + {file = "msal-1.25.0-py2.py3-none-any.whl", hash = "sha256:386df621becb506bc315a713ec3d4d5b5d6163116955c7dde23622f156b81af6"}, + {file = "msal-1.25.0.tar.gz", hash = "sha256:f44329fdb59f4f044c779164a34474b8a44ad9e4940afbc4c3a3a2bbe90324d9"}, +] + +[package.dependencies] +cryptography = ">=0.6,<44" +PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} +requests = ">=2.0.0,<3" + +[package.extras] +broker = ["pymsalruntime (>=0.13.2,<0.14)"] + +[[package]] +name = "msal-extensions" +version = "1.0.0" +description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." +optional = false +python-versions = "*" +files = [ + {file = "msal-extensions-1.0.0.tar.gz", hash = "sha256:c676aba56b0cce3783de1b5c5ecfe828db998167875126ca4b47dc6436451354"}, + {file = "msal_extensions-1.0.0-py2.py3-none-any.whl", hash = "sha256:91e3db9620b822d0ed2b4d1850056a0f133cba04455e62f11612e40f5502f2ee"}, +] + +[package.dependencies] +msal = ">=0.4.1,<2.0.0" +portalocker = [ + {version = ">=1.0,<3", markers = "python_version >= \"3.5\" and platform_system != \"Windows\""}, + {version = ">=1.6,<3", markers = "python_version >= \"3.5\" and platform_system == \"Windows\""}, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -866,6 +920,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "portalocker" +version = "2.8.2" +description = "Wraps the portalocker recipe for easy usage" +optional = false +python-versions = ">=3.8" +files = [ + {file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"}, + {file = "portalocker-2.8.2.tar.gz", hash = "sha256:2b035aa7828e46c58e9b31390ee1f169b98e1066ab10b9a6a861fe7e25ee4f33"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["sphinx (>=1.7.1)"] +redis = ["redis"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] + [[package]] name = "psutil" version = "5.9.6" @@ -938,6 +1011,26 @@ files = [ {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, ] +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pyreadline3" version = "3.4.1" @@ -1518,4 +1611,4 @@ pyyaml = ">=6.0,<7.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "08bb4f9cbfd43d4b004bf674ca1348a7e236f8cb72962c7cbc7d21049ae2f81b" +content-hash = "298b4867ac587cd9ae7edb2b4f673f4364ae3e48941dbd261a2160106c2c6c53" diff --git a/pyproject.toml b/pyproject.toml index 195cc50..c4da767 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ snakemake-interface-common = "^1.14.2" snakemake-interface-storage-plugins = "^1.2.3" azure-storage-blob = "^12.19.0" azure-core = "^1.29.5" +azure-identity = "^1.15.0" [tool.poetry.group.dev.dependencies] diff --git a/snakemake_storage_plugin_az/__init__.py b/snakemake_storage_plugin_az/__init__.py index f1f6ef8..192f9a4 100644 --- a/snakemake_storage_plugin_az/__init__.py +++ b/snakemake_storage_plugin_az/__init__.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +import re from azure.storage.blob import BlobServiceClient from typing import Any, Iterable, Optional from snakemake_interface_storage_plugins.settings import StorageProviderSettingsBase @@ -15,6 +16,23 @@ from snakemake_interface_storage_plugins.io import IOCacheStorageInterface +def is_valid_azure_storage_blob_endpoint(endpoint_url: str) -> bool: + """ + Validates if the blob account endpoint is a valid Azure Storage Account URL. + + Args: + blob_account_url (str): The name of the environment variable. + + Returns: + bool: True if the environment variable is a valid Azure Storage Account URL. + """ + url_pattern = re.compile( + r"^https:\/\/[a-z0-9]+(\.[a-z0-9]+)*\.blob\.core\.windows\.net\/?(.+)?$" + ) + + return bool(url_pattern.match(endpoint_url)) + + # Optional: # Define settings for your storage plugin (e.g. host url, credentials). # They will occur in the Snakemake CLI as --storage-- @@ -53,7 +71,10 @@ class StorageProviderSettings(StorageProviderSettingsBase): access_key: Optional[str] = field( default=None, metadata={ - "help": "Azure Blob Storage Account Access Key Credential", + "help": ( + "Azure Blob Storage Account Access Key Credential.", + "If set, takes precedence over sas_token credential.", + ), "env_var": False, }, ) @@ -65,6 +86,18 @@ class StorageProviderSettings(StorageProviderSettingsBase): }, ) + def __post_init__(self): + if not is_valid_azure_storage_blob_endpoint(self.endpoint_url): + raise ValueError( + f"Invalid Azure Storage Blob Endpoint URL: {self.endpoint_url}" + ) + + self.credential = None + if self.access_key: + self.credential = self.access_key + elif self.sas_token: + self.credential = self.sas_token + # Required: # Implementation of your storage provider