About the idea of Using Python Class Definition Syntax to create IRIS classes
Tags: python, interoperability, tutorial, programming
Published: 2024-02-13 18:41:16

<h2>Introduction</h2>
<p>Not so long ago, I came across the idea of <a href="https://ideas.intersystems.com/ideas/DPI-I-394" target="_blank">using Python Class Definition Syntax to create IRIS classes</a> on the InterSystems Ideas Portal. It caught my attention since integrating as many syntaxes as possible gives visibility to InterSystems’s products for programmers with experience in many languages.</p>
<p>The author of that idea pointed out the possibility of creating classes using Python’s syntax, in addition to the currently available ones on IRIS. This concept inspired me to write this article, exploring the possibilities of accessing the full InterSystems power employing only Python.</p>
<p>It is worth noticing that one of the precursors of all this power is the multi-model capability of IRIS. It allows you to access the same data through Globals (a multidimensional storage structure). Also, it is the underlying structure for all other models. Likewise, you can access the above-mentioned data by objects. In the scope of this article, accessing data by object means using JSON or XML adapters for connecting IRIS and Python. Besides, you can access data by SQL utilizing JDBC or ODBC connections. Lastly, you can employ <a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GDOCDB_intro" target="_blank">DocDB</a> (Document database) that also operates JSON syntax to get the same result.</p>
<h2>Create an IRIS database from a Python class using SQL</h2>
<p>To start, we should discuss the main point of the idea: how to use Python’s class definition syntax to create an IRIS class. It is possible to achieve that, but the complexity of this implementation will depend entirely on the sophistication of the relationships defined in your class. You can click <a href="https://github.com/heloisatambara/py-to-cls/" target="_blank">here</a>&nbsp;to look at my studies on this solution. Do not hesitate to contact me if you wish to discuss it more!</p>
<p>If you only need the properties of a class defined in Python, the easiest approach will be to use SQLAlchemy’s<em> mapper()</em> function to create your class.</p>
<p>You will require the following imports to do that:<br>&nbsp;</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs"><span class="hljs-keyword">from</span> sqlalchemy <span class="hljs-keyword">import</span> create_engine, MetaData
<span class="hljs-keyword">from</span> sqlalchemy.schema <span class="hljs-keyword">import</span> Table, Column
<span class="hljs-keyword">from</span> sqlalchemy.orm <span class="hljs-keyword">import</span> mapper
</code></pre>
<p>Of course, you should begin by defining your engine, and only then creating your table. Optionally, you may define a primary key column if your class does not have it already.</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs">engine = create_engine(<span class="hljs-string">"iris://"</span> + user + <span class="hljs-string">":"</span> + password + <span class="hljs-string">"@"</span> + host + <span class="hljs-string">":"</span> + port + <span class="hljs-string">"/"</span> + namespace)
metadata = MetaData()
table = Table(class_name, metadata, Column(<span class="hljs-string">'id'</span>, Integer,  primary_key = <span class="hljs-keyword">True</span>))
</code></pre>
<p>Additionally, for each property in your class, remember to append a <em>Column()</em> in your table with its name, SQL type from sqlalchemy.sql.sqltypes, and optionally a default value. You may want to check out this very useful <a href="https://stackoverflow.com/questions/4165143/easy-convert-betwen-sqlalchemy-column-types-and-python-data-types" target="_blank">link</a> with a Python to SQL dictionary to simplify your task.</p>
<p>Finally, you should call the mapper with your class and the table as arguments, and create them with your engine.<br>&nbsp;</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs">mapper(my_class, table)
metadata.create_all(engine)
</code></pre>
<p>If you nonetheless desire to develop methods, you can access your newly created .cls file, define a method with ObjectScript definition, and set its language to Python.</p>
<p>Yet, you may still avoid this single line of ObjectScript when creating your class. The way to do it is to use <a href="https://docs.intersystems.com/iris20233/csp/docbook/Doc.View.cls?KEY=PAGE_python_native" target="_blank">InterSystems Native SDK for Python</a> to <a href="https://community.intersystems.com/post/python-and-iris-practice-examples" target="_blank">connect Python to Iris</a> and utilize it to access <a href="https://docs.intersystems.com/iris20233/csp/docbook/Doc.View.cls?KEY=GOBJ_dictionary" target="_blank">%Dictionary</a> classes. Those classes will help you build your .cls file. You will need a schema, a class name, property names and types, and methods implementations to accomplish this task.</p>
<p>If you have a class defined in Python, you can easily access that information. Define the schema as you like and access the class name in one line of code, as shown below:</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs">class_name = your_class.__class__.__name__</code></pre>
<p>Next, the built-in function <em>dir()</em> will give you every attribute within a class. You can use the <em>callable()</em> method to check whether it is a property or a method. Finally, you can import the module inspect and utilize its functions <em>getsource()</em> and <em>getfullargspec()</em> to access every piece of information from your methods.</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs">implementation = i.getsource(function) <span class="hljs-comment"># this gets the written code as a string</span>
inspection = i.getfullargspec(function) 
defaults = inspection.defaults
annotation = inspection.annotations
arguments = inspection[<span class="hljs-number">0</span>]
</code></pre>
<p>Bear in mind that if the first argument in a class function from Python is “self”, it is reflected in ObjectScript as a Method. Otherwise, it will be represented as a ClassMethod.</p>
<p>With those elements, you can open your class and modify it with the %Dictionary classes.&nbsp;<br>&nbsp;</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs">new_class = irispy.classMethodObject(<span class="hljs-string">"%Dictionary.ClassDefinition"</span>, <span class="hljs-string">"%Open"</span>, class_name)
new_method = irispy.classMethodObject(<span class="hljs-string">"%Dictionary.MethodDefinition"</span>, <span class="hljs-string">"%New"</span>, class_name + <span class="hljs-string">":"</span> + method_name)
new_method.set(<span class="hljs-string">"Language"</span>, <span class="hljs-string">"python"</span>)
new_method.get(<span class="hljs-string">"Implementation"</span>).invoke(<span class="hljs-string">"Write"</span>, implementation)
new_method.set(<span class="hljs-string">"ClassMethod"</span>, isClassMethod)
new_method.set(<span class="hljs-string">"FormalSpec"</span>, formal_spec)
new_class.get(<span class="hljs-string">"Methods"</span>).invoke(<span class="hljs-string">"Insert"</span>, new_method)
</code></pre>
<p>In the above-written code, the irispy object was generated by connecting the IRIS instance with the method <em>createIRIS()</em> from the IRIS module. You can discover more details about establishing the connection in the official documentation or one of my previous articles. Yet, right now, the code illustrated below is all you need to know:</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs"><span class="hljs-keyword">import</span> iris

<span class="hljs-comment"># Open a connection to the server</span>
args = {<span class="hljs-string">'hostname'</span>:<span class="hljs-string">'127.0.0.1'</span>, <span class="hljs-string">'port'</span>:<span class="hljs-number">52773</span>,
    <span class="hljs-string">'namespace'</span>:<span class="hljs-string">'USER'</span>, <span class="hljs-string">'username'</span>:<span class="hljs-string">'_SYSTEM'</span>, <span class="hljs-string">'password'</span>:<span class="hljs-string">'SYS'</span>
}
conn = iris.connect(**args)
<span class="hljs-comment"># Create an iris object</span>
irispy = iris.createIRIS(conn)
</code></pre>
<p>You can create your formal_spec with the annotation from the inspection.</p>
<p>For the last step, remember to invoke %Save on this definition.<br>&nbsp;</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs">sc = new_class.invoke(<span class="hljs-string">"%Save"</span>)</code></pre>
<h2>&nbsp;</h2>
<h2>Create an IRIS database from a Python class using DocDB</h2>
<p>First, you should enable your instance to receive this type of REST request. To do that, go to System Administration &gt; Security &gt; Service, select %Service_DocDB, choose “Service enabled”, and click save. Next, your system may require installing the Python requests module with<code> pip install requests.&nbsp;</code></p>
<p>If you are not familiar with REST APIs with Python, you will still be able to follow this tutorial since everything that will be used here is quite intuitive.</p>
<p>We will only need to send a few URLs to the %Api.DocDB.v1, which is a built-in option in IRIS. Just import requests in your Python environment and write a URL with the pattern demonstrated below:</p>
<pre class="codeblock-container" idlang="2" lang="Python" tabsize="4"><code class="language-python hljs">url = http://HOST:PORT/api/docdb/v1/NAMESPACE/db/DBNAME?type=cls</code></pre>
<p>Then, send a POST request to this URL with <code>request.post(url)</code>. Ready! Now you should be able to see a class called CLASSNAME in the default schema ISC_DM, in the selected namespace.</p>
<p>After creating the class, you can add properties by sending more POST requests with the following URL pattern:</p>
<pre>http://HOST:PORT/api/docdb/v1/NAMESPACE/prop/DBNAME/PROPERTYNAME?type=%String&amp;path=PROPERTYPATH</pre>
<p>You can perceive the property path as the path you would use to get this property value in case it were stored in a dynamic object built from JSON. For instance, imagine that you can represent your object like the code snippet below.<br>&nbsp;</p>
<pre class="codeblock-container" idlang="3" lang="JSON" tabsize="4"><code class="language-json hljs">{
    <span class="hljs-attr">"person"</span>: {
        <span class="hljs-attr">"name"</span>:<span class="hljs-string">"heloisa tambara"</span>,
        <span class="hljs-attr">"country"</span>:<span class="hljs-string">"brazil"</span>
    }
}
</code></pre>
<p>In this case, the path for the name property would be person.name.</p>
<p>Finally, you can start populating this table with one more URL pattern:</p>
<pre>http://HOST:PORT/api/docdb/v1/NAMESPACE/doc/DBNAME/ID</pre>
<p>If you want to create a new document, use the POST method and leave the ID field empty. On the other hand, to replace an existing document, utilize the PUT method and specify the ID you wish to modify.</p>
<p>Write the body of your request according to the property paths you have employed. For example, if you have created some properties with the paths “person.name”, “person.location.country”, “person.location.state”,“person.location.city” you can add a document by sending the JSON below in the body of your request.<br>&nbsp;</p>
<pre class="codeblock-container" idlang="3" lang="JSON" tabsize="4"><code class="language-json hljs">{
    <span class="hljs-attr">"person"</span>: {
        <span class="hljs-attr">"name"</span>:<span class="hljs-string">"Heloisa Tambara"</span>,
        <span class="hljs-attr">"location"</span>: {
            <span class="hljs-attr">"country"</span>:<span class="hljs-string">"Brasil"</span>,
            <span class="hljs-attr">"state"</span>:<span class="hljs-string">"Pernambuco"</span>,
            <span class="hljs-attr">"city"</span>:<span class="hljs-string">"Recife"</span>
        }
    }
}
</code></pre>
<p>This section was designed to give you just a bit of a taste of what DocDB can do. In the documentation you can access by clicking <a href="https://docs.intersystems.com/iris20233/csp/docbook/DocBook.UI.Page.cls?KEY=GDOCDB_cos" target="_blank">here</a>, you can uncover all available methods in more detail.</p>
<p>&nbsp;</p>
<h2>Going further: use IRIS interoperability without ObjectScript</h2>
<p>For most implementations, you can almost abolish the use of COS classes. Suppose you are looking for a full interoperability production built only with Python. In that case, you can try PEX (Production EXtension framework) and limit the use of .cls files for the production class only. The best practice for utilizing PEX is to employ Guillaume Rongier’s application (<a href="https://openexchange.intersystems.com/package/interoperability-embedded-python" target="_blank">interoperability-embedded-python</a>), which bridges the gap between the InterSystems’s architecture for interoperability and Python. If you are not familiar with that structure yet, please take a look at the 30-minute-long course dedicated to <a href="https://learning.intersystems.com/course/view.php?id=908" target="_blank">Integration Architecture</a>.</p>
<p>If that is your goal, I recommend you skim through the article about <a href="https://community.intersystems.com/post/full-python-iris-production-datatransformation-csv-fhir-server#5-walkthrough" target="_blank">Full Python IRIS production: DataTransformation of a CSV to a FHIR server</a> by Lucas Enard. The GitHub page related to the OpenExchange application described in the text shows a great example of how to achieve mostly Python development.</p>
<p>&nbsp;</p>
<h2>There is always more…</h2>
<p>As you can see, there are many ways of creating, manipulating, and populating classes in IRIS. Have you already acknowledged all of those described above? Which one would you like me to write more about? What other ways do you know that I have not talked about yet? Please let me know your thoughts!<br>&nbsp;</p>