From 7774dd5dad0ba3616818e5a682facd9452cb62bf Mon Sep 17 00:00:00 2001 From: AHamptonGA <106251341+GeoCodable@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:20:17 -0600 Subject: [PATCH] Initial commit --- .gitignore | 142 ++++++ LICENSE | 24 ++ README.md | 140 +++++- helloWorldToolbox.pyt | 1 + pyt_meta_pip_helper.py | 46 ++ setup.py | 58 +++ src/pyt_meta.py | 948 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1357 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 helloWorldToolbox.pyt create mode 100644 pyt_meta_pip_helper.py create mode 100644 setup.py create mode 100644 src/pyt_meta.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be10025 --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +# ArcGIS xml metadata files +*.pyt.xml + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.pyc +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md index d2f3d5b..b17f932 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,138 @@ -# ArcGIS-Python-Toolbox-Metadata - +## python toolbox metadata add-on +Python toolbox metadata add-on or pyt_meta is a module that contains classes and functions that enable automated xml metadata file generation for ArcGIS toolboxes and/or any tools contained in a given toolbox. The pyt_meta module enables default metadata value generation based on the toolbox and tool class attributes/properties and the ArcGIS portal user profile. When geospatial developers wish to override and explicitly control metadata or values, the module enables developers to have full control and access to the xml metadata keys directly within the python toolbox code. As an added benefit among toolbox/tool deployments, the maintenance and packaging of XML support file documents can be eliminated from the process. This results in less file dependencies and increased toolbox deployment reliability and efficient. + +### Origin +pyt_meta was developed at the National Geospatial-Intelligence Agency (NGA) by a federal government employee in the course of their official duties, so it is not subject to copyright protection and is in the public domain in the United States. + +You are free to use the core public domain portions of pyt_meta for any purpose. Modifications back to the cores of any dependency functions are subject to the original licenses and are separate from the core public domain work of pyt_meta. + +### Transparency +NGA is posting code created by government officers in their official duties in transparent platforms to increase the impact and reach of taxpayer-funded code. + +### Pull Requests +If you'd like to contribute to this project, please make a pull request. We'll review the pull request and discuss the changes. This project is in the public domain within the United States and all changes to the core public domain portions will be released back into the public domain. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest. Modifications to dependencies under copyright-based open source licenses are subject to the original license conditions. + +### Requirements +The pyt_meta package is an addon feature to ArcGIS python toolboxes. As such, ArcGIS Desktop or ArcGIS Pro with python 3.6 or higher is required to take advantage of the package capabilities. + + python 3.6+ + ArcGIS desktop 10.8+ + or + ArcGIS Pro 2.0+ + +### Quick-start + +Install: + + For explicit instructions on your local system, download the pyt_meta_pip_helper.py script below. + Once downloaded, right click on the script, and select 'Run with ArcGIS Pro'. + The script will create tailored instructions based on the location of the local ArcGIS install and active virtual environment. + + https://github.com/ngageoint/python-toolbox-metadata-addon/blob/main/pyt_meta_pip_helper.py + + Alternatively follow the instructions below to pip install pyt_meta: + Locate the active python environment/venv for ArcGIS: + + Open IDLE from a local ArcGIS install + + In the python shell run: + + >>> import sys, os + + >>> print(os.path.dirname(os.path.realpath(sys.executable))) + + + pyt_meta can then be installed from the command line using pip: + + cd to the active python env identified in the step above + + Ex. $ cd "C:\Users\...\Pro\bin\Python\envs\arcgispro-py3" + + (optional) ensure the latest version of pip: + + $ pip install --upgrade pip + + Perform the pip install from the GitHub repo: + + $ pip install git+https://github.com/ngageoint/python-toolbox-metadata-addon + +Usage: + + Import: + + Key behavior: + - In create_tb_meta, "__file__" is the current file path for the .pyt (python toolbox) + - In create_tb_meta, True allows pyt_meta to overwrite existing xml metadata files if they exist. + + ***Note: See “helloWorldToolbox.pyt” toolbox for detailed import statement usage*** + + Import statement example for python toolbox: + ##--------------------------------------------------------## + if __name__ == "__main__": + # call to generate metadata from toolbox + from pyt_meta import create_tb_meta + tb_meta = create_tb_meta(__file__, True) + ##--------------------------------------------------------## + + + Defining XML Element Properties: + + Key behavior: + - Tags can be added or removed to sync with other metadata styles. + - To alter the metadata style, modifiy the default xml structure variables: + - DEFAULT_TOOLBOX_XML_STRUCT + - DEFAULT_TOOL_XML_STRUCT + - Metadata declared at the tool level will override toolbox level metadata. + - In absence of explicit values, pyt_meta will apply default / derived metadata values. + + ***Note: See “helloWorldToolbox.pyt” toolbox for both tool and toolbox property code samples*** + + Example of defining metadata properties within the toolbox object: + ##--------------------------------------------------------## + class Toolbox(object): + def __init__(self): + self.idPurp = '''Some description of the toolbox + that is a multiline string''' + self.searchKeys = ['hello', 'world', 'toolbox'] + self.idAbs = '''Some abstract of the toolbox + that is a multiline string''' + self.idCredit = '''Point of Contact (POC): Jane Doe + Organization: Hoolie INC. + Email: Jane.Doe@hoolie.co''' + self.useLimit = '''Copyright Notice: Hoolie explicitly owns + all rights to this toolbox.''' + self.formatName = 'ArcToolbox Toolbox' + self.mdDateSt = '20210701' + ##--------------------------------------------------------## + +### Dependencies +Most dependencies listed below are licensed under the Python Software Foundation (PSF) as distributed with the python 3 standard library. No changes were made directly to the core packages below. The original function was simply called or re-used. The arcpy package is an optional dependency of pyt_meta, but highly encouraged to enhance default metadata attribution. + +importlib + +xml + +os + +sys + +re + +time (Zope Public License) + +datetime + +inspect + +pathlib (MIT) + +collections + +subprocess + +shutil + +warnings + +arcpy (ESRI) -- Optional + diff --git a/helloWorldToolbox.pyt b/helloWorldToolbox.pyt new file mode 100644 index 0000000..11bee2c --- /dev/null +++ b/helloWorldToolbox.pyt @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- import arcpy ##--------------------------------------------------------## if __name__ == "__main__": # call to generate metadata from toolbox from pyt_meta import create_tb_meta tb_meta = create_tb_meta(__file__, True) ##--------------------------------------------------------## class Toolbox(object): def __init__(self): """Define the toolbox (the name of the toolbox is the name of the .pyt file).""" """Define the toolbox (the name of the toolbox is the name of the .pyt file).""" self.label = "Hello World Toolbox" self.alias = "helloworldtoolbox" # required to call custom tools via python scripts controls name and alias properties self.description = '''Some description of the toolbox that is a multiline string''' # List of tool classes associated with this toolbox self.tools = [helloTool, byeTool] ## Example of available metadata tags and values for toolbox level ## metadata within the standard ESRI "Item Description metadata style" ## *Tags can be added or removed to sync with additional metadata styles* ## *Metadata declared at the tool level will override toolbox level metadata* ## ____________________________________________________________ ## self.CreaDate = '20210720' ## self.CreaTime = '17120606' ## self.ArcGISFormat = '1.0' ## self.SyncOnce = 'TRUE' ## self.ModDate = '20210621' ## self.ModTime = '13165353' ## self.minScale = '150000000' ## self.maxScale = '5000' ## self.ArcGISProfile = 'ItemDescription' ## self.arcToolboxHelpPath = 'D:\Help\gp' ## self.resTitle = 'helloWorldToolbox' ## self.idPurp = '''Some description of the toolbox ## that is a multiline string''' ## self.searchKeys = ['hello', 'world', 'toolbox'] ## self.idAbs = '''Some abstract of the toolbox ## that is a multiline string''' ## self.idCredit = '''Point of Contact (POC): Jane Doe ## Organization: Hoolie INC. ## Email: Jane.Doe@hoolie.co''' ## self.useLimit = '''Copyright Notice: Hoolie explicitly owns ## all rights to this toolbox.''' ## self.formatName = 'ArcToolbox Toolbox' ## self.mdDateSt = '20210701' ## ____________________________________________________________ class helloTool(object): def __init__(self): """Define the tool (tool name is the name of the class).""" self.label = "Hello Tool" self.category = 'Tool examples' self.description = '''This tool prints a simple hello statement in ArcGIS Pro''' self.canRunInBackground = False ## Example of available metadata tags and values for tool level ## metadata within the standard ESRI "Item Description metadata style" ## *Tags can be added or removed to sync with additional metadata styles* ## *Metadata declared at the tool level will override toolbox level metadata* ## ____________________________________________________________ ## self.CreaDate = '20210720' ## self.CreaTime = '17120606' ## self.ArcGISFormat = '1.0' ## self.SyncOnce = 'TRUE' ## self.ModDate = '20210701' ## self.ModTime = '16591414' ## self.minScale = '150000000' ## self.maxScale = '5000' ## self.arcToolboxHelpPath = 'D:\Help\gp' ## self.summary = '''This tool prints a simple ## hello statement in ArcGIS Pro''' ## self.usage = '''This tool is useful in printing a simple ## hello statement in ArcGIS Pro''' ## ## self.scriptExamples = {'Code Sample 1 ': { ## 'desc' : 'Sample 1 of the Hello World tool code in python:', ## 'code' : ['def hello_world_func():', ## ' print("Hello World!")'] ## }, ## 'Code Sample 2 ': { ## 'desc' : 'Sample 2 of the Hello World tool code in python:', ## 'code' : ['def hello_world_func():', ## ' print("Hello World * 2!")'] ## } ## } ## ## self.resTitle = 'helloworldtoolbox.(Uncategorized)' ## self.idCredit = '''Point of Contact (POC): Jill Doe ## Organization: Hoolie INC. ## Email: Jill.Doe@hoolie.co''' ## self.useLimit = '''Copyright Notice: Hoolie explicitly owns ## all rights to this toolbox. ''' ## self.searchKeys = ['hello', 'world', 'tool'] ## self.formatName = 'ArcToolbox Tool' ## self.mdDateSt = '20210701' ## ____________________________________________________________ def getParameterInfo(self): """Define parameter definitions""" p0 = arcpy.Parameter( displayName= 'Input First Name', name='textInput1', datatype='GPString', parameterType='Required', direction='Input') ## p0.dialogReference = 'Custom tool parameter dialog/label text' ## p0.pythonReference = 'Custom python reference information' p1 = arcpy.Parameter( displayName= 'Input Last Name', name='textInput2', datatype='GPString', parameterType='Optional', direction='Input') ## p1.dialogReference = 'Custom tool parameter dialog/label text' ## p1.pythonReference = 'Custom python reference information' p2 = arcpy.Parameter( displayName= 'Address', name='addrInput', datatype='GPString', parameterType='Optional', direction='Input') ## p2.dialogReference = 'Custom tool parameter dialog/label text' ## p2.pythonReference = 'Custom python reference information' p3 = arcpy.Parameter( displayName= 'Address Type', name='addrTypeInput', datatype='GPString', parameterType='Required', direction='Input') p3.filter.type = "ValueList" p3.filter.list= ['Home', 'Work', 'Other' ] ## p3.dialogReference = 'Custom tool parameter dialog/label text' ## p3.pythonReference = 'Custom python reference information' params = [p0, p1,p2, p3] return params def isLicensed(self): """Set whether tool is licensed to execute.""" return True def updateParameters(self, parameters): """Modify the values and properties of parameters before internal validation is performed. This method is called whenever a parameter has been changed.""" return def updateMessages(self, parameters): """Modify the messages created by internal validation for each tool parameter. This method is called after internal validation.""" return def execute(self, parameters, messages): """The source code of the tool.""" # execute some code last_name = parameters[1].valueAsText if not parameters[1].value: last_name = '' message = 'Hello world and {0} {1}!'.format( parameters[0].valueAsText, last_name) arcpy.AddMessage(message) return class byeTool(object): def __init__(self): """Define the tool (tool name is the name of the class).""" self.label = "Bye Tool" self.category = 'Tool examples' self.description = '''This tool prints a simple goodbye statement in ArcGIS Pro''' self.canRunInBackground = False ## Example of available metadata tags and values for tool level ## metadata within the standard ESRI "Item Description metadata style" ## *Tags can be added or removed to sync with additional metadata styles* ## *Metadata declared at the tool level will override toolbox level metadata* ## ____________________________________________________________ ## self.CreaDate = '20210720' ## self.CreaTime = '17120606' ## self.ArcGISFormat = '1.0' ## self.SyncOnce = 'TRUE' ## self.ModDate = '20210701' ## self.ModTime = '16591414' ## self.minScale = '150000000' ## self.maxScale = '5000' ## self.arcToolboxHelpPath = 'D:\Help\gp' ## self.summary = '''This tool prints a simple ## bye statement in ArcGIS Pro''' ## self.usage = '''This tool is useful in printing a simple ## bye statement in ArcGIS Pro''' ## self.title = 'Bye Tool: Code Sample (1)' ## self.para = '''Some notes about the code sample(s)''' ## self.code = '''# import the toolbox as a module ## import arcpy ## arcpy.ImportToolbox(r'D:\folderA\helloWorldToolbox.pyt', ## r'helloworldtoolbox') ## ## # call the tool and return the output ## result = arcpy.byeTool_helloworldtoolbox( ## textInput1 #Input First Name- Type(String), ## textInput2 #Input Last Name- Type(String) ## )''' ## self.resTitle = 'helloworldtoolbox.(Uncategorized)' ## self.idCredit = '''Point of Contact (POC): Jermey Doe ## Organization: Hoolie INC. ## Email: Jermey.Doe@hoolie.co''' ## self.useLimit = '''Copyright Notice: Hoolie explicitly owns ## all rights to this toolbox. ''' ## self.searchKeys = ['bye', 'world', 'tool'] ## self.formatName = 'ArcToolbox Tool' ## self.mdDateSt = '20210701' ## ____________________________________________________________ def getParameterInfo(self): """Define parameter definitions""" p0 = arcpy.Parameter( displayName= 'Input First Name', name='textInput1', datatype='GPString', parameterType='Required', direction='Input') ## p0.dialogReference = 'Custom tool parameter dialog/label text' ## p0.pythonReference = 'Custom python reference information' p1 = arcpy.Parameter( displayName= 'Input Last Name', name='textInput2', datatype='GPString', parameterType='Optional', direction='Input') ## p1.dialogReference = 'Custom tool parameter dialog/label text' ## p1.pythonReference = 'Custom python reference information' params = [p0, p1] return params def isLicensed(self): """Set whether tool is licensed to execute.""" return True def updateParameters(self, parameters): """Modify the values and properties of parameters before internal validation is performed. This method is called whenever a parameter has been changed.""" return def updateMessages(self, parameters): """Modify the messages created by internal validation for each tool parameter. This method is called after internal validation.""" return def execute(self, parameters, messages): """The source code of the tool.""" # execute some logic last_name = parameters[1].valueAsText if not parameters[1].value: last_name = '' message = 'Goodbye and farewell {0} {1}!'.format( parameters[0].valueAsText, last_name) arcpy.AddMessage(message) return \ No newline at end of file diff --git a/pyt_meta_pip_helper.py b/pyt_meta_pip_helper.py new file mode 100644 index 0000000..db70d4c --- /dev/null +++ b/pyt_meta_pip_helper.py @@ -0,0 +1,46 @@ +import os, sys, time + +# ----------------------------------------------------------------------------- + +def get_pip_cmds(pacakge_name, github_url, wait_sec=15) : + '''Function prints instructions to perform pip installs from + a GitHUb repo. After instructions are printed, a command + line (cmd) terminal will be launched for the user after a + wait period. + :param - pacakge_name - name of package to be installed + :param - github_url - repo & version https url + :param - wait_sec - seconds to wait before opening cmd + :returns - none - + ''' + # create the standard instructions + insts = [ + '***Instructions to install: {0}***'.format(pacakge_name), + ' A command line terminal will open after {0} seconds'.format(wait_sec), + ' Note: "pip install --upgrade pip" is a recommended, but optional command\n', + ' Copy each line below into the open command line terminal one at a time:', + '-' * 115 + '\n', + ' cd "{0}"'.format(os.path.dirname(os.path.realpath(sys.executable))), + ' pip install --upgrade pip', + ' pip install git+{0}'.format(github_url), + '\n' + '-' * 115, ] + # print the instructions + [print(_) for _ in insts] + + # give the user time to read the instructions before opening cmd + print('The command line will open in:') + for _ in range(wait_sec,0,-1): + if _ > 0: + print(_, end='...\r') + time.sleep(1) + + # open cmd + os.system("start /wait cmd") + print('\nRemember to post issues, questions and feedback on GitHub @:\n\t', github_url) + +# ----------------------------------------------------------------------------- + +pacakge_name = 'pyt_meta' +github_url = r'https://github.com/ngageoint/python-toolbox-metadata-addon' +get_pip_cmds(pacakge_name, github_url, 20) + +# ----------------------------------------------------------------------------- diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b429f45 --- /dev/null +++ b/setup.py @@ -0,0 +1,58 @@ +import setuptools + +with open('README.md', 'r') as fh: + long_description = fh.read() + +setuptools.setup( + name='pyt_meta', # name of the package + version='0.0.1', # release version + author='NGA', # org/author + description=\ + ''' + Description: + The pyt_meta module contains classes and functions that enable automated + xml metadata file generation for ArcGIS toolboxes and/or any tools contained + in a given toolbox. The pyt_meta module enables default metadata + value generation based on the toolbox and tool class attributes/properties + and the ArcGIS portal user profile. When geospatial developers wish to override + and explicitly control metadata, values, the module enables developers full control + and access to the xml metadata directly within the python toolbox code. As an + added benefit among toolbox/tool deployments, maintenance and packaging of xml + support file documents can be eliminated from the process. This results in less file + dependencies and can make toolbox deployments more reliable and efficient. + ''', + long_description=long_description, # long description read from the the readme file + long_description_content_type='text/markdown', + packages=setuptools.find_packages(), # list of all python modules to be installed + classifiers=[ # information to filter the project on PyPi website + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Natural Language :: English', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + ], + python_requires='>=3.6', # minimum version requirement of the package + py_modules=['pyt_meta'], # name of the python package + package_dir={'':'src'}, # directory of the source code of the package + install_requires=[ # package dependencies + + ## Python 3 Standard Library Members + ##----------------------------------------- + ## 'importlib', + ## 'xml', + ## 'os', + ## 'sys', + ## 're', + ## 'time', + ## 'datetime', + ## 'inspect', + ## 'pathlib', + ## 'collections', + ## 'subprocess', + ## 'shutil', + ## 'warnings' + ##----------------------------------------- + ] + ) diff --git a/src/pyt_meta.py b/src/pyt_meta.py new file mode 100644 index 0000000..c6195b1 --- /dev/null +++ b/src/pyt_meta.py @@ -0,0 +1,948 @@ +# -*- coding: utf-8 -*- + +# ----------------------------------------------------------------------------- + +import importlib.util +import os +import sys +import re +import time +import inspect +import warnings +from pathlib import Path +from collections import OrderedDict +from datetime import datetime +from xml.etree.ElementTree import \ + ElementTree as et_root, \ + Element as et_elm, \ + SubElement as et_se +# ----------------------------------------------------------------------------- + +__name__ = 'pyt_meta' + +__info__ = \ + ''' +Description: + The pyt_meta module contains classes and functions that enable automated + xml metadata file generation for ArcGIS toolboxes and/or any tools contained + in a given toolbox. The pyt_meta module enables default metadata + value generation based on the toolbox and tool class attributes/properties + and the ArcGIS portal user profile. When geospatial developers wish to override + and explicitly control metadata, values, the module enables developers to have full control + and access to the xml metadata keys directly within the python toolbox code. As an + added benefit among toolbox/tool deployments, maintenance, and packaging of xml + support file documents can be eliminated from the process. This results in less file + dependencies and can make toolbox deployments more reliable and efficient. + ''' +__alias__ = 'pyt_meta' + +__author__ = 'A. Hampton' + +__version__ = '0.0.1' + +__create_date__ = '20201124' + +__modified_date__ = '20210727' + +__all__ = \ + [ + 'import_toolbox', + 'get_file_dates', + 'py_text_to_html', + 'create_tb_meta' + ] + +# module constants +# ----------------------------------------------------------------------------- + +# constant for the xml root default attributes +DEFAULT_XML_ATTRIBS= \ + { + "metadata": {"xml:lang": "en"}, + "ScopeCd": {"value": "005"}, + "mdDateSt": {"Sync": "TRUE"} + } + +# constant for the default toolbox xml structure +DEFAULT_TOOLBOX_XML_STRUCT = \ + OrderedDict( + { + "metadata": "None", + "Esri": "metadata", + "CreaDate": "Esri", + "CreaTime": "Esri", + "ArcGISFormat": "Esri", + "SyncOnce": "Esri", + "ModDate": "Esri", + "ModTime": "Esri", + "scaleRange": "Esri", + "minScale": "scaleRange", + "maxScale": "scaleRange", + "ArcGISProfile": "Esri", + "toolbox": "metadata", + "arcToolboxHelpPath": "toolbox", + "dataIdInfo": "metadata", + "idCitation": "dataIdInfo", + "resTitle": "idCitation", + "idPurp": "dataIdInfo", + "searchKeys": "dataIdInfo", + "idAbs": "dataIdInfo", + "idCredit": "dataIdInfo", + "resConst": "dataIdInfo", + "Consts": "resConst", + "useLimit": "Consts", + "distInfo": "metadata", + "distributor": "distInfo", + "distorFormat": "distributor", + "formatName": "distorFormat", + "mdHrLv": "metadata", + "ScopeCd": "mdHrLv", + "mdDateSt": "metadata", + } + ) + +# constant for the default tool xml structure +DEFAULT_TOOL_XML_STRUCT = \ + OrderedDict( + { + "metadata": "None", + "Esri": "metadata", + "CreaDate": "Esri", + "CreaTime": "Esri", + "ArcGISFormat": "Esri", + "SyncOnce": "Esri", + "ModDate": "Esri", + "ModTime": "Esri", + "scaleRange": "Esri", + "minScale": "scaleRange", + "maxScale": "scaleRange", + "tool": "metadata", + "arcToolboxHelpPath": "tool", + "summary": "tool", + "usage": "tool", + "scriptExamples": "tool", + "parameters": "tool", + "dataIdInfo": "metadata", + "idCitation": "dataIdInfo", + "resTitle": "idCitation", + "idCredit": "dataIdInfo", + "searchKeys": "dataIdInfo", + "resConst": "dataIdInfo", + "Consts": "resConst", + "useLimit": "Consts", + "distInfo": "metadata", + "distributor": "distInfo", + "distorFormat": "distributor", + "formatName": "distorFormat", + "mdHrLv": "metadata", + "ScopeCd": "mdHrLv", + "mdDateSt": "metadata", + } + ) + +# constants for default Esri Toolbox metadata values +TOOLBOX_FORMAT_NAME = "ArcToolbox Toolbox" +ARC_GIS_FORMAT = "1.0" +SYNC_ONCE = "TRUE" +MIN_SCALE = "150000000" +MAX_SCALE = "5000" +ARC_GIS_PROFILE = "ItemDescription" + +# constants for default Esri Tool metadata values +TOOL_FORMAT_NAME = "ArcToolbox Tool" + +# ----------------------------------------------------------------------------- + + +def import_toolbox(toolbox_path): + """Function imports an ArcGIS python toolbox by name + from the current script directory and returns + the Toolbox class object. + :param - toolbox_path - python toolbox path + :returns -pyt.Toolbox - ArcGIS python Toolbox class object""" + if toolbox_path.endswith('.pyt'): + importlib.machinery.SOURCE_SUFFIXES.append('pyt') + toolbox_name = os.path.splitext( + os.path.basename(toolbox_path))[0] + spec = importlib.util.spec_from_file_location( + toolbox_name, toolbox_path) + pyt = importlib.util.module_from_spec(spec) + spec.loader.exec_module(pyt) + return pyt + +# ----------------------------------------------------------------------------- + + +def get_file_dates(file_path, date_fmt ='%Y%m%d', time_fmt='%H%M%S%S'): + '''Function returns created, modified, and last accessed + date and time values along with the last modified date. + and time for a given file in UTC. + :param - file_path - path to a file + :param - date_fmt - desired string date format + :param - time_fmt - desired string time format + :returns - dt_vals - a tuple of file dates and times, see example + ex. ( created date,created time, + modified date, modified time, + last accessed date, last accessed time, + current date, current time) + ''' + gt = time.gmtime + # get the last modified date time + mtime = os.path.getmtime(file_path) + mod_date = \ + time.strftime(date_fmt, gt(mtime)) + mod_time = \ + time.strftime(time_fmt, gt(mtime)) + + # get the created date time + ctime = os.path.getctime(file_path) + create_date = \ + time.strftime(date_fmt, gt(ctime)) + create_time = \ + time.strftime(time_fmt, gt(ctime)) + + # get the last accessed date time + atime = os.path.getctime(file_path) + access_date = \ + time.strftime(date_fmt, gt(atime)) + access_time = \ + time.strftime(time_fmt, gt(atime)) + + # get the current accessed date time + cur_time = datetime.utcnow() + cur_date = cur_time.strftime(date_fmt) + cur_time = cur_time.strftime(time_fmt) + + dt_vals = (create_date, create_time, + mod_date, mod_time, + access_date, access_time, + cur_date, cur_time) + return dt_vals + + +# ----------------------------------------------------------------------------- + + +def py_text_to_html(str_value): + """Function returns a multiline string variable from python + as a html block. The inspect.cleandoc method is used to + clean up indentation from python docstrings that are + indented to line up within blocks of python code. All + leading whitespaces are removed from the first line. + Any leading whitespace that can be uniformly removed + from the second line onwards are removed. Empty lines + at the beginning and end are subsequently removed. Also, + all tabs are expanded to spaces. + :param - str_value - string (multiline string) + :returns -rtn_text- reformatted string value""" + rtn_text = str_value + rtn_text = inspect.cleandoc(str_value) + rtn_text = "

".join( + ["{0}".format(l) for l in rtn_text.splitlines()] + ) + return rtn_text + + +# ----------------------------------------------------------------------------- + + +def set_default_keywords(strs=[]): + """Function returns search keywords from + a list of strings. + :param - strs - list of string values to build keywords from + :returns - fmt_kws - list object containing keywords""" + kws = [kw for kw in re.split("[^a-zA-Z]", " ".join(strs)) if kw.isalnum()] + kws = [(kw if any(e.isupper() for e in kw) else kw.upper()) for kw in kws] + kws = list(set(kws)) + fmt_kws = [] + for t in kws: + matches = re.findall(t, str(kws), re.IGNORECASE) + if len(matches) > 1: + for m in matches: + if m != m.upper(): + fmt_kws.append(m) + if t.upper() not in [x.upper() for x in fmt_kws]: + fmt_kws.append(t) + fmt_kws = list(set(fmt_kws)) + return fmt_kws + + +# ----------------------------------------------------------------------------- + + +def create_credits_list(user_name="", org_name="", email=""): + """Function returns the ArcGIS portal user + name , org, and email info (if logged in) as a list. + Otherwise, just the system user name will be + returned. + :param - user_name - default user name + :param - org_name - default org name + :param - email - default email + :returns - user_info - list object""" + + if user_name == "": + try: + import getpass + userId = getpass.getuser() + except: + userId == "Unknown" + + try: + if user_name == "" or org_name == "" or email == "": + import arcpy + + active_portal = arcpy.GetActivePortalURL() + portal_info = arcpy.GetPortalDescription(active_portal) + # get the user name + if user_name == "": + userId = portal_info["user"]["fullName"] + else: + userId == user_name + # get the org name + if org_name == "": + org_name = portal_info["name"] + # get the user's email + if email == "": + email = portal_info["user"]["email"] + except BaseException: + # create the output json object + pass + user_info = [ + " Point of Contact (POC):{0} ".format(userId), + " Organization: {0} ".format(org_name), + " Email: {0} ".format(email) + ] + return user_info + + +# ----------------------------------------------------------------------------- + + +def get_class_attrib(co, prop_name, default_value="", + attrib_multiline=True, dflt_multiline=True): + """Function retrieves an object property value + if it exists. Otherwise the default value is returned. + Options for format the return value as an html block + and/or remove multiline spacing are available. + :param - co - input class/object + :param - prop_name - object property by name + :param - default_value - default for non-existent property + :param - attrib_multiline - option to format class attribute as multiline text + :param - dflt_multiline - option to format default value as multiline text""" + + if hasattr(co, prop_name): + val = getattr(co, prop_name) + if isinstance(val, str): + if inspect.cleandoc(val) != val and attrib_multiline: + val = py_text_to_html(val) + else: + val = default_value + if isinstance(default_value, str): + if inspect.cleandoc(default_value) != default_value and dflt_multiline: + val = py_text_to_html(default_value) + return val + + +# ----------------------------------------------------------------------------- + + +def build_metadata_structure(metadata_dict, xml_attrib_dict): + """Function generates a metadata xml etree structure + given a ordered dictionary object representing + the hierarchical structure/node order. See example below: + ex. { 'root_elem': 'None', + 'child_elem':'root_elem' } + :param - metadata_dict - Ordered dictionary; see example above + returns - (xml_root, xml_elms)- a tuple containing the root xml element + and a dictionary of all xml elements""" + + xml_elms = {} + xml_root = None + for elm, elm_par in metadata_dict.items(): + if str(elm_par) == "None": + # make the root element + xml_elms[elm] = et_elm(elm) + if elm in xml_attrib_dict.keys(): + for attrib, val in xml_attrib_dict[elm].items(): + xml_elms[elm].set(attrib, val) + xml_root = xml_elms[elm] + continue + else: + # make the sub element structures + xml_elms[elm] = et_se(xml_elms[elm_par], elm) + if elm in xml_attrib_dict.keys(): + for attrib, val in xml_attrib_dict[elm].items(): + xml_elms[elm].set(attrib, val) + return (xml_root, xml_elms) + + +# ----------------------------------------------------------------------------- + + +def set_xml_text_by_class_attrib(class_inst, xml_elms, overwrite=False): + """Function sets the text value for each xml element in + a dictionary of xml elements when there is a + corresponding xml tag and class attribute in + the class instance. + :param - class_inst - instance of a python object/class + :param - xml_elms - dictionary of xml etree elements + ex. {xml_tag: etree_element} + :param - overwrite - allow existing xml text values to + be overwritten + :returns - xml_elms - dictionary of xml etree elements""" + for tag, elm in xml_elms.items(): + class_attrib = get_class_attrib(class_inst, tag, "") + if isinstance(class_attrib, str): + if bool(class_attrib): + if not elm.text or not bool(elm.text): + elm.text = class_attrib + elif overwrite: + elm.text = class_attrib + return xml_elms + + +# ----------------------------------------------------------------------------- + + +class toolMetadata(object): + def __init__(self, tb_metadata, tb_tool): + self.tb_meta = tb_metadata + self.tb_tool = tb_tool + + # get general tool information + # ------------------------------------------------------------------------ + # get the tool name + self.tool_name = self.tb_tool.__name__ + # get the xml path + tb_pardir = os.path.abspath(os.path.join( + self.tb_meta.toolbox_path, + os.pardir)) + + self.xml_path = os.path.abspath( + r'{0}\{1}.{2}.pyt.xml'.format( + tb_pardir, + self.tb_meta.toolbox_name, + self.tool_name + ) + ) + + # create an instance of the tool + self.tool_inst = self.tb_tool() + + # get tool level metadata from tool instance attributes + # ------------------------------------------------------------------------ + # tool label + self.label = get_class_attrib(self.tool_inst, "label", self.tool_name) + # toolbox alias + self.alias = self.tb_meta.alias + # toolbox keywords + self.toolbox_keywords = " ".join(self.tb_meta.keywords) + # tool category + self.category = get_class_attrib(self.tool_inst, "category", "Uncategorized") + # summary/description text + dflt_summary = """{0} is an ArcGIS python toolbox tool. + Contact POC below for more information.""".format( + self.tool_name) + self.summary = get_class_attrib(self.tool_inst, "description", dflt_summary) + # tool usage text + self.usage = get_class_attrib(self.tool_inst, "usage", self.summary) + # set derived tool metadata variables + self.resTitle = "{0}.({1})".format(self.alias, self.category) + + # check for tool searchKeys in the tool attributes + self.keywords = get_class_attrib(self.tool_inst, "searchKeys", []) + # generate default keywords from the toolbox + if not isinstance(self.keywords, list) or not bool(self.keywords): + self.keywords = set_default_keywords( + [self.label, self.tool_name, self.toolbox_keywords, self.category] + ) + + self.tb_meta.keyword_master.extend(self.keywords) + # generate a default code sample from the toolbox and tool + self.code_ex = get_class_attrib(self.tool_inst, "scriptExamples", {}) + self.code_ex = self.validate_code_examples() + + # set default Esri tool metadata values: + # --------------------------------------------------------------------------- + formatName = TOOL_FORMAT_NAME + # -------------------------------------------------------------------------- + + def gen_default_code(self): + """Function generates a default code sample for an arcGIS tool. + :param - self - arcGIS toolbox metadata object/class + :returns - code_text- a text code sample""" + + param_text = "" + + tb_import = [ + "# import the toolbox as a module", + "import arcpy", + "arcpy.ImportToolbox(r'{0}',".format( + self.tb_meta.toolbox_path), + "{0}r'{1}')".format(" " * 20, self.tb_meta.alias), + ] + + tool_call = "result = arcpy.{0}_{1}".format( + self.tool_name, self.tb_meta.alias) + lead_sp = " " * 11 + if hasattr(self.tool_inst, "getParameterInfo"): + t_args = self.tool_inst.getParameterInfo() + max_p_len = max([len(a.name) for a in t_args]) + p_sep = ",\n" + lead_sp + param_text = p_sep.join( + [ + "{0}{1}#{2}- Type({3})".format( + a.name, + " " * (max_p_len + 4 - len(a.name)), + a.displayName, + a.datatype) + for a in t_args + ] + ) + tool_code = [ + "# call the tool and return the output", + "{0}(\n{1}{2}\n{1})".format(tool_call, lead_sp, param_text) + ] + code_text = '{0}\n\n{1}'.format( + '\n'.join(tb_import), + '\n'.join(tool_code) + ) + return code_text + + # -------------------------------------------------------------------------- + + def validate_code_examples(self): + """Function to parse and validate code description json objects. + See example json properly formatted object below: + Ex. JSON object + {'Code Sample 1' : { + 'para' : 'Sample of tool code in python:', + 'code' : ['def hello_world_func():', + ' print("Hello World!")'] + } + } + If the input object is not properly formatted or absent, the default + code sample will be returned. + :param - self - arcGIS toolbox metadata object/class + :returns - rtn_json- a validated code sample json object""" + rtn_json = {} + try: + # if any error due to null or improper formatting, + # skip to except and return the default code example only + if self.code_ex == {}: + raise ValueError("Code example is not available") + for (title, details) in self.code_ex.items(): + title_str = title + desc_str_ = details["para"] + code_str ="\n".join(details["code"]) + if title_str.strip() == "": + title_str = "{0}: Code Sample)".format(self.label) + if desc_str_.strip() == "": + details["para"] = "Sample Description: {0}".format(self.summary) + if not bool(code_str): + raise ValueError("Code example is not available") + rtn_json[title] = {"para": desc_str_, "code": code_str} + return rtn_json + except BaseException: + # generate the default code sample + title_str = "{0}: Code Sample (1)".format(self.label) + desc_str_ = ''' + Note : Calling custom toolboxes is only available + within external python interpreters and script files! + Code sample will not work in ArcGIS python windows. + Code sample will not work in ArcGIS python notebooks.''' + code_str = self.gen_default_code() + rtn_json = {title_str: {"para": desc_str_, "code": code_str}} + return rtn_json + +# ------------------------------------------------------------------------------ + + +class toolboxMetadata(object): + def __init__( + self, + toolbox_path, + overwrite=False, + toolbox_xml_dict={}, + toolbox_xml_attrib_dict={}, + tool_xml_dict={}, + tool_xml_attrib_dict={}): + + source_path = str(Path(toolbox_path).resolve()) + # ------------------------------------------------------------------------ + # set the path property to the toolbox (.pyt file) + self.toolbox_path = source_path + # set the toolbox name property + self.toolbox_name = os.path.splitext( + os.path.basename(toolbox_path))[0] + # set the object overwrite property + self.overwrite = overwrite + + # set the default xml structure dictionaries + # ------------------------------------------------------------------------ + if not bool(tool_xml_dict): + # use a default xml structure + self.tool_xml_dict = DEFAULT_TOOL_XML_STRUCT + else: + self.tool_xml_dict = tool_xml_dict + if not bool(toolbox_xml_dict): + # use a default xml structure + self.toolbox_xml_dict = DEFAULT_TOOLBOX_XML_STRUCT + else: + self.toolbox_xml_dict = toolbox_xml_dict + # set the default xml attribute dictionaries + # ------------------------------------------------------------------------ + + self.toolbox_xml_attrib_dict = toolbox_xml_attrib_dict + if not bool(self.toolbox_xml_attrib_dict): + self.toolbox_xml_attrib_dict = DEFAULT_XML_ATTRIBS + self.tool_xml_attrib_dict = tool_xml_attrib_dict + if not bool(self.tool_xml_attrib_dict): + self.tool_xml_attrib_dict = DEFAULT_XML_ATTRIBS + # get general toolbox information + # ------------------------------------------------------------------------ + # get the name of the current script/module file + self.mod_name = os.path.splitext(os.path.basename(__file__))[0] + + # import the Toolbox class object + self.tb = import_toolbox(self.toolbox_path).Toolbox + + # create an instance of the Toolbox + self.tb_inst = self.tb() + + # get a list of the tools in the Toolbox instances + self.tb_tools = [t for t in self.tb_inst.tools] + + # get the ArcGIS help resources path + self.arcToolboxHelpPath = os.path.join( + os.path.abspath(os.path.join(sys.path[0], os.pardir)), "Help\\gp") + + # get toolbox level metadata from toolbox instance attributes + # ------------------------------------------------------------------------ + # toolbox alias + self.alias = get_class_attrib(self.tb_inst, "alias", self.toolbox_name) + # toolbox idPurp/description text + dflt_idPurp = '''{0} is an ArcGIS python toolbox. + Contact POC below for more information.'''.format( + self.toolbox_name) + + self.idPurp = get_class_attrib(self.tb_inst, "description", dflt_idPurp) + # toolbox idAbs/abstract text + dflt_idAbs = '{0}{1}{2}'.format( + self.idPurp, + "

" * 2, + self.create_abstract_tool_text(self.tb_tools) + ) + + self.idAbs = get_class_attrib(self.tb_inst, "idAbs", dflt_idAbs) + # set derived tool metadata variables + self.resTitle = self.toolbox_name + # check for toolbox searchKeys in the toolbox attributes + self.keyword_master = [] + self.keywords = get_class_attrib(self.tb_inst, "searchKeys", []) + + # generate default keywords from the toolbox + if not isinstance(self.keywords, list) or not bool(self.keywords): + self.keywords = set_default_keywords([self.alias, self.toolbox_name]) + self.keyword_master = self.keywords.copy() + # get the toolbox created and mod date/times + self.toolbox_dates = get_file_dates(self.toolbox_path) + self.CreaDate = self.toolbox_dates[1] + self.CreaTime = self.toolbox_dates[0] + self.ModDate = self.toolbox_dates[2] + self.ModTime = self.toolbox_dates[3] + self.mdDateSt = self.toolbox_dates[6] + + # create default credit info from the ArcGIS portal session + self.creditsList = create_credits_list() + dflt_idCredit = "

".join(self.creditsList) + self.idCredit = get_class_attrib(self.tb_inst, "idCredit", dflt_idCredit) + # build a default usage limits statement + dflt_useLimit = '''For questions regarding usage limitations, contact: + {0} + {1} + {2} +

+ Disclaimer: **Metadata auto generated with module {3}** + -For detailed release notes, contact the POC above! + '''.format( + self.creditsList[0], self.creditsList[1], self.creditsList[2], self.mod_name) + self.useLimit = get_class_attrib(self.tb_inst, "useLimit", dflt_useLimit) + # holders for tool metadata objects and xml root objects + self.def_tool_metas = [] + self.xml_roots = {} + + # set default Esri Toolbox metadata values: + # ------------------------------------------------------------------------ + self.formatName = TOOLBOX_FORMAT_NAME + self.ArcGISFormat = ARC_GIS_FORMAT + self.SyncOnce = SYNC_ONCE + self.minScale = MIN_SCALE + self.maxScale = MAX_SCALE + self.ArcGISProfile = ARC_GIS_PROFILE + # ------------------------------------------------------------------------ + + def generate_toolbox_metadata(self): + """Method imports creates a toolbox xml metadata object + given the toolbox metadata structure as an ordered dictionary, + a dictionary of element attribute values, an instance of the toolbox + and a toolbox metadata object (self). + :param - self - toolbox metadata object + :returns -tb_meta_dict - dictionary of toolbox metadata path + and root elements + Ex. {xml output path, etree root element}""" + if "toolbox" not in self.toolbox_xml_attrib_dict.keys(): + self.toolbox_xml_attrib_dict["toolbox"] = { + "name": self.toolbox_name, + "alias": self.alias + } + # -------------------------------------------------------------------- + # build the xml metadata structure + xml_root, xml_elms = build_metadata_structure( + self.toolbox_xml_dict, self.toolbox_xml_attrib_dict) + # -------------------------------------------------------------------- + # set the xml element text with text values from the toolbox class + xml_elms = set_xml_text_by_class_attrib(self.tb_inst, xml_elms) + # set the xml element text with text via the toolbox metadata co + xml_elms = set_xml_text_by_class_attrib(self, xml_elms) + # -------------------------------------------------------------------- + # create and set the searchKeys sub-element 'keyword' values + self.keyword_master = set_default_keywords([" ".join(self.keyword_master)]) + for kw in self.keyword_master: + et_se(xml_elms["searchKeys"], "keyword").text = kw + tb_meta_dict = {self.toolbox_path + '.xml': xml_root} + return tb_meta_dict + + # ------------------------------------------------------------------------ + + def generate_tool_metadata(self): + """Method creates a tool xml metadata object + given the tool metadata structure as an ordered dictionary, + a dictionary of element attribute values, an instance of the + tool, an instance of the toolbox, the tool metadata object + and the toolbox metadata object (self). + :param - self - toolbox metadata object + :returns -xml_roots - dictionary of tool metadata path + and root elements + Ex. {xml output path, etree root element}""" + # begin creating the xml data for the tools in the toolbox + for tb_tool in self.tb_tools: + def_tool_meta = toolMetadata(self, tb_tool) + tool_inst = tb_tool() + + if "tool" not in self.tool_xml_attrib_dict.keys(): + self.tool_xml_attrib_dict["tool"] = { + "xmlns": "", + "name": tb_tool.__name__, + "displayname": def_tool_meta.label, + "toolboxalias": self.alias + } + # -------------------------------------------------------------------- + # build the xml metadata structure + xml_root, xml_elms = build_metadata_structure( + self.tool_xml_dict, self.tool_xml_attrib_dict) + # -------------------------------------------------------------------- + # attribute the xml element text with values from a tool instance + xml_elms = set_xml_text_by_class_attrib(tool_inst, xml_elms) + # attribute the xml element text with values from a toolbox instance + xml_elms = set_xml_text_by_class_attrib(self.tb_inst, xml_elms) + + # attribute the remaining null xml element text items with default values + # values set in the toolbox/tool code override the defaults + # set default values from the tool metadata co + xml_elms = set_xml_text_by_class_attrib(def_tool_meta, xml_elms) + # set default values from the toolbox metadata co + xml_elms = set_xml_text_by_class_attrib(self, xml_elms) + # -------------------------------------------------------------------- + # create and set the searchKeys sub-element 'keyword' values + for kw in def_tool_meta.keywords: + et_se(xml_elms["searchKeys"], "keyword").text = kw + # -------------------------------------------------------------------- + # create and set the code example metadata + for (title, details) in def_tool_meta.code_ex.items(): + scriptExample = et_se( + xml_elms["scriptExamples"], "scriptExample") + et_se(scriptExample, "title").text = title + et_se(scriptExample, "para").text = py_text_to_html( + details["para"] ) + et_se(scriptExample, "code").text = details["code"] + # -------------------------------------------------------------------- + # loop the tool's input arguments/params, for each arg: + # create and set the tool sub-element 'parameters' attributes and values + if hasattr(tool_inst, "getParameterInfo"): + t_args = tool_inst.getParameterInfo() + for a in t_args: + # create an new parameter elm and set the xml attributes + p_elm = et_se( + xml_elms["parameters"], + "param", type = a.parameterType, datatype = a.datatype, + name = a.name, displayname = a.displayName, + direction = a.direction, + ) + # get/set the parameter sub-element 'dialogReference' values + et_se( + p_elm, "dialogReference").text = py_text_to_html( + '' + get_class_attrib(a, "dialogReference", a.displayName + ) + '

' + ) + # get p_elm dependencies + p_depen = 'N/A' + if hasattr(a, 'parameterDependencies'): + if bool(a.parameterDependencies): + p_depen = a.parameterDependencies + p_depen = 'Dependencies: {0}'.format(p_depen) + + # get p_elm default + p_def = 'Default Value: N/A' + if hasattr(a, 'valueAsText'): + if bool(a.valueAsText): + p_def = 'Default Value: {0}'.format(a.value) + + + # get p_elm allowed values (up to 10 values) + p_filter_type = '' + p_filter_list = '' + if hasattr(a, 'filter'): + if bool(a.filter.type): + p_filter_type = a.filter.type + if p_filter_type == 'ValueList' and bool(a.filter.list): + + lead_space = '  ' * 8 + p_filter_prefix = '\n{0}-'.format(lead_space) + p_filter_list = 'Allowed Values:' + \ + ((p_filter_prefix).join([''] + a.filter.list[0:10])) + if len(a.filter.list) > 10: + msg_prefix = '\n{0}'.format(lead_space) + p_filter_list = p_filter_list + (msg_prefix + '*Only first 10 values displayed...*') + p_filter_list = p_filter_list + (msg_prefix + ' *See tool parameter for full list...*') + if p_filter_type == 'Range' and len(a.filter.list) == 2: + p_filter_list = 'Allowed Range: Min({0}), Max ({1})'.format( + a.filter.list[0], + a.filter.list[1]) + + # create the default pythonReference text + py_ref = ( + '''Python variable name: ({0}) + Description: {1} {2} value representing the tool + {8}"{3}" {4} parameter. + {5} + {6} + {7}'''.format( + a.name, a.parameterType, a.datatype.lower(), + a.displayName, a.direction, p_depen, p_def, + p_filter_list, (10 * '  ')) + ) + + # get/set the p_elm sub-element 'pythonReference' attributes + et_se( + p_elm, "pythonReference").text = py_text_to_html( + get_class_attrib(a, "pythonReference", py_ref + ) + ) + # -------------------------------------------------------------------- + # retain metadata and xml variables as attributes for debugging + self.def_tool_metas.append(def_tool_meta) + self.xml_roots[def_tool_meta.xml_path] = xml_root + return self.xml_roots + + # -------------------------------------------------------------------------- + + def create_abstract_tool_text(self, tb_tools): + """Method creates the default toolbox metadata abstract + text statement. The resulting statement has provides an + overview and lists out the tools contained in the toolbox. + :param - self - toolbox metadata object + :param - tb_tools - list of toolbox tool classes + :returns -rtn_text - display text for the toolbox abstract""" + tool_lines = ["Included Tools:"] + for tb_tool in tb_tools: + tool_inst = tb_tool() + tool_name = get_class_attrib(tool_inst, "label", tb_tool.__name__) + tool_cat = get_class_attrib(tool_inst, "category", "None") + tool_desc = get_class_attrib(tool_inst, "description", "", False, False) + if not bool(tool_desc): + tool_desc = get_class_attrib(tool_inst, "usage", "", False, False) + tool_lines.append( + "

- {0} (Category: {1})".format(tool_name, tool_cat) + ) + for line in tool_desc.splitlines(): + tool_lines.append(" {0}".format(line.strip())) + rtn_text = "

".join(tool_lines) + return rtn_text + + # -------------------------------------------------------------------------- + + def xml_tree_to_file(self, out_path, xml_root): + """Method creates or overwrites an xml document + with the xml etree object provided at the given path. + :param - self - toolbox metadata object + :param - out_path - output path + :param - xml_root - xml etree root element + :returns -none - """ + # skip creating metadata if the xml exists and overwrite == False + if (not os.path.exists(out_path)) or self.overwrite: + t = et_root(xml_root) + t.write(out_path, encoding="utf-8", xml_declaration=True) + print(out_path) + return + + # -------------------------------------------------------------------------- + + def write_tool_xml_metadata(self): + """Method writes each tool xml element + to an output path derived from the + generate_tool_metadata method. + :param - self - toolbox metadata object + :returns -none - """ + xml_roots = self.generate_tool_metadata() + for out_path, xml_root in xml_roots.items(): + self.xml_tree_to_file(out_path, xml_root) + return + + # -------------------------------------------------------------------------- + + def write_toolbox_xml_metadata(self): + """Method writes the toolbox xml element + to an output path derived from the + generate_tool_metadata method. + :param - self - toolbox metadata object + :returns -none - """ + xml_roots = self.generate_toolbox_metadata() + for out_path, xml_root in xml_roots.items(): + self.xml_tree_to_file(out_path, xml_root) + return + + # -------------------------------------------------------------------------- + + def write_all_xml_metadata(self): + """Method writes the xml metadata for + the toolbox and all tools belonging to the + toolbox to the toolbox (pyt) directory. + :param - self - toolbox metadata object + :returns -none - """ + self.write_tool_xml_metadata() + self.write_toolbox_xml_metadata() + return + +# ----------------------------------------------------------------------------- + +def create_tb_meta(toolbox_path,overwrite=False): + """Function calls a process that writes + xml metadata for all toolbox class objects. + :param - toolbox_path - path to the toolbox file (.pyt) + :returns -Boolean - True on success """ + rtn_val = False + if not toolbox_path.endswith('.pyt'): + return + source_path = str(Path(toolbox_path).resolve()) + try: + toolboxMetadata( + source_path, + overwrite + ).write_all_xml_metadata() + rtn_val = True + except: + warnings.warn('Failed to generate toolbox metadata!') + return rtn_val + +# -----------------------------------------------------------------------------