I have the below script which does the following:
This script works when run in the interpretter, and has been running on our server when trigged by Task Scheduler overnight for some weeks. All of a sudden, now it doesn't.
I'm getting told that the feature service URL is not a valid parameter for ExportFeatures, and also not compatible with Update Cursor (according to Copilot).
I managed to find a way around the ExportFeatures issue by using Feature Layer to create a local copy.
What I can't figure out is why this was working with no issues, now isn't despite no software updates being done on the server, or data configuration changes occurring. Has ArcGIS Online restricted the use of the feature service URL as a data source in arcpy?
import arcpy
import time
import os
import csv
import tempfile
from arcgis.gis import GIS
from arcgis.features import FeatureLayerCollection
# Start Timer
startTime = time.time()
try:
import certifi
import urllib3
import ssl
import warnings
from urllib3.exceptions import InsecureRequestWarning
# Overwrite Output
arcpy.env.overwriteOutput = True
# Create a default SSL context with certificate verification
ssl_context = ssl.create_default_context(cafile=certifi.where())
http = urllib3.PoolManager(ssl_context=ssl_context)
# Make a request to verify the setup
response = http.request('GET', 'https://org-wa.maps.arcgis.com')
print("http response: " + str(response.status))
# Suppress only the single InsecureRequestWarning from urllib3 if necessary
warnings.simplefilter('ignore', InsecureRequestWarning)
# Create GIS object
print("Connecting to AGOL")
client_id = '#####'
client_secret = '#####'
gis = GIS("https://org-wa.maps.arcgis.com", client_id=client_id, client_secret=client_secret)
print("Logged in as: " + gis.properties.user.username)
# Create a temporary directory
temp_dir = tempfile.mkdtemp()
# Define the path for the temporary File Geodatabase
temp_gdb = os.path.join(temp_dir, "tempAreaHa.gdb")
# Create the File Geodatabase
arcpy.CreateFileGDB_management(temp_dir, "tempAreaHa.gdb")
# Set the workspace to the temporary File Geodatabase
arcpy.env.workspace = temp_gdb
def remove_last_part(url):
# Split the URL by '/' and join all parts except the last one
return '/'.join(url.split('/')[:-1])
def extract_fc_name(url):
# Extract the feature class name from the URL and clean it
path_parts = url.split('/')
fc_name = path_parts[-3]
return fc_name
# Read the CSV file containing feature service URLs
with open(r'\\gis-prod\orggis\Update\Feature Service Update Scripts\FeatureServiceAreaUpdates.csv', mode='r') as file:
csv_reader = csv.reader(file)
next(csv_reader) # Skip header row if present
for row in csv_reader:
featureservice = row[0]
fc_name = extract_fc_name(featureservice)
with arcpy.EnvManager(preserveGlobalIds=True):
temp_Areas = arcpy.conversion.ExportFeatures(
in_features=featureservice,
out_features=arcpy.env.workspace + "\\" + fc_name,
where_clause="",
use_field_alias_as_name="NOT_USE_ALIAS",
field_mapping=f'AreaHa "Area (ha)" true true false 0 Double 0 0,First,#,{fc_name},AreaHa,-1,-1;GlobalID "GlobalID" false false true 38 GlobalID 0 0,First,#,{fc_name},GlobalID,-1,-1',
sort_field=None
)
print(f"Features exported for {fc_name}")
# Determine which area field exists
fields = [f.name for f in arcpy.ListFields(temp_Areas)]
area_field = None
for candidate in ["AreaHa", "Area", "Hectares"]:
if candidate in fields:
area_field = candidate
break
if area_field:
print(f"Calculating area for field: {area_field}")
arcpy.management.CalculateGeometryAttributes(
in_features=temp_Areas,
geometry_property=f"{area_field} AREA",
length_unit="",
area_unit="HECTARES",
coordinate_system='PROJCS["GDA2020_Australian_Albers",GEOGCS["GDA2020",DATUM["GDA2020",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Albers"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",132.0],PARAMETER["Standard_Parallel_1",-18.0],PARAMETER["Standard_Parallel_2",-36.0],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]',
coordinate_format="SAME_AS_INPUT"
)
print(f"Areas calculated for {fc_name}")
else:
raise ValueError(f"No matching area field found in {fc_name}. Expected one of: 'AreaHa', 'Area', or 'Hectares'.")
# Remove the last part of the URL
modified_url = remove_last_part(featureservice)
# Access the feature layer collection
flc = FeatureLayerCollection(modified_url, gis)
# Disable editor tracking
disable_params = {
"editorTrackingInfo": {
"enableEditorTracking": False
}
}
flc.manager.update_definition(disable_params)
print(f"Editor tracking disabled for {fc_name}")
# Create a dictionary from the exported table
sourceFieldsList = ['GlobalID', area_field]
valueDict = {r[0]: r[1] for r in arcpy.da.SearchCursor(temp_Areas, sourceFieldsList)}
# Update the feature service using the dictionary
updateFieldsList = ['GlobalID', area_field]
print(f"\nUpdating {area_field} field in {fc_name}...\n")
with arcpy.da.UpdateCursor(featureservice, updateFieldsList) as updateRows:
for updateRow in updateRows:
keyValue = updateRow[0].upper()
if keyValue in valueDict:
if valueDict[keyValue] != updateRow[1]:
for n in range(1, len(sourceFieldsList)):
print(f"Updating {area_field} for {updateRow[0]} from {updateRow[1]} to {valueDict[keyValue]}")
updateRow[n] = valueDict[keyValue]
updateRows.updateRow(updateRow)
del valueDict
print(f"\n{area_field} field updated successfully for {fc_name}")
# Re-enable editor tracking
enable_params = {
"editorTrackingInfo": {
"enableEditorTracking": True,
"enableOwnershipAccessControl": True,
"allowOthersToUpdate": True,
"allowOthersToDelete": True
}
}
flc.manager.update_definition(enable_params)
print(f"Editor tracking re-enabled for {fc_name}\n")
endTime = time.time()
print(f"\nScript completed in {endTime - startTime} seconds.")
except Exception as e:
if "The Product License has not been initialized" in str(e):
e = "\n\n" + r"ArcGIS Pro license not available."
if "cannot open 'https" in str(e):
e = "\n\n" + r"Login to ArcGIS Online failed - check ArcGIS Pro on the FPC-GIS-01 server is logged in using an Admin account."
if "Exception: Failed to execute. Parameters are not valid." in str(e):
e = "\n\n" + r"Either login to ArcGIS Online failed - check ArcGIS Pro on the FPC-GIS-01 server is logged in using an Admin account, or Parameters need fixing if changes have been made in ArcGIS Online to the input layers."
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib, os
fromaddr = "GISNotifications@org.wa.gov.au"
toaddr = "gis@org.wa.gov.au"
msg = MIMEMultipart()
msg['From'] = fromaddr
msg['To'] = toaddr
msg['Subject'] = "Updating AreaHa field process failed (" + os.path.basename(__file__) + ")"
# Enter email body text below
body = "There has been an error in the script that checks and updates the AreaHa field in listed feature services. \n\nPlease check the script to troubleshoot any problems. \n\nException: " + str(e) # DON'T include URL's or links to the web in the body of the email or it might get blocked by the scam filters
msg.attach(MIMEText(body, 'plain'))
server = smtplib.SMTP('org-wa-gov-au.mail.protection.outlook.com')
#No login required so this section is commented out
#server.login("youremailusername", "password")
server.sendmail(fromaddr, toaddr, str(msg))
print ("\nScript failed - email notification sent to gis@org.wa.gov.au")
print ("\nException: " + str(e))
@LindsayRaabe_FPCWA wrote:
This script works when run in the interpretter, and has been running on our server when trigged by Task Scheduler overnight for some weeks. All of a sudden, now it doesn't.
You mention both the interpreter and a scheduled task, but it is unclear what doesn't work. Does the script fail when running both ways or just one?
Also, can you paste the text of the error message and traceback? Generally people don't do the best job of summarizing error messages, so it helps to see the exact and entire message.
Sorry - I was in a bit of a rush so glossed over it a bit. It was still working when run via the Interpreter. But when called by Task Scheduler it has started to fail. It returns the below exception:
Exception: cannot open 'https://services-ap1.arcgis.com/##########/arcgis/rest/services/PTNs_FPC416Polygons/FeatureServer/0'
The URL is referencing the first feature service in the csv file.
I just find this really bewildering. I write (however rough it might be compared the wizardry below from @HaydenWelch ) and test it. I set it up in Task Scheduler and monitor and it works just fine for weeks. Then all of a sudden I start getting these errors, but when I run it manually through the interpreter, it's perfectly happy.
I spent some time on Friday reconfiguring parts of the code to use Feature Layers instead of ExportFeatures from arcpy. That seemed to work mostly, but now I get a different error on just 1 layer in the list 🤦Will give Haydens code below a good crack and see if I can make that work.
All a good learning opportunity if nothing else!
I always hate messing around with the Results of arcpy functions and will avoid them if I can accomplish the task with cursors. I just always end up having some weird bug like this with the Result objects.
I would also thoroughly test my example before using it. The main point was to show how to set up logging and keep the final script flow as readable/modular as possible. Giant try/catch blocks are the best way to shoot yourself in the foot lol.
My major change from your original code was just doing the calculation in the UpdateCursor. I use projectAs then getArea to get the projected hectares, thenan equality check before writing the row to minimize updates.
Since it now only uses one search cursor (to validate fields) and one update cursor per feature you should be able to tell if the issue is with the permissions or not. Would also definitly make sure that the URLs are being passed properly to each cursor and to the feature managers, I wasn't exactly sure how you formatted them in the csv.
Thanks for the added context. I have been playing with your code to try and implement it. Found a couple of easy errors and fixes early on, but working on through my logic and error shooting is starting to do my head in. Starting to think it's time to hire in some professional python help to give all my coding the once over and fix it where necessary and improve it where possible to make it more robust (like in your logic) and potentially check other things like security.
I appreciate the time you've spent putting this together. Still valuable for myself as I try to improve my understand of python!
Adding proper logging and error handling to a script is always a pain. I would be lying if I said I don't frequently just let things fail then fix them when they break. When the script is meant to run unattended on a schedule though you kinda have to bite the bullet and make sure you can see exactly what's going on since you usually aren't sitting at the console when it runs to monitor the output.
It never hurts to have another set of eyes on your code either, I've found that writing something that works, no matter how convoluted, then going back and re-writing it from scratch a few times can really help. You'll pretty quickly identify areas that you can simplify, modularize, or just completely remove on the second or third pass.
If you notice, almost half the code I shared it comments and error handling. First step for me is always writing the function header with input types and return types, then writing a docstring stating the explicit goal of that function. That way you have something written down to check against, and even if you mess up the implementation, someone can come by later and say "Oh, this function says it's supposed to return this, but it's actually returning that!"
Great tips there - documentation so frequently doesn't get done well, or at all! I'm just as guilty as the next person there, though trying hard to change that. I don't want to be the one that lets it all fall apart in my absence!
This is a bit unrelated to this discussion, but you've sent me down a logging rabbit hole and I did come across this:
https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SMTPHandler
Seems like if you're doing a lot of email based logging in your unattended scripts, it could be a good alternative to rolling your own emailer code. Seems like it will send one email per logging call (depending on the configured filter level) so you could put them all in one message by using the global ERRORS list and formatting the last log as critical.
How are you executing the scheduled task? Is it running with the same credentials you are running the code from the interpreter? Considering OAuth access tokens last for 2 weeks, it is common for a cached token to make something work for 2 weeks and then it breaks. If you can explain more about how you are running the scheduled task and how you are authenticating with Portal, I might be able to offer more specific feedback.
The scheduled task is on our GIS Server (not Esri Enterprise - Microsoft). I'm logged in with a service account and the task is registered to use the same service account. Besides the login process you can see at the start of the code in my original post, I also ensure that ArcGIS Pro has been signed into ArcGIS Online using my admin account which owns all the content and has all privileges. I wish there was a way of not relying on the ArcGIS Pro login as that always seemed like the mostly likely point to fail, but even with all that done, it still decides it not going to work in it's original format.