Package amplee :: Package atompub :: Module collection
[hide private]
[frames] | no frames]

Source Code for Module amplee.atompub.collection

  1  # -*- coding: utf-8 -*- 
  2   
  3  __doc__ = """ 
  4  Collections are the container of member and media resources. 
  5   
  6  Collections are important to amplee as they will map eventually 
  7  to a physical container in the storage. 
  8   
  9  For instance a collection named 'notes' would map to: 
 10   
 11  - a table named 'notes' in a database 
 12  - a directory names 'notes' on the filesystem 
 13  - a bucket named 'notes' on Amazon S3 
 14   
 15  etc. 
 16   
 17  So it is very important to carefully choose the 'name_or_id' parameter 
 18  in the class AtomPubCollection 
 19  """ 
 20   
 21  from bridge import Element, Attribute 
 22  from bridge.common import ATOM10_PREFIX, ATOMPUB_PREFIX, XML_PREFIX, XML_NS, \ 
 23       ATOM10_NS, ATOMPUB_NS 
 24   
 25  from amplee.utils import generate_uuid_uri, get_isodate 
 26  from amplee.atompub.member import EntryMember 
 27  from amplee.error import UnsupportedMediaType 
 28   
29 -class AtomPubCollection(object):
30 - def __init__(self, workspace, name_or_id, title, base_uri, 31 base_edit_uri, base_media_edit_uri=None, xml_attrs=None, 32 member_extension=u'atom', member_media_type=u'application/atom+xml', 33 accept_media_types=None, categories=None, 34 fixed_categories=None, favorite=False):
35 """ 36 Atom Publishing Protocol collection handler. 37 38 Keyword arguments: 39 workspace -- AtomPubWorkspace instance carrying this collection 40 name_or_id -- Name or identifier by which you reference this 41 collection within the store [eg: 'audio'] 42 base_uri -- Base URI of the content used in links and external content 43 base_edit_uri -- Base URI used for the edit links 44 base_media_edit_uri -- Base URI used for the edit-media links. If None will 45 use base_edit_uri. 46 xml_attrs -- dictionary of XML attributes belonging to the 47 xml namespace (http://www.w3.org/XML/1998/namespace). They will be passed 48 to the top level Atom elements (feed, entry, etc.) 49 member_extension -- Extension used for the Atom entries representing 50 the member resource (default to 'atom') 51 member_media_type -- Media type for the Atom entries representing the 52 member resource default to 'application/atom+xml') 53 accept_media_types -- List of a strings of acceptable media-types for 54 this collection 55 categories -- List of bridge.Element instances 56 fixed_categories -- True => 'yes', False => 'no', None => undefined 57 favorite -- if True means thiscollection is the preferred one in the workspace 58 59 Once an instance created you can set on_create, on_update and on_delete 60 attributes to a callback that will be applied before the 61 new member is being attached to the collection. 62 63 If the callback raise amplee.error.ResourceOperationException 64 it will not attach the resource but instead will force the 65 HTTP method handler to returns the error code and message 66 defines in the exception. This allows extra processing on the 67 member and resource before it being stored. 68 69 on_create take two arguments: 70 amplee.atompub.member.* -- newly created or existing member 71 string object -- new resource content 72 returns member, content to be persisted into the store 73 74 on_update takes three arguments: 75 amplee.atompub.member.EntryMember -- existing member in the store 76 amplee.atompub.member.* -- newly created or existing member 77 string object -- new resource content 78 returns member, content to be persisted into the store 79 80 on_delete takes: 81 amplee.atompub.member.* -- existing member 82 returns None 83 84 Becareful when you update a resource, the on_update member provides 85 the existing entry member as well as the new member generated frm the 86 updated content provided. Amplee will not try to be smarter than you. 87 This means that if your callback does nothing at all the existing 88 member will be entirely replaced by the one generated. It would be wise 89 that your callback keeps at least the id and published elements. 90 91 For instance: 92 93 def on_update_cb(existing_member, new_member, new_content): 94 # Ensure that atom:id stays the same throughout of the 95 # life of the resource 96 if new_member.atom.has_element('id', ATOM10_NS): 97 del new_member.atom.id 98 Element(u'id', content=unicode(existing_member.atom.id), 99 prefix=new_member.atom.xml_prefix, namespace=new_member.atom.xml_ns, 100 parent=new_member.atom) 101 102 if new_member.atom.has_element('published', ATOM10_NS): 103 del new_member.atom.published 104 Element(u'published', content=unicode(existing_member.atom.published), 105 prefix=new_member.atom.xml_prefix, namespace=new_member.atom.xml_ns, 106 parent=new_member.atom) 107 108 new_member.member_id = existing_member.member_id 109 new_member.media_id = existing_member.media_id 110 111 return new_member, new_content 112 113 Although this seems heavy work it allows the developer to have a very fine 114 granularity over resources. 115 """ 116 self.workspace = workspace 117 self.workspace.collections.append(self) 118 self.name_or_id = name_or_id 119 self.base_uri = base_uri 120 xml_attrs = xml_attrs or {} 121 self.xml_base = xml_attrs.get('base', None) 122 self.xml_id = xml_attrs.get('id', None) 123 self.xml_lang = xml_attrs.get('lang', None) 124 self.title = title 125 self.categories = categories 126 self.favorite = favorite 127 self.fixed_categories = fixed_categories 128 self.member_extension = member_extension 129 self.member_media_type = member_media_type 130 self.base_edit_uri = base_edit_uri 131 if not base_media_edit_uri: 132 base_media_edit_uri = base_edit_uri 133 self.base_media_edit_uri = base_media_edit_uri 134 self.members = {} 135 136 if not accept_media_types: 137 accept_media_types = [] 138 if isinstance(accept_media_types, basestring): 139 accept_media_types = [accept_media_types] 140 accept_media_types.append('application/atom+xml') 141 self.accept_media_types = accept_media_types 142 143 # Set those to Python callables and they will be called during the 144 # processing of each request. 145 self.on_create = None 146 self.on_update = None 147 self.on_delete = None
148
149 - def store_container(self):
150 """ 151 Returns the store carrying this collection 152 """ 153 return self.workspace.service.store
154 store = property(store_container) 155
156 - def attach(self, member, member_content, 157 member_id=None, member_path=None, 158 media_id=None, media_path=None, 159 media_content=None, check_media_type=True):
160 """ 161 Add a member to this collection by 162 * adding it to the store 163 * adding it to the self.members dictionary 164 165 You are not forced to pass the resource content through this 166 method if you prefer storing it in a different location 167 without using amplee. 168 169 This method will throw a amplee.error.UnsupportedMediaType 170 exception if check_media_type is True and the media type of 171 the media resource does not fall into the allowed list. 172 173 Keyword arguments: 174 member -- amplee.atompub.member.* instance 175 member_content -- Content to be persisted into the storage. 176 Usually an XML string of the Atom entry 177 member_id -- Internal id used to reference this member. 178 Usually equals to the resource_name + collection.member_extension 179 member_path -- Path under this member is stored 180 media_id -- Internal id used to reference the resource associated 181 to this member. 182 media_path -- Path under the media resource is stored 183 media_content -- Resource content 184 check_media_type -- If False the resource media type will not be 185 checked against collection.accept_media_types 186 """ 187 if not member_id: 188 member_id = member.member_id 189 if not media_id: 190 media_id = member.media_id 191 192 if media_content and check_media_type: 193 if member.media_type not in self.accept_media_types: 194 raise UnsupportedMediaType 195 196 if not member_path: 197 member_path = self.get_meta_data_path(member_id) 198 199 self.store.add_meta_data(member_path, member_content, 200 media_type=self.member_media_type, 201 member_id=member_id, 202 media_id=media_id, 203 collection_name=self.name_or_id) 204 if media_content: 205 if not media_path: 206 media_path = self.get_content_path(media_id) 207 208 self.store.add_content(media_path, media_content, 209 media_type=member.media_type, 210 member_id=member_id, 211 media_id=media_id, 212 collection_name=self.name_or_id) 213 214 self.members[member_id] = member
215
216 - def prune(self, member_id=None, media_id=None):
217 """ 218 Removes a member from a collection and the underlying stor. 219 Removes only objects passed in the id parameters. 220 221 Keyword arguments: 222 member_id -- Identifier of the member 223 media_id -- Identifier of the media resource 224 """ 225 if member_id: 226 if member_id in self.members: 227 del self.members[member_id] 228 path = self.get_meta_data_path(member_id) 229 self.store.remove_meta_data(path) 230 231 if media_id: 232 path = self.get_content_path(media_id) 233 self.store.remove_content(path)
234
235 - def convert_id(self, id):
236 """ 237 Take the parameter and returns a tuple such as (member_id, media_id). 238 239 Keyword arguments: 240 id -- Can be either member_id or media_id 241 242 """ 243 ext = '.%s' % self.member_extension 244 pos_ext = 0 - len(ext) 245 if id[pos_ext:] == ext: 246 return (id, id[:pos_ext]) 247 return ('%s.%s' % (id, self.member_extension), id)
248
249 - def contains(self, path):
250 """ 251 Does a path belong to the store 252 """ 253 return self.store.exists(path)
254
255 - def get_meta_data_path(self, id):
256 """ 257 Constructs and returns the path to the member 258 pointed by the id parameter. 259 """ 260 return self.store.get_meta_data_path(self.name_or_id, id)
261
262 - def get_content_path(self, id):
263 """ 264 Constructs and returns the path to the resource 265 pointed by the id parameter. 266 """ 267 return self.store.get_content_path(self.name_or_id, id)
268
269 - def get_meta_data(self, path):
270 """ 271 Returns an object stored as meta-data information in the store 272 for a resource. 273 274 Does not check the existence of the resource. 275 """ 276 return self.store.fetch_meta_data(path)
277
278 - def get_content(self, path):
279 """ 280 Returns the content of the media resource or None. 281 282 Does not check the existence of the resource. 283 """ 284 return self.store.fetch_content(path)
285
286 - def to_feed(self, prefix=ATOM10_PREFIX, namespace=ATOM10_NS):
287 """ 288 Transforms and returns the collection into an bridge.Element instance 289 """ 290 feed = Element(u'feed', prefix=prefix, namespace=namespace) 291 uuid = unicode(generate_uuid_uri(seed=self.base_uri.encode('utf-8'))) 292 Element(u'id', content=uuid, prefix=prefix, namespace=namespace, parent=feed) 293 Element(u'updated', content=get_isodate(), 294 prefix=prefix, namespace=namespace, parent=feed) 295 title = Element(u'title', content=unicode(self.title), attributes={u'type': u'text'}, 296 prefix=prefix, namespace=namespace, parent=feed) 297 if self.xml_base: 298 Attribute(u'base', self.xml_base, prefix=XML_PREFIX, 299 namespace=XML_NS, parent=feed) 300 feed.entry = [] 301 for name in self.members: 302 member = self.members[name] 303 member.atom.parent = feed 304 feed.xml_children.append(member.atom) 305 feed.entry.append(member.atom) 306 307 return feed
308 feed = property(to_feed) 309
310 - def to_collection(self, prefix=ATOMPUB_PREFIX, namespace=ATOMPUB_NS):
311 """ 312 Tranforms and returns the collection into a bridge.Element instance 313 """ 314 collection = Element(u'collection', attributes={u'href': self.base_edit_uri}, 315 prefix=prefix, namespace=namespace) 316 if self.xml_base: 317 Attribute(u'base', self.xml_base, prefix=XML_PREFIX, 318 namespace=XML_NS, parent=collection) 319 Element(u'title', content=self.title, attributes={u'type': u'text'}, 320 prefix=ATOM10_PREFIX, namespace=ATOM10_NS, parent=collection) 321 Element(u'accept', content=u','.join(self.accept_media_types), 322 prefix=prefix, namespace=namespace, parent=collection) 323 categories = self.categories or [] 324 if categories: 325 attr = {} 326 if self.fixed_categories: 327 if self.fixed_categories: fixed = u'yes' 328 else: fixed = u'no' 329 attr[u'fixed'] = fixed 330 cats = Element(u'categories', attributes=attr, prefix=prefix, 331 namespace=namespace, parent=collection) 332 for category in categories: 333 Element(u'category', attributes={u'term': unicode(category.term)}, 334 prefix=ATOM10_PREFIX, namespace=ATOM10_NS, parent=cats) 335 336 return collection
337 collection = property(to_collection) 338
339 - def reload_members(self):
340 """ 341 Reloads every existing members into self.members. 342 Call this at server startup to refresh the collection. 343 Careful as this could be a fairly long process. 344 """ 345 members = self.store.list_members(self.name_or_id, self.member_extension) 346 member_ext = None 347 if self.member_extension: 348 member_ext = '.%s' % self.member_extension 349 for member_id in members: 350 member_path = members[member_id]['path'] 351 data = self.get_meta_data(member_path) 352 353 if data: 354 entry = Element.load(data) 355 member = EntryMember(self, id=member_id, atom=entry) 356 if member_ext: 357 pos = member.member_id.rfind(member_ext) 358 if pos == -1: 359 member.media_id = member.member_id 360 else: 361 member.media_id = member.member_id[:pos] 362 else: 363 member.media_id = member.member_id 364 self.members[member_id] = member
365