Forum Archive

Correct way to call Pythonista script from within a Shortcuts workflow?

felciano

Hi —

I’m trying to call Pythonista for the first time from a Shortcuts workflow. I understand that I should be able to call the Python code, passing values into argv somehow, but that there isn’t a direct way to return values unless you use a workaround (e.g. store the output value in the clipboard from Python, then retrieve the clipboard when the shortcut picks up again).

I’m having trouble getting the Shortcut to actually find my Pythonista script. I’ve tried saving it both on iCloud and “on my iPad”.

I’m triggering Pythonista by adding the “Run Script” step to my workflow, and then providing the name of the script. I’ve trying various syntaxes:

  • MyScript.py
  • MyScript
  • pythonista3://getPTEimg.py
  • pythonista3://iCloud/getPTEimg.py
  • pythonista3://iCloud/getPTEimg.py?action=run&root=iCloud
  • pythonista3://iCloud/getPTEimg.py?action=run

The last one is the one that Pythonista seems to recommend, in that it is auto-generated when I select Wrench > Shortcuts... > Pythonista URL > Copy URL.

In all cases, the call fails a "The file [filename] cannot be found" dialog box in Pythonista.

Can someone confirm that I should, in fact, be using the "Run Script" command in Shortcuts for this, as well as the syntax to use and location the script needs to be at in order to be found?

Thanks in advance!

Ramon

7upser

This works for me, the script is in This iPad/dir:

tag

The iCloud Path should be:
/private/var/mobile/Library/Mobile Documents/iCloud~com~omz-software~Pythonista3/Documents

but i dont use parameter

Edit:
Did a test for parameter with url scheme:
see here: Pythonista Url scheme

tag

works too

felciano

Thanks @7upser -- very helpful. I've been able to reproduce your example with the Pythonista Url scheme and the script is now found and launched correctly. However it jumps into/drops me into Pythonista proper. Is there a way to avoid that (i.e. just run the whole script in the background)?

cvp

@felciano said:

just run the whole script in the background

I don't think that Pythonista can run in the background.
Apple only authorizes that for music or Bluetooth apps.

7upser

You can Start a Shortcut with Pythonista:

import webbrowser
vUrl = 'shortcuts://run-shortcut?name=testStartWithUrlScheme&input=10'
webbrowser.get('safari').open(vUrl)

Maybe you can split your Shortcut into 2 different Shortcuts.
This should work, if you use Url Scheme on both sides.
(it's not really in the Bachground, as cvp mentioned)

mikeno

Hi everybody, I could call a Pythonista script from shortcuts using url and safari but it works only if the iPad is awake, is there a way to let it work when the iPad is asleep?
I also tried running a Pythonista script using scheduler but it works only if I stay in the Pythonista app.
What I want to achieve is writing some info into a file every hour

cvp

@mikeno I don't know what you want to do while your IPad is asleep but Shortcuts offers automations. Perhaps that could help.

mikeno

Yes but only everyday, I want to read the value of the barometer sensor every hour, I can get this value with Pythonista. I didn’t find a way to get this information in shortcuts nor in JS (scriptable).
I found also a way to do some tasks every hour in shortcuts but I don’t find a way to get the barometer sensor value.

cvp

@mikeno said

I found also a way to do some tasks every hour in shortcuts

But you could start a Pythonista script every hour?

mikeno

Yes, it works but only if the iPad is awake

cvp

@mikeno did you try to run an automation at specified time, which runs a Pythonista short script logging the pressure in a file and put your IPad asleep some time before the specified time?

mikeno

@cvp I’m not sure to understand what you mean, but calling a pythonista script from a shortcuts or an automation requires the iPad to be awake

cvp

@mikeno ok, I thought that calling the script from the shortcut did launch Pythonista even if iPad not awake. If you have Pyto, you could test because it runs really in background like a music player

mikeno

I don’t know PyTo but I will try, thx in any case

cvp

@mikeno not Py To but Pyto, I think there is a free test version

mikeno

I just downloaded it but my trial period already expired because I probably already tried it some times ago and since I don’t know if it works, I don’t want to buy it. If you’ve it, could you try if it runs when iPad is asleep?

cvp

@mikeno I'll do it and let it know

cvp

@mikeno I have tried a script which prints the time each second and closed my iPad cover during 200 seconds and when I have reopened it, the script was still running

mikeno

Thx, the question is now if Pyto can read the barometer sensor value, below a short code which runs fine under Pythonista:

from objc_util import ObjCInstance, ObjCClass, ObjCBlock, c_void_p

pressure = None

def get_pressure():

  def handler(_cmd, _data, _error):
    global pressure
    pressure = ObjCInstance(_data).pressure()

  handler_block = ObjCBlock(handler, restype=None, argtypes=[c_void_p, c_void_p, c_void_p])

  CMAltimeter = ObjCClass('CMAltimeter')
  NSOperationQueue = ObjCClass('NSOperationQueue')
  if not CMAltimeter.isRelativeAltitudeAvailable():
    print('This device has no barometer.')
    return
  altimeter = CMAltimeter.new()
  main_q = NSOperationQueue.mainQueue()
  altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(main_q, handler_block)
  try:
    while pressure is None:
      pass
  finally:
    altimeter.stopRelativeAltitudeUpdates()
    #print('Updates stopped.')
    return pressure.floatValue()*10

pressure = get_pressure()
print(pressure)
cvp

@mikeno I know this code but I'm new in Pyto and surely not (yet?) a specialist in ObjectiveC of Pyto.
I don't not yet know how to define an ObjcBlock in Pyto but I'll try.
But, obviously, I'll need some time

cvp

@mikeno Sorry, no idea how to define an ObjcBlock in rubicon (ObjectiveC in Pyto).
Hoping that @JonB will read this and be able to help, as usual.

# coding: utf-8
from rubicon.objc import *
from  ctypes import *

def handler(_cmd, _data, _error):
    print(ObjCInstance(_data))

handler_block = ObjCBlock(handler, None, [c_void_p, c_void_p, c_void_p])

def main():
    CMAltimeter = ObjCClass('CMAltimeter')
    NSOperationQueue = ObjCClass('NSOperationQueue')
    if not CMAltimeter.isRelativeAltitudeAvailable():
        print('This device has no barometer.')
        return
    altimeter = CMAltimeter.new()
    main_q = NSOperationQueue.mainQueue
    altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(main_q, handler_block)
    print('Started altitude updates.')
    try:
        while True:
            pass
    finally:
        altimeter.stopRelativeAltitudeUpdates()
        print('Updates stopped.')

if __name__ == '__main__':
    main()

Gives

Traceback (most recent call last):
  File "iCloud/barometer.py", line 8, in <module>
    handler_block = ObjCBlock(handler, None, [c_void_p, c_void_p, c_void_p])
  File "Pyto.app/Lib/rubicon/objc/api.py", line 1834, in __init__
    self.struct = cast(self.pointer, POINTER(ObjCBlockStruct))
  File "Pyto.app/site-packages/python3.10/ctypes/__init__.py", line 510, in cast
    return _cast(obj, obj, typ)
ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type
mikeno

Thx for trying, I’ll wait!

JonB

@cvp I believe in Rubicon, the preferred usage is via type annotations and decorators. Also ObjCBlock wraps ObjC blocks so they can be called in python, while Block wraps python so it is calls me in objc-- so you want plain old Block.

I think the way you'd do it in Rubicon is:

(Edited)

@Block
def handler(altitudeData: ObjCInstance, err:NSError) -> None:
    print(altitudeData)

Or, I think you can skip the annotation on ObjCInstances:

@Block
def handler(altitudeData, err:NSError) -> None:
    print(altitudeData)
cvp

@JonB problems
1)

@Block
def handler(altitudeData , err:NSError) -> None:
    print(altitudeData)

handler_block = ObjCBlock(handler, None, [c_void_p, c_void_p])

Gives

Traceback (most recent call last):
  File "iCloud/barometer.py", line 6, in <module>
    def handler(altitudeData, err:NSError) -> None:
NameError: name 'NSError' is not defined. Did you mean 'OSError'?

2)

@Block
def handler(altitudeData, err) -> None:
    print(altitudeData)

handler_block = ObjCBlock(handler, None, [c_void_p, c_void_p])

Gives

Traceback (most recent call last):
  File "iCloud/barometer.py", line 6, in <module>
    def handler(altitudeData, err) -> None:
  File "Pyto.app/Lib/rubicon/objc/api.py", line 1939, in __init__
    raise ValueError(
ValueError: Function has no argument type annotation for parameter 'altitudeData' - please add one, or
 pass return and argument types directly into Block

3)

```
@Block
def handler(altitudeData:ObjCInstance, err:ObjCInstance) -> None:
print(altitudeData)

handler_block = ObjCBlock(handler, None, [c_void_p, c_void_p])

Gives

Traceback (most recent call last):
File "iCloud/barometer.py", line 9, in
handler_block = ObjCBlock(handler, None, [c_void_p, c_void_p])
File "Pyto.app/Lib/rubicon/objc/api.py", line 1846, in init
self.struct.contents.invoke.argtypes = (objc_id, ) + tuple(ctype_for_type(arg_type) for arg_type i
n argtypes)
File "Pyto.app/Lib/rubicon/objc/api.py", line 1846, in
self.struct.contents.invoke.argtypes = (objc_id, ) + tuple(ctype_for_type(arg_type) for arg_type i
n argtypes)
File "Pyto.app/Lib/rubicon/objc/types.py", line 103, in ctype_for_type
return _ctype_for_type_map.get(tp, tp)
TypeError: unhashable type: 'list'


4)

@Block
def handler(altitudeData:ObjCInstance, err:ObjCInstance) :#-> None:
print(altitudeData)

handler_block = ObjCBlock(handler, None)#, [c_void_p, c_void_p])

Gives

Traceback (most recent call last):
File "iCloud/barometer.py", line 6, in
def handler(altitudeData:ObjCInstance, err:ObjCInstance) :#-> None:
File "Pyto.app/Lib/rubicon/objc/api.py", line 1930, in init
raise ValueError(
ValueError: Function has no return type annotation - please add one, or pass return and argument types
directly into Block
Traceback (most recent call last):
File "iCloud/barometer.py", line 6, in
def handler(altitudeData:ObjCInstance, err:ObjCInstance) :#-> None:
File "Pyto.app/Lib/rubicon/objc/api.py", line 1930, in init
raise ValueError(
ValueError: Function has no return type annotation - please add one, or pass return and argument types
directly into Block


5)

@Block
def handler(altitudeData:ObjCInstance, err:ObjCInstance) -> None:
print(altitudeData)

handler_block = ObjCBlock(handler, None, (c_void_p, c_void_p))

Gives

Traceback (most recent call last):
File "iCloud/barometer.py", line 10, in
handler_block = ObjCBlock(handler, None, (c_void_p, c_void_p))
File "Pyto.app/Lib/rubicon/objc/api.py", line 1846, in init
self.struct.contents.invoke.argtypes = (objc_id, ) + tuple(ctype_for_type(arg_type) for arg_type i
n argtypes)
TypeError: item 2 in argtypes has no from_param method
```

bosco

@mikeno This code works for me with the latest version of pyto.
```

coding: utf-8

from rubicon.objc import Block, ObjCClass, ObjCInstance, py_from_ns
from rubicon.objc.runtime import objc_id

pressure = None

def handler(_data) -> None:
nspressure = ObjCInstance(_data).pressure
global pressure
pressure = py_from_ns(nspressure)

handler_block = Block(handler, None, (objc_id))

def get_pressure():
CMAltimeter = ObjCClass('CMAltimeter')
NSOperationQueue = ObjCClass('NSOperationQueue')
if not CMAltimeter.isRelativeAltitudeAvailable():
print('This device has no barometer.')
return
altimeter = CMAltimeter.new()
main_q = NSOperationQueue.mainQueue
altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(main_q, handler_block)
print('Started altitude updates.')
try:
while pressure is None:
pass
finally:
altimeter.stopRelativeAltitudeUpdates()
print('Updates stopped.')
return pressure

if name == 'main':
result = get_pressure()
print(result)
del pressure

cvp

@bosco Thanks for him, and for me, so I don't have to test anymore.
Do you know why the handler does not have a 2nd parameter (error) like described in Apple doc?

JonB

@cvp, I think this one was correct:

@Block
def handler(altitudeData:ObjCInstance, err:ObjCInstance) -> None:
   print(altitudeData)

But then pass handler directly to ObjC -- don't call ObjCBlock on it. ObjCBlock makes an Objc block callable by python, which isn't needed here.

Or, to make no other changes:

@Block
def handler_block(altitudeData:ObjCInstance, err:ObjCInstance) -> None:
   print(altitudeData)
cvp

@mikeno With the barometer module of @bosco, where print lines are commented, this script works and continues to log the pressure even if I close the iPad cover. Not tested during a day.

import background as bg
import barometer

with bg.BackgroundTask() as b:
  while True:
    result = barometer.get_pressure()
    l= f"{b.execution_time()}:{result}\n"
    with open("/private/var/mobile/Library/Mobile Documents/iCloud~is~workflow~my~workflows/Documents/bg.txt", mode='at') as fil:
      fil.write(l)
    #print(b.execution_time(), result)
    b.wait(5)
mikeno

Hi everybody, thx for helping, I’ll buy Pyto and try.

cvp

@JonB said

But then pass handler directly to ObjC

how do I do that?

In the bosco solution, there is no @Block line

Édit: ok, understood, use the handler_block directly in

    altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(main_q, handler)

Thanks for your explanations

Edit2: but I need

@Block
def handler(altitudeData:ObjCInstance, err:ObjCInstance) -> None:
   print(ObjCInstance(altitudeData).pressure)
bosco

I dropped the 2nd parameter (error) because it caused an exception: "item 2 in argtypes has no from_param method", so I tried running without the error parameter.

After reading the last comment by @JonB I now understand the proper use of @Block witch can be called directly.

This works for me.

```

coding: utf-8

from rubicon.objc import Block, ObjCClass, ObjCInstance, py_from_ns
from rubicon.objc.runtime import objc_id

pressure = None

@Block
def handler(altitudeData:ObjCInstance, err:ObjCInstance) -> None:
nspressure = ObjCInstance(altitudeData).pressure
global pressure
pressure = py_from_ns(nspressure)

"""
def bhandler(_data) -> None:
nspressure = ObjCInstance(_data).pressure
global pressure
pressure = py_from_ns(nspressure)

handler_block = Block(bhandler, None, (objc_id))
"""

def get_pressure():
CMAltimeter = ObjCClass('CMAltimeter')
NSOperationQueue = ObjCClass('NSOperationQueue')
if not CMAltimeter.isRelativeAltitudeAvailable():
print('This device has no barometer.')
return
altimeter = CMAltimeter.new()
main_q = NSOperationQueue.mainQueue
#altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(main_q, handler_block)
altimeter.startRelativeAltitudeUpdatesToQueue_withHandler_(main_q, handler)
print('Started altitude updates.')
try:
while pressure is None:
pass
finally:
altimeter.stopRelativeAltitudeUpdates()
print('Updates stopped.')
return pressure

if name == 'main':
result = get_pressure()
print(result)
del pressure

cvp

@bosco said

After reading the last comment by @JonB I now understand the proper use of @Block witch can be called directly.

Yes, this is what I had also found and explained in my last post

bosco

@cvp Correct. After my morning brain fog cleared, that is what you said in your last edit. :-)

cvp

@bosco this, you bought Pyto and already tried...

bosco

@cvp I bought Pyto in 2019. I am currently running Pyto 17.1.1 I have tested barometer.py on iPhone 12 mini and iPad mini 4th gen.

cvp

@bosco I'm sincerely sorry, my "thus, you bought Pyto and already tried..." was erroneously for @mikeno. I guess that you, @bosco, you know Pyto because you have been part of the solution with @jonB.

bosco

@cvp No problem. I thought maybe your question was intended for @mikeno.