# File name: Message.py
# This file is part of: pyxwf
#
# LICENSE
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
# the License for the specific language governing rights and limitations
# under the License.
#
# Alternatively, the contents of this file may be used under the terms
# of the GNU General Public license (the "GPL License"), in which case
# the provisions of GPL License are applicable instead of those above.
#
# FEEDBACK & QUESTIONS
#
# For feedback and questions about pyxwf please e-mail one of the
# authors named in the AUTHORS file.
########################################################################
"""
In PyXWF, we call all content we send over the wire a Message. Messages thus
contain information about their mime type, encoding and of course their payload.
They represent it internally however they like, but must be able to serialize
the payload to a properly encoded bytes object for conversion in a MessageInfo
instance.
"""
import abc, copy
from PyXWF.utils import ET
import PyXWF.utils as utils
import PyXWF.Namespaces as NS
import PyXWF.ContentTypes as ContentTypes
import PyXWF.Errors as Errors
[docs]class Message(object):
"""
Baseclass for any message. For proper function, messages must implement
the :meth:`get_encoded_body` method.
It handles sending the message in a given transaction context if all
properties and methods are set up properly.
*mimetype* is the MIME type according to RFC 2046.
"""
__metaclass__ = abc.ABCMeta
def __init__(self, mimetype, status=Errors.OK, encoding=None):
super(Message, self).__init__()
self._mimetype = mimetype
self._encoding = encoding
self._last_modified = None
self._status = status
@property
[docs] def MIMEType(self):
"""
The internet media type (aka *content type*) of the :class:`~Message`.
"""
return self._mimetype
@property
def Encoding(self):
"""
Encoding (charset in the Content Type context) of the message.
"""
return self._encoding
@Encoding.setter
[docs] def Encoding(self, value):
self._encoding = value
@abc.abstractmethod
[docs] def get_encoded_body(self):
"""
Return the bytes object resembling the contents encoded in the encoding
set up in :attr:`Encoding`.
Derived classes must implement this method.
"""
@property
def StatusCode(self):
return self._status.code
@property
def Status(self):
return self._status
@Status.setter
def Status(self, value):
self._status = value
def __eq__(self, other):
try:
return (self._status.code == other._status.code and
self._encoding == other._encoding and
self._mimetype == other._mimetype and
self.get_encoded_body() == other.get_encoded_body())
except AttributeError:
return NotImplemented
def __ne__(self, other):
result = self == other
if result is NotImplemented:
return result
return not result
def __str__(self):
return """\
{0} {1}
Content-Type: {2}; charset={3}
Last-Modified: {5}
{4}\n""".format(
self._status.code,
self._status.title,
self._mimetype,
self._encoding,
self.get_encoded_body(),
self._last_modified.isoformat() if self._last_modified else "None"
)
def __repr__(self):
return str(self)
[docs]class XMLMessage(Message):
"""
Represent a generic XML message. *doctree* must be a valid lxml Element or
ElementTree. *content_type* must specify the MIME type of the document.
If cleanup_namespaces is True, :func:`lxml.etree.cleanup_namespaces` will be
called on the tree.
"""
def __init__(self, doctree, content_type, cleanup_namespaces=False,
pretty_print=False, force_namespaces={}, **kwargs):
super(XMLMessage, self).__init__(content_type, **kwargs)
self._doctree = doctree
self._pretty_print = pretty_print
if cleanup_namespaces:
try:
# this is only available with lxml backend
ET.cleanup_namespaces(self._doctree)
except AttributeError:
pass
if force_namespaces:
root = self._doctree.getroot()
# this is an ugly hack
nsmap = root.nsmap
nsmap.update(force_namespaces)
newroot = ET.Element(root.tag, attrib=root.attrib, nsmap=nsmap)
newroot.extend(root)
self._doctree = ET.ElementTree(newroot)
"""for prefix, uri in force_namespaces.viewitems():
root.set("{{{0}}}{1}".format("http://www.w3.org/2000/xmlns/", prefix), uri)"""
@property
def DocTree(self):
return self._doctree
@DocTree.setter
def DocTree(self, value):
self._doctree = value
def get_encoded_body(self, **kwargs):
simpleargs = {
"encoding": self.Encoding or "utf-8",
"xml_declaration": "yes",
"pretty_print": self._pretty_print
}
simpleargs.update(kwargs)
return ET.tostring(self.DocTree,
**simpleargs
)
[docs]class XHTMLMessage(XMLMessage):
"""
Represent an XHTML message. *doctree* must be a valid XHTML document tree
as lxml.etree node. Conversion to bytes payload is handled by this class
automatically.
"""
def __init__(self, doctree, minify_namespaces=True,
**kwargs):
myargs = {
"cleanup_namespaces": True
}
myargs.update(kwargs)
super(XHTMLMessage, self).__init__(doctree, ContentTypes.xhtml,
**myargs)
self._minify_namespaces = minify_namespaces
def get_encoded_body(self):
kwargs = {
"doctype": "<!DOCTYPE html>"
}
return super(XHTMLMessage, self).get_encoded_body(**kwargs)
[docs]class HTMLMessage(Message):
"""
Represent an HTML message. *doctree* must be a valid HTML document tree
(the same as the XHTML tree, but without namespaces) as lxml.etree node.
Conversion to bytes payload is handled by this class automatically.
You can specify the HTML version via *version*, which is currently
restricted to `HTML5`.
"""
@classmethod
[docs] def from_xhtml_tree(cls, doctree, version="HTML5", **kwargs):
"""
Return an :class:`~HTMLMessage` instance from the given XHTML *doctree*.
This performs automatic conversion by removing the XHTML namespace from
all elements. Raises :class:`ValueError` if a non-xhtml namespace is
encountered.
"""
doctree = copy.copy(doctree)
utils.XHTMLToHTML(doctree)
try:
# this is only available with lxml backend
ET.cleanup_namespaces(doctree)
except AttributeError:
pass
return cls(doctree, version=version, **kwargs)
def __init__(self, doctree, version="HTML5", pretty_print=False, **kwargs):
if version != "HTML5":
raise ValueError("Invalid HTMLMessage version: {0}".format(version))
super(HTMLMessage, self).__init__(ContentTypes.html, **kwargs)
self._doctree = doctree
self._pretty_print = pretty_print
@property
def DocTree(self):
return self._doctree
@DocTree.setter
def DocTree(self, value):
self._doctree = value
def get_encoded_body(self):
encoding = self.Encoding or "utf-8"
return ET.tostring(self.DocTree,
encoding=encoding,
doctype="<!DOCTYPE html>",
method="html",
pretty_print=self._pretty_print
)
[docs]class TextMessage(Message):
"""
Represent a plain-text message. *contents* must be either a string (which
must be convertible into unicode using the default encoding) or a unicode
instance.
"""
def __init__(self, contents, **kwargs):
super(TextMessage, self).__init__(ContentTypes.plaintext, **kwargs)
self.Contents = contents
@property
def Contents(self):
"""
Contents of the plain text message. Assigning to this property will
convert the assigned value to unicode if neccessary and fail for
anything which does not inherit from str or unicode.
"""
return self._contents
@Contents.setter
[docs] def Contents(self, value):
if isinstance(value, str):
self._contents = value.decode()
elif isinstance(value, unicode):
self._contents = value
else:
raise TypeError("TextMessage contents must be string-like.")
def get_encoded_body(self):
return self._contents.encode(self.Encoding)
[docs]class EmptyMessage(Message):
"""
Represent a message without a body.
"""
def __init__(self, **kwargs):
kwargs.setdefault("status", Errors.NoContent)
super(EmptyMessage, self).__init__(None, **kwargs)
def get_encoded_body(self):
return None