1# Copyright 2015 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Define API Datasets."""
16
17from __future__ import absolute_import
18
19import copy
20import json
21
22import typing
23from typing import Optional, List, Dict, Any, Union
24
25import google.cloud._helpers # type: ignore
26
27from google.cloud.bigquery import _helpers
28from google.cloud.bigquery.model import ModelReference
29from google.cloud.bigquery.routine import Routine, RoutineReference
30from google.cloud.bigquery.table import Table, TableReference
31from google.cloud.bigquery.encryption_configuration import EncryptionConfiguration
32from google.cloud.bigquery import external_config
33
34
35def _get_table_reference(self, table_id: str) -> TableReference:
36 """Constructs a TableReference.
37
38 Args:
39 table_id (str): The ID of the table.
40
41 Returns:
42 google.cloud.bigquery.table.TableReference:
43 A table reference for a table in this dataset.
44 """
45 return TableReference(self, table_id)
46
47
48def _get_model_reference(self, model_id):
49 """Constructs a ModelReference.
50
51 Args:
52 model_id (str): the ID of the model.
53
54 Returns:
55 google.cloud.bigquery.model.ModelReference:
56 A ModelReference for a model in this dataset.
57 """
58 return ModelReference.from_api_repr(
59 {"projectId": self.project, "datasetId": self.dataset_id, "modelId": model_id}
60 )
61
62
63def _get_routine_reference(self, routine_id):
64 """Constructs a RoutineReference.
65
66 Args:
67 routine_id (str): the ID of the routine.
68
69 Returns:
70 google.cloud.bigquery.routine.RoutineReference:
71 A RoutineReference for a routine in this dataset.
72 """
73 return RoutineReference.from_api_repr(
74 {
75 "projectId": self.project,
76 "datasetId": self.dataset_id,
77 "routineId": routine_id,
78 }
79 )
80
81
82class DatasetReference(object):
83 """DatasetReferences are pointers to datasets.
84
85 See
86 https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#datasetreference
87
88 Args:
89 project (str): The ID of the project
90 dataset_id (str): The ID of the dataset
91
92 Raises:
93 ValueError: If either argument is not of type ``str``.
94 """
95
96 def __init__(self, project: str, dataset_id: str):
97 if not isinstance(project, str):
98 raise ValueError("Pass a string for project")
99 if not isinstance(dataset_id, str):
100 raise ValueError("Pass a string for dataset_id")
101 self._project = project
102 self._dataset_id = dataset_id
103
104 @property
105 def project(self):
106 """str: Project ID of the dataset."""
107 return self._project
108
109 @property
110 def dataset_id(self):
111 """str: Dataset ID."""
112 return self._dataset_id
113
114 @property
115 def path(self):
116 """str: URL path for the dataset based on project and dataset ID."""
117 return "/projects/%s/datasets/%s" % (self.project, self.dataset_id)
118
119 table = _get_table_reference
120
121 model = _get_model_reference
122
123 routine = _get_routine_reference
124
125 @classmethod
126 def from_api_repr(cls, resource: dict) -> "DatasetReference":
127 """Factory: construct a dataset reference given its API representation
128
129 Args:
130 resource (Dict[str, str]):
131 Dataset reference resource representation returned from the API
132
133 Returns:
134 google.cloud.bigquery.dataset.DatasetReference:
135 Dataset reference parsed from ``resource``.
136 """
137 project = resource["projectId"]
138 dataset_id = resource["datasetId"]
139 return cls(project, dataset_id)
140
141 @classmethod
142 def from_string(
143 cls, dataset_id: str, default_project: Optional[str] = None
144 ) -> "DatasetReference":
145 """Construct a dataset reference from dataset ID string.
146
147 Args:
148 dataset_id (str):
149 A dataset ID in standard SQL format. If ``default_project``
150 is not specified, this must include both the project ID and
151 the dataset ID, separated by ``.``.
152 default_project (Optional[str]):
153 The project ID to use when ``dataset_id`` does not include a
154 project ID.
155
156 Returns:
157 DatasetReference:
158 Dataset reference parsed from ``dataset_id``.
159
160 Examples:
161 >>> DatasetReference.from_string('my-project-id.some_dataset')
162 DatasetReference('my-project-id', 'some_dataset')
163
164 Raises:
165 ValueError:
166 If ``dataset_id`` is not a fully-qualified dataset ID in
167 standard SQL format.
168 """
169 output_dataset_id = dataset_id
170 parts = _helpers._split_id(dataset_id)
171
172 if len(parts) == 1:
173 if default_project is not None:
174 output_project_id = default_project
175 else:
176 raise ValueError(
177 "When default_project is not set, dataset_id must be a "
178 "fully-qualified dataset ID in standard SQL format, "
179 'e.g., "project.dataset_id" got {}'.format(dataset_id)
180 )
181 elif len(parts) == 2:
182 output_project_id, output_dataset_id = parts
183 else:
184 raise ValueError(
185 "Too many parts in dataset_id. Expected a fully-qualified "
186 "dataset ID in standard SQL format, "
187 'e.g. "project.dataset_id", got {}'.format(dataset_id)
188 )
189
190 return cls(output_project_id, output_dataset_id)
191
192 def to_api_repr(self) -> dict:
193 """Construct the API resource representation of this dataset reference
194
195 Returns:
196 Dict[str, str]: dataset reference represented as an API resource
197 """
198 return {"projectId": self._project, "datasetId": self._dataset_id}
199
200 def _key(self):
201 """A tuple key that uniquely describes this field.
202
203 Used to compute this instance's hashcode and evaluate equality.
204
205 Returns:
206 Tuple[str]: The contents of this :class:`.DatasetReference`.
207 """
208 return (self._project, self._dataset_id)
209
210 def __eq__(self, other):
211 if not isinstance(other, DatasetReference):
212 return NotImplemented
213 return self._key() == other._key()
214
215 def __ne__(self, other):
216 return not self == other
217
218 def __hash__(self):
219 return hash(self._key())
220
221 def __str__(self):
222 return f"{self.project}.{self._dataset_id}"
223
224 def __repr__(self):
225 return "DatasetReference{}".format(self._key())
226
227
228class AccessEntry(object):
229 """Represents grant of an access role to an entity.
230
231 An entry must have exactly one of the allowed
232 :class:`google.cloud.bigquery.enums.EntityTypes`. If anything but ``view``, ``routine``,
233 or ``dataset`` are set, a ``role`` is also required. ``role`` is omitted for ``view``,
234 ``routine``, ``dataset``, because they are always read-only.
235
236 See https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets.
237
238 Args:
239 role:
240 Role granted to the entity. The following string values are
241 supported: `'READER'`, `'WRITER'`, `'OWNER'`. It may also be
242 :data:`None` if the ``entity_type`` is ``view``, ``routine``, or ``dataset``.
243
244 entity_type:
245 Type of entity being granted the role. See
246 :class:`google.cloud.bigquery.enums.EntityTypes` for supported types.
247
248 entity_id:
249 If the ``entity_type`` is not 'view', 'routine', or 'dataset', the
250 ``entity_id`` is the ``str`` ID of the entity being granted the role. If
251 the ``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict``
252 representing the view or routine from a different dataset to grant access
253 to in the following format for views::
254
255 {
256 'projectId': string,
257 'datasetId': string,
258 'tableId': string
259 }
260
261 For routines::
262
263 {
264 'projectId': string,
265 'datasetId': string,
266 'routineId': string
267 }
268
269 If the ``entity_type`` is 'dataset', the ``entity_id`` is a ``dict`` that includes
270 a 'dataset' field with a ``dict`` representing the dataset and a 'target_types'
271 field with a ``str`` value of the dataset's resource type::
272
273 {
274 'dataset': {
275 'projectId': string,
276 'datasetId': string,
277 },
278 'target_types: 'VIEWS'
279 }
280
281 Raises:
282 ValueError:
283 If a ``view``, ``routine``, or ``dataset`` has ``role`` set, or a non ``view``,
284 non ``routine``, and non ``dataset`` **does not** have a ``role`` set.
285
286 Examples:
287 >>> entry = AccessEntry('OWNER', 'userByEmail', 'user@example.com')
288
289 >>> view = {
290 ... 'projectId': 'my-project',
291 ... 'datasetId': 'my_dataset',
292 ... 'tableId': 'my_table'
293 ... }
294 >>> entry = AccessEntry(None, 'view', view)
295 """
296
297 def __init__(
298 self,
299 role: Optional[str] = None,
300 entity_type: Optional[str] = None,
301 entity_id: Optional[Union[Dict[str, Any], str]] = None,
302 **kwargs,
303 ):
304 self._properties: Dict[str, Any] = {}
305 if entity_type is not None:
306 self._properties[entity_type] = entity_id
307 self._properties["role"] = role
308 self._entity_type: Optional[str] = entity_type
309 for prop, val in kwargs.items():
310 setattr(self, prop, val)
311
312 @property
313 def role(self) -> Optional[str]:
314 """The role of the entry."""
315 return typing.cast(Optional[str], self._properties.get("role"))
316
317 @role.setter
318 def role(self, value):
319 self._properties["role"] = value
320
321 @property
322 def dataset(self) -> Optional[DatasetReference]:
323 """API resource representation of a dataset reference."""
324 value = _helpers._get_sub_prop(self._properties, ["dataset", "dataset"])
325 return DatasetReference.from_api_repr(value) if value else None
326
327 @dataset.setter
328 def dataset(self, value):
329 if self.role is not None:
330 raise ValueError(
331 "Role must be None for a dataset. Current " "role: %r" % (self.role)
332 )
333
334 if isinstance(value, str):
335 value = DatasetReference.from_string(value).to_api_repr()
336
337 if isinstance(value, DatasetReference):
338 value = value.to_api_repr()
339
340 if isinstance(value, (Dataset, DatasetListItem)):
341 value = value.reference.to_api_repr()
342
343 _helpers._set_sub_prop(self._properties, ["dataset", "dataset"], value)
344 _helpers._set_sub_prop(
345 self._properties,
346 ["dataset", "targetTypes"],
347 self._properties.get("targetTypes"),
348 )
349
350 @property
351 def dataset_target_types(self) -> Optional[List[str]]:
352 """Which resources that the dataset in this entry applies to."""
353 return typing.cast(
354 Optional[List[str]],
355 _helpers._get_sub_prop(self._properties, ["dataset", "targetTypes"]),
356 )
357
358 @dataset_target_types.setter
359 def dataset_target_types(self, value):
360 self._properties.setdefault("dataset", {})
361 _helpers._set_sub_prop(self._properties, ["dataset", "targetTypes"], value)
362
363 @property
364 def routine(self) -> Optional[RoutineReference]:
365 """API resource representation of a routine reference."""
366 value = typing.cast(Optional[Dict], self._properties.get("routine"))
367 return RoutineReference.from_api_repr(value) if value else None
368
369 @routine.setter
370 def routine(self, value):
371 if self.role is not None:
372 raise ValueError(
373 "Role must be None for a routine. Current " "role: %r" % (self.role)
374 )
375
376 if isinstance(value, str):
377 value = RoutineReference.from_string(value).to_api_repr()
378
379 if isinstance(value, RoutineReference):
380 value = value.to_api_repr()
381
382 if isinstance(value, Routine):
383 value = value.reference.to_api_repr()
384
385 self._properties["routine"] = value
386
387 @property
388 def view(self) -> Optional[TableReference]:
389 """API resource representation of a view reference."""
390 value = typing.cast(Optional[Dict], self._properties.get("view"))
391 return TableReference.from_api_repr(value) if value else None
392
393 @view.setter
394 def view(self, value):
395 if self.role is not None:
396 raise ValueError(
397 "Role must be None for a view. Current " "role: %r" % (self.role)
398 )
399
400 if isinstance(value, str):
401 value = TableReference.from_string(value).to_api_repr()
402
403 if isinstance(value, TableReference):
404 value = value.to_api_repr()
405
406 if isinstance(value, Table):
407 value = value.reference.to_api_repr()
408
409 self._properties["view"] = value
410
411 @property
412 def group_by_email(self) -> Optional[str]:
413 """An email address of a Google Group to grant access to."""
414 return typing.cast(Optional[str], self._properties.get("groupByEmail"))
415
416 @group_by_email.setter
417 def group_by_email(self, value):
418 self._properties["groupByEmail"] = value
419
420 @property
421 def user_by_email(self) -> Optional[str]:
422 """An email address of a user to grant access to."""
423 return typing.cast(Optional[str], self._properties.get("userByEmail"))
424
425 @user_by_email.setter
426 def user_by_email(self, value):
427 self._properties["userByEmail"] = value
428
429 @property
430 def domain(self) -> Optional[str]:
431 """A domain to grant access to."""
432 return typing.cast(Optional[str], self._properties.get("domain"))
433
434 @domain.setter
435 def domain(self, value):
436 self._properties["domain"] = value
437
438 @property
439 def special_group(self) -> Optional[str]:
440 """A special group to grant access to."""
441 return typing.cast(Optional[str], self._properties.get("specialGroup"))
442
443 @special_group.setter
444 def special_group(self, value):
445 self._properties["specialGroup"] = value
446
447 @property
448 def condition(self) -> Optional["Condition"]:
449 """Optional[Condition]: The IAM condition associated with this entry."""
450 value = typing.cast(Dict[str, Any], self._properties.get("condition"))
451 return Condition.from_api_repr(value) if value else None
452
453 @condition.setter
454 def condition(self, value: Union["Condition", dict, None]):
455 """Set the IAM condition for this entry."""
456 if value is None:
457 self._properties["condition"] = None
458 elif isinstance(value, Condition):
459 self._properties["condition"] = value.to_api_repr()
460 elif isinstance(value, dict):
461 self._properties["condition"] = value
462 else:
463 raise TypeError("condition must be a Condition object, dict, or None")
464
465 @property
466 def entity_type(self) -> Optional[str]:
467 """The entity_type of the entry."""
468
469 # The api_repr for an AccessEntry object is expected to be a dict with
470 # only a few keys. Two keys that may be present are role and condition.
471 # Any additional key is going to have one of ~eight different names:
472 # userByEmail, groupByEmail, domain, dataset, specialGroup, view,
473 # routine, iamMember
474
475 # if self._entity_type is None, see if it needs setting
476 # i.e. is there a key: value pair that should be associated with
477 # entity_type and entity_id?
478 if self._entity_type is None:
479 resource = self._properties.copy()
480 # we are empyting the dict to get to the last `key: value`` pair
481 # so we don't keep these first entries
482 _ = resource.pop("role", None)
483 _ = resource.pop("condition", None)
484
485 try:
486 # we only need entity_type, because entity_id gets set elsewhere.
487 entity_type, _ = resource.popitem()
488 except KeyError:
489 entity_type = None
490
491 self._entity_type = entity_type
492
493 return self._entity_type
494
495 @property
496 def entity_id(self) -> Optional[Union[Dict[str, Any], str]]:
497 """The entity_id of the entry."""
498 if self.entity_type:
499 entity_type = self.entity_type
500 else:
501 return None
502 return typing.cast(
503 Optional[Union[Dict[str, Any], str]],
504 self._properties.get(entity_type, None),
505 )
506
507 def __eq__(self, other):
508 if not isinstance(other, AccessEntry):
509 return NotImplemented
510 return (
511 self.role == other.role
512 and self.entity_type == other.entity_type
513 and self._normalize_entity_id(self.entity_id)
514 == self._normalize_entity_id(other.entity_id)
515 and self.condition == other.condition
516 )
517
518 @staticmethod
519 def _normalize_entity_id(value):
520 """Ensure consistent equality for dicts like 'view'."""
521 if isinstance(value, dict):
522 return json.dumps(value, sort_keys=True)
523 return value
524
525 def __ne__(self, other):
526 return not self == other
527
528 def __repr__(self):
529 return f"<AccessEntry: role={self.role}, {self.entity_type}={self.entity_id}>"
530
531 def _key(self):
532 """A tuple key that uniquely describes this field.
533 Used to compute this instance's hashcode and evaluate equality.
534 Returns:
535 Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
536 """
537
538 properties = self._properties.copy()
539
540 # Dicts are not hashable.
541 # Convert condition to a hashable datatype(s)
542 condition = properties.get("condition")
543 if isinstance(condition, dict):
544 condition_key = tuple(sorted(condition.items()))
545 properties["condition"] = condition_key
546
547 prop_tup = tuple(sorted(properties.items()))
548 return (self.role, self.entity_type, self.entity_id, prop_tup)
549
550 def __hash__(self):
551 return hash(self._key())
552
553 def to_api_repr(self):
554 """Construct the API resource representation of this access entry
555
556 Returns:
557 Dict[str, object]: Access entry represented as an API resource
558 """
559 resource = copy.deepcopy(self._properties)
560 return resource
561
562 @classmethod
563 def from_api_repr(cls, resource: dict) -> "AccessEntry":
564 """Factory: construct an access entry given its API representation
565
566 Args:
567 resource (Dict[str, object]):
568 Access entry resource representation returned from the API
569
570 Returns:
571 google.cloud.bigquery.dataset.AccessEntry:
572 Access entry parsed from ``resource``.
573 """
574 access_entry = cls()
575 access_entry._properties = resource.copy()
576 return access_entry
577
578
579class Dataset(object):
580 """Datasets are containers for tables.
581
582 See
583 https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#resource-dataset
584
585 Args:
586 dataset_ref (Union[google.cloud.bigquery.dataset.DatasetReference, str]):
587 A pointer to a dataset. If ``dataset_ref`` is a string, it must
588 include both the project ID and the dataset ID, separated by
589 ``.``.
590
591 Note:
592 Fields marked as "Output Only" are populated by the server and will only be
593 available after calling :meth:`google.cloud.bigquery.client.Client.get_dataset`.
594 """
595
596 _PROPERTY_TO_API_FIELD = {
597 "access_entries": "access",
598 "created": "creationTime",
599 "default_partition_expiration_ms": "defaultPartitionExpirationMs",
600 "default_table_expiration_ms": "defaultTableExpirationMs",
601 "friendly_name": "friendlyName",
602 "default_encryption_configuration": "defaultEncryptionConfiguration",
603 "is_case_insensitive": "isCaseInsensitive",
604 "storage_billing_model": "storageBillingModel",
605 "max_time_travel_hours": "maxTimeTravelHours",
606 "default_rounding_mode": "defaultRoundingMode",
607 "resource_tags": "resourceTags",
608 "external_catalog_dataset_options": "externalCatalogDatasetOptions",
609 "access_policy_version": "accessPolicyVersion",
610 }
611
612 def __init__(self, dataset_ref) -> None:
613 if isinstance(dataset_ref, str):
614 dataset_ref = DatasetReference.from_string(dataset_ref)
615 self._properties = {"datasetReference": dataset_ref.to_api_repr(), "labels": {}}
616
617 @property
618 def max_time_travel_hours(self):
619 """
620 Optional[int]: Defines the time travel window in hours. The value can
621 be from 48 to 168 hours (2 to 7 days), and in multiple of 24 hours
622 (48, 72, 96, 120, 144, 168).
623 The default value is 168 hours if this is not set.
624 """
625 return self._properties.get("maxTimeTravelHours")
626
627 @max_time_travel_hours.setter
628 def max_time_travel_hours(self, hours):
629 if not isinstance(hours, int):
630 raise ValueError(f"max_time_travel_hours must be an integer. Got {hours}")
631 if hours < 2 * 24 or hours > 7 * 24:
632 raise ValueError(
633 "Time Travel Window should be from 48 to 168 hours (2 to 7 days)"
634 )
635 if hours % 24 != 0:
636 raise ValueError("Time Travel Window should be multiple of 24")
637 self._properties["maxTimeTravelHours"] = hours
638
639 @property
640 def default_rounding_mode(self):
641 """Union[str, None]: defaultRoundingMode of the dataset as set by the user
642 (defaults to :data:`None`).
643
644 Set the value to one of ``'ROUND_HALF_AWAY_FROM_ZERO'``, ``'ROUND_HALF_EVEN'``, or
645 ``'ROUNDING_MODE_UNSPECIFIED'``.
646
647 See `default rounding mode
648 <https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#Dataset.FIELDS.default_rounding_mode>`_
649 in REST API docs and `updating the default rounding model
650 <https://cloud.google.com/bigquery/docs/updating-datasets#update_rounding_mode>`_
651 guide.
652
653 Raises:
654 ValueError: for invalid value types.
655 """
656 return self._properties.get("defaultRoundingMode")
657
658 @default_rounding_mode.setter
659 def default_rounding_mode(self, value):
660 possible_values = [
661 "ROUNDING_MODE_UNSPECIFIED",
662 "ROUND_HALF_AWAY_FROM_ZERO",
663 "ROUND_HALF_EVEN",
664 ]
665 if not isinstance(value, str) and value is not None:
666 raise ValueError("Pass a string, or None")
667 if value is None:
668 self._properties["defaultRoundingMode"] = "ROUNDING_MODE_UNSPECIFIED"
669 if value not in possible_values and value is not None:
670 raise ValueError(
671 f'rounding mode needs to be one of {",".join(possible_values)}'
672 )
673 if value:
674 self._properties["defaultRoundingMode"] = value
675
676 @property
677 def project(self):
678 """str: Project ID of the project bound to the dataset."""
679 return self._properties["datasetReference"]["projectId"]
680
681 @property
682 def path(self):
683 """str: URL path for the dataset based on project and dataset ID."""
684 return "/projects/%s/datasets/%s" % (self.project, self.dataset_id)
685
686 @property
687 def access_entries(self):
688 """List[google.cloud.bigquery.dataset.AccessEntry]: Dataset's access
689 entries.
690
691 ``role`` augments the entity type and must be present **unless** the
692 entity type is ``view`` or ``routine``.
693
694 Raises:
695 TypeError: If 'value' is not a sequence
696 ValueError:
697 If any item in the sequence is not an
698 :class:`~google.cloud.bigquery.dataset.AccessEntry`.
699 """
700 entries = self._properties.get("access", [])
701 return [AccessEntry.from_api_repr(entry) for entry in entries]
702
703 @access_entries.setter
704 def access_entries(self, value):
705 if not all(isinstance(field, AccessEntry) for field in value):
706 raise ValueError("Values must be AccessEntry instances")
707 entries = [entry.to_api_repr() for entry in value]
708 self._properties["access"] = entries
709
710 @property
711 def created(self):
712 """Union[datetime.datetime, None]: Output only. Datetime at which the dataset was
713 created (:data:`None` until set from the server).
714 """
715 creation_time = self._properties.get("creationTime")
716 if creation_time is not None:
717 # creation_time will be in milliseconds.
718 return google.cloud._helpers._datetime_from_microseconds(
719 1000.0 * float(creation_time)
720 )
721
722 @property
723 def dataset_id(self):
724 """str: Dataset ID."""
725 return self._properties["datasetReference"]["datasetId"]
726
727 @property
728 def full_dataset_id(self):
729 """Union[str, None]: Output only. ID for the dataset resource
730 (:data:`None` until set from the server).
731
732 In the format ``project_id:dataset_id``.
733 """
734 return self._properties.get("id")
735
736 @property
737 def reference(self):
738 """google.cloud.bigquery.dataset.DatasetReference: A reference to this
739 dataset.
740 """
741 return DatasetReference(self.project, self.dataset_id)
742
743 @property
744 def etag(self):
745 """Union[str, None]: Output only. ETag for the dataset resource
746 (:data:`None` until set from the server).
747 """
748 return self._properties.get("etag")
749
750 @property
751 def modified(self):
752 """Union[datetime.datetime, None]: Output only. Datetime at which the dataset was
753 last modified (:data:`None` until set from the server).
754 """
755 modified_time = self._properties.get("lastModifiedTime")
756 if modified_time is not None:
757 # modified_time will be in milliseconds.
758 return google.cloud._helpers._datetime_from_microseconds(
759 1000.0 * float(modified_time)
760 )
761
762 @property
763 def self_link(self):
764 """Union[str, None]: Output only. URL for the dataset resource
765 (:data:`None` until set from the server).
766 """
767 return self._properties.get("selfLink")
768
769 @property
770 def default_partition_expiration_ms(self):
771 """Optional[int]: The default partition expiration for all
772 partitioned tables in the dataset, in milliseconds.
773
774 Once this property is set, all newly-created partitioned tables in
775 the dataset will have an ``time_paritioning.expiration_ms`` property
776 set to this value, and changing the value will only affect new
777 tables, not existing ones. The storage in a partition will have an
778 expiration time of its partition time plus this value.
779
780 Setting this property overrides the use of
781 ``default_table_expiration_ms`` for partitioned tables: only one of
782 ``default_table_expiration_ms`` and
783 ``default_partition_expiration_ms`` will be used for any new
784 partitioned table. If you provide an explicit
785 ``time_partitioning.expiration_ms`` when creating or updating a
786 partitioned table, that value takes precedence over the default
787 partition expiration time indicated by this property.
788 """
789 return _helpers._int_or_none(
790 self._properties.get("defaultPartitionExpirationMs")
791 )
792
793 @default_partition_expiration_ms.setter
794 def default_partition_expiration_ms(self, value):
795 self._properties["defaultPartitionExpirationMs"] = _helpers._str_or_none(value)
796
797 @property
798 def default_table_expiration_ms(self):
799 """Union[int, None]: Default expiration time for tables in the dataset
800 (defaults to :data:`None`).
801
802 Raises:
803 ValueError: For invalid value types.
804 """
805 return _helpers._int_or_none(self._properties.get("defaultTableExpirationMs"))
806
807 @default_table_expiration_ms.setter
808 def default_table_expiration_ms(self, value):
809 if not isinstance(value, int) and value is not None:
810 raise ValueError("Pass an integer, or None")
811 self._properties["defaultTableExpirationMs"] = _helpers._str_or_none(value)
812
813 @property
814 def description(self):
815 """Optional[str]: Description of the dataset as set by the user
816 (defaults to :data:`None`).
817
818 Raises:
819 ValueError: for invalid value types.
820 """
821 return self._properties.get("description")
822
823 @description.setter
824 def description(self, value):
825 if not isinstance(value, str) and value is not None:
826 raise ValueError("Pass a string, or None")
827 self._properties["description"] = value
828
829 @property
830 def friendly_name(self):
831 """Union[str, None]: Title of the dataset as set by the user
832 (defaults to :data:`None`).
833
834 Raises:
835 ValueError: for invalid value types.
836 """
837 return self._properties.get("friendlyName")
838
839 @friendly_name.setter
840 def friendly_name(self, value):
841 if not isinstance(value, str) and value is not None:
842 raise ValueError("Pass a string, or None")
843 self._properties["friendlyName"] = value
844
845 @property
846 def location(self):
847 """Union[str, None]: Location in which the dataset is hosted as set by
848 the user (defaults to :data:`None`).
849
850 Raises:
851 ValueError: for invalid value types.
852 """
853 return self._properties.get("location")
854
855 @location.setter
856 def location(self, value):
857 if not isinstance(value, str) and value is not None:
858 raise ValueError("Pass a string, or None")
859 self._properties["location"] = value
860
861 @property
862 def labels(self):
863 """Dict[str, str]: Labels for the dataset.
864
865 This method always returns a dict. To change a dataset's labels,
866 modify the dict, then call
867 :meth:`google.cloud.bigquery.client.Client.update_dataset`. To delete
868 a label, set its value to :data:`None` before updating.
869
870 Raises:
871 ValueError: for invalid value types.
872 """
873 return self._properties.setdefault("labels", {})
874
875 @labels.setter
876 def labels(self, value):
877 if not isinstance(value, dict):
878 raise ValueError("Pass a dict")
879 self._properties["labels"] = value
880
881 @property
882 def resource_tags(self):
883 """Dict[str, str]: Resource tags of the dataset.
884
885 Optional. The tags attached to this dataset. Tag keys are globally
886 unique. Tag key is expected to be in the namespaced format, for
887 example "123456789012/environment" where 123456789012 is
888 the ID of the parent organization or project resource for this tag
889 key. Tag value is expected to be the short name, for example
890 "Production".
891
892 Raises:
893 ValueError: for invalid value types.
894 """
895 return self._properties.setdefault("resourceTags", {})
896
897 @resource_tags.setter
898 def resource_tags(self, value):
899 if not isinstance(value, dict) and value is not None:
900 raise ValueError("Pass a dict")
901 self._properties["resourceTags"] = value
902
903 @property
904 def default_encryption_configuration(self):
905 """google.cloud.bigquery.encryption_configuration.EncryptionConfiguration: Custom
906 encryption configuration for all tables in the dataset.
907
908 Custom encryption configuration (e.g., Cloud KMS keys) or :data:`None`
909 if using default encryption.
910
911 See `protecting data with Cloud KMS keys
912 <https://cloud.google.com/bigquery/docs/customer-managed-encryption>`_
913 in the BigQuery documentation.
914 """
915 prop = self._properties.get("defaultEncryptionConfiguration")
916 if prop:
917 prop = EncryptionConfiguration.from_api_repr(prop)
918 return prop
919
920 @default_encryption_configuration.setter
921 def default_encryption_configuration(self, value):
922 api_repr = value
923 if value:
924 api_repr = value.to_api_repr()
925 self._properties["defaultEncryptionConfiguration"] = api_repr
926
927 @property
928 def is_case_insensitive(self):
929 """Optional[bool]: True if the dataset and its table names are case-insensitive, otherwise False.
930 By default, this is False, which means the dataset and its table names are case-sensitive.
931 This field does not affect routine references.
932
933 Raises:
934 ValueError: for invalid value types.
935 """
936 return self._properties.get("isCaseInsensitive") or False
937
938 @is_case_insensitive.setter
939 def is_case_insensitive(self, value):
940 if not isinstance(value, bool) and value is not None:
941 raise ValueError("Pass a boolean value, or None")
942 if value is None:
943 value = False
944 self._properties["isCaseInsensitive"] = value
945
946 @property
947 def storage_billing_model(self):
948 """Union[str, None]: StorageBillingModel of the dataset as set by the user
949 (defaults to :data:`None`).
950
951 Set the value to one of ``'LOGICAL'``, ``'PHYSICAL'``, or
952 ``'STORAGE_BILLING_MODEL_UNSPECIFIED'``. This change takes 24 hours to
953 take effect and you must wait 14 days before you can change the storage
954 billing model again.
955
956 See `storage billing model
957 <https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#Dataset.FIELDS.storage_billing_model>`_
958 in REST API docs and `updating the storage billing model
959 <https://cloud.google.com/bigquery/docs/updating-datasets#update_storage_billing_models>`_
960 guide.
961
962 Raises:
963 ValueError: for invalid value types.
964 """
965 return self._properties.get("storageBillingModel")
966
967 @storage_billing_model.setter
968 def storage_billing_model(self, value):
969 if not isinstance(value, str) and value is not None:
970 raise ValueError(
971 "storage_billing_model must be a string (e.g. 'LOGICAL',"
972 " 'PHYSICAL', 'STORAGE_BILLING_MODEL_UNSPECIFIED'), or None."
973 f" Got {repr(value)}."
974 )
975 self._properties["storageBillingModel"] = value
976
977 @property
978 def external_catalog_dataset_options(self):
979 """Options defining open source compatible datasets living in the
980 BigQuery catalog. Contains metadata of open source database, schema
981 or namespace represented by the current dataset."""
982
983 prop = _helpers._get_sub_prop(
984 self._properties, ["externalCatalogDatasetOptions"]
985 )
986
987 if prop is not None:
988 prop = external_config.ExternalCatalogDatasetOptions.from_api_repr(prop)
989 return prop
990
991 @external_catalog_dataset_options.setter
992 def external_catalog_dataset_options(self, value):
993 value = _helpers._isinstance_or_raise(
994 value, external_config.ExternalCatalogDatasetOptions, none_allowed=True
995 )
996 self._properties[
997 self._PROPERTY_TO_API_FIELD["external_catalog_dataset_options"]
998 ] = (value.to_api_repr() if value is not None else None)
999
1000 @property
1001 def access_policy_version(self):
1002 return self._properties.get("accessPolicyVersion")
1003
1004 @access_policy_version.setter
1005 def access_policy_version(self, value):
1006 if not isinstance(value, int) and value is not None:
1007 raise ValueError("Pass an integer, or None")
1008 self._properties["accessPolicyVersion"] = value
1009
1010 @classmethod
1011 def from_string(cls, full_dataset_id: str) -> "Dataset":
1012 """Construct a dataset from fully-qualified dataset ID.
1013
1014 Args:
1015 full_dataset_id (str):
1016 A fully-qualified dataset ID in standard SQL format. Must
1017 include both the project ID and the dataset ID, separated by
1018 ``.``.
1019
1020 Returns:
1021 Dataset: Dataset parsed from ``full_dataset_id``.
1022
1023 Examples:
1024 >>> Dataset.from_string('my-project-id.some_dataset')
1025 Dataset(DatasetReference('my-project-id', 'some_dataset'))
1026
1027 Raises:
1028 ValueError:
1029 If ``full_dataset_id`` is not a fully-qualified dataset ID in
1030 standard SQL format.
1031 """
1032 return cls(DatasetReference.from_string(full_dataset_id))
1033
1034 @classmethod
1035 def from_api_repr(cls, resource: dict) -> "Dataset":
1036 """Factory: construct a dataset given its API representation
1037
1038 Args:
1039 resource (Dict[str: object]):
1040 Dataset resource representation returned from the API
1041
1042 Returns:
1043 google.cloud.bigquery.dataset.Dataset:
1044 Dataset parsed from ``resource``.
1045 """
1046 if (
1047 "datasetReference" not in resource
1048 or "datasetId" not in resource["datasetReference"]
1049 ):
1050 raise KeyError(
1051 "Resource lacks required identity information:"
1052 '["datasetReference"]["datasetId"]'
1053 )
1054 project_id = resource["datasetReference"]["projectId"]
1055 dataset_id = resource["datasetReference"]["datasetId"]
1056 dataset = cls(DatasetReference(project_id, dataset_id))
1057 dataset._properties = copy.deepcopy(resource)
1058 return dataset
1059
1060 def to_api_repr(self) -> dict:
1061 """Construct the API resource representation of this dataset
1062
1063 Returns:
1064 Dict[str, object]: The dataset represented as an API resource
1065 """
1066 return copy.deepcopy(self._properties)
1067
1068 def _build_resource(self, filter_fields):
1069 """Generate a resource for ``update``."""
1070 return _helpers._build_resource_from_properties(self, filter_fields)
1071
1072 table = _get_table_reference
1073
1074 model = _get_model_reference
1075
1076 routine = _get_routine_reference
1077
1078 def __repr__(self):
1079 return "Dataset({})".format(repr(self.reference))
1080
1081
1082class DatasetListItem(object):
1083 """A read-only dataset resource from a list operation.
1084
1085 For performance reasons, the BigQuery API only includes some of the
1086 dataset properties when listing datasets. Notably,
1087 :attr:`~google.cloud.bigquery.dataset.Dataset.access_entries` is missing.
1088
1089 For a full list of the properties that the BigQuery API returns, see the
1090 `REST documentation for datasets.list
1091 <https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets/list>`_.
1092
1093
1094 Args:
1095 resource (Dict[str, str]):
1096 A dataset-like resource object from a dataset list response. A
1097 ``datasetReference`` property is required.
1098
1099 Raises:
1100 ValueError:
1101 If ``datasetReference`` or one of its required members is missing
1102 from ``resource``.
1103 """
1104
1105 def __init__(self, resource):
1106 if "datasetReference" not in resource:
1107 raise ValueError("resource must contain a datasetReference value")
1108 if "projectId" not in resource["datasetReference"]:
1109 raise ValueError(
1110 "resource['datasetReference'] must contain a projectId value"
1111 )
1112 if "datasetId" not in resource["datasetReference"]:
1113 raise ValueError(
1114 "resource['datasetReference'] must contain a datasetId value"
1115 )
1116 self._properties = resource
1117
1118 @property
1119 def project(self):
1120 """str: Project bound to the dataset."""
1121 return self._properties["datasetReference"]["projectId"]
1122
1123 @property
1124 def dataset_id(self):
1125 """str: Dataset ID."""
1126 return self._properties["datasetReference"]["datasetId"]
1127
1128 @property
1129 def full_dataset_id(self):
1130 """Union[str, None]: ID for the dataset resource (:data:`None` until
1131 set from the server)
1132
1133 In the format ``project_id:dataset_id``.
1134 """
1135 return self._properties.get("id")
1136
1137 @property
1138 def friendly_name(self):
1139 """Union[str, None]: Title of the dataset as set by the user
1140 (defaults to :data:`None`).
1141 """
1142 return self._properties.get("friendlyName")
1143
1144 @property
1145 def labels(self):
1146 """Dict[str, str]: Labels for the dataset."""
1147 return self._properties.setdefault("labels", {})
1148
1149 @property
1150 def reference(self):
1151 """google.cloud.bigquery.dataset.DatasetReference: A reference to this
1152 dataset.
1153 """
1154 return DatasetReference(self.project, self.dataset_id)
1155
1156 table = _get_table_reference
1157
1158 model = _get_model_reference
1159
1160 routine = _get_routine_reference
1161
1162
1163class Condition(object):
1164 """Represents a textual expression in the Common Expression Language (CEL) syntax.
1165
1166 Typically used for filtering or policy rules, such as in IAM Conditions
1167 or BigQuery row/column access policies.
1168
1169 See:
1170 https://cloud.google.com/iam/docs/reference/rest/Shared.Types/Expr
1171 https://github.com/google/cel-spec
1172
1173 Args:
1174 expression (str):
1175 The condition expression string using CEL syntax. This is required.
1176 Example: ``resource.type == "compute.googleapis.com/Instance"``
1177 title (Optional[str]):
1178 An optional title for the condition, providing a short summary.
1179 Example: ``"Request is for a GCE instance"``
1180 description (Optional[str]):
1181 An optional description of the condition, providing a detailed explanation.
1182 Example: ``"This condition checks whether the resource is a GCE instance."``
1183 """
1184
1185 def __init__(
1186 self,
1187 expression: str,
1188 title: Optional[str] = None,
1189 description: Optional[str] = None,
1190 ):
1191 self._properties: Dict[str, Any] = {}
1192 # Use setters to initialize properties, which also handle validation
1193 self.expression = expression
1194 self.title = title
1195 self.description = description
1196
1197 @property
1198 def title(self) -> Optional[str]:
1199 """Optional[str]: The title for the condition."""
1200 return self._properties.get("title")
1201
1202 @title.setter
1203 def title(self, value: Optional[str]):
1204 if value is not None and not isinstance(value, str):
1205 raise ValueError("Pass a string for title, or None")
1206 self._properties["title"] = value
1207
1208 @property
1209 def description(self) -> Optional[str]:
1210 """Optional[str]: The description for the condition."""
1211 return self._properties.get("description")
1212
1213 @description.setter
1214 def description(self, value: Optional[str]):
1215 if value is not None and not isinstance(value, str):
1216 raise ValueError("Pass a string for description, or None")
1217 self._properties["description"] = value
1218
1219 @property
1220 def expression(self) -> str:
1221 """str: The expression string for the condition."""
1222
1223 # Cast assumes expression is always set due to __init__ validation
1224 return typing.cast(str, self._properties.get("expression"))
1225
1226 @expression.setter
1227 def expression(self, value: str):
1228 if not isinstance(value, str):
1229 raise ValueError("Pass a non-empty string for expression")
1230 if not value:
1231 raise ValueError("expression cannot be an empty string")
1232 self._properties["expression"] = value
1233
1234 def to_api_repr(self) -> Dict[str, Any]:
1235 """Construct the API resource representation of this Condition."""
1236 return self._properties
1237
1238 @classmethod
1239 def from_api_repr(cls, resource: Dict[str, Any]) -> "Condition":
1240 """Factory: construct a Condition instance given its API representation."""
1241
1242 # Ensure required fields are present in the resource if necessary
1243 if "expression" not in resource:
1244 raise ValueError("API representation missing required 'expression' field.")
1245
1246 return cls(
1247 expression=resource["expression"],
1248 title=resource.get("title"),
1249 description=resource.get("description"),
1250 )
1251
1252 def __eq__(self, other: object) -> bool:
1253 """Check for equality based on expression, title, and description."""
1254 if not isinstance(other, Condition):
1255 return NotImplemented
1256 return self._key() == other._key()
1257
1258 def _key(self):
1259 """A tuple key that uniquely describes this field.
1260 Used to compute this instance's hashcode and evaluate equality.
1261 Returns:
1262 Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
1263 """
1264
1265 properties = self._properties.copy()
1266
1267 # Dicts are not hashable.
1268 # Convert object to a hashable datatype(s)
1269 prop_tup = tuple(sorted(properties.items()))
1270 return prop_tup
1271
1272 def __ne__(self, other: object) -> bool:
1273 """Check for inequality."""
1274 return not self == other
1275
1276 def __hash__(self) -> int:
1277 """Generate a hash based on expression, title, and description."""
1278 return hash(self._key())
1279
1280 def __repr__(self) -> str:
1281 """Return a string representation of the Condition object."""
1282 parts = [f"expression={self.expression!r}"]
1283 if self.title is not None:
1284 parts.append(f"title={self.title!r}")
1285 if self.description is not None:
1286 parts.append(f"description={self.description!r}")
1287 return f"Condition({', '.join(parts)})"