cloudbridge 4.2.0__tar.gz → 4.2.1__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 (70) hide show
  1. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/PKG-INFO +5 -2
  2. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/__init__.py +1 -1
  3. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/resources.py +17 -1
  4. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/resources.py +10 -2
  5. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/azure_client.py +6 -2
  6. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/resources.py +6 -2
  7. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/resources.py +13 -2
  8. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/resources.py +21 -3
  9. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/PKG-INFO +5 -2
  10. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/pyproject.toml +7 -2
  11. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_object_store_service.py +63 -0
  12. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/LICENSE +0 -0
  13. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/README.rst +0 -0
  14. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/__init__.py +0 -0
  15. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/helpers.py +0 -0
  16. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/middleware.py +0 -0
  17. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/provider.py +0 -0
  18. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/resources.py +0 -0
  19. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/services.py +0 -0
  20. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/base/subservices.py +0 -0
  21. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/factory.py +0 -0
  22. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/__init__.py +0 -0
  23. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/exceptions.py +0 -0
  24. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/provider.py +0 -0
  25. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/services.py +0 -0
  26. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/interfaces/subservices.py +0 -0
  27. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/__init__.py +0 -0
  28. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/__init__.py +0 -0
  29. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/helpers.py +0 -0
  30. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/provider.py +0 -0
  31. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/services.py +0 -0
  32. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/aws/subservices.py +0 -0
  33. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/__init__.py +0 -0
  34. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/helpers.py +0 -0
  35. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/provider.py +0 -0
  36. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/services.py +0 -0
  37. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/azure/subservices.py +0 -0
  38. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/__init__.py +0 -0
  39. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/helpers.py +0 -0
  40. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/provider.py +0 -0
  41. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/services.py +0 -0
  42. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/gcp/subservices.py +0 -0
  43. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/mock/__init__.py +0 -0
  44. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/mock/provider.py +0 -0
  45. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/__init__.py +0 -0
  46. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/helpers.py +0 -0
  47. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/provider.py +0 -0
  48. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/services.py +0 -0
  49. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/providers/openstack/subservices.py +0 -0
  50. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge/py.typed +0 -0
  51. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/SOURCES.txt +0 -0
  52. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/dependency_links.txt +0 -0
  53. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/requires.txt +0 -0
  54. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/cloudbridge.egg-info/top_level.txt +0 -0
  55. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/setup.cfg +0 -0
  56. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_base_helpers.py +0 -0
  57. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_block_store_service.py +0 -0
  58. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_cloud_factory.py +0 -0
  59. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_cloud_helpers.py +0 -0
  60. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_compute_service.py +0 -0
  61. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_dns_service.py +0 -0
  62. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_image_service.py +0 -0
  63. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_interface.py +0 -0
  64. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_middleware_system.py +0 -0
  65. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_multipart_driver.py +0 -0
  66. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_network_service.py +0 -0
  67. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_object_life_cycle.py +0 -0
  68. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_region_service.py +0 -0
  69. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_security_service.py +0 -0
  70. {cloudbridge-4.2.0 → cloudbridge-4.2.1}/tests/test_vm_types_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudbridge
3
- Version: 4.2.0
3
+ Version: 4.2.1
4
4
  Summary: A simple layer of abstraction over multiple cloud providers.
5
5
  Author-email: Galaxy and GVL Projects <help@genome.edu.au>
6
6
  License-Expression: MIT
@@ -15,10 +15,13 @@ Classifier: Intended Audience :: System Administrators
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
18
21
  Classifier: Programming Language :: Python :: 3.13
19
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
20
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
- Requires-Python: >=3.13
24
+ Requires-Python: >=3.10
22
25
  Description-Content-Type: text/x-rst
23
26
  License-File: LICENSE
24
27
  Requires-Dist: tenacity>=6.0
@@ -3,7 +3,7 @@ import logging
3
3
  from typing import Any
4
4
 
5
5
  # Current version of the library
6
- __version__ = '4.2.0'
6
+ __version__ = '4.2.1'
7
7
 
8
8
 
9
9
  def get_version() -> str:
@@ -2435,7 +2435,9 @@ class BucketObject(CloudResource):
2435
2435
  pass
2436
2436
 
2437
2437
  @abstractmethod
2438
- def generate_url(self, expires_in: int, writable: bool = False) -> str:
2438
+ def generate_url(self, expires_in: int, writable: bool = False,
2439
+ content_disposition: str | None = None,
2440
+ content_type: str | None = None) -> str:
2439
2441
  """
2440
2442
  Generate a signed URL to this object.
2441
2443
 
@@ -2449,6 +2451,20 @@ class BucketObject(CloudResource):
2449
2451
  :param writable: Write permission for this signed URL. Users with the URL
2450
2452
  will be able to upload to this object, but they will NOT be able to
2451
2453
  read from it.
2454
+ :type content_disposition: ``str``
2455
+ :param content_disposition: When set, ask the backing store to serve
2456
+ the object with this ``Content-Disposition`` response header on
2457
+ GET (e.g. ``attachment; filename="data.txt"``). This is a
2458
+ response-serving hint: every provider accepts it and honors it
2459
+ where the backing store supports response-header overrides (AWS,
2460
+ Azure and GCP fully; OpenStack Swift honors the filename portion
2461
+ via its tempurl ``filename`` parameter). Ignored when
2462
+ ``writable`` is ``True``.
2463
+ :type content_type: ``str``
2464
+ :param content_type: When set, ask the backing store to serve the
2465
+ object with this ``Content-Type`` response header on GET. Honored
2466
+ by AWS, Azure and GCP; OpenStack Swift cannot override the
2467
+ content type. Ignored when ``writable`` is ``True``.
2452
2468
 
2453
2469
  :rtype: ``str``
2454
2470
  :return: A URL to access the object.
@@ -947,11 +947,19 @@ class AWSBucketObject(BaseBucketObject):
947
947
  def delete(self) -> None:
948
948
  self._obj.delete()
949
949
 
950
- def generate_url(self, expires_in: int, writable: bool = False) -> str:
950
+ def generate_url(self, expires_in: int, writable: bool = False,
951
+ content_disposition: str | None = None,
952
+ content_type: str | None = None) -> str:
953
+ params = {'Bucket': self._obj.bucket_name, 'Key': self.id}
954
+ if not writable:
955
+ if content_disposition:
956
+ params['ResponseContentDisposition'] = content_disposition
957
+ if content_type:
958
+ params['ResponseContentType'] = content_type
951
959
  return cast("AWSCloudProvider", self._provider).s3_conn.meta.client \
952
960
  .generate_presigned_url(
953
961
  'put_object' if writable else 'get_object',
954
- Params={'Bucket': self._obj.bucket_name, 'Key': self.id},
962
+ Params=params,
955
963
  ExpiresIn=expires_in)
956
964
 
957
965
  def refresh(self) -> None:
@@ -508,7 +508,9 @@ class AzureClient(object):
508
508
  blob_client.delete_blob(delete_snapshots)
509
509
 
510
510
  def get_blob_url(self, container_name: Any, blob_name: str,
511
- expiry_time: int, writable: bool) -> str:
511
+ expiry_time: int, writable: bool,
512
+ content_disposition: str | None = None,
513
+ content_type: str | None = None) -> str:
512
514
  now = datetime.datetime.utcnow()
513
515
  expiry = now + datetime.timedelta(
514
516
  seconds=expiry_time)
@@ -520,7 +522,9 @@ class AzureClient(object):
520
522
  sas = generate_blob_sas(
521
523
  self.storage_account, container_name, blob_name,
522
524
  permission=BlobSasPermissions(read=True, write=writable), expiry=expiry,
523
- user_delegation_key=delegation_key
525
+ user_delegation_key=delegation_key,
526
+ content_disposition=content_disposition,
527
+ content_type=content_type
524
528
  )
525
529
  url = f"https://{self.storage_account}.blob.core.windows.net/{container_name}/{blob_name}?{sas}"
526
530
  return url
@@ -311,13 +311,17 @@ class AzureBucketObject(BaseBucketObject):
311
311
  """
312
312
  self._blob_client.delete_blob()
313
313
 
314
- def generate_url(self, expires_in: int, writable: bool = False) -> str:
314
+ def generate_url(self, expires_in: int, writable: bool = False,
315
+ content_disposition: str | None = None,
316
+ content_type: str | None = None) -> str:
315
317
  """
316
318
  Generate a URL to this object.
317
319
  """
318
320
  return cast(
319
321
  "AzureCloudProvider", self._provider).azure_client.get_blob_url(
320
- self._container, self.name, expires_in, writable)
322
+ self._container, self.name, expires_in, writable,
323
+ content_disposition=None if writable else content_disposition,
324
+ content_type=None if writable else content_type)
321
325
 
322
326
  def refresh(self) -> None:
323
327
  pass
@@ -2198,7 +2198,9 @@ class GCPBucketObject(BaseBucketObject):
2198
2198
  .delete(bucket=self._obj['bucket'], object=self.name)
2199
2199
  .execute())
2200
2200
 
2201
- def generate_url(self, expires_in: int, writable: bool = False) -> str:
2201
+ def generate_url(self, expires_in: int, writable: bool = False,
2202
+ content_disposition: str | None = None,
2203
+ content_type: str | None = None) -> str:
2202
2204
  """
2203
2205
  Generates a signed URL accessible to everyone.
2204
2206
 
@@ -2210,10 +2212,19 @@ class GCPBucketObject(BaseBucketObject):
2210
2212
  provider = cast("GCPCloudProvider", self._provider)
2211
2213
  http_method = "PUT" if writable else "GET"
2212
2214
 
2215
+ query_parameters: dict[str, Any] = {}
2216
+ if not writable:
2217
+ if content_disposition:
2218
+ query_parameters[
2219
+ 'response-content-disposition'] = content_disposition
2220
+ if content_type:
2221
+ query_parameters['response-content-type'] = content_type
2222
+
2213
2223
  # pylint:disable=protected-access
2214
2224
  return helpers.generate_signed_url(
2215
2225
  provider._credentials, self._obj['bucket'], self.name,
2216
- expiration=expires_in, http_method=http_method)
2226
+ expiration=expires_in, http_method=http_method,
2227
+ query_parameters=query_parameters or None)
2217
2228
 
2218
2229
  def refresh(self) -> None:
2219
2230
  # pylint:disable=protected-access
@@ -14,6 +14,7 @@ from typing import IO
14
14
  from typing import Iterable
15
15
  from typing import TYPE_CHECKING
16
16
  from typing import cast
17
+ from urllib.parse import quote
17
18
  from urllib.parse import urljoin
18
19
  from urllib.parse import urlparse
19
20
 
@@ -1474,7 +1475,9 @@ class OpenStackBucketObject(BaseBucketObject):
1474
1475
  for del_res in swift.delete(self.cbcontainer.name, [self.name, ]):
1475
1476
  del_res['success']
1476
1477
 
1477
- def generate_url(self, expires_in: int, writable: bool = False) -> str:
1478
+ def generate_url(self, expires_in: int, writable: bool = False,
1479
+ content_disposition: str | None = None,
1480
+ content_type: str | None = None) -> str:
1478
1481
  http_method = "PUT" if writable else "GET"
1479
1482
  # Set a temp url key on the object (http://bit.ly/2NBiXGD)
1480
1483
  temp_url_key = "cloudbridge-tmp-url-key"
@@ -1484,8 +1487,23 @@ class OpenStackBucketObject(BaseBucketObject):
1484
1487
  base_url = urlparse(swift.get_service_auth()[0])
1485
1488
  access_point = "{0}://{1}".format(base_url.scheme, base_url.netloc)
1486
1489
  url_path = "/".join([base_url.path, self.cbcontainer.name, self.name])
1487
- return urljoin(access_point, generate_temp_url(url_path, expires_in,
1488
- temp_url_key, http_method))
1490
+ url = urljoin(access_point, generate_temp_url(url_path, expires_in,
1491
+ temp_url_key,
1492
+ http_method))
1493
+ if not writable and content_disposition:
1494
+ # Swift's tempurl middleware serves Content-Disposition via the
1495
+ # unsigned `filename`/`inline` query parameters (the signature
1496
+ # covers only method, path and expiry); Content-Type cannot be
1497
+ # overridden.
1498
+ match = re.search(r'filename\*?=(?:"([^"]+)"|([^;]+))',
1499
+ content_disposition)
1500
+ filename = (match.group(1) or match.group(2)).strip() \
1501
+ if match else None
1502
+ if content_disposition.strip().lower().startswith('inline'):
1503
+ url += '&inline'
1504
+ if filename:
1505
+ url += '&filename=' + quote(filename)
1506
+ return url
1489
1507
 
1490
1508
  def refresh(self) -> None:
1491
1509
  self._obj = self.cbcontainer.objects.get(self.id)._obj
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudbridge
3
- Version: 4.2.0
3
+ Version: 4.2.1
4
4
  Summary: A simple layer of abstraction over multiple cloud providers.
5
5
  Author-email: Galaxy and GVL Projects <help@genome.edu.au>
6
6
  License-Expression: MIT
@@ -15,10 +15,13 @@ Classifier: Intended Audience :: System Administrators
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
18
21
  Classifier: Programming Language :: Python :: 3.13
19
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
20
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
- Requires-Python: >=3.13
24
+ Requires-Python: >=3.10
22
25
  Description-Content-Type: text/x-rst
23
26
  License-File: LICENSE
24
27
  Requires-Dist: tenacity>=6.0
@@ -8,7 +8,7 @@ description = "A simple layer of abstraction over multiple cloud providers."
8
8
  readme = "README.rst"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
11
- requires-python = ">=3.13"
11
+ requires-python = ">=3.10"
12
12
  authors = [
13
13
  { name = "Galaxy and GVL Projects", email = "help@genome.edu.au" },
14
14
  ]
@@ -21,6 +21,9 @@ classifiers = [
21
21
  "Operating System :: OS Independent",
22
22
  "Programming Language :: Python",
23
23
  "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
24
27
  "Programming Language :: Python :: 3.13",
25
28
  "Programming Language :: Python :: Implementation :: CPython",
26
29
  "Topic :: Software Development :: Libraries :: Python Modules",
@@ -118,7 +121,9 @@ parallel = true
118
121
  # factory) is held to a strict baseline so downstream users get a fully-typed
119
122
  # API; the base implementations and providers (which wrap untyped cloud SDKs)
120
123
  # are temporarily exempted below and ratcheted to strict module-by-module.
121
- python_version = "3.13"
124
+ # Type-check against the LOWEST supported version so use of newer-stdlib
125
+ # APIs is caught at lint time.
126
+ python_version = "3.10"
122
127
  files = ["cloudbridge"]
123
128
  ignore_missing_imports = true # untyped cloud SDKs (boto3, azure-*, ...) -> Any
124
129
  namespace_packages = true
@@ -229,6 +229,69 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
229
229
  obj.save_content(target_stream)
230
230
  self.assertEqual(target_stream.getvalue(), content)
231
231
 
232
+ @helpers.skipIfNoService(['storage.buckets'])
233
+ def test_generate_url_with_response_headers(self):
234
+ name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
235
+ test_bucket = self.provider.storage.buckets.create(name)
236
+
237
+ with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
238
+ obj_name = "hello_response_headers.txt"
239
+ obj = test_bucket.objects.create(obj_name)
240
+
241
+ with cb_helpers.cleanup_action(lambda: obj.delete()):
242
+ content = b"Hello World. Serve me with response headers."
243
+ obj.upload(content)
244
+
245
+ disposition = 'attachment; filename="hello.txt"'
246
+ content_type = "application/octet-stream"
247
+ url = obj.generate_url(100,
248
+ content_disposition=disposition,
249
+ content_type=content_type)
250
+ if isinstance(self.provider, TestMockHelperMixin):
251
+ # Presigned URLs are constructed client-side, so the
252
+ # response-header overrides can be asserted on the query
253
+ # string without a network round trip.
254
+ self.assertIn('response-content-disposition', url)
255
+ self.assertIn('response-content-type', url)
256
+ raise self.skipTest(
257
+ "Skipping rest of test - mock providers can't"
258
+ " access generated url")
259
+ response = requests.get(url)
260
+ self.assertEqual(response.content, content)
261
+ got_disposition = response.headers.get(
262
+ 'Content-Disposition', '')
263
+ self.assertTrue(got_disposition.startswith('attachment'),
264
+ "Expected attachment disposition, got: "
265
+ f"{got_disposition}")
266
+ self.assertIn('hello.txt', got_disposition)
267
+ if self.provider.PROVIDER_ID != 'openstack':
268
+ # Swift's tempurl middleware cannot override Content-Type
269
+ # (it only honors the filename portion of the disposition
270
+ # via its `filename` query parameter).
271
+ self.assertEqual(response.headers.get('Content-Type'),
272
+ content_type)
273
+
274
+ @helpers.skipIfNoService(['storage.buckets'])
275
+ def test_generate_url_writable_ignores_response_headers(self):
276
+ name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
277
+ test_bucket = self.provider.storage.buckets.create(name)
278
+
279
+ with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
280
+ obj_name = "hello_response_headers.txt"
281
+ obj = test_bucket.objects.create(obj_name)
282
+
283
+ with cb_helpers.cleanup_action(lambda: obj.delete()):
284
+ url = obj.generate_url(
285
+ 100, writable=True,
286
+ content_disposition='attachment; filename="hello.txt"',
287
+ content_type="application/octet-stream")
288
+ # Response-header overrides only make sense for reads; a
289
+ # writable URL must not carry them (across all providers:
290
+ # AWS/GCP query params, Azure SAS rscd/rsct, Swift filename).
291
+ self.assertNotIn('response-content-disposition', url)
292
+ self.assertNotIn('rscd=', url)
293
+ self.assertNotIn('filename=', url)
294
+
232
295
  @helpers.skipIfNoService(['storage.buckets'])
233
296
  def test_upload_download_bucket_content_from_file(self):
234
297
  name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
File without changes
File without changes
File without changes