Skip to content

Commit febece7

Browse files
authored
feat: Custom Placement Config Dual Region Support (#819)
* refactor: dual-region API update * test coverage
1 parent fda745e commit febece7

File tree

7 files changed

+77
-14
lines changed

7 files changed

+77
-14
lines changed

google/cloud/storage/bucket.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,13 +2418,27 @@ def location(self, value):
24182418
warnings.warn(_LOCATION_SETTER_MESSAGE, DeprecationWarning, stacklevel=2)
24192419
self._location = value
24202420

2421+
@property
2422+
def data_locations(self):
2423+
"""Retrieve the list of regional locations for custom dual-region buckets.
2424+
2425+
See https://cloud.google.com/storage/docs/json_api/v1/buckets and
2426+
https://cloud.google.com/storage/docs/locations
2427+
2428+
Returns ``None`` if the property has not been set before creation,
2429+
if the bucket's resource has not been loaded from the server,
2430+
or if the bucket is not a dual-regions bucket.
2431+
:rtype: list of str or ``NoneType``
2432+
"""
2433+
custom_placement_config = self._properties.get("customPlacementConfig", {})
2434+
return custom_placement_config.get("dataLocations")
2435+
24212436
@property
24222437
def location_type(self):
2423-
"""Retrieve or set the location type for the bucket.
2438+
"""Retrieve the location type for the bucket.
24242439
24252440
See https://cloud.google.com/storage/docs/storage-classes
24262441
2427-
:setter: Set the location type for this bucket.
24282442
:getter: Gets the the location type for this bucket.
24292443
24302444
:rtype: str or ``NoneType``

google/cloud/storage/client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ def _post_resource(
602602
google.cloud.exceptions.NotFound
603603
If the bucket is not found.
604604
"""
605+
605606
return self._connection.api_request(
606607
method="POST",
607608
path=path,
@@ -847,6 +848,7 @@ def create_bucket(
847848
project=None,
848849
user_project=None,
849850
location=None,
851+
data_locations=None,
850852
predefined_acl=None,
851853
predefined_default_object_acl=None,
852854
timeout=_DEFAULT_TIMEOUT,
@@ -876,7 +878,11 @@ def create_bucket(
876878
location (str):
877879
(Optional) The location of the bucket. If not passed,
878880
the default location, US, will be used. If specifying a dual-region,
879-
can be specified as a string, e.g., 'US-CENTRAL1+US-WEST1'. See:
881+
`data_locations` should be set in conjunction.. See:
882+
https://cloud.google.com/storage/docs/locations
883+
data_locations (list of str):
884+
(Optional) The list of regional locations of a custom dual-region bucket.
885+
Dual-regions require exactly 2 regional locations. See:
880886
https://cloud.google.com/storage/docs/locations
881887
predefined_acl (str):
882888
(Optional) Name of predefined ACL to apply to bucket. See:
@@ -979,6 +985,9 @@ def create_bucket(
979985
if location is not None:
980986
properties["location"] = location
981987

988+
if data_locations is not None:
989+
properties["customPlacementConfig"] = {"dataLocations": data_locations}
990+
982991
api_response = self._post_resource(
983992
"/b",
984993
properties,

samples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ View the [source code](https://github.com/googleapis/python-storage/blob/main/sa
324324
View the [source code](https://github.com/googleapis/python-storage/blob/main/samples/snippets/storage_create_bucket_dual_region.py). To run this sample:
325325
326326
327-
`python storage_create_bucket_dual_region.py <BUCKET_NAME> <REGION_1> <REGION_2>`
327+
`python storage_create_bucket_dual_region.py <BUCKET_NAME> <LOCATION> <REGION_1> <REGION_2>`
328328
329329
-----
330330
### Create Bucket Notifications

samples/snippets/snippets_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,11 @@ def test_create_bucket_class_location(test_bucket_create):
435435

436436

437437
def test_create_bucket_dual_region(test_bucket_create, capsys):
438+
location = "US"
438439
region_1 = "US-EAST1"
439440
region_2 = "US-WEST1"
440441
storage_create_bucket_dual_region.create_bucket_dual_region(
441-
test_bucket_create.name, region_1, region_2
442+
test_bucket_create.name, location, region_1, region_2
442443
)
443444
out, _ = capsys.readouterr()
444445
assert f"Bucket {test_bucket_create.name} created in {region_1}+{region_2}" in out

samples/snippets/storage_create_bucket_dual_region.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
from google.cloud import storage
2525

2626

27-
def create_bucket_dual_region(bucket_name, region_1, region_2):
28-
"""Creates a Dual-Region Bucket with provided locations."""
27+
def create_bucket_dual_region(bucket_name, location, region_1, region_2):
28+
"""Creates a Dual-Region Bucket with provided location and regions.."""
2929
# The ID of your GCS bucket
3030
# bucket_name = "your-bucket-name"
3131

@@ -34,9 +34,10 @@ def create_bucket_dual_region(bucket_name, region_1, region_2):
3434
# https://cloud.google.com/storage/docs/locations
3535
# region_1 = "US-EAST1"
3636
# region_2 = "US-WEST1"
37+
# location = "US"
3738

3839
storage_client = storage.Client()
39-
storage_client.create_bucket(bucket_name, location=f"{region_1}+{region_2}")
40+
storage_client.create_bucket(bucket_name, location=location, data_locations=[region_1, region_2])
4041

4142
print(f"Bucket {bucket_name} created in {region_1}+{region_2}.")
4243

@@ -46,5 +47,5 @@ def create_bucket_dual_region(bucket_name, region_1, region_2):
4647

4748
if __name__ == "__main__":
4849
create_bucket_dual_region(
49-
bucket_name=sys.argv[1], region_1=sys.argv[2], region_2=sys.argv[3]
50+
bucket_name=sys.argv[1], location=sys.argv[2], region_1=sys.argv[3], region_2=sys.argv[4]
5051
)

tests/system/test_client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,21 @@ def test_create_bucket_dual_region(storage_client, buckets_to_delete):
6868
from google.cloud.storage.constants import DUAL_REGION_LOCATION_TYPE
6969

7070
new_bucket_name = _helpers.unique_name("dual-region-bucket")
71-
region_1 = "US-EAST1"
72-
region_2 = "US-WEST1"
73-
dual_region = f"{region_1}+{region_2}"
71+
location = "US"
72+
data_locations = ["US-EAST1", "US-WEST1"]
7473

7574
with pytest.raises(exceptions.NotFound):
7675
storage_client.get_bucket(new_bucket_name)
7776

7877
created = _helpers.retry_429_503(storage_client.create_bucket)(
79-
new_bucket_name, location=dual_region
78+
new_bucket_name, location=location, data_locations=data_locations
8079
)
8180
buckets_to_delete.append(created)
8281

8382
assert created.name == new_bucket_name
84-
assert created.location == dual_region
83+
assert created.location == location
8584
assert created.location_type == DUAL_REGION_LOCATION_TYPE
85+
assert created.data_locations == data_locations
8686

8787

8888
def test_list_buckets(storage_client, buckets_to_delete):

tests/unit/test_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,44 @@ def test_create_bucket_w_explicit_location(self):
14511451
_target_object=bucket,
14521452
)
14531453

1454+
def test_create_bucket_w_custom_dual_region(self):
1455+
project = "PROJECT"
1456+
bucket_name = "bucket-name"
1457+
location = "US"
1458+
data_locations = ["US-EAST1", "US-WEST1"]
1459+
api_response = {
1460+
"location": location,
1461+
"customPlacementConfig": {"dataLocations": data_locations},
1462+
"name": bucket_name,
1463+
}
1464+
credentials = _make_credentials()
1465+
client = self._make_one(project=project, credentials=credentials)
1466+
client._post_resource = mock.Mock()
1467+
client._post_resource.return_value = api_response
1468+
1469+
bucket = client.create_bucket(
1470+
bucket_name, location=location, data_locations=data_locations
1471+
)
1472+
1473+
self.assertEqual(bucket.location, location)
1474+
self.assertEqual(bucket.data_locations, data_locations)
1475+
1476+
expected_path = "/b"
1477+
expected_data = {
1478+
"location": location,
1479+
"customPlacementConfig": {"dataLocations": data_locations},
1480+
"name": bucket_name,
1481+
}
1482+
expected_query_params = {"project": project}
1483+
client._post_resource.assert_called_once_with(
1484+
expected_path,
1485+
expected_data,
1486+
query_params=expected_query_params,
1487+
timeout=self._get_default_timeout(),
1488+
retry=DEFAULT_RETRY,
1489+
_target_object=bucket,
1490+
)
1491+
14541492
def test_create_bucket_w_explicit_project(self):
14551493
project = "PROJECT"
14561494
other_project = "other-project-123"

0 commit comments

Comments
 (0)