Repository: googleapis/python-spanner (language: python)

Issue:
Add samples for Cloud Spanner Scheduled Backups
Cloud Spanner Scheduled Backups supports the following APIs:
1. CreateBackupSchedule
2. ListBackupSchedules
3. GetBackupSchedule
4. UpdateBackupSchedule
5. DeleteBackupSchedule

We should add code samples for these newly introduced APIs.


Discussion / hints:

Fix patch:
diff --git a/samples/samples/backup_schedule_samples.py b/samples/samples/backup_schedule_samples.py
new file mode 100644
index 0000000000..621febf0fc
--- /dev/null
+++ b/samples/samples/backup_schedule_samples.py
@@ -0,0 +1,303 @@
+# Copyright 2024 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This application demonstrates how to create and manage backup schedules using
+Cloud Spanner.
+"""
+
+import argparse
+
+from enum import Enum
+
+
+# [START spanner_create_full_backup_schedule]
+def create_full_backup_schedule(
+        instance_id: str,
+        database_id: str,
+        schedule_id: str,
+) -> None:
+    from datetime import timedelta
+    from google.cloud import spanner
+    from google.cloud.spanner_admin_database_v1.types import \
+        backup_schedule as backup_schedule_pb
+    from google.cloud.spanner_admin_database_v1.types import \
+        CreateBackupEncryptionConfig, FullBackupSpec
+
+    client = spanner.Client()
+    database_admin_api = client.database_admin_api
+
+    request = backup_schedule_pb.CreateBackupScheduleRequest(
+        parent=database_admin_api.database_path(
+            client.project,
+            instance_id,
+            database_id
+        ),
+        backup_schedule_id=schedule_id,
+        backup_schedule=backup_schedule_pb.BackupSchedule(
+            spec=backup_schedule_pb.BackupScheduleSpec(
+                cron_spec=backup_schedule_pb.CrontabSpec(
+                    text="30 12 * * *",
+                ),
+            ),
+            retention_duration=timedelta(hours=24),
+            encryption_config=CreateBackupEncryptionConfig(
+                encryption_type=CreateBackupEncryptionConfig.EncryptionType.USE_DATABASE_ENCRYPTION,
+            ),
+            full_backup_spec=FullBackupSpec(),
+        ),
+    )
+
+    response = database_admin_api.create_backup_schedule(request)
+    print(f"Created full backup schedule: {response}")
+
+# [END spanner_create_full_backup_schedule]
+
+
+# [START spanner_create_incremental_backup_schedule]
+def create_incremental_backup_schedule(
+        instance_id: str,
+        database_id: str,
+        schedule_id: str,
+) -> None:
+    from datetime import timedelta
+    from google.cloud import spanner
+    from google.cloud.spanner_admin_database_v1.types import \
+        backup_schedule as backup_schedule_pb
+    from google.cloud.spanner_admin_database_v1.types import \
+        CreateBackupEncryptionConfig, IncrementalBackupSpec
+
+    client = spanner.Client()
+    database_admin_api = client.database_admin_api
+
+    request = backup_schedule_pb.CreateBackupScheduleRequest(
+        parent=database_admin_api.database_path(
+            client.project,
+            instance_id,
+            database_id
+        ),
+        backup_schedule_id=schedule_id,
+        backup_schedule=backup_schedule_pb.BackupSchedule(
+            spec=backup_schedule_pb.BackupScheduleSpec(
+                cron_spec=backup_schedule_pb.CrontabSpec(
+                    text="30 12 * * *",
+                ),
+            ),
+            retention_duration=timedelta(hours=24),
+            encryption_config=CreateBackupEncryptionConfig(
+                encryption_type=CreateBackupEncryptionConfig.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION,
+            ),
+            incremental_backup_spec=IncrementalBackupSpec(),
+        ),
+    )
+
+    response = database_admin_api.create_backup_schedule(request)
+    print(f"Created incremental backup schedule: {response}")
+
+# [END spanner_create_incremental_backup_schedule]
+
+
+# [START spanner_list_backup_schedules]
+def list_backup_schedules(instance_id: str, database_id: str) -> None:
+    from google.cloud import spanner
+    from google.cloud.spanner_admin_database_v1.types import \
+        backup_schedule as backup_schedule_pb
+
+    client = spanner.Client()
+    database_admin_api = client.database_admin_api
+
+    request = backup_schedule_pb.ListBackupSchedulesRequest(
+        parent=database_admin_api.database_path(
+            client.project,
+            instance_id,
+            database_id,
+        ),
+    )
+
+    for backup_schedule in database_admin_api.list_backup_schedules(request):
+        print(f"Backup schedule: {backup_schedule}")
+
+# [END spanner_list_backup_schedules]
+
+
+# [START spanner_get_backup_schedule]
+def get_backup_schedule(
+        instance_id: str,
+        database_id: str,
+        schedule_id: str,
+) -> None:
+    from google.cloud import spanner
+    from google.cloud.spanner_admin_database_v1.types import \
+        backup_schedule as backup_schedule_pb
+
+    client = spanner.Client()
+    database_admin_api = client.database_admin_api
+
+    request = backup_schedule_pb.GetBackupScheduleRequest(
+        name=database_admin_api.backup_schedule_path(
+            client.project,
+            instance_id,
+            database_id,
+            schedule_id,
+        ),
+    )
+
+    response = database_admin_api.get_backup_schedule(request)
+    print(f"Backup schedule: {response}")
+
+# [END spanner_get_backup_schedule]
+
+
+# [START spanner_update_backup_schedule]
+def update_backup_schedule(
+        instance_id: str,
+        database_id: str,
+        schedule_id: str,
+) -> None:
+    from datetime import timedelta
+    from google.cloud import spanner
+    from google.cloud.spanner_admin_database_v1.types import \
+        backup_schedule as backup_schedule_pb
+    from google.cloud.spanner_admin_database_v1.types import \
+        CreateBackupEncryptionConfig
+    from google.protobuf.field_mask_pb2 import FieldMask
+
+    client = spanner.Client()
+    database_admin_api = client.database_admin_api
+
+    request = backup_schedule_pb.UpdateBackupScheduleRequest(
+        backup_schedule=backup_schedule_pb.BackupSchedule(
+            name=database_admin_api.backup_schedule_path(
+                client.project,
+                instance_id,
+                database_id,
+                schedule_id,
+            ),
+            spec=backup_schedule_pb.BackupScheduleSpec(
+                cron_spec=backup_schedule_pb.CrontabSpec(
+                    text="45 15 * * *",
+                ),
+            ),
+            retention_duration=timedelta(hours=48),
+            encryption_config=CreateBackupEncryptionConfig(
+                encryption_type=CreateBackupEncryptionConfig.EncryptionType.USE_DATABASE_ENCRYPTION,
+            ),
+        ),
+        update_mask=FieldMask(
+            paths=[
+                "spec.cron_spec.text",
+                "retention_duration",
+                "encryption_config",
+            ],
+        ),
+    )
+
+    response = database_admin_api.update_backup_schedule(request)
+    print(f"Updated backup schedule: {response}")
+
+# [END spanner_update_backup_schedule]
+
+
+# [START spanner_delete_backup_schedule]
+def delete_backup_schedule(
+        instance_id: str,
+        database_id: str,
+        schedule_id: str,
+) -> None:
+    from google.cloud import spanner
+    from google.cloud.spanner_admin_database_v1.types import \
+        backup_schedule as backup_schedule_pb
+
+    client = spanner.Client()
+    database_admin_api = client.database_admin_api
+
+    request = backup_schedule_pb.DeleteBackupScheduleRequest(
+        name=database_admin_api.backup_schedule_path(
+            client.project,
+            instance_id,
+            database_id,
+            schedule_id,
+        ),
+    )
+
+    database_admin_api.delete_backup_schedule(request)
+    print("Deleted backup schedule")
+
+# [END spanner_delete_backup_schedule]
+
+
+class Command(Enum):
+    CREATE_FULL_BACKUP_SCHEDULE = "create-full-backup-schedule"
+    CREATE_INCREMENTAL_BACKUP_SCHEDULE = "create-incremental-backup-schedule"
+    LIST_BACKUP_SCHEDULES = "list-backup-schedules"
+    GET_BACKUP_SCHEDULE = "get-backup-schedule"
+    UPDATE_BACKUP_SCHEDULE = "update-backup-schedule"
+    DELETE_BACKUP_SCHEDULE = "delete-backup-schedule"
+
+    def __str__(self):
+        return self.value
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument("--instance-id", required=True)
+    parser.add_argument("--database-id", required=True)
+    parser.add_argument("--schedule-id", required=False)
+    parser.add_argument(
+        "command",
+        type=Command,
+        choices=list(Command),
+    )
+    args = parser.parse_args()
+
+    if args.command == Command.CREATE_FULL_BACKUP_SCHEDULE:
+        create_full_backup_schedule(
+            args.instance_id,
+            args.database_id,
+            args.schedule_id,
+        )
+    elif args.command == Command.CREATE_INCREMENTAL_BACKUP_SCHEDULE:
+        create_incremental_backup_schedule(
+            args.instance_id,
+            args.database_id,
+            args.schedule_id,
+        )
+    elif args.command == Command.LIST_BACKUP_SCHEDULES:
+        list_backup_schedules(
+            args.instance_id,
+            args.database_id,
+        )
+    elif args.command == Command.GET_BACKUP_SCHEDULE:
+        get_backup_schedule(
+            args.instance_id,
+            args.database_id,
+            args.schedule_id,
+        )
+    elif args.command == Command.UPDATE_BACKUP_SCHEDULE:
+        update_backup_schedule(
+            args.instance_id,
+            args.database_id,
+            args.schedule_id,
+        )
+    elif args.command == Command.DELETE_BACKUP_SCHEDULE:
+        delete_backup_schedule(
+            args.instance_id,
+            args.database_id,
+            args.schedule_id,
+        )
+    else:
+        print(f"Unknown command: {args.command}")


Test patch:
diff --git a/samples/samples/backup_schedule_samples_test.py b/samples/samples/backup_schedule_samples_test.py
new file mode 100644
index 0000000000..eb4be96b43
--- /dev/null
+++ b/samples/samples/backup_schedule_samples_test.py
@@ -0,0 +1,159 @@
+# Copyright 2024 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import backup_schedule_samples as samples
+import pytest
+import uuid
+
+
+__FULL_BACKUP_SCHEDULE_ID = "full-backup-schedule"
+__INCREMENTAL_BACKUP_SCHEDULE_ID = "incremental-backup-schedule"
+
+
+@pytest.fixture(scope="module")
+def sample_name():
+    return "backup_schedule"
+
+
+@pytest.fixture(scope="module")
+def database_id():
+    return f"test-db-{uuid.uuid4().hex[:10]}"
+
+
+@pytest.mark.dependency(name="create_full_backup_schedule")
+def test_create_full_backup_schedule(
+        capsys,
+        sample_instance,
+        sample_database,
+) -> None:
+    samples.create_full_backup_schedule(
+        sample_instance.instance_id,
+        sample_database.database_id,
+        __FULL_BACKUP_SCHEDULE_ID,
+    )
+    out, _ = capsys.readouterr()
+    assert "Created full backup schedule" in out
+    assert (
+        f"/instances/{sample_instance.instance_id}"
+        f"/databases/{sample_database.database_id}"
+        f"/backupSchedules/{__FULL_BACKUP_SCHEDULE_ID}"
+    ) in out
+
+
+@pytest.mark.dependency(name="create_incremental_backup_schedule")
+def test_create_incremental_backup_schedule(
+        capsys,
+        sample_instance,
+        sample_database,
+) -> None:
+    samples.create_incremental_backup_schedule(
+        sample_instance.instance_id,
+        sample_database.database_id,
+        __INCREMENTAL_BACKUP_SCHEDULE_ID,
+    )
+    out, _ = capsys.readouterr()
+    assert "Created incremental backup schedule" in out
+    assert (
+        f"/instances/{sample_instance.instance_id}"
+        f"/databases/{sample_database.database_id}"
+        f"/backupSchedules/{__INCREMENTAL_BACKUP_SCHEDULE_ID}"
+    ) in out
+
+
+@pytest.mark.dependency(depends=[
+    "create_full_backup_schedule",
+    "create_incremental_backup_schedule",
+])
+def test_list_backup_schedules(
+        capsys,
+        sample_instance,
+        sample_database,
+) -> None:
+    samples.list_backup_schedules(
+        sample_instance.instance_id,
+        sample_database.database_id,
+    )
+    out, _ = capsys.readouterr()
+    assert (
+        f"/instances/{sample_instance.instance_id}"
+        f"/databases/{sample_database.database_id}"
+        f"/backupSchedules/{__FULL_BACKUP_SCHEDULE_ID}"
+    ) in out
+    assert (
+        f"/instances/{sample_instance.instance_id}"
+        f"/databases/{sample_database.database_id}"
+        f"/backupSchedules/{__INCREMENTAL_BACKUP_SCHEDULE_ID}"
+    ) in out
+
+
+@pytest.mark.dependency(depends=["create_full_backup_schedule"])
+def test_get_backup_schedule(
+        capsys,
+        sample_instance,
+        sample_database,
+) -> None:
+    samples.get_backup_schedule(
+        sample_instance.instance_id,
+        sample_database.database_id,
+        __FULL_BACKUP_SCHEDULE_ID,
+    )
+    out, _ = capsys.readouterr()
+    assert (
+        f"/instances/{sample_instance.instance_id}"
+        f"/databases/{sample_database.database_id}"
+        f"/backupSchedules/{__FULL_BACKUP_SCHEDULE_ID}"
+    ) in out
+
+
+@pytest.mark.dependency(depends=["create_full_backup_schedule"])
+def test_update_backup_schedule(
+        capsys,
+        sample_instance,
+        sample_database,
+) -> None:
+    samples.update_backup_schedule(
+        sample_instance.instance_id,
+        sample_database.database_id,
+        __FULL_BACKUP_SCHEDULE_ID,
+    )
+    out, _ = capsys.readouterr()
+    assert "Updated backup schedule" in out
+    assert (
+        f"/instances/{sample_instance.instance_id}"
+        f"/databases/{sample_database.database_id}"
+        f"/backupSchedules/{__FULL_BACKUP_SCHEDULE_ID}"
+    ) in out
+
+
+@pytest.mark.dependency(depends=[
+    "create_full_backup_schedule",
+    "create_incremental_backup_schedule",
+])
+def test_delete_backup_schedule(
+        capsys,
+        sample_instance,
+        sample_database,
+) -> None:
+    samples.delete_backup_schedule(
+        sample_instance.instance_id,
+        sample_database.database_id,
+        __FULL_BACKUP_SCHEDULE_ID,
+    )
+    samples.delete_backup_schedule(
+        sample_instance.instance_id,
+        sample_database.database_id,
+        __INCREMENTAL_BACKUP_SCHEDULE_ID,
+    )
+    out, _ = capsys.readouterr()
+    assert "Deleted backup schedule" in out