├── pytexportutils ├── _esriSystem.pyd ├── _esriGeoDatabase.pyd ├── _esriGeoprocessing.pyd └── __init__.py ├── .gitignore ├── README.md ├── converttbx.pyt └── tbxtopyt.py /pytexportutils/_esriSystem.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/tbx-pyt-translator/HEAD/pytexportutils/_esriSystem.pyd -------------------------------------------------------------------------------- /pytexportutils/_esriGeoDatabase.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/tbx-pyt-translator/HEAD/pytexportutils/_esriGeoDatabase.pyd -------------------------------------------------------------------------------- /pytexportutils/_esriGeoprocessing.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/tbx-pyt-translator/HEAD/pytexportutils/_esriGeoprocessing.pyd -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TBX to PYT Translator 2 | 3 | This Python toolbox (converttbx.pyt) will take any geoprocessing 4 | toolbox file (.TBX) and create a corresponding stub .PYT with a 5 | corresponding Python implementation of the tools with the original 6 | parameters of original toolbox. 7 | 8 | ## Features 9 | * Create Skeleton PYT from a TBX 10 | * Basic conversion from geoprocessing toolbox (.tbx) to Python toolbox (.pyt). 11 | 12 | ## Instructions 13 | 14 | 1. Download and unzip the .zip file or clone the repo. 15 | 2. Open the provided converttbx.pyt inside of ArcCatalog or Catalog View. 16 | 3. Provide the existing .tbx file as input. 17 | 4. Examine and refine the resulting .pyt file. 18 | 19 | [New to Github? Get started here.](https://github.com/) 20 | 21 | ## Requirements 22 | 23 | * ArcGIS 10.1 24 | * Some experience editing Python code 25 | 26 | ## Resources 27 | 28 | * [Python for ArcGIS Resource Center](http://resources.arcgis.com/en/communities/python/) 29 | * [Analysis and Geoprocessing Blog](http://blogs.esri.com/esri/arcgis/category/subject-analysis-and-geoprocessing/) 30 | * [twitter@arcpy](http://twitter.com/arcpy) 31 | 32 | ## Issues 33 | 34 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 35 | 36 | # ! WARNING ! 37 | 38 | THIS IS NOT A 100% AUTOMATED SOLUTION TO CREATING PYTS. You will 39 | need to go in and look over the source before you use it. There 40 | will be areas where you NEED to change the source of the new PYT, 41 | and others where you'll need to do some sanity checking to make 42 | sure the PYT's functionality is similar to your original TBX. 43 | 44 | 45 | ## Contributing 46 | 47 | Anyone and everyone is welcome to contribute. 48 | 49 | ## Licensing 50 | Copyright 2012 Esri 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License"); 53 | you may not use this file except in compliance with the License. 54 | You may obtain a copy of the License at 55 | 56 | http://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | 64 | A copy of the license is available in the repository's [license.txt](https://raw.github.com/Esri/switch-basemaps-js/master/license.txt) file. 65 | 66 | [](Esri Tags: ArcGIS Toolboxes) 67 | [](Esri Language: Python) 68 | -------------------------------------------------------------------------------- /pytexportutils/__init__.py: -------------------------------------------------------------------------------- 1 | """Type library collection pytexportutils""" 2 | __version__ = '10.1' 3 | __all__ = ['ArcGISVersion', 'esriSystem', 'esriSystemUI', 'esriGeometry', 'esriGraphicsCore', 'esriGraphicsSymbols', 'esriDisplay', 'esriGeoDatabase', 'esriGeoDatabaseDistributed', 'esriGeoDatabaseExtensions', 'esriGeoDatabasePS', 'esriDataSourcesFile', 'esriDataSourcesGDB', 'esriDataSourcesOleDB', 'esriDataSourcesRaster', 'esriDataSourcesNetCDF', 'esriDataSourcesRasterUI', 'esriCarto', 'esriGeoprocessing'] 4 | # Required by all submodules, if these get in a bad state the C modules will crash. 5 | _IIDMap = {} 6 | _CLSIDMap = {} 7 | _RecordMap = {} 8 | 9 | class Enumeration(object): 10 | "Base class for enumerations" 11 | @classmethod 12 | def valueFor(cls, value): 13 | """Look up the textual name for an integer representation of an 14 | enumeration value""" 15 | return ([x for x in cls.__slots__ 16 | if getattr(cls, x) == value] or [None]).pop() 17 | __slots__ = [] 18 | pass 19 | 20 | 21 | def IndexProperty(getter=None, setter=None): 22 | "For getter/setters with an index argument" 23 | class IndexedPropertyGetter(object): 24 | def __init__(self, other): 25 | self._other = other 26 | def __setitem__(self, index, value): 27 | if setter is not None: 28 | return setter(self._other, index, value) 29 | raise TypeError("%s object does not support item assignment" % 30 | self._other.__class__.__name__) 31 | def __getitem__(self, index): 32 | if getter is not None: 33 | return getter(self._other, index) 34 | raise TypeError("%s object does not support indexing" % 35 | self._other.__class__.__name__) 36 | return property(IndexedPropertyGetter) 37 | 38 | 39 | def FAILED(item): 40 | """Usage: FAILED(HRESULT or IFace) 41 | 42 | Indicates if the specified HRESULT indicates a failure or the last call 43 | on the specified interface instance failed.""" 44 | hr = item 45 | if not isinstance(item, (int, long)): 46 | if hasattr(item, '_HR'): 47 | hr = item._HR 48 | if not isinstance(hr, (int, long)): 49 | raise ValueError(repr(hr)) 50 | return bool(hr & 0x80000000) 51 | 52 | def SUCCEEDED(item): 53 | """SUCCEEDED(HRESULT or IFace) 54 | 55 | Indicates if the specified HRESULT indicates a failure or the last call 56 | on the specified interface instance succeeded.""" 57 | return not FAILED(item) 58 | 59 | def interfaces_supported(interface_object): 60 | """Returns a list of Interface types in this packages supported by the 61 | supplied COM object instance.""" 62 | if not hasattr(interface_object, 'supports'): 63 | interface_object = IUnknown(interface_object) # coerce 64 | return [iface for iid, iface in _IIDMap.iteritems() 65 | if interface_object.supports(iid)] 66 | 67 | from .esriSystem import * 68 | from .esriGeoDatabase import * 69 | from .esriGeoprocessing import * 70 | -------------------------------------------------------------------------------- /converttbx.pyt: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import contextlib 4 | import os 5 | import sys 6 | 7 | import arcpy 8 | 9 | @contextlib.contextmanager 10 | def script_run_as(filename, args=None): 11 | oldpath = sys.path[:] 12 | oldargv = sys.argv[:] 13 | newdir = os.path.dirname(filename) 14 | sys.path = oldpath + [newdir] 15 | sys.argv = [filename] + [arg.valueAsText for arg in (args or [])] 16 | oldcwd = os.getcwdu() 17 | os.chdir(newdir) 18 | 19 | try: 20 | # Actually run 21 | yield filename 22 | finally: 23 | # Restore old settings 24 | sys.path = oldpath 25 | sys.argv = oldargv 26 | os.chdir(oldcwd) 27 | 28 | class Toolbox(object): 29 | def __init__(self): 30 | self.label = u'Convert to PYT' 31 | self.alias = u'pytmaker' 32 | self.tools = [CreatePYT] 33 | 34 | # Tool implementation code 35 | 36 | class CreatePYT(object): 37 | def __init__(self): 38 | self.label = u'Create PYT Template from TBX' 39 | self.description = u'Create a new PYT skeleton from the structure and parameters of an ArcGIS Toolbox (TBX) file.' 40 | self.canRunInBackground = False 41 | def getParameterInfo(self): 42 | # Input_Toolbox 43 | param_1 = arcpy.Parameter() 44 | param_1.name = u'Input_Toolbox' 45 | param_1.displayName = u'Input Toolbox' 46 | param_1.parameterType = 'Required' 47 | param_1.direction = 'Input' 48 | param_1.datatype = u'Toolbox' 49 | 50 | # Output_File 51 | param_2 = arcpy.Parameter() 52 | param_2.name = u'Output_File' 53 | param_2.displayName = u'Output File' 54 | param_2.parameterType = 'Required' 55 | param_2.direction = 'Output' 56 | param_2.datatype = u'File' 57 | 58 | return [param_1, param_2] 59 | def isLicensed(self): 60 | return True 61 | def updateParameters(self, parameters): 62 | if parameters[0].valueAsText and (not (parameters[1].valueAsText or parameters[1].altered)): 63 | if os.path.isdir(os.path.dirname(parameters[0].valueAsText)): 64 | newnamepart = os.path.splitext(parameters[0].valueAsText)[0] 65 | newnamepart += u"_converted.pyt" 66 | parameters[1].value = newnamepart 67 | elif arcpy.env.workspace and os.path.isdir(arcpy.env.workspace): 68 | newnamepart = os.path.splitext(os.path.split(parameters[0].valueAsText)[1])[0] 69 | newname = os.path.join(arcpy.env.workspace, newnamepart + "_converted.pyt") 70 | else: 71 | newnamepart = os.path.splitext(os.path.split(parameters[0].valueAsText)[1])[0] 72 | newname = os.path.join(os.getcwdu(), newnamepart + "_converted.pyt") 73 | pass 74 | def updateMessages(self, parameters): 75 | pass 76 | def execute(self, parameters, messages): 77 | with script_run_as(__file__): 78 | import tbxtopyt 79 | if not parameters[1].valueAsText.lower().endswith('.pyt'): 80 | parameters[1].value = parameters[1].valueAsText + ".pyt" 81 | tbxtopyt.export_tbx_to_pyt(parameters[0].valueAsText, parameters[1].valueAsText) 82 | -------------------------------------------------------------------------------- /tbxtopyt.py: -------------------------------------------------------------------------------- 1 | import arcpy 2 | 3 | import imp 4 | import os 5 | import re 6 | 7 | __all__ = ['export_tbx_to_pyt'] 8 | 9 | mod_find = imp.find_module('pytexportutils', [os.path.abspath(os.path.dirname(__file__))]) 10 | pytexportutils = imp.load_module('pytexportutils', *mod_find) 11 | 12 | ACCEPTABLE_VARIABLENAME = re.compile("^[_a-z][_a-z0-9]*$", re.IGNORECASE) 13 | CALL_RE_TEMPLATE = "((?:[_a-z][_a-z0-9]* *[.] *)*{}\(([^)]*)\))" 14 | CODING_RE = re.compile("coding: ([^ ]+)", re.IGNORECASE) 15 | HEADER_SOURCE = """# -*- coding: utf-8 -*- 16 | 17 | import contextlib 18 | import os 19 | import sys 20 | 21 | import arcpy 22 | 23 | # You can ignore/delete this code; these are basic utility functions to 24 | # streamline porting 25 | 26 | @contextlib.contextmanager 27 | def script_run_as(filename, args=None): 28 | oldpath = sys.path[:] 29 | oldargv = sys.argv[:] 30 | newdir = os.path.dirname(filename) 31 | sys.path = oldpath + [newdir] 32 | sys.argv = [filename] + [arg.valueAsText for arg in (args or [])] 33 | oldcwd = os.getcwdu() 34 | os.chdir(newdir) 35 | 36 | try: 37 | # Actually run 38 | yield filename 39 | finally: 40 | # Restore old settings 41 | sys.path = oldpath 42 | sys.argv = oldargv 43 | os.chdir(oldcwd) 44 | 45 | def set_parameter_as_text(params, index, val): 46 | if (hasattr(params[index].value, 'value')): 47 | params[index].value.value = val 48 | else: 49 | params[index].value = val 50 | """ 51 | 52 | FUNCTION_REMAPPINGS = ( 53 | ('AddMessage', 'messages.AddMessage({})'), 54 | ('AddWarning', 'messages.AddWarningMessage({})'), 55 | ('AddError', 'messages.AddErrorMessage({})'), 56 | ('AddIDMessage', 'messages.AddIDMessage({})'), 57 | ('GetParameterAsText', 'parameters[{}].valueAsText'), 58 | ('SetParameterAsText', 'set_parameter_as_text(parameters, {})'), 59 | ('GetParameter', 'parameters[{}]'), 60 | ('GetArgumentCount', 'len(parameters)'), 61 | ('GetParameterInfo', 'parameters') 62 | ) 63 | 64 | def collect_lines(fn): 65 | def fn_(*args, **kws): 66 | return '\n'.join(fn(*args, **kws)) 67 | return fn_ 68 | 69 | def rearrange_source(source, indentation): 70 | source = source.replace("\r\n", "\n").replace("\t", " ") 71 | # Guess coding? 72 | if '\n' in source: 73 | id1 = source.find('\n') 74 | if '\n' in source[id1+1:]: 75 | id1 = source.find('\n', id1 + 1) 76 | match_obj = CODING_RE.findall(source[:id1]) 77 | if match_obj: 78 | source = source.decode(match_obj[0]) 79 | else: 80 | try: 81 | source = source.decode("utf-8") 82 | except: 83 | pass 84 | src = "\n".join("{}{}".format(" " * indentation, 85 | line.encode("utf-8")) 86 | for line in source.split("\n")) 87 | # Now apply some mechanical translations to common parameter access routines 88 | for fnname, replacement_pattern in FUNCTION_REMAPPINGS: 89 | regexp = re.compile(CALL_RE_TEMPLATE.format(fnname), re.IGNORECASE) 90 | finds = regexp.findall(src) 91 | if finds: 92 | for codepattern, arguments in finds: 93 | src = src.replace(codepattern, replacement_pattern.format(arguments)) 94 | return src 95 | 96 | class Tool(object): 97 | def __init__(self, tool_object): 98 | self._tool = tool_object 99 | param_list = self._tool.ParameterInfo 100 | self._parameters = [pytexportutils.IGPParameter(param_list.Element[idx]) 101 | for idx in xrange(param_list.Count)] 102 | @property 103 | def name(self): 104 | try: 105 | if ACCEPTABLE_VARIABLENAME.match(self._tool.Name): 106 | return self._tool.Name 107 | except: 108 | pass 109 | return "Tool{}".format(hex(id(self))[2:]) 110 | @property 111 | @collect_lines 112 | def python_code(self): 113 | yield "class {}(object):".format(self.name) 114 | yield ' """{}"""'.format(self._tool.PathName.encode("utf-8")) 115 | 116 | try: 117 | gptool = pytexportutils.IGPScriptTool2(self._tool) 118 | codeblock = gptool.CodeBlock.encode("utf-8").replace("__init__(self)", "__init__(self, parameters)") 119 | yield rearrange_source(codeblock, 4) 120 | except: 121 | pass 122 | 123 | yield " def __init__(self):" 124 | try: 125 | yield " self.label = {}".format(repr(self._tool.DisplayName)) 126 | except: 127 | pass 128 | try: 129 | yield " self.description = {}".format(repr(self._tool.Description)) 130 | except: 131 | pass 132 | try: 133 | yield " self.canRunInBackground = {}".format(not pytexportutils.IGPScriptTool2(self._tool).RunInProc) 134 | except: 135 | yield " self.canRunInBackground = False" 136 | yield " def getParameterInfo(self):" 137 | index_dict = {parameter.Name.lower(): idx for idx, parameter in enumerate(self._parameters)} 138 | for idx, parameter in enumerate(self._parameters): 139 | yield " # {}".format(parameter.Name.encode("utf-8")) 140 | yield " param_{} = arcpy.Parameter()".format(idx + 1) 141 | yield " param_{}.name = {}".format(idx + 1, repr(parameter.Name)) 142 | yield " param_{}.displayName = {}".format(idx + 1, repr(parameter.DisplayName)) 143 | yield " param_{}.parameterType = {}".format(idx + 1, 144 | repr(pytexportutils.esriGPParameterType.valueFor(parameter.ParameterType) 145 | [len('esriGPParameterType'):])) 146 | yield " param_{}.direction = '{}'".format(idx + 1, 147 | "Output" if (parameter.Direction == 148 | pytexportutils.esriGPParameterDirection 149 | .esriGPParameterDirectionOutput) else "Input") 150 | if (parameter.DataType.supports(pytexportutils.IGPMultiValueType.IID)): 151 | yield " param_{}.datatype = {}".format(idx + 1, repr(pytexportutils.IGPMultiValueType(parameter.DataType).MemberDataType.DisplayName)) 152 | yield " param_{}.multiValue = True".format(idx + 1) 153 | elif (parameter.DataType.supports(pytexportutils.IGPCompositeDataType.IID)): 154 | cv = pytexportutils.IGPCompositeDataType(parameter.DataType) 155 | yield " param_{}.datatype = {}".format(idx + 1, repr(tuple(cv.DataType[x].DisplayName for x in xrange(cv.Count)))) 156 | elif (parameter.DataType.supports(pytexportutils.IGPValueTableType.IID)): 157 | vt = pytexportutils.IGPValueTableType(parameter.DataType) 158 | tablecols = [(vt.DataType[colindex].DisplayName, vt.DisplayName[colindex]) for colindex in xrange(vt.Count)] 159 | yield " param_{}.columns = {}".format(idx + 1, repr(tablecols)) 160 | else: 161 | yield " param_{}.datatype = {}".format(idx + 1, repr(parameter.DataType.DisplayName)) 162 | # default value 163 | try: 164 | value = parameter.Value.GetAsText() 165 | if value: 166 | yield " param_{}.value = {}".format(idx + 1, repr(value)) 167 | except: 168 | pass 169 | 170 | # .filter.list 171 | try: 172 | cvd = pytexportutils.IGPCodedValueDomain(parameter.Domain) 173 | cvd_list = [cvd.Value[domainidx].GetAsText() for domainidx in xrange(cvd.CodeCount)] 174 | yield " param_{}.filter.list = {}".format(idx + 1, repr(cvd_list)) 175 | except: 176 | pass 177 | 178 | # .parameterDependencies 179 | try: 180 | deps = parameter.ParameterDependencies 181 | keep_going = True 182 | dep_list = [] 183 | try: 184 | while keep_going: 185 | dep_list.append(index_dict.get(deps.Next().lower(), 0)) 186 | if not dep_list[-1]: 187 | keep_going = False 188 | dep_list = dep_list[:-1] 189 | except: 190 | pass 191 | if dep_list: 192 | yield " param_{}.parameterDependencies = {}".format(idx + 1, repr(dep_list)) 193 | except: 194 | pass 195 | yield "" 196 | yield " return [{}]".format(", ".join("param_{}".format(idx + 1) for idx in xrange(len(self._parameters)))) 197 | yield " def isLicensed(self):" 198 | yield " return True" 199 | yield " def updateParameters(self, parameters):" 200 | yield " validator = getattr(self, 'ToolValidator', None)" 201 | yield " if validator:" 202 | yield " return validator(parameters).updateParameters()" 203 | yield " def updateMessages(self, parameters):" 204 | yield " validator = getattr(self, 'ToolValidator', None)" 205 | yield " if validator:" 206 | yield " return validator(parameters).updateMessages()" 207 | yield " def execute(self, parameters, messages):" 208 | try: 209 | filename = pytexportutils.IGPScriptTool(self._tool).FileName 210 | if os.path.isfile(filename): 211 | file_contents = open(filename, 'rb').read() 212 | yield " with script_run_as({}):".format(repr(filename)) 213 | yield rearrange_source(file_contents, 12) 214 | else: 215 | yield " # {}".format(repr(filename)) 216 | try: 217 | # Coerce as source? 218 | compile(filename, self._tool.PathName, "exec") 219 | yield " with script_run_as({}):".format(repr(self._tool.PathName)) 220 | yield rearrange_source(filename, 12) 221 | except: 222 | pass 223 | except: 224 | yield " pass" 225 | 226 | class PYTToolbox(object): 227 | def __init__(self, tbx_name): 228 | gpu = pytexportutils.IGPUtilities(pytexportutils.GPUtilities()) 229 | name = gpu.GetNameObjectFromLocation(tbx_name) 230 | assert name, "Could not find {}".format(tbx_name) 231 | new_object = name.Open() 232 | assert new_object, "could not open toolbox" 233 | assert new_object.supports(pytexportutils.IGPToolbox.IID), "{} is not a toolbox".format(tbx_name) 234 | self._toolbox = pytexportutils.IGPToolbox(new_object) 235 | self._tools = map(Tool, iter(self._toolbox.Tools.Next, None)) 236 | @property 237 | @collect_lines 238 | def python_code(self): 239 | yield "# Export of toolbox {}".format(self._toolbox.PathName.encode("utf-8")) 240 | yield "" 241 | yield "import arcpy" 242 | yield "" 243 | yield "class Toolbox(object):" 244 | yield " def __init__(self):" 245 | tbx_name = os.path.splitext(os.path.basename(self._toolbox.PathName))[0] 246 | try: 247 | tbx_name = pytexportutils.IGPToolbox2(self._toolbox).DisplayName 248 | except: 249 | pass 250 | yield " self.label = {}".format(repr(tbx_name)) 251 | alias = "" 252 | try: 253 | alias = self._toolbox.Alias 254 | except: 255 | pass 256 | yield " self.alias = {}".format(repr(alias)) 257 | yield " self.tools = [{}]".format(", ".join(t.name for t in self._tools)) 258 | yield "" 259 | yield "# Tool implementation code" 260 | for tool in self._tools: 261 | yield "" 262 | yield tool.python_code 263 | 264 | def export_tbx_to_pyt(in_tbx, out_file): 265 | toolbox = PYTToolbox(in_tbx) 266 | with open(out_file, 'wb') as out: 267 | out.write(HEADER_SOURCE) 268 | out.write('\n') 269 | out.write(toolbox.python_code) 270 | 271 | if __name__ == "__main__": 272 | import glob 273 | #[r'C:\SupportFiles\ArcGIS\ArcToolBox\Toolboxes\Spatial Statistics Tools.tbx']: #[r'Toolboxes\My Toolboxes\OutScript.tbx']: 274 | for filename in [os.path.join(os.path.dirname(os.path.abspath(__file__)), "dashboard.tbx")]: 275 | print HEADER_SOURCE 276 | print PYTToolbox(filename).python_code.encode("ascii", "replace") 277 | --------------------------------------------------------------------------------