Coverage for grm\plugin\_base.py: 0%
243 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-10 14:44 +0900
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-10 14:44 +0900
2#
3# spyne - Copyright (C) Spyne contributors.
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18#
20import gc, logging
21logger = logging.getLogger('spyne')
23from time import time
24from copy import copy
25from collections import deque, namedtuple, defaultdict
27from spyne.const import MIN_GC_INTERVAL
28from spyne.const.xml_ns import DEFAULT_NS
30from spyne.util.oset import oset
32class BODY_STYLE_WRAPPED: pass
33class BODY_STYLE_EMPTY: pass
34class BODY_STYLE_BARE: pass
35class BODY_STYLE_OUT_BARE: pass
38_LAST_GC_RUN = 0.0
41# When spyne.server.twisted gets imported, this type gets a static method named
42# `from_twisted_address`. Dark magic.
43Address = namedtuple("Address", ["type", "host", "port"])
46class _add_address_types():
47 Address.TCP4 = 'TCP4'
48 Address.TCP6 = 'TCP6'
49 Address.UDP4 = 'UDP4'
50 Address.UDP6 = 'UDP6'
51 def address_str(self):
52 return ":".join((self.type, self.host, str(self.port)))
53 Address.__str__ = address_str
56class AuxMethodContext(object):
57 """Generic object that holds information specific to auxiliary methods"""
58 def __init__(self, parent, error):
59 self.parent = parent
60 """Primary context that this method was bound to."""
62 self.error = error
63 """Error from primary context (if any)."""
66class TransportContext(object):
67 """Generic object that holds transport-specific context information"""
68 def __init__(self, parent, transport, type=None):
69 self.parent = parent
70 """The MethodContext this object belongs to"""
72 self.itself = transport
73 """The transport itself; i.e. a ServerBase instance."""
75 self.type = type
76 """The protocol the transport uses."""
78 self.app = transport.app
80 self.request_encoding = None
81 """General purpose variable to hold the string identifier of a request
82 encoding. It's nowadays usually 'utf-8', especially with http data"""
84 self.remote_addr = None
85 """The address of the other end of the connection."""
87 self.sessid = ''
88 """The session id."""
91class ProtocolContext(object):
92 """Generic object that holds protocol-specific context information"""
93 def __init__(self, parent, transport, type=None):
94 self.parent = parent
95 """The MethodContext this object belongs to"""
97 self.itself = transport
98 """The protocol itself as passed to the `Application` init. This is a
99 `ProtocolBase` instance."""
101 self.type = type
102 """The protocol the transport uses."""
104 self._subctx = defaultdict(
105 lambda: self.__class__(parent, transport, type))
107 def __getitem__(self, item):
108 return self._subctx[item]
111class EventContext(object):
112 """Generic object that holds event-specific context information"""
113 def __init__(self, parent, event_id=None):
114 self.parent = parent
115 self.event_id = event_id
118class MethodContext(object):
119 """The base class for all RPC Contexts. Holds all information about the
120 current state of execution of a remote procedure call.
121 """
123 SERVER = type("SERVER", (object,), {})
124 CLIENT = type("CLIENT", (object,), {})
126 frozen = False
128 def copy(self):
129 retval = copy(self)
131 if retval.transport is not None:
132 retval.transport.parent = retval
133 if retval.inprot_ctx is not None:
134 retval.inprot_ctx.parent = retval
135 if retval.outprot_ctx is not None:
136 retval.outprot_ctx.parent = retval
137 if retval.event is not None:
138 retval.event.parent = retval
139 if retval.aux is not None:
140 retval.aux.parent = retval
142 return retval
144 @property
145 def method_name(self):
146 """The public name of the method the ``method_request_string`` was
147 matched to.
148 """
150 if self.descriptor is None:
151 return None
152 else:
153 return self.descriptor.name
155 def __init__(self, transport, way):
156 # metadata
157 self.call_start = time()
158 """The time the rpc operation was initiated in seconds-since-epoch
159 format.
161 Useful for benchmarking purposes."""
163 self.call_end = None
164 """The time the rpc operation was completed in seconds-since-epoch
165 format.
167 Useful for benchmarking purposes."""
169 self.is_closed = False
171 self.app = transport.app
172 """The parent application."""
174 self.udc = None
175 """The user defined context. Use it to your liking."""
177 self.transport = TransportContext(self, transport)
178 """The transport-specific context. Transport implementors can use this
179 to their liking."""
181 self.outprot_ctx = None
182 """The output-protocol-specific context. Protocol implementors can use
183 this to their liking."""
185 if self.app.out_protocol is not None:
186 self.outprot_ctx = self.app.out_protocol.get_context(self, transport)
188 self.inprot_ctx = None
189 """The input-protocol-specific context. Protocol implementors can use
190 this to their liking."""
192 if self.app.in_protocol is not None:
193 self.inprot_ctx = self.app.in_protocol.get_context(self, transport)
195 self.protocol = None
196 """The protocol-specific context. This points to the in_protocol when an
197 incoming message is being processed and out_protocol when an outgoing
198 message is being processed."""
200 if way is MethodContext.SERVER:
201 self.protocol = self.inprot_ctx
202 elif way is MethodContext.CLIENT:
203 self.protocol = self.outprot_ctx
204 else:
205 raise ValueError(way)
207 self.event = EventContext(self)
208 """Event-specific context. Use this as you want, preferably only in
209 events, as you'd probably want to separate the event data from the
210 method data."""
212 self.aux = None
213 """Auxiliary-method specific context. You can use this to share data
214 between auxiliary sessions. This is not set in primary contexts.
215 """
217 self.method_request_string = None
218 """This is used to decide which native method to call. It is set by
219 the protocol classes."""
221 self.files = []
222 """List of stuff to be closed when closing this context. Anything that
223 has a close() callable can go in."""
225 self.__descriptor = None
227 #
228 # Input
229 #
231 # stream
232 self.in_string = None
233 """Incoming bytestream as a sequence of ``str`` or ``bytes`` instances."""
235 # parsed
236 self.in_document = None
237 """Incoming document, what you get when you parse the incoming stream."""
238 self.in_header_doc = None
239 """Incoming header document of the request."""
240 self.in_body_doc = None
241 """Incoming body document of the request."""
243 # native
244 self.in_error = None
245 """Native python error object. If this is set, either there was a
246 parsing error or the incoming document was representing an exception.
247 """
248 self.in_header = None
249 """Deserialized incoming header -- a native object."""
250 self.in_object = None
251 """In the request (i.e. server) case, this contains the function
252 argument sequence for the function in the service definition class.
253 In the response (i.e. client) case, this contains the object returned
254 by the remote procedure call.
256 It's always a sequence of objects:
257 * ``[None]`` when the function has no output (client)/input (server)
258 types.
259 * A single-element list that wraps the return value when the
260 function has one return type defined,
261 * A tuple of return values in case of the function having more than
262 one return value.
264 The order of the argument sequence is in line with
265 ``self.descriptor.in_message._type_info.keys()``.
266 """
268 #
269 # Output
270 #
272 # native
273 self.out_object = None
274 """In the response (i.e. server) case, this contains the native python
275 object(s) returned by the function in the service definition class.
276 In the request (i.e. client) case, this contains the function arguments
277 passed to the function call wrapper.
279 It's always a sequence of objects:
280 * ``[None]`` when the function has no output (server)/input (client)
281 types.
282 * A single-element list that wraps the return value when the
283 function has one return type defined,
284 * A tuple of return values in case of the function having more than
285 one return value.
287 The order of the argument sequence is in line with
288 ``self.descriptor.out_message._type_info.keys()``.
289 """
290 self.out_header = None
291 """Native python object set by the function in the service definition
292 class."""
293 self.out_error = None
294 """Native exception thrown by the function in the service definition
295 class."""
297 # parsed
298 self.out_body_doc = None
299 """Serialized body object."""
300 self.out_header_doc = None
301 """Serialized header object."""
302 self.out_document = None
303 """out_body_doc and out_header_doc wrapped in the outgoing envelope"""
305 # stream
306 self.out_string = None
307 """The pull interface to the outgoing bytestream. It's a sequence of
308 strings (which could also be a generator)."""
310 self.out_stream = None
311 """The push interface to the outgoing bytestream. It's a file-like
312 object."""
314 #self.out_stream = None
315 #"""The push interface to the outgoing bytestream. It's a file-like
316 #object."""
318 self.function = None
319 """The callable of the user code."""
321 self.locale = None
322 """The locale the request will use when needed for things like date
323 formatting, html rendering and such."""
325 self.in_protocol = transport.app.in_protocol
326 """The protocol that will be used to (de)serialize incoming input"""
328 self.out_protocol = transport.app.out_protocol
329 """The protocol that will be used to (de)serialize outgoing input"""
331 self.frozen = True
332 """When this is set, no new attribute can be added to this class
333 instance. This is mostly for internal use.
334 """
336 self.app.event_manager.fire_event("method_context_created", self)
338 def get_descriptor(self):
339 return self.__descriptor
341 def set_descriptor(self, descriptor):
342 self.__descriptor = descriptor
343 self.function = descriptor.function
345 descriptor = property(get_descriptor, set_descriptor)
346 """The :class:``MethodDescriptor`` object representing the current method.
347 It is only set when the incoming request was successfully mapped to a method
348 in the public interface. The contents of this property should not be changed
349 by the user code.
350 """
352 # Deprecated. Use self.descriptor.service_class.
353 @property
354 def service_class(self):
355 if self.descriptor is not None:
356 return self.descriptor.service_class
358 def __setattr__(self, k, v):
359 if not self.frozen or k in self.__dict__ or k in \
360 ('descriptor', 'out_protocol'):
361 object.__setattr__(self, k, v)
362 else:
363 raise ValueError("use the udc member for storing arbitrary data "
364 "in the method context")
366 def __repr__(self):
367 retval = deque()
368 for k, v in self.__dict__.items():
369 if isinstance(v, dict):
370 ret = deque(['{'])
371 items = sorted(v.items())
372 for k2, v2 in items:
373 ret.append('\t\t%r: %r,' % (k2, v2))
374 ret.append('\t}')
375 ret = '\n'.join(ret)
376 retval.append("\n\t%s=%s" % (k, ret))
377 else:
378 retval.append("\n\t%s=%r" % (k, v))
380 retval.append('\n)')
382 return ''.join((self.__class__.__name__, '(', ', '.join(retval), ')'))
384 def close(self):
385 global _LAST_GC_RUN
387 self.call_end = time()
388 self.app.event_manager.fire_event("method_context_closed", self)
389 for f in self.files:
390 f.close()
392 self.is_closed = True
394 # this is important to have file descriptors returned in a timely manner
395 t = time()
396 if (t - _LAST_GC_RUN) > MIN_GC_INTERVAL:
397 gc.collect()
399 dt = (time() - t)
400 _LAST_GC_RUN = t
402 logger.debug("gc.collect() took around %dms.", round(dt, 2) * 1000)
404 def set_out_protocol(self, what):
405 self._out_protocol = what
407 def get_out_protocol(self):
408 return self._out_protocol
410 out_protocol = property(get_out_protocol, set_out_protocol)
413class MethodDescriptor(object):
414 """This class represents the method signature of an exposed service. It is
415 produced by the :func:`spyne.decorator.srpc` decorator.
416 """
418 def __init__(self, function, in_message, out_message, doc,
419 is_callback=False, is_async=False, mtom=False, in_header=None,
420 out_header=None, faults=None,
421 port_type=None, no_ctx=False, udp=None, class_key=None,
422 aux=None, patterns=None, body_style=None, args=None,
423 operation_name=None, no_self=None, translations=None, when=None,
424 in_message_name_override=True, out_message_name_override=True,
425 service_class=None, href=None):
427 self.__real_function = function
428 """The original callable for the user code."""
430 self.reset_function()
432 self.operation_name = operation_name
433 """The base name of an operation without the request suffix, as
434 generated by the ``@srpc`` decorator."""
436 self.in_message = in_message
437 """A :class:`spyne.model.complex.ComplexModel` subclass that defines the
438 input signature of the user function and that was automatically
439 generated by the ``@srpc`` decorator."""
441 self.name = None
442 """The public name of the function. Equals to the type_name of the
443 in_message."""
445 if body_style is BODY_STYLE_BARE:
446 self.name = in_message.Attributes.sub_name
448 if self.name is None:
449 self.name = self.in_message.get_type_name()
451 self.out_message = out_message
452 """A :class:`spyne.model.complex.ComplexModel` subclass that defines the
453 output signature of the user function and that was automatically
454 generated by the ``@srpc`` decorator."""
456 self.doc = doc
457 """The function docstring."""
459 # these are not working, so they are not documented.
460 self.is_callback = is_callback
461 self.is_async = is_async
462 self.mtom = mtom
463 #"""Flag to indicate whether to use MTOM transport with SOAP."""
464 self.port_type = port_type
465 #"""The portType this function belongs to."""
467 self.in_header = in_header
468 """An iterable of :class:`spyne.model.complex.ComplexModel`
469 subclasses to denote the types of header objects that this method can
470 accept."""
472 self.out_header = out_header
473 """An iterable of :class:`spyne.model.complex.ComplexModel`
474 subclasses to denote the types of header objects that this method can
475 emit along with its return value."""
477 self.faults = faults
478 """An iterable of :class:`spyne.model.fault.Fault` subclasses to denote
479 the types of exceptions that this method can throw."""
481 self.no_ctx = no_ctx
482 """no_ctx: Boolean flag to denote whether the user code gets an
483 implicit :class:`spyne.MethodContext` instance as first argument."""
485 self.udp = udp
486 """Short for "User Defined Properties", this is just an arbitrary python
487 object set by the user to pass arbitrary metadata via the ``@srpc``
488 decorator."""
490 self.class_key = class_key
491 """ The identifier of this method in its parent
492 :class:`spyne.service.ServiceBase` subclass."""
494 self.aux = aux
495 """Value to indicate what kind of auxiliary method this is. (None means
496 primary)
498 Primary methods block the request as long as they're running. Their
499 return values are returned to the client. Auxiliary ones execute
500 asyncronously after the primary method returns, and their return values
501 are ignored by the rpc layer.
502 """
504 self.patterns = patterns
505 """This list stores patterns which will match this callable using
506 various elements of the request protocol.
508 Currently, the only object supported here is the
509 :class:`spyne.protocol.http.HttpPattern` object.
510 """
512 self.body_style = body_style
513 """One of (BODY_STYLE_EMPTY, BODY_STYLE_BARE, BODY_STYLE_WRAPPED)."""
515 self.args = args
516 """A sequence of the names of the exposed arguments, or None."""
518 # FIXME: docstring yo.
519 self.no_self = no_self
520 """FIXME: docstring yo."""
522 self.service_class = service_class
523 """The ServiceBase subclass the method belongs to, if there's any."""
525 self.parent_class = None
526 """The ComplexModel subclass the method belongs to. Only set for @mrpc
527 methods."""
529 # HATEOAS Stuff
530 self.translations = translations
531 """None or a dict of locale-translation pairs."""
533 self.href = href
534 """None or a dict of locale-translation pairs."""
536 self.when = when
537 """None or a callable that takes the object instance and returns a
538 boolean value. If true, the object can process that action.
539 """
541 # Method Customizations
542 self.in_message_name_override = in_message_name_override
543 """When False, no mangling of in message name will be performed by later
544 stages of the interface generation. Naturally, it will be up to you to
545 resolve name clashes."""
547 self.out_message_name_override = out_message_name_override
548 """When False, no mangling of out message name will be performed by
549 later stages of the interface generation. Naturally, it will be up to
550 you to resolve name clashes."""
552 def translate(self, locale, default):
553 """
554 :param locale: locale string
555 :param default: default string if no translation found
556 :returns: translated string
557 """
559 if locale is None:
560 locale = 'en_US'
561 if self.translations is not None:
562 return self.translations.get(locale, default)
563 return default
565 @property
566 def key(self):
567 """The function identifier in '{namespace}name' form."""
569 assert not (self.in_message.get_namespace() is DEFAULT_NS)
571 return '{%s}%s' % (
572 self.in_message.get_namespace(), self.in_message.get_type_name())
574 def reset_function(self, val=None):
575 if val != None:
576 self.__real_function = val
577 self.function = self.__real_function
580class EventManager(object):
581 """Spyne supports a simple event system that can be used to have repetitive
582 boilerplate code that has to run for every method call nicely tucked away
583 in one or more event handlers. The popular use-cases include things like
584 database transaction management, logging and measuring performance.
586 Various Spyne components support firing events at various stages during the
587 processing of a request, which are documented in the relevant classes.
589 The classes that support events are:
590 * :class:`spyne.application.Application`
591 * :class:`spyne.service.ServiceBase`
592 * :class:`spyne.protocol._base.ProtocolBase`
593 * :class:`spyne.server.wsgi.WsgiApplication`
595 The events are stored in an ordered set. This means that the events are ran
596 in the order they were added and adding a handler twice does not cause it to
597 run twice.
598 """
600 def __init__(self, parent, handlers={}):
601 self.parent = parent
602 self.handlers = dict(handlers)
604 def add_listener(self, event_name, handler):
605 """Register a handler for the given event name.
607 :param event_name: The event identifier, indicated by the documentation.
608 Usually, this is a string.
609 :param handler: A static python function that receives a single
610 MethodContext argument.
611 """
613 handlers = self.handlers.get(event_name, oset())
614 handlers.add(handler)
615 self.handlers[event_name] = handlers
617 def fire_event(self, event_name, ctx, *args, **kwargs):
618 """Run all the handlers for a given event name.
620 :param event_name: The event identifier, indicated by the documentation.
621 Usually, this is a string.
622 :param ctx: The method context. Event-related data is conventionally
623 stored in ctx.event attribute.
624 """
626 handlers = self.handlers.get(event_name, oset())
627 for handler in handlers:
628 handler(ctx, *args, **kwargs)
631class FakeContext(object):
632 def __init__(self, app=None, descriptor=None,
633 in_object=None, in_error=None, in_document=None, in_string=None,
634 out_object=None, out_error=None, out_document=None, out_string=None,
635 in_protocol=None, out_protocol=None):
636 self.app = app
637 self.descriptor = descriptor
638 self.in_object = in_object
639 self.in_error = in_error
640 self.in_document = in_document
641 self.in_string = in_string
642 self.out_error = out_error
643 self.out_object = out_object
644 self.out_document = out_document
645 self.out_string = out_string
646 self.in_protocol = in_protocol
647 self.out_protocol = out_protocol
649 if self.in_protocol is not None:
650 self.inprot_ctx = self.in_protocol.get_context(self, None)
651 else:
652 self.inprot_ctx = type("ProtocolContext", (object,), {})()
654 from spyne.protocol.html._base import HtmlClothProtocolContext
656 if self.out_protocol is not None:
657 self.outprot_ctx = self.out_protocol.get_context(self, None)
658 else:
659 # The outprot_ctx here must contain properties from ALL tested
660 # protocols' context objects. That's why we use
661 # HtmlClothProtocolContext here, it's just the one with most
662 # attributes.
663 self.outprot_ctx = HtmlClothProtocolContext(self, None)
665 self.protocol = self.outprot_ctx
666 self.transport = type("ProtocolContext", (object,), {})()