Whether you need to remove an old member’s content or delete deprecated items across your organization, manually deleting items is tedious, especially with delete protection and dependencies.
Sounds like a great workflow to automate using the ArcGIS API for Python, right? But how do we programmatically handle delete protection and dependent items?
Let’s go over our options (and highlight some things to avoid).
Here's a preview of where we're headed:
The rest of this blog will go over why this Python recipe is so handy compared to other more verbose options.
The most succinct way to delete an old member’s content is to loop through their folders and call the ArcGIS API for Python’s ContentManager’s .delete_items() method on all the content in each folder:
# import standard library modules
from getpass import getpass
# import the ArcGIS API for Python
from arcgis.gis import GIS, User
# instantiate GIS object with admin credentials
gis = GIS("https://arcgis.com", "<admin_username>", getpass("Enter admin password > "))
# instantiate User object with the old user's username
old_member = User(gis, "<old_username>")
# delete all items in root folder
gis.content.delete_items(old_member.items(max_items=1000))
# delete items in all other folders
for f in old_member.folders:
gis.content.delete_items(old_member.items(folder=f, max_items=1000))
Alright, not too bad, but what happens when items have delete protection or dependents?
False (that’s all we get from the method).
Everything that can be deleted is deleted, but items with delete protection enabled or with dependents are not.
Okay, but the ContentManager class has another method – .can_delete() – that we could use to handle this, right?
Yes, but now instead of deleting all the content in a folder with one succinct line of code:
gis.content.delete_items(old_member.items(max_items=1000))
we’re looping through every item in each folder, checking if it can be deleted, and then if it can’t, dealing with the reason it can’t be deleted. In the case of dependent items, that means we also need another loop to check and delete the dependent items:
# import standard library modules
from getpass import getpass
# import the ArcGIS API for Python
from arcgis.gis import GIS, User
# instantiate GIS object with admin credentials
gis = GIS("https://arcgis.com", "<admin_username>", getpass("Enter admin password > "))
# instantiate User object with the old user's username
old_member = User(gis, "<old_username>")
# delete all items in root folder
for item in old_member.items(max_items=1000):
dry_run = item.delete(dry_run=True)
# if item can be deleted, delete it
if dry_run["can_delete"]:
item.delete()
# if item can't be deleted due to delete protection, disable delete protection and then delete it
elif dry_run["details"]["message"] == f"Unable to delete item {item.id}. Delete protection is turned on.":
item.protect(enable=False)
item.delete()
# if item can't be delete due to dependents, delete the dependents then delete the item
elif dry_run["details"]["message"] == "Unable to delete item. This service item has a related Service item":
for related_item in dry_run["details"]["offending_items"]:
related_dry_run = related_item.delete(dry_run=True)
# as above, if the related item can be deleted, delete it
if related_dry_run["can_delete"]:
related_item.delete()
# as above, if the related item can't be deleted due to delete protection, disable delete protection and then delete it
elif related_dry_run["details"]["message"] == f"Unable to delete item {related_item.id}. Delete protection is turned on.":
related_item.protect(enable=False)
related_item.delete()
# now that the dependents have been deleted, we can delete the original item
item.delete()
Our code is no longer succinct and is becoming harder to read with all the nested for loops and if-else statements. Time to bring in recursion.
Note: The dictionary returned by the Item class' .delete() method with the dry_run parameter set to True is easier to work with for this purpose than the dictionary returned by the ContentManager class' .can_delete() method, which is why it is used above.
Recursion allows us to call a function within its own definition. When applicable, this can significantly reduce how much code we have to write to accomplish our goal.
Here's how we can use recursion to reduce the code required to handle delete protection and dependents:
def recursive_delete(items):
"""Deletes all items and their dependents."""
for item in items:
try:
dry_run = item.delete(dry_run=True)
if dry_run["can_delete"]:
item.delete()
elif dry_run["details"]["message"] == f"Unable to delete item {item.id}. Delete protection is turned on.":
item.protect(enable=False)
item.delete()
elif dry_run["details"]["message"] == "Unable to delete item. This service item has a related Service item":
recursive_delete(dry_run["details"]["offending_items"])
except TypeError:
print(f"Item ({item.id}) no longer exists or is inaccessible.")
return
To use recursion, we need to use functions, so the flow control from the previous code snippet has been placed in the recursive_delete() function defined above. But it seems a lot shorter? Exactly 🙂
Since we've already written the logic for handling item deletion based on the dictionary returned by the Item class' .delete() method with the dry_run parameter set to True, it's unnecessary to rewrite it all again for dependent items. Instead, in the case of dependent items, we can just call the recursive_delete() function again. When there are no longer any dependent items to delete, the function will return.
Here's what using this function in a full script looks like:
# import standard library modules
from getpass import getpass
# import the ArcGIS API for Python
from arcgis.gis import GIS, User
def recursive_delete(items):
"""Deletes all items and their dependents."""
for item in items:
try:
dry_run = item.delete(dry_run=True)
if dry_run["can_delete"]:
item.delete()
elif dry_run["details"]["message"] == f"Unable to delete item {item.id}. Delete protection is turned on.":
item.protect(enable=False)
item.delete()
elif dry_run["details"]["message"] == "Unable to delete item. This service item has a related Service item":
recursive_delete(dry_run["details"]["offending_items"])
except TypeError:
print(f"Item ({item.id}) no longer exists or is inaccessible.")
return
# instantiate GIS object with admin credentials
gis = GIS("https://arcgis.com", "<admin_username>", getpass("Enter admin password > "))
# instantiate User object with the old user's username
old_member = User(gis, "<old_username>")
# delete all items in root folder
recursive_delete(old_member.items(max_items=1000))
# delete items in all other folders
for f in old_member.folders:
recursive_delete(old_member.items(folder=f, max_items=1000))
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.