Select to view content in your preferred language

Script to update area fields in feature service works in Interpreter but not when triggered by Task Scheduler

419
16
a week ago
LindsayRaabe_FPCWA
MVP Regular Contributor

I have the below script which does the following:

  1. Logs into ArcGIS Online
  2. Reads a feature service URL from a csv
  3. Uses that feature service URL to save a local copy of the data as a feature class
  4. Recalculates the relevant Area field in the dataset
  5. Disables the editor tracking on the feature service
  6. Uses UpdateCursor to match any updated records to the original in the feature service and updates the area value in the feature service
  7. Renables editor tracking
  8. Repeat for the next feature service in the csv. 

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))
Lindsay Raabe
GIS Officer
Forest Products Commission WA
0 Kudos
16 Replies
JoshuaBixby
MVP Esteemed Contributor

@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.

0 Kudos
LindsayRaabe_FPCWA
MVP Regular Contributor

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! 

Lindsay Raabe
GIS Officer
Forest Products Commission WA
HaydenWelch
MVP Regular Contributor

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.

LindsayRaabe_FPCWA
MVP Regular Contributor

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!

Lindsay Raabe
GIS Officer
Forest Products Commission WA
HaydenWelch
MVP Regular Contributor

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!"

LindsayRaabe_FPCWA
MVP Regular Contributor

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!

Lindsay Raabe
GIS Officer
Forest Products Commission WA
HaydenWelch
MVP Regular Contributor

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.

0 Kudos
JoshuaBixby
MVP Esteemed Contributor

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.

LindsayRaabe_FPCWA
MVP Regular Contributor

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. 

Lindsay Raabe
GIS Officer
Forest Products Commission WA
0 Kudos