Select to view content in your preferred language

Inset Maps using ArcPy

3 weeks ago
New Contributor

Hello I'm working on automating the process of making maps using ArcPy and an issue I'm running into is trying to make inset maps. I've successfully used ArcPy to find clusters of locations using DBSCAN, but I want to make an inset map, along with an extent indicator using ArcPy. Is this possible?

0 Kudos
1 Reply
Regular Contributor II

I just did a project with 3 inset maps. The project has four maps, "Main Map", "Overview 1".. etc

There is one layout, and it has frames for the main map and each overview map.

There is a script tool in the projects toolbox that I run to (1) set up the layout. I am also using a map series, so the script tool tries to pull in the current map series page as the default but you can enter the name for a page. When I run it, it changes the contents of the overviews to highlight and set the extent.

Some of the maps don't need all three overviews so on maps that don't need all of them the script makes those frame elements invisible.

Execution code


    This is the code that you see in the ArcGIS Pro toolbox, in the Properties->Execution box.
    This file gets reloaded into the script tool in ArcGIS Pro every time you save it.
import sys
from importlib import reload
import arcpy
import cc_taxmaps 

__version__ = "1.0"

if __name__ == "__main__":
    arcpy.AddMessage(f"ExportTaxmapTool {__version__}")

# Get the parameter values from ArcGIS Pro 
    layoutName = arcpy.GetParameterAsText(0)
    pageName = arcpy.GetParameterAsText(1)
    can_table = arcpy.GetParameterAsText(2)
    outputFolder = arcpy.GetParameterAsText(3)

    projectFile = 'CURRENT'
    tm = cc_taxmaps.BuildTaxMap(projectFile, layoutName, can_table)
    except Exception as e:
        arcpy.AddError(f"Export failed. {e}")

# That's all!




Validation code


import os
import arcpy

# I struggled and failed to get these constants to load from a separate file.
# Map names we need to have
MAIN_MAP = "Main Map"
# Name of the layer that should appear in the MAIN_MAP
CANCELLED_NUMBERS_TABLE = "cancelled_numbers"
# Use these settings if none were provided
DEFAULT_LAYOUT = "Taxmap_18x24"
DEFAULT_CANCELLED_NUMBERS_TABLE = 'W:\\ALL\\GIS\CONNECTIONS\\cc-sqlservers_WINAUTH.sde\\cancelled_numbers'
DEFAULT_MAP_INDEX = 'W:\\ALL\\GIS\CONNECTIONS\\cc-sqlservers_WINAUTH.sde\\taxlots_fd\\mapindex'

class ToolValidator:
  # Class to add custom behavior and properties to the tool and tool parameters.

    def __init__(self):
        # Set self.params for use in other validation methods.
        self.params = arcpy.GetParameterInfo()

    def initializeParameters(self):
        # Customize parameter properties. This method gets called when the tool is opened.
            0 layout
            1 page name
            2 cancel table
            3 output
        project ='CURRENT')
        layout = None
        layoutName = self.params[0].value # This is always just a string
        if not layoutName:
            # If no layout is selected, pick the first one in this project
            layout = project.listLayouts(wildcard='*')[0]
            self.params[0].value =
        ms = None
            layout = project.listLayouts(wildcard=layoutName)[0]
            ms = layout.mapSeries
        except Exception as e:
            self.params[0].setErrorMessage("Can't find layout or map series.")    
        # In the layout, figure out which page is selected, if any.
        self.params[1].value = ''
            self.params[1].value = ms.pageRow.PageName
        except Exception as e:
            self.params[1].setErrorMessage("Can't find pageName.")

        # Cancelled numbers table
        self.params[2].value = ''
            # Find the cancelled numbers table in the map.
            map = project.listMaps(wildcard=MAIN_MAP)
            self.params[2].value = map.listTables(wildcard=CANCELLED_NUMBERS_TABLE)[0]
        except Exception as e:
            self.params[2].setErrorMessage("Can't find table.")
        self.params[3].value = DEFAULT_PDF_FOLDER

    def updateParameters(self):
        # Modify the values and properties of parameters before internal
        # validation is performed.
        # Make sure can_table exists
        if not self.params[2].valueAsText:
            self.params[2].value = DEFAULT_CANCELLED_NUMBERS_TABLE

    def updateMessages(self):
        # Modify the messages created by internal validation for each tool
        # parameter. This method is called after internal validation.

        # Make sure the selected page name exists.
        layoutName = self.params[0].valueAsText
        pageName = self.params[1].valueAsText
            project ='CURRENT')
            layout = project.listLayouts(wildcard=layoutName)[0]        
        except Exception as e:
            self.params[1].setErrorMessage("Blimey, can't find layout")
            layout = None
        if layout:
            ms = None
                ms = layout.mapSeries
            except Exception as e:
                self.params[1].setErrorMessage("Blimey, can't find map series.")
            if pageName:
                    pageNum = ms.getPageNumberFromName(pageName)
                except Exception as e:
                    pageNum = None
                if not pageNum:
                    self.params[1].setErrorMessage("Blimey, I don't see that page name!")

        if not arcpy.Exists(self.params[2].valueAsText):
            self.params[2].setWarningMessage("Blimey, fix this or I won't be able to find any cancelled numbers!")
        # Make sure the output folder exists
        if not os.path.exists(self.params[3].valueAsText):
            self.params[3].setErrorMessage("Blimey, I can't do anything without a folder!")


    def isLicensed(self):
         # Set whether the tool is licensed to execute.
         return True

    # def postExecute(self):
    #     # This method takes place after outputs are processed and
    #     # added to the display.
    #     return

"Business logic" from



# Search for polygons
# Use them to set extent
# Generate a PDF for each extent, named after the polygon and with a numeric prefix. e.g. "01_shively.pdf"
# Set title to name from polygon
# Set extent polygon in key map
import sys
import os
import re
from importlib import reload
import datetime
from pypdf import PdfWriter
import arcpy
import ormapnum

__version__ = '1.0'

# Other scalebars have to have prefix followed by scale, eg "Scalebar 24000" for 1:24000 maps

MAXCANCELLEDROWS = 15 # Go to 8 point font if # of rows exceeds this

def make_table(cancellations:list, columns:int) -> tuple:
    """Break the list of cancellations into 'n' columns and return them.

        cancellations (list): list of cancellations
        columns (int): number of columns

        (int, list): int is the height of the tallest column and list contains a list for each column. 

    col = "" # Each column is a string with line breaks in it.
    maxy = int((len(cancellations)+columns-1)/columns) # max items to put in one column
    i = 0
    columns = []
    for item in cancellations:
        col += str(item).ljust(6) + "\n"
        i += 1
        if not i % maxy:
            col = "" # we're done with this
    if col: 
        # some left overs

    return maxy,columns

def load_cancellations(mapnumber: str, tablename: str) -> list:
    """Load the cancelled numbers table and return it as a list.
    cancellations = []
        with arcpy.da.SearchCursor(in_table=tablename, 
                                    where_clause=f"MapNumber='{mapnumber}'") as rows:
            cancellations = [row[1] for row in rows]
    except Exception as e:
        arcpy.AddError(f"Could not load cancelled numbers table. {e}")
    return cancellations
def sort_taxlots(mylist:list) -> list:
    """Sorts a list of taxlots. Deals with the weird suffixes like "M1"
    and sorts numerically (e.g. 100 is greater than 20)

        mylist (list): list to be sorted

        list: sorted list
    d = dict()
    for orig in mylist:

        # Taxlots can be a simple integer like "4900" or a string with something on the end like "400M1"
        # and they should sort into a proper list as if they were all just integers.

        mo ='^(\d+).*$', orig)
        s = orig
            # Add some leading zeros if the integer part is less than 8 digits
            # so, "100M1" will become "00000100M1"
            n =
            s = '0' * (8 - len(n)) + orig 
        except Exception as e:
        d[s] = orig # This will de-duplicate too.

    # Sort the dictionary. 
    # sorted() returns a list of keys.
    # Make the dict into a list of values
    # return the new list
    return [d[k] for k in sorted(d)]

class BuildTaxMap(object):

    # map the scale of the current map to the scalebar element to use
    scales = {
        120 : 'Scalebar 240',  
        240 : 'Scalebar 240',  
        360 : 'Scalebar 240',     
        480 : 'Scalebar 240',    
        600 : 'Scalebar 600',     
        720 : 'Scalebar 600',    
        1200 : 'Scalebar 1200', 
        2400 : 'Scalebar 600', 
        4800 : 'Scalebar 600',    
        9600 : 'Scalebar 24000', # no examples; untested
        12000 : 'Scalebar 24000', # no examples; untested
        24000 : 'Scalebar 24000',

    def __init__(self, projectFile, layoutName, can_table) -> None:

        self.project =
        self.cancelled_taxlots_table = can_table
        self.layout = self.project.listLayouts(wildcard=layoutName)[0] = self.layout.mapSeries
        self.orm = None
        self.pageNames = None
        #arcpy.AddMessage(f"Layout name={}")
        # Make a dictionary of all the scalebar elements in this layout.
        self.scalebar_elements = {}
        for elem in self.layout.listElements(element_type="MAPSURROUND_ELEMENT", wildcard="Scalebar*"):
            name =
            self.scalebar_elements[name] = elem
    def findPageNum(self, pageName: str) -> int:
        """Find the page number for a given pageName. Map series has to be set.

            pageName (str): A page name from the map series.

            pageNum (int) or None
        pageNum = None
        if pageName:
                pageNum =
            except Exception as e:
                pageNum = None
        return pageNum

    def range(self, firstPageName=None, lastPageName=None) -> list:
        """Return a list of page names

        Be careful using first and last, because the sort order is not
        what you expect. Leave them empty to get a complete list.

            firstPageName (str): start of range, use first page if None
            lastPageName (str): end of range, use last page if None

            list: page names found
        pageNames = list()
        layer =

        # Cartographers are not maintaining PageNumber
        # which is why we can't use it to sort the table.
        #fields = ['PageNumber','PageName']
        fields = ['MapNumber','PageName'] # IGNORE unmaintained PageNumber

        started = False
        if not firstPageName:
            started = True # Begin at the first page

        # The table is not automatically sorted out,
        # we have to read it and sort it before finding our fields

        if not self.pageNames: # Only hit the database once
            # Create a dictionary with the pageNumber as the key
            # so that when we sort it, the items end up in the correct order.
            # Then generate a sorted list of pageNames from that and cache it.
            d = {}
            i = 0
            with arcpy.da.SearchCursor(layer,fields) as rows:
                for row in rows:
                    if row[0] in d:
                        arcpy.AddMessage(f"Duplicate pageNumber {row[0]}, {row[1]}!!")
                    d[row[0]] = row[1]
                    i += 1
                self.pageNames = [d[i] for i in sorted(d)]
            print(len(d), len(self.pageNames)) # sanity check, should be 1082 (or more)

        for pageName in self.pageNames:
            if pageName == firstPageName:
                started = True
            if started:
            if pageName == lastPageName:

        return pageNames
    def find_scalebar(self, mapscale: int) -> arcpy._mp.MapSurroundElement:
        """Given a mapscale like 24000, find the scalebar to use with this map.

            mapscale (int): Scale from a known set; 1200, 4800, etc; 
            if there is no match, returns the default which is the scalebar on the layout
            arcpy._mp.MapSurroundElement: An element from the layout we're working with.

        sb = self.scalebar_elements["Scalebar"] # The scalebar on the layout MUST be named "Scalebar"!!
            sb = self.scalebar_elements[self.scales[mapscale]]
        except KeyError:
            print("scalebar key error")
        return sb

    def setPlotDate(self) -> None:
        # Set plot date (because the dateExported property fails!)
        d =
        plotDate = d.strftime("PLOT DATE: %x")
        text_element = self.layout.listElements(element_type='Text_Element', wildcard="Plot Date")[0]
        text_element.text = plotDate

    def setCountyOverview(self) -> None: 
        # note, extent of the county map never needs to change, 
        # the whole county always in view
        county_overview_map = self.project.listMaps(wildcard="County Overview Map")[0]

        # Highlight the township
        highlight_layer = county_overview_map.listLayers(wildcard="Highlight")[0]
        highlight_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql':, 'isActive': True}])
        #arcpy.AddMessage(f"County overview Highlight query={highlight_layer.listDefinitionQueries()}")

        # Set def query to turn off the township so highlight label shows (instead of both showing)
        township_layer = county_overview_map.listLayers(wildcard="Townships")[0]
        township_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': f"NOT({})", 'isActive': True}])
        #arcpy.AddMessage(f"County overview Township query={township_layer.listDefinitionQueries()}")


    def setTownshipOverview(self, frame) -> None:
        # Note that pagename queries don't work here, we have to use definition queries.
        township_overview_map = self.project.listMaps(wildcard="Township Overview Map")[0]

        # Highlight the section
        highlight_layer = township_overview_map.listLayers(wildcard="Highlight")[0]
        highlight_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.trs, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Highlight query={highlight_layer.listDefinitionQueries()}")

        # Outline the township
        township_layer = township_overview_map.listLayers(wildcard="Township")[0]        
        township_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql':, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Township query={township_layer.listDefinitionQueries()}")

        # Turn off the sections outside the current section.
        # Turn off the selected section in "Sections" (highlight layer will BOLD it the label.)
        sections_layer = township_overview_map.listLayers(wildcard="Sections")[0]
        sections_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.trsonly, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Sections query={sections_layer.listDefinitionQueries()}")

        background_layer = township_overview_map.listLayers(wildcard="Background")[0]
        background_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.notsection, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Background query={background_layer.listDefinitionQueries()}")

        # Adjust extent to center on the township (not the section or taxmap)
            rows = arcpy.da.SearchCursor(township_layer, ['SHAPE@', 'tr'])
            row = # There should only be one feature if the definition query actually worked?
            extent = row[0].extent
            arcpy.AddMessage(f"In township map, using extent of township {row[1]}")
            padding = (extent.XMax - extent.XMin) / 10
            new_extent = arcpy.Extent(extent.XMin - padding, extent.YMin - padding,
                extent.XMax + padding, extent.YMax + padding,
                None, None, None, None,
            frame.visible = True
        except Exception as e:
            arcpy.AddMessage(f"Can't show township overview. {e}")
            del rows # release the lock

    def setSectionOverview(self, frame) -> None:
        # Page Query on Current Taxmap selects the correct taxmap shape
        section_overview_map = self.project.listMaps(wildcard="Section Overview Map")[0]

        highlight_layer = section_overview_map.listLayers(wildcard="Highlighted Section")[0]
        highlight_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.trs, 'isActive': True}])
        #arcpy.AddMessage(f"Highlight query={highlight_layer.listDefinitionQueries()}")

        # Turn off the selected section in "Sections" (highlight layer will BOLD it the label.)
        sections_layer = section_overview_map.listLayers(wildcard="Sections")[0]
        sections_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': f"NOT({self.trs})", 'isActive': True}])
        #arcpy.AddMessage(f"Section Overview Sections query={sections_layer.listDefinitionQueries()}")

        # Adjust extent to center on the section (not the Current Taxmap) 
            rows = arcpy.da.SearchCursor(highlight_layer, ['SHAPE@', 'SECTION'])
            row = # There should only be one feature if the definition query actually worked?
            extent = row[0].extent
            padding = (extent.XMax - extent.XMin) / 10
            new_extent = arcpy.Extent(extent.XMin - padding, extent.YMin - padding,
                extent.XMax + padding, extent.YMax + padding,
                None, None, None, None,
            arcpy.AddMessage(f"In section map, using extent of section {row[1]}")
            frame.visible = True
        except Exception as e:
            arcpy.AddMessage(f"Can't show section overview. {e}")
            del rows # release the lock

    def adjust_scalebar_element(self):
            scale =
            settings = self.find_scalebar(scale) # These are the settings we want
            sb_element = self.scalebar_elements["Scalebar"] # This is the one we see
            #print(scale,, sb_element.elementWidth)
            if settings != sb_element:
                # Copy settings from desired scalebar
                #   Some things simply are ignored if you set them on the element;
                #   you have to set them in the CIM definition.

                new_cim = settings.getDefinition("V3")
                sb_cim = sb_element.getDefinition("V3")
                sb_cim.unitLabel = new_cim.unitLabel
                sb_cim.units = new_cim.units
                sb_element.elementWidth = settings.elementWidth


        except Exception as e:
            arcpy.AddError(f"No scale set for this map. {e}")

    def populate_cancel_table(self, cancellations:list) -> None:

        # Find the cancel table elements and put them in order
        elements = self.layout.listElements(element_type='Text_Element', wildcard="can_col*")
        ncols = len(elements)
        column_elements = [None] * ncols # Make a list of the right size
        for e in elements:
            mo ='^can_col(\d+)$',
            if mo:
                n = int(
                column_elements[n] = e
                e.text = " " # This element has some text in it (event just a single space) so ArcMap does not "lose" it.
        # Fill in the table in an aesthetically pleasing fashion.
        maxy, table = make_table(cancellations, ncols)
        # Adjust the font size of the table according to the number of rows
        fontsize = 10
        if maxy > MAXCANCELLEDROWS:
            fontsize = 8

        x = 0
        for column in table:
            column_elements[x].text = column
            column_elements[x].fontSize = fontsize
            x += 1

    def ConfigureLayout(self, pageName) -> None:
        """ Change the current layout to set it up for the named page. """
        self.pageName = pageName
        pageNum =

        # Fix sundry metadata (titles etc)
        self.setPlotDate() = pageNum
        pageRow =

        # Parse the ormapnum from the current page to break out all its fields
        self.orm = ormapnum.ORMapNum()
        cancellations = load_cancellations(self.orm.dotted, self.cancelled_taxlots_table) = f"TR = '{self.orm.township}{self.orm.township_dir}{self.orm.range}{self.orm.range_dir}'"
        self.trs = f"TOWN = '{self.orm.township}' AND RANGE = '{self.orm.range}' AND SECTION = '{self.orm.section}'"
        self.trsonly = f"(TOWN = '{self.orm.township}' AND RANGE = '{self.orm.range}') AND NOT({self.trs})"
        self.notsection = f"NOT(TOWN = '{self.orm.township}' AND RANGE = '{self.orm.range}')"

        top_title = self.layout.listElements(element_type='Text_Element', wildcard="Top Title")[0]
        bottom_title = self.layout.listElements(element_type='Text_Element', wildcard="Bottom Title")[0]
        plss_description = self.layout.listElements(element_type='Text_Element', wildcard="PLSS Description")[0]

        #print(f"short title=\"{self.orm.shortmaptitle}\"" long title=\"{self.orm.longmaptitle}\"")
        top_title.text = bottom_title.text = self.orm.shortmaptitle
        plss_description.text = self.orm.longmaptitle


        township_overview_frame = self.layout.listElements(element_type="MAPFRAME_ELEMENT", wildcard="Township Overview Frame")[0]
        township_overview_frame.visible = False
        section_overview_frame = self.layout.listElements(element_type="MAPFRAME_ELEMENT", wildcard="Section Overview Frame")[0]
        section_overview_frame.visible = False
        if self.orm.section:


        # Fix up the cancelled numbers table, or hide it if it's empty.
        can_table = self.layout.listElements(element_type='GROUP_ELEMENT', wildcard="Cancelled Numbers Table")[0]
        if len(cancellations):

            cancellations = sort_taxlots(cancellations)
            can_table.visible = True

            # Position the table based on which overview maps are visible
            if not township_overview_frame.visible:
                can_table.elementPositionY = township_overview_frame.elementPositionY
            elif not section_overview_frame.visible:
                can_table.elementPositionY = section_overview_frame.elementPositionY
                can_table.elementPositionY = section_overview_frame.elementPositionY - section_overview_frame.elementHeight - 0.20

            #arcpy.AddMessage(f"can_table y-pos set to {can_table.elementPositionY}")
            can_table.visible = False


    def show_elements(self) -> None:
        """ This is just to help in development """
        # Find all the layout elements
        main_elements = self.layout.listElements(element_type="MAPFRAME_ELEMENT", wildcard="*")
        print("Map Frames:")
        for e in main_elements:
        print("Text Elements:")
        text_elements = self.layout.listElements(element_type='Text_Element', wildcard="*")
        for e in text_elements:
        print("Scale bars")
        scalebar_elements = self.layout.listElements(element_type='MAPSURROUND_ELEMENT', wildcard="scale*")
        for e in scalebar_elements:
    def ExportPdf(self, outputFolder: str) -> bool:
        """Generate a Taxmap PDF file.

            outputFolder (_type_): An existing folder to hold output files.

            bool: True if PDF was successfully written.

        # Generate a PDF without metadata.
            # Export the map to the scratch folder
            scratch_output_pathname = os.path.join(arcpy.env.scratchFolder, self.orm.filename + "_scratch.pdf")
        except Exception as e:
            if not self.orm:
                arcpy.AddError(f"Did you call ConfigureLayout? {e}")
                arcpy.AddError(f"Error in export; is file open (in Acrobat maybe)? {e}")
            return False
        projfile = os.path.join(self.project.homeFolder, self.project.filePath)
        person = os.environ['USERNAME']

        # Update the metadata on the PDF file. Then delete the scratch file.
            final_output_pathname = os.path.join(outputFolder, self.orm.filename + ".pdf")
            metadata = {
                '/Title': f"Taxmap {self.pageName}",
                '/Subject': self.orm.longmaptitle + " Clatsop County, Oregon",
                '/Author':  f"{person} {}",
                '/Creator': f'{projfile}', #  Shows as "Application" in Acrobat
                '/Producer': f"{__file__} {__version__}", # Shows as "PDF Producer"
                '/Keywords': ','.join(['property tax', 'map', 'gis']),
            arcpy.AddMessage(f"Exported to {final_output_pathname}")
        except Exception as e:
            arcpy.AddError(f"Error in export. {e}")
            # Delete the file without the metadata

        return True

# =====================================================================================
if __name__ == "__main__":

    DEFAULT_PROJECT_FILE = "taxmaps.aprx"
    DEFAULT_CAN_TABLE = "W:\\ALL\\GIS\\CONNECTIONS\\cc-sqlservers_WINAUTH.sde\\cancelled_numbers"


    tm = BuildTaxMap(DEFAULT_PROJECT_FILE, "Taxmap_18x24", can_table=DEFAULT_CAN_TABLE)
    pageNames = (tm.range(None, None))
    print(f"There are {len(pageNames)} pages in the index.")

#    pageNames = (tm.range('4 06', '4 09'))
#    print(f"{len(pageNames)} pages will be exported.")

    # This list of page names tests every aspect of the process.
    sample_list = [   
        # scales
        "4 06",         # 1:24000 "2000 Scale"
        "8 09",         # 1:24000 "2000 Scale"
        "5 10 19AD D2", # 1:120     "10 Scale"
        "5 10 30AA D1", # 1:240     "20 Scale"
        "6 10 3 D2",    # 1:360     "30 Scale"
        "6 10 16DD D3", # 1:480     "40 Scale"
        "8 09 18BA D2", # 1:600     "50 Scale"
        "6 10 4DA D4",  # 1:720     "60 Scale"
        "4 07 3BC",     # 1:1200   "100 Scale"
        "5 07 29A",     # 1:2400   "200 Scale"  SCALEBAR shows 0.1 Mile
        "4 09 22",      # 1:4800   "400 Scale"  SCALEBAR shows 0.2 Mile
        #"NONE",        # 1:9600   "800 Scale"
        #"NONE",        # 1:12000 "1000 Scale"
    print(f"{len(sample_list)} sample pages will be exported.")

    for pageName in pageNames:

        tm = BuildTaxMap(DEFAULT_PROJECT_FILE, "Taxmap_18x24", can_table=DEFAULT_CAN_TABLE)
        print(f"Page {pageName}")



        scale =
        sbe = tm.find_scalebar(scale)
        print(f"1:{scale} {sbe.longName}")

        outputFolder = "C:\\Temp\\pdflib" # testing

        del tm # Attempt to release the project so APRX can be updated

# That's all!



0 Kudos