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.

What matters is more the maturity of each resource.
Maturity is rated from 0 to 5, where 0 is a draft, and 5 is normative.
Further the realse of FHIR, more mature resources are.
It's plan than R6 will be the first normative release of FHIR, it's even plan to be an ISO standard.
This mean that all resources related to Patient will be normative, and will not change in the future.

I haven't tried it yet on 2021.2, but on 2023.1 you use case is working :

IRISAPP>w ##class(Zpy.Utility).Info()
def
IRISAPP>zn "USER"
USER>w ##class(Zpy.Utility).Info()
def
USER>zw $zv
"IRIS for UNIX (Ubuntu Server LTS for x86-64 Containers) 2023.1 (Build 229U) Fri Apr 14 2023 17:37:52 EDT"

Can you update iris ?

If you can't try to map rPYC global to the USER namespace.

rPYC global actually contains the python code.

Hello Cyril,

I have finished this exercise, I will share my experience on this subject with you:

Have you checked that the snmpd deamon is installed and configured on your docker instance?

By default, it is not installed, so you have to install and configure it.

Example of a dockerfile to install snmpd:

ARG IMAGE=intersystemsdc/iris-community:latest
FROM $IMAGE

WORKDIR /irisdev/app

USER root
RUN apt-get update && apt-get install -y \
    nano \
    snmpd \
    snmp \
    sudo && \
    /bin/echo -e ${ISC_PACKAGE_MGRUSER}\\tALL=\(ALL\)\\tNOPASSWD: ALL >> /etc/sudoers && \
    sudo -u ${ISC_PACKAGE_MGRUSER} sudo echo enabled passwordless sudo-ing for ${ISC_PACKAGE_MGRUSER}

COPY snmpd.conf /etc/snmp/snmpd.conf
USER ${ISC_PACKAGE_MGRUSER}

Example of a snmpd.conf:

###############################################################################
#
# snmpd.conf:
#   An example configuration file for configuring the NET-SNMP agent with Cache.
#
#   This has been used successfully on Red Hat Enterprise Linux and running
#   the snmpd daemon in the foreground with the following command:
#
#   /usr/sbin/snmpd -f -L -x TCP:localhost:705 -c./snmpd.conf
#
#   You may want/need to change some of the information, especially the
#   IP address of the trap receiver of you expect to get traps. I've also seen
#   one case (on AIX) where we had to use  the "-C" option on the snmpd command
#   line, to make sure we were getting the correct snmpd.conf file. 
#
###############################################################################

###########################################################################
# SECTION: System Information Setup
#
#   This section defines some of the information reported in
#   the "system" mib group in the mibII tree.

# syslocation: The [typically physical] location of the system.
#   Note that setting this value here means that when trying to
#   perform an snmp SET operation to the sysLocation.0 variable will make
#   the agent return the "notWritable" error code.  IE, including
#   this token in the snmpd.conf file will disable write access to
#   the variable.
#   arguments:  location_string

syslocation  "System Location"

# syscontact: The contact information for the administrator
#   Note that setting this value here means that when trying to
#   perform an snmp SET operation to the sysContact.0 variable will make
#   the agent return the "notWritable" error code.  IE, including
#   this token in the snmpd.conf file will disable write access to
#   the variable.
#   arguments:  contact_string

syscontact  "Your Name"

# sysservices: The proper value for the sysServices object.
#   arguments:  sysservices_number

sysservices 76

###########################################################################
# SECTION: Agent Operating Mode
#
#   This section defines how the agent will operate when it
#   is running.

# master: Should the agent operate as a master agent or not.
#   Currently, the only supported master agent type for this token
#   is "agentx".
#   
#   arguments: (on|yes|agentx|all|off|no)

master agentx
agentXSocket tcp:localhost:705

###########################################################################
# SECTION: Trap Destinations
#
#   Here we define who the agent will send traps to.

# trapsink: A SNMPv1 trap receiver
#   arguments: host [community] [portnum]

trapsink  localhost public   

###############################################################################
# Access Control
###############################################################################

# As shipped, the snmpd demon will only respond to queries on the
# system mib group until this file is replaced or modified for
# security purposes.  Examples are shown below about how to increase the
# level of access.
#
# By far, the most common question I get about the agent is "why won't
# it work?", when really it should be "how do I configure the agent to
# allow me to access it?"
#
# By default, the agent responds to the "public" community for read
# only access, if run out of the box without any configuration file in 
# place.  The following examples show you other ways of configuring
# the agent so that you can change the community names, and give
# yourself write access to the mib tree as well.
#
# For more information, read the FAQ as well as the snmpd.conf(5)
# manual page.
#
####
# First, map the community name "public" into a "security name"

#       sec.name  source          community
com2sec notConfigUser  default       public

####
# Second, map the security name into a group name:

#       groupName      securityModel securityName
group   notConfigGroup v1           notConfigUser
group   notConfigGroup v2c           notConfigUser

####
# Third, create a view for us to let the group have rights to:

# Make at least  snmpwalk -v 1 localhost -c public system fast again.
#       name           incl/excl     subtree         mask(optional)
# access to 'internet' subtree
view    systemview    included   .1.3.6.1

# access to Cache MIBs Caché and Ensemble
view    systemview    included   .1.3.6.1.4.1.16563.1
view    systemview    included   .1.3.6.1.4.1.16563.2
####
# Finally, grant the group read-only access to the systemview view.

#       group          context sec.model sec.level prefix read   write  notif
access  notConfigGroup ""      any       noauth    exact  systemview none none

From this neat article : https://community.intersystems.com/post/intersystems-data-platforms-and-...

Then, you have to start the snmpd deamon:

sudo service snmpd start

On iris, you have to configure the snmp agent:

%SYS> w $$start^SNMP()

With all these steps, you should be able to retrieve information via snmp.

snmpwalk -m ALL -v 2c -c public localhost .iso.3.6.1.4.1.16563.4.1.1.1.5.4.73.82.73.83

Result :

iso.3.6.1.4.1.16563.4.1.1.1.5.4.73.82.73.83 = STRING: "IRIS for UNIX (Ubuntu Server LTS for x86-64 Containers) 2023.1 (Build 229U) Fri Apr 14 2023 17:37:52 EDT"

Now that the snmpd service is running and functional as well as the snmp agent.

I encourage you to have a look at OpenTelemetry, it's a new standard for monitoring and tracing.

https://opentelemetry.io/

And our implementation of OpenTelemetry for IRIS :

https://docs.intersystems.com/iris20233/csp/docbook/Doc.View.cls?KEY=GCM...

Great article :)

It brings back memories of a project where we built a state machine: https://developer.mozilla.org/fr/docs/Glossary/State_machine

Back then, message queues didn't exist, and neither did your tutorial ;) it would have helped us a lot.

Think about completing it with an example applied to websockets, I have the impression that it lends itself well to this.