Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
excelRTDServer.py435 linesDownload Raw Back to demos
1"""Excel IRTDServer implementation.2 3This module is a functional example of how to implement the IRTDServer interface4in python, using the pywin32 extensions. Further details, about this interface5and it can be found at:6     http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnexcl2k2/html/odc_xlrtdfaq.asp7"""8 9# Copyright (c) 2003-2004 by Chris Nilsson <chris@slort.org>10#11# By obtaining, using, and/or copying this software and/or its12# associated documentation, you agree that you have read, understood,13# and will comply with the following terms and conditions:14#15# Permission to use, copy, modify, and distribute this software and16# its associated documentation for any purpose and without fee is17# hereby granted, provided that the above copyright notice appears in18# all copies, and that both that copyright notice and this permission19# notice appear in supporting documentation, and that the name of20# Christopher Nilsson (the author) not be used in advertising or publicity21# pertaining to distribution of the software without specific, written22# prior permission.23#24# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD25# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-26# ABILITY AND FITNESS.  IN NO EVENT SHALL THE AUTHOR27# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY28# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,29# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS30# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE31# OF THIS SOFTWARE.32 33import datetime  # For the example classes...34import threading35 36import pythoncom37import win32com.client38from win32com import universal39from win32com.client import gencache40from win32com.server.exception import COMException41 42# Typelib info for version 10 - aka Excel XP.43# This is the minimum version of excel that we can work with as this is when44# Microsoft introduced these interfaces.45EXCEL_TLB_GUID = "{00020813-0000-0000-C000-000000000046}"46EXCEL_TLB_LCID = 047EXCEL_TLB_MAJOR = 148EXCEL_TLB_MINOR = 449 50# Import the excel typelib to make sure we've got early-binding going on.51# The "ByRef" parameters we use later won't work without this.52gencache.EnsureModule(EXCEL_TLB_GUID, EXCEL_TLB_LCID, EXCEL_TLB_MAJOR, EXCEL_TLB_MINOR)53 54# Tell pywin to import these extra interfaces.55# --56# QUESTION: Why? The interfaces seem to descend from IDispatch, so57# I'd have thought, for example, calling callback.UpdateNotify() (on the58# IRTDUpdateEvent callback excel gives us) would work without molestation.59# But the callback needs to be cast to a "real" IRTDUpdateEvent type. Hmm...60# This is where my small knowledge of the pywin framework / COM gets hazy.61# --62# Again, we feed in the Excel typelib as the source of these interfaces.63universal.RegisterInterfaces(64    EXCEL_TLB_GUID,65    EXCEL_TLB_LCID,66    EXCEL_TLB_MAJOR,67    EXCEL_TLB_MINOR,68    ["IRtdServer", "IRTDUpdateEvent"],69)70 71 72class ExcelRTDServer(object):73    """Base RTDServer class.74 75    Provides most of the features needed to implement the IRtdServer interface.76    Manages topic adding, removal, and packing up the values for excel.77 78    Shouldn't be instanciated directly.79 80    Instead, descendant classes should override the CreateTopic() method.81    Topic objects only need to provide a GetValue() function to play nice here.82    The values given need to be atomic (eg. string, int, float... etc).83 84    Also note: nothing has been done within this class to ensure that we get85    time to check our topics for updates. I've left that up to the subclass86    since the ways, and needs, of refreshing your topics will vary greatly. For87    example, the sample implementation uses a timer thread to wake itself up.88    Whichever way you choose to do it, your class needs to be able to wake up89    occaisionally, since excel will never call your class without being asked to90    first.91 92    Excel will communicate with our object in this order:93      1. Excel instanciates our object and calls ServerStart, providing us with94         an IRTDUpdateEvent callback object.95      2. Excel calls ConnectData when it wants to subscribe to a new "topic".96      3. When we have new data to provide, we call the UpdateNotify method of the97         callback object we were given.98      4. Excel calls our RefreshData method, and receives a 2d SafeArray (row-major)99         containing the Topic ids in the 1st dim, and the topic values in the100         2nd dim.101      5. When not needed anymore, Excel will call our DisconnectData to102         unsubscribe from a topic.103      6. When there are no more topics left, Excel will call our ServerTerminate104         method to kill us.105 106    Throughout, at undetermined periods, Excel will call our Heartbeat107    method to see if we're still alive. It must return a non-zero value, or108    we'll be killed.109 110    NOTE: By default, excel will at most call RefreshData once every 2 seconds.111          This is a setting that needs to be changed excel-side. To change this,112          you can set the throttle interval like this in the excel VBA object model:113            Application.RTD.ThrottleInterval = 1000 ' milliseconds114    """115 116    _com_interfaces_ = ["IRtdServer"]117    _public_methods_ = [118        "ConnectData",119        "DisconnectData",120        "Heartbeat",121        "RefreshData",122        "ServerStart",123        "ServerTerminate",124    ]125    _reg_clsctx_ = pythoncom.CLSCTX_INPROC_SERVER126    # _reg_clsid_ = "# subclass must provide this class attribute"127    # _reg_desc_ = "# subclass should provide this description"128    # _reg_progid_ = "# subclass must provide this class attribute"129 130    ALIVE = 1131    NOT_ALIVE = 0132 133    def __init__(self):134        """Constructor"""135        super(ExcelRTDServer, self).__init__()136        self.IsAlive = self.ALIVE137        self.__callback = None138        self.topics = {}139 140    def SignalExcel(self):141        """Use the callback we were given to tell excel new data is available."""142        if self.__callback is None:143            raise COMException(desc="Callback excel provided is Null")144        self.__callback.UpdateNotify()145 146    def ConnectData(self, TopicID, Strings, GetNewValues):147        """Creates a new topic out of the Strings excel gives us."""148        try:149            self.topics[TopicID] = self.CreateTopic(Strings)150        except Exception as why:151            raise COMException(desc=str(why))152        GetNewValues = True153        result = self.topics[TopicID]154        if result is None:155            result = "# %s: Waiting for update" % self.__class__.__name__156        else:157            result = result.GetValue()158 159        # fire out internal event...160        self.OnConnectData(TopicID)161 162        # GetNewValues as per interface is ByRef, so we need to pass it back too.163        return result, GetNewValues164 165    def DisconnectData(self, TopicID):166        """Deletes the given topic."""167        self.OnDisconnectData(TopicID)168 169        if TopicID in self.topics:170            self.topics[TopicID] = None171            del self.topics[TopicID]172 173    def Heartbeat(self):174        """Called by excel to see if we're still here."""175        return self.IsAlive176 177    def RefreshData(self, TopicCount):178        """Packs up the topic values. Called by excel when it's ready for an update.179 180        Needs to:181          * Return the current number of topics, via the "ByRef" TopicCount182          * Return a 2d SafeArray of the topic data.183            - 1st dim: topic numbers184            - 2nd dim: topic values185 186        We could do some caching, instead of repacking everytime...187        But this works for demonstration purposes."""188        TopicCount = len(self.topics)189        self.OnRefreshData()190 191        # Grow the lists, so we don't need a heap of calls to append()192        results = [[None] * TopicCount, [None] * TopicCount]193 194        # Excel expects a 2-dimensional array. The first dim contains the195        # topic numbers, and the second contains the values for the topics.196        # In true VBA style (yuck), we need to pack the array in row-major format,197        # which looks like:198        #   ( (topic_num1, topic_num2, ..., topic_numN), \199        #     (topic_val1, topic_val2, ..., topic_valN) )200        for idx, topicdata in enumerate(self.topics.items()):201            topicNum, topic = topicdata202            results[0][idx] = topicNum203            results[1][idx] = topic.GetValue()204 205        # TopicCount is meant to be passed to us ByRef, so return it as well, as per206        # the way pywin32 handles ByRef arguments.207        return tuple(results), TopicCount208 209    def ServerStart(self, CallbackObject):210        """Excel has just created us... We take its callback for later, and set up shop."""211        self.IsAlive = self.ALIVE212 213        if CallbackObject is None:214            raise COMException(desc="Excel did not provide a callback")215 216        # Need to "cast" the raw PyIDispatch object to the IRTDUpdateEvent interface217        IRTDUpdateEventKlass = win32com.client.CLSIDToClass.GetClass(218            "{A43788C1-D91B-11D3-8F39-00C04F3651B8}"219        )220        self.__callback = IRTDUpdateEventKlass(CallbackObject)221 222        self.OnServerStart()223 224        return self.IsAlive225 226    def ServerTerminate(self):227        """Called when excel no longer wants us."""228        self.IsAlive = self.NOT_ALIVE  # On next heartbeat, excel will free us229        self.OnServerTerminate()230 231    def CreateTopic(self, TopicStrings=None):232        """Topic factory method. Subclass must override.233 234        Topic objects need to provide:235          * GetValue() method which returns an atomic value.236 237        Will raise NotImplemented if not overridden.238        """239        raise NotImplemented("Subclass must implement")240 241    # Overridable class events...242    def OnConnectData(self, TopicID):243        """Called when a new topic has been created, at excel's request."""244        pass245 246    def OnDisconnectData(self, TopicID):247        """Called when a topic is about to be deleted, at excel's request."""248        pass249 250    def OnRefreshData(self):251        """Called when excel has requested all current topic data."""252        pass253 254    def OnServerStart(self):255        """Called when excel has instanciated us."""256        pass257 258    def OnServerTerminate(self):259        """Called when excel is about to destroy us."""260        pass261 262 263class RTDTopic(object):264    """Base RTD Topic.265    Only method required by our RTDServer implementation is GetValue().266    The others are more for convenience."""267 268    def __init__(self, TopicStrings):269        super(RTDTopic, self).__init__()270        self.TopicStrings = TopicStrings271        self.__currentValue = None272        self.__dirty = False273 274    def Update(self, sender):275        """Called by the RTD Server.276        Gives us a chance to check if our topic data needs to be277        changed (eg. check a file, quiz a database, etc)."""278        raise NotImplemented("subclass must implement")279 280    def Reset(self):281        """Call when this topic isn't considered "dirty" anymore."""282        self.__dirty = False283 284    def GetValue(self):285        return self.__currentValue286 287    def SetValue(self, value):288        self.__dirty = True289        self.__currentValue = value290 291    def HasChanged(self):292        return self.__dirty293 294 295# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=296 297######################################298# Example classes299######################################300 301 302class TimeServer(ExcelRTDServer):303    """Example Time RTD server.304 305    Sends time updates back to excel.306 307    example of use, in an excel sheet:308      =RTD("Python.RTD.TimeServer","","seconds","5")309 310    This will cause a timestamp string to fill the cell, and update its value311    every 5 seconds (or as close as possible depending on how busy excel is).312 313    The empty string parameter denotes the com server is running on the local314    machine. Otherwise, put in the hostname to look on. For more info315    on this, lookup the Excel help for its "RTD" worksheet function.316 317    Obviously, you'd want to wrap this kind of thing in a friendlier VBA318    function.319 320    Also, remember that the RTD function accepts a maximum of 28 arguments!321    If you want to pass more, you may need to concatenate arguments into one322    string, and have your topic parse them appropriately.323    """324 325    # win32com.server setup attributes...326    # Never copy the _reg_clsid_ value in your own classes!327    _reg_clsid_ = "{EA7F2CF1-11A2-45E4-B2D5-68E240DB8CB1}"328    _reg_progid_ = "Python.RTD.TimeServer"329    _reg_desc_ = "Python class implementing Excel IRTDServer -- feeds time"330 331    # other class attributes...332    INTERVAL = 0.5  # secs. Threaded timer will wake us up at this interval.333 334    def __init__(self):335        super(TimeServer, self).__init__()336 337        # Simply timer thread to ensure we get to update our topics, and338        # tell excel about any changes. This is a pretty basic and dirty way to339        # do this. Ideally, there should be some sort of waitable (eg. either win32340        # event, socket data event...) and be kicked off by that event triggering.341        # As soon as we set up shop here, we _must_ return control back to excel.342        # (ie. we can't block and do our own thing...)343        self.ticker = threading.Timer(self.INTERVAL, self.Update)344 345    def OnServerStart(self):346        self.ticker.start()347 348    def OnServerTerminate(self):349        if not self.ticker.finished.isSet():350            self.ticker.cancel()  # Cancel our wake-up thread. Excel has killed us.351 352    def Update(self):353        # Get our wake-up thread ready...354        self.ticker = threading.Timer(self.INTERVAL, self.Update)355        try:356            # Check if any of our topics have new info to pass on357            if len(self.topics):358                refresh = False359                for topic in self.topics.values():360                    topic.Update(self)361                    if topic.HasChanged():362                        refresh = True363                    topic.Reset()364 365                if refresh:366                    self.SignalExcel()367        finally:368            self.ticker.start()  # Make sure we get to run again369 370    def CreateTopic(self, TopicStrings=None):371        """Topic factory. Builds a TimeTopic object out of the given TopicStrings."""372        return TimeTopic(TopicStrings)373 374 375class TimeTopic(RTDTopic):376    """Example topic for example RTD server.377 378    Will accept some simple commands to alter how long to delay value updates.379 380    Commands:381      * seconds, delay_in_seconds382      * minutes, delay_in_minutes383      * hours, delay_in_hours384    """385 386    def __init__(self, TopicStrings):387        super(TimeTopic, self).__init__(TopicStrings)388        try:389            self.cmd, self.delay = self.TopicStrings390        except Exception as E:391            # We could simply return a "# ERROR" type string as the392            # topic value, but explosions like this should be able to get handled by393            # the VBA-side "On Error" stuff.394            raise ValueError("Invalid topic strings: %s" % str(TopicStrings))395 396        # self.cmd = str(self.cmd)397        self.delay = float(self.delay)398 399        # setup our initial value400        self.checkpoint = self.timestamp()401        self.SetValue(str(self.checkpoint))402 403    def timestamp(self):404        return datetime.datetime.now()405 406    def Update(self, sender):407        now = self.timestamp()408        delta = now - self.checkpoint409        refresh = False410        if self.cmd == "seconds":411            if delta.seconds >= self.delay:412                refresh = True413        elif self.cmd == "minutes":414            if delta.minutes >= self.delay:415                refresh = True416        elif self.cmd == "hours":417            if delta.hours >= self.delay:418                refresh = True419        else:420            self.SetValue("#Unknown command: " + self.cmd)421 422        if refresh:423            self.SetValue(str(now))424            self.checkpoint = now425 426 427if __name__ == "__main__":428    import win32com.server.register429 430    # Register/Unregister TimeServer example431    # eg. at the command line: excelrtd.py --register432    # Then type in an excel cell something like:433    # =RTD("Python.RTD.TimeServer","","seconds","5")434    win32com.server.register.UseCommandLine(TimeServer)435 
codekingpro/portable-devtools · Team Ai