April 15, 2025
One of the things that makes Python great is the rich standard libraries and an extensive 3rd party ecosystem. Unfortunately, with Jython Automation Scripts in Maximo, only a subset of the standard libraries such as os
and sys
are available by default, modules such as re
, uuid
or the httplib
are not available. Since Jython is based on Java, you can use the available Java libraries instead, but this can be cumbersome and removes a key benefit using Python.
Fortunately, there are options to restore access to the standard library and load 3rd party modules. In this post we will discuss how to load the Python standard library, first using a simple, but limited approach provided by IBM and then a more robust and comprehensive solution using an Automation Script library.
By default Maximo will look in two locations for Python libraries. The path defined by the mxe.pylib.path
property. If this property is set, then the directory pylib
is added to the path and the result is prepended to the Python engine path. For example if the mxe.pylib.path
property is /opt/python
, then the /opt/python/pylib
directory will be added to the path.
If the mxe.pylib.create
property is set to true
and the mxe.pylib.path
is not set, then the Java System property user.dir
will be used. The user.dir
property defines the path where the Java runtime process was started. The directory pylib
is then added to the user.dir
path, the same as with the mxe.lib.path
Maximo property. If the path does not exist, it is created and the contents of the com/ibm/tivoli/maximo/script/pylib.zip
, as found on the Java classpath, are extracted to the new directory.
Setting a static library path works well on Maximo 7.6 systems, where Maximo is installed and copying files to a specific directory is simple. However, this is not the case with the Maximo Application Suite, where pods are ephemeral and may be destroyed and recreated at any time.
The following two approaches provide a mechanism to ensure the Python libraries survive pod recreation.
The pylib.zip
is not included in the Maximo distribution and may be an artifact of an incomplete implementation or a feature that was previously available and later removed. But since the mechanism is still there, including a customization archive that contains a zip file named pylib.zip
and containing the desired libraries in the maximo/applications/maximo/classes/com/ibm/tivoli/maximo/script/
directory would then be included in the pod build process and the resulting businessobjects.jar
. The system would then extract the libraries to the specified path and add that directory to the Jython engine path.
Another other option is to create a PVC and then copy the desired libraries to a directory in the PVC in a subdirectory named pylib
, add a volume mount directive for the pod and then set the mxe.pylib.path
property to that directory. It is important to note that this volume must be mounted on all pods where the script may be executed, including UI, MIF and CRON deployment types.
Fortunately, the standard library is present within the jython.jar
under a directory named Lib
and is packaged with Maximo. But since it is inside a jar file, it is not available to the Jython script engine path. This is a problem we can solve and automate to provide the script engine access to the standard libraries.
The first thing we need to do is locate the jython.jar
within the Maximo Java classpath. It is possible that the Lib
directory is contained within multiple jar
files, so we will use the getClassLoader().getResources(<path_here>)
method to return an array of java.net.URI
objects representing the path to the Lib
directories on the Java classpath. The Py
class is imported and used to the the ClassLoader
object so we can ensure that we are using the ClassLoader
that loaded the Py
class from the jython.jar
and therefore, where we will also find the Lib
folder. The URI file paths that are return start with file:
and have an exclamation mark (!
) after the .jar
extension to denote directories and files within a jar file. Removing these two elements and performing a substring of the middle content provides the path to the jython.jar
.
The following code snippets do not include imports, this will be provided in the full script at the end.
# The path to the standard library within the jython.jarlib_path = "Lib"def getPythonJarLocations():#### Returns a list of jar locations that contain the Lib directory within the Jar.# Example:# getPythonJarLocations()#### Create a list to store the jar locationslibs = []# Get the class loader and get the resources for the Lib directorylocations = Py.getClassLoader().getResources(File.separator + lib_path)# Iterate through the locations and add the jar paths to the list# The getPath() method returns a string representation of the URLwhile locations.hasMoreElements():location = locations.nextElement()jarPath = location.getPath()# Find the start and end of the jar pathstart = int(jarPath.find("file:"))end = int(jarPath.find("!"))# If the start and end are valid, extract the jar path it should start with file:/ and end with !if start > -1 and end > -1:# Add the URL decoded jar path to the listlibs.append(URLDecoder.decode(File(URI(jarPath[start:end])).getAbsolutePath(), "UTF-8"))return libs
Once we have found the jython.jar
we need to extract the Lib
folder. The function below accepts a path to a Jar file and a path to an extract directory, then extracts the Lib
directory to the extract directory. It uses the JarFile
class that is provided by the Java runtime library to loop through entries and create the necessary directory structure, writing out the files to the extract directory.
def extractJar(jarFilePath, outputFolderPath):#### Extracts the Lib directory from the jar file to the output folder.# jarFilePath: The path to the jar file.# outputFolderPath: The path to the output folder.# Example:# extractJar("C:\\jython\\jython.jar", "C:\\pylib")###jarFile = JarFile(jarFilePath)entries = jarFile.entries()while entries.hasMoreElements():entry = entries.nextElement()if entry.getName().startswith(lib_path):entryFile = File(outputFolderPath, entry.getName()[3:])if not entryFile.exists():if entry.isDirectory():entryFile.mkdirs()else:entryFile.getParentFile().mkdirs()input = jarFile.getInputStream(entry)output = FileOutputStream(entryFile)buffer = array.array("b", [0] * 1024)length = input.read(buffer)while length > 0:output.write(buffer, 0, length)length = input.read(buffer)output.flush()output.close()input.close()
To ensure that the path is writable to the Java runtime, we use the java.io.tmpdir
to get the temporary directory path and then append pylib
as the location to extract the library files. Next, we check if the directory exists and if it has files. If the directory exists and contains files we skip the process as the files have already be extracted. Finally we call Py.getSystemState()
to get the Python runtime system state, which includes the system path. If it does not contain our new pythonPath
directory path, we add it to the path.
# Use a StringBuilder to build the path because concatenating strings with the + operator results in encoding errors and conflicts between encoded types.pythonPath = (StringBuilder(System.getProperty("java.io.tmpdir")).append(File.separator).append("pylib").toString())# Create a File object for the pythonPathpyPathFile = File(pythonPath)# Check that the pythonPath directory exists and that it is not emptyif not pyPathFile.exists() or pyPathFile.list() is None or len(pyPathFile.list()) == 0:# Get the available jar locations that contain the Lib directory within the Jar. This should only be the jython.jar file, but to be safe, we will check all jars.locations = getPythonJarLocations()# For each jar location, extract the Lib directory to the pythonPath directory.for location in locations:extractJar(location, pyPathFile.getAbsolutePath())# Get the system state and add the pythonPath to the system path if it is not already there.state = Py.getSystemState()# Check if the pythonPath is already in the system path and if not append it to the system path.if pythonPath not in state.path:state.path.insert(0, pythonPath)
By dynamically locating and extracting the Lib
directory from the jython.jar
file we ensure that the files are available to the Jython engine even if a pod is recreated. Evrery time the script is called it checks and will extract the necessary library files before executing the rest of the script. Additionally, if other jar files are included on the classpath, using a custom archive, that contain a Lib
directory, they too will be included in the available modules.
The complete script is available below and contains a scriptConfig
variable that can be used to deploy the script to Maximo using the Maximo Developer tools:
https://marketplace.visualstudio.com/items?itemName=sharptree.maximo-script-deploy
Otherwise you can manually create a new script named PYLIB
in your Maximo system.
from org.python.core import Pyfrom java.util.jar import JarFilefrom java.net import URLDecoder, URIfrom java.io import File, FileOutputStreamfrom java.lang import System, StringBuilderimport arraylib_path = "Lib"def getPythonJarLocations():#### Returns a list of jar locations that contain the Lib directory within the Jar.# Example:# getPythonJarLocations()#### Create a list to store the jar locationslibs = []# Get the class loader and get the resources for the Lib directorylocations = Py.getClassLoader().getResources(File.separator + lib_path)# Iterate through the locations and add the jar paths to the list# The getPath() method returns a string representation of the URLwhile locations.hasMoreElements():location = locations.nextElement()jarPath = location.getPath()# Find the start and end of the jar pathstart = int(jarPath.find("file:"))end = int(jarPath.find("!"))# If the start and end are valid, extract the jar path it should start with file:/ and end with !if start > -1 and end > -1:# Add the URL decoded jar path to the listlibs.append(URLDecoder.decode(File(URI(jarPath[start:end])).getAbsolutePath(), "UTF-8"))return libsdef extractJar(jarFilePath, outputFolderPath):#### Extracts the Lib directory from the jar file to the output folder.# jarFilePath: The path to the jar file.# outputFolderPath: The path to the output folder.# Example:# extractJar("C:\\jython\\jython.jar", "C:\\pylib")###jarFile = JarFile(jarFilePath)entries = jarFile.entries()while entries.hasMoreElements():entry = entries.nextElement()if entry.getName().startswith(lib_path):entryFile = File(outputFolderPath, entry.getName()[3:])if not entryFile.exists():if entry.isDirectory():entryFile.mkdirs()else:entryFile.getParentFile().mkdirs()input = jarFile.getInputStream(entry)output = FileOutputStream(entryFile)buffer = array.array("b", [0] * 1024)length = input.read(buffer)while length > 0:output.write(buffer, 0, length)length = input.read(buffer)output.flush()output.close()input.close()# Use a StringBuilder to build the path because concatenating strings with the + operator results in encoding errors and conflicts between encoded types.pythonPath = (StringBuilder(System.getProperty("java.io.tmpdir")).append(File.separator).append("pylib").toString())# Create a File object for the pythonPathpyPathFile = File(pythonPath)# Check that the pythonPath directory exists and that it is not emptyif not pyPathFile.exists() or pyPathFile.list() is None or len(pyPathFile.list()) == 0:# Get the available jar locations that contain the Lib directory within the Jar. This should only be the jython.jar file, but to be safe, we will check all jars.locations = getPythonJarLocations()# For each jar location, extract the Lib directory to the pythonPath directory.for location in locations:extractJar(location, pyPathFile.getAbsolutePath())# Get the system state and add the pythonPath to the system path if it is not already there.state = Py.getSystemState()# Check if the pythonPath is already in the system path and if not append it to the system path.if pythonPath not in state.path:state.path.insert(0, pythonPath)scriptConfig="""{"autoscript": "PYLIB","description": "Python Standard Library","version": "1.0.0","active": true,"logLevel": "ERROR","allowInvokingScriptFunctions": false}"""
Including the entire solution at the beginning of every script that uses the standard library is cumbersome and difficult to maintain. Fortunately, there is a mechanism that allows a script into another, using the exec
function.
Once the above PYLIB
script has been created, the source for the PYLIB
script can be obtained using the ScriptCache.getInstance().getScript("PYLIB").getScriptSource()
function. This retrieves the script source from the Maximo script cache minimizing any performance penalty since no database calls are required. Once the source is obtained it is included in the script by calling exec
and passing the source of the PYLIB
script.
This can be accomplished in as little as two lines as shown below.
from com.ibm.tivoli.maximo.script import ScriptCacheexec(ScriptCache.getInstance().getScriptInfo("PYLIB").getScriptSource())
However, in production code we recommend using this more robust example that checks that the PYLIB
script exists and ensures it is a Jython script.
from com.ibm.tivoli.maximo.script import ScriptCache, JSR223ScriptDriver# Note the use of "snake_case" for variable names as is the PEP8 naming convention standard for Pythondef get_auto_script(auto_script_name):# Use the ScriptCache to get the script source so you are not making a database call, which is otherwise expensivescriptInfo = ScriptCache.getInstance().getScriptInfo(auto_script_name)if scriptInfo is not None:# Make sure that the script is Jython / Python. Note that we are comparing the engine name because both Jython and Python use the same engine.# Also note that getScriptLanguge() is the correct method name and Language is misspelled in the Maximo APIif JSR223ScriptDriver.getSupportedScriptEngineInfo(scriptInfo.getScriptLanguge()).getEngineName() == "jython":return scriptInfo.getScriptSource()else:raise Error("The script " + auto_script_name + " is not a Jython script.")else:raise Error("The script " + auto_script_name + " was not found.")exec(get_auto_script("PYLIB"))
Calling
exec
on untrusted code can be extremely dangerous. Do not use this feature to execute untrusted scripts.
Once the PYLIB
script has been called, any of the standard library modules can be imported and used. In the simple example below the httplib
module is used to connecto the the https://www.python.org
site and print the resulting status code and message.
import httplibconn = httplib.HTTPSConnection("www.python.org")conn.request("GET", "/")r1 = conn.getresponse()print(str(r1.status) + " " + r1.reason)
A full list of modules can be found here https://docs.python.org/2.7/library/index.html
In this post, we explored the standard Maximo features for adding Python libraries to the system path and then demonstrated how to dynamically locate and load the Python standard library modules. This allows the Python standard libraries or any other pure Python 2.7 libraries to be added to the system path. This allows the rich ecosystem of native Python modules to be utilized within Maximo and greatly enhances what is possible with Jython in Automation Scripts.
If you have any questions, comments or have suggestions for topics you would like to see us cover, please reach out to us at [email protected]