DS2 Interface to Python

DS2 modules, running in SAS Micro Analytic Service, can publish and execute Python modules.
Note that Python 2.7 or Python 3.4 must be available for SAS Micro Analytic Service to load. If both are available, SAS Micro Analytic Service loads Python 3.4. See Python Support in SAS Micro Analytic Service, for information about installing Python and configuring the environment variables necessary to allow Python to run embedded in SAS Micro Analytic Service. As is the case when calling any package from DS2, it is recommended that you always check return codes where available, and return any error codes using an output argument from your DS2 method.
To call Python from DS2, use the DS2 package called pymas. Each pymas package instance represents exactly one Python module revision. You can create as many instances as you want, allowing multiple modules to be used.
Here are some operations that a DS2 module would typically perform.
Instantiate the following DS2 package:
py = _new_ pymas();
Calling publish() compiles your Python module and sets it as the module that is represented by this pymas instance. Subsequent pymas function calls, such as setting values and executing methods, operate on this module. The Python code is passed as a string in the first argument. Pass the name that you want to give to your new Python module in the second argument. publish() returns the revision number that SAS Micro Analytic Service assigned to your new module. You could use this revision number later to execute or delete a specific revision of your module. If you do not specify a revision number, the latest revision is assumed. If your Python code fails to publish (because of syntax errors, for example), then -1 is returned for the revision number.
revision = py.publish( pgm, moduleName );
In very rare cases, you might need to use a prior revision of a module rather than the latest revision that would be selected by default. Or, rather than publishing a Python module from DS2, you might need to specify a module that was previously published to SAS Micro Analytic Service by an external client. In these rare cases, you can call useModule() instead of publish(). If a module was already associated with your pymas instance before calling useModule(), then useModule() disassociates the current module from the instance before making the specified module current.
rc = py.useModule( moduleName, revision);
Before calling Python, you must tell the pymas instance which method to execute. This is accomplished by calling useMethod(). In addition to specifying the method (Python function) to call, useMethod() also validates that the method exists within the current module, prepares the pymas instance to receive the input values for the specific method arguments, and prepares to return any output values from the method execution.
rc = py.useMethod( methodName );
Call the type-specific setter methods to set input values before executing the method. Because these setters store arguments by name, they can be called in any order, and they insert the values in the correct positions:
py.setDouble(“airflow”, sensor_maf);
Since the DS2 package instance represents a single revision, the execute() method needs no arguments.
rc = py.execute();
After execution, call getters to retrieve the results.
score = py.getDouble(“credit_score”);
Scalar argument setters are of the form:
return_code = set<type>(name, value)
Scalar argument getters are of the form:
value = get<type>(name)
Array argument setters are of the form:
rc = set<type>Array(name, array-value)
Array argument getters are of the following form.
Note: DS2 passes arrays and output values by reference.
get<type>Array(name, array-value, rc)
The example below assumes that you have declared your package as py:
dcl package pymas py;
dcl int rc;
dcl bigint result;

rc = py.publish(python_source_code, my_module_name);
py.setString(“inString”, “A string”);

py.execute()

result = py.getLong(“outLong”);
The complete set of DS2 package methods follows, where rc is the integer return code, and py is the package instance.
Methods for Python module management and execution:
rc = py.publish(python_source_code, "module_name");
rc = py.remove();
rc = py.isLoaded(); // returns true is Python is available and false otherwise
revision = py.getRevisionNumber();
rc = py.setTimeZone(time_zone_identifier);
rc = py.execute();
Scalar argument setters:
 rc = py.setString(argument_name, value);
rc = py.setBool(argument_name, value);
rc = py.setLong(argument_name, value);
rc = py.setInt(argument_name, value);
rc = py.setDouble(argument_name, value);
rc = py.setDateTime(argument_name, value);
rc = py.setDate(argument_name, value);
rc = py.setTime(argument_name, value);
Scalar argument getters:
string_value   = py.getString(argument_name);
int_value              = py.getBool(argument_name);
long_value             = py.getLong(argument_name);
int_value              = py.getInt(argument_name);
double_value           = py.getDouble(argument_name);
date_time_value        = py.getDateTime(argument_name);
date_value             = py.getDate(argument_name);
time_value             = py.getTime(argument_name);
Array argument setters:
rc = py.setStringArray(argument_name, string_array);
rc = py.setBoolArray(argument_name, integer_array);
rc = py.setLongArray(argument_name, bigint_array);
rc = py.setIntArray(argument_name, integer_array);
rc = py.setDoubleArray(argument_name, double_array);
rc = py.setDateTimeArray(argument_name, date_time_array);
rc = py.setDateArray(argument_name, date_array);
rc = py.setTimeArray(argument_name, time_array);
Array argument getters:
py.getStringArray(argument_name, string_array, rc);
py.getBoolArray(argument_name, integer_array, rc);
py.getLongArray(argument_name, bigint_array, rc);
py.getIntArray(argument_name, integer_array, rc);
py.getDoubleArray(argument_name, double_array, rc);
py.getDateTimeArray(argument_name, date_time_array, rc);
py.getDateArray(argument_name, date_array, rc);
py.getTimeArray(argument_name, time_array, rc);
Python 2.x uses ASCII as the default encoding. Therefore, you must specify another encoding at the top of the file to use non-ASCII Unicode characters in literals. As a best practice, when using Python 2.x, always use the following as the first line of your Python script:
# -*- coding: utf-8 -*-
Also, in Python 2.x, the Unicode literal must be preceded by the letter u. Therefore, literal strings should be written using the following form:
u”xxxxx”
Note: Python 3.x uses UTF-8 as the default encoding, so these issues affect Python 2.x only. When using Python 3.x, the default encoding can be used, and literals can simply be enclosed in quotation marks.
If you prefer not to insert the linefeed characters yourself, you can add the Python source code line-by-line using the appendSrcLine() method. When the entire Python program has been added, you then call the getSource() method. The getSource() method returns the Python program as one string, inserting linefeed characters between Python source code lines. You can then pass that string to the publish method to publish the Python program in SAS Micro Analytic Service. Here is an example.
data tstinput; a = 8;  b = 4; output; a = 10; b = 2; output;
run;

proc ds2;
    ds2_options sas;
    package testpkg /overwrite=yes;
        dcl package pymas py();
        dcl package logger logr('App.TableServices.DS2.Runtime.Log');
        dcl varchar(67108864) character set utf8 pycode;
        dcl int rc revision;

        method testpkg( varchar(2048) modulename, varchar(2048)pyfuncname );
            rc = py.appendSrcLine('# Here is the first Python function:');
            rc = py.appendSrcLine('def domath1(a, b):');
            rc = py.appendSrcLine('  "Output: c, d"');
            rc = py.appendSrcLine('  print("Will compute {0} times {1}".format(a, b))');
            rc = py.appendSrcLine('  c = a * b');
            rc = py.appendSrcLine('  print("domath1 c is {0}".format(c))');
            rc = py.appendSrcLine('  print("domath1 also do {0} div {1}".format(a, b))');
            rc = py.appendSrcLine('  d = a / b');
            rc = py.appendSrcLine('  print("domath1 d is {0}".format(d))');
            rc = py.appendSrcLine('  return c, d');
            rc = py.appendSrcLine('');
            rc = py.appendSrcLine('# Here is the second function:');
            rc = py.appendSrcLine('def domath2(a, b):');
            rc = py.appendSrcLine('  "Output: c, d"');
            rc = py.appendSrcLine('  c,d = domath1( a, b )');
            rc = py.appendSrcLine('  print("domath2: c is {0} and d is {1}".format(c,d))');
            rc = py.appendSrcLine('  return c, d' );
            pycode = py.getSource();
            logr.log( 'I', 'pycode=$s', pycode );
            revision = py.publish( pycode, modulename );
            if revision lt 1 then 
                logr.log( 'E', 'pymas.publish() failed.');
            rc = py.useMethod( pyfuncname );
            if rc then 
                logr.log( 'E', 'pymas.useMethod() failed.');
        end;

        method exec( double a, double b, in_out int rc,
                     in_out double c, in_out double d );
            rc = py.setDouble( 'a', a );   if rc then return;
            rc = py.setDouble( 'b', b );   if rc then return;
            rc = py.execute();             if rc then return;
            c = py.getDouble( 'c' );
            d = py.getDouble( 'd' );
        end;
    endpackage;

    data _null_;
        dcl package logger logr( 'App.TableServices.DS2.Runtime.Log' );
        dcl package testpkg t( 'my Python Module Context name', 'domath2' );
        dcl int rc;
        dcl double a b c d;

        method run();
            a = b = c = d = 0.0;
            set tstinput;
            t.exec( a, b, rc, c, d );
            logr.log( 'I', '##### Results: a=$s   b=$s   c=$s   d=$s',
                      a, b, c, d );
            put a= b= c= d=;
        end;
    enddata;
    run;
quit;
When using PROC DS2 in a SAS session to create a pymas package instance, you cannot provide the Python program as one big quoted literal string. The reason is that the SAS tokenizer strips out the embedded line-ending characters, causing indentation problems in the Python code. In this situation, the pymas package's appendSrcLine() and getSource() methods can be used to produce a DS2 character variable containing the lines of code concatenated together with embedded linefeed characters separating the lines of Python code. Once you have added each line of your Python code to the pymas package instance using the appendSrcLine() method, you can use the "getSource() method to retrieve the complete program into a DS2 character variable, which can then be provided as the first input argument to the pymas publish() method. Here is an example.
ds2_options sas;
    package testpkg /overwrite=yes;
        dcl package pymas py();
        dcl package logger logr('App.tk.MAS');
        dcl varchar(67108864) character set utf8 pycode;
        dcl int rc revision;
 
        method testpkg( varchar(256) modulename, 
                        varchar(256) pyfuncname );
            rc = py.appendSrcLine('# The first Python function:');
            rc = py.appendSrcLine('def domath1(a, b):');
            rc = py.appendSrcLine('  "Output: c, d"');
            rc = py.appendSrcLine('  c = a * b');
            rc = py.appendSrcLine('  d = a / b');
            rc = py.appendSrcLine('  return c, d');
            rc = py.appendSrcLine('');
            rc = py.appendSrcLine('# Here is the second function:');
            rc = py.appendSrcLine('def domath2(a, b):');
            rc = py.appendSrcLine('  "Output: c, d"');
            rc = py.appendSrcLine('  c,d = domath1( a, b )');
            if rc then logr.log( 'E', 'py.appendSrcLine() failed.');
            rc = py.appendSrcLine('  return c, d' );
            pycode = py.getSource();
            revision = py.publish( pycode, modulename );
            if revision lt 1 then 
                logr.log( 'E', 'py.publish() failed.');
            rc = py.useMethod( pyfuncname );
            if rc then logr.log( 'E', 'py.useMethod() failed.');
        end;
 
        method usefunc( varchar(256) pyfuncname );
            rc = py.useMethod( pyfuncname );
            if rc then logr.log( 'E', 'py.useMethod() failed.');
        end;
 
        method exec( double a, double b, in_out int rc,
                     in_out double c, in_out double d );
            rc = py.setDouble( 'a', a );   if rc then return;
            rc = py.setDouble( 'b', b );   if rc then return;
            rc = py.execute();             if rc then return;
            c = py.getDouble( 'c' );
            d = py.getDouble( 'd' );
        end;
    endpackage;