Skip to content
5 changes: 3 additions & 2 deletions bigframes/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -4234,11 +4234,12 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs):
# At this point column-wise or element-wise remote function operation will
# be performed (not supported).
if hasattr(func, "bigframes_remote_function"):
raise NotImplementedError(
raise formatter.create_exception_with_feedback_link(
NotImplementedError,
"BigFrames DataFrame '.apply()' does not support remote function "
"for column-wise (i.e. with axis=0) operations, please use a "
"regular python function instead. For element-wise operations of "
"the remote function, please use '.map()'."
"the remote function, please use '.map()'.",
)

# Per-column apply
Expand Down
12 changes: 11 additions & 1 deletion bigframes/formatting_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import datetime
import random
from typing import Any, Optional, Union
from typing import Any, Optional, Type, Union

import bigframes_vendored.constants as constants
import google.api_core.exceptions as api_core_exceptions
Expand Down Expand Up @@ -48,6 +48,16 @@ def add_feedback_link(
exception.message = exception.message + f" {constants.FEEDBACK_LINK}"


def create_exception_with_feedback_link(
exception: Type[Exception],
arg: str = "",
):
if arg:
return exception(arg + f" {constants.FEEDBACK_LINK}")

return exception(constants.FEEDBACK_LINK)


def repr_query_job_html(query_job: Optional[bigquery.QueryJob]):
"""Return query job in html format.
Args:
Expand Down
30 changes: 15 additions & 15 deletions bigframes/functions/_function_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
import types
from typing import cast, Tuple, TYPE_CHECKING

from bigframes_vendored import constants
import requests

import bigframes.formatting_helpers as bf_formatting
import bigframes.functions.function_template as bff_template

if TYPE_CHECKING:
Expand Down Expand Up @@ -366,10 +366,9 @@ def create_cloud_function(
headers={"content-type": "application/zip"},
)
if response.status_code != 200:
raise RuntimeError(
"Failed to upload user code. code={}, reason={}, text={}".format(
response.status_code, response.reason, response.text
)
raise bf_formatting.create_exception_with_feedback_link(
RuntimeError,
f"Failed to upload user code. code={response.status_code}, reason={response.reason}, text={response.text}",
)

# Deploy Cloud Function
Expand Down Expand Up @@ -399,10 +398,11 @@ def create_cloud_function(
function.service_config.available_memory = f"{memory_mib}Mi"
if timeout_seconds is not None:
if timeout_seconds > 1200:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"BigQuery remote function can wait only up to 20 minutes"
", see for more details "
"https://cloud.google.com/bigquery/quotas#remote_function_limits."
"https://cloud.google.com/bigquery/quotas#remote_function_limits.",
)
function.service_config.timeout_seconds = timeout_seconds
if max_instance_count is not None:
Expand All @@ -413,10 +413,9 @@ def create_cloud_function(
self._cloud_function_service_account
)
if ingress_settings not in _INGRESS_SETTINGS_MAP:
raise ValueError(
"'{}' not one of the supported ingress settings values: {}".format(
ingress_settings, list(_INGRESS_SETTINGS_MAP)
)
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
f"'{ingress_settings}' not one of the supported ingress settings values: {list(_INGRESS_SETTINGS_MAP)}",
)
function.service_config.ingress_settings = cast(
functions_v2.ServiceConfig.IngressSettings,
Expand Down Expand Up @@ -447,8 +446,8 @@ def create_cloud_function(
# Fetch the endpoint of the just created function
endpoint = self.get_cloud_function_endpoint(cf_name)
if not endpoint:
raise ValueError(
f"Couldn't fetch the http endpoint. {constants.FEEDBACK_LINK}"
raise bf_formatting.create_exception_with_feedback_link(
ValueError, "Couldn't fetch the http endpoint."
)

logger.info(
Expand Down Expand Up @@ -541,8 +540,9 @@ def provision_bq_remote_function(
):
input_args = inspect.getargs(def_.__code__).args
if len(input_args) != len(input_types):
raise ValueError(
"Exactly one type should be provided for every input arg."
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"Exactly one type should be provided for every input arg.",
)
self.create_bq_remote_function(
input_args,
Expand Down
82 changes: 54 additions & 28 deletions bigframes/functions/_function_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
)
import warnings

import bigframes_vendored.constants as constants
import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes
import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes
import bigframes_vendored.ibis.expr.operations.udf as ibis_udf
Expand All @@ -49,6 +48,7 @@
from bigframes import clients
import bigframes.core.compile.ibis_types
import bigframes.exceptions as bfe
import bigframes.formatting_helpers as bf_formatting
import bigframes.series as bf_series

if TYPE_CHECKING:
Expand Down Expand Up @@ -87,9 +87,10 @@ def _resolve_bigquery_client(
if not bigquery_client:
bigquery_client = session.bqclient
if not bigquery_client:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"A bigquery client must be provided, either directly or via "
f"session. {constants.FEEDBACK_LINK}"
"session.",
)
return bigquery_client

Expand All @@ -104,9 +105,10 @@ def _resolve_bigquery_connection_client(
if not bigquery_connection_client:
bigquery_connection_client = session.bqconnectionclient
if not bigquery_connection_client:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"A bigquery connection client must be provided, either "
f"directly or via session. {constants.FEEDBACK_LINK}"
"directly or via session.",
)
return bigquery_connection_client

Expand All @@ -119,9 +121,10 @@ def _resolve_resource_manager_client(
if not resource_manager_client:
resource_manager_client = session.resourcemanagerclient
if not resource_manager_client:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"A resource manager client must be provided, either directly "
f"or via session. {constants.FEEDBACK_LINK}"
"or via session.",
)
return resource_manager_client

Expand Down Expand Up @@ -149,9 +152,10 @@ def _resolve_cloud_functions_client(
if not cloud_functions_client:
cloud_functions_client = session.cloudfunctionsclient
if not cloud_functions_client:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"A cloud functions client must be provided, either directly "
f"or via session. {constants.FEEDBACK_LINK}"
"or via session.",
)
return cloud_functions_client

Expand All @@ -178,14 +182,16 @@ def _resolve_bigquery_connection_id(
bq_connection_id,
) = bigquery_connection.split(".")
if gcp_project_id.casefold() != dataset_ref.project.casefold():
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"The project_id does not match BigQuery connection "
f"gcp_project_id: {dataset_ref.project}."
f"gcp_project_id: {dataset_ref.project}.",
)
if bq_connection_location.casefold() != bq_location.casefold():
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"The location does not match BigQuery connection location: "
f"{bq_location}."
f"{bq_location}.",
)
return bq_connection_id

Expand Down Expand Up @@ -506,9 +512,10 @@ def remote_function(
cloud_function_kms_key_name is not None
and cloud_function_docker_repository is None
):
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"cloud_function_docker_repository must be specified with cloud_function_kms_key_name."
" For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin"
" For more details see https://cloud.google.com/functions/docs/securing/cmek#before_you_begin.",
)

if cloud_function_ingress_settings is None:
Expand All @@ -521,13 +528,25 @@ def remote_function(
)
warnings.warn(msg, category=FutureWarning, stacklevel=2)

if cloud_function_ingress_settings is None:
cloud_function_ingress_settings = "all"
msg = bfe.format_message(
"The `cloud_function_ingress_settings` are set to 'all' by default, "
"which will change to 'internal-only' for enhanced security in future version 2.0 onwards. "
"However, you will be able to explicitly pass cloud_function_ingress_settings='all' if you need. "
"See https://cloud.google.com/functions/docs/networking/network-settings#ingress_settings for details."
)
warnings.warn(msg, category=FutureWarning, stacklevel=2)

bq_connection_manager = session.bqconnectionmanager

def wrapper(func):
nonlocal input_types, output_type

if not callable(func):
raise TypeError("f must be callable, got {}".format(func))
raise bf_formatting.create_exception_with_feedback_link(
TypeError, f"func must be a callable, got {func}"
)

if sys.version_info >= (3, 10):
# Add `eval_str = True` so that deferred annotations are turned into their
Expand All @@ -547,10 +566,11 @@ def wrapper(func):
input_types = []
for parameter in signature.parameters.values():
if (param_type := parameter.annotation) is inspect.Signature.empty:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"'input_types' was not set and parameter "
f"'{parameter.name}' is missing a type annotation. "
"Types are required to use @remote_function."
"Types are required to use @remote_function.",
)
input_types.append(param_type)
elif not isinstance(input_types, collections.abc.Sequence):
Expand All @@ -560,10 +580,11 @@ def wrapper(func):
if (
output_type := signature.return_annotation
) is inspect.Signature.empty:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"'output_type' was not set and function is missing a "
"return type annotation. Types are required to use "
"@remote_function."
"@remote_function.",
)

# The function will actually be receiving a pandas Series, but allow both
Expand Down Expand Up @@ -789,14 +810,15 @@ def udf(
https://pip.pypa.io/en/stable/reference/requirements-file-format/.
"""
if not bigframes.options.experiments.udf:
raise NotImplementedError()
raise bf_formatting.create_exception_with_feedback_link(NotImplementedError)

# Check the Python version.
python_version = _utils.get_python_version()
if python_version not in _MANAGED_FUNC_PYTHON_VERSIONS:
raise RuntimeError(
raise bf_formatting.create_exception_with_feedback_link(
RuntimeError,
f"Python version {python_version} is not supported yet for "
"BigFrames managed function."
"BigFrames managed function.",
)

# Some defaults may be used from the session if not provided otherwise.
Expand All @@ -823,7 +845,9 @@ def wrapper(func):
nonlocal input_types, output_type

if not callable(func):
raise TypeError("f must be callable, got {}".format(func))
raise bf_formatting.create_exception_with_feedback_link(
TypeError, f"func must be a callable, got {func}"
)

# Managed function supports version >= 3.11.
signature_kwargs: Mapping[str, Any] = {"eval_str": True}
Expand All @@ -834,10 +858,11 @@ def wrapper(func):
input_types = []
for parameter in signature.parameters.values():
if (param_type := parameter.annotation) is inspect.Signature.empty:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"'input_types' was not set and parameter "
f"'{parameter.name}' is missing a type annotation. "
"Types are required to use managed function."
"Types are required to use managed function.",
)
input_types.append(param_type)
elif not isinstance(input_types, collections.abc.Sequence):
Expand All @@ -847,10 +872,11 @@ def wrapper(func):
if (
output_type := signature.return_annotation
) is inspect.Signature.empty:
raise ValueError(
raise bf_formatting.create_exception_with_feedback_link(
ValueError,
"'output_type' was not set and function is missing a "
"return type annotation. Types are required to use "
"managed function."
"managed function.",
)

# The function will actually be receiving a pandas Series, but allow
Expand Down
5 changes: 3 additions & 2 deletions bigframes/functions/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import bigframes.core.compile.ibis_types
import bigframes.dtypes
import bigframes.formatting_helpers as bf_formatting

# Naming convention for the function artifacts
_BIGFRAMES_FUNCTION_PREFIX = "bigframes"
Expand Down Expand Up @@ -276,8 +277,8 @@ def get_bigframes_metadata(*, python_output_type: Optional[type] = None) -> str:
get_python_output_type_from_bigframes_metadata(metadata_ser)
!= python_output_type
):
raise ValueError(
f"python_output_type {python_output_type} is not serializable."
raise bf_formatting.create_exception_with_feedback_link(
ValueError, f"python_output_type {python_output_type} is not serializable."
)

return metadata_ser
Expand Down
Loading