Programmatically Deleting Content in ArcGIS Online

2262
12
05-31-2022 11:18 AM
by Anonymous User
Not applicable
7 12 2,262

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

 

 

12 Comments
GlenShepherd
Esri Contributor

Thanks @Anonymous User !
Only snag I hit was there's an 'f' missing before:

"Unable to delete item. This service item has a related Service item"

in your sample code above.

This is so helpful though, thank you for the succinct explanation every step of the way.

 

AmberKelly
New Contributor III

Hi all,

For the section:

# delete items in all other folders
for f in old_member.folders:
recursive_delete(old_member.items(folder=f, max=1000))

I'm getting the following error:

TypeError: items() got an unexpected keyword argument 'max'

 Anyone got any advise for an ArcGIS API for Python beginner?

Thanks in advance!

PeterKnoop
MVP Regular Contributor

@AmberKelly the items method is expecting a parameter called max_items, rather than max. It specifies the maximum number of items to return.

AmberKelly
New Contributor III

Champion! Thank you Peter!

All running smoothly now 😊

by Anonymous User
Not applicable

Thanks for catching that @PeterKnoop -- I updated the code snippet. 

@AmberKelly As Peter mentioned the correct parameter name for the User class's .items() method is max_items, not max as I mistakenly wrote (https://developers.arcgis.com/python/api-reference/arcgis.gis.toc.html#arcgis.gis.User.items). Glad updating that resolved the issue for you!

AmberKelly
New Contributor III

Hi again,

Any suggestions on how to deal with hosted view layers? I've come across some users with these and the code throws an error - Exception: Unable to delete item. This service item has a related Service item.

 

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Input In [44], in <cell line: 2>()
      1 # delete items in all other folders
      2 for f in old_member.folders:
----> 3     recursive_delete(old_member.items(folder=f, max_items=1000))

Input In [37], in recursive_delete(items)
      9 elif dry_run["details"]["message"] == f"Unable to delete item {item.id}. Delete protection is turned on.":
     10     item.protect(enable=False)
---> 11     item.delete()
     12 elif dry_run["details"]["message"] == "Unable to delete item. This service item has a related Service item":
     13     recursive_delete(dry_run["details"]["offending_items"])

File /opt/conda/lib/python3.9/site-packages/arcgis/gis/__init__.py:12696, in Item.delete(self, force, dry_run)
  12694         return {"can_delete": False, "details": error_dict}
  12695 else:
> 12696     return self._portal.delete_item(self.itemid, self._user_id, folder, force)

File /opt/conda/lib/python3.9/site-packages/arcgis/gis/_impl/_portalpy.py:761, in Portal.delete_item(self, item_id, owner, folder, force)
    759 else:
    760     post_data = self._postdata()
--> 761 resp = self.con.post(path, post_data)
    763 if resp:
    764     return resp.get("success")

File /opt/conda/lib/python3.9/site-packages/arcgis/gis/_impl/_con/_connection.py:1407, in Connection.post(self, path, params, files, **kwargs)
   1405 if return_raw_response:
   1406     return resp
-> 1407 return self._handle_response(
   1408     resp=resp,
   1409     out_path=out_path,
   1410     file_name=file_name,
   1411     try_json=try_json,
   1412     force_bytes=kwargs.pop("force_bytes", False),
   1413 )

File /opt/conda/lib/python3.9/site-packages/arcgis/gis/_impl/_con/_connection.py:900, in Connection._handle_response(self, resp, file_name, out_path, try_json, force_bytes, ignore_error_key)
    898             return data
    899         errorcode = data["error"]["code"] if "code" in data["error"] else 0
--> 900         self._handle_json_error(data["error"], errorcode)
    901     return data
    902 else:

File /opt/conda/lib/python3.9/site-packages/arcgis/gis/_impl/_con/_connection.py:923, in Connection._handle_json_error(self, error, errorcode)
    920                 # _log.error(errordetail)
    922 errormessage = errormessage + "\n(Error Code: " + str(errorcode) + ")"
--> 923 raise Exception(errormessage)

Exception: Unable to delete item. This service item has a related Service item
(Error Code: 400)

 

Thanks again!

MobiusSnake
MVP

@AmberKelly  - Do you have views created off that hosted feature layer?  If so you'll need to drill into those and nuke them first.

GlenShepherd
Esri Contributor

Hi @AmberKelly ,

If you see my comment above, you need to place an 'f' in your script.
In the example here, it would be on line 19, immediately before the message "Unable to delete item. This service item has a related Service item".

AmberKelly
New Contributor III

Thanks Glen and MobiusSnake. Much appreciated!

WillAnderson
New Contributor III

Is this possible using ArcGIS Online yet (not Enterprise)?

RussellH
Esri Contributor

Hi @WillAnderson,

This is possible in a few different ways depending on what your end goal is.  I will suggest reviewing the 'How To' article below as well as the related information that's included as a good place to start.

Your script can be scheduled to run from ArcGIS Pro, ArcGIS Notebooks, or a machine.

WillAnderson
New Contributor III

I was able to get a simple version working through Jupyter Notebook (below) for an ArcGIS Online (not Enterprise) user's items. Just imported modules, authenticated as admin, specified target user, specified target folder, then ran to remove delete protection for all items in the folder (I had moved target items there previously via the web GUI.

The script I ran didn't seem to apply to more than 10-30 items at a time. So had to run several times. Probably something to do with pagination? Also, I had tried it authenticating as the actual user, not Organization Admin account, and once I changed to authenticate to the Admin account, then specify the target User and their folder, it seemed to apply to more items each time it ran. Anyway, was able to remove protection for over 400 items, then delete them with relative ease.

# import modules
from arcgis.gis import GIS

# authenticate - add inyour AGOL organization URL
gis = GIS("https://ORGANIZATIONURL.maps.arcgis.com", "ADMINUSERNAME", "PASSWORD")

# specify target user and folder names
owner_username = "USERNAME"
folder_name = "FOLDERNAME"

# specify items in that users' folder
user = gis.users.get(owner_username)
content = user.items(folder=folder_name)

# remove delete protection for all items in that folder
for item in content:
item.protect(enable=False)