src/google/appengine/api/datastore_entities.py (128 lines of code) (raw):
#!/usr/bin/env python
#
# Copyright 2007 Google LLC
#
# Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Classes for common kinds, including Contact, Message, and Event.
Most of these kinds are based on the gd namespace "kinds" from GData:
https://developers.google.com/gdata/docs/1.0/elements
"""
from xml.sax import saxutils
import six
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
from google.appengine.api import datastore_types
class GdKind(datastore.Entity):
"""A base class for gd namespace kinds.
This class contains common logic for all gd namespace kinds. For example,
this class translates datastore `(app id, kind, key)` tuples to tag:
URIs appropriate for use in <key> tags.
"""
HEADER = """<entry xmlns:gd='http://schemas.google.com/g/2005'>
<category scheme='http://schemas.google.com/g/2005#kind'
term='http://schemas.google.com/g/2005#%s' />"""
FOOTER = """
</entry>"""
_kind_properties = set()
_contact_properties = set()
def __init__(self, kind, title, kind_properties, contact_properties=[]):
""" Ctor.
`title` is the name of this particular entity, e.g. Bob Jones or Mom's
Birthday Party.
`kind_properties` is a list of property names that should be included in
this entity's XML encoding as first-class XML elements, instead of
<property> elements. `title` and `content` are added to `kind_properties`
automatically, and may not appear in `contact_properties`.
`contact_properties` is a list of property names that are Keys that point to
`Contact` entities, and should be included in this entity's XML encoding as
`<gd:who>` elements. If a property name is included in both
`kind_properties` and `contact_properties`, it is treated as a Contact
property.
Args:
kind: String
title: String
kind_properties: List of strings
contact_properties: List of string
"""
datastore.Entity.__init__(self, kind)
if not isinstance(title, six.string_types):
raise datastore_errors.BadValueError(
'Expected a string for title; received %s (a %s).' %
(title, datastore_types.typename(title)))
self['title'] = title
self['content'] = ''
self._contact_properties = set(contact_properties)
assert not self._contact_properties.intersection(list(self.keys()))
self._kind_properties = set(kind_properties) - self._contact_properties
self._kind_properties.add('title')
self._kind_properties.add('content')
def _KindPropertiesToXml(self):
""" Convert the properties that are part of this gd kind to XML. For
testability, the XML elements in the output are sorted alphabetically
by property name.
Returns:
String # the XML representation of the gd kind properties
"""
properties = self._kind_properties.intersection(set(self.keys()))
xml = ''
for prop in sorted(properties):
prop_xml = saxutils.quoteattr(prop)[1:-1]
value = self[prop]
has_toxml = (hasattr(value, 'ToXml') or
isinstance(value, list) and hasattr(value[0], 'ToXml'))
for val in self._XmlEscapeValues(prop):
if has_toxml:
xml += '\n %s' % val
else:
xml += '\n <%s>%s</%s>' % (prop_xml, val, prop_xml)
return xml
def _ContactPropertiesToXml(self):
""" Convert this kind's Contact properties kind to XML. For testability,
the XML elements in the output are sorted alphabetically by property name.
Returns:
String # the XML representation of the Contact properties
"""
properties = self._contact_properties.intersection(set(self.keys()))
xml = ''
for prop in sorted(properties):
values = self[prop]
if not isinstance(values, list):
values = [values]
for value in values:
assert isinstance(value, datastore_types.Key)
xml += """
<gd:who rel="http://schemas.google.com/g/2005#%s.%s>
<gd:entryLink href="%s" />
</gd:who>""" % (self.kind().lower(), prop, value.ToTagUri())
return xml
def _LeftoverPropertiesToXml(self):
""" Convert all of this entity's properties that *aren't* part of this gd
kind to XML.
Returns:
String # the XML representation of the leftover properties
"""
leftovers = set(self.keys())
leftovers -= self._kind_properties
leftovers -= self._contact_properties
if leftovers:
return '\n ' + '\n '.join(self._PropertiesToXml(leftovers))
else:
return ''
def ToXml(self):
""" Returns an XML representation of this entity, as a string.
"""
xml = GdKind.HEADER % self.kind().lower()
xml += self._KindPropertiesToXml()
xml += self._ContactPropertiesToXml()
xml += self._LeftoverPropertiesToXml()
xml += GdKind.FOOTER
return xml
class Message(GdKind):
"""A message, such as an email, a discussion group posting, or a comment.
Includes the message title, contents, participants, and other properties.
This is the gd Message kind. See:
https://developers.google.com/gdata/docs/1.0/elements#gdMessageKind
These properties are meaningful. They are all optional.
Property name | Property type | Meaning
--------------- | ------------- | --------
`title` | String | Message subject
`content` | String | Message body
`from` | Contact* | Sender
`to` | Contact* | Primary recipient
`cc` | Contact* | CC recipient
`bcc` | Contact* | BCC recipient
`reply-to` | Contact* | Intended recipient of replies
`link` | Link* | Attachment
`category` | Category* | Tag or label associated with this message
`geoPt` | GeoPt* | Geographic location the message was posted from
`rating` | Rating* | Message rating, as defined by the application
The asterisk (*) means this property may be repeated.
The Contact properties should be Keys of Contact entities. They are
represented in the XML encoding as linked `<gd:who>` elements.
"""
KIND_PROPERTIES = ['title', 'content', 'link', 'category', 'geoPt', 'rating']
CONTACT_PROPERTIES = ['from', 'to', 'cc', 'bcc', 'reply-to']
def __init__(self, title, kind='Message'):
GdKind.__init__(self, kind, title, Message.KIND_PROPERTIES,
Message.CONTACT_PROPERTIES)
class Event(GdKind):
"""A calendar event.
Includes the event title, description, location, organizer, start and end
time, and other details.
This is the gd Event kind. See:
https://developers.google.com/gdata/docs/1.0/elements#gdEventKind
These properties are meaningful. They are all optional.
Property name | Property type | Meaning
---------------- | ---------------| --------
`title` | String | Event name
`content` | String | Event description
`author` | String | The organizer's name
`where` | String* | Human-readable location (not a GeoPt)
`startTime` | Timestamp | Start time
`endTime` | Timestamp | End time
`eventStatus` | String | One of the `Event.Status` values
`link` | Link* | Page with more information
`category` | Category* | Tag or label associated with this event
`attendee` | Contact* | Attendees and other related people
The asterisk (*) means this property may be repeated.
The Contact properties should be Keys of Contact entities. They are
represented in the XML encoding as linked `<gd:who>` elements.
"""
KIND_PROPERTIES = ['title', 'content', 'author', 'where', 'startTime',
'endTime', 'eventStatus', 'link', 'category']
CONTACT_PROPERTIES = ['attendee']
class Status:
CONFIRMED = 'confirmed'
TENTATIVE = 'tentative'
CANCELED = 'canceled'
def __init__(self, title, kind='Event'):
GdKind.__init__(self, kind, title, Event.KIND_PROPERTIES,
Event.CONTACT_PROPERTIES)
def ToXml(self):
"""Returns the XML format of `author`, `gd:where`, `gd:when`, and `gd:eventStatus`.
Overrides `GdKind.ToXml()` to `author`, `gd:where`, `gd:when`, and
`gd:eventStatus`.
"""
xml = GdKind.HEADER % self.kind().lower()
self._kind_properties = set(Contact.KIND_PROPERTIES)
xml += self._KindPropertiesToXml()
if 'author' in self:
xml += """
<author><name>%s</name></author>""" % self['author']
if 'eventStatus' in self:
xml += """
<gd:eventStatus value="http://schemas.google.com/g/2005#event.%s" />""" % (
self['eventStatus'])
if 'where' in self:
lines = ['<gd:where valueString="%s" />' % val
for val in self._XmlEscapeValues('where')]
xml += '\n ' + '\n '.join(lines)
iso_format = '%Y-%m-%dT%H:%M:%S'
xml += '\n <gd:when'
for key in ['startTime', 'endTime']:
if key in self:
xml += ' %s="%s"' % (key, self[key].isoformat())
xml += ' />'
self._kind_properties.update(['author', 'where', 'startTime', 'endTime',
'eventStatus'])
xml += self._ContactPropertiesToXml()
xml += self._LeftoverPropertiesToXml()
xml += GdKind.FOOTER
return xml
class Contact(GdKind):
"""A contact: a person, a venue (e.g., club, restaurant), or an organization.
This is the gd Contact kind. See:
https://developers.google.com/gdata/docs/1.0/elements#gdContactKind
Most of the information about the contact is in the `<gd:contactSection>`
element; see the reference section for that element for details.
These properties are meaningful. They are all optional.
Property name | Property type | Meaning
----------------- | --------------- | ---------
`title` | String | Contact's name
`content` | String | Notes
`email` | Email* | Email address
`geoPt` | GeoPt* | Geographic location
`im` | IM* | IM address
`phoneNumber` | Phonenumber* | Phone number
`postalAddress` | PostalAddress* | Mailing address
`link` | Link* | Link to more information
`category` | Category* | Tag or label associated with this contact
An asterisk (*) means this property may be repeated.
"""
CONTACT_SECTION_HEADER = """
<gd:contactSection>"""
CONTACT_SECTION_FOOTER = """
</gd:contactSection>"""
KIND_PROPERTIES = ['title', 'content', 'link', 'category']
CONTACT_SECTION_PROPERTIES = ['email', 'geoPt', 'im', 'phoneNumber',
'postalAddress']
def __init__(self, title, kind='Contact'):
GdKind.__init__(self, kind, title, Contact.KIND_PROPERTIES)
def ToXml(self):
"""Returns the XML format of `gd:contactSection`.
Overrides `GdKind.ToXml()` to put some properties inside a
`gd:contactSection`.
"""
xml = GdKind.HEADER % self.kind().lower()
self._kind_properties = set(Contact.KIND_PROPERTIES)
xml += self._KindPropertiesToXml()
xml += Contact.CONTACT_SECTION_HEADER
self._kind_properties = set(Contact.CONTACT_SECTION_PROPERTIES)
xml += self._KindPropertiesToXml()
xml += Contact.CONTACT_SECTION_FOOTER
self._kind_properties.update(Contact.KIND_PROPERTIES)
xml += self._LeftoverPropertiesToXml()
xml += GdKind.FOOTER
return xml