Skip to content

Commit 03842c1

Browse files
authored
feat: Remove old users from export ownership (#365)
* feat: Remove old users from export ownership * Fixing type hint * Fixing pre-commit compatibility issues
1 parent 6cf704e commit 03842c1

File tree

5 files changed

+282
-15
lines changed

5 files changed

+282
-15
lines changed

dev-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ mccabe==0.7.0
4040
multidict==6.0.2
4141
nodeenv==1.7.0
4242
numpy==1.23.1
43-
packaging==21.3
43+
packaging==24.0
4444
pandas==1.4.3
4545
pep517==0.13.0
4646
pip-tools==6.13.0

src/preset_cli/api/clients/superset.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,11 +1165,15 @@ def get_role_id(self, role_name: str) -> int:
11651165

11661166
return id_
11671167

1168-
def export_ownership(self, resource_name: str) -> Iterator[OwnershipType]:
1168+
def export_ownership(
1169+
self,
1170+
resource_name: str,
1171+
users: Dict[int, str],
1172+
exclude_old_users: bool,
1173+
) -> Iterator[OwnershipType]:
11691174
"""
11701175
Return information about resource ownership.
11711176
"""
1172-
emails = {user["id"]: user["email"] for user in self.export_users()}
11731177
uuids = self.get_uuids(resource_name)
11741178
name_key = {
11751179
"dataset": "table_name",
@@ -1178,11 +1182,30 @@ def export_ownership(self, resource_name: str) -> Iterator[OwnershipType]:
11781182
}[resource_name]
11791183

11801184
for resource in self.get_resources(resource_name):
1181-
yield {
1185+
info: OwnershipType = {
11821186
"name": resource[name_key],
11831187
"uuid": uuids[resource["id"]],
1184-
"owners": [emails[owner["id"]] for owner in resource.get("owners", [])],
1188+
"owners": [],
11851189
}
1190+
for owner in resource.get("owners", []):
1191+
if user := users.get(owner["id"]):
1192+
info["owners"].append(user)
1193+
else:
1194+
if not exclude_old_users:
1195+
raise Exception(
1196+
f"User {owner['first_name']} {owner['last_name']} owns the "
1197+
f"{resource_name} {resource[name_key]} but is not a member in the team."
1198+
" You can trigger this command with ``--exclude-old-users`` to "
1199+
"automatically remove old members.",
1200+
)
1201+
_logger.warning(
1202+
"User %s %s not found in team. Removing from ownership config for %s %s.",
1203+
owner["first_name"],
1204+
owner["last_name"],
1205+
resource_name,
1206+
resource[name_key],
1207+
)
1208+
yield info
11861209

11871210
def import_ownership(
11881211
self,

src/preset_cli/cli/superset/export.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -456,17 +456,30 @@ def export_rls(
456456
type=click.Path(resolve_path=True),
457457
default="ownership.yaml",
458458
)
459+
@click.option(
460+
"--asset-type",
461+
help="Asset type",
462+
multiple=True,
463+
)
459464
@click.option(
460465
"--force-unix-eol",
461466
is_flag=True,
462467
default=False,
463468
help="Force Unix end-of-line characters, otherwise use system default",
464469
)
470+
@click.option(
471+
"--exclude-old-users",
472+
is_flag=True,
473+
default=False,
474+
help="Exclude users that are no longer a member in the team from the generated file",
475+
)
465476
@click.pass_context
466477
def export_ownership(
467478
ctx: click.core.Context,
468479
path: str,
480+
asset_type: Tuple[str, ...],
469481
force_unix_eol: bool = False,
482+
exclude_old_users: bool = False,
470483
) -> None:
471484
"""
472485
Export DBs/datasets/charts/dashboards ownership to a YAML file.
@@ -475,9 +488,21 @@ def export_ownership(
475488
url = URL(ctx.obj["INSTANCE"])
476489
client = SupersetClient(url, auth)
477490

491+
users = {user["id"]: user["email"] for user in client.export_users()}
492+
asset_types = set(asset_type) if asset_type else set()
493+
resources_to_export = (
494+
[r for r in ["dataset", "chart", "dashboard"] if r in asset_types]
495+
if asset_types
496+
else ["dataset", "chart", "dashboard"]
497+
)
498+
478499
ownership = defaultdict(list)
479-
for resource_name in ["dataset", "chart", "dashboard"]:
480-
for resource in client.export_ownership(resource_name):
500+
for resource_name in resources_to_export:
501+
for resource in client.export_ownership(
502+
resource_name,
503+
users,
504+
exclude_old_users,
505+
):
481506
ownership[resource_name].append(
482507
{
483508
"name": resource["name"],
@@ -486,6 +511,10 @@ def export_ownership(
486511
},
487512
)
488513

489-
newline = get_newline_char(force_unix_eol)
490-
with open(path, "w", encoding="utf-8", newline=newline) as output:
514+
with open(
515+
path,
516+
"w",
517+
encoding="utf-8",
518+
newline=get_newline_char(force_unix_eol),
519+
) as output:
491520
yaml.dump(dict(ownership), output)

tests/api/clients/superset_test.py

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2577,12 +2577,41 @@ def test_export_ownership(mocker: MockerFixture) -> None:
25772577
"""
25782578
mocker.patch.object(
25792579
SupersetClient,
2580-
"export_users",
2580+
"get_uuids",
2581+
return_value={
2582+
1: UUID("e0d20af0-cef9-4bdb-80b4-745827f441bf"),
2583+
},
2584+
)
2585+
mocker.patch.object(
2586+
SupersetClient,
2587+
"get_resources",
25812588
return_value=[
2582-
{"id": 1, "email": "[email protected]"},
2583-
{"id": 2, "email": "[email protected]"},
2589+
{
2590+
"slice_name": "My chart",
2591+
"id": 1,
2592+
"owners": [{"id": 1}, {"id": 2}],
2593+
},
25842594
],
25852595
)
2596+
2597+
auth = Auth()
2598+
client = SupersetClient("https://superset.example.org/", auth)
2599+
2600+
assert list(client.export_ownership("chart", users, exclude_old_users=False)) == [
2601+
{
2602+
"name": "My chart",
2603+
2604+
"uuid": UUID("e0d20af0-cef9-4bdb-80b4-745827f441bf"),
2605+
},
2606+
]
2607+
2608+
2609+
def test_export_ownership_user_not_found_raises_exception(
2610+
mocker: MockerFixture,
2611+
) -> None:
2612+
"""
2613+
Test ``export_ownership`` when user not found and exclude_old_users=False.
2614+
"""
25862615
mocker.patch.object(
25872616
SupersetClient,
25882617
"get_uuids",
@@ -2597,17 +2626,64 @@ def test_export_ownership(mocker: MockerFixture) -> None:
25972626
{
25982627
"slice_name": "My chart",
25992628
"id": 1,
2600-
"owners": [{"id": 1}, {"id": 2}],
2629+
"owners": [
2630+
{"id": 1, "first_name": "John", "last_name": "Doe"},
2631+
{"id": 999, "first_name": "Missing", "last_name": "User"},
2632+
],
2633+
},
2634+
],
2635+
)
2636+
2637+
auth = Auth()
2638+
client = SupersetClient("https://superset.example.org/", auth)
2639+
users = {1: "[email protected]"} # User 999 is not in the dict
2640+
2641+
with pytest.raises(
2642+
Exception,
2643+
match="User Missing User owns the chart My chart but is not a member in the team",
2644+
):
2645+
list(client.export_ownership("chart", users, exclude_old_users=False))
2646+
2647+
2648+
def test_export_ownership_user_not_found_with_exclude_flag(
2649+
mocker: MockerFixture,
2650+
) -> None:
2651+
"""
2652+
Test ``export_ownership`` when user not found and exclude_old_users=True.
2653+
"""
2654+
mocker.patch.object(
2655+
SupersetClient,
2656+
"get_uuids",
2657+
return_value={
2658+
1: UUID("e0d20af0-cef9-4bdb-80b4-745827f441bf"),
2659+
},
2660+
)
2661+
mocker.patch.object(
2662+
SupersetClient,
2663+
"get_resources",
2664+
return_value=[
2665+
{
2666+
"slice_name": "My chart",
2667+
"id": 1,
2668+
"owners": [
2669+
{"id": 1, "first_name": "John", "last_name": "Doe"},
2670+
{"id": 999, "first_name": "Missing", "last_name": "User"},
2671+
],
26012672
},
26022673
],
26032674
)
26042675

26052676
auth = Auth()
26062677
client = SupersetClient("https://superset.example.org/", auth)
2607-
assert list(client.export_ownership("chart")) == [
2678+
users = {1: "[email protected]"} # User 999 is not in the dict
2679+
2680+
# Should not raise exception, and should exclude the missing user
2681+
result = list(client.export_ownership("chart", users, exclude_old_users=True))
2682+
2683+
assert result == [
26082684
{
26092685
"name": "My chart",
2610-
2686+
"owners": ["[email protected]"], # Only user 1, user 999 excluded
26112687
"uuid": UUID("e0d20af0-cef9-4bdb-80b4-745827f441bf"),
26122688
},
26132689
]

0 commit comments

Comments
 (0)