Commits

Bob Ippolito committed ef4ade7

Moved Twisted refactor of WebServicesTool here due to license issues

  • Participants
  • Parent commits c638c31

Comments (0)

Files changed (18)

pyobjc/Examples/Twisted/README.txt

+These are PyObjC examples that require Twisted 1.1 or later to run
+
+Twisted is available from http://twistedmatrix.com/

pyobjc/Examples/Twisted/WebServicesTool/English.lproj/InfoPlist.strings

Binary file added.

pyobjc/Examples/Twisted/WebServicesTool/English.lproj/MainMenu.nib/classes.nib

+{
+    IBClasses = (
+        {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, 
+        {
+            ACTIONS = {newConnectionAction = id; }; 
+            CLASS = WSTApplicationDelegate; 
+            LANGUAGE = ObjC; 
+            SUPERCLASS = NSObject; 
+        }
+    ); 
+    IBVersion = 1; 
+}

pyobjc/Examples/Twisted/WebServicesTool/English.lproj/MainMenu.nib/info.nib

+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IBEditorPositions</key>
+	<dict>
+		<key>29</key>
+		<string>83 318 318 44 0 0 1152 746 </string>
+	</dict>
+	<key>IBFramework Version</key>
+	<string>286.0</string>
+	<key>IBOpenObjects</key>
+	<array>
+		<integer>29</integer>
+	</array>
+	<key>IBSystem Version</key>
+	<string>6D52</string>
+</dict>
+</plist>

pyobjc/Examples/Twisted/WebServicesTool/English.lproj/MainMenu.nib/keyedobjects.nib

Binary file added.

pyobjc/Examples/Twisted/WebServicesTool/English.lproj/WSTConnection.nib/classes.nib

+{
+    IBClasses = (
+        {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, 
+        {
+            ACTIONS = {printDocument = id; reloadVisibleData = id; }; 
+            CLASS = WSTConnectionWindowController; 
+            LANGUAGE = ObjC; 
+            OUTLETS = {
+                methodDescriptionTextView = NSTextView; 
+                methodsTable = NSTableView; 
+                progressIndicator = NSProgressIndicator; 
+                statusTextField = NSTextField; 
+                urlTextField = NSTextField; 
+            }; 
+            SUPERCLASS = NSWindowController; 
+        }
+    ); 
+    IBVersion = 1; 
+}

pyobjc/Examples/Twisted/WebServicesTool/English.lproj/WSTConnection.nib/info.nib

+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IBDocumentLocation</key>
+	<string>11 17 356 240 0 0 1152 746 </string>
+	<key>IBEditorPositions</key>
+	<dict>
+		<key>15</key>
+		<string>328 456 454 82 0 0 1152 746 </string>
+	</dict>
+	<key>IBFramework Version</key>
+	<string>291.0</string>
+	<key>IBOpenObjects</key>
+	<array>
+		<integer>15</integer>
+		<integer>5</integer>
+	</array>
+	<key>IBSystem Version</key>
+	<string>6L60</string>
+</dict>
+</plist>

pyobjc/Examples/Twisted/WebServicesTool/English.lproj/WSTConnection.nib/keyedobjects.nib

Binary file added.

pyobjc/Examples/Twisted/WebServicesTool/LICENSE.txt

+(This is the MIT license)
+
+ Copyright (c) 2002 CodeFab, Inc.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

pyobjc/Examples/Twisted/WebServicesTool/Main.py

+import sys
+from PyObjCTools import AppHelper
+
+# import classes required to start application
+import WSTApplicationDelegateClass
+import WSTConnectionWindowControllerClass
+
+# pass control to the AppKit
+AppHelper.runEventLoop()

pyobjc/Examples/Twisted/WebServicesTool/Preferences.png

Added
New image

pyobjc/Examples/Twisted/WebServicesTool/README.txt

+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.
+
+See: http://pyobjc.sourceforge.net/
+
+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
+
+To run the demo:
+python buildapp.py build
+open build/Web\ Services\ Tool.app
+
+bob@redivi.com

pyobjc/Examples/Twisted/WebServicesTool/Reload.png

Added
New image

pyobjc/Examples/Twisted/WebServicesTool/WST.icns

Binary file added.

pyobjc/Examples/Twisted/WebServicesTool/WST.png

Added
New image

pyobjc/Examples/Twisted/WebServicesTool/WSTApplicationDelegateClass.py

+"""
+WSTApplicationDelegateClass
+
+An instance of this class is instantiated in the MainMenu.nib default NIB file.
+All outlets and the base class are automatically derived at runtime by the
+AutoBaseClass mechanism provided by the NibClassBuilder.
+"""
+
+from PyObjCTools import NibClassBuilder
+
+# Make NibClassBuilder aware of the classes in the main NIB file.
+NibClassBuilder.extractClasses( "MainMenu" )
+
+# WSTApplicationDelegate will automatically inherit from the
+# appropriate ObjC class [NSObject, in this case] and will have the
+# appropriate IBOutlets already defined based on the data found in the
+# NIB file(s) that define the class. 
+class WSTApplicationDelegate(NibClassBuilder.AutoBaseClass):
+
+    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_().
+        """
+        from WSTConnectionWindowControllerClass import WSTConnectionWindowController
+        WSTConnectionWindowController.connectionWindowController().showWindow_(sender)
+
+    def applicationDidFinishLaunching_(self, aNotification):
+        """Create and display a new connection window
+        """
+        self.newConnectionAction_(None)

pyobjc/Examples/Twisted/WebServicesTool/WSTConnectionWindowControllerClass.py

+"""
+Instances of WSTConnectionWindowController are the controlling object
+for the document windows for the Web Services Tool application.
+
+Implements a standard toolbar.
+"""
+
+# Note about multi-threading.
+# Although WST does its network stuff in a background thread, with Python 2.2
+# there are still moments where the app appears to hang briefly. This should
+# only be noticable when your DNS is slow-ish. The hang is caused by the
+# socket.getaddrinfo() function, which is used (indirectly) when connecting
+# to a server, which is a frequent operation when using xmlrpclib (it makes
+# a new connection for each request). Up to (and including) version 2.3b1,
+# Python would not grant time to other threads while blocking inside
+# getaddrinfo(). This has been fixed *after* 2.3b1 was released. (jvr)
+
+from AppKit import *
+from Foundation import *
+from PyObjCTools import NibClassBuilder
+
+from objc import IBOutlet
+from objc import selector
+from objc import YES, NO
+
+import twisted.internet.cfreactor
+reactor = twisted.internet.cfreactor.install()
+from twisted.internet import defer
+from twisted.web.xmlrpc import Proxy
+
+import sys
+import types
+import string
+import traceback
+
+#from twisted.python import log
+#log.startLogging(sys.stdout)
+
+kWSTReloadContentsToolbarItemIdentifier = "WST: Reload Contents Toolbar Identifier"
+"""Identifier for 'reload contents' toolbar item."""
+
+kWSTPreferencesToolbarItemIdentifier = "WST: Preferences Toolbar Identifier"
+"""Identifier for 'preferences' toolbar item."""
+
+kWSTUrlTextFieldToolbarItemIdentifier = "WST: URL Textfield Toolbar Identifier"
+"""Idnetifier for URL text field toolbar item."""
+
+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 untility.
+    """
+    toolbarItem = NSToolbarItem.alloc().initWithItemIdentifier_(anIdentifier)
+    
+    toolbarItem.setLabel_(aLabel)
+    toolbarItem.setPaletteLabel_(aPaletteLabel)
+    toolbarItem.setToolTip_(aToolTip)
+    toolbarItem.setTarget_(aTarget)
+    if anAction:
+        toolbarItem.setAction_(anAction)
+    
+    if type(anItemContent) == 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 = NSMenuItem.alloc().init()
+        menuItem.setSubmenu_(aMenu)
+        menuItem.setTitle_( aMenu.title() )
+        toolbarItem.setMenuFormRepresentation_(menuItem)
+    
+    aController._toolbarItems[anIdentifier] = toolbarItem
+
+NibClassBuilder.extractClasses( "WSTConnection" )
+
+class WSTConnectionWindowController(NibClassBuilder.AutoBaseClass):
+    """
+    As per the definition in the NIB file,
+    WSTConnectionWindowController is a subclass of
+    NSWindowController.  It acts as a NSTableView data source and
+    implements a standard toolbar.
+    """
+    __slots__ = ('_toolbarItems',
+        '_toolbarDefaultItemIdentifiers',
+        '_toolbarAllowedItemIdentifiers',
+        '_methods',
+        '_methodSignatures',
+        '_methodDescriptions',
+        '_server',
+        '_methodPrefix',)
+    
+    def connectionWindowController(self):
+        """
+        Create and return a default connection window instance.
+        """
+        return WSTConnectionWindowController.alloc().init()
+    
+    connectionWindowController = classmethod(connectionWindowController)
+    
+    def init(self):
+        """
+        Designated initializer.
+
+        Returns self (as per ObjC designated initializer definition,
+        unlike Python's __init__() method).
+        """
+        self = self.initWithWindowNibName_("WSTConnection")
+
+        self._toolbarItems = {}
+        self._toolbarDefaultItemIdentifiers = []
+        self._toolbarAllowedItemIdentifiers = []
+
+        self._methods = []
+        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_(NSProgressIndicatorSpinningStyle)
+        self.progressIndicator.setDisplayedWhenStopped_(NO)
+        
+        self.createToolbar()
+        # Start the CFReactor if it's not already going
+        if not reactor.running:
+            reactor.run()
+    
+    def windowWillClose_(self, aNotification):
+        """
+        Clean up when the document window is closed.
+        """
+        reactor.stop()
+        self.autorelease()
+
+    def createToolbar(self):
+        """
+        Creates and configures the toolbar to be used by the window.
+        """
+        toolbar = NSToolbar.alloc().initWithIdentifier_("WST Connection Window")
+        toolbar.setDelegate_(self)
+        toolbar.setAllowsUserCustomization_(YES)
+        toolbar.setAutosavesConfiguration_(YES)
+        
+        self.createToolbarItems()
+        
+        self.window().setToolbar_(toolbar)
+
+        lastURL = 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:", NSImage.imageNamed_("Reload"), None)
+        addToolbarItem(self, kWSTPreferencesToolbarItemIdentifier,
+                       "Preferences", "Preferences", "Show Preferences", None,
+                       "orderFrontPreferences:", NSImage.imageNamed_("Preferences"), None)
+        addToolbarItem(self, kWSTUrlTextFieldToolbarItemIdentifier,
+                       "URL", "URL", "Server URL", None, None, self.urlTextField, None)
+        
+        self._toolbarDefaultItemIdentifiers = [
+            kWSTReloadContentsToolbarItemIdentifier,
+            kWSTUrlTextFieldToolbarItemIdentifier,
+            NSToolbarSeparatorItemIdentifier,
+            NSToolbarCustomizeToolbarItemIdentifier,
+        ]
+        
+        self._toolbarAllowedItemIdentifiers = [
+            kWSTReloadContentsToolbarItemIdentifier,
+            kWSTUrlTextFieldToolbarItemIdentifier,
+            NSToolbarSeparatorItemIdentifier,
+            NSToolbarSpaceItemIdentifier,
+            NSToolbarFlexibleSpaceItemIdentifier,
+            NSToolbarPrintItemIdentifier,
+            kWSTPreferencesToolbarItemIdentifier,
+            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._toolbarDefaultItemIdentifiers
+
+    def toolbarAllowedItemIdentifiers_(self, anIdentifier):
+        """
+        Return an array of toolbar items that may be used in the toolbar.
+        """
+        return self._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 = NSToolbarItem.alloc().initWithItemIdentifier_(itemIdentifier)
+        item = self._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._methods)
+        # All UI calls should be directed to the main thread
+        self.statusTextField.performSelectorOnMainThread_withObject_waitUntilDone_(
+            "setStringValue:", aMessage, 0)
+    
+    def reloadData(self):
+        """Tell the main thread to update the table view."""
+        self.methodsTable.reloadData()
+    
+    def startWorking(self):
+        """Signal the UI there's work goin on."""
+        self.progressIndicator.startAnimation_(self)
+    
+    def stopWorking(self):
+        """Signal the UI that the work is done."""
+        self.progressIndicator.stopAnimation_(self)
+    
+    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._methods = []
+        self._methodSignatures = {}
+        self._methodDescriptions = {}
+        
+        if not url:
+            self.window().setTitle_("Untitled.")
+            self.setStatusTextFieldMessage_("No URL specified.")
+            return
+
+        self.window().setTitle_(url)
+        NSUserDefaults.standardUserDefaults().setObject_forKey_(url, "LastURL")
+
+        self.setStatusTextFieldMessage_("Retrieving method list...")
+        self.getMethods(url)
+    
+    def getMethods(self, url):
+        _server = self._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()
+        )
+            
+    def receivedMethodsFailure(self, why, method):
+        self._server = None
+        self._methodPrefix = None
+        self.setStatusTextFieldMessage_(
+           ("Server failed to respond to %s.  " 
+            "See below for more information."       ) % (method,)
+        )
+        #log.err(why)
+        self.methodDescriptionTextView.setString_(why.getTraceback())
+        
+    def receivedMethods(self, _methods, _server, _methodPrefix):
+        self._server = _server
+        self._methods = _methods
+        self._methodPrefix = _methodPrefix
+        
+        self._methods.sort()
+        self.reloadData()
+        self.setStatusTextFieldMessage_(
+            "Retrieving information about %d methods." % (len(self._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._methods):
+            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)
+        ).addCallback(
+            lambda ig: self.reloadData()
+        )
+
+    def fetchMethodSignature(self, ignore, index, aMethod):
+        if (index % 5)==0:
+            self.reloadData()
+        self.setStatusTextFieldMessage_(
+            "Retrieving signature for method %s (%d of %d)." 
+            % (aMethod , index, len(self._methods))
+        )
+        return self._server.callRemote(
+            self._methodPrefix + 'methodSignature',
+            aMethod
+        )
+            
+    
+    def processSignatureForMethod(self, methodSignature, index, aMethod):
+        signatures = None
+        if not len(methodSignature):
+            return
+        for aSignature in methodSignature:
+            if (type(aSignature) == types.ListType) and (len(aSignature) > 0):
+                signature = "%s %s(%s)" % (aSignature[0], aMethod, string.join(aSignature[1:], ", "))
+            else:
+                signature = aSignature
+        if signatures:
+            signatures = signatures + ", " + signature
+        else:
+            signatures = signature
+        self._methodSignatures[aMethod] = signatures
+    
+    def couldntProcessSignatureForMethod(self, why, index, aMethod):
+
+        #log.err(why)
+        self._methodSignatures[aMethod] = (
+            "<error> %s %s" % (aMethod, why.getBriefTraceback())
+        )
+            
+    def tableViewSelectionDidChange_(self, sender):
+        """
+        When the user selects a remote method, this method displays
+        the documentation for that method as returned by the XML-RPC
+        server.  If the method's documentation has been previously
+        queried, the documentation will be retrieved from a cache.
+        """
+        selectedRow = self.methodsTable.selectedRow()
+        selectedMethod = self._methods[selectedRow]
+        
+        def displayMethod(methodDescription):
+            self.setStatusTextFieldMessage_(None)
+            self.methodDescriptionTextView.setString_(methodDescription)
+        self.fetchMethodDescription(selectedMethod).addCallback(displayMethod)
+        
+    def fetchMethodDescription(self, aMethod):
+        desc = self._methodDescriptions
+        if aMethod in desc:
+            return defer.succeed(desc[aMethod])
+
+        def cacheDesc(v):
+            v = v or "No description available."
+            desc[aMethod] = v
+            return v
+        
+        def _stopWorking(v):
+            self.stopWorking()
+            return v
+
+        desc[aMethod] = "<description is being retrieved>"
+        self.setStatusTextFieldMessage_(
+            "Retrieving signature for method %s..." % (aMethod,)
+        )
+        self.startWorking()
+        return self._server.callRemote(
+            self._methodPrefix + 'methodHelp',
+            aMethod
+        ).addCallback(_stopWorking).addCallback(cacheDesc)
+            
+            
+    def numberOfRowsInTableView_(self, aTableView):
+        """
+        Returns the number of methods found on the server.
+        """
+        return len(self._methods)
+
+    def tableView_objectValueForTableColumn_row_(self, aTableView, aTableColumn, rowIndex):
+        """
+        Returns either the raw method name or the method signature,
+        depending on if a signature had been found on the server.
+        """
+        aMethod = self._methods[rowIndex]
+        if self._methodSignatures.has_key(aMethod):
+            return self._methodSignatures[aMethod]
+        else:
+            return aMethod
+
+    def tableView_shouldEditTableColumn_row_(self, aTableView, aTableColumn, rowIndex):
+        # don't allow editing of any cells
+        return 0

pyobjc/Examples/Twisted/WebServicesTool/buildapp.py

+from bundlebuilder import buildapp
+
+OTHERSRC=[
+    'WSTApplicationDelegateClass.py', 
+    'WSTConnectionWindowControllerClass.py']
+
+buildapp(
+	name = "Web Services Tool",
+	mainprogram = "Main.py",
+	resources = ["English.lproj", "Preferences.png", "Reload.png", "WST.png"] + OTHERSRC,
+	nibname = "MainMenu",
+	iconfile = "WST.icns",
+)