databricks-sdk 0.59.0__py3-none-any.whl → 0.61.0__py3-none-any.whl

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.
@@ -3,11 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import random
7
+ import time
6
8
  from dataclasses import dataclass
9
+ from datetime import timedelta
7
10
  from enum import Enum
8
- from typing import Any, Dict, Iterator, List, Optional
11
+ from typing import Any, Callable, Dict, Iterator, List, Optional
9
12
 
10
- from ._internal import _enum, _from_dict, _repeated_dict
13
+ from ._internal import Wait, _enum, _from_dict, _repeated_dict
11
14
 
12
15
  _LOG = logging.getLogger("databricks.sdk")
13
16
 
@@ -132,15 +135,24 @@ class CleanRoomAccessRestricted(Enum):
132
135
  class CleanRoomAsset:
133
136
  """Metadata of the clean room asset"""
134
137
 
135
- added_at: Optional[int] = None
136
- """When the asset is added to the clean room, in epoch milliseconds."""
138
+ name: str
139
+ """A fully qualified name that uniquely identifies the asset within the clean room. This is also
140
+ the name displayed in the clean room UI.
141
+
142
+ For UC securable assets (tables, volumes, etc.), the format is
143
+ *shared_catalog*.*shared_schema*.*asset_name*
144
+
145
+ For notebooks, the name is the notebook file name."""
137
146
 
138
- asset_type: Optional[CleanRoomAssetAssetType] = None
147
+ asset_type: CleanRoomAssetAssetType
139
148
  """The type of the asset."""
140
149
 
150
+ added_at: Optional[int] = None
151
+ """When the asset is added to the clean room, in epoch milliseconds."""
152
+
141
153
  clean_room_name: Optional[str] = None
142
- """The name of the clean room this asset belongs to. This is an output-only field to ensure proper
143
- resource identification."""
154
+ """The name of the clean room this asset belongs to. This field is required for create operations
155
+ and populated by the server for responses."""
144
156
 
145
157
  foreign_table: Optional[CleanRoomAssetForeignTable] = None
146
158
  """Foreign table details available to all collaborators of the clean room. Present if and only if
@@ -150,15 +162,6 @@ class CleanRoomAsset:
150
162
  """Local details for a foreign that are only available to its owner. Present if and only if
151
163
  **asset_type** is **FOREIGN_TABLE**"""
152
164
 
153
- name: Optional[str] = None
154
- """A fully qualified name that uniquely identifies the asset within the clean room. This is also
155
- the name displayed in the clean room UI.
156
-
157
- For UC securable assets (tables, volumes, etc.), the format is
158
- *shared_catalog*.*shared_schema*.*asset_name*
159
-
160
- For notebooks, the name is the notebook file name."""
161
-
162
165
  notebook: Optional[CleanRoomAssetNotebook] = None
163
166
  """Notebook details available to all collaborators of the clean room. Present if and only if
164
167
  **asset_type** is **NOTEBOOK_FILE**"""
@@ -314,7 +317,7 @@ class CleanRoomAssetForeignTable:
314
317
 
315
318
  @dataclass
316
319
  class CleanRoomAssetForeignTableLocalDetails:
317
- local_name: Optional[str] = None
320
+ local_name: str
318
321
  """The fully qualified name of the foreign table in its owner's local metastore, in the format of
319
322
  *catalog*.*schema*.*foreign_table_name*"""
320
323
 
@@ -340,13 +343,13 @@ class CleanRoomAssetForeignTableLocalDetails:
340
343
 
341
344
  @dataclass
342
345
  class CleanRoomAssetNotebook:
343
- etag: Optional[str] = None
344
- """Server generated etag that represents the notebook version."""
345
-
346
- notebook_content: Optional[str] = None
346
+ notebook_content: str
347
347
  """Base 64 representation of the notebook contents. This is the same format as returned by
348
348
  :method:workspace/export with the format of **HTML**."""
349
349
 
350
+ etag: Optional[str] = None
351
+ """Server generated etag that represents the notebook version."""
352
+
350
353
  review_state: Optional[CleanRoomNotebookReviewNotebookReviewState] = None
351
354
  """top-level status derived from all reviews"""
352
355
 
@@ -432,7 +435,7 @@ class CleanRoomAssetTable:
432
435
 
433
436
  @dataclass
434
437
  class CleanRoomAssetTableLocalDetails:
435
- local_name: Optional[str] = None
438
+ local_name: str
436
439
  """The fully qualified name of the table in its owner's local metastore, in the format of
437
440
  *catalog*.*schema*.*table_name*"""
438
441
 
@@ -490,7 +493,7 @@ class CleanRoomAssetView:
490
493
 
491
494
  @dataclass
492
495
  class CleanRoomAssetViewLocalDetails:
493
- local_name: Optional[str] = None
496
+ local_name: str
494
497
  """The fully qualified name of the view in its owner's local metastore, in the format of
495
498
  *catalog*.*schema*.*view_name*"""
496
499
 
@@ -516,7 +519,7 @@ class CleanRoomAssetViewLocalDetails:
516
519
 
517
520
  @dataclass
518
521
  class CleanRoomAssetVolumeLocalDetails:
519
- local_name: Optional[str] = None
522
+ local_name: str
520
523
  """The fully qualified name of the volume in its owner's local metastore, in the format of
521
524
  *catalog*.*schema*.*volume_name*"""
522
525
 
@@ -540,6 +543,83 @@ class CleanRoomAssetVolumeLocalDetails:
540
543
  return cls(local_name=d.get("local_name", None))
541
544
 
542
545
 
546
+ @dataclass
547
+ class CleanRoomAutoApprovalRule:
548
+ author_collaborator_alias: Optional[str] = None
549
+
550
+ author_scope: Optional[CleanRoomAutoApprovalRuleAuthorScope] = None
551
+
552
+ clean_room_name: Optional[str] = None
553
+ """The name of the clean room this auto-approval rule belongs to."""
554
+
555
+ created_at: Optional[int] = None
556
+ """Timestamp of when the rule was created, in epoch milliseconds."""
557
+
558
+ rule_id: Optional[str] = None
559
+ """A generated UUID identifying the rule."""
560
+
561
+ rule_owner_collaborator_alias: Optional[str] = None
562
+ """The owner of the rule to whom the rule applies."""
563
+
564
+ runner_collaborator_alias: Optional[str] = None
565
+
566
+ def as_dict(self) -> dict:
567
+ """Serializes the CleanRoomAutoApprovalRule into a dictionary suitable for use as a JSON request body."""
568
+ body = {}
569
+ if self.author_collaborator_alias is not None:
570
+ body["author_collaborator_alias"] = self.author_collaborator_alias
571
+ if self.author_scope is not None:
572
+ body["author_scope"] = self.author_scope.value
573
+ if self.clean_room_name is not None:
574
+ body["clean_room_name"] = self.clean_room_name
575
+ if self.created_at is not None:
576
+ body["created_at"] = self.created_at
577
+ if self.rule_id is not None:
578
+ body["rule_id"] = self.rule_id
579
+ if self.rule_owner_collaborator_alias is not None:
580
+ body["rule_owner_collaborator_alias"] = self.rule_owner_collaborator_alias
581
+ if self.runner_collaborator_alias is not None:
582
+ body["runner_collaborator_alias"] = self.runner_collaborator_alias
583
+ return body
584
+
585
+ def as_shallow_dict(self) -> dict:
586
+ """Serializes the CleanRoomAutoApprovalRule into a shallow dictionary of its immediate attributes."""
587
+ body = {}
588
+ if self.author_collaborator_alias is not None:
589
+ body["author_collaborator_alias"] = self.author_collaborator_alias
590
+ if self.author_scope is not None:
591
+ body["author_scope"] = self.author_scope
592
+ if self.clean_room_name is not None:
593
+ body["clean_room_name"] = self.clean_room_name
594
+ if self.created_at is not None:
595
+ body["created_at"] = self.created_at
596
+ if self.rule_id is not None:
597
+ body["rule_id"] = self.rule_id
598
+ if self.rule_owner_collaborator_alias is not None:
599
+ body["rule_owner_collaborator_alias"] = self.rule_owner_collaborator_alias
600
+ if self.runner_collaborator_alias is not None:
601
+ body["runner_collaborator_alias"] = self.runner_collaborator_alias
602
+ return body
603
+
604
+ @classmethod
605
+ def from_dict(cls, d: Dict[str, Any]) -> CleanRoomAutoApprovalRule:
606
+ """Deserializes the CleanRoomAutoApprovalRule from a dictionary."""
607
+ return cls(
608
+ author_collaborator_alias=d.get("author_collaborator_alias", None),
609
+ author_scope=_enum(d, "author_scope", CleanRoomAutoApprovalRuleAuthorScope),
610
+ clean_room_name=d.get("clean_room_name", None),
611
+ created_at=d.get("created_at", None),
612
+ rule_id=d.get("rule_id", None),
613
+ rule_owner_collaborator_alias=d.get("rule_owner_collaborator_alias", None),
614
+ runner_collaborator_alias=d.get("runner_collaborator_alias", None),
615
+ )
616
+
617
+
618
+ class CleanRoomAutoApprovalRuleAuthorScope(Enum):
619
+
620
+ ANY_AUTHOR = "ANY_AUTHOR"
621
+
622
+
543
623
  @dataclass
544
624
  class CleanRoomCollaborator:
545
625
  """Publicly visible clean room collaborator."""
@@ -1017,6 +1097,41 @@ class ComplianceSecurityProfile:
1017
1097
  )
1018
1098
 
1019
1099
 
1100
+ @dataclass
1101
+ class CreateCleanRoomAssetReviewResponse:
1102
+ notebook_review_state: Optional[CleanRoomNotebookReviewNotebookReviewState] = None
1103
+ """top-level status derived from all reviews"""
1104
+
1105
+ notebook_reviews: Optional[List[CleanRoomNotebookReview]] = None
1106
+ """All existing notebook approvals or rejections"""
1107
+
1108
+ def as_dict(self) -> dict:
1109
+ """Serializes the CreateCleanRoomAssetReviewResponse into a dictionary suitable for use as a JSON request body."""
1110
+ body = {}
1111
+ if self.notebook_review_state is not None:
1112
+ body["notebook_review_state"] = self.notebook_review_state.value
1113
+ if self.notebook_reviews:
1114
+ body["notebook_reviews"] = [v.as_dict() for v in self.notebook_reviews]
1115
+ return body
1116
+
1117
+ def as_shallow_dict(self) -> dict:
1118
+ """Serializes the CreateCleanRoomAssetReviewResponse into a shallow dictionary of its immediate attributes."""
1119
+ body = {}
1120
+ if self.notebook_review_state is not None:
1121
+ body["notebook_review_state"] = self.notebook_review_state
1122
+ if self.notebook_reviews:
1123
+ body["notebook_reviews"] = self.notebook_reviews
1124
+ return body
1125
+
1126
+ @classmethod
1127
+ def from_dict(cls, d: Dict[str, Any]) -> CreateCleanRoomAssetReviewResponse:
1128
+ """Deserializes the CreateCleanRoomAssetReviewResponse from a dictionary."""
1129
+ return cls(
1130
+ notebook_review_state=_enum(d, "notebook_review_state", CleanRoomNotebookReviewNotebookReviewState),
1131
+ notebook_reviews=_repeated_dict(d, "notebook_reviews", CleanRoomNotebookReview),
1132
+ )
1133
+
1134
+
1020
1135
  @dataclass
1021
1136
  class CreateCleanRoomOutputCatalogResponse:
1022
1137
  output_catalog: Optional[CleanRoomOutputCatalog] = None
@@ -1062,6 +1177,38 @@ class DeleteCleanRoomAssetResponse:
1062
1177
  return cls()
1063
1178
 
1064
1179
 
1180
+ @dataclass
1181
+ class ListCleanRoomAssetRevisionsResponse:
1182
+ next_page_token: Optional[str] = None
1183
+
1184
+ revisions: Optional[List[CleanRoomAsset]] = None
1185
+
1186
+ def as_dict(self) -> dict:
1187
+ """Serializes the ListCleanRoomAssetRevisionsResponse into a dictionary suitable for use as a JSON request body."""
1188
+ body = {}
1189
+ if self.next_page_token is not None:
1190
+ body["next_page_token"] = self.next_page_token
1191
+ if self.revisions:
1192
+ body["revisions"] = [v.as_dict() for v in self.revisions]
1193
+ return body
1194
+
1195
+ def as_shallow_dict(self) -> dict:
1196
+ """Serializes the ListCleanRoomAssetRevisionsResponse into a shallow dictionary of its immediate attributes."""
1197
+ body = {}
1198
+ if self.next_page_token is not None:
1199
+ body["next_page_token"] = self.next_page_token
1200
+ if self.revisions:
1201
+ body["revisions"] = self.revisions
1202
+ return body
1203
+
1204
+ @classmethod
1205
+ def from_dict(cls, d: Dict[str, Any]) -> ListCleanRoomAssetRevisionsResponse:
1206
+ """Deserializes the ListCleanRoomAssetRevisionsResponse from a dictionary."""
1207
+ return cls(
1208
+ next_page_token=d.get("next_page_token", None), revisions=_repeated_dict(d, "revisions", CleanRoomAsset)
1209
+ )
1210
+
1211
+
1065
1212
  @dataclass
1066
1213
  class ListCleanRoomAssetsResponse:
1067
1214
  assets: Optional[List[CleanRoomAsset]] = None
@@ -1095,6 +1242,40 @@ class ListCleanRoomAssetsResponse:
1095
1242
  return cls(assets=_repeated_dict(d, "assets", CleanRoomAsset), next_page_token=d.get("next_page_token", None))
1096
1243
 
1097
1244
 
1245
+ @dataclass
1246
+ class ListCleanRoomAutoApprovalRulesResponse:
1247
+ next_page_token: Optional[str] = None
1248
+ """Opaque token to retrieve the next page of results. Absent if there are no more pages. page_token
1249
+ should be set to this value for the next request (for the next page of results)."""
1250
+
1251
+ rules: Optional[List[CleanRoomAutoApprovalRule]] = None
1252
+
1253
+ def as_dict(self) -> dict:
1254
+ """Serializes the ListCleanRoomAutoApprovalRulesResponse into a dictionary suitable for use as a JSON request body."""
1255
+ body = {}
1256
+ if self.next_page_token is not None:
1257
+ body["next_page_token"] = self.next_page_token
1258
+ if self.rules:
1259
+ body["rules"] = [v.as_dict() for v in self.rules]
1260
+ return body
1261
+
1262
+ def as_shallow_dict(self) -> dict:
1263
+ """Serializes the ListCleanRoomAutoApprovalRulesResponse into a shallow dictionary of its immediate attributes."""
1264
+ body = {}
1265
+ if self.next_page_token is not None:
1266
+ body["next_page_token"] = self.next_page_token
1267
+ if self.rules:
1268
+ body["rules"] = self.rules
1269
+ return body
1270
+
1271
+ @classmethod
1272
+ def from_dict(cls, d: Dict[str, Any]) -> ListCleanRoomAutoApprovalRulesResponse:
1273
+ """Deserializes the ListCleanRoomAutoApprovalRulesResponse from a dictionary."""
1274
+ return cls(
1275
+ next_page_token=d.get("next_page_token", None), rules=_repeated_dict(d, "rules", CleanRoomAutoApprovalRule)
1276
+ )
1277
+
1278
+
1098
1279
  @dataclass
1099
1280
  class ListCleanRoomNotebookTaskRunsResponse:
1100
1281
  next_page_token: Optional[str] = None
@@ -1164,6 +1345,130 @@ class ListCleanRoomsResponse:
1164
1345
  )
1165
1346
 
1166
1347
 
1348
+ @dataclass
1349
+ class NotebookVersionReview:
1350
+ etag: str
1351
+ """etag that identifies the notebook version"""
1352
+
1353
+ review_state: CleanRoomNotebookReviewNotebookReviewState
1354
+ """review outcome"""
1355
+
1356
+ comment: Optional[str] = None
1357
+ """review comment"""
1358
+
1359
+ def as_dict(self) -> dict:
1360
+ """Serializes the NotebookVersionReview into a dictionary suitable for use as a JSON request body."""
1361
+ body = {}
1362
+ if self.comment is not None:
1363
+ body["comment"] = self.comment
1364
+ if self.etag is not None:
1365
+ body["etag"] = self.etag
1366
+ if self.review_state is not None:
1367
+ body["review_state"] = self.review_state.value
1368
+ return body
1369
+
1370
+ def as_shallow_dict(self) -> dict:
1371
+ """Serializes the NotebookVersionReview into a shallow dictionary of its immediate attributes."""
1372
+ body = {}
1373
+ if self.comment is not None:
1374
+ body["comment"] = self.comment
1375
+ if self.etag is not None:
1376
+ body["etag"] = self.etag
1377
+ if self.review_state is not None:
1378
+ body["review_state"] = self.review_state
1379
+ return body
1380
+
1381
+ @classmethod
1382
+ def from_dict(cls, d: Dict[str, Any]) -> NotebookVersionReview:
1383
+ """Deserializes the NotebookVersionReview from a dictionary."""
1384
+ return cls(
1385
+ comment=d.get("comment", None),
1386
+ etag=d.get("etag", None),
1387
+ review_state=_enum(d, "review_state", CleanRoomNotebookReviewNotebookReviewState),
1388
+ )
1389
+
1390
+
1391
+ class CleanRoomAssetRevisionsAPI:
1392
+ """Clean Room Asset Revisions denote new versions of uploaded assets (e.g. notebooks) in the clean room."""
1393
+
1394
+ def __init__(self, api_client):
1395
+ self._api = api_client
1396
+
1397
+ def get(self, clean_room_name: str, asset_type: CleanRoomAssetAssetType, name: str, etag: str) -> CleanRoomAsset:
1398
+ """Get a specific revision of an asset
1399
+
1400
+ :param clean_room_name: str
1401
+ Name of the clean room.
1402
+ :param asset_type: :class:`CleanRoomAssetAssetType`
1403
+ Asset type. Only NOTEBOOK_FILE is supported.
1404
+ :param name: str
1405
+ Name of the asset.
1406
+ :param etag: str
1407
+ Revision etag to fetch. If not provided, the latest revision will be returned.
1408
+
1409
+ :returns: :class:`CleanRoomAsset`
1410
+ """
1411
+
1412
+ headers = {
1413
+ "Accept": "application/json",
1414
+ }
1415
+
1416
+ res = self._api.do(
1417
+ "GET",
1418
+ f"/api/2.0/clean-rooms/{clean_room_name}/assets/{asset_type.value}/{name}/revisions/{etag}",
1419
+ headers=headers,
1420
+ )
1421
+ return CleanRoomAsset.from_dict(res)
1422
+
1423
+ def list(
1424
+ self,
1425
+ clean_room_name: str,
1426
+ asset_type: CleanRoomAssetAssetType,
1427
+ name: str,
1428
+ *,
1429
+ page_size: Optional[int] = None,
1430
+ page_token: Optional[str] = None,
1431
+ ) -> Iterator[CleanRoomAsset]:
1432
+ """List revisions for an asset
1433
+
1434
+ :param clean_room_name: str
1435
+ Name of the clean room.
1436
+ :param asset_type: :class:`CleanRoomAssetAssetType`
1437
+ Asset type. Only NOTEBOOK_FILE is supported.
1438
+ :param name: str
1439
+ Name of the asset.
1440
+ :param page_size: int (optional)
1441
+ Maximum number of asset revisions to return. Defaults to 10.
1442
+ :param page_token: str (optional)
1443
+ Opaque pagination token to go to next page based on the previous query.
1444
+
1445
+ :returns: Iterator over :class:`CleanRoomAsset`
1446
+ """
1447
+
1448
+ query = {}
1449
+ if page_size is not None:
1450
+ query["page_size"] = page_size
1451
+ if page_token is not None:
1452
+ query["page_token"] = page_token
1453
+ headers = {
1454
+ "Accept": "application/json",
1455
+ }
1456
+
1457
+ while True:
1458
+ json = self._api.do(
1459
+ "GET",
1460
+ f"/api/2.0/clean-rooms/{clean_room_name}/assets/{asset_type.value}/{name}/revisions",
1461
+ query=query,
1462
+ headers=headers,
1463
+ )
1464
+ if "revisions" in json:
1465
+ for v in json["revisions"]:
1466
+ yield CleanRoomAsset.from_dict(v)
1467
+ if "next_page_token" not in json or not json["next_page_token"]:
1468
+ return
1469
+ query["page_token"] = json["next_page_token"]
1470
+
1471
+
1167
1472
  class CleanRoomAssetsAPI:
1168
1473
  """Clean room assets are data and code objects — Tables, volumes, and notebooks that are shared with the
1169
1474
  clean room."""
@@ -1178,8 +1483,8 @@ class CleanRoomAssetsAPI:
1178
1483
  access the asset. Typically, you should use a group as the clean room owner.
1179
1484
 
1180
1485
  :param clean_room_name: str
1181
- The name of the clean room this asset belongs to. This is an output-only field to ensure proper
1182
- resource identification.
1486
+ The name of the clean room this asset belongs to. This field is required for create operations and
1487
+ populated by the server for responses.
1183
1488
  :param asset: :class:`CleanRoomAsset`
1184
1489
 
1185
1490
  :returns: :class:`CleanRoomAsset`
@@ -1193,6 +1498,41 @@ class CleanRoomAssetsAPI:
1193
1498
  res = self._api.do("POST", f"/api/2.0/clean-rooms/{clean_room_name}/assets", body=body, headers=headers)
1194
1499
  return CleanRoomAsset.from_dict(res)
1195
1500
 
1501
+ def create_clean_room_asset_review(
1502
+ self,
1503
+ clean_room_name: str,
1504
+ asset_type: CleanRoomAssetAssetType,
1505
+ name: str,
1506
+ notebook_review: NotebookVersionReview,
1507
+ ) -> CreateCleanRoomAssetReviewResponse:
1508
+ """submit an asset review
1509
+
1510
+ :param clean_room_name: str
1511
+ Name of the clean room
1512
+ :param asset_type: :class:`CleanRoomAssetAssetType`
1513
+ can only be NOTEBOOK_FILE for now
1514
+ :param name: str
1515
+ Name of the asset
1516
+ :param notebook_review: :class:`NotebookVersionReview`
1517
+
1518
+ :returns: :class:`CreateCleanRoomAssetReviewResponse`
1519
+ """
1520
+ body = {}
1521
+ if notebook_review is not None:
1522
+ body["notebook_review"] = notebook_review.as_dict()
1523
+ headers = {
1524
+ "Accept": "application/json",
1525
+ "Content-Type": "application/json",
1526
+ }
1527
+
1528
+ res = self._api.do(
1529
+ "POST",
1530
+ f"/api/2.0/clean-rooms/{clean_room_name}/assets/{asset_type.value}/{name}/reviews",
1531
+ body=body,
1532
+ headers=headers,
1533
+ )
1534
+ return CreateCleanRoomAssetReviewResponse.from_dict(res)
1535
+
1196
1536
  def delete(self, clean_room_name: str, asset_type: CleanRoomAssetAssetType, name: str):
1197
1537
  """Delete a clean room asset - unshare/remove the asset from the clean room
1198
1538
 
@@ -1302,6 +1642,128 @@ class CleanRoomAssetsAPI:
1302
1642
  return CleanRoomAsset.from_dict(res)
1303
1643
 
1304
1644
 
1645
+ class CleanRoomAutoApprovalRulesAPI:
1646
+ """Clean room auto-approval rules automatically create an approval on your behalf when an asset (e.g.
1647
+ notebook) meeting specific criteria is shared in a clean room."""
1648
+
1649
+ def __init__(self, api_client):
1650
+ self._api = api_client
1651
+
1652
+ def create(self, clean_room_name: str, auto_approval_rule: CleanRoomAutoApprovalRule) -> CleanRoomAutoApprovalRule:
1653
+ """Create an auto-approval rule
1654
+
1655
+ :param clean_room_name: str
1656
+ The name of the clean room this auto-approval rule belongs to.
1657
+ :param auto_approval_rule: :class:`CleanRoomAutoApprovalRule`
1658
+
1659
+ :returns: :class:`CleanRoomAutoApprovalRule`
1660
+ """
1661
+ body = {}
1662
+ if auto_approval_rule is not None:
1663
+ body["auto_approval_rule"] = auto_approval_rule.as_dict()
1664
+ headers = {
1665
+ "Accept": "application/json",
1666
+ "Content-Type": "application/json",
1667
+ }
1668
+
1669
+ res = self._api.do(
1670
+ "POST", f"/api/2.0/clean-rooms/{clean_room_name}/auto-approval-rules", body=body, headers=headers
1671
+ )
1672
+ return CleanRoomAutoApprovalRule.from_dict(res)
1673
+
1674
+ def delete(self, clean_room_name: str, rule_id: str):
1675
+ """Delete a auto-approval rule by rule ID
1676
+
1677
+ :param clean_room_name: str
1678
+ :param rule_id: str
1679
+
1680
+
1681
+ """
1682
+
1683
+ headers = {
1684
+ "Accept": "application/json",
1685
+ }
1686
+
1687
+ self._api.do("DELETE", f"/api/2.0/clean-rooms/{clean_room_name}/auto-approval-rules/{rule_id}", headers=headers)
1688
+
1689
+ def get(self, clean_room_name: str, rule_id: str) -> CleanRoomAutoApprovalRule:
1690
+ """Get a auto-approval rule by rule ID
1691
+
1692
+ :param clean_room_name: str
1693
+ :param rule_id: str
1694
+
1695
+ :returns: :class:`CleanRoomAutoApprovalRule`
1696
+ """
1697
+
1698
+ headers = {
1699
+ "Accept": "application/json",
1700
+ }
1701
+
1702
+ res = self._api.do(
1703
+ "GET", f"/api/2.0/clean-rooms/{clean_room_name}/auto-approval-rules/{rule_id}", headers=headers
1704
+ )
1705
+ return CleanRoomAutoApprovalRule.from_dict(res)
1706
+
1707
+ def list(
1708
+ self, clean_room_name: str, *, page_size: Optional[int] = None, page_token: Optional[str] = None
1709
+ ) -> Iterator[CleanRoomAutoApprovalRule]:
1710
+ """List all auto-approval rules for the caller
1711
+
1712
+ :param clean_room_name: str
1713
+ :param page_size: int (optional)
1714
+ Maximum number of auto-approval rules to return. Defaults to 100.
1715
+ :param page_token: str (optional)
1716
+ Opaque pagination token to go to next page based on previous query.
1717
+
1718
+ :returns: Iterator over :class:`CleanRoomAutoApprovalRule`
1719
+ """
1720
+
1721
+ query = {}
1722
+ if page_size is not None:
1723
+ query["page_size"] = page_size
1724
+ if page_token is not None:
1725
+ query["page_token"] = page_token
1726
+ headers = {
1727
+ "Accept": "application/json",
1728
+ }
1729
+
1730
+ while True:
1731
+ json = self._api.do(
1732
+ "GET", f"/api/2.0/clean-rooms/{clean_room_name}/auto-approval-rules", query=query, headers=headers
1733
+ )
1734
+ if "rules" in json:
1735
+ for v in json["rules"]:
1736
+ yield CleanRoomAutoApprovalRule.from_dict(v)
1737
+ if "next_page_token" not in json or not json["next_page_token"]:
1738
+ return
1739
+ query["page_token"] = json["next_page_token"]
1740
+
1741
+ def update(
1742
+ self, clean_room_name: str, rule_id: str, auto_approval_rule: CleanRoomAutoApprovalRule
1743
+ ) -> CleanRoomAutoApprovalRule:
1744
+ """Update a auto-approval rule by rule ID
1745
+
1746
+ :param clean_room_name: str
1747
+ The name of the clean room this auto-approval rule belongs to.
1748
+ :param rule_id: str
1749
+ A generated UUID identifying the rule.
1750
+ :param auto_approval_rule: :class:`CleanRoomAutoApprovalRule`
1751
+ The auto-approval rule to update. The rule_id field is used to identify the rule to update.
1752
+
1753
+ :returns: :class:`CleanRoomAutoApprovalRule`
1754
+ """
1755
+ body = auto_approval_rule.as_dict()
1756
+ headers = {
1757
+ "Accept": "application/json",
1758
+ "Content-Type": "application/json",
1759
+ }
1760
+
1761
+ res = self._api.do(
1762
+ "PATCH", f"/api/2.0/clean-rooms/{clean_room_name}/auto-approval-rules/{rule_id}", body=body, headers=headers
1763
+ )
1764
+ return CleanRoomAutoApprovalRule.from_dict(res)
1765
+
1766
+
1305
1767
  class CleanRoomTaskRunsAPI:
1306
1768
  """Clean room task runs are the executions of notebooks in a clean room."""
1307
1769
 
@@ -1359,7 +1821,32 @@ class CleanRoomsAPI:
1359
1821
  def __init__(self, api_client):
1360
1822
  self._api = api_client
1361
1823
 
1362
- def create(self, clean_room: CleanRoom) -> CleanRoom:
1824
+ def wait_get_clean_room_active(
1825
+ self, name: str, timeout=timedelta(minutes=20), callback: Optional[Callable[[CleanRoom], None]] = None
1826
+ ) -> CleanRoom:
1827
+ deadline = time.time() + timeout.total_seconds()
1828
+ target_states = (CleanRoomStatusEnum.ACTIVE,)
1829
+ status_message = "polling..."
1830
+ attempt = 1
1831
+ while time.time() < deadline:
1832
+ poll = self.get(name=name)
1833
+ status = poll.status
1834
+ status_message = f"current status: {status}"
1835
+ if status in target_states:
1836
+ return poll
1837
+ if callback:
1838
+ callback(poll)
1839
+ prefix = f"name={name}"
1840
+ sleep = attempt
1841
+ if sleep > 10:
1842
+ # sleep 10s max per attempt
1843
+ sleep = 10
1844
+ _LOG.debug(f"{prefix}: ({status}) {status_message} (sleeping ~{sleep}s)")
1845
+ time.sleep(sleep + random.random())
1846
+ attempt += 1
1847
+ raise TimeoutError(f"timed out after {timeout}: {status_message}")
1848
+
1849
+ def create(self, clean_room: CleanRoom) -> Wait[CleanRoom]:
1363
1850
  """Create a new clean room with the specified collaborators. This method is asynchronous; the returned
1364
1851
  name field inside the clean_room field can be used to poll the clean room status, using the
1365
1852
  :method:cleanrooms/get method. When this method returns, the clean room will be in a PROVISIONING
@@ -1370,7 +1857,9 @@ class CleanRoomsAPI:
1370
1857
 
1371
1858
  :param clean_room: :class:`CleanRoom`
1372
1859
 
1373
- :returns: :class:`CleanRoom`
1860
+ :returns:
1861
+ Long-running operation waiter for :class:`CleanRoom`.
1862
+ See :method:wait_get_clean_room_active for more details.
1374
1863
  """
1375
1864
  body = clean_room.as_dict()
1376
1865
  headers = {
@@ -1378,8 +1867,13 @@ class CleanRoomsAPI:
1378
1867
  "Content-Type": "application/json",
1379
1868
  }
1380
1869
 
1381
- res = self._api.do("POST", "/api/2.0/clean-rooms", body=body, headers=headers)
1382
- return CleanRoom.from_dict(res)
1870
+ op_response = self._api.do("POST", "/api/2.0/clean-rooms", body=body, headers=headers)
1871
+ return Wait(
1872
+ self.wait_get_clean_room_active, response=CleanRoom.from_dict(op_response), name=op_response["name"]
1873
+ )
1874
+
1875
+ def create_and_wait(self, clean_room: CleanRoom, timeout=timedelta(minutes=20)) -> CleanRoom:
1876
+ return self.create(clean_room=clean_room).result(timeout=timeout)
1383
1877
 
1384
1878
  def create_output_catalog(
1385
1879
  self, clean_room_name: str, output_catalog: CleanRoomOutputCatalog