Select to view content in your preferred language

Programmatically Deleting Content in ArcGIS Online

4204
13
05-31-2022 11:18 AM
by Anonymous User
Not applicable
7 13 4,204

Introduction

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:

recursive_delete() function -- code snippets throughout blogrecursive_delete() function -- code snippets throughout blog

The rest of this blog will go over why this Python recipe is so handy compared to other more verbose options.

Batch deleting an old member's content

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.

Handling Delete Protection and Dependents

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. 

Using Recursion to handle Delete Protection and Dependents

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))

 

 

13 Comments
Contributors