Changeset 3026

Show
Ignore:
Timestamp:
01/10/08 20:02:28 (10 months ago)
Author:
ixokai
Message:

Remove breadcrumbs hack that accidentally made it in; modified API documentation to reflect recent re-factoring before aat harassed me into a commit. :)

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • announcerplugin/0.11/announcerplugin/api.py

    r3015 r3026  
    1010    An IAnnouncementSubscriber component can use any means to determine  
    1111    if a user is interested in hearing about a given event. More then one 
    12     component can handle the same realms and categories.""" 
     12    component can handle the same realms and categories. 
     13     
     14    The subscriber must also indicate not just that a user is interested 
     15    in receiving a particular notice. Again, how it makes that decision is 
     16    entirely up to a particular implementation.""" 
    1317 
    1418    def get_subscription_realms(): 
    15         """Yields a list of realms that it is able to handle 
    16         subscriptions for.""" 
    17  
     19        """Returns an iterable that lists all the realms that this subscriber 
     20        is capable of handling subscriptions for. 
     21         
     22        Although these usually correspond to realms within Trac, there is no 
     23        actual requirement for that. Conspiracy between a specialied  
     24        producer, subscriber and formatter could result in messages about all 
     25        kinds of things not directly relatable to Trac resources. 
     26         
     27        If a single realm is handled, use 'yield' instead of 'return'.""" 
     28         
    1829    def get_subscription_categories(realm): 
    19         """Yields a list of realms that it is able to handle 
    20         subscriptions for.""" 
    21  
    22  
    23     def check_event(event): 
    24         """Yields a list of subscriptions that are interested in the  
     30        """Returns an iterable that lists all the categories that this 
     31        subscriber can handle for the specified realm. 
     32         
     33        If a single realm is handled, use 'yield' instead of 'return'.""" 
     34         
     35    def get_subscriptions_for_event(event): 
     36        """Returns a list of subscriptions that are interested in the  
    2537        specified event. 
    26  
     38         
    2739        Each subscription that is returned is in the form of: 
    28             ('method', 'username', 'format') 
    29  
    30         The method should correspond to a distribution method that is 
    31         provided by an IAnnouncementDistributor component. The 
    32         destination itself varies depending on the method; if the  
    33         method is 'email', the destination will be an email address. 
     40            ('transport', 'name', 'address') 
     41         
     42        The transport should be one that a distributor (and formatter) can 
     43        handle, but if not? The events will be dropped later at the 
     44        appropriate stage. 
     45         
     46        A subscriber must return at least the name or the address, but it 
     47        doesn't have to return both. In many cases returning both is 
     48        actually undesirable-- in such a case resolvers will be bypassed 
     49        entirely. 
    3450        """ 
    3551         
    3652class IAnnouncementFormatter(Interface): 
    37     def get_format_scheme(): 
    38         """email""" 
    39          
    40     def get_format_realms(scheme): 
    41         """Yields a list of realms.""" 
    42          
    43     def get_format_styles(scheme, realm): 
    44         """Yields a list of (format, weight)""" 
    45  
    46     def format(format, event): 
    47         """Returns the event rendered according to the appropriate  
    48         method. 
     53    """Formatters are responsible for converting an event into a message 
     54    appropriate for a given transport. 
     55     
     56    For transports like 'aim' or 'irc', this may be a short summary of a 
     57    change. For 'email', it may be a plaintext or html overview of all 
     58    the changes and perhaps the existing state. 
     59     
     60    It's up to a formatter to determine what ends up ultimately being sent 
     61    to the end-user. It's capable of pulling data out of the target object 
     62    that wasn't changed, picking and choosing details for whatever reason. 
     63     
     64    Since a formatter must be intimately familiar with the realm that  
     65    originated the event, formatters are tied to specific transport + realm 
     66    combinations. This means there may be a proliferation of formatters as 
     67    options expand. 
     68    """ 
     69     
     70    def get_format_transport(): 
     71        """Returns an iterable of the transports this formatter is capable of 
     72        handling. 
     73         
     74        If a single item is to be returned, use yield instead of return.""" 
     75         
     76    def get_format_realms(transport): 
     77        """Returns an iterable of realms that this formatter knows how to 
     78        handle for the specified transport. 
     79         
     80        If a single item is to be returned, use yield instead of return.""" 
     81 
     82    def get_format_styles(transport, realm): 
     83        """Returns an iterable of styles that this formatter supports for 
     84        a specified transport and realm.  
     85         
     86        Many formatters may simply return a single style and never have more; 
     87        that's fine. But if its useful to encapsulate code for several similar 
     88        styles a formatter can handle more then one. For example, 'plaintext' 
     89        and 'html' may be useful variants the same formatter handles. 
     90         
     91        Style names should be distinct between formatters for a specific 
     92        transport. 
     93         
     94        If a single item is to be returned, use yield instead of return.""" 
     95 
     96    def format(transport, realm, style, event): 
     97        """Converts the event into the specified style. If the transport or 
     98        realm passed into this method are not ones this formatter can handle, 
     99        it should return silently and without error. 
     100         
     101        The exact return type of this method is intentionally undefined. It  
     102        will be whatever the distributor that it is designed to work with  
     103        expects. 
    49104        """ 
    50105         
    51106class IAnnouncementDistributor(Interface): 
    52     def get_distribution_scheme(): 
    53         """Yields a list of methods that this distributor knows how 
    54         to use to deliver an event announcement. 
    55         """ 
    56  
    57     def distribute(destinations, message): 
    58         """Distributes an actual message.""" 
     107    """The Distributor is responsible for actually delivering an event to the 
     108    desired subscriptions. 
     109     
     110    A distributor should attempt to avoid blocking; using subprocesses is  
     111    preferred to threads.  
     112     
     113    Each distributor handles a single transport, and only one distributor 
     114    in the system should handle that. For example, there should not be  
     115    two distributors for the 'email' transport. 
     116    """ 
     117     
     118    def get_distribution_transports(): 
     119        """Returns a string representing the transport supported.""" 
     120 
     121    def distribute(transport, recipients, event): 
     122        """This method is meant to actually distribute the event to the 
     123        specified recipients, over the specified transport. 
     124         
     125        If it is passed a transport it does not support, it should return 
     126        silently and without error. 
     127         
     128        The recipients is a list of (name, address) pairs with either (but not 
     129        both) being allowed to be None. If name is provided but address isn't, 
     130        then the distributor should defer to IAnnouncementAddressResolver 
     131        implementations to determine what the address should be. 
     132         
     133        If the name is None but the address is not, then the distributor 
     134        should rely on the address being correct and use it-- if possible. 
     135         
     136        The distributor may initiate as many transactions as are necessecary 
     137        to deliver a message, but should use as few as possible; for example 
     138        in the EmailDistributor, if all of the recipients are receiving a 
     139        plain text form of the message, a single message with many BCC's 
     140        should be used. 
     141         
     142        The distributor is responsible for determining which of the 
     143        IAnnouncementFormatters should get the privilege of actually turning 
     144        an event into content. In cases where multiple formatters are capable 
     145        of converting an event into a message for a given transport, a 
     146        user preference would be a dandy idea. 
     147        """ 
    59148         
    60149class IAnnouncementPreferenceProvider(Interface): 
     150    """Represents a single 'box' in the Announcements preference panel. 
     151     
     152    Any component can always implement IPreferencePanelProvider to get 
     153    preferences from users, of course. However, considering there may be 
     154    several components related to the Announcement system, and many may 
     155    have different preferences for a user to set, that would clutter up 
     156    the preference interfac quite a bit. 
     157     
     158    The IAnnouncementPreferenceProvider allows several boxes to be 
     159    chained in the same panel to group the preferenecs related to the 
     160    Announcement System. 
     161     
     162    Implementing announcement preference boxes should be essentially 
     163    identical to implementing entire panels. 
     164    """ 
     165     
    61166    def get_announcement_preference_boxes(req): 
    62         """Returns (name, label)""" 
    63          
    64     def render_announcement_preference_box(req, panel): 
    65         """Returns (template, data)""" 
     167        """Accepts a request object, and returns an iterable of  
     168        (name, label) pairs; one for each box that the implementation 
     169        can generate. 
     170         
     171        If a single item is returned, be sure to 'yield' it instead of 
     172        returning it.""" 
     173 
     174    def render_announcement_preference_box(req, box): 
     175        """Accepts a request object, and the name (as from the previous 
     176        method) of the box that should be rendered. 
     177         
     178        Returns a tuple of (template, data) with the template being a 
     179        filename in a directory provided by an ITemplateProvider which 
     180        shall be rendered into a single <div> element, when combined 
     181        with the data member. 
     182        """ 
    66183        
    67184class IAnnouncementAddressResolver(Interface): 
     185    """Handles mapping Trac usernames to addresses for distributors to use.""" 
     186     
    68187    def get_address_for_name(name): 
    69         """Returns an address or name.""" 
     188        """Accepts a session name, and returns an address. 
     189         
     190        This address explicitly does not always have to mean an email address, 
     191        nor does it have to be an address stored within the Trac system at 
     192        all.  
     193         
     194        Implementations of this interface are never 'detected' automatically, 
     195        and must instead be specifically named for a particular distributor. 
     196        This way, some may find email addresses (for EmailDistributor), and 
     197        others may find AIM screen name. 
     198         
     199        If no address for the specified name can be found, None should be 
     200        returned. The next resolver will be attempted in the chain. 
     201        """ 
    70202         
    71203class AnnouncementEvent(object): 
     204    """AnnouncementEvent 
     205     
     206    This packages together in a single place all data related to a particular 
     207    event; notably the realm, category, and the target that represents the 
     208    initiator of the event.  
     209     
     210    In some (rare) cases, the target may be None; in cases where the message 
     211    is all that matters and there's no possible data you could conceivably 
     212    get beyond just the message. 
     213    """ 
    72214    def __init__(self, realm, category, target): 
    73215        self.realm = realm 
     
    81223 
    82224class AnnouncementSystem(Component): 
     225    """AnnouncementSystem represents the entry-point into the announcement 
     226    system, and is also the central controller that handles passing notices 
     227    around. 
     228     
     229    An announcement begins when something-- an announcement provider--  
     230    constructs an AnnouncementEvent (or subclass) and calls the send method 
     231    on the AnnouncementSystem.  
     232     
     233    Every event is classified by two required fields-- realm and category. 
     234    In general, the realm corresponds to the realm of a Resource within Trac; 
     235    ticket, wiki, milestone, and such. This is not a requirement, however.  
     236    Realms can be anything distinctive-- if you specify novel realms to solve 
     237    a particular problem, you'll simply also have to specify subscribers and 
     238    formatters who are able to deal with data in those realms. 
     239     
     240    The other classifier is a category that is defined by the providers and 
     241    has no particular meaning; for the providers that implement the 
     242    I*ChangeListener interfaces, the categories will often correspond to the 
     243    kinds of events they receive. For tickets, they would be 'created',  
     244    'changed' and 'deleted'. 
     245     
     246    There is no requirement for an event to have more then realm and category 
     247    to classify an event, but if more is provided in a subclass that the 
     248    subscribers can use to pick through events, all power to you. 
     249    """ 
    83250     
    84251    implements(IEnvironmentSetupParticipant) 
     
    140307 
    141308    def send(self, evt): 
    142         supported_subscribers = [] 
    143         for sp in self.subscribers: 
    144             categories = sp.get_subscription_categories(evt.realm) 
    145             if ('*' in categories) or (evt.category in categories): 
    146                 supported_subscribers.append(sp) 
    147          
    148         self.log.debug( 
    149             "AnnouncementSystem found the following subscribers capable of " 
    150             "handling '%s, %s': %s" % (evt.realm, evt.category,  
    151             ', '.join([ss.__class__.__name__ for ss in supported_subscribers])) 
    152         ) 
    153          
    154         subscriptions = set() 
    155         for sp in supported_subscribers: 
    156             subscriptions.update( 
    157                 x for x in sp.check_event(evt) if x 
     309        """Accepts a single AnnouncementEvent instance (or subclass), and 
     310        returns nothing.  
     311         
     312        There is no way (intentionally) to determine what the  
     313        AnnouncementSystem did with a particular event besides looking through 
     314        the debug logs. 
     315        """ 
     316         
     317        try: 
     318         
     319            supported_subscribers = [] 
     320            for sp in self.subscribers: 
     321                categories = sp.get_subscription_categories(evt.realm) 
     322                if ('*' in categories) or (evt.category in categories): 
     323                    supported_subscribers.append(sp) 
     324         
     325            self.log.debug( 
     326                "AnnouncementSystem found the following subscribers capable of " 
     327                "handling '%s, %s': %s" % (evt.realm, evt.category,  
     328                ', '.join([ss.__class__.__name__ for ss in supported_subscribers])) 
    158329            ) 
    159330         
    160         self.log.debug( 
    161             "AnnouncementSystem has found the following subscriptions: %s" % ( 
    162                 ', '.join( 
    163                     ['(%s via %s)' % ((s[1] or s[2]), s[0]) for s in subscriptions] 
     331            subscriptions = set() 
     332            for sp in supported_subscribers: 
     333                subscriptions.update( 
     334                    x for x in sp.check_event(evt) if x 
     335                ) 
     336         
     337            self.log.debug( 
     338                "AnnouncementSystem has found the following subscriptions: %s" % ( 
     339                    ', '.join( 
     340                        ['(%s via %s)' % ((s[1] or s[2]), s[0]) for s in subscriptions] 
     341                    ) 
    164342                ) 
    165343            ) 
    166         ) 
    167          
    168         packages = {} 
    169         for scheme, target, address in subscriptions: 
    170             if scheme not in packages: 
    171                 packages[scheme] = set() 
     344         
     345            packages = {} 
     346            for transport, target, address in subscriptions: 
     347                if transport not in packages: 
     348                    packages[transport] = set() 
    172349             
    173             packages[scheme].add((target,address)) 
     350                packages[transport].add((target,address)) 
    174351             
    175         for distributor in self.distributors: 
    176             scheme = distributor.get_distribution_scheme() 
    177             if scheme in packages: 
    178                 distributor.distribute(scheme, packages[scheme], evt) 
    179          
    180         return 
     352            for distributor in self.distributors: 
     353                transport = distributor.get_distribution_transport() 
     354                if transport in packages: 
     355                    distributor.distribute(transport, packages[transport], evt) 
     356        except: 
     357            self.log.error("AnnouncementSystem failed.", exc_info=True) 
  • announcerplugin/0.11/announcerplugin/distributors/email.py

    r3015 r3026  
    9292 
    9393    # IAnnouncementDistributor 
    94     def get_distribution_scheme(self): 
     94    def get_distribution_transport(self): 
    9595        return "email" 
    9696         
    97     def distribute(self, scheme, recipients, event): 
    98         if scheme == self.get_distribution_scheme(): 
     97    def distribute(self, transport, recipients, event): 
     98        if transport == self.get_distribution_transport(): 
    9999            formats = {} 
    100100             
    101101            for f in self.formatters: 
    102                 if f.get_format_scheme() == scheme
    103                     if event.realm in f.get_format_realms(scheme): 
    104                         styles = f.get_format_styles(scheme, event.realm) 
     102                if f.get_format_transport() == transport
     103                    if event.realm in f.get_format_realms(transport): 
     104                        styles = f.get_format_styles(transport, event.realm) 
    105105                        for style in styles: 
    106106                            formats[style] = f 
     
    109109                "EmailDistributor has found the following formats capable " 
    110110                "of handling '%s' of '%s': %s" % ( 
    111                     scheme, event.realm, ', '.join(formats.keys()) 
     111                    transport, event.realm, ', '.join(formats.keys()) 
    112112                ) 
    113113            ) 
     
    136136                #     ) 
    137137                # ) 
    138                 self._do_send(scheme, event, format, messages[format], formats[format]) 
     138                self._do_send(transport, event, format, messages[format], formats[format]) 
    139139 
    140140    def _get_default_format(self): 
     
    161161            return self._get_default_format() 
    162162             
    163     def _do_send(self, scheme, event, format, recipients, formatter): 
     163    def _do_send(self, transport, event, format, recipients, formatter): 
    164164        print "SENDING", recipients 
    165165        for name, address in recipients: 
    166166            print name, address 
    167167            print self.resolvers 
    168         output = formatter.format(scheme, event.realm, format, event) 
     168        output = formatter.format(transport, event.realm, format, event) 
    169169        print "THE OUTPUT IS", output 
    170170         
     
    177177        cfg = self.config 
    178178        sess = req.session 
    179         scheme = self.get_distribution_scheme() 
     179        transport = self.get_distribution_transport() 
    180180         
    181181        supported_realms = {} 
    182182        for formatter in self.formatters: 
    183             if formatter.get_format_scheme() == scheme
    184                 for realm in formatter.get_format_realms(scheme): 
     183            if formatter.get_format_transport() == transport
     184                for realm in formatter.get_format_realms(transport): 
    185185                    if realm not in supported_realms: 
    186186                        supported_realms[realm] = set() 
    187187                         
    188188                    supported_realms[realm].update( 
    189                        formatter.get_format_styles(scheme, realm) 
     189                       formatter.get_format_styles(transport, realm) 
    190190                    ) 
    191191                     
  • announcerplugin/0.11/announcerplugin/formatters/ticket_email.py

    r3015 r3026  
    88    default_email_format = Option('announcer', 'default_email_format', 'plaintext') 
    99     
    10     def get_format_scheme(self): 
     10    def get_format_transport(self): 
    1111        return "email" 
    1212         
    13     def get_format_realms(self, perspective): 
    14         if perspective == "email": 
     13    def get_format_realms(self, transport): 
     14        if transport == "email": 
    1515            yield "ticket" 
    1616        return 
    1717         
    18     def get_format_styles(self, perspective, realm): 
    19         if perspective == "email": 
     18    def get_format_styles(self, transport, realm): 
     19        if transport == "email": 
    2020            if realm == "ticket": 
    2121                yield "plaintext" 
     
    2424        return 
    2525 
    26     def format(self, scheme, realm, style, event): 
     26    def format(self, transport, realm, style, event): 
    2727        if realm == "ticket": 
    2828            if hasattr(self, '_format_%s' % style): 
  • announcerplugin/0.11/announcerplugin/subscribers/ticket_compat.py

    r3015 r3026  
    9393            return tuple() 
    9494             
    95     def check_event(self, event): 
     95    def get_subscriptions_for_event(self, event): 
    9696        if event.realm == "ticket": 
    9797            ticket = event.target 
  • announcerplugin/0.11/announcerplugin/subscribers/ticket.py

    r3015 r3026  
    1313        return ('created', 'changed') 
    1414         
    15     def check_event(self, event): 
     15    def get_subscriptions_for_event(self, event): 
    1616        return 
    1717        yield