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

20 import gc, logging

21 logger = logging.getLogger('spyne')

22

23 from time import time

24 from copy import copy

25 from collections import deque, namedtuple, defaultdict

26

27 from spyne.const import MIN_GC_INTERVAL

28 from spyne.const.xml_ns import DEFAULT_NS

29

30 from spyne.util.oset import oset

31

32 class BODY_STYLE_WRAPPED: pass

33 class BODY_STYLE_EMPTY: pass

34 class BODY_STYLE_BARE: pass

35 class 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.

43 Address = namedtuple("Address", ["type", "host", "port"])

44

45

46 class _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

56 class 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

66 class 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

91 class 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

111 class 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

118 class 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

413 class 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

580 class 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

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