Hello, A colleague and I have developed some python that, among other things, joins a table to a feature class and then copies specific features from there into a different, existing feature class. This has run smoothly for over a year. Suddenly the script is failing, stating that an edit session is needed to run the Append tool - which doesn't seem right - and anyway, our attempts at adding an edit session into the script causes the whole thing to fail from the get-go.
A month or so ago there was a recent change to the database that the table comes from, but we've accommodated that change. Nothing has changed on the gis-side in any way - feature classes, locations, etc. Nothing has changed within our organizations enterprise / software that should affect this as far as I'm aware. Any idea what's going on and how to repair this issue?
I can't attach the full error message but the most pertinent part seems to be this: "arcgisscripting.ExecuteError: ERROR 160250: Objects in this class cannot be updated outside an edit session. Failed to execute (Append)"
Here's what I think is the pertinent section of code, though it hasn't been changed:
...
survey_plat = r'V:\\gislu\\_DatabaseConnection\\editgis1.lu.user.dc.sde\\lu.LU.SURV_SurveyPlat'
count_pre_append = int(arcpy.management.GetCount(survey_plat)[0])
results = arcpy.management.Append(
inputs=surveys_added,
target=survey_plat,
schema_type='NO_TEST',
field_mapping="SURVEY_ID \"SURVEY_ID\" true true false 12 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_id,0,7999;SURVEY_TYP \"SURVEY_TYP\" true true false 25 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_type,0,7999;SURVEY_DES \"SURVEY_DES\" true true false 254 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_descr,0,7999;SURVEY_YR \"SURVEY_YR\" true true false 4 Long 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_year,-1,-1;PLSS_COORD \"PLSS_COORD\" true true false 2 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,PLSS_COORD,0,7999;LINK \"LINK\" true true false 256 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,link,0,7999;PARCEL_NO \"PARCEL_NO\" true true false 12 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,parcel_id,0,7999"
)
print(results)
print(arcpy.GetMessages())
count_post_append = int(arcpy.management.GetCount(survey_plat)[0])
count_delta = count_post_append - count_pre_append
print(f'{count_delta} records appended')
Can you print out count_pre_append to see if the script is evaluating that variable correctly?
Maybe its a misleading error message, so I would put in additional print statements to make sure variables are being evaluated correctly.
We added this and confirmed all looks well until the failure point - but then the script fails at the same point so I think the message is valid.
This isn't super helpful, but can confirm stuff like this happens. I had a script that worked just fine on day 1 and then on day 2 required an edit session when I was using an update cursor. Hadn't even changed it yet.
Did something changed on the database side? Register As Versioned or enabled branch versioning. This has happened to me for no reason as well, but I also know if something on the database has changed it can trig it.
Try adding,
workspace = r"V:\gislu\_DatabaseConnection\editgis1.lu.user.dc.sde"
edit = arcpy.da.Editor(workspace)
edit.startEditing(False, True)
edit.startOperation()
arcpy.management.Append(
inputs=surveys_added,
target=survey_plat,
schema_type="NO_TEST",
field_mapping=...
)
edit.stopOperation()
edit.stopEditing(True)
Thanks! Gave this a shot but no change in behavior. I think we may have a solution below though...
The Append function will hook into existing edit sessions if run in a map context. That behavior can be a bit odd when you then run the same tool with no active edit session
It's best practice to use the `with arcpy.da.Editor` context block when doing any write operations since it'll prevent undefined behavior when an edit session can't be found, and will fail gracefully if there's an issue during the transaction/edit block.
Additionally, using an edit context with arcpy.da.UpdateCursor can be a lot more informative since Append just kinda consumes a lot of row level messages. doing it with UpdateCursor lets you handle bad data (e.g. a single input row that has a text field of length of 1000 being appended to a target field of length 255).
Some others mentioned how to use the edit session here, but I would absolutely make sure to use the `with Editor as edit` pattern, since an exception being raised before editor.stopEditing is called will leave your database with an open edit lock.
Here's your script using Edit sessions and an InsertCursor
from arcpy.da import InsertCursor, SearchCursor, Editor
# Target Workspace
gdb = r'V:\prjlu\SurveyPlats\PlatProcessingAutomation\SurveyProcessing.gdb'
new_survey = 'SurveysAdded'
sde = r'V:\gislu\_DatabaseConnection\editgis1.lu.user.dc.sde'
survey_plat = 'lu.LU.SURV_SurveyPlat'
# Map new_survey -> survey_plat
field_map = {
'SURVEY_ID': 'survey_id',
'SURVEY_TYP': 'survey_typ',
'SURVEY_DES': 'survey_descr',
'SURVEY_YR': 'survey_year',
'PLSS_COORD': 'PLSS_COORD',
'LINK': 'link',
'PARCEL_NO': 'parcel_id',
}
# Use these in the cursors
source_fields = list(field_map.keys())
target_fields = list(field_map.values())
new_rows = list(SearchCursor(new_survey, source_fields))
# You can use multiple context managers in one with block
with Editor(sde), InsertCursor(survey_plat, target_fields) as cur:
for row in new_rows:
cur.insertRow(row)
print(f'{len(new_rows)} records appended')
Another reason to use an update cursor is that it's MILES faster than Append().
Additionally, this style plays way better with version control since the field map isn't just a giant one line string that's hundreds of characters long.
Now you just update the mapping dictionary and can see exactly what changed with a commit.
Edit: Here's a more robust version of the above code that supports arbitrary mappings using lambda expressions (You can also just define normal functions and use those. That's a much better idea):
from collections.abc import Callable, Generator, Sequence
from typing import Any
from arcpy.da import InsertCursor, SearchCursor, Editor
type FieldCalc = Callable[[tuple[Any, ...]], Any]
type FieldNames = Sequence[str]
type FieldMap = dict[str, tuple[FieldCalc, FieldNames] | str]
def process_map(f_map: FieldMap) -> tuple[list[str], list[str]]:
"""Consume a field mapping dictionary and return all fields participating on both sides
Note: Returned values are in order (source, target)
"""
target_fields = list(f_map.keys())
source_fields: list[str] = []
for mapping in f_map.values():
if isinstance(mapping, tuple):
# mapping has a functional component
_, fields = mapping
source_fields.extend(fields)
else:
# mapping is direct
source_fields.append(mapping)
return source_fields, target_fields
# Map the row values to field names
def as_dict(cur: SearchCursor) -> Generator[dict[str, Any]]:
"""Make cursor rows dictionaries"""
for row in cur:
yield dict(zip(cur.fields, row))
def calculate_row(target_fields: Sequence[str], source_row: dict[str, Any], field_map: FieldMap) -> list[Any]:
"""Apply a Row-Wise functional/direct map to a Cursor
Note: Returned list will match the field order supplied by `target_fields`
"""
row_vals: list[Any] = []
for field in target_fields:
mapping = field_map.get(field)
if mapping is None:
raise ValueError(f'Field {field} is not mapped!')
if isinstance(mapping, tuple):
# Functional Map
func, source_fields = mapping
row_vals.append(func(tuple(source_row[f] for f in source_fields)))
else:
# Direct Map
row_vals.append(source_row[mapping])
return row_vals
def main():
# Target Workspace
new_survey = 'SurveysAdded'
sde = r'V:\gislu\_DatabaseConnection\editgis1.lu.user.dc.sde'
survey_plat = 'lu.LU.SURV_SurveyPlat'
field_map: FieldMap = {
'survey_id': (lambda vs: ';'.join(map(str, vs)), ['SURVEY_ID1', 'SURVEY_ID2']),
'survey_typ': (lambda vs: ([v for v in vs if v]+[None]).pop(0), ['SURVEY_TYP', 'ALT_TYP']),
'survey_descr': 'SURVEY_DES',
'survey_year': 'SURVEY_YR',
'PLSS_COORD': (lambda vs: vs[0][:6], ['PLSS_COORD']), # Truncate
'link': (lambda vs: str(vs[0]).replace('http', 'https'), ['LINK']),
'parcel_id': 'PARCEL_NO',
}
source_fields, target_fields = process_map(field_map)
new_rows = list(as_dict(SearchCursor(new_survey, source_fields)))
# You can use multiple context managers in one with block
with Editor(sde), InsertCursor(survey_plat, target_fields) as cur:
for row in new_rows:
cur.insertRow(calculate_row(target_fields, row, field_map))
print(f'{len(new_rows)} records appended')
if __name__ == '__main__':
main()
Edit 2:
Here's a more reasonable way to do the functional mappings:
...
def join(vals: tuple[Any, ...]) -> str:
return ';'.join(map(str, vals))
def first(vals: tuple[Any, ...]) -> Any:
return next(filter(bool, vals), None)
def truncate(vals: tuple[str]) -> str:
return vals[0][:6]
def make_https(vals: tuple[str]) -> str:
return vals[0].replace('http', 'https')
field_map: FieldMap = {
'survey_id': (join, ['SURVEY_ID1', 'SURVEY_ID2']),
'survey_typ': (first, ['SURVEY_TYP', 'ALT_TYP']),
'survey_descr': 'SURVEY_DES',
'survey_year': 'SURVEY_YR',
'PLSS_COORD': (truncate, ['PLSS_COORD']), # Truncate
'link': (make_https, ['LINK']),
'parcel_id': 'PARCEL_NO',
}
...
@Sean_Gambrel did any of this work for you?