Skip to content

Commit 157a6ea

Browse files
Plugin support (#32)
A plugin should define a new subclass of CloudPath and reexport it as `ExportedCloudPath`. It should also register new dispatches for all the functions it implements. --------- Co-authored-by: Shantanu <[email protected]> Co-authored-by: Shantanu <[email protected]>
1 parent 7e0be79 commit 157a6ea

File tree

4 files changed

+84
-14
lines changed

4 files changed

+84
-14
lines changed

boostedblob/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@
3535
from .path import isfile as isfile
3636
from .path import stat as stat
3737
from .syncing import sync as sync
38+
39+
# isort: off
40+
from .path_registry import _register_plugins
41+
42+
_register_plugins()
43+
# isort: on

boostedblob/azure_auth.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ async def get_access_token(cache_key: tuple[str, str | None]) -> tuple[Any, floa
155155
# This enables the use of Managed Identity, Workload Identity, and other auth methods not implemented here
156156
if creds["_azure_auth"] == "azure-identity":
157157
try:
158-
from azure.identity.aio import DefaultAzureCredential # type: ignore
158+
from azure.identity.aio import ( # type: ignore[import, unused-ignore]
159+
DefaultAzureCredential,
160+
)
159161
except ImportError as e:
160162
raise RuntimeError(
161163
"When setting AZURE_USE_IDENTITY=1, you must also install the azure-identity package"

boostedblob/path.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,28 @@
2222
class BasePath:
2323
@staticmethod
2424
def from_str(path: str) -> BasePath:
25+
from .path_registry import try_get_cloud_path_type
26+
2527
url = urllib.parse.urlparse(path)
26-
if url.scheme == "gs":
27-
return GooglePath.from_str(path)
28-
if url.scheme == "az" or (
29-
url.scheme == "https" and url.netloc.endswith(".blob.core.windows.net")
30-
):
31-
return AzurePath.from_str(path)
28+
cloud_path_type = try_get_cloud_path_type(url)
29+
if cloud_path_type:
30+
return cloud_path_type.from_str(path)
31+
3232
if url.scheme:
3333
raise ValueError(f"Invalid path '{path}'")
3434
return LocalPath(path)
3535

3636
@property
3737
def name(self) -> str:
3838
"""Returns the name of path, normalised to exclude any trailing slash."""
39-
raise NotImplementedError
39+
raise NotImplementedError(f"Name not implemented for {type(self)}")
4040

4141
@property
4242
def parent(self: T) -> T:
43-
raise NotImplementedError
43+
raise NotImplementedError(f"Parent not implemented for {type(self)}")
4444

4545
def relative_to(self: T, other: T) -> str:
46-
raise NotImplementedError
46+
raise NotImplementedError(f"relative_to not implemented for {type(self)}")
4747

4848
def is_relative_to(self: T, other: T) -> bool:
4949
try:
@@ -53,13 +53,13 @@ def is_relative_to(self: T, other: T) -> bool:
5353
return False
5454

5555
def is_directory_like(self) -> bool:
56-
raise NotImplementedError
56+
raise NotImplementedError(f"is_directory_like not implemented for {type(self)}")
5757

5858
def ensure_directory_like(self: T) -> T:
59-
raise NotImplementedError
59+
raise NotImplementedError(f"ensure_directory_like not implemented for {type(self)}")
6060

6161
def __truediv__(self: T, relative_path: str) -> T:
62-
raise NotImplementedError
62+
raise NotImplementedError(f"__truediv__ not implemented for {type(self)}")
6363

6464

6565
@dataclass(frozen=True)
@@ -115,7 +115,12 @@ def __fspath__(self) -> str:
115115

116116

117117
class CloudPath(BasePath):
118-
pass
118+
@staticmethod
119+
def is_cloud_path(url: urllib.parse.ParseResult) -> bool:
120+
"""
121+
Returns True if the URL is a cloud path for this cloud provider
122+
"""
123+
raise NotImplementedError
119124

120125

121126
@dataclass(frozen=True)
@@ -124,6 +129,12 @@ class AzurePath(CloudPath):
124129
container: str
125130
blob: str
126131

132+
@staticmethod
133+
def is_cloud_path(url: urllib.parse.ParseResult) -> bool:
134+
return url.scheme == "az" or (
135+
url.scheme == "https" and url.netloc.endswith(".blob.core.windows.net")
136+
)
137+
127138
@staticmethod
128139
def from_str(url: str) -> AzurePath:
129140
parsed_url = urllib.parse.urlparse(url)
@@ -196,6 +207,10 @@ class GooglePath(CloudPath):
196207
bucket: str
197208
blob: str
198209

210+
@staticmethod
211+
def is_cloud_path(url: urllib.parse.ParseResult) -> bool:
212+
return url.scheme == "gs"
213+
199214
@staticmethod
200215
def from_str(url: str) -> GooglePath:
201216
parsed_url = urllib.parse.urlparse(url)

boostedblob/path_registry.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import functools
2+
import importlib
3+
import pkgutil
4+
import urllib.parse
5+
from typing import Optional, Sequence
6+
7+
from .path import CloudPath
8+
9+
plugins: list[type[CloudPath]] = []
10+
11+
12+
def _available_plugin_modules() -> Sequence[str]:
13+
# boostedblob_ext is a namespace package
14+
# submodules inside boostedblob_ext will be inspected for ExportedCloudPath attributes
15+
# - we use namespace package pattern so `pkgutil.iter_modules` is fast
16+
# - it's a separate top-level package because namespace subpackages of non-namespace
17+
# packages don't quite do what you want with editable installs
18+
try:
19+
import boostedblob_ext # type: ignore[import, unused-ignore]
20+
except ImportError:
21+
return []
22+
23+
mods = []
24+
plugin_mods = pkgutil.iter_modules(boostedblob_ext.__path__, boostedblob_ext.__name__ + ".")
25+
for _, mod_name, _ in plugin_mods:
26+
mods.append(mod_name)
27+
return mods
28+
29+
30+
@functools.cache
31+
def _register_plugins() -> None:
32+
from .path import AzurePath, GooglePath
33+
34+
plugins.append(AzurePath)
35+
plugins.append(GooglePath)
36+
37+
for mod_name in _available_plugin_modules():
38+
mod = importlib.import_module(mod_name)
39+
if hasattr(mod, "ExportedCloudPath"):
40+
plugins.append(mod.ExportedCloudPath)
41+
42+
43+
def try_get_cloud_path_type(url: urllib.parse.ParseResult) -> Optional[type[CloudPath]]:
44+
for plugin in plugins:
45+
if plugin.is_cloud_path(url):
46+
return plugin
47+
return None

0 commit comments

Comments
 (0)