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.
Files changed (21) hide show
  1. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/PKG-INFO +1 -1
  2. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/__init__.py +1 -1
  3. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/__init__.py +1 -1
  4. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/azure_blob_client.py +1 -1
  5. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/azure_container_client.py +5 -64
  6. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/base_container_client.py +1 -1
  7. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/clients/base_file_client.py +1 -1
  8. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/config.py +1 -1
  9. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/factories.py +2 -2
  10. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/managers/__init__.py +1 -1
  11. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/managers/azure_blob_manager.py +1 -1
  12. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/managers/base_manager.py +1 -1
  13. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/__init__.py +1 -1
  14. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/azure.py +1 -62
  15. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/enum.py +1 -1
  16. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/__init__.py +1 -1
  17. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/bucket_client.py +1 -1
  18. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/model.py +1 -1
  19. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/object_storage/providers/gcp/object_client.py +51 -5
  20. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/pyproject.toml +1 -1
  21. {pilot_platform_object_storage-2.0.0 → pilot_platform_object_storage-3.0.0}/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pilot-platform-object-storage
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: Python library for interacting with multiple object storage APIs.
5
5
  License: Proprietary
6
6
  Author: Indoc Systems
@@ -1,3 +1,3 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
1
+ # Copyright (C) 2023-2026 Indoc Systems
2
2
  #
3
3
  # Contact Indoc Systems for any questions regarding the use of this source code.
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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 delete_versions(self, key: str, permanent_delete: bool = False) -> None:
173
- """Deleted the versions in the file."""
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
- version_ids = await self._get_blob_versions(container_client, key)
179
- await self._delete_versions(container_client, key, version_ids, permanent_delete)
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-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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"Unknown provider: {provider}")
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-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
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) 2023-2025 Indoc Systems
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
 
@@ -1,3 +1,3 @@
1
- # Copyright (C) 2023-2025 Indoc Systems
1
+ # Copyright (C) 2023-2026 Indoc Systems
2
2
  #
3
3
  # Contact Indoc Systems for any questions regarding the use of this source code.
@@ -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
 
@@ -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
 
@@ -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"multipart/{upload_id}/{final_key}/part-{chunk_number:06d}"
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"multipart/{upload_id}/{final_key}/"
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"No soft-deleted versions found for {object_name!r}")
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"Restore failed: {resp.status_code} {resp.text}")
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)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pilot-platform-object-storage"
3
- version = "2.0.0"
3
+ version = "3.0.0"
4
4
  description = "Python library for interacting with multiple object storage APIs."
5
5
  authors = ["Indoc Systems"]
6
6
  readme = "README.md"