For this given python function :

file name demo.py

def return_tuple():
    return 1, 2, 3

To retrieve the values in ObjectScript, you can use the following code, basically use dunder methods to access the values.

set return = ##class(%SYS.Python).Import("demo")."return_tuple"()
write return."__len__"() // 3
write return."__getitem__"(0) // 1
write return."__getitem__"(1) // 2
write return."__getitem__"(2) // 3

Once again, can we wrote a wrapper class for JDBC driver, eg : com.intersystems.dc.jdbc.IRISJDBCDriver, and use it to connect to IRIS?

The wrapper can be a public repository, and published to maven central, so that it can be used by anyone.

Like that no conflict with the official driver, and we can use it in any project.

Downside is that the name is not the same, so we need to change the driver name in the code, but it's a small price to pay.

Hi,

As you are in Aync mode, it's a bit tricky to get the end time. In sync mode, you just have to wait for the answer to be back.

In this case I would use the session id from the message that is going out of the business service to the BO.

So in the BS, try something like this:

Method OnProcessInput(
    pDocIn As %RegisteredObject,
    Output pDocOut As %RegisteredObject) As %Status
{
    // here you need to pass the jobid from the %CSP.REST that is invoking the BS
    set tJobId = pDocIn.JobID
    // your code here
    set ^CallApi($CLASSNAME(),tJobId,"sessionid") = ..%SessionId
    set pDocOut = pDocIn
    quit $$$OK
}

Then in the BO, you can use the session id to get the information from the global.

Method MyAsyncMethod(
    pRequest As %RegisteredObject,
    Output pResponse As %RegisteredObject) As %Status
{
    set tSessionId = ..%SessionId
    // lookup for the good session id
    for {
        set tClassname = $ORDER(^CallApi(tClassname))
        quit:tClassname=""
        for {
            set tJobId = $ORDER(^CallApi(tClassname,tJobId))
            quit:tJobId=""
            if ^CallApi(tClassname,tJobId,"sessionid") = tSessionId {
                set ^CallApi(tClassname,tJobId,"End") = $HOROLOG
                quit
            }
        }
    }
    // your code here
    quit $$$OK
}

I hope this helps.

Thank you for your kind words, I'm glad you liked the article. I've read your article and it's a great use case of Embedded Python in IRIS.

I noticed that you are making an extensive use of the language tags ( [ language = python ])in your article.

In this article, I try to explain how I try to move away from the language tags and instead stick with only ObjectScript code and use the ##class(%SYS.Python).Import() method to call Python code from ObjectScript or even have a python first approach.

Since you are using the language tags, I'm curious to know if you have tried the ##class(%SYS.Python).Import() method or a python first approach and if so, what are the advantages and disadvantages of using the language tags over the ##class(%SYS.Python).Import() method and the python first approach?

Are you agree with me on all the drawbacks of using the language tags ( it's not Pythonic, it's not ObjectScript either, you don't have a debugger, you don't have a linter, you mixing two language in the same file, when you process crashes, you don't have a stack trace, ... ) ?

Ok, I see your point and I have a better understanding of your first question.

The issue here is that the Python interpreter is not able to find the module hello_world because it is not in the Python path. The Python path is a list of directories that the interpreter searches to find the modules that you import. By default, the Python path in embedded python is /<install dir>/lib/python.

To solve this issue, you can add the directory containing the hello_world module to the Python path. You can do this by adding the directory to the sys.path list in Python. Here is an example of how you can do this:

Class dc.PythonProxy Extends %RegisteredObject
{

Property PythonPath As %String;
Property PythonModule As %String;
Property PythonClassname As %String;
Property PythonClass As %SYS.Python;

ClassMethod SetPythonPath(pClasspaths)
{
    set sys = ##class(%SYS.Python).Import("sys")

    // avoid adding the same path multiple times
    for i=0:1:(sys.path."__len__"()-1) {
        Try {
            if sys.path."__getitem__"(i) = pClasspaths {
                do sys.path."__delitem__"(i)
            }
        }
        Catch ex {
            // do nothing
        }

    }
    do sys.path.insert(0, pClasspaths)
}

ClassMethod GetPythonInstance(
    pModule,
    pRemoteClassname) As %SYS.Python
{
    set importlib = ##class(%SYS.Python).Import("importlib")
    set builtins = ##class(%SYS.Python).Import("builtins")

    set module = importlib."import_module"(pModule)
    do importlib."reload"(module)

    set class = builtins.getattr(module, pRemoteClassname)
    return class."__new__"(class)
}


Method %OnNew(pPythonPath,pPythonModule,pPythonClassname) As %Status
{
    // set variables
    set ..PythonPath = pPythonPath
    set ..PythonModule = pPythonModule
    set ..PythonClassname = pPythonClassname

    // Then set the python class
    do ..SetPythonPath(..PythonPath)
    set ..PythonClass = ..GetPythonInstance(..PythonModule, ..PythonClassname)

    quit $$$OK
}

}

With this class you have a generic way to import any python module and class. You can use it like this:

Set pythonProxy = ##class(dc.PythonProxy).%New("/iris-shared/python", "hello_world", "HelloWorld")
Write pythonProxy.PythonClass.sayhello()

This will add the /iris-shared/python directory to the Python path and then import the hello_world module and create an instance of the HelloWorld class.

With the language tag you can do something like this:

Class dc.boto Extends %RegisteredObject
{

ClassMethod test1() [ Language = python ]
{
    # set path
    import sys

    # check if the path is already in the sys.path
    if "/iris-shared/python" not in sys.path:
        sys.path.insert(0, "/iris-shared/python")

    # import module
    import hello_world
    greeting = hello_world.HelloWorld()

    # Call the say_hello method
    ret=greeting.sayhello()
    print(ret)
}

}

I hope this helps! Let me know if you have any other questions.

Many thanks for your article, Heloisa!

I guess that we have the same goal: to make IRIS more accessible to Python developers.

Do you know that IRIS 2024.1 will have a native support of WSGI :)

I'm looking forward to your next article!

If you have time can you have a look at my article about feedback using embedded python daily for more than 2 years and tell me what do you think about it?

Thanks @Mario Sanchez Macias for your feedback. I understand your concerns and will address them in this response.

The example I provided is indeed complex, and I appreciate your patience. I will provide a simpler example to illustrate the integration of Python with ObjectScript.

First, I will give you a quick tip on how to simplify your code without the need of importing the same libraries repeatedly.

In ObjectScript, you create a class like this:

Class dc.boto3 Extends %RegisteredObject
{

XData %import [ MimeType = application/python ]
{
import boto3
}

ClassMethod moveFile() [ Language = python ]
{
    boto3.client('s3').download_file('bucket', 'key', 'filename')
    # do stuff
}

ClassMethod getFile() [ Language = python ]
{
    boto3.client('s3').upload_file('filename', 'bucket', 'key')
    # do stuff
}

}

In this example, I've created a class called dc.boto3 that extends %RegisteredObject. I've added an XData block to import the boto3 library. This way, you don't need to import the library in each method.

But in this example you are still mixing Python and ObjectScript code.

You can also try to stick to only to ObjectScript and use the ##class(%SYS.Python).Import method to import the Python library and use it in your ObjectScript code.

Here's an example of how you can use it to import the boto3 library and use it in your ObjectScript code:

Class dc.boto3 Extends %RegisteredObject
{

ClassMethod moveFile()
{
    set boto3 = ##class(%SYS.Python).Import("boto3")
    set s3 = boto3.client('s3')
    do s3."download_file"('bucket', 'key', 'filename')
    # do stuff
}

ClassMethod getFile()
{
    set boto3 = ##class(%SYS.Python).Import("boto3")
    set s3 = boto3.client('s3')
    do s3."upload_file"('filename', 'bucket', 'key')
    # do stuff
}

}

Now, to move to a python only approach, this will depend on your application architecture and the complexity of your Python code.

  • First, do you use the Interoperability framework of Iris ?

If yes, it will be quite easy have a look at IoP, it will allow you to create Services, Processes, Operations in Python Only and call them from a Production.

  • Your entry point is %CSP.Rest application ?

You can start to create .py files and import them in your ObjectScript code. with the ##class(%SYS.Python).Import method.

  • Create an ObjectScript abstract class that will be used to call your Python code.

This is my favorite approach, but the most complex one. This is the one I try to explain in the article.

If we take the example of the boto3 library.

First create the abstract class in ObjectScript:

Class dc.boto3 Extends %RegisteredObject [ Abstract ]
{

ClassMethod moveFile()
{
    Quit $$$ERROR($$$NOTIMPLEMENTED)
}

ClassMethod getFile()
{
    Quit $$$ERROR($$$NOTIMPLEMENTED)
}

}

The great thing about this approach is that you can create a class that extends this abstract class and implement the methods in Python, ObjectScript or any other language.

Then create the implementation of this class in ObjectScript:

Class dc.boto3Python Extends dc.boto3
{

Property PythonPath As %String;
Property PythonClassname As %String;
Property PythonModule As %String;
Property PythonClass As %SYS.Python;

Method %OnNew() As %Status
{
    // %OnNew is called when the object is created.
    set ..PythonPath = $system.Util.GetEnviron("PYTHON_BOTO_PATH")
    // Then set the python class name from the env var
    set ..PythonClassname = $system.Util.GetEnviron("PYTHON_BOTO_CLASS")
    // Then set the python module name from the env var
    set ..PythonModule = $system.Util.GetEnviron("PYTHON_BOTO_MODULE")

    if (..PythonPath = "") || (..PythonClassname = "") || (..PythonModule = "") {
        //quit ##super(pStrategy)
        set ..PythonPath = "/irisdev/app/src/python/"
        set ..PythonClassname = "CustomPythonBoto"
        set ..PythonModule = "custom"
    }


    // Then set the python class
    do ..SetPythonPath(..PythonPath)
    set ..PythonClass = ##class(FHIR.Python.Interactions).GetPythonInstance(..PythonModule, ..PythonClassname)

    quit ##super(pStrategy)
}

ClassMethod moveFile()
{
    do ..PythonClass."moveFile"()
}

ClassMethod getFile()
{
    do ..PythonClass."getFile"()
}

}

Now you can create a Python class that is used to implement the methods of the dc.boto3Python class.

import boto3

class CustomPythonBoto:
    def moveFile(self):
        s3 = boto3.client('s3')
        s3.download_file('bucket', 'key', 'filename')
        # do stuff

    def getFile(self):
        s3 = boto3.client('s3')
        s3.upload_file('filename', 'bucket', 'key')
        # do stuff

Now anone in your company can use the dc.boto3Python class as it's an ObjectScript class. But in fact, it's proxying the Python class CustomPythonBoto.

I hope this helps. Let me know if you have any further questions.

Hi Henrique and José,

I love your project and the idea behind it.

I'm wondering if you have any plans to integrate IRIS-FHIRfy with IoP (Interoperability on Python) ?

Why ?

Because you project at the end creates a lot of Python code that can be used and wrapped in IoP.

May be it's just need to update your prompt to include some notion of IoP and we can end up with a turnkey solution with ready to use code with IoP and IRIS interoperability.