google-cloud-bigquery 1.24.0 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/CONTRIBUTING.md +2 -2
- data/LOGGING.md +1 -1
- data/lib/google/cloud/bigquery/convert.rb +0 -4
- data/lib/google/cloud/bigquery/copy_job.rb +1 -0
- data/lib/google/cloud/bigquery/data.rb +2 -2
- data/lib/google/cloud/bigquery/dataset.rb +106 -21
- data/lib/google/cloud/bigquery/dataset/access.rb +112 -14
- data/lib/google/cloud/bigquery/dataset/list.rb +2 -2
- data/lib/google/cloud/bigquery/external.rb +328 -3
- data/lib/google/cloud/bigquery/extract_job.rb +8 -10
- data/lib/google/cloud/bigquery/job.rb +43 -3
- data/lib/google/cloud/bigquery/job/list.rb +4 -4
- data/lib/google/cloud/bigquery/load_job.rb +177 -24
- data/lib/google/cloud/bigquery/model/list.rb +2 -2
- data/lib/google/cloud/bigquery/policy.rb +432 -0
- data/lib/google/cloud/bigquery/project.rb +3 -3
- data/lib/google/cloud/bigquery/project/list.rb +2 -2
- data/lib/google/cloud/bigquery/query_job.rb +25 -14
- data/lib/google/cloud/bigquery/routine.rb +128 -9
- data/lib/google/cloud/bigquery/routine/list.rb +2 -2
- data/lib/google/cloud/bigquery/service.rb +44 -13
- data/lib/google/cloud/bigquery/standard_sql.rb +4 -3
- data/lib/google/cloud/bigquery/table.rb +261 -45
- data/lib/google/cloud/bigquery/table/async_inserter.rb +24 -15
- data/lib/google/cloud/bigquery/table/list.rb +2 -2
- data/lib/google/cloud/bigquery/version.rb +1 -1
- metadata +16 -15
@@ -215,6 +215,17 @@ module Google
|
|
215
215
|
@gapi.statistics.parent_job_id
|
216
216
|
end
|
217
217
|
|
218
|
+
##
|
219
|
+
# An array containing the job resource usage breakdown by reservation, if present. Reservation usage statistics
|
220
|
+
# are only reported for jobs that are executed within reservations. On-demand jobs do not report this data.
|
221
|
+
#
|
222
|
+
# @return [Array<Google::Cloud::Bigquery::Job::ReservationUsage>, nil] The reservation usage, if present.
|
223
|
+
#
|
224
|
+
def reservation_usage
|
225
|
+
return nil unless @gapi.statistics.reservation_usage
|
226
|
+
Array(@gapi.statistics.reservation_usage).map { |g| ReservationUsage.from_gapi g }
|
227
|
+
end
|
228
|
+
|
218
229
|
##
|
219
230
|
# The statistics including stack frames for a child job of a script.
|
220
231
|
#
|
@@ -489,6 +500,30 @@ module Google
|
|
489
500
|
end
|
490
501
|
end
|
491
502
|
|
503
|
+
##
|
504
|
+
# Represents Job resource usage breakdown by reservation.
|
505
|
+
#
|
506
|
+
# @attr_reader [String] name The reservation name or "unreserved" for on-demand resources usage.
|
507
|
+
# @attr_reader [Fixnum] slot_ms The slot-milliseconds the job spent in the given reservation.
|
508
|
+
#
|
509
|
+
class ReservationUsage
|
510
|
+
attr_reader :name
|
511
|
+
attr_reader :slot_ms
|
512
|
+
|
513
|
+
##
|
514
|
+
# @private Creates a new ReservationUsage instance.
|
515
|
+
def initialize name, slot_ms
|
516
|
+
@name = name
|
517
|
+
@slot_ms = slot_ms
|
518
|
+
end
|
519
|
+
|
520
|
+
##
|
521
|
+
# @private New ReservationUsage from a statistics.reservation_usage value.
|
522
|
+
def self.from_gapi gapi
|
523
|
+
new gapi.name, gapi.slot_ms
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
492
527
|
##
|
493
528
|
# Represents statistics for a child job of a script.
|
494
529
|
#
|
@@ -537,7 +572,8 @@ module Google
|
|
537
572
|
# end
|
538
573
|
#
|
539
574
|
class ScriptStatistics
|
540
|
-
attr_reader :evaluation_kind
|
575
|
+
attr_reader :evaluation_kind
|
576
|
+
attr_reader :stack_frames
|
541
577
|
|
542
578
|
##
|
543
579
|
# @private Creates a new ScriptStatistics instance.
|
@@ -547,7 +583,7 @@ module Google
|
|
547
583
|
end
|
548
584
|
|
549
585
|
##
|
550
|
-
# @private New ScriptStatistics from a statistics.script_statistics
|
586
|
+
# @private New ScriptStatistics from a statistics.script_statistics value.
|
551
587
|
def self.from_gapi gapi
|
552
588
|
frames = Array(gapi.stack_frames).map { |g| ScriptStackFrame.from_gapi g }
|
553
589
|
new gapi.evaluation_kind, frames
|
@@ -602,7 +638,11 @@ module Google
|
|
602
638
|
# end
|
603
639
|
#
|
604
640
|
class ScriptStackFrame
|
605
|
-
attr_reader :start_line
|
641
|
+
attr_reader :start_line
|
642
|
+
attr_reader :start_column
|
643
|
+
attr_reader :end_line
|
644
|
+
attr_reader :end_column
|
645
|
+
attr_reader :text
|
606
646
|
|
607
647
|
##
|
608
648
|
# @private Creates a new ScriptStackFrame instance.
|
@@ -72,8 +72,8 @@ module Google
|
|
72
72
|
return nil unless next?
|
73
73
|
ensure_service!
|
74
74
|
next_kwargs = @kwargs.merge token: token
|
75
|
-
next_gapi = @service.list_jobs
|
76
|
-
self.class.from_gapi next_gapi, @service, next_kwargs
|
75
|
+
next_gapi = @service.list_jobs(**next_kwargs)
|
76
|
+
self.class.from_gapi next_gapi, @service, **next_kwargs
|
77
77
|
end
|
78
78
|
|
79
79
|
##
|
@@ -121,12 +121,12 @@ module Google
|
|
121
121
|
# puts job.state
|
122
122
|
# end
|
123
123
|
#
|
124
|
-
def all request_limit: nil
|
124
|
+
def all request_limit: nil, &block
|
125
125
|
request_limit = request_limit.to_i if request_limit
|
126
126
|
return enum_for :all, request_limit: request_limit unless block_given?
|
127
127
|
results = self
|
128
128
|
loop do
|
129
|
-
results.each
|
129
|
+
results.each(&block)
|
130
130
|
if request_limit
|
131
131
|
request_limit -= 1
|
132
132
|
break if request_limit.negative?
|
@@ -37,8 +37,8 @@ module Google
|
|
37
37
|
# bigquery = Google::Cloud::Bigquery.new
|
38
38
|
# dataset = bigquery.dataset "my_dataset"
|
39
39
|
#
|
40
|
-
#
|
41
|
-
# load_job = dataset.load_job "my_new_table",
|
40
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
41
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |schema|
|
42
42
|
# schema.string "first_name", mode: :required
|
43
43
|
# schema.record "cities_lived", mode: :repeated do |nested_schema|
|
44
44
|
# nested_schema.string "place", mode: :required
|
@@ -112,8 +112,7 @@ module Google
|
|
112
112
|
# `false` otherwise.
|
113
113
|
#
|
114
114
|
def iso8859_1?
|
115
|
-
|
116
|
-
val == "ISO-8859-1"
|
115
|
+
@gapi.configuration.load.encoding == "ISO-8859-1"
|
117
116
|
end
|
118
117
|
|
119
118
|
##
|
@@ -195,8 +194,7 @@ module Google
|
|
195
194
|
# `NEWLINE_DELIMITED_JSON`, `false` otherwise.
|
196
195
|
#
|
197
196
|
def json?
|
198
|
-
|
199
|
-
val == "NEWLINE_DELIMITED_JSON"
|
197
|
+
@gapi.configuration.load.source_format == "NEWLINE_DELIMITED_JSON"
|
200
198
|
end
|
201
199
|
|
202
200
|
##
|
@@ -218,8 +216,27 @@ module Google
|
|
218
216
|
# `false` otherwise.
|
219
217
|
#
|
220
218
|
def backup?
|
221
|
-
|
222
|
-
|
219
|
+
@gapi.configuration.load.source_format == "DATASTORE_BACKUP"
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# Checks if the source format is ORC.
|
224
|
+
#
|
225
|
+
# @return [Boolean] `true` when the source format is `ORC`,
|
226
|
+
# `false` otherwise.
|
227
|
+
#
|
228
|
+
def orc?
|
229
|
+
@gapi.configuration.load.source_format == "ORC"
|
230
|
+
end
|
231
|
+
|
232
|
+
##
|
233
|
+
# Checks if the source format is Parquet.
|
234
|
+
#
|
235
|
+
# @return [Boolean] `true` when the source format is `PARQUET`,
|
236
|
+
# `false` otherwise.
|
237
|
+
#
|
238
|
+
def parquet?
|
239
|
+
@gapi.configuration.load.source_format == "PARQUET"
|
223
240
|
end
|
224
241
|
|
225
242
|
##
|
@@ -347,6 +364,58 @@ module Google
|
|
347
364
|
nil
|
348
365
|
end
|
349
366
|
|
367
|
+
###
|
368
|
+
# Checks if hive partitioning options are set.
|
369
|
+
#
|
370
|
+
# @see https://cloud.google.com/bigquery/docs/hive-partitioned-loads-gcs Loading externally partitioned data
|
371
|
+
#
|
372
|
+
# @return [Boolean] `true` when hive partitioning options are set, or `false` otherwise.
|
373
|
+
#
|
374
|
+
# @!group Attributes
|
375
|
+
#
|
376
|
+
def hive_partitioning?
|
377
|
+
!@gapi.configuration.load.hive_partitioning_options.nil?
|
378
|
+
end
|
379
|
+
|
380
|
+
###
|
381
|
+
# The mode of hive partitioning to use when reading data. The following modes are supported:
|
382
|
+
#
|
383
|
+
# 1. `AUTO`: automatically infer partition key name(s) and type(s).
|
384
|
+
# 2. `STRINGS`: automatically infer partition key name(s). All types are interpreted as strings.
|
385
|
+
# 3. `CUSTOM`: partition key schema is encoded in the source URI prefix.
|
386
|
+
#
|
387
|
+
# @see https://cloud.google.com/bigquery/docs/hive-partitioned-loads-gcs Loading externally partitioned data
|
388
|
+
#
|
389
|
+
# @return [String, nil] The mode of hive partitioning, or `nil` if not set.
|
390
|
+
#
|
391
|
+
# @!group Attributes
|
392
|
+
#
|
393
|
+
def hive_partitioning_mode
|
394
|
+
@gapi.configuration.load.hive_partitioning_options.mode if hive_partitioning?
|
395
|
+
end
|
396
|
+
|
397
|
+
###
|
398
|
+
# The common prefix for all source uris when hive partition detection is requested. The prefix must end
|
399
|
+
# immediately before the partition key encoding begins. For example, consider files following this data layout:
|
400
|
+
#
|
401
|
+
# ```
|
402
|
+
# gs://bucket/path_to_table/dt=2019-01-01/country=BR/id=7/file.avro
|
403
|
+
# gs://bucket/path_to_table/dt=2018-12-31/country=CA/id=3/file.avro
|
404
|
+
# ```
|
405
|
+
#
|
406
|
+
# When hive partitioning is requested with either `AUTO` or `STRINGS` mode, the common prefix can be either of
|
407
|
+
# `gs://bucket/path_to_table` or `gs://bucket/path_to_table/` (trailing slash does not matter).
|
408
|
+
#
|
409
|
+
# @see https://cloud.google.com/bigquery/docs/hive-partitioned-loads-gcs Loading externally partitioned data
|
410
|
+
#
|
411
|
+
# @return [String, nil] The common prefix for all source uris, or `nil` if not set.
|
412
|
+
#
|
413
|
+
# @!group Attributes
|
414
|
+
#
|
415
|
+
def hive_partitioning_source_uri_prefix
|
416
|
+
@gapi.configuration.load.hive_partitioning_options.source_uri_prefix if hive_partitioning?
|
417
|
+
end
|
418
|
+
|
350
419
|
###
|
351
420
|
# Checks if the destination table will be range partitioned. See [Creating and using integer range partitioned
|
352
421
|
# tables](https://cloud.google.com/bigquery/docs/creating-integer-range-partitions).
|
@@ -537,6 +606,7 @@ module Google
|
|
537
606
|
##
|
538
607
|
# @private Create an Updater object.
|
539
608
|
def initialize gapi
|
609
|
+
super()
|
540
610
|
@updates = []
|
541
611
|
@gapi = gapi
|
542
612
|
@schema = nil
|
@@ -1326,6 +1396,89 @@ module Google
|
|
1326
1396
|
@gapi.configuration.update! labels: val
|
1327
1397
|
end
|
1328
1398
|
|
1399
|
+
##
|
1400
|
+
# Sets the mode of hive partitioning to use when reading data. The following modes are supported:
|
1401
|
+
#
|
1402
|
+
# 1. `auto`: automatically infer partition key name(s) and type(s).
|
1403
|
+
# 2. `strings`: automatically infer partition key name(s). All types are interpreted as strings.
|
1404
|
+
# 3. `custom`: partition key schema is encoded in the source URI prefix.
|
1405
|
+
#
|
1406
|
+
# Not all storage formats support hive partitioning. Requesting hive partitioning on an unsupported format
|
1407
|
+
# will lead to an error. Currently supported types include: `avro`, `csv`, `json`, `orc` and `parquet`.
|
1408
|
+
#
|
1409
|
+
# See {#format=} and {#hive_partitioning_source_uri_prefix=}.
|
1410
|
+
#
|
1411
|
+
# @see https://cloud.google.com/bigquery/docs/hive-partitioned-loads-gcs Loading externally partitioned data
|
1412
|
+
#
|
1413
|
+
# @param [String, Symbol] mode The mode of hive partitioning to use when reading data.
|
1414
|
+
#
|
1415
|
+
# @example
|
1416
|
+
# require "google/cloud/bigquery"
|
1417
|
+
#
|
1418
|
+
# bigquery = Google::Cloud::Bigquery.new
|
1419
|
+
# dataset = bigquery.dataset "my_dataset"
|
1420
|
+
#
|
1421
|
+
# gcs_uri = "gs://cloud-samples-data/bigquery/hive-partitioning-samples/autolayout/*"
|
1422
|
+
# source_uri_prefix = "gs://cloud-samples-data/bigquery/hive-partitioning-samples/autolayout/"
|
1423
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1424
|
+
# job.format = :parquet
|
1425
|
+
# job.hive_partitioning_mode = :auto
|
1426
|
+
# job.hive_partitioning_source_uri_prefix = source_uri_prefix
|
1427
|
+
# end
|
1428
|
+
#
|
1429
|
+
# load_job.wait_until_done!
|
1430
|
+
# load_job.done? #=> true
|
1431
|
+
#
|
1432
|
+
# @!group Attributes
|
1433
|
+
#
|
1434
|
+
def hive_partitioning_mode= mode
|
1435
|
+
@gapi.configuration.load.hive_partitioning_options ||= Google::Apis::BigqueryV2::HivePartitioningOptions.new
|
1436
|
+
@gapi.configuration.load.hive_partitioning_options.mode = mode.to_s.upcase
|
1437
|
+
end
|
1438
|
+
|
1439
|
+
##
|
1440
|
+
# Sets the common prefix for all source uris when hive partition detection is requested. The prefix must end
|
1441
|
+
# immediately before the partition key encoding begins. For example, consider files following this data
|
1442
|
+
# layout:
|
1443
|
+
#
|
1444
|
+
# ```
|
1445
|
+
# gs://bucket/path_to_table/dt=2019-01-01/country=BR/id=7/file.avro
|
1446
|
+
# gs://bucket/path_to_table/dt=2018-12-31/country=CA/id=3/file.avro
|
1447
|
+
# ```
|
1448
|
+
#
|
1449
|
+
# When hive partitioning is requested with either `AUTO` or `STRINGS` mode, the common prefix can be either of
|
1450
|
+
# `gs://bucket/path_to_table` or `gs://bucket/path_to_table/` (trailing slash does not matter).
|
1451
|
+
#
|
1452
|
+
# See {#hive_partitioning_mode=}.
|
1453
|
+
#
|
1454
|
+
# @see https://cloud.google.com/bigquery/docs/hive-partitioned-loads-gcs Loading externally partitioned data
|
1455
|
+
#
|
1456
|
+
# @param [String] source_uri_prefix The common prefix for all source uris.
|
1457
|
+
#
|
1458
|
+
# @example
|
1459
|
+
# require "google/cloud/bigquery"
|
1460
|
+
#
|
1461
|
+
# bigquery = Google::Cloud::Bigquery.new
|
1462
|
+
# dataset = bigquery.dataset "my_dataset"
|
1463
|
+
#
|
1464
|
+
# gcs_uri = "gs://cloud-samples-data/bigquery/hive-partitioning-samples/autolayout/*"
|
1465
|
+
# source_uri_prefix = "gs://cloud-samples-data/bigquery/hive-partitioning-samples/autolayout/"
|
1466
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1467
|
+
# job.format = :parquet
|
1468
|
+
# job.hive_partitioning_mode = :auto
|
1469
|
+
# job.hive_partitioning_source_uri_prefix = source_uri_prefix
|
1470
|
+
# end
|
1471
|
+
#
|
1472
|
+
# load_job.wait_until_done!
|
1473
|
+
# load_job.done? #=> true
|
1474
|
+
#
|
1475
|
+
# @!group Attributes
|
1476
|
+
#
|
1477
|
+
def hive_partitioning_source_uri_prefix= source_uri_prefix
|
1478
|
+
@gapi.configuration.load.hive_partitioning_options ||= Google::Apis::BigqueryV2::HivePartitioningOptions.new
|
1479
|
+
@gapi.configuration.load.hive_partitioning_options.source_uri_prefix = source_uri_prefix
|
1480
|
+
end
|
1481
|
+
|
1329
1482
|
##
|
1330
1483
|
# Sets the field on which to range partition the table. See [Creating and using integer range partitioned
|
1331
1484
|
# tables](https://cloud.google.com/bigquery/docs/creating-integer-range-partitions).
|
@@ -1345,8 +1498,8 @@ module Google
|
|
1345
1498
|
# bigquery = Google::Cloud::Bigquery.new
|
1346
1499
|
# dataset = bigquery.dataset "my_dataset"
|
1347
1500
|
#
|
1348
|
-
#
|
1349
|
-
# load_job = dataset.load_job "my_new_table",
|
1501
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1502
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1350
1503
|
# job.schema do |schema|
|
1351
1504
|
# schema.integer "my_table_id", mode: :required
|
1352
1505
|
# schema.string "my_table_data", mode: :required
|
@@ -1386,8 +1539,8 @@ module Google
|
|
1386
1539
|
# bigquery = Google::Cloud::Bigquery.new
|
1387
1540
|
# dataset = bigquery.dataset "my_dataset"
|
1388
1541
|
#
|
1389
|
-
#
|
1390
|
-
# load_job = dataset.load_job "my_new_table",
|
1542
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1543
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1391
1544
|
# job.schema do |schema|
|
1392
1545
|
# schema.integer "my_table_id", mode: :required
|
1393
1546
|
# schema.string "my_table_data", mode: :required
|
@@ -1427,8 +1580,8 @@ module Google
|
|
1427
1580
|
# bigquery = Google::Cloud::Bigquery.new
|
1428
1581
|
# dataset = bigquery.dataset "my_dataset"
|
1429
1582
|
#
|
1430
|
-
#
|
1431
|
-
# load_job = dataset.load_job "my_new_table",
|
1583
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1584
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1432
1585
|
# job.schema do |schema|
|
1433
1586
|
# schema.integer "my_table_id", mode: :required
|
1434
1587
|
# schema.string "my_table_data", mode: :required
|
@@ -1468,8 +1621,8 @@ module Google
|
|
1468
1621
|
# bigquery = Google::Cloud::Bigquery.new
|
1469
1622
|
# dataset = bigquery.dataset "my_dataset"
|
1470
1623
|
#
|
1471
|
-
#
|
1472
|
-
# load_job = dataset.load_job "my_new_table",
|
1624
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1625
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1473
1626
|
# job.schema do |schema|
|
1474
1627
|
# schema.integer "my_table_id", mode: :required
|
1475
1628
|
# schema.string "my_table_data", mode: :required
|
@@ -1510,8 +1663,8 @@ module Google
|
|
1510
1663
|
# bigquery = Google::Cloud::Bigquery.new
|
1511
1664
|
# dataset = bigquery.dataset "my_dataset"
|
1512
1665
|
#
|
1513
|
-
#
|
1514
|
-
# load_job = dataset.load_job "my_new_table",
|
1666
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1667
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1515
1668
|
# job.time_partitioning_type = "DAY"
|
1516
1669
|
# end
|
1517
1670
|
#
|
@@ -1549,8 +1702,8 @@ module Google
|
|
1549
1702
|
# bigquery = Google::Cloud::Bigquery.new
|
1550
1703
|
# dataset = bigquery.dataset "my_dataset"
|
1551
1704
|
#
|
1552
|
-
#
|
1553
|
-
# load_job = dataset.load_job "my_new_table",
|
1705
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1706
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1554
1707
|
# job.time_partitioning_type = "DAY"
|
1555
1708
|
# job.time_partitioning_field = "dob"
|
1556
1709
|
# job.schema do |schema|
|
@@ -1585,8 +1738,8 @@ module Google
|
|
1585
1738
|
# bigquery = Google::Cloud::Bigquery.new
|
1586
1739
|
# dataset = bigquery.dataset "my_dataset"
|
1587
1740
|
#
|
1588
|
-
#
|
1589
|
-
# load_job = dataset.load_job "my_new_table",
|
1741
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1742
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1590
1743
|
# job.time_partitioning_type = "DAY"
|
1591
1744
|
# job.time_partitioning_expiration = 86_400
|
1592
1745
|
# end
|
@@ -1645,8 +1798,8 @@ module Google
|
|
1645
1798
|
# bigquery = Google::Cloud::Bigquery.new
|
1646
1799
|
# dataset = bigquery.dataset "my_dataset"
|
1647
1800
|
#
|
1648
|
-
#
|
1649
|
-
# load_job = dataset.load_job "my_new_table",
|
1801
|
+
# gcs_uri = "gs://my-bucket/file-name.csv"
|
1802
|
+
# load_job = dataset.load_job "my_new_table", gcs_uri do |job|
|
1650
1803
|
# job.time_partitioning_type = "DAY"
|
1651
1804
|
# job.time_partitioning_field = "dob"
|
1652
1805
|
# job.schema do |schema|
|
@@ -124,12 +124,12 @@ module Google
|
|
124
124
|
# puts model.model_id
|
125
125
|
# end
|
126
126
|
#
|
127
|
-
def all request_limit: nil
|
127
|
+
def all request_limit: nil, &block
|
128
128
|
request_limit = request_limit.to_i if request_limit
|
129
129
|
return enum_for :all, request_limit: request_limit unless block_given?
|
130
130
|
results = self
|
131
131
|
loop do
|
132
|
-
results.each
|
132
|
+
results.each(&block)
|
133
133
|
if request_limit
|
134
134
|
request_limit -= 1
|
135
135
|
break if request_limit.negative?
|
@@ -0,0 +1,432 @@
|
|
1
|
+
# Copyright 2020 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
|
+
# https://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
|
+
|
16
|
+
require "google/apis/bigquery_v2"
|
17
|
+
|
18
|
+
module Google
|
19
|
+
module Cloud
|
20
|
+
module Bigquery
|
21
|
+
##
|
22
|
+
# # Policy
|
23
|
+
#
|
24
|
+
# Represents a Cloud IAM Policy for BigQuery resources.
|
25
|
+
#
|
26
|
+
# A Policy is a collection of bindings. A {Policy::Binding} binds one or more members to a single role. Member
|
27
|
+
# strings can describe user accounts, service accounts, Google groups, and domains. A role string represents a
|
28
|
+
# named list of permissions; each role can be an IAM predefined role or a user-created custom role.
|
29
|
+
#
|
30
|
+
# @see https://cloud.google.com/iam/docs/managing-policies Managing Policies
|
31
|
+
# @see https://cloud.google.com/bigquery/docs/table-access-controls-intro Controlling access to tables
|
32
|
+
#
|
33
|
+
# @attr [String] etag Used to check if the policy has changed since the last request. When you make a request with
|
34
|
+
# an `etag` value, Cloud IAM compares the `etag` value in the request with the existing `etag` value associated
|
35
|
+
# with the policy. It writes the policy only if the `etag` values match.
|
36
|
+
# @attr [Array<Binding>] bindings The bindings in the policy, which may be mutable or frozen depending on the
|
37
|
+
# context. See [Understanding Roles](https://cloud.google.com/iam/docs/understanding-roles) for a list of
|
38
|
+
# primitive and curated roles. See [BigQuery Table ACL
|
39
|
+
# permissions](https://cloud.google.com/bigquery/docs/table-access-controls-intro#permissions) for a list of
|
40
|
+
# values and patterns for members.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# require "google/cloud/bigquery"
|
44
|
+
#
|
45
|
+
# bigquery = Google::Cloud::Bigquery.new
|
46
|
+
# dataset = bigquery.dataset "my_dataset"
|
47
|
+
# table = dataset.table "my_table"
|
48
|
+
# policy = table.policy
|
49
|
+
#
|
50
|
+
# policy.frozen? #=> true
|
51
|
+
# binding_owner = policy.bindings.find { |b| b.role == "roles/owner" }
|
52
|
+
#
|
53
|
+
# binding_owner.role #=> "roles/owner"
|
54
|
+
# binding_owner.members #=> ["user:owner@example.com"]
|
55
|
+
# binding_owner.frozen? #=> true
|
56
|
+
# binding_owner.members.frozen? #=> true
|
57
|
+
#
|
58
|
+
# @example Update mutable bindings in the policy.
|
59
|
+
# require "google/cloud/bigquery"
|
60
|
+
#
|
61
|
+
# bigquery = Google::Cloud::Bigquery.new
|
62
|
+
# dataset = bigquery.dataset "my_dataset"
|
63
|
+
# table = dataset.table "my_table"
|
64
|
+
#
|
65
|
+
# table.update_policy do |p|
|
66
|
+
# p.grant role: "roles/viewer", members: "user:viewer@example.com"
|
67
|
+
# p.revoke role: "roles/editor", members: "user:editor@example.com"
|
68
|
+
# p.revoke role: "roles/owner"
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# @example Iterate over frozen bindings.
|
72
|
+
# require "google/cloud/bigquery"
|
73
|
+
#
|
74
|
+
# bigquery = Google::Cloud::Bigquery.new
|
75
|
+
# dataset = bigquery.dataset "my_dataset"
|
76
|
+
# table = dataset.table "my_table"
|
77
|
+
# policy = table.policy
|
78
|
+
#
|
79
|
+
# policy.frozen? #=> true
|
80
|
+
# policy.bindings.each do |b|
|
81
|
+
# puts b.role
|
82
|
+
# puts b.members
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# @example Update mutable bindings.
|
86
|
+
# require "google/cloud/bigquery"
|
87
|
+
#
|
88
|
+
# bigquery = Google::Cloud::Bigquery.new
|
89
|
+
# dataset = bigquery.dataset "my_dataset"
|
90
|
+
# table = dataset.table "my_table"
|
91
|
+
#
|
92
|
+
# table.update_policy do |p|
|
93
|
+
# p.bindings.each do |b|
|
94
|
+
# b.members.delete_if { |m| m.include? "@example.com" }
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
class Policy
|
99
|
+
attr_reader :etag
|
100
|
+
attr_reader :bindings
|
101
|
+
|
102
|
+
# @private
|
103
|
+
def initialize etag, bindings
|
104
|
+
@etag = etag.freeze
|
105
|
+
@bindings = bindings
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Convenience method adding or updating a binding in the policy. See [Understanding
|
110
|
+
# Roles](https://cloud.google.com/iam/docs/understanding-roles) for a list of primitive and curated roles. See
|
111
|
+
# [BigQuery Table ACL
|
112
|
+
# permissions](https://cloud.google.com/bigquery/docs/table-access-controls-intro#permissions) for a list of
|
113
|
+
# values and patterns for members.
|
114
|
+
#
|
115
|
+
# @param [String] role The role that is bound to members in the binding. For example, `roles/viewer`,
|
116
|
+
# `roles/editor`, or `roles/owner`. Required.
|
117
|
+
# @param [String, Array<String>] members Specifies the identities requesting access for a Cloud Platform
|
118
|
+
# resource. `members` can have the following values. Required.
|
119
|
+
#
|
120
|
+
# * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google
|
121
|
+
# account.
|
122
|
+
# * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google
|
123
|
+
# account or a service account.
|
124
|
+
# * `user:<emailid>`: An email address that represents a specific Google account. For example,
|
125
|
+
# `alice@example.com`.
|
126
|
+
# * `serviceAccount:<emailid>`: An email address that represents a service account. For example,
|
127
|
+
# `my-other-app@appspot.gserviceaccount.com`.
|
128
|
+
# * `group:<emailid>`: An email address that represents a Google group. For example, `admins@example.com`.
|
129
|
+
# * `deleted:user:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a user
|
130
|
+
# that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user
|
131
|
+
# is recovered, this value reverts to `user:<emailid>` and the recovered user retains the role in the
|
132
|
+
# binding.
|
133
|
+
# * `deleted: serviceAccount:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing
|
134
|
+
# a service account that has been recently deleted. For example,
|
135
|
+
# `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted,
|
136
|
+
# this value reverts to `serviceAccount:<emailid>` and the undeleted service account retains the role in
|
137
|
+
# the binding.
|
138
|
+
# * `deleted:group:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a Google
|
139
|
+
# group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the
|
140
|
+
# group is recovered, this value reverts to `group:<emailid>` and the recovered group retains the role in
|
141
|
+
# the binding.
|
142
|
+
# * `domain:<domain>`: The G Suite domain (primary) that represents all the users of that domain. For example,
|
143
|
+
# `google.com` or `example.com`.
|
144
|
+
#
|
145
|
+
# @return [nil]
|
146
|
+
#
|
147
|
+
# @example Grant a role to a member.
|
148
|
+
# require "google/cloud/bigquery"
|
149
|
+
#
|
150
|
+
# bigquery = Google::Cloud::Bigquery.new
|
151
|
+
# dataset = bigquery.dataset "my_dataset"
|
152
|
+
# table = dataset.table "my_table"
|
153
|
+
#
|
154
|
+
# table.update_policy do |p|
|
155
|
+
# p.grant role: "roles/viewer", members: "user:viewer@example.com"
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
def grant role:, members:
|
159
|
+
existing_binding = bindings.find { |b| b.role == role }
|
160
|
+
if existing_binding
|
161
|
+
existing_binding.members.concat Array(members)
|
162
|
+
existing_binding.members.uniq!
|
163
|
+
else
|
164
|
+
bindings << Binding.new(role, members)
|
165
|
+
end
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Convenience method for removing a binding or bindings from the policy. See
|
171
|
+
# [Understanding Roles](https://cloud.google.com/iam/docs/understanding-roles) for a list of primitive and
|
172
|
+
# curated roles. See [BigQuery Table ACL
|
173
|
+
# permissions](https://cloud.google.com/bigquery/docs/table-access-controls-intro#permissions) for a list of
|
174
|
+
# values and patterns for members.
|
175
|
+
#
|
176
|
+
# @param [String] role A role that is bound to members in the policy. For example, `roles/viewer`,
|
177
|
+
# `roles/editor`, or `roles/owner`. Optional.
|
178
|
+
# @param [String, Array<String>] members Specifies the identities receiving access for a Cloud Platform
|
179
|
+
# resource. `members` can have the following values. Optional.
|
180
|
+
#
|
181
|
+
# * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google
|
182
|
+
# account.
|
183
|
+
# * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google
|
184
|
+
# account or a service account.
|
185
|
+
# * `user:<emailid>`: An email address that represents a specific Google account. For example,
|
186
|
+
# `alice@example.com`.
|
187
|
+
# * `serviceAccount:<emailid>`: An email address that represents a service account. For example,
|
188
|
+
# `my-other-app@appspot.gserviceaccount.com`.
|
189
|
+
# * `group:<emailid>`: An email address that represents a Google group. For example, `admins@example.com`.
|
190
|
+
# * `deleted:user:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a user
|
191
|
+
# that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user
|
192
|
+
# is recovered, this value reverts to `user:<emailid>` and the recovered user retains the role in the
|
193
|
+
# binding.
|
194
|
+
# * `deleted: serviceAccount:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing
|
195
|
+
# a service account that has been recently deleted. For example,
|
196
|
+
# `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted,
|
197
|
+
# this value reverts to `serviceAccount:<emailid>` and the undeleted service account retains the role in
|
198
|
+
# the binding.
|
199
|
+
# * `deleted:group:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a Google
|
200
|
+
# group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the
|
201
|
+
# group is recovered, this value reverts to `group:<emailid>` and the recovered group retains the role in
|
202
|
+
# the binding.
|
203
|
+
# * `domain:<domain>`: The G Suite domain (primary) that represents all the users of that domain. For example,
|
204
|
+
# `google.com` or `example.com`.
|
205
|
+
#
|
206
|
+
# @return [nil]
|
207
|
+
#
|
208
|
+
# @example Revoke a role for a member or members.
|
209
|
+
# require "google/cloud/bigquery"
|
210
|
+
#
|
211
|
+
# bigquery = Google::Cloud::Bigquery.new
|
212
|
+
# dataset = bigquery.dataset "my_dataset"
|
213
|
+
# table = dataset.table "my_table"
|
214
|
+
#
|
215
|
+
# table.update_policy do |p|
|
216
|
+
# p.revoke role: "roles/viewer", members: "user:viewer@example.com"
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# @example Revoke a role for all members.
|
220
|
+
# require "google/cloud/bigquery"
|
221
|
+
#
|
222
|
+
# bigquery = Google::Cloud::Bigquery.new
|
223
|
+
# dataset = bigquery.dataset "my_dataset"
|
224
|
+
# table = dataset.table "my_table"
|
225
|
+
#
|
226
|
+
# table.update_policy do |p|
|
227
|
+
# p.revoke role: "roles/viewer"
|
228
|
+
# end
|
229
|
+
#
|
230
|
+
# @example Revoke all roles for a member or members.
|
231
|
+
# require "google/cloud/bigquery"
|
232
|
+
#
|
233
|
+
# bigquery = Google::Cloud::Bigquery.new
|
234
|
+
# dataset = bigquery.dataset "my_dataset"
|
235
|
+
# table = dataset.table "my_table"
|
236
|
+
#
|
237
|
+
# table.update_policy do |p|
|
238
|
+
# p.revoke members: ["user:viewer@example.com", "user:editor@example.com"]
|
239
|
+
# end
|
240
|
+
#
|
241
|
+
def revoke role: nil, members: nil
|
242
|
+
bindings_for_role = role ? bindings.select { |b| b.role == role } : bindings
|
243
|
+
bindings_for_role.each do |b|
|
244
|
+
if members
|
245
|
+
b.members -= Array(members)
|
246
|
+
bindings.delete b if b.members.empty?
|
247
|
+
else
|
248
|
+
bindings.delete b
|
249
|
+
end
|
250
|
+
end
|
251
|
+
nil
|
252
|
+
end
|
253
|
+
|
254
|
+
##
|
255
|
+
# @private Convert the Policy to a Google::Apis::BigqueryV2::Policy.
|
256
|
+
def to_gapi
|
257
|
+
Google::Apis::BigqueryV2::Policy.new(
|
258
|
+
bindings: bindings_to_gapi,
|
259
|
+
etag: etag,
|
260
|
+
version: 1
|
261
|
+
)
|
262
|
+
end
|
263
|
+
|
264
|
+
##
|
265
|
+
# @private Deep freeze the policy including its bindings.
|
266
|
+
def freeze
|
267
|
+
super
|
268
|
+
@bindings.each(&:freeze)
|
269
|
+
@bindings.freeze
|
270
|
+
self
|
271
|
+
end
|
272
|
+
|
273
|
+
##
|
274
|
+
# @private New Policy from a Google::Apis::BigqueryV2::Policy object.
|
275
|
+
def self.from_gapi gapi
|
276
|
+
bindings = Array(gapi.bindings).map do |binding|
|
277
|
+
Binding.new binding.role, binding.members.to_a
|
278
|
+
end
|
279
|
+
new gapi.etag, bindings
|
280
|
+
end
|
281
|
+
|
282
|
+
##
|
283
|
+
# # Policy::Binding
|
284
|
+
#
|
285
|
+
# Represents a Cloud IAM Binding for BigQuery resources within the context of a {Policy}.
|
286
|
+
#
|
287
|
+
# A binding binds one or more members to a single role. Member strings can describe user accounts, service
|
288
|
+
# accounts, Google groups, and domains. A role is a named list of permissions; each role can be an IAM
|
289
|
+
# predefined role or a user-created custom role.
|
290
|
+
#
|
291
|
+
# @see https://cloud.google.com/bigquery/docs/table-access-controls-intro Controlling access to tables
|
292
|
+
#
|
293
|
+
# @attr [String] role The role that is assigned to `members`. For example, `roles/viewer`, `roles/editor`, or
|
294
|
+
# `roles/owner`. Required.
|
295
|
+
# @attr [Array<String>] members Specifies the identities requesting access for a Cloud Platform resource.
|
296
|
+
# `members` can have the following values. Required.
|
297
|
+
#
|
298
|
+
# * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google
|
299
|
+
# account.
|
300
|
+
# * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google
|
301
|
+
# account or a service account.
|
302
|
+
# * `user:<emailid>`: An email address that represents a specific Google account. For example,
|
303
|
+
# `alice@example.com`.
|
304
|
+
# * `serviceAccount:<emailid>`: An email address that represents a service account. For example,
|
305
|
+
# `my-other-app@appspot.gserviceaccount.com`.
|
306
|
+
# * `group:<emailid>`: An email address that represents a Google group. For example, `admins@example.com`.
|
307
|
+
# * `deleted:user:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a user
|
308
|
+
# that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user
|
309
|
+
# is recovered, this value reverts to `user:<emailid>` and the recovered user retains the role in the
|
310
|
+
# binding.
|
311
|
+
# * `deleted: serviceAccount:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing
|
312
|
+
# a service account that has been recently deleted. For example,
|
313
|
+
# `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted,
|
314
|
+
# this value reverts to `serviceAccount:<emailid>` and the undeleted service account retains the role in
|
315
|
+
# the binding.
|
316
|
+
# * `deleted:group:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a Google
|
317
|
+
# group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the
|
318
|
+
# group is recovered, this value reverts to `group:<emailid>` and the recovered group retains the role in
|
319
|
+
# the binding.
|
320
|
+
# * `domain:<domain>`: The G Suite domain (primary) that represents all the users of that domain. For example,
|
321
|
+
# `google.com` or `example.com`.
|
322
|
+
#
|
323
|
+
# @example
|
324
|
+
# require "google/cloud/bigquery"
|
325
|
+
#
|
326
|
+
# bigquery = Google::Cloud::Bigquery.new
|
327
|
+
# dataset = bigquery.dataset "my_dataset"
|
328
|
+
# table = dataset.table "my_table"
|
329
|
+
#
|
330
|
+
# policy = table.policy
|
331
|
+
# binding_owner = policy.bindings.find { |b| b.role == "roles/owner" }
|
332
|
+
#
|
333
|
+
# binding_owner.role #=> "roles/owner"
|
334
|
+
# binding_owner.members #=> ["user:owner@example.com"]
|
335
|
+
#
|
336
|
+
# binding_owner.frozen? #=> true
|
337
|
+
# binding_owner.members.frozen? #=> true
|
338
|
+
#
|
339
|
+
# @example Update mutable bindings.
|
340
|
+
# require "google/cloud/bigquery"
|
341
|
+
#
|
342
|
+
# bigquery = Google::Cloud::Bigquery.new
|
343
|
+
# dataset = bigquery.dataset "my_dataset"
|
344
|
+
# table = dataset.table "my_table"
|
345
|
+
#
|
346
|
+
# table.update_policy do |p|
|
347
|
+
# binding_owner = p.bindings.find { |b| b.role == "roles/owner" }
|
348
|
+
# binding_owner.members.delete_if { |m| m.include? "@example.com" }
|
349
|
+
# end
|
350
|
+
#
|
351
|
+
class Binding
|
352
|
+
attr_accessor :role
|
353
|
+
attr_reader :members
|
354
|
+
|
355
|
+
# @private
|
356
|
+
def initialize role, members
|
357
|
+
members = Array(members).uniq
|
358
|
+
raise ArgumentError, "members cannot be empty" if members.empty?
|
359
|
+
@role = role
|
360
|
+
@members = members
|
361
|
+
end
|
362
|
+
|
363
|
+
##
|
364
|
+
# Sets the binding members.
|
365
|
+
#
|
366
|
+
# @param [Array<String>] new_members Specifies the identities requesting access for a Cloud Platform resource.
|
367
|
+
# `new_members` can have the following values. Required.
|
368
|
+
#
|
369
|
+
# * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google
|
370
|
+
# account.
|
371
|
+
# * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google
|
372
|
+
# account or a service account.
|
373
|
+
# * `user:<emailid>`: An email address that represents a specific Google account. For example,
|
374
|
+
# `alice@example.com`.
|
375
|
+
# * `serviceAccount:<emailid>`: An email address that represents a service account. For example,
|
376
|
+
# `my-other-app@appspot.gserviceaccount.com`.
|
377
|
+
# * `group:<emailid>`: An email address that represents a Google group. For example, `admins@example.com`.
|
378
|
+
# * `deleted:user:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a user
|
379
|
+
# that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user
|
380
|
+
# is recovered, this value reverts to `user:<emailid>` and the recovered user retains the role in the
|
381
|
+
# binding.
|
382
|
+
# * `deleted: serviceAccount:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier)
|
383
|
+
# representing a service account that has been recently deleted. For example,
|
384
|
+
# `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is
|
385
|
+
# undeleted, this value reverts to `serviceAccount:<emailid>` and the undeleted service account retains
|
386
|
+
# the role in the binding.
|
387
|
+
# * `deleted:group:<emailid>?uid=<uniqueid>`: An email address (plus unique identifier) representing a
|
388
|
+
# Google group that has been recently deleted. For example,
|
389
|
+
# `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to
|
390
|
+
# `group:<emailid>` and the recovered group retains the role in the binding.
|
391
|
+
# * `domain:<domain>`: The G Suite domain (primary) that represents all the users of that domain. For
|
392
|
+
# example, `google.com` or `example.com`.
|
393
|
+
#
|
394
|
+
def members= new_members
|
395
|
+
@members = Array(new_members).uniq
|
396
|
+
end
|
397
|
+
|
398
|
+
##
|
399
|
+
# @private Convert the Binding to a Google::Apis::BigqueryV2::Binding.
|
400
|
+
def to_gapi
|
401
|
+
Google::Apis::BigqueryV2::Binding.new role: role, members: members
|
402
|
+
end
|
403
|
+
|
404
|
+
##
|
405
|
+
# @private Deep freeze the policy including its members.
|
406
|
+
def freeze
|
407
|
+
super
|
408
|
+
role.freeze
|
409
|
+
members.each(&:freeze)
|
410
|
+
members.freeze
|
411
|
+
self
|
412
|
+
end
|
413
|
+
|
414
|
+
##
|
415
|
+
# @private New Binding from a Google::Apis::BigqueryV2::Binding object.
|
416
|
+
def self.from_gapi gapi
|
417
|
+
new gapi.etag, gapi.members.to_a
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
protected
|
422
|
+
|
423
|
+
def bindings_to_gapi
|
424
|
+
@bindings.compact.uniq.map do |b|
|
425
|
+
next if b.members.empty?
|
426
|
+
b.to_gapi
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|