root/tags/amplee-0.5.0/amplee/loader.py

Revision 432, 21.5 kB (checked in by sylvain, 1 year ago)

Different code cleanup

Line 
1 # -*- coding: utf-8 -*-
2
3 __doc__ = """Atom Publising Store loader
4
5 Synopsis
6 --------
7
8 The process of creating an Atom Publishing store is
9 a repetitive task that is common to all projects.
10
11 The loader module offers the possibility to automate
12 this task by putting required information in a
13 configuration file and let the `loader`function
14 generates the store from the configuration settings.
15
16 Consider the following:
17
18 >>> from amplee.loader import loader
19 >>> service, config = loader('/my/config.cfg')
20
21 The ``service`` object returned is an instance of
22 ``amplee.atompub.service.AtomPubService`` and is your
23 reference to all the related objects.
24
25 The ``config`` object is an instance of
26 ``amplee.loader.Config`` and is the representation
27 of your configuration file.
28
29 """
30
31 import sys
32 import os.path
33 import imp
34 import re
35 import ConfigParser
36
37 from amplee.atompub.store import *
38 from amplee.atompub.service import *
39 from amplee.atompub.workspace import *
40 from amplee.atompub.collection import *
41 from amplee.handler import MemberType
42 from amplee.utils import strip_list
43
44 from bridge import Element as E
45 from bridge.common import ATOM10_PREFIX, ATOM10_NS
46
47 __all__ = ['loader', 'Config']
48
49 class Config(object):
50     """Represents an INI file as a tree of Config instances
51
52     Say you have ``test.conf``:
53     
54     [general]
55     verbose = True
56
57     >>> from amplee.loader import Config
58     >>> config = Config()
59     >>> config.from_ini('test.conf')
60     >>> print config.general.verbose
61     >>> True
62
63     Each loaded value is an unicode object except the following strings:
64     'True' --> True
65     'False' --> False
66     'None' --> None
67
68     An empty string will not be transformed into a None though.
69     """
70     def from_ini(self, filepath, encoding='ISO-8859-1'):
71         """Loads the configuration file into the current instance
72         of `Config`.
73
74         The ``filepath`` is the path to the configuration file.
75
76         The ``encoding`` indicates how to decode each value.
77         """
78         config = ConfigParser.ConfigParser()
79         config.readfp(file(filepath, 'rb'))
80
81         for section in config.sections():
82             section_prop = Config()
83             section_prop.keys = []
84             setattr(self, section, section_prop)
85             for option in config.options(section):
86                 section_prop.keys.append(option)
87                 value = self._convert_type(config.get(section, option).decode(encoding))
88                 setattr(section_prop, option, value)
89
90     def get(self, section, option, default=None, raise_error=False):
91         """Retrieve the value for a particular node in the configuration tree.
92
93         >>> print config.get('general', 'verbose', default=1)
94
95         but also:
96         
97         >>> try:
98         >>>    config.get('general', 'verbose', raise_error=True)
99         >>> except AttributeError:
100         >>>    print 'Could not find it'
101
102         The ``section`` is the name of the parent node of which ``option``
103         is a child.
104
105         The ``option`` is the name of the node to lookup.
106
107         You can provide a ``default`` value when the node was not found.
108         This applies when ``raise_error`` is ``False``, which is the default.
109
110         If you set ``raise_error`` to ``True`` an ``AttributeError``
111         exception will be raised if the node is not found.
112         
113         """
114         if hasattr(self, section):
115             obj = getattr(self, section, None)
116             if obj and hasattr(obj, option):
117                 return getattr(obj, option, default)
118
119         if raise_error:
120             raise AttributeError, "%s %s" % (section, option)
121
122         return default
123
124     def _convert_type(self, value):
125         """Do dummy conversion of the string 'True', 'False' and 'None'
126         into their object equivalent"""
127         if value == 'True':
128             return True
129         elif value == 'False':
130             return False
131         elif value == 'None':
132             return None
133         return value
134
135     def get_section(self, section):
136         return getattr(self, section, None)
137
138     def get_all_values(self, section, capitalize=False):
139         """Returns a dictionnary of all nodes which parent is the section.
140
141         The ``section`` is the name of the parent node to look for.
142         The ``capitalize`` parameter indicates if the keys of the
143         returned dictionnary should start with a cpital letter.
144         """
145         values = {}
146         if hasattr(self, section):
147             obj = getattr(self, section, None)
148             for key in obj.keys:
149                 if hasattr(obj, key):
150                     if capitalize:
151                         values[key.capitalize()] = getattr(obj, key, None)
152                     else:
153                         values[key] = getattr(obj, key, None)
154
155         return values
156    
157 _module_callable_regex = re.compile('module:(.*),callable:(.*)')
158    
159 def init_storage(config, storage, base_path=None):
160     """Initializes a storage as described in the configuration object.
161
162     The ``storage`` string value is looked up within the ``config``
163     object and then based upon which a storage object is constructed.
164
165     The optional ``base_path`` is only required when your configuration
166     settings use relative path in their values.
167     """
168     if storage.startswith('fs_storage'):
169         from amplee.storage import storefs
170         root_dir = config.get(storage, 'base_path')
171         if base_path and not os.path.isabs(root_dir):
172             root_dir = os.path.join(base_path, root_dir)
173         enable_lock = config.get(storage, 'enable_lock', False)
174         encoding = config.get(storage, 'encoding', 'utf-8')
175         return storefs.FilesystemStorage(root_dir, enable_lock, encoding=encoding)
176     elif storage.startswith('svn_storage'):
177         from amplee.storage import storesvn
178         repository_uri = config.get(storage, 'repository_uri')
179         working_copy_path = config.get(storage, 'working_copy_path')
180         if base_path:
181             working_copy_path = os.path.join(base_path, working_copy_path)
182         username = config.get(storage, 'username', '')
183         password = config.get(storage, 'password', '')
184         if username == '': username = None
185         if password == '': password = None
186         return storesvn.SubversionStorage(repository_uri, working_copy_path, username, password)
187     elif storage.startswith('zodb_storage'):
188         from ZODB import DB
189         from amplee.storage import storezodb
190         fs_type = config.get(storage, 'fs_type', 'filestorage')
191         if fs_type == 'filestorage':
192             from ZODB import FileStorage
193             storage_path = config.get(storage, 'fs_path')
194             if base_path:
195                 storage_path = os.path.join(base_path, storage_path)
196             db = DB(FileStorage.FileStorage(storage_path))
197         elif fs_type == 'clientstorage':
198             from ZEO import ClientStorage
199             db = DB(ClientStorage.ClientStorage(config.get(storage, 'address')))
200         return storezodb.ZODBStorage(db, config.get(storage, 'top_level_node_name'))
201     elif storage.startswith('dejavu_storage'):
202         from amplee.storage import storedejavu
203         capitalize = config.get(storage, 'capitalize', True)
204         conf = config.get_all_values(storage, capitalize=capitalize)
205         return storedejavu.DejavuStorage(config.get(storage, 'db_type'), conf)
206     elif storage.startswith('sqlite_storage'):
207         from amplee.storage import storesqlite3
208         return storesqlite3.SQLite3Storage(connection=config.get(storage, 'connection'))
209     elif storage.startswith('s3_storage'):
210         from amplee.storage import stores3
211         aws_access_key_id = config.get(storage, 'access_key')
212         aws_secret_access_key = config.get(storage, 'private_key')
213         unique_prefix = config.get(storage, 'bucket_unique_prefix')
214         encoding = config.get(storage, 'encoding', 'utf-8')
215         separator = config.get(storage, 'separator', '_')
216         aws_key_lookup = config.get(storage, 'aws_key_lookup', None)
217         aws_file_path = None
218         if aws_key_lookup:
219             aws_key_lookup = _load_callable(aws_key_lookup, base_path)
220             aws_file_path = config.get(storage, 'aws_file_path', None)
221         return stores3.S3Storage(aws_access_key_id, aws_secret_access_key,
222                                  unique_prefix, encoding=encoding,
223                                  separator=separator, aws_key_lookup=aws_key_lookup,
224                                  aws_file_path=aws_file_path)
225     elif storage.startswith('tar_storage'):
226         from amplee.storage import storetar
227         root_dir = config.get(storage, 'base_path')
228         if base_path and not os.path.isabs(root_dir):
229             root_dir = os.path.join(base_path, root_dir)
230         compression = config.get(storage, 'compression', 'gz')
231         encoding = config.get(storage, 'encoding', 'utf-8')
232         return storetar.TarFileStorage(root_dir, compression=compression, encoding=encoding)
233     elif storage.startswith('nonbuiltin_storage'):
234         storage_module = config.get(storage, 'storage_module')
235         storage_class = config.get(storage, 'storage_class')
236         directory, name = os.path.split(storage_module)
237         if base_path:
238             directory = os.path.join(base_path, directory)
239            
240         file, filename, description = imp.find_module(name, [directory])
241         mod = imp.load_module(name, file, filename, description)
242         mod_class = getattr(mod_class, storage_class, None)
243         if not mod_class:
244             raise RuntimeError, "could not load non built-in storage %s" % storage
245
246         return mod_class(config=getattr(config, storage, None))
247    
248 def init_workspaces(config, service, base_path=None):
249     """Initializes the Atom Publishing Protocol workspaces from the
250     provided service.
251     """
252     workspaces = strip_list(config.get('store', 'workspaces', '').split(','))
253     for workspace_name in workspaces:
254         name = config.get(workspace_name, 'name')
255         title = config.get(workspace_name, 'title')
256         xml_attrs = get_xml_attribs(config, workspace_name)
257         workspace = AtomPubWorkspace(service, name, title=title, xml_attrs=xml_attrs)
258         init_collections(config, workspace_name, workspace, base_path)
259
260 def get_xml_attribs(config, section_name):
261     """Returns a dictionary of xml attributes with their associated value."""
262     xml_attrs = config.get(section_name, 'xml_attrs', None)
263     if xml_attrs:
264         attrs = strip_list(xml_attrs.split(';'))
265         xml_attrs = {}
266         for attr in attrs:
267             key, value = attr.split(',')
268             xml_attrs[key] = value
269     return xml_attrs or {}
270    
271 def init_collections(config, workspace_name, workspace, base_path=None):
272     """Initializes the Atom Publishing Protocol collections from the
273     provided workspace.
274     """
275     collections = strip_list(config.get(workspace_name, 'collections', '').split(','))
276     for collection_name in collections:
277         base_uri = config.get(collection_name, 'base_uri', u'')
278         id = config.get(collection_name, 'name', raise_error=True)
279         title = config.get(collection_name, 'title', raise_error=True)
280         base_edit_uri = config.get(collection_name, 'base_edit_uri')
281         base_media_edit_uri = config.get(collection_name, 'base_media_edit_uri', None)
282         accept_media_types = config.get(collection_name, 'accept_media_types', [])
283         editable_media_types = config.get(collection_name, 'editable_media_types', None)
284         member_media_type = config.get(collection_name, 'member_media_type', u'application/atom+xml;type=entry')
285         member_extension = config.get(collection_name, 'member_media_extension', u'atom')
286         if accept_media_types:
287             accept_media_types = strip_list(accept_media_types.split(','))
288         if editable_media_types:
289             editable_media_types = strip_list(editable_media_types.split(','))
290         favorite = config.get(collection_name, 'favorite', False)
291         fixed_categories = config.get(collection_name, 'fixed_categories', False)
292         xml_attrs = get_xml_attribs(config, collection_name)
293
294         categories = config.get(collection_name, 'categories', [])
295         cats = []
296         if categories:
297             for category in strip_list(categories.split(',')):
298                 category = E(u'category', attributes={u'term': category},
299                              prefix=ATOM10_PREFIX, namespace=ATOM10_NS)
300                 cats.append(category)
301                
302         collection = AtomPubCollection(workspace, name_or_id=id, title=title,
303                                        xml_attrs=xml_attrs, favorite=favorite,
304                                        base_uri=base_uri, base_edit_uri=base_edit_uri,
305                                        base_media_edit_uri=base_media_edit_uri,
306                                        accept_media_types=accept_media_types,
307                                        fixed_categories=fixed_categories,
308                                        categories=cats,
309                                        member_media_type=member_media_type,
310                                        member_extension=member_extension,
311                                        editable_media_types=editable_media_types)
312         read_only = config.get(collection_name, 'read_only', False)
313         collection.is_read_only = read_only
314
315         feed_module = config.get(collection_name, 'feed_handler_module', None)
316         feed_class = config.get(collection_name, 'feed_handler_class', None)
317        
318         if feed_module and feed_class:
319             directory, name = os.path.split(feed_module)
320             if base_path:
321                 directory = os.path.join(base_path, directory)
322             file, filename, description = imp.find_module(name, [directory])
323
324             mod = imp.load_module(name, file, filename, description)
325
326             class_obj = getattr(mod, class_name, None)
327             if not class_obj:
328                 raise ImportError, "cannot import %s from %s" % (feed_class, feed_module)
329
330             collection.feed_handler = class_obj(collection)
331
332         with_cache = config.get(collection_name, 'enable_cache', True)
333         if with_cache:
334             collection.enable_cache()
335        
336         init_handlers(config, collection_name, collection, base_path)
337         autoreload = config.get(collection_name, 'reload_members', False)
338         if autoreload:
339             collection.reload_members()
340            
341         collection.feed_handler.set(collection.feed)
342         entry_processor = config.get(collection_name, 'public_feed_entry_processor', None)
343         if entry_processor:
344             entry_processor = _load_callable(entry_processor, base_path)
345         post_processor = config.get(collection_name, 'public_feed_post_processor', None)
346         if post_processor:
347             post_processor = _load_callable(post_processor, base_path)
348         collection.feed_handler.init_public(collection=collection,
349                                             entry_processor=entry_processor,
350                                             post_processor=post_processor)
351         collection.feed_handler.set_public_xslt(config.get(collection_name,
352                                                            'public_feed_xslt_path', None))
353         collection.feed_handler.set_collection_xslt(config.get(collection_name,
354                                                                'collection_feed_xslt_path', None))
355    
356            
357 def _load_callable(value, base_path=None):
358     match = _module_callable_regex.match(value)
359     path, cb = match.groups()
360     directory, name = os.path.split(path)
361     if base_path:
362         directory = os.path.join(base_path, directory)
363     file, filename, description = imp.find_module(name, [directory])
364     mod = imp.load_module(name, file, filename, description)
365     if hasattr(mod, cb):
366         return getattr(mod, cb)
367     else:
368         raise ImportError, "cannot import name %s from %s" % (cb, name)
369    
370 def handle_member_type(config, handler_name, base_path=None):
371     """Initializes the member type instances."""
372     member_type = config.get(handler_name, 'member_type', None)
373     mt = {}
374     if member_type:
375         values = config.get_all_values(member_type)
376         for key in values:
377             value = values[key]
378             if isinstance(value, basestring) and not value.startswith('module:'):
379                 mt[key] = value
380             elif not isinstance(value, basestring):
381                 mt[key] = value
382             else:
383                 mt[key] = _load_callable(value, base_path)
384     return mt
385
386 def init_handlers(config, collection_name, collection, base_path=None):
387     """Initializes the handlers that will take handle one media-type
388     for the collection they are registered with.
389     """
390     handlers = strip_list(config.get(collection_name, 'handlers', '').split(','))
391     for handler_name in handlers:
392         # Load the member module
393         member_module = config.get(handler_name, 'member_module', raise_error=True)
394         member_class = config.get(handler_name, 'member_class', raise_error=True)
395         if member_module.startswith('builtin:'):
396             name = member_module[8:]
397             file, filename, description = None, None, None
398             # what about MacOSX?
399             if sys.platform == "win32":
400                 # Ugly hack to handle Windows
401                 for prefix in sys.path:
402                     mod_path = "%s" % member_module[8:]
403                     try:
404                         prefix = "%s\\amplee\\atompub\\member" % prefix
405                         file, filename, description = imp.find_module(mod_path, [prefix])
406                         break
407                     except ImportError:
408                         pass
409                 if (file, filename, description) == (None, None, None):
410                     raise ImportError, "could not import %s" % mod_path
411             else:
412                 mod_path = "amplee/atompub/member/%s" % member_module[8:]
413                 file, filename, description = imp.find_module(os.path.normpath(mod_path))
414         else:
415             directory, name = os.path.split(member_module)
416             if base_path:
417                 directory = os.path.join(base_path, directory)
418             file, filename, description = imp.find_module(name, [directory])
419
420         mod = imp.load_module(name, file, filename, description)
421        
422         class_obj = getattr(mod, member_class, None)
423         if not class_obj:
424             raise ImportError, "cannot import %s from %s" % (member_class, member_module)
425        
426         params = handle_member_type(config, handler_name, base_path)
427         media_type = config.get(handler_name, 'media_type', raise_error=True)
428         member_type = MemberType(media_type, class_obj, params=params)
429        
430         # Load the handler module
431         handler_module = config.get(handler_name, 'handler_module', raise_error=True)
432         handler_class = config.get(handler_name, 'handler_class', raise_error=True)
433         directory, name = os.path.split(handler_module)
434         if base_path:
435             directory = os.path.join(base_path, directory)
436         file, filename, description = imp.find_module(name, [directory])
437         mod = imp.load_module(name, file, filename, description)
438
439         class_obj = getattr(mod, handler_class, None)
440         if not class_obj:
441             raise ImportError, "cannot import %s from %s" % (handler_class, handler_module)
442
443         class_inst = class_obj(member_type)
444        
445         # Now let's register this handler to the collection
446         collection.register_handler(class_inst)
447            
448 def loader(conf_path, encoding='ISO-8859-1', base_path=None):
449     """
450     Creates the structure of an APP store following settings
451     provided by the configuration file. It returns the created
452     service instance as well as the configuration instance.
453
454     The ``conf_path`` is the path to the configuration settings
455     The ``encoding`` indicates how to encode each value of the settings
456     The ``base_path``, if provided, will be prepended to all the
457       values which require a path
458     """
459     config = Config()
460     config.from_ini(conf_path, encoding=encoding)
461
462     base_path = os.path.normpath(base_path)
463     member_storage = media_storage = None
464     # Initiating the storage
465     storage = config.get('store', 'member_storage')
466     if storage:
467         member_storage = init_storage(config, storage, base_path)
468
469     distinct_media_storage = False
470     media_storage = config.get('store', 'media_storage')
471     if media_storage:
472         if storage != media_storage:
473             distinct_media_storage = True
474             media_storage = init_storage(config, media_storage, base_path)
475         else:
476             media_storage = member_storage
477
478     proxied_member_storage = config.get('store', 'member_storage_memcached', None)
479     if proxied_member_storage:
480         from amplee.storage.storememcache import StorageMemcache
481         member_storage = StorageMemcache(strip_list(proxied_member_storage.split(',')),
482                                          storage=member_storage)
483
484     if distinct_media_storage:
485         proxied_media_storage = config.get('store', 'media_storage_memcached', None)
486         if proxied_media_storage:
487             from amplee.storage.storememcache import StorageMemcache
488             media_storage = StorageMemcache(strip_list(proxied_media_storage.split(',')),
489                                             storage=media_storage)
490    
491     # Initiate the store
492     enable_lock = config.get('store', 'enable_lock', True)
493     store = AtomPubStore(member_storage, media_storage=media_storage,
494                          enable_lock=enable_lock)
495
496     # Initiate the service
497     xml_attrs = get_xml_attribs(config, "service")
498     service = AtomPubService(store, xml_attrs=xml_attrs)
499     service.set_xslt(config.get('service', 'xslt_path', None))
500    
501     # Initiate the workspaces and their collections
502     init_workspaces(config, service, base_path)
503
504     return service, config
Note: See TracBrowser for help on using the browser.