1
# -*- test-case-name: epsilon.test.test_juice -*-
2
# Copyright 2005 Divmod, Inc. See LICENSE file for details
6
import warnings, pprint
8
from twisted.internet.main import CONNECTION_LOST
9
from twisted.internet.defer import Deferred, maybeDeferred, fail
10
from twisted.internet.protocol import ServerFactory, ClientFactory
11
from twisted.internet.ssl import Certificate
12
from twisted.python.failure import Failure
13
from twisted.python import log, filepath
15
from epsilon.liner import LineReceiver
16
from epsilon import extime
22
ERROR_CODE = '_error_code'
23
ERROR_DESCRIPTION = '_error_description'
30
""" I am a packet in the JUICE protocol. """
32
def __init__(self, __body='', **kw):
35
assert isinstance(__body, str), "body must be a string: %r" % ( repr(__body),)
40
warnings.warn("body attribute of boxes is now just a regular field",
43
def set(self, newbody):
44
warnings.warn("body attribute of boxes is now just a regular field",
46
self['body'] = newbody
48
body = property(*body())
51
newBox = self.__class__()
58
assert LENGTH not in self
61
for (k, v) in self.iteritems():
64
v = str(len(self[BODY]))
65
L.append(k.replace('_', '-').title())
67
L.append(v.replace(delimiter, escaped))
77
def sendTo(self, proto):
79
Serialize and send this box to a Juice instance. By the time it is
80
being sent, several keys are required. I must have exactly ONE of::
86
If the '-ask' header is set, then the '-command' header must also be
89
proto.sendPacket(self)
91
# juice.Box => JuiceBox
95
class TLSBox(JuiceBox):
97
return 'TLS(**%s)' % (super(TLSBox, self).__repr__(),)
100
def __init__(self, __certificate, __verify=None, __sslstarted=None, **kw):
101
super(TLSBox, self).__init__(**kw)
102
self.certificate = __certificate
103
self.verify = __verify
104
self.sslstarted = __sslstarted
106
def sendTo(self, proto):
107
super(TLSBox, self).sendTo(proto)
108
if self.verify is None:
109
proto.startTLS(self.certificate)
111
proto.startTLS(self.certificate, self.verify)
112
if self.sslstarted is not None:
115
class QuitBox(JuiceBox):
117
return 'Quit(**%s)' % (super(QuitBox, self).__repr__(),)
120
def sendTo(self, proto):
121
super(QuitBox, self).sendTo(proto)
122
proto.transport.loseConnection()
124
class _SwitchBox(JuiceBox):
126
return 'Switch(**%s)' % (super(_SwitchBox, self).__repr__(),)
129
def __init__(self, __proto, **kw):
130
super(_SwitchBox, self).__init__(**kw)
131
self.innerProto = __proto
133
def sendTo(self, proto):
134
super(_SwitchBox, self).sendTo(proto)
135
proto._switchTo(self.innerProto)
139
class NegotiateBox(JuiceBox):
141
return 'Negotiate(**%s)' % (super(NegotiateBox, self).__repr__(),)
144
def sendTo(self, proto):
145
super(NegotiateBox, self).sendTo(proto)
146
proto._setProtocolVersion(int(self['version']))
150
class JuiceError(Exception):
153
class RemoteJuiceError(JuiceError):
155
This error indicates that something went wrong on the remote end of the
156
connection, and the error was serialized and transmitted to you.
158
def __init__(self, errorCode, description, fatal=False):
159
"""Create a remote error with an error code and description.
161
Exception.__init__(self, "Remote[%s]: %s" % (errorCode, description))
162
self.errorCode = errorCode
163
self.description = description
166
class UnhandledRemoteJuiceError(RemoteJuiceError):
167
def __init__(self, description):
168
errorCode = "UNHANDLED"
169
RemoteJuiceError.__init__(self, errorCode, description)
171
class JuiceBoxError(JuiceError):
174
class MalformedJuiceBox(JuiceBoxError):
177
class UnhandledCommand(JuiceError):
181
class IncompatibleVersions(JuiceError):
185
def __init__(self, store, callable):
187
self.callable = callable
189
def __call__(self, box):
190
return self.store.transact(self.callable, box)
193
return '<Transaction in: %s of: %s>' % (self.store, self.callable)
196
baseDispatchPrefix = 'juice_'
197
autoDispatchPrefix = 'command_'
201
def _auto(self, aCallable, proto, namespace=None):
202
if aCallable is None:
204
command = aCallable.command
205
if namespace not in command.namespaces:
206
# if you're in the wrong namespace, you are very likely not allowed
207
# to invoke the command you are trying to invoke. some objects
208
# have commands exposed in a separate namespace for security
209
# reasons, since the security model is a role : namespace mapping.
210
log.msg('WRONG NAMESPACE: %r, %r' % (namespace, command.namespaces))
213
kw = stringsToObjects(box, command.arguments, proto)
214
for name, extraArg in command.extra:
215
kw[name] = extraArg.fromTransport(proto.transport)
216
# def checkIsDict(result):
217
# if not isinstance(result, dict):
218
# raise RuntimeError("%r returned %r, not dictionary" % (
219
# aCallable, result))
221
def checkKnownErrors(error):
222
key = error.trap(*command.allErrors)
223
code = command.allErrors[key]
224
desc = str(error.value)
225
return Failure(RemoteJuiceError(
226
code, desc, error in command.fatalErrors))
227
return maybeDeferred(aCallable, **kw).addCallback(
228
command.makeResponse, proto).addErrback(
232
def _wrap(self, aCallable):
233
if aCallable is None:
237
return wrap(aCallable)
241
def normalizeCommand(self, cmd):
242
"""Return the canonical form of a command.
244
return cmd.upper().strip().replace('-', '_')
246
def lookupFunction(self, proto, name, namespace):
247
"""Return a callable to invoke when executing the named command.
249
# Try to find a method to be invoked in a transaction first
250
# Otherwise fallback to a "regular" method
251
fName = self.autoDispatchPrefix + name
252
fObj = getattr(self, fName, None)
254
# pass the namespace along
255
return self._auto(fObj, proto, namespace)
257
assert namespace is None, 'Old-style parsing'
258
# Fall back to simplistic command dispatching - we probably want to get
259
# rid of this eventually, there's no reason to do extra work and write
260
# fewer docs all the time.
261
fName = self.baseDispatchPrefix + name
262
return getattr(self, fName, None)
264
def dispatchCommand(self, proto, cmd, box, namespace=None):
265
fObj = self.lookupFunction(proto, self.normalizeCommand(cmd), namespace)
267
return fail(UnhandledCommand(cmd))
268
return maybeDeferred(self._wrap(fObj), box)
271
'and', 'del', 'for', 'is', 'raise', 'assert', 'elif', 'from', 'lambda',
272
'return', 'break', 'else', 'global', 'not', 'try', 'class', 'except',
273
'if', 'or', 'while', 'continue', 'exec', 'import', 'pass', 'yield',
274
'def', 'finally', 'in', 'print']
276
def normalizeKey(key):
277
lkey = key.lower().replace('-', '_')
278
if lkey in PYTHON_KEYWORDS:
283
def parseJuiceHeaders(lines):
285
Create a JuiceBox from a list of header lines.
287
@param lines: a list of lines.
295
assert key is not None
296
b[key] += '\r\n'+L[1:]
298
parts = L.split(': ', 1)
300
raise MalformedJuiceBox("Wrong number of parts: %r" % (L,))
302
key = normalizeKey(key)
304
return int(b.pop(LENGTH, 0)), b
306
class JuiceParserBase(DispatchMixin):
309
self._outstandingRequests = {}
311
def _puke(self, failure):
312
log.msg("Juice server or network failure "
313
"unhandled by client application:")
316
"Dropping connection! "
317
"To avoid, add errbacks to ALL remote commands!")
318
if self.transport is not None:
319
self.transport.loseConnection()
325
return '%x' % (self._counter,)
327
def failAllOutgoing(self, reason):
328
OR = self._outstandingRequests.items()
329
self._outstandingRequests = None # we can never send another request
330
for key, value in OR:
331
value.errback(reason)
333
def juiceBoxReceived(self, box):
335
log.msg("Juice receive: %s" % pprint.pformat(dict(box.iteritems())))
338
question = self._outstandingRequests.pop(box[ANSWER])
339
question.addErrback(self._puke)
340
self._wrap(question.callback)(box)
342
question = self._outstandingRequests.pop(box[ERROR])
343
question.addErrback(self._puke)
344
self._wrap(question.errback)(
345
Failure(RemoteJuiceError(box[ERROR_CODE],
346
box[ERROR_DESCRIPTION])))
349
def sendAnswer(answerBox):
352
if self.transport is None:
354
answerBox[ANSWER] = box[ASK]
355
answerBox.sendTo(self)
356
def sendError(error):
359
if error.check(RemoteJuiceError):
360
code = error.value.errorCode
361
desc = error.value.description
362
if error.value.fatal:
365
errorBox = JuiceBox()
368
log.err(error) # here is where server-side logging happens
369
# if the error isn't handled
371
desc = "Unhandled Remote System Exception "
372
errorBox[ERROR] = box[ASK]
373
errorBox[ERROR_DESCRIPTION] = desc
374
errorBox[ERROR_CODE] = code
375
if self.transport is not None:
376
errorBox.sendTo(self)
377
return None # intentionally stop the error here: don't log the
378
# traceback if it's handled, do log it (earlier) if
380
self.dispatchCommand(self, cmd, box).addCallbacks(sendAnswer, sendError
381
).addErrback(self._puke)
384
"Empty packet received over connection-oriented juice: %r" % (box,))
386
def sendBoxCommand(self, command, box, requiresAnswer=True):
388
Send a command across the wire with the given C{juice.Box}.
390
Returns a Deferred which fires with the response C{juice.Box} when it
391
is received, or fails with a C{juice.RemoteJuiceError} if an error is
394
If the Deferred fails and the error is not handled by the caller of
395
this method, the failure will be logged and the connection dropped.
397
if self._outstandingRequests is None:
398
return fail(CONNECTION_LOST)
399
box[COMMAND] = command
400
tag = self._nextTag()
403
result = self._outstandingRequests[tag] = Deferred()
417
def __init__(self, optional=False):
418
self.optional = optional
420
def retrieve(self, d, name):
423
if value is not None:
429
def fromBox(self, name, strings, objects, proto):
430
st = self.retrieve(strings, name)
431
if self.optional and st is None:
434
objects[name] = self.fromStringProto(st, proto)
436
def toBox(self, name, strings, objects, proto):
437
obj = self.retrieve(objects, name)
438
if self.optional and obj is None:
439
# strings[name] = None
442
strings[name] = self.toStringProto(obj, proto)
444
def fromStringProto(self, inString, proto):
445
return self.fromString(inString)
447
def toStringProto(self, inObject, proto):
448
return self.toString(inObject)
450
def fromString(self, inString):
451
raise NotImplementedError()
453
def toString(self, inObject):
454
raise NotImplementedError()
456
class JuiceList(Argument):
457
def __init__(self, subargs):
458
self.subargs = subargs
460
def fromStringProto(self, inString, proto):
461
boxes = parseString(inString)
462
values = [stringsToObjects(box, self.subargs, proto)
466
def toStringProto(self, inObject, proto):
467
return ''.join([objectsToStrings(
468
objects, self.subargs, Box(), proto
469
).serialize() for objects in inObject])
471
class ListOf(Argument):
472
def __init__(self, subarg, delimiter=', '):
474
self.delimiter = delimiter
476
def fromStringProto(self, inString, proto):
477
strings = inString.split(self.delimiter)
478
L = [self.subarg.fromStringProto(string, proto)
479
for string in strings]
482
def toStringProto(self, inObject, proto):
484
for inSingle in inObject:
485
outString = self.subarg.toStringProto(inSingle, proto)
486
assert self.delimiter not in outString
488
return self.delimiter.join(L)
490
class Integer(Argument):
492
def toString(self, inObject):
493
return str(int(inObject))
495
class String(Argument):
496
def toString(self, inObject):
498
def fromString(self, inString):
501
class EncodedString(Argument):
503
def __init__(self, encoding):
504
self.encoding = encoding
506
def toString(self, inObject):
507
return inObject.encode(self.encoding)
509
def fromString(self, inString):
510
return inString.decode(self.encoding)
512
# Temporary backwards compatibility for Exponent
516
class Unicode(String):
517
def toString(self, inObject):
518
# assert isinstance(inObject, unicode)
519
return String.toString(self, inObject.encode('utf-8'))
521
def fromString(self, inString):
522
# assert isinstance(inString, str)
523
return String.fromString(self, inString).decode('utf-8')
526
def fromString(self, inString):
527
return filepath.FilePath(Unicode.fromString(self, inString))
529
def toString(self, inObject):
530
return Unicode.toString(self, inObject.path)
533
class Float(Argument):
537
class Base64Binary(Argument):
538
def toString(self, inObject):
539
return inObject.encode('base64').replace('\n', '')
540
def fromString(self, inString):
541
return inString.decode('base64')
543
class Time(Argument):
544
def toString(self, inObject):
545
return inObject.asISO8601TimeAndDate()
546
def fromString(self, inString):
547
return extime.Time.fromISO8601TimeAndDate(inString)
550
def fromTransport(self, inTransport):
551
raise NotImplementedError()
553
class Peer(ExtraArg):
554
def fromTransport(self, inTransport):
555
return inTransport.getQ2QPeer()
557
class PeerDomain(ExtraArg):
558
def fromTransport(self, inTransport):
559
return inTransport.getQ2QPeer().domain
561
class PeerUser(ExtraArg):
562
def fromTransport(self, inTransport):
563
return inTransport.getQ2QPeer().resource
565
class Host(ExtraArg):
566
def fromTransport(self, inTransport):
567
return inTransport.getQ2QHost()
569
class HostDomain(ExtraArg):
570
def fromTransport(self, inTransport):
571
return inTransport.getQ2QHost().domain
573
class HostUser(ExtraArg):
574
def fromTransport(self, inTransport):
575
return inTransport.getQ2QHost().resource
579
class Boolean(Argument):
580
def fromString(self, inString):
581
if inString == 'True':
583
elif inString == 'False':
586
raise RuntimeError("Bad boolean value: %r" % (inString,))
588
def toString(self, inObject):
595
class __metaclass__(type):
596
def __new__(cls, name, bases, attrs):
597
re = attrs['reverseErrors'] = {}
598
er = attrs['allErrors'] = {}
599
for v, k in attrs.get('errors',{}).iteritems():
602
for v, k in attrs.get('fatalErrors',{}).iteritems():
605
return type.__new__(cls, name, bases, attrs)
610
namespaces = [None] # This is set to [None] on purpose: None means
611
# "no namespace", not "empty list". "empty
612
# list" will make your command invalid in _all_
613
# namespaces, effectively uncallable.
622
return self.__class__.__name__
623
raise NotImplementedError("Missing command name")
625
commandName = property(*commandName())
627
def __init__(self, **kw):
629
givenArgs = [normalizeKey(k) for k in kw.keys()]
631
for name, arg in self.arguments:
632
if normalizeKey(name) not in givenArgs and not arg.optional:
633
forgotten.append(normalizeKey(name))
634
# for v in kw.itervalues():
636
# from pprint import pformat
637
# raise RuntimeError("ARGH: %s" % pformat(kw))
639
if len(forgotten) == 1:
640
plural = 'an argument'
642
plural = 'some arguments'
643
raise RuntimeError("You forgot %s to %r: %s" % (
644
plural, self.commandName, ', '.join(forgotten)))
647
def makeResponse(cls, objects, proto):
649
return objectsToStrings(objects, cls.response, cls.responseType(), proto)
651
log.msg("Exception in %r.makeResponse" % (cls,))
653
makeResponse = classmethod(makeResponse)
655
def do(self, proto, namespace=None, requiresAnswer=True):
656
if namespace is not None:
657
cmd = namespace + ":" + self.commandName
659
cmd = self.commandName
660
def _massageError(error):
661
error.trap(RemoteJuiceError)
663
return Failure(self.reverseErrors.get(rje.errorCode, UnhandledRemoteJuiceError)(rje.description))
665
d = proto.sendBoxCommand(
666
cmd, objectsToStrings(self.structured, self.arguments, self.commandType(),
671
d.addCallback(stringsToObjects, self.response, proto)
672
d.addCallback(self.addExtra, proto.transport)
673
d.addErrback(_massageError)
677
def addExtra(self, d, transport):
678
for name, extraArg in self.extra:
679
d[name] = extraArg.fromTransport(transport)
683
class ProtocolSwitchCommand(Command):
684
"""Use this command to switch from something Juice-derived to a different
685
protocol mid-connection. This can be useful to use juice as the
686
connection-startup negotiation phase. Since TLS is a different layer
687
entirely, you can use Juice to negotiate the security parameters of your
688
connection, then switch to a different protocol, and the connection will
692
def __init__(self, __protoToSwitchToFactory, **kw):
693
self.protoToSwitchToFactory = __protoToSwitchToFactory
694
super(ProtocolSwitchCommand, self).__init__(**kw)
696
def makeResponse(cls, innerProto, proto):
697
return _SwitchBox(innerProto)
699
makeResponse = classmethod(makeResponse)
701
def do(self, proto, namespace=None):
702
d = super(ProtocolSwitchCommand, self).do(proto)
705
innerProto = self.protoToSwitchToFactory.buildProtocol(proto.transport.getPeer())
706
proto._switchTo(innerProto, self.protoToSwitchToFactory)
709
proto.transport.loseConnection()
712
self.protoToSwitchToFactory.clientConnectionFailed(None, Failure(CONNECTION_LOST))
714
return d.addCallbacks(switchNow, handle).addErrback(die)
716
class Negotiate(Command):
717
commandName = 'Negotiate'
719
arguments = [('versions', ListOf(Integer()))]
720
response = [('version', Integer())]
722
responseType = NegotiateBox
725
class Juice(LineReceiver, JuiceParserBase):
727
JUICE (JUice Is Concurrent Events) is a simple connection-oriented
728
request/response protocol. Packets, or "boxes", are collections of
729
RFC2822-inspired headers, plus a body. Note that this is NOT a literal
730
interpretation of any existing RFC, 822, 2822 or otherwise, but a simpler
731
version that does not do line continuations, does not specify any
732
particular format for header values, dispatches semantic meanings of most
733
headers on the -Command header rather than giving them global meaning, and
734
allows multiple sets of headers (messages, or JuiceBoxes) on a connection.
736
All headers whose names begin with a dash ('-') are reserved for use by the
737
protocol. All others are for application use - their meaning depends on
738
the value of the "-Command" header.
741
protocolName = 'juice-base'
743
hostCertificate = None
745
MAX_LENGTH = 1024 * 1024
747
isServer = property(lambda self: self._issueGreeting,
749
True if this is a juice server, e.g. it is going to
750
issue or has issued a server greeting upon
754
isClient = property(lambda self: not self._issueGreeting,
756
True if this is a juice server, e.g. it is not going to
757
issue or did not issue a server greeting upon
761
def __init__(self, issueGreeting):
763
@param issueGreeting: whether to issue a greeting when connected. This
764
should be set on server-side Juice protocols.
766
JuiceParserBase.__init__(self)
767
self._issueGreeting = issueGreeting
770
return '<%s %s/%s at 0x%x>' % (self.__class__.__name__, self.isClient and 'client' or 'server', self.innerProtocol, id(self))
775
""" Lock this Juice instance so that no further Juice traffic may be sent.
776
This is used when sending a request to switch underlying protocols.
777
You probably want to subclass ProtocolSwitchCommand rather than calling
784
def _switchTo(self, newProto, clientFactory=None):
785
""" Switch this Juice instance to a new protocol. You need to do this
786
'simultaneously' on both ends of a connection; the easiest way to do
787
this is to use a subclass of ProtocolSwitchCommand.
790
assert self.innerProtocol is None, "Protocol can only be safely switched once."
792
self.innerProtocol = newProto
793
self.innerProtocolClientFactory = clientFactory
794
newProto.makeConnection(self.transport)
796
innerProtocolClientFactory = None
798
def juiceBoxReceived(self, box):
799
if self.__locked and COMMAND in box and ASK in box:
800
# This is a command which will trigger an answer, and we can no
801
# longer answer anything, so don't bother delivering it.
803
return super(Juice, self).juiceBoxReceived(box)
805
def sendPacket(self, completeBox):
807
Send a juice.Box to my peer.
809
Note: transport.write is never called outside of this method.
811
assert not self.__locked, "You cannot send juice packets when a connection is locked"
812
if self._startingTLSBuffer is not None:
813
self._startingTLSBuffer.append(completeBox)
816
log.msg("Juice send: %s" % pprint.pformat(dict(completeBox.iteritems())))
818
self.transport.write(completeBox.serialize())
820
def sendCommand(self, command, __content='', __answer=True, **kw):
821
box = JuiceBox(__content, **kw)
822
return self.sendBoxCommand(command, box, requiresAnswer=__answer)
824
_outstandingRequests = None
825
_justStartedTLS = False
827
def makeConnection(self, transport):
828
self._transportPeer = transport.getPeer()
829
self._transportHost = transport.getHost()
830
log.msg("%s %s connection established (HOST:%s PEER:%s)" % (self.isClient and "client" or "server",
831
self.__class__.__name__,
833
self._transportPeer))
834
self._outstandingRequests = {}
835
self._requestBuffer = []
836
LineReceiver.makeConnection(self, transport)
838
_startingTLSBuffer = None
840
def prepareTLS(self):
841
self._startingTLSBuffer = []
843
def startTLS(self, certificate, *verifyAuthorities):
844
if self.hostCertificate is None:
845
self.hostCertificate = certificate
846
self._justStartedTLS = True
847
self.transport.startTLS(certificate.options(*verifyAuthorities))
848
stlsb = self._startingTLSBuffer
849
if stlsb is not None:
850
self._startingTLSBuffer = None
855
"Previously authenticated connection between %s and %s "
856
"is trying to re-establish as %s" % (
857
self.hostCertificate,
858
Certificate.peerFromTransport(self.transport),
859
(certificate, verifyAuthorities)))
861
def dataReceived(self, data):
862
# If we successfully receive any data after TLS has been started, that
863
# means the connection was secured properly. Make a note of that fact.
864
if self._justStartedTLS:
865
self._justStartedTLS = False
866
return LineReceiver.dataReceived(self, data)
868
def connectionLost(self, reason):
869
log.msg("%s %s connection lost (HOST:%s PEER:%s)" % (
870
self.isClient and 'client' or 'server',
871
self.__class__.__name__,
873
self._transportPeer))
874
self.failAllOutgoing(reason)
875
if self.innerProtocol is not None:
876
self.innerProtocol.connectionLost(reason)
877
if self.innerProtocolClientFactory is not None:
878
self.innerProtocolClientFactory.clientConnectionLost(None, reason)
880
def lineReceived(self, line):
882
self._requestBuffer.append(line)
884
buf = self._requestBuffer
885
self._requestBuffer = []
886
bodylen, b = parseJuiceHeaders(buf)
888
self._bodyRemaining = bodylen
889
self._bodyBuffer = []
893
self.juiceBoxReceived(b)
895
def rawDataReceived(self, data):
896
if self.innerProtocol is not None:
897
self.innerProtocol.dataReceived(data)
899
self._bodyRemaining -= len(data)
900
if self._bodyRemaining <= 0:
901
if self._bodyRemaining < 0:
902
self._bodyBuffer.append(data[:self._bodyRemaining])
903
extraData = data[self._bodyRemaining:]
905
self._bodyBuffer.append(data)
907
self._pendingBox['body'] = ''.join(self._bodyBuffer)
908
self._bodyBuffer = None
909
b, self._pendingBox = self._pendingBox, None
910
self.juiceBoxReceived(b)
911
if self.innerProtocol is not None:
912
self.innerProtocol.makeConnection(self.transport)
914
self.innerProtocol.dataReceived(extraData)
916
self.setLineMode(extraData)
918
self._bodyBuffer.append(data)
922
def _setProtocolVersion(self, version):
923
# if we ever want to actually mangle encodings, this is the place to do
925
self.protocolVersion = version
928
def renegotiateVersion(self, newVersion):
929
assert newVersion in VERSIONS, (
930
"This side of the connection doesn't support version %r"
934
return Negotiate(versions=[newVersion]).do(self).addCallback(
935
lambda ver: self._setProtocolVersion(ver['version']))
937
def command_NEGOTIATE(self, versions):
938
for version in versions:
939
if version in VERSIONS:
940
return dict(version=version)
941
raise IncompatibleVersions()
942
command_NEGOTIATE.command = Negotiate
947
from cStringIO import StringIO
948
class _ParserHelper(Juice):
950
Juice.__init__(self, False)
952
self.results = Deferred()
960
disconnecting = False
962
def juiceBoxReceived(self, box):
963
self.boxes.append(box)
965
# Synchronous helpers
966
def parse(cls, fileObj):
969
p.dataReceived(fileObj.read())
971
parse = classmethod(parse)
973
def parseString(cls, data):
974
return cls.parse(StringIO(data))
975
parseString = classmethod(parseString)
977
parse = _ParserHelper.parse
978
parseString = _ParserHelper.parseString
980
def stringsToObjects(strings, arglist, proto):
982
myStrings = strings.copy()
983
for argname, argparser in arglist:
984
argparser.fromBox(argname, myStrings, objects, proto)
987
def objectsToStrings(objects, arglist, strings, proto):
989
for (k, v) in objects.items():
990
myObjects[normalizeKey(k)] = v
992
for argname, argparser in arglist:
993
argparser.toBox(argname, strings, myObjects, proto)
996
class JuiceServerFactory(ServerFactory):
998
def buildProtocol(self, addr):
999
prot = self.protocol(True)
1003
class JuiceClientFactory(ClientFactory):
1005
def buildProtocol(self, addr):
1006
prot = self.protocol(False)