Using the Python Binding
This chapter provides concrete examples of Python code that uses the Caché Python binding. The following subjects are discussed:
-
Python Binding Basics — the basics of accessing and manipulating Caché database objects.
-
Passing Parameters by Reference — some Python-specific ways to access Python bindings.
-
Using Collections — iterating through Caché lists and arrays.
-
Using Relationships — manipulating embedded objects.
-
Using Queries — running Caché queries and dynamic SQL queries.
-
Using %Binary Data — moving data between Caché %Binary and Python list of integers.
-
Handling Exceptions — handling Python exceptions and error messages from the Python binding.
Many of the examples presented here are modified versions of the sample programs. The argument processing and error trapping (try/catch) statements have been removed to simplify the code. See Sample Programs for details about loading and running the complete sample programs.
Python Binding Basics
A Caché Python binding application can be quite simple. Here is a complete sample program:
import codecs, sys
import intersys.pythonbind
# Connect to the Cache' database
url = "localhost[1972]:Samples"
user = "_SYSTEM"
password = "SYS"
conn = intersys.pythonbind.connection()
conn.connect_now(url,user,password, None)
database = intersys.pythonbind.database(conn)
# Create and use a Cache' object
person = database.create_new("Sample.Person", None)
person.set("Name","Doe, Joe A")
print "Name: " + str(person.get("Name"))
This code imports the intersys.pythonbind module, and then performs the following actions:
-
Connects to the Samples namespace in the Caché database:
-
Defines the information needed to connect to the Caché database.
-
Creates a Connection object (conn).
-
Uses the Connection object to create a Database object (database).
-
-
Creates and uses a Caché object:
-
Uses the Database object to create an instance of the Caché Sample.PersonOpens in a new tab class.
-
Sets the Name property of the Sample.PersonOpens in a new tab object.
-
Gets and prints the Name property.
-
The following sections discuss these basic actions in more detail.
Connecting to the Caché Database
The basic procedure for creating a connection to a namespace in a Cache database is as follows:
-
Establish the physical connection:
conn = intersys.pythonbind.connection() conn.connect_now(url,user,password, timeout)
The connect_now() method creates a physical connection to a namespace in a Caché database. The url parameter defines which server and namespace the Connection object will access. The Connection class also provides the secure_connect_now() method for establishing secure connections using Kerberos. See Connections for a detailed discussion of both methods.
-
Create a logical connection:
database = intersys.pythonbind.database(conn)
The Connection object is used to create a Database object, which is a logical connection that lets you use the classes in the namespace to manipulate the database.
The following code establishes a connection to the Samples namespace:
address ="localhost" # server TCP/IP address ("localhost" is 127.0.0.1) port = "1972" # server TCP/IP port number namespace = "SAMPLES" # sample namespace installed with Cache' url = address + "[" + port + "]:" + namespace user = "_SYSTEM"; password = "SYS"; conn = intersys.pythonbind.connection() conn.connect_now(url,user,password, None) database = intersys.pythonbind.database(conn)
Using Caché Database Methods
The intersys.pythonbind.database class allows you to run Caché class methods and connect to Caché objects on the server. Here are the basic operations that can be performed with the Database class methods:
-
Create Objects
The create_new() method is used to create a new Caché object. The syntax is:
object = database.create_new(class_name, initial_value)
where class_name is the name of a Caché class in the namespace accessed by database. For example, the following statement creates a new instance of the Sample.PersonOpens in a new tab class:
person = database.create_new("Sample.Person", None)
In this example, the initial value of person is undefined.
-
Open Objects
The openid() method is used to open an existing Caché object. The syntax is:
object = database.openid(class_name, id, concurrency, timeout)
For example, the following statement opens the Sample.PersonOpens in a new tab object that has an id with a value of 1:
person = database.openid("Sample.Person",str(1),-1,-1)
Concurrency and timeout are set to their default values.
-
Run Class Methods
You can run class methods using the following syntax:
result = database.run_class_method(classname,methodname,[LIST])
where LIST is a list of method arguments. For example, the database.openid() example shown previously is equivalent to the following code:
person = database.run_class_method("Sample.Person","%OpenId",[str(1)])
This method is the analogous to the Caché ##class syntax. For example, the following code:
list = database.run_class_method("%ListOfDataTypes","%New",[]) list.run_obj_method("Insert",["blue"])
is exactly equivalent to the following ObjectScript code:
set list=##class(%ListOfDataTypes).%New() do list.Insert("blue")
Using Caché Object Methods
The intersys.pythonbind.object class provides access to Caché objects. Here are the basic operations that can be performed with the Object class methods:
-
Get and Set Properties
Properties are accessed through the set() and get() accessor methods. The syntax for these methods is:
object.set(propname,value) value = object.get(propname)
For example:
person.set("Name","Doe, Joe A") name = person.get("Name")
Private and multidimensional properties are not accessible through the Python binding.
-
Run Object Methods
You can run object methods by calling run_obj_method():
answer = object.run_obj_method(MethodName,[LIST]);
For example:
answer = person.run_obj_method("Addition",[17,20])
This method is useful when calling inherited methods (such as %Save or %Id) that are not directly available.
-
Save Objects
To save an object, use run_obj_method() to call %Save:
object.run_obj_method("%Save",[])
To get the id of a saved object, use run_obj_method() to call %Id:
id = object.run_obj_method("%Id",[])
Passing Parameters by Reference
It is possible to pass arguments by reference with the Python binding. Since Python does not have a native mechanism for passing by reference, the binding passes all arguments as a list that can be modified by the called function. For example, assume the following Caché class:
Class Caudron.PassByReference Extends %Persistent [ ProcedureBlock ] {
ClassMethod PassByReference(ByRef Arg1 As %String) {
set Arg1="goodbye"
}
}
The following code passes the argument by reference:
list = ["hello"]
print "passed to method PassByReference = " + str(list[0])
database.run_class_method("Caudron.PassByReference","PassByReference",list)
print "returned by reference = " + str(list[0])
The print statements will produce the following output:
passed to method PassByReference = hello
returned by reference = goodbye
Using Collections and Lists
Caché %Collection objects are handled like any other Python binding object. Caché %ListOpens in a new tab variables are mapped to Python array references. The following sections demonstrate how to use both of these items.
%Collection Objects
Collections are manipulated through object methods of the Caché %Collection class. The following example shows how you might manipulate a Caché %ListOfDataTypes collection:
# Create a %ListOfDataTypes object and add a list of colors
newcolors = database.create_new("%ListOfDataTypes", None)
color_list = ['red', 'blue', 'green']
print "Adding colors to list object:"
for i in range(len(color_list)):
newcolors.run_obj_method("Insert",[color_list[i]])
print " added >"+ str(color_list[i]) +"<"
# Add the list to a Sample.Person object.
person = database.openid("Sample.Person",str(1),-1,0)
person.set("FavoriteColors",newcolors)
# Get the list back from person and print it out.
colors = person.get("FavoriteColors")
print "\nNumber of colors in 'FavoriteColors' list: %d"\
% (colors.get("Size"))
index = [0]
while (index[0] != None):
color = colors.run_obj_method("GetNext",index)
if (index[0] != None):
print " Color #%d = >%s<" % (index[0], str(color))
# Remove and replace the second element
index = 2
if (colors.get("Size") > 0):
colors.run_obj_method("RemoveAt",[index])
colors.run_obj_method("InsertAt",['purple',index])
newcolor = colors.run_obj_method("GetAt",[index])
print "\nChanged color #%d to %s." % (index, str(newcolor))
%List Variables
The Python binding maps Caché %ListOpens in a new tab variables to Python array references.
While a Python array has no size limit, Caché %ListOpens in a new tab variables are limited to approximately 32KB. The actual limit depends on the datatype and the exact amount of header data required for each element. Be sure to use appropriate error checking (as demonstrated in the following examples) if your %ListOpens in a new tab data is likely to approach this limit.
The examples in this section assume the following Caché class:
Class Sample.List Extends %Persistent
{
Property CurrentList As %List;
Method InitList() As %List {
q $ListBuild(1,"hello",3.14) }
Method TestList(NewList As %List) As %Integer {
set $ZTRAP="ErrTestList"
set ItemCount = $ListLength(NewList)
if (ItemCount = 0) {set ItemCount = -1}
q ItemCount
ErrTestList
set $ZERROR = ""
set $ZTRAP = ""
q 0 }
}
The TestList() method is used to test if a Python array is a valid Caché list. If the list is too large,the method traps an error and returns 0 (Python false). If the list is valid, it returns the number of elements. If a valid list has 0 elements, it returns -1.
The following code creates a Sample.List object, gets a predefined Caché list from the InitList() method, transforms it into a Python array, and displays information about the array:
listobj = database.create_new("Sample.List",None)
mylist = listobj.run_obj_method("InitList",[])
print "Initial List from Cache:"
print "array contents = " + str(mylist)
print "There are %d elements in the list:" % (len(mylist))
for i in range(len(mylist)):
print " element %d = [%s]" % (i,mylist[i])
This code produces the following output:
Initial List from Cache:
array contents = [1, 'hello', 3.1400000000000001]
There are 3 elements in the list:
element 0 = [1]
element 1 = [hello]
element 2 = [3.14]
In element 3, the null list element in Caché corresponds to value of None in the Python array.
The following code passes a list in both directions. It creates a small Python array, stores it in the Caché object's CurrentList property, then gets it back from the property and converts it back to a Python array.
# Generate a small Python list, pass it to Cache and get it back.
oldarray = [1, None, 2.78,"Just a small list."]
listobj.set("CurrentList",oldarray)
newarray = listobj.get("CurrentList")
print "\nThe 'CurrentList' property now has %d elements:"\
% (len(newarray))
for i in range(len(newarray)):
print " element %d = [%s]" % (i,newarray[i])
This code produces the following output:
The 'CurrentList' property now has 4 elements:
element 0 = [1]
element 1 = [None]
element 2 = [2.78]
element 3 = [Just a small list.]
It is important to make sure that a Cache %ListOpens in a new tab variable can contain the entire Python array. The following code creates a Python array that is too large, and attempts to store it in the CurrentList property.
# Create a large array and print array information.
longitem = "1022 character element" + ("1234567890" * 100)
array =["This array is too long."]
cache_list_size = len(array[0])
while (cache_list_size < 32768):
array.append(longitem)
cache_list_size = cache_list_size + len(longitem)
print "\n\nNow for a HUGE list:"
print "Total bytes required by Cache' list: more than %d" % (cache_list_size)
print "There are %d elements in the ingoing list.\n" % (len(array))
# Check to see if the array will fit.
bool = listobj.run_obj_method("TestList",[array])
print "TestList reports that "
if (bool):
print "the array is OK, and has %d elements.\n" % bool
else:
print "the array will be damaged by the conversion.\n"
# Pass the array to Cache', get it back, and display the results
listobj.set("CurrentList",array)
badarray = listobj.get("CurrentList")
print "There are %d elements in the returned array:\n" % (len(badarray))
for i in range(len(badarray)):
line = str(badarray[i])
if (len(line)> 80):
# long elements are shortened for readability.
line = line[:][:9] + "..." + line[:][-50:]
print " element %d = [%s]\n" % (i, line)
The printout shortens undamaged sections of the long elements to make the output more readable. The following output is produced on a unicode system:
Now for a HUGE list:
Total bytes in array: at least 33884
There are 34 elements in the ingoing array.
~
TestList reports that the array will be damaged by the conversion.
There are 17 elements in the returned array :
element 1 = [This list is too long.]
element 2 = [1022 chara...90123456789012345678901234567890]
element 3 = [1022 chara...90123456789012345678901234567890]
element 4 = [1022 chara...90123456789012345678901234567890]
element 5 = [1022 chara...90123456789012345678901234567890]
element 6 = [1022 chara...90123456789012345678901234567890]
element 7 = [1022 chara...90123456789012345678901234567890]
element 8 = [1022 chara...90123456789012345678901234567890]
element 9 = [1022 chara...90123456789012345678901234567890]
element 10 = [1022 chara...90123456789012345678901234567890]
element 11 = [1022 chara...90123456789012345678901234567890]
element 12 = [1022 chara...90123456789012345678901234567890]
element 13 = [1022 chara...90123456789012345678901234567890]
element 14 = [1022 chara...90123456789012345678901234567890]
element 15 = [1022 chara...90123456789012345678901234567890]
element 16 = [1022 chara...90123456789012345678901234567890]
element 17 = [1022 chara...901234567st is too long.ï´√ȇ1022 c]
The damaged list contains only 17 of the original 34 elements, and element 17 is corrupted.
Using Relationships
Relationships are supported through the relationship object and its methods.
The following example generates a report by using the one/many relationship between the company object and a set of employee objects. The relationship object emp_relationship allows the code to access all employee objects associated with the company object:
company = database.openid("Sample.Company",str(1),-1,0)
emp_relationship = company.get("Employees")
index = [None]
print "Employee list:\n"
while 1:
employee = emp_relationship.run_obj_method("GetNext",index)
# "GetNext" sets index[0] to the next valid index, or
# to None if there are no more records.
if (employee != None):
i = str(index[0])
name = str(employee.get("Name"))
title = str(employee.get("Title"))
company = employee.get("Company")
compname = str(company.get("Name"))
SSN = str(employee.get("SSN"))
print " employee #%s:" % i
print " name=%s SSN=%s" % (name, SSN)
print " title=%s companyname=%s\n"% (title, compname)
else:
break
The following code creates a new employee record, adds it to the relationship, and automatically saves the employee information when it saves company.
new_employee = database.create_new("Sample.Employee","")
new_employee.set("Name",name)
new_employee.set("Title",title)
new_employee.set("SSN",SSN)
emp_relationship.run_obj_method("Insert", new_employee)
company.run_obj_method("%Save")
Using Queries
The basic procedure for running a query is as follows:
-
Create the query object
query = intersys.pythonbind.query(database)
where database is an intersys.pythonbind.database object and query is an intersys.pythonbind.query object.
-
Prepare the query
An SQL query uses the prepare() method:
sql_code = query.prepare(sql_query)
where sql_query is a string containing the query to be executed.
A Caché class query uses the prepare_class() method:
sql_code = query.prepare_class(class_name, query_name)
-
Assign parameter values
query.set_par(idx, val)
The set_par() method assigns value val to parameter idx. The value of an existing parameter can be changed by calling set_par() again with the new value. The Query class provides several methods that return information about an existing parameter.
-
Execute the query
sql_code = query.execute()
The execute() method generates a result set using any parameters defined by calls to set_par().
-
Fetch a row of data
data_row = query.fetch([None])
Each call to the fetch() method retrieves a row of data from the result set and returns it as a list. When there is no more data to be fetched, it returns an empty list. The Query class provides several methods that return information about the columns in a row.
Here is a simple SQL query that retrieves data from Sample.Person:
# create a query sqlstring ="SELECT ID, Name, DOB, SSN \ FROM SAMPLE.PERSON \ WHERE Name %STARTSWITH ?" query = intersys.pythonbind.query(database) query.prepare(sqlstring) query.set_par(1,"A") query.execute(); # Fetch each row in the result set, and print the # name and value of each column in a row: while 1: cols = query.fetch([None]) if len(cols) == 0: break print "\nRow %s ===================" % str(cols[0]) print " %s: %s, %s: %s, %s: %s" \ % (str(query.col_name(2)), str(cols[1]), \ str(query.col_name(3)), str(cols[2]), \ str(query.col_name(4)), str(cols[3]))
For more information on the intersys.pythonbind.Query class, see Queries in the Python Client Class Reference chapter. For information about queries in Caché, see Defining and Using Class Queries in "Using Caché Objects".
Using %Binary Data
The Python binding uses the Python pack() and unpack() functions to convert data between a Caché %BinaryOpens in a new tab and a Python list of integers. Each byte of the Caché binary data is represented in Python as an integer between 0 and 255.
In this example, the binary initially contains the string "hello". The following code changes the binary to "hellos":
binary = reg.get("MyByte")
for c in binary:
print "%c" % c
print type(ord("s"))
binary.append(ord("s"))
reg.set("MyByte",binary)
binary = reg.get("MyByte")
for c in binary:
print "%c" % c
Handling Exceptions
The Python binding uses Python exceptions to return errors from the C binding and elsewhere. Here is an example using Python exceptions:
try:
#some code
except intersys.pythonbind.cache_exception, err:
print "InterSystems Cache' exception"
print sys.exc_type
print sys.exc_value
print sys.exc_traceback
print str(err)
Error reporting
When processing an argument or a return value, error messages from the C binding are specially formatted by the Python binding layer. This information can be used by InterSystems WRC to help diagnose the problem.
Here is a sample error message:
file=PythonBIND.xs line=71 err=-1 message=cbind_variant_set_buf()
cpp_type=4 var.cpp_type=-1 var.obj.oref=1784835886
class_name=%Library.RelationshipObject mtd_name=GetNext argnum=0
The error message components are:
message |
meaning |
---|---|
file=PythonBIND.xs |
file where the failure occurred. |
line=71 |
line number in the file. |
err=-1 |
return code from the C binding. |
message= cbind_variant_set_buf() |
C binding error message. |
cpp_type=4 |
cpp type of the method argument or return type. |
var.cpp_type=-1 |
variant cpp type. |
var.obj.oref=1784835886 |
variant oref. |
class_name= %Library.RelationshipObject |
class name of the object on which the method is invoked. |
mtd_name=GetNext |
method name. |
argnum=0 |
argument number. 0 is the first argument and -1 indicates a return value. |