[CalendarServer-changes] [12760] CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py

source_changes at macosforge.org source_changes at macosforge.org
Wed Feb 26 18:34:09 PST 2014


Revision: 12760
          http://trac.calendarserver.org//changeset/12760
Author:   wsanchez at apple.com
Date:     2014-02-26 18:34:09 -0800 (Wed, 26 Feb 2014)
Log Message:
-----------
New module for EventSource code.

Added Paths:
-----------
    CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py

Added: CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py	2014-02-27 02:34:09 UTC (rev 12760)
@@ -0,0 +1,224 @@
+# -*- test-case-name: calendarserver.webadmin.test.test_principals -*-
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from __future__ import print_function
+
+"""
+Calendar Server principal management web UI.
+"""
+
+__all__ = [
+    "textAsEvent",
+    "EventSourceResource",
+]
+
+from collections import deque
+
+from zope.interface import implementer, Interface
+
+from twisted.internet.defer import succeed
+
+from txweb2.stream import IByteStream, fallbackSplit
+from txweb2.resource import Resource
+from txweb2.http_headers import MimeType
+from txweb2.http import Response
+
+
+def textAsEvent(text, eventID=None, eventClass=None):
+    """
+    Format some text as an HTML5 EventSource event.  Since the EventSource data
+    format is text-oriented, this function expects L{unicode}, not L{bytes};
+    binary data should be encoded as text if it is to be used in an EventSource
+    stream.
+
+    @param text: The text (ie. the message) to send in the event.
+    @type text: L{unicode}
+
+    @param eventID: An unique identifier for the event.
+    @type eventID: L{unicode}
+
+    @param eventClass: A class name (ie. a categorization) for the event.
+    @type eventClass: L{unicode}
+
+    @return: An HTML5 EventSource event as text.
+    @rtype: L{unicode}
+    """
+    event = []
+
+    if eventID is not None:
+        event.append(u"id: {0}".format(eventID))
+
+    if eventClass is not None:
+        event.append(u"event: {0}".format(eventClass))
+
+    event.extend(
+        u"data: {0}".format(l) for l in text.split("\n")
+    )
+
+    return u"\n".join(event) + u"\n\n"
+
+
+
+class IEventDecoder(Interface):
+    """
+    An object that can be used to extract data from an application-specific
+    event object for encoding into an EventSource data stream.
+    """
+
+    def idForEvent(event):
+        """
+        @return: An unique identifier for the given event.
+        @rtype: L{unicode}
+        """
+
+    def classForEvent(event):
+        """
+        @return: A class name (ie. a categorization) for the event.
+        @rtype: L{unicode}
+        """
+
+    def textForEvent(event):
+        """
+        @return: The text (ie. the message) to send in the event.
+        @rtype: L{unicode}
+        """
+
+
+
+class EventSourceResource(Resource):
+    """
+    Resource that vends HTML5 EventSource events.
+
+    Events are stored in a ring buffer and streamed to clients.
+    """
+
+    addSlash = False
+
+
+    def __init__(self, eventDecoder, bufferSize=400):
+        """
+        @param eventDecoder: An object that can be used to extract data from
+            an event for encoding into an EventSource data stream.
+        @type eventDecoder: L{IEventDecoder}
+
+        @param bufferSize: The maximum number of events to keep in the ring
+            buffer.
+        @type bufferSize: L{int}
+        """
+        Resource.__init__(self)
+
+        self._events = deque(maxlen=bufferSize)
+
+
+    def render(self, request):
+        lastID = request.headers.getRawHeaders(u"last-event-id")
+
+        response = Response()
+        response.stream = EventStream(self._eventDecoder, self._events, lastID)
+        response.headers.setHeader(
+            b"content-type", MimeType.fromString(b"text/event-stream")
+        )
+        return response
+
+
+
+ at implementer(IByteStream)
+class EventStream(object):
+    """
+    L{IByteStream} that streams out HTML5 EventSource events.
+    """
+
+    length = None
+
+
+    def __init__(self, eventDecoder, events, lastID):
+        """
+        @param eventDecoder: An object that can be used to extract data from
+            an event for encoding into an EventSource data stream.
+        @type eventDecoder: L{IEventDecoder}
+
+        @param events: Application-specific event objects.
+        @type events: sequence of L{object}
+
+        @param lastID: The identifier for the last event that was vended from
+            C{events}.  Vending will resume starting from the following event.
+        @type lastID: L{int}
+        """
+        object.__init__(self)
+
+        self._eventDecoder = eventDecoder
+        self._events = events
+        self._lastID = lastID
+        self._closed = False
+
+
+    def addEvent(self, event):
+        self._events.append(event)
+
+
+    def read(self):
+        if self._closed:
+            return None
+
+        lastID = self._lastID
+        eventID = None
+
+        idForEvent = self.eventDecoder.idForEvent
+        classForEvent = self.eventDecoder.classForEvent
+        textForEvent = self.eventDecoder.textForEvent
+
+        for event in self._events:
+            eventID = idForEvent(event)
+
+            # If lastID is not None, skip messages up to and including the one
+            # referenced by lastID.
+            if lastID is not None:
+                if eventID == lastID:
+                    eventID = None
+                    lastID = None
+
+                continue
+
+            eventClass = classForEvent(event)
+            eventText = textForEvent(event)
+
+            return succeed(
+                textAsEvent(eventText, eventID, eventClass).encode("utf-8")
+            )
+
+
+        if eventID is not None:
+            # We just scanned all the messages, and none are the last one the
+            # client saw.
+            self._lastID = None
+
+            return succeed("")
+
+        return succeed(None)
+
+        # from twisted.internet.task import deferLater
+        # from twisted.internet import reactor
+
+        # return deferLater(reactor, 1.0, self.read)
+
+
+    def split(self, point):
+        return fallbackSplit(self, point)
+
+
+    def close(self):
+        self._closed = True
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140226/1712669e/attachment-0001.html>


More information about the calendarserver-changes mailing list