Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
[Share Code] Implemented x-callback-url
-
This morning I asked if it is possible to use x-callback-urls with Pythonista and access data other apps provide.
I did some experimentation and came up with a script that is working perfectly.
What it does:
- Add support for x-callback.urls to Pythonista
- Replace the default
UIApplication -openURL:sourceApplication:annotation:
method with a custom one (Using @JonB's swizzle.py) - The custom
-openURL:
method checks if the url that is opened is the callback from the other app - If the url is from the other app, a handler function that you provide will be called, with some information about the url as a parameter
- Passed information includes:
- The full url called
- The app that opened the callback-url
- A dictionary containing all parameters and their values from the url (This is where you get data you requested from another app)
- If the url is NOT in response to the x-callback-url, Pythonistas default
-openURL:
will be called, with all parameters passed to the swizzled one. This ensures that other scripts using thepythonista://
url scheme to open/launch files still work.
How to use it:
(Using Drafts as an example, works with any app that supports x-callback-urls)import x_callback_url url = 'drafts4://x-callback-url/get?uuid=YOUR_UUID&x-success=pythonista://' def handler(url_info): print(info) # Do something with the data passed back to Pythonista (available through url_info.parameters) x_callback_url.open_url(url, handler)
Notes:
- You'll also need swizzle.py
- If you don't want to do anything with data passed back to Pythonista, you can omit the handler parameter and just pass the url to
x_callback_url.open_url()
function - This currently supports only the
x-success
parameter, I'll add support forx-source
andx-error
at a later point (x-success
is by far the most important one) - If you have any questions about this, just ask!
Where you can get this awesome piece of code:
- Just download it from GitHub
I have to say that this is the Pythonista script I'm by far the most proud of. Thanks @omz for making this great app!
-
Nice.
I will point out that, at least for drafts, I believe you could have used [[draft]], or [[title]] or [[body]] in the callback url in order to pass data to pythonistas argv:
pythonista://myscript?action=run&argv=[[draft]]
your solution does seem more general for a variety of other apps. kudos!
-
This looks awesome! Unfortunately I'm not able to get it working! Would you mind taking a look at this example and telling me what's going on?
# coding: utf-8 import x_callback_url url = "working-copy://x-callback-url/status/?repo=MY_REPO&unchanged=1&key=MY_KEY&x-success=pythonista://" def handler(response): print(response) x_callback_url.open_url(url, handler)
This results in the following output in the console:
Traceback (most recent call last): File "_ctypes/callbacks.c", line 314, in 'calling callback function' File "/private/var/mobile/Containers/Shared/AppGroup/84B2FC5A-8F6A-4B20-BA21-BE5B5A07629F/Documents/site-packages/x_callback_url.py", line 32, in application_openURL_sourceApplication_annotation_ url_str = str(ObjCInstance(url)) NameError: global name 'ObjCInstance' is not defined
-
what version of pythonista are you using?
This looks like the x callback module was cleared, so its tlobals no longe exist.
You may be able to correct this with a
from objc_utils import ObjCInstance
inside the callback function that is failing (i.e make sure all dependencies of that function are imported locally, and do not rely on closures to expose globals)
-
v2.0
I threw some imports in, but now I'm stuck at:
NameError: global name 'c' is not defined
# coding: utf-8 import x_callback_url from objc_util import ObjCInstance _handler = None _requestID = None url = "working-copy://x-callback-url/status/?repo=MY_REPO&unchanged=1&key=MY_KEY&x-success=pythonista://" def handler(response): print(response) x_callback_url.open_url(url, handler)
EDIT: Ahh. Putting a
from objc_util import *
into my calling module got me over the worst of it. But I still need access to the globals (because with the code above I end up with_requestID
and_handler
beingNone
).EDIT2: Well I'm stumped. I wondered if the leading
_
was messing with the global variables (something something name mangling?) so I renamed_requestID
and_handler
tog_requestID
andg_handler
, respectively and it worked! Everything ran fine after that. A little later I tried reverting that change (the engineer in me likes reproducible errors) and everything still worked fine, despite my earlier problems. So then I tried removing the extra import in my calling module (from objc_utils import *
) and everything still worked fine. So it seems my original issue has vanished without a trace and now I cannot reproduce it. -
Please excuse the double post, but this is about a different issue to my last post.
I note that the OP's version of this script assumes that the x-callback response data will be formatted something like
app://xcallbackresponse-REQUEST_ID/?query=value&query=value
. But one of the apps I'm working with formats its response likeapp://xcallbackresponse-REQUEST_IDvalue
which causes the URL parsing in this script to break.Here is a modified version which handles this case a little more gracefully by setting
x_callback_response.parameters
toNone
when it can't parse the URL directly and by creating a newx_callback_response.raw_response_data
which is simply the response URL without theapp://xcallbackresponse-REQUEST_ID
bit. -
@Subject22 thank you! I'll look into it when I find some time
-
This is absolutely fantastic !
have you by any chance tried calling pythonista from another app ?
say workflow->pythonista->callback ? -
When trying to use this recipe and consequently swizzle.py I'm seeing the follow error:
Traceback (most recent call last): File "_ctypes/callbacks.c", line 234, in 'calling callback function' File "/private/var/mobile/Containers/Shared/AppGroup/74CC34ED-493E-431F-9C45-5BD2EF3B2AE0/Pythonista3/Documents/firebaseapp/swizzle.py", line 146, in saveData File "/var/containers/Bundle/Application/71C9338F-1BD7-4D52-9DAD-EE24DDF5139E/Pythonista3.app/Frameworks/Py3Kit.framework/pylib/site-packages/objc_util.py", line 796, in __call__ method_name, kwarg_order = resolve_instance_method(obj, self.name, args, kwargs) File "/var/containers/Bundle/Application/71C9338F-1BD7-4D52-9DAD-EE24DDF5139E/Pythonista3.app/Frameworks/Py3Kit.framework/pylib/site-packages/objc_util.py", line 403, in resolve_instance_method raise AttributeError('No method found for %s' % (name,)) AttributeError: No method found for originalsaveData
Any ideas how I can debug this?
-
I posted a github comment -- basically the code was swizzling a subclass's method, but then is passed an instance of the parent class, which does not have the original method. Swizzling the parent class (somewhat manual in this case) resolves the issue
-
was wanting to let you know that x_callback_url is no longer working with the most recent pythonista release woth python 3.6
-
In what way? Are you getting exceptions?
-
the callout works, the callback comes back to pythonista
however the swizzled handler doesnt fire -
no exceptions are showing, and the print statements ive put into the callback dont get called indicating that the swizzel method is not getting called or has changed
-
When it comes back, is the app still running? I.e anything you had printed to the console is still there?
-
I see that API is depreciated, so likely needs to be updated to use application:openURL:options: instead.
-
ended up using
openPythonistaURL:
instead of
application:openURL:sourceApplication:annotation:cheers D
-
@eddo888 what do you mean?
did you swizzleopenPythonistaURL:
instead of swizzling -[UIApplicationDelegate application:openURL:options:]
? -
here is my uodated and sligjtly modified x_callback_url.py
# coding: utf-8 import swizzle from objc_util import * import sys, re, os, argparse import ctypes, json, urllib, uuid, urllib import webbrowser def argue(): parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode') parser.add_argument('-t', '--test', action='store_true', help='run test') return parser.parse_args() def params(data): if len(data) == 0: return '' p = '&'.join( map( lambda x: '%s=%s'%(x,urllib.quote(data[x])), data.keys() ) ) return '?%s'%p def reverse(url): query = NSURLComponents.componentsWithURL_resolvingAgainstBaseURL_(nsurl(url), False) parameters = dict() if query.queryItems() is not None: for queryItem in query.queryItems(): parameters[str(queryItem.name())] = str(queryItem.value()) return parameters def open_url(url, handler): global _handler global _requestID _requestID = uuid.uuid1() _handler = handler x_success = urllib.quote('pythonista://?request=%s'%_requestID) url_with_uuid = url.replace('?','?x-success=%s&'%x_success) #sys.stderr.write('> %s\n'% url_with_uuid) webbrowser.open(url_with_uuid) def openPythonistaURL_(_self, _sel, url): url_str = str(ObjCInstance(url)) #sys.stderr.write('< %s\n'%url_str) global _call_me, _handler, _requestID if '?request=%s'%_requestID in url_str: url_str = url_str.replace('?request=%s&'%_requestID, '?') parameters = reverse(url_str) if _handler: _handler(parameters) return True elif _call_me in url_str: #print url_str parameters = reverse(url_str) x_parameters = dict() for x in [ 'x-source', 'x-success', 'x-error', 'x-cancel', 'x-script', ]: if x in parameters.keys(): x_parameters[x] = parameters[x] del parameters[x] #print '%s\n%s'%( # json.dumps(x_parameters), # json.dumps(parameters) #) if 'x-script' not in x_parameters.keys(): return try: import importlib mod = importlib.import_module( x_parameters['x-script'] ) res = str(mod.main(parameters)) url=x_parameters['x-success']+'?args=%s'%urllib.quote(res) except: error=str(sys.exc_info()[0]) url=x_parameters['x-error']+'?args=%s'%urllib.quote(error) #print url webbrowser.open(url) return True else: #print('original url=%s'%url_str) obj = ObjCInstance(_self) original_method = getattr(obj, 'original'+c.sel_getName(_sel), None) if original_method: _annotation = ObjCInstance(annotation) if annotation else None return original_method( ObjCInstance(app), ObjCInstance(url), ObjCInstance(source_app), _annotation ) return def test(): data={ 'statement' : 'select * from MyTable' } url='generaldb://x-callback-url/execute-select-statement' + params(data) print url def myhandler(parameters): print parameters for row in parameters['rows'].split('\n'): print row return open_url(url,myhandler) def setup(): global NSURLComponents, _call_me, _handler, _requestID _call_me = 'pythonista://x-callback-url' _handler = None _requestID = None NSURLComponents = ObjCClass('NSURLComponents') appDelegate = UIApplication.sharedApplication().delegate() # Do the swizzling cls = ObjCInstance(c.object_getClass(appDelegate.ptr)) swizzle.swizzle( cls, 'openPythonistaURL:', openPythonistaURL_ ) #print 'swizzled' return def main(): setup() args = argue() if args.test : test(); return print 'setup complete:'#, sys.argv #webbrowser.open('workflow://') return if __name__ == '__main__': main()
-
(fyi, you should wrap the code block with ```, to get proper syntax highlighting. reading the code w/out that is really difficult)