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

1 

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# 

19 

20import gc, logging 

21logger = logging.getLogger('spyne') 

22 

23from time import time 

24from copy import copy 

25from collections import deque, namedtuple, defaultdict 

26 

27from spyne.const import MIN_GC_INTERVAL 

28from spyne.const.xml_ns import DEFAULT_NS 

29 

30from spyne.util.oset import oset 

31 

32class BODY_STYLE_WRAPPED: pass 

33class BODY_STYLE_EMPTY: pass 

34class BODY_STYLE_BARE: pass 

35class BODY_STYLE_OUT_BARE: pass 

36 

37 

38_LAST_GC_RUN = 0.0 

39 

40 

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"]) 

44 

45 

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 

54 

55 

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.""" 

61 

62 self.error = error 

63 """Error from primary context (if any).""" 

64 

65 

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""" 

71 

72 self.itself = transport 

73 """The transport itself; i.e. a ServerBase instance.""" 

74 

75 self.type = type 

76 """The protocol the transport uses.""" 

77 

78 self.app = transport.app 

79 

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""" 

83 

84 self.remote_addr = None 

85 """The address of the other end of the connection.""" 

86 

87 self.sessid = '' 

88 """The session id.""" 

89 

90 

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""" 

96 

97 self.itself = transport 

98 """The protocol itself as passed to the `Application` init. This is a 

99 `ProtocolBase` instance.""" 

100 

101 self.type = type 

102 """The protocol the transport uses.""" 

103 

104 self._subctx = defaultdict( 

105 lambda: self.__class__(parent, transport, type)) 

106 

107 def __getitem__(self, item): 

108 return self._subctx[item] 

109 

110 

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 

116 

117 

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 """ 

122 

123 SERVER = type("SERVER", (object,), {}) 

124 CLIENT = type("CLIENT", (object,), {}) 

125 

126 frozen = False 

127 

128 def copy(self): 

129 retval = copy(self) 

130 

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 

141 

142 return retval 

143 

144 @property 

145 def method_name(self): 

146 """The public name of the method the ``method_request_string`` was 

147 matched to. 

148 """ 

149 

150 if self.descriptor is None: 

151 return None 

152 else: 

153 return self.descriptor.name 

154 

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. 

160 

161 Useful for benchmarking purposes.""" 

162 

163 self.call_end = None 

164 """The time the rpc operation was completed in seconds-since-epoch 

165 format. 

166 

167 Useful for benchmarking purposes.""" 

168 

169 self.is_closed = False 

170 

171 self.app = transport.app 

172 """The parent application.""" 

173 

174 self.udc = None 

175 """The user defined context. Use it to your liking.""" 

176 

177 self.transport = TransportContext(self, transport) 

178 """The transport-specific context. Transport implementors can use this 

179 to their liking.""" 

180 

181 self.outprot_ctx = None 

182 """The output-protocol-specific context. Protocol implementors can use 

183 this to their liking.""" 

184 

185 if self.app.out_protocol is not None: 

186 self.outprot_ctx = self.app.out_protocol.get_context(self, transport) 

187 

188 self.inprot_ctx = None 

189 """The input-protocol-specific context. Protocol implementors can use 

190 this to their liking.""" 

191 

192 if self.app.in_protocol is not None: 

193 self.inprot_ctx = self.app.in_protocol.get_context(self, transport) 

194 

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.""" 

199 

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) 

206 

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.""" 

211 

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 """ 

216 

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.""" 

220 

221 self.files = [] 

222 """List of stuff to be closed when closing this context. Anything that 

223 has a close() callable can go in.""" 

224 

225 self.__descriptor = None 

226 

227 # 

228 # Input 

229 # 

230 

231 # stream 

232 self.in_string = None 

233 """Incoming bytestream as a sequence of ``str`` or ``bytes`` instances.""" 

234 

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.""" 

242 

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. 

255 

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. 

263 

264 The order of the argument sequence is in line with 

265 ``self.descriptor.in_message._type_info.keys()``. 

266 """ 

267 

268 # 

269 # Output 

270 # 

271 

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. 

278 

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. 

286 

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.""" 

296 

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""" 

304 

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).""" 

309 

310 self.out_stream = None 

311 """The push interface to the outgoing bytestream. It's a file-like 

312 object.""" 

313 

314 #self.out_stream = None 

315 #"""The push interface to the outgoing bytestream. It's a file-like 

316 #object.""" 

317 

318 self.function = None 

319 """The callable of the user code.""" 

320 

321 self.locale = None 

322 """The locale the request will use when needed for things like date 

323 formatting, html rendering and such.""" 

324 

325 self.in_protocol = transport.app.in_protocol 

326 """The protocol that will be used to (de)serialize incoming input""" 

327 

328 self.out_protocol = transport.app.out_protocol 

329 """The protocol that will be used to (de)serialize outgoing input""" 

330 

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 """ 

335 

336 self.app.event_manager.fire_event("method_context_created", self) 

337 

338 def get_descriptor(self): 

339 return self.__descriptor 

340 

341 def set_descriptor(self, descriptor): 

342 self.__descriptor = descriptor 

343 self.function = descriptor.function 

344 

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 """ 

351 

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 

357 

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") 

365 

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)) 

379 

380 retval.append('\n)') 

381 

382 return ''.join((self.__class__.__name__, '(', ', '.join(retval), ')')) 

383 

384 def close(self): 

385 global _LAST_GC_RUN 

386 

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() 

391 

392 self.is_closed = True 

393 

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() 

398 

399 dt = (time() - t) 

400 _LAST_GC_RUN = t 

401 

402 logger.debug("gc.collect() took around %dms.", round(dt, 2) * 1000) 

403 

404 def set_out_protocol(self, what): 

405 self._out_protocol = what 

406 

407 def get_out_protocol(self): 

408 return self._out_protocol 

409 

410 out_protocol = property(get_out_protocol, set_out_protocol) 

411 

412 

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 """ 

417 

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): 

426 

427 self.__real_function = function 

428 """The original callable for the user code.""" 

429 

430 self.reset_function() 

431 

432 self.operation_name = operation_name 

433 """The base name of an operation without the request suffix, as 

434 generated by the ``@srpc`` decorator.""" 

435 

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.""" 

440 

441 self.name = None 

442 """The public name of the function. Equals to the type_name of the 

443 in_message.""" 

444 

445 if body_style is BODY_STYLE_BARE: 

446 self.name = in_message.Attributes.sub_name 

447 

448 if self.name is None: 

449 self.name = self.in_message.get_type_name() 

450 

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.""" 

455 

456 self.doc = doc 

457 """The function docstring.""" 

458 

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.""" 

466 

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.""" 

471 

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.""" 

476 

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.""" 

480 

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.""" 

484 

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.""" 

489 

490 self.class_key = class_key 

491 """ The identifier of this method in its parent 

492 :class:`spyne.service.ServiceBase` subclass.""" 

493 

494 self.aux = aux 

495 """Value to indicate what kind of auxiliary method this is. (None means 

496 primary) 

497 

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 """ 

503 

504 self.patterns = patterns 

505 """This list stores patterns which will match this callable using 

506 various elements of the request protocol. 

507 

508 Currently, the only object supported here is the 

509 :class:`spyne.protocol.http.HttpPattern` object. 

510 """ 

511 

512 self.body_style = body_style 

513 """One of (BODY_STYLE_EMPTY, BODY_STYLE_BARE, BODY_STYLE_WRAPPED).""" 

514 

515 self.args = args 

516 """A sequence of the names of the exposed arguments, or None.""" 

517 

518 # FIXME: docstring yo. 

519 self.no_self = no_self 

520 """FIXME: docstring yo.""" 

521 

522 self.service_class = service_class 

523 """The ServiceBase subclass the method belongs to, if there's any.""" 

524 

525 self.parent_class = None 

526 """The ComplexModel subclass the method belongs to. Only set for @mrpc 

527 methods.""" 

528 

529 # HATEOAS Stuff 

530 self.translations = translations 

531 """None or a dict of locale-translation pairs.""" 

532 

533 self.href = href 

534 """None or a dict of locale-translation pairs.""" 

535 

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 """ 

540 

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.""" 

546 

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.""" 

551 

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 """ 

558 

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 

564 

565 @property 

566 def key(self): 

567 """The function identifier in '{namespace}name' form.""" 

568 

569 assert not (self.in_message.get_namespace() is DEFAULT_NS) 

570 

571 return '{%s}%s' % ( 

572 self.in_message.get_namespace(), self.in_message.get_type_name()) 

573 

574 def reset_function(self, val=None): 

575 if val != None: 

576 self.__real_function = val 

577 self.function = self.__real_function 

578 

579 

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. 

585 

586 Various Spyne components support firing events at various stages during the 

587 processing of a request, which are documented in the relevant classes. 

588 

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` 

594 

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 """ 

599 

600 def __init__(self, parent, handlers={}): 

601 self.parent = parent 

602 self.handlers = dict(handlers) 

603 

604 def add_listener(self, event_name, handler): 

605 """Register a handler for the given event name. 

606 

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 """ 

612 

613 handlers = self.handlers.get(event_name, oset()) 

614 handlers.add(handler) 

615 self.handlers[event_name] = handlers 

616 

617 def fire_event(self, event_name, ctx, *args, **kwargs): 

618 """Run all the handlers for a given event name. 

619 

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 """ 

625 

626 handlers = self.handlers.get(event_name, oset()) 

627 for handler in handlers: 

628 handler(ctx, *args, **kwargs) 

629 

630 

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 

648 

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,), {})() 

653 

654 from spyne.protocol.html._base import HtmlClothProtocolContext 

655 

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) 

664 

665 self.protocol = self.outprot_ctx 

666 self.transport = type("ProtocolContext", (object,), {})()