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