omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    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

    Pythonista
    x-callback url scheme
    10
    36
    31260
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • lukaskollmer
      lukaskollmer last edited by lukaskollmer

      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 the pythonista:// 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 for x-source and x-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!

      1 Reply Last reply Reply Quote 5
      • JonB
        JonB last edited by

        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!

        1 Reply Last reply Reply Quote 2
        • Subject22
          Subject22 last edited by Subject22

          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
          
          1 Reply Last reply Reply Quote 0
          • JonB
            JonB last edited by JonB

            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)

            1 Reply Last reply Reply Quote 0
            • Subject22
              Subject22 last edited by Subject22

              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 being None).

              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 to g_requestID and g_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.

              1 Reply Last reply Reply Quote 0
              • Subject22
                Subject22 last edited by Subject22

                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 like app://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 to None when it can't parse the URL directly and by creating a new x_callback_response.raw_response_data which is simply the response URL without the app://xcallbackresponse-REQUEST_ID bit.

                1 Reply Last reply Reply Quote 1
                • lukaskollmer
                  lukaskollmer last edited by

                  @Subject22 thank you! I'll look into it when I find some time

                  1 Reply Last reply Reply Quote 1
                  • eddo888
                    eddo888 last edited by

                    This is absolutely fantastic !
                    have you by any chance tried calling pythonista from another app ?
                    say workflow->pythonista->callback ?

                    1 Reply Last reply Reply Quote 0
                    • silverjam
                      silverjam last edited by

                      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?

                      1 Reply Last reply Reply Quote 0
                      • JonB
                        JonB last edited by ccc

                        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

                        1 Reply Last reply Reply Quote 0
                        • eddo888
                          eddo888 last edited by

                          was wanting to let you know that x_callback_url is no longer working with the most recent pythonista release woth python 3.6

                          1 Reply Last reply Reply Quote 0
                          • JonB
                            JonB last edited by

                            In what way? Are you getting exceptions?

                            1 Reply Last reply Reply Quote 0
                            • eddo888
                              eddo888 last edited by

                              the callout works, the callback comes back to pythonista
                              however the swizzled handler doesnt fire

                              1 Reply Last reply Reply Quote 0
                              • eddo888
                                eddo888 last edited by

                                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

                                1 Reply Last reply Reply Quote 0
                                • JonB
                                  JonB last edited by

                                  When it comes back, is the app still running? I.e anything you had printed to the console is still there?

                                  1 Reply Last reply Reply Quote 0
                                  • JonB
                                    JonB last edited by

                                    I see that API is depreciated, so likely needs to be updated to use application:openURL:options: instead.

                                    1 Reply Last reply Reply Quote 0
                                    • eddo888
                                      eddo888 last edited by

                                      ended up using
                                      openPythonistaURL:
                                      instead of
                                      application:openURL:sourceApplication:annotation:

                                      cheers D

                                      1 Reply Last reply Reply Quote 0
                                      • lukaskollmer
                                        lukaskollmer last edited by

                                        @eddo888 what do you mean?
                                        did you swizzle openPythonistaURL: instead of swizzling -[UIApplicationDelegate application:openURL:options:]?

                                        1 Reply Last reply Reply Quote 0
                                        • eddo888
                                          eddo888 last edited by eddo888

                                          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()
                                          
                                          
                                          1 Reply Last reply Reply Quote 0
                                          • lukaskollmer
                                            lukaskollmer last edited by

                                            (fyi, you should wrap the code block with ```, to get proper syntax highlighting. reading the code w/out that is really difficult)

                                            1 Reply Last reply Reply Quote 0
                                            • First post
                                              Last post
                                            Powered by NodeBB Forums | Contributors