:%KWArgsToString", args)
219 | }
220 | }
221 |
222 | if (($listvalid(args)) && ($ll(args)>0)) {
223 | set argsComputed = ""
224 | for i=1:1:$ll(args) {
225 | set arg = $lg(args, i)
226 | set argsComputed = argsComputed _ $lb($lg(arg) _ "=" _ ..%EscapeArg($lg(arg,2), ..#EscapeOnCall))
227 | }
228 | set result = $lts(argsComputed)
229 | } else {
230 | // We don't want to escape singular key=value expression
231 | set result = args
232 | }
233 | quit result
234 | }
235 |
236 | /// Convert args... to string
237 | /// w ##class(isc.py.gw.DynamicObject).%CallArgsToString()
238 | ClassMethod %CallArgsToString(args...) As %String
239 | {
240 | quit:'$d(args) ""
241 |
242 | #dim firstArg As %Boolean = $$$YES
243 |
244 | set result = ""
245 | for i=1:1:args {
246 | if $d(args(i)) {
247 | set value = ..%EscapeArg(args(i))
248 | } else {
249 | set value = ""
250 | }
251 |
252 | set result = result _ $case(firstArg, $$$YES:"", :", ") _ value
253 | set firstArg = $$$NO
254 | }
255 |
256 | quit result
257 | }
258 |
259 | /// Escape one argumet if needed
260 | /// w ##class(isc.py.gw.DynamicObject).%EscapeArg()
261 | ClassMethod %EscapeArg(arg, escapeString As %Boolean = {..#EscapeOnCall}) As %String
262 | {
263 | quit:'$d(arg) ""
264 |
265 | if ($isObject(arg) && arg.%Extends("isc.py.gw.DynamicObject")) {
266 | set value = arg.%Variable
267 | } elseif escapeString {
268 | set value = ##class(isc.py.util.Converter).EscapeString(arg)
269 | } else {
270 | set value = arg
271 | }
272 |
273 | quit value
274 | }
275 |
276 | /// Escape one keyword argumet if needed
277 | /// w ##class(isc.py.gw.DynamicObject).%EscapeArg()
278 | ClassMethod %EscapeKWArg(key As %String, arg, escapeString As %Boolean = {..#EscapeOnCall}) As %String
279 | {
280 | quit:'$d(arg) ""
281 | set value = ..%EscapeArg(arg, escapeString)
282 | quit key _ "=" _ value
283 | }
284 |
285 | /// Get unused Python variable
286 | /// w ##class(isc.py.gw.DynamicObject).%GetNewVar()
287 | ClassMethod %GetNewVar() [ CodeMode = expression ]
288 | {
289 | "variable" _ $random(100000000000000000)
290 | }
291 |
292 | /// Convert python type to InterSystems IRIS class
293 | /// w ##class(isc.py.gw.DynamicObject).%TypeToClass
294 | ClassMethod %TypeToClass(type) As %String
295 | {
296 | set basePackage = $p($classname(), ".", 1)
297 | set class = basePackage _ "." _ $case($l(type,"."), 1:"builtins.", :"") _ type
298 |
299 | if '##class(%Dictionary.CompiledClass).%ExistsId(class) {
300 | set class = $classname()
301 | }
302 |
303 | quit class
304 | }
305 |
306 | /// This callback method is invoked by the %Close method to
307 | /// provide notification that the current object is being closed.
308 | ///
309 | /// The return value of this method is ignored.
310 | Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
311 | {
312 | do:..%DeleteOnClose ..%Delete()
313 | quit $$$OK
314 | }
315 |
316 | /// Delete Python variable
317 | Method %Delete() As %Status [ CodeMode = expression ]
318 | {
319 | ##class(isc.py.Main).SimpleString("del " _ ..%Variable)
320 | }
321 |
322 | /// Rename variable on Python side
323 | Method %Rename(variable As %String) As %Status
324 | {
325 | set sc = ##class(isc.py.Main).SimpleString(variable _ " = " _ ..%Variable)
326 | quit:$$$ISERR(sc) sc
327 | set sc = ..%Delete()
328 | quit:$$$ISERR(sc) sc
329 | set ..%Variable = variable
330 |
331 | quit sc
332 | }
333 |
334 | }
335 |
336 |
--------------------------------------------------------------------------------
/Gateway.md:
--------------------------------------------------------------------------------
1 | # Gateway functionality
2 |
3 | This document describes functionality pertaining to what is commonly understood as (.Net/Java) Gateway.
4 | There are three main parts:
5 | - Execute Function
6 | - Proxyless Gateway
7 | - Proxy Gateway
8 |
9 | # Execute function
10 |
11 | Executes function by name. This API consists of two methods:
12 | - `ExecuteFunction`
13 | - `ExecuteFunctionArgs`
14 |
15 | The difference between them is caller signature. `ExecuteFunction` accepts %List, %Collection.AbstractArray and JSON object separated into positional and keyword arguments. `ExecuteFunctionArgs` accepts `args...` and parses them into positional and keyword arguments. After that `ExecuteFunctionArgs` calls `ExecuteFunction`.
16 |
17 | It is caller responsibility to escape argument values. Use `isc.py.util.Converter` class to escape:
18 | - string
19 | - boolean
20 | - date
21 | - time
22 | - timestamp
23 |
24 | ## ExecuteFunction
25 |
26 | `ExecuteFunction` method from `isc.py.Main` class. Signature:
27 | - `function` - name of function to invoke. Can be nested, i.e. `random.randint`
28 | - `variable` - name of python variable to write result to.
29 | - `positionalArguments` - positional arguments for Python function. Can be one of:
30 | + `$lb(val1, val2, ..., valN)`
31 | + `%Collection.AbstractIterator` object
32 | + JSON array
33 | - `keywordArguments` - keyword arguments for Python function. Can be one of:
34 | + `$lb($lb(name1, val1), $lb(name2, val2), ..., $lb(nameN, valN))`
35 | + `%Collection.AbstractArray` object
36 | + flat JSON object
37 | - `serialization` - how to serialize result
38 | - `result` - write result into this variable
39 |
40 | All arguments besides `function` are optional.
41 |
42 | Here's an example of how it works:
43 |
44 | ```
45 | set sc = ##class(isc.py.Main).ImportModule("random", ,.random)
46 |
47 | set posList = $lb(1, 100)
48 | set posCollection = ##class(%ListOfDataTypes).%New()
49 | do posCollection.Insert(1)
50 | do posCollection.Insert(100)
51 | set posDynamic = [1, 100]
52 |
53 | for positionalArguments = posList,posCollection,posDynamic {
54 | set sc = ##class(isc.py.Main).ExecuteFunction(random _ ".randint", positionalArguments,,,.result)
55 | write result,!
56 | }
57 |
58 | set kwList = $lb($lb("a", 1), $lb("b", 100))
59 | set kwCollection = ##class(%ArrayOfDataTypes).%New()
60 | do kwCollection.SetAt(1, "a")
61 | do kwCollection.SetAt(100, "b")
62 | set kwDynamic = { "a": 1, "b": 100}
63 |
64 | for kwArguments = kwList,kwCollection,kwDynamic {
65 | set sc = ##class(isc.py.Main).ExecuteFunction(random _ ".randint", ,kwArguments,,.result)
66 | write result,!
67 | }
68 |
69 | set posList = $lb(1)
70 | set kwDynamic = {"b": 100}
71 | set sc = ##class(isc.py.Main).ExecuteFunction(random _ ".randint", posList, kwDynamic,,.result)
72 | write result,!
73 |
74 | set posList = ##class(isc.py.util.Converter).EscapeStringList($lb("Positional: {0} {1}! Keyword: {name}, {name2}", "Hello", "World"))
75 | set kwDynamic = {"name":(##class(isc.py.util.Converter).EscapeString("Alice")),
76 | "name2":(##class(isc.py.util.Converter).EscapeString("Bob"))}
77 | set sc = ##class(isc.py.Main).ExecuteFunction("str.format", posList, kwDynamic,,.result)
78 | write result,!
79 | ```
80 |
81 | ## ExecuteFunctionArgs
82 |
83 | `ExecuteFunctionArgs` method from `isc.py.Main` class. Signature:
84 | - `function` - name of function to invoke. Can be nested, i.e. `random.randint`
85 | - `variable` - name of python variable to write result to.
86 | - `serialization` - how to serialize result
87 | - `result` - write result into this variable
88 | - `args...` - function arguments.
89 |
90 | `ExecuteFunctionArgs` attempts to determine correct positional and keyword arguments from function signature (if available). It is recommended to call `ExecuteFunction` directly if `ExecuteFunctionArgs` is unable to construct a correct argument spec (and open an issue). Example:
91 |
92 | ```
93 | set sc = ##class(isc.py.Main).ImportModule("random", ,.random)
94 | set sc = ##class(isc.py.Main).ExecuteFunctionArgs(random _ ".randint", , ,.result, 1, 100)
95 | write result,!
96 |
97 | set string = ##class(isc.py.util.Converter).EscapeString("Positional: {0}, {1}, {2}, {3}")
98 | set arg1 = ##class(isc.py.util.Converter).EscapeString("Hello")
99 | set arg2 = ##class(isc.py.util.Converter).EscapeString("World")
100 | set arg3 = ##class(isc.py.util.Converter).EscapeString("Alice")
101 | set arg4 = ##class(isc.py.util.Converter).EscapeString("Bob")
102 | set sc = ##class(isc.py.Main).ExecuteFunctionArgs("str.format",,,.result, string, arg1, arg2, arg3, arg4)
103 | write result,!
104 |
105 | set string = ##class(isc.py.util.Converter).EscapeString("Positional: {0} {1}! Keyword: {name}, {name2}")
106 | set arg1 = ##class(isc.py.util.Converter).EscapeString("Hello")
107 | set arg2 = ##class(isc.py.util.Converter).EscapeString("World")
108 | set kwargs = "**" _ {"name":"Alice","name2":"Bob"}.%ToJSON()
109 | set sc = ##class(isc.py.Main).ExecuteFunctionArgs("str.format",,, .result, string, arg1, arg2, kwargs)
110 | write result,!
111 | ```
112 |
113 | # Proxyless Gateway
114 |
115 | Proxyless gateway allows user to bind Python variables to InterSystems IRIS variables.
116 | This allows user to:
117 | - Get/Set object properties
118 | - Call object methods
119 | - Serialize variable to: Str, Repr, Pickle, Dill, JSON, Dynamic Object.
120 |
121 | Example.
122 |
123 | 1. Load Python class `Person`: `do ##class(isc.py.init.Test).Initialize(,1)`
124 |
125 | Note: here's `Person` class definition for reference:
126 | ```
127 | class Person(object):
128 | def __init__(self, name, age, city):
129 | self.name = name
130 | self.age = age
131 | self.city = city
132 | def getAge(self):
133 | return self.age
134 | def getAgePlus(self, add):
135 | return self.age + add
136 | ```
137 |
138 | 2. Create Proxy variable: `set obj = ##class(isc.py.gw.DynamicObject).%New("Person", "p1", "'Ed'", "25", "'Test'")`
139 | In this call we create Python variable `p1` of `Person` class and pass three methods to constructor `'Ed'`, `25` and `'Test'`.
140 |
141 | 3. Now we can interact with the object, let's get and set some properties:
142 | ```
143 | write obj.name
144 | set obj.name="Bob"
145 | write obj.name
146 | write obj.age
147 | ```
148 | 4. We can set some new properties too (unlike `ExecuteFunction` values are escaped automatically if `%EscapeOnSet` property is 1, which is default. You can also set properties to other dynamic objects. In that case unescaped python variable name would be used):
149 | ```
150 | set obj.pet = "Dog"
151 | write obj.pet
152 | ```
153 |
154 | 5. And we can call object methods:
155 |
156 | ```
157 | write obj.getAge()
158 | write obj.getAgePlus(10)
159 | ```
160 |
161 | 6. Finally we can convert object:
162 | ```
163 | set sc = obj.%ToJSON(.json)
164 | set sc = obj.%ToDynObj(.dynObj)
165 | set sc = obj.%ToPickle(.pickle)
166 | set sc = obj.%ToStream(.stream)
167 | ```
168 |
169 | To create proxy object from existing proxy object just skip type argument:
170 | ```
171 | kill obj
172 | set p1 = ##class(isc.py.gw.DynamicObject).%New(, "p1")
173 | ```
174 |
175 | Module objects can be proxied this way too:
176 |
177 | ```
178 | set module = "random"
179 | set sc = ##class(isc.py.Main).ImportModule(module)
180 | set random = ##class(isc.py.gw.DynamicObject).%New(,module)
181 | write random.randint(1,100)
182 | ```
183 |
184 | Now for a more complex example. In case of primitives (int, bool, str, float) proxy object returns a serialized value. Otherwise (if method call or variable get returns complex type) it returns another proxy object pointing to that result.
185 |
186 | ```
187 | set sc = ##class(isc.py.Main).ImportModule("numpy",,"np")
188 | set np = ##class(isc.py.gw.DynamicObject).%New(,"np")
189 | set arr = ##class(isc.py.gw.DynamicObject).%New("np.array", "arr","[[1.5,2],[4,5]]")
190 | set exp = np.exp(arr)
191 | write $replace(exp.%GetString(),$c(10), $c(13,10))
192 | ```
193 |
194 | And here's an example of setting property to proxy object:
195 |
196 | ```
197 | do ##class(isc.py.init.Test).Initialize(,1)
198 | set obj = ##class(isc.py.gw.DynamicObject).%New("Person", "p1", "'Ed'", "25", "'Test'")
199 | set obj2 = ##class(isc.py.gw.DynamicObject).%New("Person", "p2", "'Bob'", "22", "'Test2'")
200 | write obj.%GetJSON()
201 |
202 | set obj.relative = obj2
203 | set obj3 = obj.relative
204 | write obj3.%GetJSON()
205 | ```
206 |
207 | You can use `%EscapeOnSet` and `%EscapeOnCall` properties and `%IsPrimitive` method to affect default serialization behaviour.
208 |
209 | # Proxy Gateway
210 |
211 | Create proxy ObjectScript classes.
212 |
213 | ## Generation
214 |
215 | Fastest way to generate classes is to run this command:
216 | ```
217 | set modules = $lb("module1", ..., "moduleN") // or string in case there's only one module
218 | set sc = ##class(isc.py.gw.Generator).Generate(modules)
219 | ```
220 |
221 | It generates classes with all defaults. If you want more control you can instantiate generator object and finetune it's behavior by setting these properties:
222 |
223 | - `LogLevel`- Display logging info. Increasing the number outputs more messages. Does not affect generation.
224 | - `Package` - Base package for generated classes. Defaults to `py`. Defaults to 1, maximum is 3.
225 | - `BaseClass` - Base class for all python classes. Defaults to `isc.py.gw.DynamicObject`.
226 | - `FixClass` - Class that fixes Method/Classmethod and Signatures. Defaults to `isc.py.util.Generator`. Must implement interfaces `GetArguments(type, method, ByRef arguments)` and `IsClassMethod(type, method) As %Boolean`.
227 | - `*Mask` - White/Black masks for classes, methods and properties.
228 |
229 | 1. Create generator object: `set generator = ##class(isc.py.gw.Generator).%New()`
230 | 2. Set `generator` object properties.
231 | 3. Call `GenerateModule` method to generate classes for one module (and all related classes).
232 | 4. After all modules were generated, call `Compile` method.
233 |
234 | ## Notes
235 |
236 | 1. It's extremely recommended to do class generation from a clean process (without imported modules).
237 | 2. Module generator does not support module aliases. Use canonical names only.
238 | 3. After generation it is user's responsibility to load modules before calling corresponding methods.
239 | 4. There's no distinction between class methods and instance methods in Python. That's why there's a number of heuristics implemented to determine the correct case. You can use Fixer class to deal with corner cases.
240 | 5. If there were compile errors, `Compile` method would try to produce fixes for some of them. Add them to your `FixClass` or open an issue.
241 | 6. All packages should be generated using the same `Package`.
242 | 7. `isc.py.util.Generator` contains a list of Modules import of which on default settings should succeed.
243 |
244 | ## Use
245 |
246 | 1. Let's generate `random` module:
247 | ```
248 | set module = "random"
249 | set sc = ##class(isc.py.gw.Generator).Generate(module)
250 | ```
251 | 2. Now let's import the module: `set sc = ##class(isc.py.Main).ImportModule(module)`
252 | 3. We immediately can call functions of `random` module: `write ##class(py.random).randint(1,100)`
253 | 4. Let's init a `random.Random` object: `set rr = ##class(py.random.Random).%New()`
254 | 5. And call instance method: `write rr.betavariate(1, 3)`
255 |
256 | ## Calling conventions
257 |
258 | - Methods documentation is imported too - refer to it to know how to call the methods.
259 | - To pass positional arguments or keyword arguments just pass their value.
260 | - All arguments accept Python proxy objects and Python dynamic objects. `%Variable` is passed to function.
261 | - It is caller responsibility to escape the values.
262 | - To pass *args pass either %List or Dynamic Array.
263 | - To pass **kwargs pass either: %List of %List(key, value) or Flat dynamic object.
264 |
265 | ## Calling examples
266 |
267 | Consider this function:
268 |
269 | ```
270 | def allargs(parg, *args, name=1, **kwargs):
271 | print('first positional arg:', parg)
272 | for arg in args:
273 | print('arg from *argv:', arg)
274 | print('first keyword arg name:', name)
275 | for key, value in kwargs.items():
276 | print('kwarg: {0} = {1}'.format(key, value))
277 | ```
278 |
279 | It is a part of the `ed` module. As you can see it accepts all types of arguments.
280 |
281 | 1. Install the module: `pip install ed`.
282 | 2. Generate classes: `set sc = ##class(isc.py.gw.Generator).Generate("ed")`
283 | 3. Import the module: `set sc = ##class(isc.py.Main).ImportModule("ed")`
284 | 4. As `allargs` function just prints to stdout and does not return anything we need to redirect stdout:
285 | ```
286 | set redirect = ##class(isc.py.util.Redirect).%New()
287 | do redirect.Init()
288 | do redirect.Enable(1)
289 | ```
290 | 5. Here are different ways of calling `ed.allargs` function:
291 | ```
292 | set parg = 1
293 | set parg = ##class(isc.py.gw.DynamicObject).%New("int",, 1)
294 |
295 | set args = 3
296 | set args = $lb(2, 3)
297 | set args = [2, 3]
298 |
299 | set kwarg = 4
300 | set kwarg = ##class(isc.py.gw.DynamicObject).%New("int",, 4)
301 |
302 | set kwargs = { "a":5, "b":6 }
303 | set kwargs = $lb($lb("a", 5), $lb("b", 6))
304 |
305 | set sc = ##class(py.ed).allargs(parg, args, kwarg, kwargs)
306 |
307 | zwrite sc
308 | write redirect.GetString()
309 | ```
--------------------------------------------------------------------------------