Skip to content

Commit f78bfcc

Browse files
authored
[DENG-3526] Support for publishing/cleaning authorized views only (#8406)
1 parent 8553b84 commit f78bfcc

File tree

2 files changed

+176
-6
lines changed

2 files changed

+176
-6
lines changed

bigquery_etl/cli/view.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ def _view_is_valid(v: View) -> bool:
165165
is_flag=True,
166166
help="Don't publish views with labels: {authorized: true} in metadata.yaml",
167167
)
168+
@click.option(
169+
"--authorized-only",
170+
"--authorized_only",
171+
is_flag=True,
172+
help="Only publish views with labels: {authorized: true} in metadata.yaml",
173+
)
168174
@click.option(
169175
"--force",
170176
is_flag=True,
@@ -190,6 +196,7 @@ def publish(
190196
dry_run,
191197
user_facing_only,
192198
skip_authorized,
199+
authorized_only,
193200
force,
194201
add_managed_label,
195202
respect_dryrun_skip,
@@ -200,9 +207,17 @@ def publish(
200207
logging.basicConfig(level=log_level, format="%(levelname)s %(message)s")
201208
except ValueError as e:
202209
raise click.ClickException(f"argument --log-level: {e}")
210+
211+
if skip_authorized and authorized_only:
212+
raise click.ClickException(
213+
"Cannot use both --skip-authorized and --authorized-only"
214+
)
215+
203216
credentials = get_credentials()
204217

205-
views = _collect_views(name, sql_dir, project_id, user_facing_only, skip_authorized)
218+
views = _collect_views(
219+
name, sql_dir, project_id, user_facing_only, skip_authorized, authorized_only
220+
)
206221
if respect_dryrun_skip:
207222
views = [view for view in views if view.path not in DryRun.skipped_files()]
208223
if add_managed_label:
@@ -247,7 +262,9 @@ def _view_has_changes(target_project, credentials, view):
247262
return view.has_changes(target_project, credentials)
248263

249264

250-
def _collect_views(name, sql_dir, project_id, user_facing_only, skip_authorized):
265+
def _collect_views(
266+
name, sql_dir, project_id, user_facing_only, skip_authorized, authorized_only=False
267+
):
251268
view_files = paths_matching_name_pattern(
252269
name, sql_dir, project_id, files=("view.sql",)
253270
)
@@ -267,6 +284,17 @@ def _collect_views(name, sql_dir, project_id, user_facing_only, skip_authorized)
267284
and v.metadata.labels.get("authorized") == ""
268285
)
269286
]
287+
if authorized_only:
288+
views = [
289+
v
290+
for v in views
291+
if (
292+
v.metadata
293+
and v.metadata.labels
294+
# labels with boolean true are translated to ""
295+
and v.metadata.labels.get("authorized") == ""
296+
)
297+
]
270298
return views
271299

272300

@@ -314,7 +342,13 @@ def _collect_views(name, sql_dir, project_id, user_facing_only, skip_authorized)
314342
"--skip-authorized",
315343
"--skip_authorized",
316344
is_flag=True,
317-
help="Don't publish views with labels: {authorized: true} in metadata.yaml",
345+
help="Don't clean views with labels: {authorized: true} in metadata.yaml",
346+
)
347+
@click.option(
348+
"--authorized-only",
349+
"--authorized_only",
350+
is_flag=True,
351+
help="Only clean views with labels: {authorized: true} in metadata.yaml",
318352
)
319353
def clean(
320354
name,
@@ -326,6 +360,7 @@ def clean(
326360
dry_run,
327361
user_facing_only,
328362
skip_authorized,
363+
authorized_only,
329364
):
330365
"""Clean managed views."""
331366
# set log level
@@ -334,13 +369,23 @@ def clean(
334369
except ValueError as e:
335370
raise click.ClickException(f"argument --log-level: {e}")
336371

372+
if skip_authorized and authorized_only:
373+
raise click.ClickException(
374+
"Cannot use both --skip-authorized and --authorized-only"
375+
)
376+
337377
if project_id is None and target_project is None:
338378
raise click.ClickException("command requires --project-id or --target-project")
339379

340380
expected_view_ids = {
341381
view.target_view_identifier(target_project)
342382
for view in _collect_views(
343-
name, sql_dir, project_id, user_facing_only, skip_authorized
383+
name,
384+
sql_dir,
385+
project_id,
386+
user_facing_only,
387+
skip_authorized,
388+
authorized_only,
344389
)
345390
}
346391

@@ -365,7 +410,13 @@ def clean(
365410
for views in p.starmap(
366411
client_q.with_client,
367412
(
368-
(_list_managed_views, dataset, name, skip_authorized)
413+
(
414+
_list_managed_views,
415+
dataset,
416+
name,
417+
skip_authorized,
418+
authorized_only,
419+
)
369420
for dataset in datasets
370421
),
371422
chunksize=1,
@@ -381,7 +432,9 @@ def clean(
381432
)
382433

383434

384-
def _list_managed_views(client, dataset, pattern, skip_authorized):
435+
def _list_managed_views(
436+
client, dataset, pattern, skip_authorized, authorized_only=False
437+
):
385438
query = f"""
386439
SELECT
387440
table_catalog || "." || table_schema || "." || table_name AS table_id,
@@ -407,6 +460,7 @@ def _list_managed_views(client, dataset, pattern, skip_authorized):
407460
for row in result
408461
if (pattern is None or fnmatchcase(sql_table_id(row.table_id), f"*{pattern}"))
409462
and (not skip_authorized or not row.is_authorized)
463+
and (not authorized_only or row.is_authorized)
410464
]
411465

412466

tests/view/test_view.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from google.api_core.exceptions import NotFound
77
from google.cloud.bigquery import SchemaField
88

9+
from bigquery_etl.cli.view import _collect_views
10+
from bigquery_etl.metadata.parse_metadata import Metadata
911
from bigquery_etl.view import CREATE_VIEW_PATTERN, View
1012

1113
TEST_DIR = Path(__file__).parent.parent
@@ -212,3 +214,117 @@ def test_view_has_changes_changed_schema(
212214

213215
assert simple_view.has_changes()
214216
assert "schema" in capsys.readouterr().out
217+
218+
@patch("bigquery_etl.cli.view.get_id_token")
219+
def test_collect_views_authorized_only(self, mock_get_id_token, runner):
220+
"""Test that authorized_only flag filters views correctly."""
221+
mock_get_id_token.return_value = None
222+
223+
with runner.isolated_filesystem():
224+
# Create test directory structure
225+
Path("sql/moz-fx-data-shared-prod/test").mkdir(parents=True)
226+
227+
# Create an authorized view
228+
authorized_view_dir = Path(
229+
"sql/moz-fx-data-shared-prod/test/authorized_view"
230+
)
231+
authorized_view_dir.mkdir()
232+
(authorized_view_dir / "view.sql").write_text(
233+
"CREATE OR REPLACE VIEW `moz-fx-data-shared-prod.test.authorized_view` AS SELECT 1"
234+
)
235+
authorized_metadata = Metadata(
236+
friendly_name="Authorized View",
237+
description="Test authorized view",
238+
owners=["[email protected]"],
239+
labels={"authorized": True},
240+
)
241+
authorized_metadata.write(authorized_view_dir / "metadata.yaml")
242+
243+
# Create a non-authorized view
244+
regular_view_dir = Path("sql/moz-fx-data-shared-prod/test/regular_view")
245+
regular_view_dir.mkdir()
246+
(regular_view_dir / "view.sql").write_text(
247+
"CREATE OR REPLACE VIEW `moz-fx-data-shared-prod.test.regular_view` AS SELECT 1"
248+
)
249+
regular_metadata = Metadata(
250+
friendly_name="Regular View",
251+
description="Test regular view",
252+
owners=["[email protected]"],
253+
)
254+
regular_metadata.write(regular_view_dir / "metadata.yaml")
255+
256+
# Test authorized_only=True
257+
views = _collect_views(
258+
name=None,
259+
sql_dir="sql",
260+
project_id="moz-fx-data-shared-prod",
261+
user_facing_only=False,
262+
skip_authorized=False,
263+
authorized_only=True,
264+
)
265+
assert len(views) == 1
266+
assert views[0].name == "authorized_view"
267+
268+
# Test skip_authorized=True
269+
views = _collect_views(
270+
name=None,
271+
sql_dir="sql",
272+
project_id="moz-fx-data-shared-prod",
273+
user_facing_only=False,
274+
skip_authorized=True,
275+
authorized_only=False,
276+
)
277+
assert len(views) == 1
278+
assert views[0].name == "regular_view"
279+
280+
# Test both flags False (get all views)
281+
views = _collect_views(
282+
name=None,
283+
sql_dir="sql",
284+
project_id="moz-fx-data-shared-prod",
285+
user_facing_only=False,
286+
skip_authorized=False,
287+
authorized_only=False,
288+
)
289+
assert len(views) == 2
290+
291+
def test_publish_authorized_only_mutually_exclusive(self, runner):
292+
"""Test that --authorized-only and --skip-authorized are mutually exclusive."""
293+
from bigquery_etl.cli.view import publish
294+
295+
with runner.isolated_filesystem():
296+
Path("sql/moz-fx-data-shared-prod/test").mkdir(parents=True)
297+
298+
result = runner.invoke(
299+
publish,
300+
[
301+
"--authorized-only",
302+
"--skip-authorized",
303+
"--project-id=moz-fx-data-shared-prod",
304+
],
305+
)
306+
assert result.exit_code != 0
307+
assert (
308+
"Cannot use both --skip-authorized and --authorized-only"
309+
in result.output
310+
)
311+
312+
def test_clean_authorized_only_mutually_exclusive(self, runner):
313+
"""Test that --authorized-only and --skip-authorized are mutually exclusive in clean."""
314+
from bigquery_etl.cli.view import clean
315+
316+
with runner.isolated_filesystem():
317+
with pytest.raises(AssertionError):
318+
result = runner.invoke(
319+
clean,
320+
[
321+
"--authorized-only",
322+
"--skip-authorized",
323+
"--target-project=moz-fx-data-shared-prod",
324+
],
325+
)
326+
assert result.exit_code != 0
327+
assert (
328+
"Cannot use both --skip-authorized and --authorized-only"
329+
in result.output
330+
)

0 commit comments

Comments
 (0)