pilot-platform-object-storage 2.0.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/PKG-INFO +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/__init__.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/__init__.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/azure_blob_client.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/azure_container_client.py +5 -64
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/base_container_client.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/base_file_client.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/config.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/factories.py +2 -2
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/managers/__init__.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/managers/azure_blob_manager.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/managers/base_manager.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/__init__.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/azure.py +1 -62
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/enum.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/__init__.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/bucket_client.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/model.py +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/object_client.py +51 -5
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/pyproject.toml +1 -1
- {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/README.md +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (C) 2023-
|
|
1
|
+
# Copyright (C) 2023-2026 Indoc Systems
|
|
2
2
|
#
|
|
3
3
|
# Contact Indoc Systems for any questions regarding the use of this source code.
|
|
4
4
|
|
|
@@ -169,74 +169,15 @@ class AzureContainerClient(BaseContainerClient, AzureClient):
|
|
|
169
169
|
except Exception as exc:
|
|
170
170
|
raise self._handle_exception(exc)
|
|
171
171
|
|
|
172
|
-
async def
|
|
173
|
-
"""Deleted
|
|
172
|
+
async def batch_delete_files(self, keys: list[str]) -> None:
|
|
173
|
+
"""Deleted a list of files with all their snapshots."""
|
|
174
174
|
try:
|
|
175
175
|
async with ContainerClient.from_container_url(
|
|
176
176
|
container_url=self.container_sas_url, api_version=self.API_VERSION
|
|
177
177
|
) as container_client:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
except Exception as exc:
|
|
181
|
-
raise self._handle_exception(exc)
|
|
182
|
-
|
|
183
|
-
async def delete_file_with_versions(self, key: str, permanent_delete: bool = False) -> None:
|
|
184
|
-
"""Deleted a file with all its snapshots. Also delete the version of the file for soft deletion.
|
|
185
|
-
|
|
186
|
-
Also allowed the permanent delete of the deleted blob
|
|
187
|
-
"""
|
|
188
|
-
try:
|
|
189
|
-
async with ContainerClient.from_container_url(
|
|
190
|
-
container_url=self.container_sas_url, api_version=self.API_VERSION
|
|
191
|
-
) as container_client:
|
|
192
|
-
if not permanent_delete:
|
|
193
|
-
blob_client = container_client.get_blob_client(blob=key)
|
|
178
|
+
for key in keys:
|
|
179
|
+
blob_client = container_client.get_blob_client(key)
|
|
194
180
|
await self._delete(blob_client)
|
|
195
|
-
version_ids = await self._get_blob_versions(container_client, key)
|
|
196
|
-
await self._delete_versions(container_client, key, version_ids, permanent_delete)
|
|
197
|
-
except Exception as exc:
|
|
198
|
-
raise self._handle_exception(exc)
|
|
199
|
-
|
|
200
|
-
async def batch_delete_files_with_versions(self, keys: list[str], permanent_delete: bool = False) -> None:
|
|
201
|
-
"""Deleted a list of files with all its snapshots. Also delete the version of the files for soft deletion.
|
|
202
|
-
|
|
203
|
-
Also allowed the permanent delete of the deleted blob
|
|
204
|
-
"""
|
|
205
|
-
try:
|
|
206
|
-
for key in keys:
|
|
207
|
-
await self.delete_file_with_versions(key, permanent_delete)
|
|
208
|
-
except Exception as exc:
|
|
209
|
-
raise self._handle_exception(exc)
|
|
210
|
-
|
|
211
|
-
async def restore_file(self, key: str) -> None:
|
|
212
|
-
"""Restore the latest version of the file.
|
|
213
|
-
|
|
214
|
-
it will have following actions.
|
|
215
|
-
1. undelete the blob versions. since the delete action will move the version into
|
|
216
|
-
soft delete state. In such state, the copy from version is not allowed, which is required
|
|
217
|
-
when restoring the latest version.
|
|
218
|
-
2. restore the latest version by copying it to the base blob
|
|
219
|
-
3. delete the latest version
|
|
220
|
-
"""
|
|
221
|
-
try:
|
|
222
|
-
async with ContainerClient.from_container_url(
|
|
223
|
-
container_url=self.container_sas_url, api_version=self.API_VERSION
|
|
224
|
-
) as container_client:
|
|
225
|
-
await self._undetele_blob_versions(container_client, key)
|
|
226
|
-
version_id = await self._restore_latest_version(container_client, key)
|
|
227
|
-
|
|
228
|
-
# double delete the version soft-delete -> permanent delete
|
|
229
|
-
await container_client.delete_blob(blob=key, version_id=version_id)
|
|
230
|
-
await container_client.delete_blob(blob=key, version_id=version_id, blob_delete_type='permanent')
|
|
231
|
-
|
|
232
|
-
except Exception as exc:
|
|
233
|
-
raise self._handle_exception(exc)
|
|
234
|
-
|
|
235
|
-
async def batch_restore_files(self, keys: list[str]) -> None:
|
|
236
|
-
"""Restore the latest version of the files."""
|
|
237
|
-
try:
|
|
238
|
-
for key in keys:
|
|
239
|
-
await self.restore_file(key)
|
|
240
181
|
except Exception as exc:
|
|
241
182
|
raise self._handle_exception(exc)
|
|
242
183
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (C) 2023-
|
|
1
|
+
# Copyright (C) 2023-2026 Indoc Systems
|
|
2
2
|
#
|
|
3
3
|
# Contact Indoc Systems for any questions regarding the use of this source code.
|
|
4
4
|
|
|
@@ -17,7 +17,7 @@ def get_file_client(provider: Provider | str, sas_url: str, **kwargs: dict[str,
|
|
|
17
17
|
if provider == 'azure':
|
|
18
18
|
return AzureBlobClient(blob_sas_url=sas_url)
|
|
19
19
|
else:
|
|
20
|
-
raise ValueError(f
|
|
20
|
+
raise ValueError(f'Unknown provider: {provider}')
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def get_gcs_object_client(project_id: str, service_json: dict[str, Any]) -> GCSObjectClient:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (C) 2023-
|
|
1
|
+
# Copyright (C) 2023-2026 Indoc Systems
|
|
2
2
|
#
|
|
3
3
|
# Contact Indoc Systems for any questions regarding the use of this source code.
|
|
4
4
|
|
|
@@ -22,7 +22,6 @@ from azure.core.exceptions import HttpResponseError
|
|
|
22
22
|
from azure.core.exceptions import ResourceNotFoundError
|
|
23
23
|
from azure.storage.blob import BlobBlock
|
|
24
24
|
from azure.storage.blob.aio import BlobClient
|
|
25
|
-
from azure.storage.blob.aio import ContainerClient
|
|
26
25
|
|
|
27
26
|
logger = logging.getLogger('pilot.object-storage')
|
|
28
27
|
|
|
@@ -251,63 +250,3 @@ class AzureClient:
|
|
|
251
250
|
commited_block_list, uncommited_block_list = await client.get_block_list('all')
|
|
252
251
|
|
|
253
252
|
return commited_block_list, uncommited_block_list
|
|
254
|
-
|
|
255
|
-
async def _get_blob_versions(
|
|
256
|
-
self,
|
|
257
|
-
client: ContainerClient,
|
|
258
|
-
key: str,
|
|
259
|
-
) -> list[str]:
|
|
260
|
-
"""Get the version ids of a blob."""
|
|
261
|
-
|
|
262
|
-
items = client.list_blobs(name_starts_with=key, include=['versions', 'deleted'])
|
|
263
|
-
version_ids = [item['version_id'] async for item in items if item['version_id'] is not None]
|
|
264
|
-
if version_ids == []:
|
|
265
|
-
raise ResourceNotFoundError(message='No version id found')
|
|
266
|
-
return version_ids
|
|
267
|
-
|
|
268
|
-
async def _delete_versions(
|
|
269
|
-
self,
|
|
270
|
-
container_client: ContainerClient,
|
|
271
|
-
key: str,
|
|
272
|
-
version_ids: list[str],
|
|
273
|
-
permanent_delete: bool = False,
|
|
274
|
-
) -> None:
|
|
275
|
-
"""Delete the specific version of the blob."""
|
|
276
|
-
for version in version_ids:
|
|
277
|
-
if permanent_delete:
|
|
278
|
-
await container_client.delete_blob(blob=key, version_id=version, blob_delete_type='permanent')
|
|
279
|
-
else:
|
|
280
|
-
await container_client.delete_blob(blob=key, version_id=version)
|
|
281
|
-
|
|
282
|
-
async def _undetele_blob_versions(self, container_client: ContainerClient, key: str) -> None:
|
|
283
|
-
"""Undelete the blob versions."""
|
|
284
|
-
blob_client = container_client.get_blob_client(key)
|
|
285
|
-
await blob_client.undelete_blob()
|
|
286
|
-
|
|
287
|
-
async def _restore_latest_version(
|
|
288
|
-
self,
|
|
289
|
-
container_client: ContainerClient,
|
|
290
|
-
key: str,
|
|
291
|
-
) -> str:
|
|
292
|
-
"""Restore the latest version of the blob."""
|
|
293
|
-
version_ids = await self._get_blob_versions(container_client, key)
|
|
294
|
-
version_ids.sort(reverse=True)
|
|
295
|
-
|
|
296
|
-
# always restore the latest version, since we assume there is only one soft delete version
|
|
297
|
-
latest_version = version_ids[0]
|
|
298
|
-
version_blob_url = await self._format_blob_version_url(container_client, key, latest_version)
|
|
299
|
-
blob_client = container_client.get_blob_client(key)
|
|
300
|
-
|
|
301
|
-
# use the copy from url to restore the latest version
|
|
302
|
-
await self._copy_from_url(blob_client, version_blob_url)
|
|
303
|
-
return latest_version
|
|
304
|
-
|
|
305
|
-
async def _format_blob_version_url(
|
|
306
|
-
self,
|
|
307
|
-
container_client: ContainerClient,
|
|
308
|
-
key: str,
|
|
309
|
-
version_id: str,
|
|
310
|
-
) -> str:
|
|
311
|
-
"""Format the versioned blob url."""
|
|
312
|
-
blob_client = container_client.get_blob_client(key)
|
|
313
|
-
return f'{blob_client.url}&versionId={version_id}'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (C) 2025 Indoc Systems
|
|
1
|
+
# Copyright (C) 2025-2026 Indoc Systems
|
|
2
2
|
#
|
|
3
3
|
# Contact Indoc Systems for any questions regarding the use of this source code.
|
|
4
4
|
|
|
@@ -71,7 +71,7 @@ class GCSObjectClient:
|
|
|
71
71
|
|
|
72
72
|
def _part_object_name(self, final_key: str, upload_id: str, chunk_number: int) -> str:
|
|
73
73
|
# Temporary object path for each chunk in google starts with 'multipart/'
|
|
74
|
-
return f
|
|
74
|
+
return f'multipart/{upload_id}/{final_key}/part-{chunk_number:06d}'
|
|
75
75
|
|
|
76
76
|
# Object related methods
|
|
77
77
|
|
|
@@ -167,6 +167,52 @@ class GCSObjectClient:
|
|
|
167
167
|
|
|
168
168
|
return session_uri
|
|
169
169
|
|
|
170
|
+
async def cancel_resumable_upload(self, session_uri: str) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Summary:
|
|
173
|
+
Cancel a Google Cloud Storage resumable upload session. After cancellation,
|
|
174
|
+
the session URI becomes unusable and the upload cannot be resumed or finalized.
|
|
175
|
+
|
|
176
|
+
Behavior:
|
|
177
|
+
- Sends a DELETE to the session URI with Content-Length: 0 (required by GCS JSON API).
|
|
178
|
+
- A successful cancellation returns HTTP 499 (Client Closed Request).
|
|
179
|
+
- Some environments may return 204 (No Content) instead — also valid.
|
|
180
|
+
- If a session is already invalid, GCS may return 404 or 410; these are treated as success.
|
|
181
|
+
- Does NOT delete any uploaded draft chunks or create a final object, because the object
|
|
182
|
+
is only committed at the final POST request.
|
|
183
|
+
|
|
184
|
+
Parameters:
|
|
185
|
+
session_uri (str):
|
|
186
|
+
The resumable upload session URL returned from `get_resumable_session_uri`.
|
|
187
|
+
This is also the value returned by GCS in the `Location` header on upload init.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
None
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
RuntimeError:
|
|
194
|
+
If the DELETE request returns a status code outside the expected success cases.
|
|
195
|
+
|
|
196
|
+
References:
|
|
197
|
+
https://cloud.google.com/storage/docs/performing-resumable-uploads#cancel-upload
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def _cancel() -> None:
|
|
201
|
+
authed = self._authed()
|
|
202
|
+
headers = {'Content-Length': '0'}
|
|
203
|
+
|
|
204
|
+
resp = authed.delete(session_uri, headers=headers)
|
|
205
|
+
|
|
206
|
+
# Valid successful outcomes:
|
|
207
|
+
# 499 - documented success status for cancelled GCS resumable uploads
|
|
208
|
+
# 204 - alternative valid success response
|
|
209
|
+
# 404/410 - session already invalid/expired/completed (safe to treat as success)
|
|
210
|
+
success_statuses = (499, 204, 404, 410)
|
|
211
|
+
if resp.status_code not in success_statuses:
|
|
212
|
+
raise RuntimeError(f'Failed to cancel resumable upload: {resp.status_code} {resp.text}')
|
|
213
|
+
|
|
214
|
+
await asyncio.to_thread(_cancel)
|
|
215
|
+
|
|
170
216
|
async def get_presigned_download_url(
|
|
171
217
|
self,
|
|
172
218
|
bucket: str,
|
|
@@ -209,7 +255,7 @@ class GCSObjectClient:
|
|
|
209
255
|
"""
|
|
210
256
|
|
|
211
257
|
def _list() -> dict[int, int]:
|
|
212
|
-
prefix = f
|
|
258
|
+
prefix = f'multipart/{upload_id}/{final_key}/'
|
|
213
259
|
out: dict[int, int] = {}
|
|
214
260
|
for b in self._client.list_blobs(bucket, prefix=prefix):
|
|
215
261
|
leaf = b.name.rsplit('/', 1)[-1] # e.g., part-000037
|
|
@@ -318,7 +364,7 @@ class GCSObjectClient:
|
|
|
318
364
|
bucket_ref = self._get_bucket(bucket)
|
|
319
365
|
soft_delete_versions = list(self._client.list_blobs(bucket_ref, prefix=object_name, soft_deleted=True))
|
|
320
366
|
if not soft_delete_versions:
|
|
321
|
-
raise VersionNotFound(f
|
|
367
|
+
raise VersionNotFound(f'No soft-deleted versions found for {object_name!r}')
|
|
322
368
|
soft_delete_versions.sort(key=lambda b: (getattr(b, 'soft_delete_time', None), b.generation), reverse=True)
|
|
323
369
|
latest = soft_delete_versions[0]
|
|
324
370
|
gen = int(latest.generation)
|
|
@@ -331,7 +377,7 @@ class GCSObjectClient:
|
|
|
331
377
|
)
|
|
332
378
|
resp = authed.post(url, headers={'Content-Type': 'application/json'})
|
|
333
379
|
if resp.status_code >= 400:
|
|
334
|
-
raise RuntimeError(f
|
|
380
|
+
raise RuntimeError(f'Restore failed: {resp.status_code} {resp.text}')
|
|
335
381
|
|
|
336
382
|
live_blobs = list(self._client.list_blobs(bucket, match_glob=object_name))
|
|
337
383
|
live_blobs.sort(key=lambda b: (b.time_created, b.generation), reverse=True)
|
|
File without changes
|