I'm working on a script (run as a tool within ArcMap/Catalog) that renames the photo attachments of a feature class to include the date the photo was taken based on the exif data from the photo. The feature class and it's attachment table reside in an SDE with a PostgreSQL backend that has versioning enabled but only uses the default version.
I've had to write my own functions to update the attachment table as the built-in AddAttachments() and RemoveAttachments() have been giving me fits (subject of a separate question here). These work in basically the same way as the built-in functions, using a match table to specify which attachments to add and remove.
The issue I'm running into is that if I try to wrap the function calls into a "with arcpy.da.Editor(workspace) as editor" block, it gives me a "NULL result without error in PyObject_Call" error on the with...as line or a "Insufficient Permission [db] [state number]" error on the DeleteAttachements() call.
As a result, I'm using the manual startEditing/startOperation/<edits>/stopOperation/stopEditing(True) process. This works unless the data state shifts because someone else is editing (we have a couple of people out in the field uploading data via Collector to the default version, as well as one or two people in the office making edits). This shows up as a "start edit session" error when I try stopEditing(True). This can be worked around by running the script again until it works, but I'm worried that I'm not aborting/closing the edit session properly when these errors arise.
When I tried handling errors within the edit session via an except block based on this post, it would often throw an error when I abortOperation/stopEditing(False) saying that I need to start an edit session. As a result, I don't do anything to the edit session in my current except block, which is based of the ESRI tutorial on passing error messages through a tool.
Am I safe using the script as currently exists without using a with...as or cleaning up the edit session within the except block?
<snip: required imports here>
def DeleteAttachments(att_table, match_table, match_field, match_name):
'''
Manually deletes records from the attachment table.
Created because arcpy.DeleteAttachments_management() keeps failing.
'''
match_entries = []
with open(match_table, 'r') as table_file:
reader = csv.reader(table_file, delimiter=',')
for row in reader:
match_entries.append(row)
# Get indexes of fields
match_field_index = match_entries[0].index(match_field)
match_name_index = match_entries[0].index(match_name)
# Strip header row
data = match_entries[1:]
fields = ["rel_objectid", "att_name"]
with arcpy.da.UpdateCursor(att_table, fields) as ucursor:
for feature in ucursor:
if [str(feature[0]), feature[1]] in data:
ucursor.deleteRow()
def AddAttachments(att_table, match_table, match_field, match_name):
'''
Manually adds records from the attachment table.
Created because arcpy.AddAttachments_management() doesn't like edit sessions
'''
match_entries = []
with open(match_table, 'r') as table_file:
reader = csv.reader(table_file, delimiter=',')
for row in reader:
match_entries.append(row)
# Get indexes of fields
match_field_index = match_entries[0].index(match_field)
match_name_index = match_entries[0].index(match_name)
# Strip header row
data = match_entries[1:]
fields = ["rel_objectid", "att_name", "data", "content_type", "data_size"]
# Use match_name field (a path) to load the photo as a memory view
with arcpy.da.InsertCursor(att_table, fields) as icursor:
for feature in data:
with open(feature[match_name_index], 'rb') as f:
# Read in photo as a memory view
photo_mv = f.read()
# Calculate the size of the file
length = f.tell()
photo_name = feature[match_name_index].rpartition(os.path.sep)[2]
content_type = "image/jpeg"
row = (feature[match_field_index], photo_name, photo_mv,
content_type, length)
icursor.insertRow(row)
<snip: reading in attachments, saving to disk, renaming, creating match tables for add and remove>
try:
# Manually handling edit session instead of using a "with...as" because it
# gives us "NULL result without error in PyObject_Call" or "Insufficient
# Permission" errors on the with...as or the DeleteAttachments() lines,
# respectively.
arcpy.AddMessage("Opening edit session...")
edit = arcpy.da.Editor(workspace)
edit.startEditing()
edit.startOperation()
# Custom functions as the arcpy equivalents don't handle edit sessions
# very well. Just direct edits to the attach table based on the match
# tables created as per normal.
arcpy.AddMessage("Deleting old attachments...")
DeleteAttachments(att_table, rm_table_path, match_field, match_name)
arcpy.AddMessage("Adding renamed attachments...")
AddAttachments(att_table, add_table_path, match_field, match_name)
arcpy.AddMessage("Closing edit session...")
edit.stopOperation()
edit.stopEditing(True)
arcpy.AddMessage(arcpy.GetMessages())
except arcpy.ExecuteError:
arcpy.AddError(arcpy.GetMessages(2))
except Exception as err:
# Get the traceback object
tb = sys.exc_info()[2]
tbinfo = traceback.format_tb(tb)[0]
# Concatenate information together concerning the error into a message string
pymsg = "PYTHON ERRORS:\nTraceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
msgs = "\nArcPy ERRORS:\n" + arcpy.GetMessages(2) + "\n"
# Return python error messages for use in script tool or Python Window
arcpy.AddError(pymsg)
arcpy.AddError(msgs)
# Print Python error messages for use in Python / Python Window
print(pymsg)
print(msgs)