Skip to content

Commit 9028d6f

Browse files
committed
Extract ABSStore to zarr._storage.absstore
First step towards being able to assign CODEOWNERS to simplify maintenance. This does not yet attempt the rename to zarr.storage.absstore in order lower conflicts with open PRs. Imports should remain un- changed for the moment. see: #764
1 parent efd94fa commit 9028d6f

File tree

3 files changed

+202
-191
lines changed

3 files changed

+202
-191
lines changed

zarr/_storage/__init__.py

Whitespace-only changes.

zarr/_storage/absstore.py

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""This module contains storage classes related to Azure Blob Storage (ABS)"""
2+
3+
import warnings
4+
from collections.abc import MutableMapping
5+
from numcodecs.compat import ensure_bytes
6+
from zarr.util import normalize_storage_path
7+
8+
__doctest_requires__ = {
9+
('ABSStore', 'ABSStore.*'): ['azure.storage.blob'],
10+
}
11+
12+
13+
class ABSStore(MutableMapping):
14+
"""Storage class using Azure Blob Storage (ABS).
15+
16+
Parameters
17+
----------
18+
container : string
19+
The name of the ABS container to use.
20+
.. deprecated::
21+
Use ``client`` instead.
22+
prefix : string
23+
Location of the "directory" to use as the root of the storage hierarchy
24+
within the container.
25+
account_name : string
26+
The Azure blob storage account name.
27+
.. deprecated:: 2.8.3
28+
Use ``client`` instead.
29+
account_key : string
30+
The Azure blob storage account access key.
31+
.. deprecated:: 2.8.3
32+
Use ``client`` instead.
33+
blob_service_kwargs : dictionary
34+
Extra arguments to be passed into the azure blob client, for e.g. when
35+
using the emulator, pass in blob_service_kwargs={'is_emulated': True}.
36+
.. deprecated:: 2.8.3
37+
Use ``client`` instead.
38+
dimension_separator : {'.', '/'}, optional
39+
Separator placed between the dimensions of a chunk.
40+
client : azure.storage.blob.ContainerClient, optional
41+
And ``azure.storage.blob.ContainerClient`` to connect with. See
42+
`here <https://docs.microsoft.com/en-us/python/api/azure-storage-blob/azure.storage.blob.containerclient?view=azure-python>`_ # noqa
43+
for more.
44+
45+
.. versionadded:: 2.8.3
46+
47+
Notes
48+
-----
49+
In order to use this store, you must install the Microsoft Azure Storage SDK for Python,
50+
``azure-storage-blob>=12.5.0``.
51+
"""
52+
53+
def __init__(self, container=None, prefix='', account_name=None, account_key=None,
54+
blob_service_kwargs=None, dimension_separator=None,
55+
client=None,
56+
):
57+
self._dimension_separator = dimension_separator
58+
self.prefix = normalize_storage_path(prefix)
59+
if client is None:
60+
# deprecated option, try to construct the client for them
61+
msg = (
62+
"Providing 'container', 'account_name', 'account_key', and 'blob_service_kwargs'"
63+
"is deprecated. Provide and instance of 'azure.storage.blob.ContainerClient' "
64+
"'client' instead."
65+
)
66+
warnings.warn(msg, FutureWarning, stacklevel=2)
67+
from azure.storage.blob import ContainerClient
68+
blob_service_kwargs = blob_service_kwargs or {}
69+
client = ContainerClient(
70+
"https://{}.blob.core.windows.net/".format(account_name), container,
71+
credential=account_key, **blob_service_kwargs
72+
)
73+
74+
self.client = client
75+
self._container = container
76+
self._account_name = account_name
77+
self._account_key = account_key
78+
79+
def _warn_deprecated(self, property_):
80+
msg = ("The {} property is deprecated and will be removed in a future "
81+
"version. Get the property from 'ABSStore.client' instead.")
82+
warnings.warn(msg.format(property_), FutureWarning, stacklevel=3)
83+
84+
@property
85+
def container(self):
86+
self._warn_deprecated("container")
87+
return self._container
88+
89+
@property
90+
def account_name(self):
91+
self._warn_deprecated("account_name")
92+
return self._account_name
93+
94+
@property
95+
def account_key(self):
96+
self._warn_deprecated("account_key")
97+
return self._account_key
98+
99+
def _append_path_to_prefix(self, path):
100+
if self.prefix == '':
101+
return normalize_storage_path(path)
102+
else:
103+
return '/'.join([self.prefix, normalize_storage_path(path)])
104+
105+
@staticmethod
106+
def _strip_prefix_from_path(path, prefix):
107+
# normalized things will not have any leading or trailing slashes
108+
path_norm = normalize_storage_path(path)
109+
prefix_norm = normalize_storage_path(prefix)
110+
if prefix:
111+
return path_norm[(len(prefix_norm)+1):]
112+
else:
113+
return path_norm
114+
115+
def __getitem__(self, key):
116+
from azure.core.exceptions import ResourceNotFoundError
117+
blob_name = self._append_path_to_prefix(key)
118+
try:
119+
return self.client.download_blob(blob_name).readall()
120+
except ResourceNotFoundError:
121+
raise KeyError('Blob %s not found' % blob_name)
122+
123+
def __setitem__(self, key, value):
124+
value = ensure_bytes(value)
125+
blob_name = self._append_path_to_prefix(key)
126+
self.client.upload_blob(blob_name, value, overwrite=True)
127+
128+
def __delitem__(self, key):
129+
from azure.core.exceptions import ResourceNotFoundError
130+
try:
131+
self.client.delete_blob(self._append_path_to_prefix(key))
132+
except ResourceNotFoundError:
133+
raise KeyError('Blob %s not found' % key)
134+
135+
def __eq__(self, other):
136+
return (
137+
isinstance(other, ABSStore) and
138+
self.client == other.client and
139+
self.prefix == other.prefix
140+
)
141+
142+
def keys(self):
143+
return list(self.__iter__())
144+
145+
def __iter__(self):
146+
if self.prefix:
147+
list_blobs_prefix = self.prefix + '/'
148+
else:
149+
list_blobs_prefix = None
150+
for blob in self.client.list_blobs(list_blobs_prefix):
151+
yield self._strip_prefix_from_path(blob.name, self.prefix)
152+
153+
def __len__(self):
154+
return len(self.keys())
155+
156+
def __contains__(self, key):
157+
blob_name = self._append_path_to_prefix(key)
158+
return self.client.get_blob_client(blob_name).exists()
159+
160+
def listdir(self, path=None):
161+
dir_path = normalize_storage_path(self._append_path_to_prefix(path))
162+
if dir_path:
163+
dir_path += '/'
164+
items = [
165+
self._strip_prefix_from_path(blob.name, dir_path)
166+
for blob in self.client.walk_blobs(name_starts_with=dir_path, delimiter='/')
167+
]
168+
return items
169+
170+
def rmdir(self, path=None):
171+
dir_path = normalize_storage_path(self._append_path_to_prefix(path))
172+
if dir_path:
173+
dir_path += '/'
174+
for blob in self.client.list_blobs(name_starts_with=dir_path):
175+
self.client.delete_blob(blob)
176+
177+
def getsize(self, path=None):
178+
store_path = normalize_storage_path(path)
179+
fs_path = self._append_path_to_prefix(store_path)
180+
if fs_path:
181+
blob_client = self.client.get_blob_client(fs_path)
182+
else:
183+
blob_client = None
184+
185+
if blob_client and blob_client.exists():
186+
return blob_client.get_blob_properties().size
187+
else:
188+
size = 0
189+
if fs_path == '':
190+
fs_path = None
191+
elif not fs_path.endswith('/'):
192+
fs_path += '/'
193+
for blob in self.client.walk_blobs(name_starts_with=fs_path, delimiter='/'):
194+
blob_client = self.client.get_blob_client(blob)
195+
if blob_client.exists():
196+
size += blob_client.get_blob_properties().size
197+
return size
198+
199+
def clear(self):
200+
self.rmdir()

0 commit comments

Comments
 (0)