Skip to content

Commit 226cdf1

Browse files
authored
Add Routines API. (#8491)
* Add Routines API. Adds support for managing permanent functions in BigQuery, such as scalar UDFs and stored procedures. At present, only scalar UDF functionality is available. Routines are registered as resources inside of datasets, and allow expected CRUD operations. Currently, routines do not support partial updates. See: https://cloud.google.com/bigquery/docs/reference/rest/v2/routines * Add QueryJob.ddl_target_routine property. Adjust docstrings.
1 parent c737b74 commit 226cdf1

23 files changed

+2100
-5
lines changed

bigquery/docs/reference.rst

+10
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ Model
101101
model.Model
102102
model.ModelReference
103103

104+
Routine
105+
=======
106+
107+
.. autosummary::
108+
:toctree: generated
109+
110+
routine.Routine
111+
routine.RoutineArgument
112+
routine.RoutineReference
113+
104114
Schema
105115
======
106116

bigquery/google/cloud/bigquery/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
from google.cloud.bigquery.query import StructQueryParameter
6868
from google.cloud.bigquery.query import UDFResource
6969
from google.cloud.bigquery.retry import DEFAULT_RETRY
70+
from google.cloud.bigquery.routine import Routine
71+
from google.cloud.bigquery.routine import RoutineArgument
72+
from google.cloud.bigquery.routine import RoutineReference
7073
from google.cloud.bigquery.schema import SchemaField
7174
from google.cloud.bigquery.table import EncryptionConfiguration
7275
from google.cloud.bigquery.table import Table
@@ -105,6 +108,10 @@
105108
# Models
106109
"Model",
107110
"ModelReference",
111+
# Routines
112+
"Routine",
113+
"RoutineArgument",
114+
"RoutineReference",
108115
# Shared helpers
109116
"SchemaField",
110117
"UDFResource",

bigquery/google/cloud/bigquery/client.py

+222-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
from google.cloud.bigquery.model import ModelReference
5959
from google.cloud.bigquery.query import _QueryResults
6060
from google.cloud.bigquery.retry import DEFAULT_RETRY
61+
from google.cloud.bigquery.routine import Routine
62+
from google.cloud.bigquery.routine import RoutineReference
6163
from google.cloud.bigquery.schema import SchemaField
6264
from google.cloud.bigquery.table import _table_arg_to_table
6365
from google.cloud.bigquery.table import _table_arg_to_table_ref
@@ -374,6 +376,41 @@ def create_dataset(self, dataset, exists_ok=False, retry=DEFAULT_RETRY):
374376
raise
375377
return self.get_dataset(dataset.reference, retry=retry)
376378

379+
def create_routine(self, routine, exists_ok=False, retry=DEFAULT_RETRY):
380+
"""[Beta] Create a routine via a POST request.
381+
382+
See
383+
https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/insert
384+
385+
Args:
386+
routine (:class:`~google.cloud.bigquery.routine.Routine`):
387+
A :class:`~google.cloud.bigquery.routine.Routine` to create.
388+
The dataset that the routine belongs to must already exist.
389+
exists_ok (bool):
390+
Defaults to ``False``. If ``True``, ignore "already exists"
391+
errors when creating the routine.
392+
retry (google.api_core.retry.Retry):
393+
Optional. How to retry the RPC.
394+
395+
Returns:
396+
google.cloud.bigquery.routine.Routine:
397+
A new ``Routine`` returned from the service.
398+
"""
399+
reference = routine.reference
400+
path = "/projects/{}/datasets/{}/routines".format(
401+
reference.project, reference.dataset_id
402+
)
403+
resource = routine.to_api_repr()
404+
try:
405+
api_response = self._call_api(
406+
retry, method="POST", path=path, data=resource
407+
)
408+
return Routine.from_api_repr(api_response)
409+
except google.api_core.exceptions.Conflict:
410+
if not exists_ok:
411+
raise
412+
return self.get_routine(routine.reference, retry=retry)
413+
377414
def create_table(self, table, exists_ok=False, retry=DEFAULT_RETRY):
378415
"""API call: create a table via a PUT request
379416
@@ -472,6 +509,34 @@ def get_model(self, model_ref, retry=DEFAULT_RETRY):
472509
api_response = self._call_api(retry, method="GET", path=model_ref.path)
473510
return Model.from_api_repr(api_response)
474511

512+
def get_routine(self, routine_ref, retry=DEFAULT_RETRY):
513+
"""[Beta] Get the routine referenced by ``routine_ref``.
514+
515+
Args:
516+
routine_ref (Union[ \
517+
:class:`~google.cloud.bigquery.routine.Routine`, \
518+
:class:`~google.cloud.bigquery.routine.RoutineReference`, \
519+
str, \
520+
]):
521+
A reference to the routine to fetch from the BigQuery API. If
522+
a string is passed in, this method attempts to create a
523+
reference from a string using
524+
:func:`google.cloud.bigquery.routine.RoutineReference.from_string`.
525+
retry (:class:`google.api_core.retry.Retry`):
526+
(Optional) How to retry the API call.
527+
528+
Returns:
529+
google.cloud.bigquery.routine.Routine:
530+
A ``Routine`` instance.
531+
"""
532+
if isinstance(routine_ref, str):
533+
routine_ref = RoutineReference.from_string(
534+
routine_ref, default_project=self.project
535+
)
536+
537+
api_response = self._call_api(retry, method="GET", path=routine_ref.path)
538+
return Routine.from_api_repr(api_response)
539+
475540
def get_table(self, table, retry=DEFAULT_RETRY):
476541
"""Fetch the table referenced by ``table``.
477542
@@ -537,7 +602,7 @@ def update_model(self, model, fields, retry=DEFAULT_RETRY):
537602
538603
Use ``fields`` to specify which fields to update. At least one field
539604
must be provided. If a field is listed in ``fields`` and is ``None``
540-
in ``model``, it will be deleted.
605+
in ``model``, the field value will be deleted.
541606
542607
If ``model.etag`` is not ``None``, the update will only succeed if
543608
the model on the server has the same ETag. Thus reading a model with
@@ -567,12 +632,58 @@ def update_model(self, model, fields, retry=DEFAULT_RETRY):
567632
)
568633
return Model.from_api_repr(api_response)
569634

635+
def update_routine(self, routine, fields, retry=DEFAULT_RETRY):
636+
"""[Beta] Change some fields of a routine.
637+
638+
Use ``fields`` to specify which fields to update. At least one field
639+
must be provided. If a field is listed in ``fields`` and is ``None``
640+
in ``routine``, the field value will be deleted.
641+
642+
.. warning::
643+
During beta, partial updates are not supported. You must provide
644+
all fields in the resource.
645+
646+
If :attr:`~google.cloud.bigquery.routine.Routine.etag` is not
647+
``None``, the update will only succeed if the resource on the server
648+
has the same ETag. Thus reading a routine with
649+
:func:`~google.cloud.bigquery.client.Client.get_routine`, changing
650+
its fields, and then passing it to this method will ensure that the
651+
changes will only be saved if no modifications to the resource
652+
occurred since the read.
653+
654+
Args:
655+
routine (google.cloud.bigquery.routine.Routine): The routine to update.
656+
fields (Sequence[str]):
657+
The fields of ``routine`` to change, spelled as the
658+
:class:`~google.cloud.bigquery.routine.Routine` properties
659+
(e.g. ``type_``).
660+
retry (google.api_core.retry.Retry):
661+
(Optional) A description of how to retry the API call.
662+
663+
Returns:
664+
google.cloud.bigquery.routine.Routine:
665+
The routine resource returned from the API call.
666+
"""
667+
partial = routine._build_resource(fields)
668+
if routine.etag:
669+
headers = {"If-Match": routine.etag}
670+
else:
671+
headers = None
672+
673+
# TODO: remove when routines update supports partial requests.
674+
partial["routineReference"] = routine.reference.to_api_repr()
675+
676+
api_response = self._call_api(
677+
retry, method="PUT", path=routine.path, data=partial, headers=headers
678+
)
679+
return Routine.from_api_repr(api_response)
680+
570681
def update_table(self, table, fields, retry=DEFAULT_RETRY):
571682
"""Change some fields of a table.
572683
573684
Use ``fields`` to specify which fields to update. At least one field
574685
must be provided. If a field is listed in ``fields`` and is ``None``
575-
in ``table``, it will be deleted.
686+
in ``table``, the field value will be deleted.
576687
577688
If ``table.etag`` is not ``None``, the update will only succeed if
578689
the table on the server has the same ETag. Thus reading a table with
@@ -660,6 +771,64 @@ def list_models(
660771
result.dataset = dataset
661772
return result
662773

774+
def list_routines(
775+
self, dataset, max_results=None, page_token=None, retry=DEFAULT_RETRY
776+
):
777+
"""[Beta] List routines in the dataset.
778+
779+
See
780+
https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/list
781+
782+
Args:
783+
dataset (Union[ \
784+
:class:`~google.cloud.bigquery.dataset.Dataset`, \
785+
:class:`~google.cloud.bigquery.dataset.DatasetReference`, \
786+
str, \
787+
]):
788+
A reference to the dataset whose routines to list from the
789+
BigQuery API. If a string is passed in, this method attempts
790+
to create a dataset reference from a string using
791+
:func:`google.cloud.bigquery.dataset.DatasetReference.from_string`.
792+
max_results (int):
793+
(Optional) Maximum number of routines to return. If not passed,
794+
defaults to a value set by the API.
795+
page_token (str):
796+
(Optional) Token representing a cursor into the routines. If
797+
not passed, the API will return the first page of routines. The
798+
token marks the beginning of the iterator to be returned and
799+
the value of the ``page_token`` can be accessed at
800+
``next_page_token`` of the
801+
:class:`~google.api_core.page_iterator.HTTPIterator`.
802+
retry (:class:`google.api_core.retry.Retry`):
803+
(Optional) How to retry the RPC.
804+
805+
Returns:
806+
google.api_core.page_iterator.Iterator:
807+
Iterator of all
808+
:class:`~google.cloud.bigquery.routine.Routine`s contained
809+
within the requested dataset, limited by ``max_results``.
810+
"""
811+
if isinstance(dataset, str):
812+
dataset = DatasetReference.from_string(
813+
dataset, default_project=self.project
814+
)
815+
816+
if not isinstance(dataset, (Dataset, DatasetReference)):
817+
raise TypeError("dataset must be a Dataset, DatasetReference, or string")
818+
819+
path = "{}/routines".format(dataset.path)
820+
result = page_iterator.HTTPIterator(
821+
client=self,
822+
api_request=functools.partial(self._call_api, retry),
823+
path=path,
824+
item_to_value=_item_to_routine,
825+
items_key="routines",
826+
page_token=page_token,
827+
max_results=max_results,
828+
)
829+
result.dataset = dataset
830+
return result
831+
663832
def list_tables(
664833
self, dataset, max_results=None, page_token=None, retry=DEFAULT_RETRY
665834
):
@@ -800,6 +969,42 @@ def delete_model(self, model, retry=DEFAULT_RETRY, not_found_ok=False):
800969
if not not_found_ok:
801970
raise
802971

972+
def delete_routine(self, routine, retry=DEFAULT_RETRY, not_found_ok=False):
973+
"""[Beta] Delete a routine.
974+
975+
See
976+
https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/delete
977+
978+
Args:
979+
model (Union[ \
980+
:class:`~google.cloud.bigquery.routine.Routine`, \
981+
:class:`~google.cloud.bigquery.routine.RoutineReference`, \
982+
str, \
983+
]):
984+
A reference to the routine to delete. If a string is passed
985+
in, this method attempts to create a routine reference from a
986+
string using
987+
:func:`google.cloud.bigquery.routine.RoutineReference.from_string`.
988+
retry (:class:`google.api_core.retry.Retry`):
989+
(Optional) How to retry the RPC.
990+
not_found_ok (bool):
991+
Defaults to ``False``. If ``True``, ignore "not found" errors
992+
when deleting the routine.
993+
"""
994+
if isinstance(routine, str):
995+
routine = RoutineReference.from_string(
996+
routine, default_project=self.project
997+
)
998+
999+
if not isinstance(routine, (Routine, RoutineReference)):
1000+
raise TypeError("routine must be a Routine or a RoutineReference")
1001+
1002+
try:
1003+
self._call_api(retry, method="DELETE", path=routine.path)
1004+
except google.api_core.exceptions.NotFound:
1005+
if not not_found_ok:
1006+
raise
1007+
8031008
def delete_table(self, table, retry=DEFAULT_RETRY, not_found_ok=False):
8041009
"""Delete a table
8051010
@@ -2073,6 +2278,21 @@ def _item_to_model(iterator, resource):
20732278
return Model.from_api_repr(resource)
20742279

20752280

2281+
def _item_to_routine(iterator, resource):
2282+
"""Convert a JSON model to the native object.
2283+
2284+
Args:
2285+
iterator (google.api_core.page_iterator.Iterator):
2286+
The iterator that is currently in use.
2287+
resource (dict):
2288+
An item to be converted to a routine.
2289+
2290+
Returns:
2291+
google.cloud.bigquery.routine.Routine: The next routine in the page.
2292+
"""
2293+
return Routine.from_api_repr(resource)
2294+
2295+
20762296
def _item_to_table(iterator, resource):
20772297
"""Convert a JSON table to the native object.
20782298

bigquery/google/cloud/bigquery/dataset.py

+26
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import google.cloud._helpers
2323
from google.cloud.bigquery import _helpers
2424
from google.cloud.bigquery.model import ModelReference
25+
from google.cloud.bigquery.routine import RoutineReference
2526
from google.cloud.bigquery.table import TableReference
2627

2728

@@ -53,6 +54,25 @@ def _get_model_reference(self, model_id):
5354
)
5455

5556

57+
def _get_routine_reference(self, routine_id):
58+
"""Constructs a RoutineReference.
59+
60+
Args:
61+
routine_id (str): the ID of the routine.
62+
63+
Returns:
64+
google.cloud.bigquery.routine.RoutineReference:
65+
A RoutineReference for a routine in this dataset.
66+
"""
67+
return RoutineReference.from_api_repr(
68+
{
69+
"projectId": self.project,
70+
"datasetId": self.dataset_id,
71+
"routineId": routine_id,
72+
}
73+
)
74+
75+
5676
class AccessEntry(object):
5777
"""Represents grant of an access role to an entity.
5878
@@ -224,6 +244,8 @@ def path(self):
224244

225245
model = _get_model_reference
226246

247+
routine = _get_routine_reference
248+
227249
@classmethod
228250
def from_api_repr(cls, resource):
229251
"""Factory: construct a dataset reference given its API representation
@@ -591,6 +613,8 @@ def _build_resource(self, filter_fields):
591613

592614
model = _get_model_reference
593615

616+
routine = _get_routine_reference
617+
594618
def __repr__(self):
595619
return "Dataset({})".format(repr(self.reference))
596620

@@ -672,3 +696,5 @@ def reference(self):
672696
table = _get_table_reference
673697

674698
model = _get_model_reference
699+
700+
routine = _get_routine_reference

0 commit comments

Comments
 (0)