WebServicesTool-CocoaBindings
=============================

* :download:`Download example <PyObjCExample-WebServicesTool-CocoaBindings.zip>`

Web Services Tool

Web Services Tool queries XML-RPC enabled servers via the "standard"
introspection methods and displays a summary of the API.  It is
implemented in Python using the PyObjC module.

To use the application, simply provide the connection window with an URL
to the XML-RPC handler of a web server.  If the server at least
implements the listMethods() method, the app will display a list of
available methods.

Source for both the pyobjc module and the Web Services Tool are
available via the pyobjc sourceforge CVS repository.

The source of this application demonstrates
- using Python's network libraries inside a Cocoa app
- how to use multi-threading
- how to create an NSToolbar
- how to use an NSTableView

b.bum
bbum@codefab.com

This application has been modified for Twisted.  It demonstrates:
- using Twisted in a Cocoa app with the cfreactor
- how to write responsive single-threaded network applications
- well, it no longer demonstrates how to use multi-threading

To run the demo:
python setup.py py2app
open dist/Web\ Services\ Tool.app

bob@redivi.com


.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

Main.py
.......

.. sourcecode:: python

    # import classes required to start application
    import WSTApplicationDelegateClass  # noqa: F401
    import WSTConnectionWindowControllerClass  # noqa: F401
    from PyObjCTools import AppHelper
    from twisted.internet import cfreactor
    
    cfreactor.install()
    
    
    # pass control to the AppKit
    AppHelper.runEventLoop()

.. rst-class:: tabbertab

RPCMethod.py
............

.. sourcecode:: python

    import objc
    from Foundation import NSObject
    from objc import super  # noqa: A004
    
    
    class RPCMethod(NSObject):
        def initWithDocument_name_(self, aDocument, aName):
            self = super().init()
            self.document = aDocument
            self.k_methodName = aName
            self.k_methodSignature = None
            self.k_methodDescription = None
            return self
    
        def methodName(self):
            return self.k_methodName
    
        def displayName(self):
            if self.k_methodSignature is None:
                return self.k_methodName
            else:
                return self.k_methodSignature
    
        @objc.accessor
        def setMethodSignature_(self, aSignature):
            self.k_methodSignature = aSignature
    
        def methodDescription(self):
            if self.k_methodDescription is None:
                self.setMethodDescription_("<description not yet received>")
                self.document.fetchMethodDescription_(self)
            return self.k_methodDescription
    
        @objc.accessor
        def setMethodDescription_(self, aDescription):
            self.k_methodDescription = aDescription

.. rst-class:: tabbertab

WSTApplicationDelegateClass.py
..............................

.. sourcecode:: python

    """
    WSTApplicationDelegateClass
    """
    
    import objc
    from Foundation import NSObject
    from PyObjCTools import AppHelper
    from twisted.internet import reactor
    from WSTConnectionWindowControllerClass import WSTConnectionWindowController
    
    
    class WSTApplicationDelegate(NSObject):
        @objc.IBAction
        def newConnectionAction_(self, sender):
            """Action method fired when the user selects the 'new connection'
            menu item.  Note that the WSTConnectionWindowControllerClass is
            defined the first time this method is invoked.
    
            This kind of lazy evaluation is generally recommended;  it speeds
            app launch time and it ensures that cycles aren't wasted loading
            functionality that will never be used.
    
            (In this case, it is largely moot due to the implementation of
            applicationDidFinishLaunching_().
            """
            WSTConnectionWindowController.connectionWindowController().showWindow_(sender)
    
        def applicationShouldTerminate_(self, sender):
            if reactor.running:
                reactor.addSystemEventTrigger("after", "shutdown", AppHelper.stopEventLoop)
                reactor.stop()
                return False
            return True
    
        def applicationDidFinishLaunching_(self, aNotification):
            """Create and display a new connection window"""
            reactor.interleave(AppHelper.callAfter)
            self.newConnectionAction_(None)

.. rst-class:: tabbertab

WSTConnectionWindowControllerClass.py
.....................................

.. sourcecode:: python

    """
    Instances of WSTConnectionWindowController are the controlling object
    for the document windows for the Web Services Tool application.
    
    Implements a standard toolbar.
    """
    
    import AppKit
    import objc
    from RPCMethod import RPCMethod
    from twisted.internet import defer
    from twisted.web.xmlrpc import Proxy
    
    # from twisted.python import log
    # import sys
    # log.startLogging(sys.stdout)
    
    # cheap dirty way to turn those messages off
    # from twisted.python import log
    # log.logerr = open('/dev/null','w')
    
    # Identifier for 'reload contents' toolbar item.
    kWSTReloadContentsToolbarItemIdentifier = "WST: Reload Contents Toolbar Identifier"
    
    # Identifier for 'preferences' toolbar item.
    kWSTPreferencesToolbarItemIdentifier = "WST: Preferences Toolbar Identifier"
    
    # Identifier for URL text field toolbar item.
    kWSTUrlTextFieldToolbarItemIdentifier = "WST: URL Textfield Toolbar Identifier"
    
    
    def addToolbarItem(
        aController,
        anIdentifier,
        aLabel,
        aPaletteLabel,
        aToolTip,
        aTarget,
        anAction,
        anItemContent,
        aMenu,
    ):
        """
        Adds an freshly created item to the toolbar defined by
        aController.  Makes a number of assumptions about the
        implementation of aController.  It should be refactored into a
        generically useful toolbar management utility.
        """
        toolbarItem = AppKit.NSToolbarItem.alloc().initWithItemIdentifier_(anIdentifier)
    
        toolbarItem.setLabel_(aLabel)
        toolbarItem.setPaletteLabel_(aPaletteLabel)
        toolbarItem.setToolTip_(aToolTip)
        toolbarItem.setTarget_(aTarget)
        if anAction:
            toolbarItem.setAction_(anAction)
    
        if isinstance(anItemContent, AppKit.NSImage):
            toolbarItem.setImage_(anItemContent)
        else:
            toolbarItem.setView_(anItemContent)
            bounds = anItemContent.bounds()
            minSize = (100, bounds[1][1])
            maxSize = (1000, bounds[1][1])
            toolbarItem.setMinSize_(minSize)
            toolbarItem.setMaxSize_(maxSize)
    
        if aMenu:
            menuItem = AppKit.NSMenuItem.alloc().init()
            menuItem.setSubmenu_(aMenu)
            menuItem.setTitle_(aMenu.title())
            toolbarItem.setMenuFormRepresentation_(menuItem)
    
        aController.k_toolbarItems[anIdentifier] = toolbarItem
    
    
    class WSTConnectionWindowController(AppKit.NSWindowController):
        methodDescriptionTextView = objc.IBOutlet()
        methodsTable = objc.IBOutlet()
        progressIndicator = objc.IBOutlet()
        statusTextField = objc.IBOutlet()
        urlTextField = objc.IBOutlet()
    
        @classmethod
        def connectionWindowController(self):
            """
            Create and return a default connection window instance.
            """
            return WSTConnectionWindowController.alloc().init()
    
        def init(self):
            """
            Designated initializer.
    
            Returns self (as per ObjC designated initializer definition,
            unlike Python's __init__() method).
            """
            self = self.initWithWindowNibName_("WSTConnection")
    
            self.k_toolbarItems = {}
            self.k_toolbarDefaultItemIdentifiers = []
            self.k_toolbarAllowedItemIdentifiers = []
    
            self.k_methods = {}
            self.k_methodArray = []
            return self
    
        def awakeFromNib(self):
            """
            Invoked when the NIB file is loaded.  Initializes the various
            UI widgets.
            """
            self.retain()  # balanced by autorelease() in windowWillClose_
    
            self.statusTextField.setStringValue_("No host specified.")
            self.progressIndicator.setStyle_(AppKit.NSProgressIndicatorSpinningStyle)
            self.progressIndicator.setDisplayedWhenStopped_(False)
    
            self.createToolbar()
    
        def windowWillClose_(self, aNotification):
            """
            Clean up when the document window is closed.
            """
            self.autorelease()
    
        def createToolbar(self):
            """
            Creates and configures the toolbar to be used by the window.
            """
            toolbar = AppKit.NSToolbar.alloc().initWithIdentifier_("WST Connection Window")
            toolbar.setDelegate_(self)
            toolbar.setAllowsUserCustomization_(True)
            toolbar.setAutosavesConfiguration_(True)
    
            self.createToolbarItems()
    
            self.window().setToolbar_(toolbar)
    
            lastURL = AppKit.NSUserDefaults.standardUserDefaults().stringForKey_("LastURL")
            if lastURL and len(lastURL):
                self.urlTextField.setStringValue_(lastURL)
    
        def createToolbarItems(self):
            """
            Creates all of the toolbar items that can be made available in
            the toolbar.  The actual set of available toolbar items is
            determined by other mechanisms (user defaults, for example).
            """
            addToolbarItem(
                self,
                kWSTReloadContentsToolbarItemIdentifier,
                "Reload",
                "Reload",
                "Reload Contents",
                None,
                "reloadVisibleData:",
                AppKit.NSImage.imageNamed_("Reload"),
                None,
            )
            addToolbarItem(
                self,
                kWSTPreferencesToolbarItemIdentifier,
                "Preferences",
                "Preferences",
                "Show Preferences",
                None,
                "orderFrontPreferences:",
                AppKit.NSImage.imageNamed_("Preferences"),
                None,
            )
            addToolbarItem(
                self,
                kWSTUrlTextFieldToolbarItemIdentifier,
                "URL",
                "URL",
                "Server URL",
                None,
                None,
                self.urlTextField,
                None,
            )
    
            self.k_toolbarDefaultItemIdentifiers = [
                kWSTReloadContentsToolbarItemIdentifier,
                kWSTUrlTextFieldToolbarItemIdentifier,
                AppKit.NSToolbarSeparatorItemIdentifier,
                AppKit.NSToolbarCustomizeToolbarItemIdentifier,
            ]
    
            self.k_toolbarAllowedItemIdentifiers = [
                kWSTReloadContentsToolbarItemIdentifier,
                kWSTUrlTextFieldToolbarItemIdentifier,
                AppKit.NSToolbarSeparatorItemIdentifier,
                AppKit.NSToolbarSpaceItemIdentifier,
                AppKit.NSToolbarFlexibleSpaceItemIdentifier,
                AppKit.NSToolbarPrintItemIdentifier,
                kWSTPreferencesToolbarItemIdentifier,
                AppKit.NSToolbarCustomizeToolbarItemIdentifier,
            ]
    
        def toolbarDefaultItemIdentifiers_(self, anIdentifier):
            """
            Return an array of toolbar item identifiers that identify the
            set, in order, of items that should be displayed on the
            default toolbar.
            """
            return self.k_toolbarDefaultItemIdentifiers
    
        def toolbarAllowedItemIdentifiers_(self, anIdentifier):
            """
            Return an array of toolbar items that may be used in the toolbar.
            """
            return self.k_toolbarAllowedItemIdentifiers
    
        def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(
            self, toolbar, itemIdentifier, flag
        ):
            """
            Delegate method fired when the toolbar is about to insert an
            item into the toolbar.  Item is identified by itemIdentifier.
    
            Effectively makes a copy of the cached reference instance of
            the toolbar item identified by itemIdentifier.
            """
            newItem = AppKit.NSToolbarItem.alloc().initWithItemIdentifier_(itemIdentifier)
            item = self.k_toolbarItems[itemIdentifier]
    
            newItem.setLabel_(item.label())
            newItem.setPaletteLabel_(item.paletteLabel())
            if item.view():
                newItem.setView_(item.view())
            else:
                newItem.setImage_(item.image())
    
            newItem.setToolTip_(item.toolTip())
            newItem.setTarget_(item.target())
            newItem.setAction_(item.action())
            newItem.setMenuFormRepresentation_(item.menuFormRepresentation())
    
            if newItem.view():
                newItem.setMinSize_(item.minSize())
                newItem.setMaxSize_(item.maxSize())
    
            return newItem
    
        def setStatusTextFieldMessage_(self, aMessage):
            """
            Sets the contents of the statusTextField to aMessage and
            forces the fileld's contents to be redisplayed.
            """
            if not aMessage:
                aMessage = "Displaying information about %d methods." % (
                    len(self.k_methods),
                )
            self.statusTextField.setStringValue_(aMessage)
    
        setStatusTextFieldMessage_ = objc.accessor(setStatusTextFieldMessage_)
    
        def startWorking(self):
            """Signal the UI there's work going on."""
            self.progressIndicator.startAnimation_(self)
    
        def stopWorking(self):
            """Signal the UI that the work is done."""
            self.progressIndicator.stopAnimation_(self)
    
        @objc.IBAction
        def reloadVisibleData_(self, sender):
            """
            Reloads the list of methods and their signatures from the
            XML-RPC server specified in the urlTextField.  Displays
            appropriate error messages, if necessary.
            """
            url = self.urlTextField.stringValue()
            self.k_methods = {}
    
            if not url:
                self.window().setTitle_("Untitled.")
                self.setStatusTextFieldMessage_("No URL specified.")
                return
    
            self.window().setTitle_(url)
            AppKit.NSUserDefaults.standardUserDefaults().setObject_forKey_(url, "LastURL")
    
            self.setStatusTextFieldMessage_("Retrieving method list...")
            self.getMethods(url)
    
        @objc.python_method
        def getMethods(self, url):
            _server = self.k_server = Proxy(url.encode("utf8"))
            self.startWorking()
            return (
                _server.callRemote("listMethods")
                .addCallback(
                    # call self.receivedMethods(result, _server, "") on success
                    self.receivedMethods,
                    _server,
                    "",
                )
                .addErrback(
                    # on error, call this lambda
                    lambda e: _server.callRemote("system.listMethods").addCallback(
                        # call self.receievedMethods(result, _server, "system.")
                        self.receivedMethods,
                        _server,
                        "system.",
                    )
                )
                .addErrback(
                    # log the failure instance, with a method
                    self.receivedMethodsFailure,
                    "listMethods()",
                )
                .addBoth(
                    # stop working nomatter what trap all errors (returns None)
                    lambda n: self.stopWorking()
                )
            )
    
        @objc.python_method
        def receivedMethodsFailure(self, why, method):
            self.k_server = None
            self.k_methodPrefix = None
            self.setStatusTextFieldMessage_(
                ("Server failed to respond to %s.  " "See below for more information.")
                % (method,)
            )
            # log.err(why)
            self.methodDescriptionTextView.setString_(why.getTraceback())
    
        @objc.python_method
        def receivedMethods(self, _methods, _server, _methodPrefix):
            self.k_server = _server
            self.k_methods = {}
            self.k_methodPrefix = _methodPrefix
            for aMethod in _methods:
                self.k_methods[aMethod] = RPCMethod.alloc().initWithDocument_name_(
                    self, aMethod
                )
            self.setMethodArray_(self.k_methods.values())
            self.k_methodPrefix = _methodPrefix
    
            self.setStatusTextFieldMessage_(
                "Retrieving information about %d methods." % (len(self.k_methods),)
            )
    
            # we could make all the requests at once :)
            # but the server might not like that so we will chain them
            d = defer.succeed(None)
            for index, aMethod in enumerate(self.k_methodArray):
                d.addCallback(self.fetchMethodSignature, index, aMethod).addCallbacks(
                    callback=self.processSignatureForMethod,
                    callbackArgs=(index, aMethod),
                    errback=self.couldntProcessSignatureForMethod,
                    errbackArgs=(index, aMethod),
                )
            return d.addCallback(lambda ig: self.setStatusTextFieldMessage_(None))
    
        @objc.python_method
        def fetchMethodSignature(self, ignore, index, aMethod):
            self.setStatusTextFieldMessage_(
                "Retrieving signature for method %s (%d of %d)."
                % (aMethod.methodName(), index, len(self.k_methods))
            )
            return self.k_server.callRemote(
                (self.k_methodPrefix + "methodSignature").encode("utf-8"),
                aMethod.methodName().encode("utf-8"),
            )
    
        @objc.python_method
        def processSignatureForMethod(self, methodSignature, index, aMethod):
            signatures = None
            if not len(methodSignature):
                return
            for aSignature in methodSignature:
                if isinstance(aSignature, list) and len(aSignature) > 0:
                    signature = "{} {}({})".format(
                        aSignature[0],
                        aMethod.methodName(),
                        ", ".join(aSignature[1:]),
                    )
                else:
                    signature = aSignature
            if signatures:
                signatures = signatures + ", " + signature
            else:
                signatures = signature
    
            aMethod.setMethodSignature_(signatures)
            self.replaceObjectInMethodArrayAtIndex_withObject_(index, aMethod)
    
        @objc.python_method
        def couldntProcessSignatureForMethod(self, why, index, aMethod):
            # log.err(why)
            aMethod.setMethodSignature_(
                f"<error> {aMethod.methodName()} {why.getBriefTraceback()}"
            )
            self.replaceObjectInMethodArrayAtIndex_withObject_(index, aMethod)
    
        def fetchMethodDescription_(self, aMethod):
            def cacheDesc(v):
                aMethod.setMethodDescription_(v or "No description available.")
    
            self.setStatusTextFieldMessage_(
                f"Retrieving documentation for method {aMethod.methodName()}..."
            )
            self.startWorking()
            self.k_server.callRemote(
                (self.k_methodPrefix + "methodHelp").encode("utf-8"),
                aMethod.methodName().encode("utf-8"),
            ).addCallback(cacheDesc)
    
        def methodArray(self):
            return self.k_methodArray
    
        @objc.accessor
        def countOfMethodArray(self):
            if self.k_methodArray is None:
                return 0
            return self.k_methodArray
    
        @objc.accessor
        def objectInMethodArrayAtIndex_(self, anIndex):
            return self.k_methodArray[anIndex]
    
        @objc.accessor
        def insertObject_inMethodArrayAtIndex_(self, anObject, anIndex):
            self.k_methodArray.insert(anIndex, anObject)
    
        @objc.accessor
        def removeObjectFromMethodArrayAtIndex_(self, anIndex):
            del self.k_methodArray[anIndex]
    
        @objc.accessor
        def replaceObjectInMethodArrayAtIndex_withObject_(self, anIndex, anObject):
            self.k_methodArray[anIndex] = anObject
    
        @objc.accessor
        def setMethodArray_(self, anArray):
            self.k_methodArray = anArray

.. rst-class:: tabbertab

setup.py
........

.. sourcecode:: python

    """
    Script for building the example.
    
    Usage:
        python3 setup.py py2app
    """
    
    from setuptools import setup
    
    setup(
        name="WebServices Tool (CoreData)",
        app=["Main.py"],
        data_files=["English.lproj", "Preferences.png", "Reload.png", "WST.png"],
        options={"py2app": {"iconfile": "WST.icns"}},
        setup_requires=["py2app", "pyobjc-framework-Cocoa"],
    )

