diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 9e0a9356b..757c9dca7 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:462782b0b492346b2d9099aaff52206dd30bc8e031ea97082e6facecc2373244 + digest: sha256:81ed5ecdfc7cac5b699ba4537376f3563f6f04122c4ec9e735d3b3dc1d43dd32 +# created: 2022-05-05T22:08:23.383410683Z diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml new file mode 100644 index 000000000..311ebbb85 --- /dev/null +++ b/.github/auto-approve.yml @@ -0,0 +1,3 @@ +# https://github.com/googleapis/repo-automation-bots/tree/main/packages/auto-approve +processes: + - "OwlBotTemplateChanges" diff --git a/.github/auto-label.yaml b/.github/auto-label.yaml new file mode 100644 index 000000000..41bff0b53 --- /dev/null +++ b/.github/auto-label.yaml @@ -0,0 +1,15 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +requestsize: + enabled: true diff --git a/.github/release-please.yml b/.github/release-please.yml index 466597e5b..5161ab347 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -1,2 +1,14 @@ releaseType: python handleGHRelease: true +# NOTE: this section is generated by synthtool.languages.python +# See https://github.com/googleapis/synthtool/blob/master/synthtool/languages/python.py +branches: +- branch: v2 + handleGHRelease: true + releaseType: python +- branch: v1 + handleGHRelease: true + releaseType: python +- branch: v0 + handleGHRelease: true + releaseType: python diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index 4e1b1fb8b..238b87b9d 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ubuntu:20.04 +from ubuntu:22.04 ENV DEBIAN_FRONTEND noninteractive @@ -60,8 +60,24 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* \ && rm -f /var/cache/apt/archives/*.deb +###################### Install python 3.8.11 + +# Download python 3.8.11 +RUN wget https://www.python.org/ftp/python/3.8.11/Python-3.8.11.tgz + +# Extract files +RUN tar -xvf Python-3.8.11.tgz + +# Install python 3.8.11 +RUN ./Python-3.8.11/configure --enable-optimizations +RUN make altinstall + +###################### Install pip RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ - && python3.8 /tmp/get-pip.py \ + && python3 /tmp/get-pip.py \ && rm /tmp/get-pip.py +# Test pip +RUN python3 -m pip + CMD ["python3.8"] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62eb5a77d..46d237160 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: end-of-file-fixer - id: check-yaml - repo: https://github.com/psf/black - rev: 19.10b0 + rev: 22.3.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 diff --git a/CHANGELOG.md b/CHANGELOG.md index ca99c969f..0771a8f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ [1]: https://pypi.org/project/google-cloud-bigquery/#history +## [3.1.0](https://github.com/googleapis/python-bigquery/compare/v3.0.1...v3.1.0) (2022-05-09) + + +### Features + +* add str method to table ([#1199](https://github.com/googleapis/python-bigquery/issues/1199)) ([8da4fa9](https://github.com/googleapis/python-bigquery/commit/8da4fa9e77bcfd2b68818b5d65b38ccc59899a01)) +* refactor AccessEntry to use _properties pattern ([#1125](https://github.com/googleapis/python-bigquery/issues/1125)) ([acd5612](https://github.com/googleapis/python-bigquery/commit/acd5612d2fc469633936dbc463ce4d70951e7fdd)) +* support using BIGQUERY_EMULATOR_HOST environment variable ([#1222](https://github.com/googleapis/python-bigquery/issues/1222)) ([39294b4](https://github.com/googleapis/python-bigquery/commit/39294b4950896b084573bedb4c5adc2b8d371eac)) + + +### Bug Fixes + +* **deps:** allow pyarrow v8 ([#1245](https://github.com/googleapis/python-bigquery/issues/1245)) ([d258690](https://github.com/googleapis/python-bigquery/commit/d258690dbf01108e1426f0e28d792c418a88bce0)) +* export bigquery.HivePartitioningOptions ([#1217](https://github.com/googleapis/python-bigquery/issues/1217)) ([8eb757b](https://github.com/googleapis/python-bigquery/commit/8eb757bcded7a3ef3b2264f47ec080c0a8fca579)) +* Skip geography_as_object conversion for REPEATED fields ([#1220](https://github.com/googleapis/python-bigquery/issues/1220)) ([4d3d6ec](https://github.com/googleapis/python-bigquery/commit/4d3d6ec9e667a781f8cb4a3aee0376c6179d5ce1)) + + +### Documentation + +* updated variable typo in comment in code sample ([#1239](https://github.com/googleapis/python-bigquery/issues/1239)) ([e420112](https://github.com/googleapis/python-bigquery/commit/e4201128bdb7f49cb732e12609448bbdbc122736)) + ### [3.0.1](https://github.com/googleapis/python-bigquery/compare/v3.0.0...v3.0.1) (2022-03-30) diff --git a/google/cloud/bigquery/__init__.py b/google/cloud/bigquery/__init__.py index 1ac04d50c..81b1285e3 100644 --- a/google/cloud/bigquery/__init__.py +++ b/google/cloud/bigquery/__init__.py @@ -49,6 +49,7 @@ from google.cloud.bigquery.external_config import CSVOptions from google.cloud.bigquery.external_config import GoogleSheetsOptions from google.cloud.bigquery.external_config import ExternalSourceFormat +from google.cloud.bigquery.external_config import HivePartitioningOptions from google.cloud.bigquery.format_options import AvroOptions from google.cloud.bigquery.format_options import ParquetOptions from google.cloud.bigquery.job.base import SessionInfo @@ -161,6 +162,7 @@ "DmlStats", "CSVOptions", "GoogleSheetsOptions", + "HivePartitioningOptions", "ParquetOptions", "ScriptOptions", "TransactionInfo", diff --git a/google/cloud/bigquery/_helpers.py b/google/cloud/bigquery/_helpers.py index 6faa32606..b59bc86d3 100644 --- a/google/cloud/bigquery/_helpers.py +++ b/google/cloud/bigquery/_helpers.py @@ -19,6 +19,7 @@ import decimal import math import re +import os from typing import Optional, Union from dateutil import relativedelta @@ -28,8 +29,8 @@ from google.cloud._helpers import _RFC3339_MICROS from google.cloud._helpers import _RFC3339_NO_FRACTION from google.cloud._helpers import _to_bytes -import packaging.version +import packaging.version _RFC3339_MICROS_NO_ZULU = "%Y-%m-%dT%H:%M:%S.%f" _TIMEONLY_WO_MICROS = "%H:%M:%S" @@ -51,6 +52,16 @@ _BQ_STORAGE_OPTIONAL_READ_SESSION_VERSION = packaging.version.Version("2.6.0") +BIGQUERY_EMULATOR_HOST = "BIGQUERY_EMULATOR_HOST" +"""Environment variable defining host for emulator.""" + +_DEFAULT_HOST = "https://bigquery.googleapis.com" +"""Default host for JSON API.""" + + +def _get_bigquery_host(): + return os.environ.get(BIGQUERY_EMULATOR_HOST, _DEFAULT_HOST) + class BQStorageVersions: """Version comparisons for google-cloud-bigqueyr-storage package.""" diff --git a/google/cloud/bigquery/client.py b/google/cloud/bigquery/client.py index b388f1d4c..fb772ea11 100644 --- a/google/cloud/bigquery/client.py +++ b/google/cloud/bigquery/client.py @@ -56,7 +56,6 @@ import google.cloud._helpers # type: ignore from google.cloud import exceptions # pytype: disable=import-error from google.cloud.client import ClientWithProject # type: ignore # pytype: disable=import-error - from google.cloud.bigquery_storage_v1.services.big_query_read.client import ( DEFAULT_CLIENT_INFO as DEFAULT_BQSTORAGE_CLIENT_INFO, ) @@ -67,6 +66,8 @@ from google.cloud.bigquery._helpers import _record_field_to_json from google.cloud.bigquery._helpers import _str_or_none from google.cloud.bigquery._helpers import _verify_job_config_type +from google.cloud.bigquery._helpers import _get_bigquery_host +from google.cloud.bigquery._helpers import _DEFAULT_HOST from google.cloud.bigquery._http import Connection from google.cloud.bigquery import _pandas_helpers from google.cloud.bigquery.dataset import Dataset @@ -230,6 +231,8 @@ def __init__( ) kw_args = {"client_info": client_info} + bq_host = _get_bigquery_host() + kw_args["api_endpoint"] = bq_host if bq_host != _DEFAULT_HOST else None if client_options: if type(client_options) == dict: client_options = google.api_core.client_options.from_dict( diff --git a/google/cloud/bigquery/dataset.py b/google/cloud/bigquery/dataset.py index 0fafd5783..c30204067 100644 --- a/google/cloud/bigquery/dataset.py +++ b/google/cloud/bigquery/dataset.py @@ -17,16 +17,19 @@ from __future__ import absolute_import import copy -from typing import Dict, Any + +import typing import google.cloud._helpers # type: ignore from google.cloud.bigquery import _helpers from google.cloud.bigquery.model import ModelReference -from google.cloud.bigquery.routine import RoutineReference -from google.cloud.bigquery.table import TableReference +from google.cloud.bigquery.routine import Routine, RoutineReference +from google.cloud.bigquery.table import Table, TableReference from google.cloud.bigquery.encryption_configuration import EncryptionConfiguration +from typing import Optional, List, Dict, Any, Union + def _get_table_reference(self, table_id: str) -> TableReference: """Constructs a TableReference. @@ -75,173 +78,6 @@ def _get_routine_reference(self, routine_id): ) -class AccessEntry(object): - """Represents grant of an access role to an entity. - - An entry must have exactly one of the allowed - :class:`google.cloud.bigquery.enums.EntityTypes`. If anything but ``view``, ``routine``, - or ``dataset`` are set, a ``role`` is also required. ``role`` is omitted for ``view``, - ``routine``, ``dataset``, because they are always read-only. - - See https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets. - - Args: - role (str): - Role granted to the entity. The following string values are - supported: `'READER'`, `'WRITER'`, `'OWNER'`. It may also be - :data:`None` if the ``entity_type`` is ``view``, ``routine``, or ``dataset``. - - entity_type (str): - Type of entity being granted the role. See - :class:`google.cloud.bigquery.enums.EntityTypes` for supported types. - - entity_id (Union[str, Dict[str, str]]): - If the ``entity_type`` is not 'view', 'routine', or 'dataset', the - ``entity_id`` is the ``str`` ID of the entity being granted the role. If - the ``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict`` - representing the view or routine from a different dataset to grant access - to in the following format for views:: - - { - 'projectId': string, - 'datasetId': string, - 'tableId': string - } - - For routines:: - - { - 'projectId': string, - 'datasetId': string, - 'routineId': string - } - - If the ``entity_type`` is 'dataset', the ``entity_id`` is a ``dict`` that includes - a 'dataset' field with a ``dict`` representing the dataset and a 'target_types' - field with a ``str`` value of the dataset's resource type:: - - { - 'dataset': { - 'projectId': string, - 'datasetId': string, - }, - 'target_types: 'VIEWS' - } - - Raises: - ValueError: - If a ``view``, ``routine``, or ``dataset`` has ``role`` set, or a non ``view``, - non ``routine``, and non ``dataset`` **does not** have a ``role`` set. - - Examples: - >>> entry = AccessEntry('OWNER', 'userByEmail', 'user@example.com') - - >>> view = { - ... 'projectId': 'my-project', - ... 'datasetId': 'my_dataset', - ... 'tableId': 'my_table' - ... } - >>> entry = AccessEntry(None, 'view', view) - """ - - def __init__(self, role=None, entity_type=None, entity_id=None) -> None: - self._properties: Dict[str, Any] = {} - if entity_type in ("view", "routine", "dataset"): - if role is not None: - raise ValueError( - "Role must be None for a %r. Received " - "role: %r" % (entity_type, role) - ) - else: - if role is None: - raise ValueError( - "Role must be set for entity " "type %r" % (entity_type,) - ) - self._role = role - self._entity_type = entity_type - self._entity_id = entity_id - - @property - def role(self): - """str: The role of the entry.""" - return self._role - - @property - def entity_type(self): - """str: The entity_type of the entry.""" - return self._entity_type - - @property - def entity_id(self): - """str: The entity_id of the entry.""" - return self._entity_id - - def __eq__(self, other): - if not isinstance(other, AccessEntry): - return NotImplemented - return self._key() == other._key() - - def __ne__(self, other): - return not self == other - - def __repr__(self): - return "" % ( - self._role, - self._entity_type, - self._entity_id, - ) - - def _key(self): - """A tuple key that uniquely describes this field. - Used to compute this instance's hashcode and evaluate equality. - Returns: - Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`. - """ - return (self._role, self._entity_type, self._entity_id) - - def __hash__(self): - return hash(self._key()) - - def to_api_repr(self): - """Construct the API resource representation of this access entry - - Returns: - Dict[str, object]: Access entry represented as an API resource - """ - resource = copy.deepcopy(self._properties) - resource[self._entity_type] = self._entity_id - if self._role is not None: - resource["role"] = self._role - return resource - - @classmethod - def from_api_repr(cls, resource: dict) -> "AccessEntry": - """Factory: construct an access entry given its API representation - - Args: - resource (Dict[str, object]): - Access entry resource representation returned from the API - - Returns: - google.cloud.bigquery.dataset.AccessEntry: - Access entry parsed from ``resource``. - - Raises: - ValueError: - If the resource has more keys than ``role`` and one additional - key. - """ - entry = resource.copy() - role = entry.pop("role", None) - entity_type, entity_id = entry.popitem() - if len(entry) != 0: - raise ValueError("Entry has unexpected keys remaining.", entry) - - config = cls(role, entity_type, entity_id) - config._properties = copy.deepcopy(resource) - return config - - class DatasetReference(object): """DatasetReferences are pointers to datasets. @@ -383,6 +219,291 @@ def __repr__(self): return "DatasetReference{}".format(self._key()) +class AccessEntry(object): + """Represents grant of an access role to an entity. + + An entry must have exactly one of the allowed + :class:`google.cloud.bigquery.enums.EntityTypes`. If anything but ``view``, ``routine``, + or ``dataset`` are set, a ``role`` is also required. ``role`` is omitted for ``view``, + ``routine``, ``dataset``, because they are always read-only. + + See https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets. + + Args: + role: + Role granted to the entity. The following string values are + supported: `'READER'`, `'WRITER'`, `'OWNER'`. It may also be + :data:`None` if the ``entity_type`` is ``view``, ``routine``, or ``dataset``. + + entity_type: + Type of entity being granted the role. See + :class:`google.cloud.bigquery.enums.EntityTypes` for supported types. + + entity_id: + If the ``entity_type`` is not 'view', 'routine', or 'dataset', the + ``entity_id`` is the ``str`` ID of the entity being granted the role. If + the ``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict`` + representing the view or routine from a different dataset to grant access + to in the following format for views:: + + { + 'projectId': string, + 'datasetId': string, + 'tableId': string + } + + For routines:: + + { + 'projectId': string, + 'datasetId': string, + 'routineId': string + } + + If the ``entity_type`` is 'dataset', the ``entity_id`` is a ``dict`` that includes + a 'dataset' field with a ``dict`` representing the dataset and a 'target_types' + field with a ``str`` value of the dataset's resource type:: + + { + 'dataset': { + 'projectId': string, + 'datasetId': string, + }, + 'target_types: 'VIEWS' + } + + Raises: + ValueError: + If a ``view``, ``routine``, or ``dataset`` has ``role`` set, or a non ``view``, + non ``routine``, and non ``dataset`` **does not** have a ``role`` set. + + Examples: + >>> entry = AccessEntry('OWNER', 'userByEmail', 'user@example.com') + + >>> view = { + ... 'projectId': 'my-project', + ... 'datasetId': 'my_dataset', + ... 'tableId': 'my_table' + ... } + >>> entry = AccessEntry(None, 'view', view) + """ + + def __init__( + self, + role: Optional[str] = None, + entity_type: Optional[str] = None, + entity_id: Optional[Union[Dict[str, Any], str]] = None, + ): + self._properties = {} + if entity_type is not None: + self._properties[entity_type] = entity_id + self._properties["role"] = role + self._entity_type = entity_type + + @property + def role(self) -> Optional[str]: + """The role of the entry.""" + return typing.cast(Optional[str], self._properties.get("role")) + + @role.setter + def role(self, value): + self._properties["role"] = value + + @property + def dataset(self) -> Optional[DatasetReference]: + """API resource representation of a dataset reference.""" + value = _helpers._get_sub_prop(self._properties, ["dataset", "dataset"]) + return DatasetReference.from_api_repr(value) if value else None + + @dataset.setter + def dataset(self, value): + if self.role is not None: + raise ValueError( + "Role must be None for a dataset. Current " "role: %r" % (self.role) + ) + + if isinstance(value, str): + value = DatasetReference.from_string(value).to_api_repr() + + if isinstance(value, (Dataset, DatasetListItem)): + value = value.reference.to_api_repr() + + _helpers._set_sub_prop(self._properties, ["dataset", "dataset"], value) + _helpers._set_sub_prop( + self._properties, + ["dataset", "targetTypes"], + self._properties.get("targetTypes"), + ) + + @property + def dataset_target_types(self) -> Optional[List[str]]: + """Which resources that the dataset in this entry applies to.""" + return typing.cast( + Optional[List[str]], + _helpers._get_sub_prop(self._properties, ["dataset", "targetTypes"]), + ) + + @dataset_target_types.setter + def dataset_target_types(self, value): + self._properties.setdefault("dataset", {}) + _helpers._set_sub_prop(self._properties, ["dataset", "targetTypes"], value) + + @property + def routine(self) -> Optional[RoutineReference]: + """API resource representation of a routine reference.""" + value = typing.cast(Optional[Dict], self._properties.get("routine")) + return RoutineReference.from_api_repr(value) if value else None + + @routine.setter + def routine(self, value): + if self.role is not None: + raise ValueError( + "Role must be None for a routine. Current " "role: %r" % (self.role) + ) + + if isinstance(value, str): + value = RoutineReference.from_string(value).to_api_repr() + + if isinstance(value, RoutineReference): + value = value.to_api_repr() + + if isinstance(value, Routine): + value = value.reference.to_api_repr() + + self._properties["routine"] = value + + @property + def view(self) -> Optional[TableReference]: + """API resource representation of a view reference.""" + value = typing.cast(Optional[Dict], self._properties.get("view")) + return TableReference.from_api_repr(value) if value else None + + @view.setter + def view(self, value): + if self.role is not None: + raise ValueError( + "Role must be None for a view. Current " "role: %r" % (self.role) + ) + + if isinstance(value, str): + value = TableReference.from_string(value).to_api_repr() + + if isinstance(value, TableReference): + value = value.to_api_repr() + + if isinstance(value, Table): + value = value.reference.to_api_repr() + + self._properties["view"] = value + + @property + def group_by_email(self) -> Optional[str]: + """An email address of a Google Group to grant access to.""" + return typing.cast(Optional[str], self._properties.get("groupByEmail")) + + @group_by_email.setter + def group_by_email(self, value): + self._properties["groupByEmail"] = value + + @property + def user_by_email(self) -> Optional[str]: + """An email address of a user to grant access to.""" + return typing.cast(Optional[str], self._properties.get("userByEmail")) + + @user_by_email.setter + def user_by_email(self, value): + self._properties["userByEmail"] = value + + @property + def domain(self) -> Optional[str]: + """A domain to grant access to.""" + return typing.cast(Optional[str], self._properties.get("domain")) + + @domain.setter + def domain(self, value): + self._properties["domain"] = value + + @property + def special_group(self) -> Optional[str]: + """A special group to grant access to.""" + return typing.cast(Optional[str], self._properties.get("specialGroup")) + + @special_group.setter + def special_group(self, value): + self._properties["specialGroup"] = value + + @property + def entity_type(self) -> Optional[str]: + """The entity_type of the entry.""" + return self._entity_type + + @property + def entity_id(self) -> Optional[Union[Dict[str, Any], str]]: + """The entity_id of the entry.""" + return self._properties.get(self._entity_type) if self._entity_type else None + + def __eq__(self, other): + if not isinstance(other, AccessEntry): + return NotImplemented + return self._key() == other._key() + + def __ne__(self, other): + return not self == other + + def __repr__(self): + + return f"" + + def _key(self): + """A tuple key that uniquely describes this field. + Used to compute this instance's hashcode and evaluate equality. + Returns: + Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`. + """ + properties = self._properties.copy() + prop_tup = tuple(sorted(properties.items())) + return (self.role, self._entity_type, self.entity_id, prop_tup) + + def __hash__(self): + return hash(self._key()) + + def to_api_repr(self): + """Construct the API resource representation of this access entry + + Returns: + Dict[str, object]: Access entry represented as an API resource + """ + resource = copy.deepcopy(self._properties) + return resource + + @classmethod + def from_api_repr(cls, resource: dict) -> "AccessEntry": + """Factory: construct an access entry given its API representation + + Args: + resource (Dict[str, object]): + Access entry resource representation returned from the API + + Returns: + google.cloud.bigquery.dataset.AccessEntry: + Access entry parsed from ``resource``. + + Raises: + ValueError: + If the resource has more keys than ``role`` and one additional + key. + """ + entry = resource.copy() + role = entry.pop("role", None) + entity_type, entity_id = entry.popitem() + if len(entry) != 0: + raise ValueError("Entry has unexpected keys remaining.", entry) + + config = cls(role, entity_type, entity_id) + config._properties = copy.deepcopy(resource) + return config + + class Dataset(object): """Datasets are containers for tables. diff --git a/google/cloud/bigquery/table.py b/google/cloud/bigquery/table.py index 5a4de6a01..7b8c6441f 100644 --- a/google/cloud/bigquery/table.py +++ b/google/cloud/bigquery/table.py @@ -1019,6 +1019,9 @@ def _build_resource(self, filter_fields): def __repr__(self): return "Table({})".format(repr(self.reference)) + def __str__(self): + return f"{self.project}.{self.dataset_id}.{self.table_id}" + class TableListItem(_TableBase): """A read-only table resource from a list operation. @@ -1982,7 +1985,7 @@ def to_dataframe( if geography_as_object: for field in self.schema: - if field.field_type.upper() == "GEOGRAPHY": + if field.field_type.upper() == "GEOGRAPHY" and field.mode != "REPEATED": df[field.name] = df[field.name].dropna().apply(_read_wkt) return df diff --git a/google/cloud/bigquery/version.py b/google/cloud/bigquery/version.py index ad3213664..6ce498ba5 100644 --- a/google/cloud/bigquery/version.py +++ b/google/cloud/bigquery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "3.0.1" +__version__ = "3.1.0" diff --git a/google/cloud/bigquery_v2/types/__init__.py b/google/cloud/bigquery_v2/types/__init__.py index c038bcd74..c36b30969 100644 --- a/google/cloud/bigquery_v2/types/__init__.py +++ b/google/cloud/bigquery_v2/types/__init__.py @@ -13,7 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from .encryption_config import EncryptionConfiguration +from .encryption_config import ( + EncryptionConfiguration, +) from .model import ( DeleteModelRequest, GetModelRequest, @@ -22,14 +24,18 @@ Model, PatchModelRequest, ) -from .model_reference import ModelReference +from .model_reference import ( + ModelReference, +) from .standard_sql import ( StandardSqlDataType, StandardSqlField, StandardSqlStructType, StandardSqlTableType, ) -from .table_reference import TableReference +from .table_reference import ( + TableReference, +) __all__ = ( "EncryptionConfiguration", diff --git a/google/cloud/bigquery_v2/types/model.py b/google/cloud/bigquery_v2/types/model.py index 7786d8ea4..f32e15eb1 100644 --- a/google/cloud/bigquery_v2/types/model.py +++ b/google/cloud/bigquery_v2/types/model.py @@ -55,7 +55,7 @@ class Model(proto.Message): model. friendly_name (str): Optional. A descriptive name for this model. - labels (Sequence[google.cloud.bigquery_v2.types.Model.LabelsEntry]): + labels (Mapping[str, str]): The labels associated with this model. You can use these to organize and group your models. Label keys and values can be no longer than 63 @@ -1200,7 +1200,7 @@ class TrainingOptions(proto.Message): initial_learn_rate (float): Specifies the initial learning rate for the line search learn rate strategy. - label_class_weights (Sequence[google.cloud.bigquery_v2.types.Model.TrainingRun.TrainingOptions.LabelClassWeightsEntry]): + label_class_weights (Mapping[str, float]): Weights associated with each label class, for rebalancing the training data. Only applicable for classification models. diff --git a/owlbot.py b/owlbot.py index a445b2be9..ca96f4e08 100644 --- a/owlbot.py +++ b/owlbot.py @@ -19,6 +19,25 @@ from synthtool import gcp from synthtool.languages import python +default_version = "v2" + +for library in s.get_staging_dirs(default_version): + # Avoid breaking change due to change in field renames. + # https://github.com/googleapis/python-bigquery/issues/319 + s.replace( + library / f"google/cloud/bigquery_{library.name}/types/standard_sql.py", + r"type_ ", + "type ", + ) + # Patch docs issue + s.replace( + library / f"google/cloud/bigquery_{library.name}/types/model.py", + r"""\"predicted_\"""", + """`predicted_`""", + ) + s.move(library / f"google/cloud/bigquery_{library.name}/types") +s.remove_staging_dirs() + common = gcp.CommonTemplates() # ---------------------------------------------------------------------------- @@ -52,6 +71,7 @@ ], ) +python.configure_previous_major_version_branches() # ---------------------------------------------------------------------------- # Samples templates # ---------------------------------------------------------------------------- diff --git a/samples/geography/noxfile.py b/samples/geography/noxfile.py index 25f87a215..a40410b56 100644 --- a/samples/geography/noxfile.py +++ b/samples/geography/noxfile.py @@ -30,6 +30,7 @@ # WARNING - WARNING - WARNING - WARNING - WARNING BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" # Copy `noxfile_config.py` to your directory and modify it instead. @@ -168,12 +169,33 @@ def lint(session: nox.sessions.Session) -> None: @nox.session def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" session.install(BLACK_VERSION) python_files = [path for path in os.listdir(".") if path.endswith(".py")] session.run("black", *python_files) +# +# format = isort + black +# + + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + # # Sample Tests # diff --git a/samples/geography/requirements-test.txt b/samples/geography/requirements-test.txt index 5e29de931..fb466e509 100644 --- a/samples/geography/requirements-test.txt +++ b/samples/geography/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==7.1.1 +pytest==7.1.2 mock==4.0.3 diff --git a/samples/geography/requirements.txt b/samples/geography/requirements.txt index 1b1b008e2..dc2d9e72b 100644 --- a/samples/geography/requirements.txt +++ b/samples/geography/requirements.txt @@ -2,7 +2,7 @@ attrs==21.4.0 certifi==2021.10.8 cffi==1.15.0 charset-normalizer==2.0.12 -click==8.1.0 +click==8.1.3 click-plugins==1.1.1 cligj==0.7.2 dataclasses==0.8; python_version < '3.7' @@ -10,36 +10,36 @@ db-dtypes==1.0.0 Fiona==1.8.21 geojson==2.5.0 geopandas==0.10.2 -google-api-core==2.7.1 -google-auth==2.6.2 -google-cloud-bigquery==3.0.0 -google-cloud-bigquery-storage==2.13.0 -google-cloud-core==2.2.3 +google-api-core==2.7.3 +google-auth==2.6.6 +google-cloud-bigquery==3.0.1 +google-cloud-bigquery-storage==2.13.1 +google-cloud-core==2.3.0 google-crc32c==1.3.0 google-resumable-media==2.3.2 googleapis-common-protos==1.56.0 -grpcio==1.44.0 +grpcio==1.46.0 idna==3.3 -libcst==0.4.1 +libcst==0.4.2 munch==2.5.0 mypy-extensions==0.4.3 packaging==21.3 pandas===1.3.5; python_version == '3.7' -pandas==1.4.1; python_version >= '3.8' +pandas==1.4.2; python_version >= '3.8' proto-plus==1.20.3 -protobuf==3.19.4 +protobuf==3.20.1 pyarrow==7.0.0 pyasn1==0.4.8 pyasn1-modules==0.2.8 pycparser==2.21 -pyparsing==3.0.7 +pyparsing==3.0.8 python-dateutil==2.8.2 pytz==2022.1 PyYAML==6.0 requests==2.27.1 rsa==4.8 -Shapely==1.8.1.post1 +Shapely==1.8.2 six==1.16.0 -typing-extensions==4.1.1 +typing-extensions==4.2.0 typing-inspect==0.7.1 urllib3==1.26.9 diff --git a/samples/magics/noxfile.py b/samples/magics/noxfile.py index 25f87a215..a40410b56 100644 --- a/samples/magics/noxfile.py +++ b/samples/magics/noxfile.py @@ -30,6 +30,7 @@ # WARNING - WARNING - WARNING - WARNING - WARNING BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" # Copy `noxfile_config.py` to your directory and modify it instead. @@ -168,12 +169,33 @@ def lint(session: nox.sessions.Session) -> None: @nox.session def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" session.install(BLACK_VERSION) python_files = [path for path in os.listdir(".") if path.endswith(".py")] session.run("black", *python_files) +# +# format = isort + black +# + + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + # # Sample Tests # diff --git a/samples/magics/requirements-test.txt b/samples/magics/requirements-test.txt index c5864d4f7..d771b647d 100644 --- a/samples/magics/requirements-test.txt +++ b/samples/magics/requirements-test.txt @@ -1,3 +1,3 @@ google-cloud-testutils==1.3.1 -pytest==7.1.1 +pytest==7.1.2 mock==4.0.3 diff --git a/samples/magics/requirements.txt b/samples/magics/requirements.txt index 94ce22b00..decfc4764 100644 --- a/samples/magics/requirements.txt +++ b/samples/magics/requirements.txt @@ -1,13 +1,13 @@ db-dtypes==1.0.0 -google-cloud-bigquery-storage==2.13.0 +google-cloud-bigquery-storage==2.13.1 google-auth-oauthlib==0.5.1 -grpcio==1.44.0 +grpcio==1.46.0 ipython===7.31.1; python_version == '3.7' ipython===8.0.1; python_version == '3.8' -ipython==8.2.0; python_version >= '3.9' -matplotlib==3.5.1 +ipython==8.3.0; python_version >= '3.9' +matplotlib==3.5.2 pandas===1.3.5; python_version == '3.7' -pandas==1.4.1; python_version >= '3.8' +pandas==1.4.2; python_version >= '3.8' pyarrow==7.0.0 pytz==2022.1 -typing-extensions==4.1.1 +typing-extensions==4.2.0 diff --git a/samples/snippets/create_table_external_hive_partitioned.py b/samples/snippets/create_table_external_hive_partitioned.py index 1170c57da..aecf8ca4c 100644 --- a/samples/snippets/create_table_external_hive_partitioned.py +++ b/samples/snippets/create_table_external_hive_partitioned.py @@ -50,7 +50,7 @@ def create_table_external_hive_partitioned(table_id: str) -> "bigquery.Table": external_config.autodetect = True # Configure partitioning options. - hive_partitioning_opts = bigquery.external_config.HivePartitioningOptions() + hive_partitioning_opts = bigquery.HivePartitioningOptions() # The layout of the files in here is compatible with the layout requirements for hive partitioning, # so we can add an optional Hive partitioning configuration to leverage the object paths for deriving diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 25f87a215..a40410b56 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -30,6 +30,7 @@ # WARNING - WARNING - WARNING - WARNING - WARNING BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" # Copy `noxfile_config.py` to your directory and modify it instead. @@ -168,12 +169,33 @@ def lint(session: nox.sessions.Session) -> None: @nox.session def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" session.install(BLACK_VERSION) python_files = [path for path in os.listdir(".") if path.endswith(".py")] session.run("black", *python_files) +# +# format = isort + black +# + + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + # # Sample Tests # diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index c5864d4f7..d771b647d 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -1,3 +1,3 @@ google-cloud-testutils==1.3.1 -pytest==7.1.1 +pytest==7.1.2 mock==4.0.3 diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 94ce22b00..decfc4764 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,13 +1,13 @@ db-dtypes==1.0.0 -google-cloud-bigquery-storage==2.13.0 +google-cloud-bigquery-storage==2.13.1 google-auth-oauthlib==0.5.1 -grpcio==1.44.0 +grpcio==1.46.0 ipython===7.31.1; python_version == '3.7' ipython===8.0.1; python_version == '3.8' -ipython==8.2.0; python_version >= '3.9' -matplotlib==3.5.1 +ipython==8.3.0; python_version >= '3.9' +matplotlib==3.5.2 pandas===1.3.5; python_version == '3.7' -pandas==1.4.1; python_version >= '3.8' +pandas==1.4.2; python_version >= '3.8' pyarrow==7.0.0 pytz==2022.1 -typing-extensions==4.1.1 +typing-extensions==4.2.0 diff --git a/samples/undelete_table.py b/samples/undelete_table.py index c230a9230..5ae345247 100644 --- a/samples/undelete_table.py +++ b/samples/undelete_table.py @@ -28,7 +28,7 @@ def undelete_table(table_id: str, recovered_table_id: str) -> None: # table_id = "your-project.your_dataset.your_table" # TODO(developer): Choose a new table ID for the recovered table data. - # recovery_table_id = "your-project.your_dataset.your_table_recovered" + # recovered_table_id = "your-project.your_dataset.your_table_recovered" # TODO(developer): Choose an appropriate snapshot point as epoch # milliseconds. For this example, we choose the current time as we're about diff --git a/scripts/readme-gen/readme_gen.py b/scripts/readme-gen/readme_gen.py index d309d6e97..91b59676b 100644 --- a/scripts/readme-gen/readme_gen.py +++ b/scripts/readme-gen/readme_gen.py @@ -28,7 +28,10 @@ jinja_env = jinja2.Environment( trim_blocks=True, loader=jinja2.FileSystemLoader( - os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates')))) + os.path.abspath(os.path.join(os.path.dirname(__file__), "templates")) + ), + autoescape=True, +) README_TMPL = jinja_env.get_template('README.tmpl.rst') diff --git a/setup.py b/setup.py index 86eb2d41d..52ffac019 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ "packaging >= 14.3", "protobuf >= 3.12.0", # For the legacy proto-based types. "python-dateutil >= 2.7.2, <3.0dev", - "pyarrow >= 3.0.0, < 8.0dev", + "pyarrow >= 3.0.0, < 9.0dev", "requests >= 2.18.0, < 3.0.0dev", ] extras = { diff --git a/tests/unit/test__helpers.py b/tests/unit/test__helpers.py index 885e773d3..2e714c707 100644 --- a/tests/unit/test__helpers.py +++ b/tests/unit/test__helpers.py @@ -1288,3 +1288,29 @@ def test_decimal_as_float_api_repr(): "parameterValue": {"value": 42.0}, "name": "x", } + + +class Test__get_bigquery_host(unittest.TestCase): + @staticmethod + def _call_fut(): + from google.cloud.bigquery._helpers import _get_bigquery_host + + return _get_bigquery_host() + + def test_wo_env_var(self): + from google.cloud.bigquery._helpers import _DEFAULT_HOST + + with mock.patch("os.environ", {}): + host = self._call_fut() + + self.assertEqual(host, _DEFAULT_HOST) + + def test_w_env_var(self): + from google.cloud.bigquery._helpers import BIGQUERY_EMULATOR_HOST + + HOST = "https://api.example.com" + + with mock.patch("os.environ", {BIGQUERY_EMULATOR_HOST: HOST}): + host = self._call_fut() + + self.assertEqual(host, HOST) diff --git a/tests/unit/test_create_dataset.py b/tests/unit/test_create_dataset.py index 67b21225d..81af52261 100644 --- a/tests/unit/test_create_dataset.py +++ b/tests/unit/test_create_dataset.py @@ -109,7 +109,10 @@ def test_create_dataset_w_attrs(client, PROJECT, DS_ID): "friendlyName": FRIENDLY_NAME, "location": LOCATION, "defaultTableExpirationMs": "3600", - "access": [{"role": "OWNER", "userByEmail": USER_EMAIL}, {"view": VIEW}], + "access": [ + {"role": "OWNER", "userByEmail": USER_EMAIL}, + {"view": VIEW, "role": None}, + ], "labels": LABELS, }, timeout=DEFAULT_TIMEOUT, diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index c554782bf..856674daf 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -15,14 +15,20 @@ import unittest import mock +from google.cloud.bigquery.routine.routine import Routine, RoutineReference import pytest +from google.cloud.bigquery.dataset import ( + AccessEntry, + Dataset, + DatasetReference, + Table, + TableReference, +) class TestAccessEntry(unittest.TestCase): @staticmethod def _get_target_class(): - from google.cloud.bigquery.dataset import AccessEntry - return AccessEntry def _make_one(self, *args, **kw): @@ -34,16 +40,6 @@ def test_ctor_defaults(self): self.assertEqual(entry.entity_type, "userByEmail") self.assertEqual(entry.entity_id, "phred@example.com") - def test_ctor_bad_entity_type(self): - with self.assertRaises(ValueError): - self._make_one(None, "unknown", None) - - def test_ctor_view_with_role(self): - role = "READER" - entity_type = "view" - with self.assertRaises(ValueError): - self._make_one(role, entity_type, None) - def test_ctor_view_success(self): role = None entity_type = "view" @@ -53,12 +49,6 @@ def test_ctor_view_success(self): self.assertEqual(entry.entity_type, entity_type) self.assertEqual(entry.entity_id, entity_id) - def test_ctor_routine_with_role(self): - role = "READER" - entity_type = "routine" - with self.assertRaises(ValueError): - self._make_one(role, entity_type, None) - def test_ctor_routine_success(self): role = None entity_type = "routine" @@ -68,12 +58,6 @@ def test_ctor_routine_success(self): self.assertEqual(entry.entity_type, entity_type) self.assertEqual(entry.entity_id, entity_id) - def test_ctor_nonview_without_role(self): - role = None - entity_type = "userByEmail" - with self.assertRaises(ValueError): - self._make_one(role, entity_type, None) - def test___eq___role_mismatch(self): entry = self._make_one("OWNER", "userByEmail", "phred@example.com") other = self._make_one("WRITER", "userByEmail", "phred@example.com") @@ -127,7 +111,7 @@ def test_to_api_repr_view(self): } entry = self._make_one(None, "view", view) resource = entry.to_api_repr() - exp_resource = {"view": view} + exp_resource = {"view": view, "role": None} self.assertEqual(resource, exp_resource) def test_to_api_repr_routine(self): @@ -136,9 +120,10 @@ def test_to_api_repr_routine(self): "datasetId": "my_dataset", "routineId": "my_routine", } + entry = self._make_one(None, "routine", routine) resource = entry.to_api_repr() - exp_resource = {"routine": routine} + exp_resource = {"routine": routine, "role": None} self.assertEqual(resource, exp_resource) def test_to_api_repr_dataset(self): @@ -148,21 +133,9 @@ def test_to_api_repr_dataset(self): } entry = self._make_one(None, "dataset", dataset) resource = entry.to_api_repr() - exp_resource = {"dataset": dataset} + exp_resource = {"dataset": dataset, "role": None} self.assertEqual(resource, exp_resource) - def test_to_api_w_incorrect_role(self): - dataset = { - "dataset": { - "projectId": "my-project", - "datasetId": "my_dataset", - "tableId": "my_table", - }, - "target_type": "VIEW", - } - with self.assertRaises(ValueError): - self._make_one("READER", "dataset", dataset) - def test_from_api_repr(self): resource = {"role": "OWNER", "userByEmail": "salmon@example.com"} entry = self._get_target_class().from_api_repr(resource) @@ -198,6 +171,311 @@ def test_from_api_repr_entries_w_extra_keys(self): with self.assertRaises(ValueError): self._get_target_class().from_api_repr(resource) + def test_view_getter_setter(self): + view = { + "projectId": "my_project", + "datasetId": "my_dataset", + "tableId": "my_table", + } + view_ref = TableReference.from_api_repr(view) + entry = self._make_one(None) + entry.view = view + resource = entry.to_api_repr() + exp_resource = {"view": view, "role": None} + self.assertEqual(entry.view, view_ref) + self.assertEqual(resource, exp_resource) + + def test_view_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.view, None) + + def test_view_getter_setter_string(self): + project = "my_project" + dataset = "my_dataset" + table = "my_table" + view = { + "projectId": project, + "datasetId": dataset, + "tableId": table, + } + entry = self._make_one(None) + entry.view = f"{project}.{dataset}.{table}" + resource = entry.to_api_repr() + exp_resource = {"view": view, "role": None} + self.assertEqual(resource, exp_resource) + + def test_view_getter_setter_table(self): + project = "my_project" + dataset = "my_dataset" + table = "my_table" + view = { + "projectId": project, + "datasetId": dataset, + "tableId": table, + } + view_ref = Table.from_string(f"{project}.{dataset}.{table}") + entry = self._make_one(None) + entry.view = view_ref + resource = entry.to_api_repr() + exp_resource = {"view": view, "role": None} + self.assertEqual(resource, exp_resource) + + def test_view_getter_setter_table_ref(self): + project = "my_project" + dataset = "my_dataset" + table = "my_table" + view = { + "projectId": project, + "datasetId": dataset, + "tableId": table, + } + view_ref = TableReference.from_string(f"{project}.{dataset}.{table}") + entry = self._make_one(None) + entry.view = view_ref + resource = entry.to_api_repr() + exp_resource = {"view": view, "role": None} + self.assertEqual(resource, exp_resource) + + def test_view_getter_setter_incorrect_role(self): + view = { + "projectId": "my_project", + "datasetId": "my_dataset", + "tableId": "my_table", + } + view_ref = TableReference.from_api_repr(view) + entry = self._make_one("READER") + with self.assertRaises(ValueError): + entry.view = view_ref + + def test_dataset_getter_setter(self): + dataset = {"projectId": "my-project", "datasetId": "my_dataset"} + entry = self._make_one(None) + entry.dataset = dataset + resource = entry.to_api_repr() + exp_resource = { + "dataset": {"dataset": dataset, "targetTypes": None}, + "role": None, + } + dataset_ref = DatasetReference.from_api_repr(dataset) + prop = entry.dataset + self.assertEqual(resource, exp_resource) + self.assertEqual(prop, dataset_ref) + + def test_dataset_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.dataset, None) + + def test_dataset_getter_setter_string(self): + project = "my-project" + dataset_id = "my_dataset" + dataset = { + "projectId": project, + "datasetId": dataset_id, + } + entry = self._make_one(None) + string_ref = f"{project}.{dataset_id}" + entry.dataset = string_ref + resource = entry.to_api_repr() + exp_resource = { + "dataset": {"dataset": dataset, "targetTypes": None}, + "role": None, + } + self.assertEqual(resource, exp_resource) + + def test_dataset_getter_setter_dataset_ref(self): + project = "my-project" + dataset_id = "my_dataset" + dataset_ref = DatasetReference(project, dataset_id) + entry = self._make_one(None) + entry.dataset = dataset_ref + resource = entry.to_api_repr() + exp_resource = { + "dataset": {"dataset": dataset_ref, "targetTypes": None}, + "role": None, + } + self.assertEqual(resource, exp_resource) + + def test_dataset_getter_setter_dataset(self): + project = "my-project" + dataset_id = "my_dataset" + dataset_repr = { + "projectId": project, + "datasetId": dataset_id, + } + dataset = Dataset(f"{project}.{dataset_id}") + entry = self._make_one(None) + entry.dataset = dataset + resource = entry.to_api_repr() + exp_resource = { + "role": None, + "dataset": {"dataset": dataset_repr, "targetTypes": None}, + } + self.assertEqual(resource, exp_resource) + + def test_dataset_getter_setter_incorrect_role(self): + dataset = {"dataset": {"projectId": "my-project", "datasetId": "my_dataset"}} + entry = self._make_one("READER") + with self.assertRaises(ValueError): + entry.dataset = dataset + + def test_routine_getter_setter(self): + routine = { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + } + entry = self._make_one(None) + entry.routine = routine + resource = entry.to_api_repr() + exp_resource = {"routine": routine, "role": None} + self.assertEqual(resource, exp_resource) + + def test_routine_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.routine, None) + + def test_routine_getter_setter_string(self): + project = "my-project" + dataset_id = "my_dataset" + routine_id = "my_routine" + routine = { + "projectId": project, + "datasetId": dataset_id, + "routineId": routine_id, + } + entry = self._make_one(None) + entry.routine = f"{project}.{dataset_id}.{routine_id}" + resource = entry.to_api_repr() + exp_resource = { + "routine": routine, + "role": None, + } + self.assertEqual(resource, exp_resource) + + def test_routine_getter_setter_routine_ref(self): + routine = { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + } + entry = self._make_one(None) + entry.routine = RoutineReference.from_api_repr(routine) + resource = entry.to_api_repr() + exp_resource = { + "routine": routine, + "role": None, + } + self.assertEqual(resource, exp_resource) + + def test_routine_getter_setter_routine(self): + routine = { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + } + routine_ref = RoutineReference.from_api_repr(routine) + entry = self._make_one(None) + entry.routine = Routine(routine_ref) + resource = entry.to_api_repr() + exp_resource = { + "routine": routine, + "role": None, + } + self.assertEqual(entry.routine, routine_ref) + self.assertEqual(resource, exp_resource) + + def test_routine_getter_setter_incorrect_role(self): + routine = { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + } + entry = self._make_one("READER") + with self.assertRaises(ValueError): + entry.routine = routine + + def test_group_by_email_getter_setter(self): + email = "cloud-developer-relations@google.com" + entry = self._make_one(None) + entry.group_by_email = email + resource = entry.to_api_repr() + exp_resource = {"groupByEmail": email, "role": None} + self.assertEqual(entry.group_by_email, email) + self.assertEqual(resource, exp_resource) + + def test_group_by_email_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.group_by_email, None) + + def test_user_by_email_getter_setter(self): + email = "cloud-developer-relations@google.com" + entry = self._make_one(None) + entry.user_by_email = email + resource = entry.to_api_repr() + exp_resource = {"userByEmail": email, "role": None} + self.assertEqual(entry.user_by_email, email) + self.assertEqual(resource, exp_resource) + + def test_user_by_email_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.user_by_email, None) + + def test_domain_setter(self): + domain = "my_domain" + entry = self._make_one(None) + entry.domain = domain + resource = entry.to_api_repr() + exp_resource = {"domain": domain, "role": None} + self.assertEqual(entry.domain, domain) + self.assertEqual(resource, exp_resource) + + def test_domain_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.domain, None) + + def test_special_group_getter_setter(self): + special_group = "my_special_group" + entry = self._make_one(None) + entry.special_group = special_group + resource = entry.to_api_repr() + exp_resource = {"specialGroup": special_group, "role": None} + self.assertEqual(entry.special_group, special_group) + self.assertEqual(resource, exp_resource) + + def test_special_group_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.special_group, None) + + def test_role_getter_setter(self): + role = "READER" + entry = self._make_one(None) + entry.role = role + resource = entry.to_api_repr() + exp_resource = {"role": role} + self.assertEqual(resource, exp_resource) + + def test_role_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.role, None) + + def test_dataset_target_types_getter_setter(self): + target_types = ["VIEWS"] + entry = self._make_one(None) + entry.dataset_target_types = target_types + self.assertEqual(entry.dataset_target_types, target_types) + + def test_dataset_target_types_getter_setter_none(self): + entry = self._make_one(None) + self.assertEqual(entry.dataset_target_types, None) + + def test_dataset_target_types_getter_setter_w_dataset(self): + dataset = {"projectId": "my-project", "datasetId": "my_dataset"} + target_types = ["VIEWS"] + entry = self._make_one(None) + entry.dataset = dataset + entry.dataset_target_types = target_types + self.assertEqual(entry.dataset_target_types, target_types) + class TestDatasetReference(unittest.TestCase): @staticmethod diff --git a/tests/unit/test_table.py b/tests/unit/test_table.py index 66bc1d3db..ba35b2297 100644 --- a/tests/unit/test_table.py +++ b/tests/unit/test_table.py @@ -27,6 +27,8 @@ import google.api_core.exceptions +from google.cloud.bigquery.table import TableReference + from google.cloud import bigquery_storage from google.cloud.bigquery_storage_v1.services.big_query_read.transports import ( grpc as big_query_read_grpc_transport, @@ -1410,6 +1412,11 @@ def test___repr__(self): ) self.assertEqual(repr(table1), expected) + def test___str__(self): + dataset = DatasetReference("project1", "dataset1") + table1 = self._make_one(TableReference(dataset, "table1")) + self.assertEqual(str(table1), "project1.dataset1.table1") + class Test_row_from_mapping(unittest.TestCase, _SchemaBase):