pax_global_header00006660000000000000000000000064120363357010014512gustar00rootroot0000000000000052 comment=f4501dd7cd6b3addc0e3fa86dbf2c24d66789f9a blogofile-0.8b1/000077500000000000000000000000001203633570100135465ustar00rootroot00000000000000blogofile-0.8b1/.gitignore000066400000000000000000000004641203633570100155420ustar00rootroot00000000000000*~ \#* .#* *.pyc *.ropeproject/* /Blogofile.egg-info/* /build/* /dist/* /blogofile/.ropeproject* /blogofile/site_init/*.zip /blogofile/test_py2/* _site/* docs/_build /.tox/* /MANIFEST /.coverage /blogofile/tests/plugin/plugin_test/blogofile_plugin_test.egg-info/* /blogofile/tests/plugin/plugin_test/build/* blogofile-0.8b1/CHANGES.txt000066400000000000000000000032571203633570100153660ustar00rootroot000000000000000.8b1 ===== If you've been using the ``plugins`` branch from GitHub but haven't updated in a while you should take note of the following. Bug Fixes --------- - Fix a typo in the README. See https://github.com/EnigmaCurry/blogofile/pull/127 - Fix an issue with the template lookup order whereby user's templates failed to override plugin site_src template of the same name. See https://github.com/EnigmaCurry/blogofile/issues/126 - Fix permalinks in RSS feeds for Apple Mail. See https://github.com/EnigmaCurry/blogofile/pull/114 - Fix a class name reference error in the Jinga2 template loader. See https://github.com/EnigmaCurry/blogofile/issues/105 Features -------- - The documentation source files have been moved into the project repository. They are built and rendered at http://docs.blogofile.com/ thanks to the readthedocs.org service. - The init sub-command syntax and functionality has changed; see ``blogofile help init``. - The configuration system has been refactored. The default configuration settings are now in the ``default_config.py`` module. - As a result of the refactoring of the initialization function, and the configuration system, the ``site_init`` directory has been eliminated. - Improved Unicode handling in slugs. See https://github.com/EnigmaCurry/blogofile/issues/124 - The codebase has been unified for Python 2.6, 2.7 and 3.2 (no 2to3 or 3to2 conversion required). - The command line completion feature has been removed so as to avoid maintaining a bundled version of the ``argparse`` library. ``argparse`` is included in the standard library for Python 2.7 and 3.2+. ``setup.py`` will install it from PyPI for Python 2.6. blogofile-0.8b1/CONTRIBUTORS.txt000066400000000000000000000017501203633570100162470ustar00rootroot00000000000000-*- coding: utf-8 -*- Blogofile Contributors ====================== Blogofile and the blogofile_blog plugin were created by: Ryan McGuire Since May-2012 the project has been maintained and developed by: Doug Latornell The following people have contributed code, documentation, bug reports, enlightened discussion, etc. to the project: Nick Craig-Wood Sean Davis Nicolas Dumazet Ron DuPlain John Feminella Thomas Gstädtner Seth de l'Isle Jaemok Jeong Tshepang Lekhonkhobe Macha Julian Melville Mike Pirnat Ravikiran Rajagopal Brandon Craig Rhodes Wasil Sergejczyk Brandon Stafford Klein Stéphane Manuel Strehl Ashish Tonse Thomas Weißschuh W. Matthew Wilson David Wolever Peter Zsoldos Ant Zucaro Thank you, one and all! The above list is derived primarily from the git log. If you think your name should be there and its not, or you want it changed somehow (add your email address for example), please submit a pull request, or email Doug. blogofile-0.8b1/LICENSE.txt000066400000000000000000000067241203633570100154020ustar00rootroot00000000000000################################################################################ ### Blogofile is written by Ryan McGuire (EnigmaCurry.com) ################################################################################ Blogofile is released under an MIT style license. However, this author chooses to not enforce any violations of the license. Any contributors to Blogofile must agree that they relinquish their code to Ryan McGuire to be released under the MIT license with the understanding that he has no intention of enforcing the copyright and permission notice republication requirement. ################################################################################ ### MIT License ################################################################################ Copyright (c) 2009 Ryan McGuire Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ################################################################################ ### A statement about Free Software ################################################################################ I believe in free software. For me, this also means that I do not believe in copyrights, trademarks, patents, nor any other government enforced monopoly privilege. You probably downloaded this software from my website. By doing so, you did not first enter into any sort of agreement or contract with me. There were no rules established before you downloaded it -- I simply offered the file on my website and you simply downloaded it. I am not your master -- it is immoral for me to use any (initiated) force against you, including the force of government. However, I am a human being. I enjoy praise, attribution, and other rewards for my work. If you find this software useful, I appreciate comments to that effect. If you find this software _very_ useful, I appreciate gifts. If you make useful modifications to this software, I appreciate patches. If you incorporate this software into a product of your own, I like to know about it, and I like being mentioned in your product's documentation or website. But again, I am not your master -- It is wrong for me to force you to do anything. I can only ask. I have chosen to release this software under the MIT license for the convenience of contributers and users who are accustomed to working with software released under traditional open source licenses, but I pledge that I will not pursue any legal recourse for violations of the MIT license. -- Ryan McGuire, aka "EnigmaCurry" blogofile-0.8b1/MANIFEST.in000066400000000000000000000002211203633570100152770ustar00rootroot00000000000000include README.rst include CHANGES.txt include LICENSE.txt include CONTRIBUTORS.txt include requirements/*.txt recursive-include converters *.py blogofile-0.8b1/README.rst000066400000000000000000000026741203633570100152460ustar00rootroot00000000000000Blogofile is a static website compiler that lets you use various template libraries (Mako, Jinja2), and various markup languages (reStructuredText, Markdown, Textile) to create sites that can be served from any web server you like. Version 0.8 of Blogofile breaks out the core static site compiler and gives it a plugin interface. That allows features like the blog engine that was Blogofile's original raison d`être to be built on top of the core. `blogofile_blog`_ is a blog engine plugin created by the Blogofile developers. With it installed you get a simple blog engine that requires no database and no special hosting environment. You customize a set of Mako templates, create posts in reStructuredText, Markdown, or Textile, (or even plain HTML) and blogofile generates your entire blog as plain HTML, CSS, images, and Atom/RSS feeds which you can then upload to any old web server you like. No CGI or scripting environment is needed on the server. See the `Blogofile website`_ for an example of a Blogofile-generated site that includes a blog, and check out the `project docs`_ for a quick-start guide, and detailed usage instructions. Or, if you're the "just get it done sort", create a virtualenv, and dive in with:: pip install -U Blogofile pip install -U blogofile_blog .. _blogofile_blog: http://pypi.python.org/pypi/blogofile_blog/ .. _Blogofile website: http://www.blogofile.com/ .. _project docs: http://blogofile.readthedocs.org/en/latest/ blogofile-0.8b1/RELEASING.rst000066400000000000000000000027511203633570100156160ustar00rootroot00000000000000Releasing Blogofile ******************* Checklist for doing a release of Blogofile and the blogifile_blog plugin. * Do a platform test via tox:: $ tox -r * Ensure that all features of the release are documented (audit CHANGES.txt). * Ensure that the docs build:: $ cd docs $ make clean html * Write a release post for blogofile.com. * Change version number in: * Blogofile: * blogofile/__init__.py * docs/conf.py * CHANGES.txt * blogofile_blog: * blogofile_blog/__init__.py * blogofile.com: * _config.py * Test upload to PyPI:: $ python setup.py sdist register -r testpypi upload -r testpypi for both Blogofile and blogofile_blog. * Test installation in a pristine virtualenv:: $ virtualenv --python=python3.2 blogofile-testrel $ cd blogofile-testrel $ source bin/activate $ pip install --extra-index-url http://testpypi.python.org/pypi \ "Blogofile==" $ pip install --extra-index-url http://testpypi.python.org/pypi \ "blogofile_blog==" and then test building a site, even if it's the sample blog via:: $ blogofile init test_blog blog $ blogofile build -s test_blog * Create release tags in Blogofile and blogofile_blog repos. * Release to PyPI:: $ python setup.py sdist register upload for both Blogofile and blogofile_blog. * Publish the release post for blogofile.com. * Announce to blogofile-discuss group/maillist. * Annouce to Google+. * Announce to Twitter. blogofile-0.8b1/blogofile/000077500000000000000000000000001203633570100155105ustar00rootroot00000000000000blogofile-0.8b1/blogofile/__init__.py000066400000000000000000000004651203633570100176260ustar00rootroot00000000000000# -*- coding: utf-8 -*- """This is Blogofile -- http://www.Blogofile.com A static website compiler and blog engine, written and extensible in Python. Please take a moment to read LICENSE.txt. It's short. """ __author__ = "Ryan McGuire, Doug Latornell, and the Blogofile Contributors" __version__ = '0.8b1' blogofile-0.8b1/blogofile/cache.py000066400000000000000000000072741203633570100171370ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys from . import __version__ as bf_version class Cache(dict): """A cache object used for attatching things we want to remember This works like a normal object, attributes that are undefined raise an AttributeError >>> c = Cache() >>> c.whatever = "whatever" >>> c.whatever 'whatever' >>> c.section.subsection.attribute = "whatever" Traceback (most recent call last): ... AttributeError: 'Cache' object has no attribute 'section' """ def __init__(self, **kw): dict.__init__(self, kw) self.__dict__ = self class HierarchicalCache(Cache): """A cache object used for attatching things we want to remember This works differently than a normal object, attributes that are undefined do *not* raise an AttributeError but are silently created as an additional HierarchicalCache object. >>> c = HierarchicalCache() >>> c.whatever = "whatever" >>> c.whatever 'whatever' >>> c.section.subsection.attribute = "whatever" >>> c.section.subsection.attribute 'whatever' >>> c.sub.d['one'].value.stuff = "whatever" >>> c.sub.d.one.value.stuff 'whatever' >>> c.sub.d['one'].value.stuff 'whatever' >>> c.sub.d['one.value.stuff'] 'whatever' >>> c.sub.d['one.value.stuff'] = "whatever2" >>> c.sub.d.one.value.stuff 'whatever2' >>> list(c.sub.d.one.value.items()) [('stuff', 'whatever2')] >>> "doesn't have this" in c.sub.d False """ def __getattr__(self, attr): if not attr.startswith("_") and \ "(" not in attr and \ "[" not in attr and \ attr != "trait_names": c = HierarchicalCache() Cache.__setitem__(self, attr, c) return c else: raise AttributeError def __getitem__(self, item): if(type(item) == slice or not hasattr(item, "split")): raise TypeError("HierarchicalCache objects are not indexable nor " "sliceable. If you were expecting another object " "here, a parent cache object may be inproperly " "configured.") dotted_parts = item.split(".") try: c = self.__getattribute__(dotted_parts[0]) except AttributeError: c = self.__getattr__(item) for dotted_part in dotted_parts[1:]: c = getattr(c, dotted_part) return c def __call__(self): raise TypeError("HierarchicalCache objects are not callable. If " "you were expecting this to be a method, a " "parent cache object may be inproperly configured.") def __setitem__(self, key, item): c = self try: try: dotted_parts = key.split(".") except AttributeError: return if len(dotted_parts) > 1: c = self.__getitem__(".".join(dotted_parts[:-1])) key = dotted_parts[-1] finally: Cache.__setitem__(c, key, item) #The main blogofile cache object, transfers state between templates bf = HierarchicalCache() def setup_bf(): global bf sys.modules['blogofile_bf'] = bf bf.__version__ = bf_version bf.cache = sys.modules['blogofile.cache'] def reset_bf(assign_modules=True): global bf bf.clear() setup_bf() if assign_modules: from . import config, util, server, filter, controller, template bf.config = config bf.util = util bf.server = server bf.filter = filter bf.controller = controller bf.template = template return bf setup_bf() blogofile-0.8b1/blogofile/config.py000066400000000000000000000053201203633570100173270ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Load the default config, and the user's _config.py file, and provides the interface to the config. """ __author__ = "Ryan McGuire (ryan@enigmacurry.com)" import os import logging import sys import re from . import cache from . import controller from . import plugin from . import filter as _filter from .cache import HierarchicalCache as HC # TODO: This import MUST come after cache is imported; that's too brittle! import blogofile_bf as bf logger = logging.getLogger("blogofile.config") bf.config = sys.modules['blogofile.config'] site = cache.HierarchicalCache() controllers = cache.HierarchicalCache() filters = cache.HierarchicalCache() plugins = cache.HierarchicalCache() templates = cache.HierarchicalCache() default_config_path = os.path.join( os.path.dirname(__file__), "default_config.py") def init_interactive(args=None): """Reset the blogofile cache objects, and load the configuration. The user's _config.py is always loaded from the current directory because we assume that the function/method that calls this has already changed to the directory specified by the --src-dir command line option. """ # TODO: What purpose does cache.reset_bf() serve? Looks like a # testing hook. cache.reset_bf() try: _load_config("_config.py") except IOError: sys.stderr.write("No configuration found in source dir: {0}\n" .format(args.src_dir)) sys.stderr.write("Want to make a new site? Try `blogofile init`\n") sys.exit(1) def _load_config(user_config_path): """Load the configuration. Strategy: 1) Load the default config 2) Load the plugins 3) Load the site filters and controllers 4) Load the user's config. 5) Compile file ignore pattern regular expressions This establishes sane defaults that the user can override as they wish. config is exec-ed from Python modules into locals(), then updated into globals(). """ with open(default_config_path) as f: exec(f.read()) plugin.load_plugins() _filter.preload_filters() controller.load_controllers(namespace=bf.config.controllers) try: with open(user_config_path) as f: exec(f.read()) except IOError: raise _compile_file_ignore_patterns() globals().update(locals()) def _compile_file_ignore_patterns(): site.compiled_file_ignore_patterns = [] for p in site.file_ignore_patterns: if hasattr(p, "findall"): # probably already a compiled regex. site.compiled_file_ignore_patterns.append(p) else: site.compiled_file_ignore_patterns.append( re.compile(p, re.IGNORECASE)) blogofile-0.8b1/blogofile/controller.py000066400000000000000000000207651203633570100202570ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Controllers Blogofile controllers reside in the user's _controllers directory and can generate content for a site. Controllers can either be standalone .py files, or they can be modules. Every controller has a contract to provide the following: * a run() method, which accepts no arguments. * A dictionary called "config" containing the following information: * name - The human friendly name for the controller. * author - The name or group responsible for writing the controller. * description - A brief description of what the controller does. * url - The URL where the controller is hosted. * priority - The default priority to determine sequence of execution This is optional, if not provided, it will default to 50. Controllers with higher priorities get run sooner than ones with lower priorities. Example controller (either a standalone .py file or __init__.py inside a module): meta = { "name" : "My Controller", "description" : "Does cool stuff", "author" : "Joe Programmer", "url" : "http://www.yoururl.com/my-controller" } config = {"some_config_option" : "some_default_setting", "priority" : 90.0} def run(): do_whatever_it_needs_to() Users can configure a controller in _config.py: #To enable the controller (default is always disabled): controller.name_of_controller.enabled = True #To set the priority: controllers.name_of_controller.priority = 40 #To set a controller specific setting: controllers.name_of_controller.some_config_option = "whatever" Settings set in _config.py always override any default configuration for the controller. """ from __future__ import print_function import sys import os import operator import logging import imp from .cache import bf bf.controller = sys.modules['blogofile.controller'] logger = logging.getLogger("blogofile.controller") default_controller_config = {"priority": 50.0, "enabled": False} def __find_controller_names(directory="_controllers"): if(not os.path.isdir(directory)): return # Find all the standalone .py files and modules in the _controllers dir for fn in os.listdir(directory): p = os.path.join(directory, fn) if os.path.isfile(p): if fn.endswith(".py"): yield fn[:-3] elif os.path.isdir(p): if os.path.isfile(os.path.join(p, "__init__.py")): yield fn def init_controllers(namespace): """Controllers have an optional init method that runs before the run method""" # Prune the configured controllers to only those that have a # discoverable implementation: actual_controllers = {} for name, controller in namespace.items(): if "mod" in controller and type(controller.mod).__name__ == "module": actual_controllers[name] = controller elif "enabled" in controller and controller.enabled: # Throw a fatal error if an enabled controller is unimplemented print("Cannot find requested controller: {0}".format(name)) print("Build aborted.") sys.exit(1) # Initialize all the actual controllers: for name, controller in sorted(actual_controllers.items(), key=lambda c: c[1].priority): if not controller.mod.__initialized: try: init_method = controller.mod.init except AttributeError: controller.mod.__initialized = True continue else: init_method() def load_controller(name, namespace, directory="_controllers", defaults={}, is_plugin=False): """Load a single controller by name. """ logger.debug("loading controller: {0}" .format(bf.util.path_join(directory, name))) # Don't generate pyc files in the _controllers directory try: initial_dont_write_bytecode = sys.dont_write_bytecode except KeyError: initial_dont_write_bytecode = False try: try: sys.dont_write_bytecode = True controller = imp.load_module( name, *imp.find_module(name, [directory])) controller.__initialized = False logger.debug("found controller: {0} - {1}" .format(name, controller)) except (ImportError,) as e: logger.error( "Cannot import controller : {0} ({1})".format(name, e)) raise # Remember the actual imported module namespace[name].mod = controller # Load the blogofile defaults for controllers: for k, v in list(default_controller_config.items()): namespace[name][k] = v # Load provided defaults: for k, v in list(defaults.items()): namespace[name][k] = v if not is_plugin: # Load any of the controller defined defaults: try: controller_config = getattr(controller, "config") for k, v in list(controller_config.items()): if "." in k: # This is a hierarchical setting tail = namespace[name] parts = k.split(".") for part in parts[:-1]: tail = tail[part] tail[parts[-1]] = v if k == "enabled" and v is True: # Controller default value can't turn itself # on, but it can turn itself off. pass if k == "mod": # Don't ever redefine the module reference pass else: namespace[name][k] = v except AttributeError: pass # Provide every controller with a logger: c_logger = logging.getLogger("blogofile.controllers." + name) namespace[name]["logger"] = c_logger return namespace[name].mod finally: # Reset the original sys.dont_write_bytecode setting when we're done sys.dont_write_bytecode = initial_dont_write_bytecode def load_controllers(namespace, directory="_controllers", defaults={}): """Find all the controllers in the _controllers directory and import them into the bf context. """ for name in __find_controller_names(directory): load_controller(name, namespace, directory, defaults) def defined_controllers(namespaces, only_enabled=True): """Find all the enabled controllers in order of priority if only_enabled == False, find all controllers, regardless of their enabled status >>> bf_test = bf.cache.HierarchicalCache() >>> bf_test.controllers.one.enabled = True >>> bf_test.controllers.one.priority = 30 >>> bf_test.controllers.two.enabled = False >>> bf_test.controllers.two.priority = 90 >>> bf_test.controllers.three.enabled = True >>> bf_test.controllers.three.priority = 50 >>> bf_test2 = bf.cache.HierarchicalCache() >>> bf_test2.controllers.one.enabled = True >>> bf_test2.controllers.one.priority = 100 >>> c = defined_controllers((bf_test2,)) >>> c == [bf_test2.controllers.one] True >>> c = defined_controllers((bf_test,bf_test2)) >>> c == [bf_test2.controllers.one, bf_test.controllers.three, bf_test.controllers.one] True """ controllers = [] for namespace in namespaces: for c in list(namespace.controllers.values()): # Get only the ones that are enabled: if "enabled" not in c or c['enabled'] is False: # The controller is disabled if only_enabled: continue controllers.append(c) # Sort the controllers by priority return [x for x in sorted(controllers, key=operator.attrgetter("priority"), reverse=True)] def run_all(namespaces): """Run each controller in priority order. """ # Get the controllers in priority order: controllers = defined_controllers(namespaces) # Temporarily add _controllers directory onto sys.path for c in controllers: if "run" in dir(c.mod): logger.info("running controller (priority {0}): {1}" .format(c.priority, c.mod.__file__)) c.mod.run() else: logger.debug( "controller {0} has no run() method, skipping it.".format(c)) blogofile-0.8b1/blogofile/default_config.py000066400000000000000000000073421203633570100210410ustar00rootroot00000000000000###################################################################### # This is the main Blogofile configuration file. # www.Blogofile.com # # This is the canonical _config.py with every single default setting. # # Don't edit this file directly; create your own _config.py (from # scratch or using 'blogofile init') and your settings will override # these defaults. # ###################################################################### ###################################################################### # Basic Settings # (almost all sites will want to configure these settings) ###################################################################### ## site.url -- Your site's full URL # Your "site" is the same thing as your _site directory. # If you're hosting a blogofile powered site as a subdirectory of a larger # non-blogofile site, then you would set the site_url to the full URL # including that subdirectory: "http://www.yoursite.com/path/to/blogofile-dir" site.url = "http://www.example.com" ## site.author -- Your name, the author of the website. # This is optional. If set to anything other than None, the # simple_blog template creates a meta tag for the site author. site.author = None ###################################################################### # Advanced Settings ###################################################################### # Use hard links when copying files. This saves disk space and shortens # the time to build sites that copy lots of static files. # This is turned off by default though, because hard links are not # necessarily what every user wants. site.use_hard_links = False #Warn when we're overwriting a file? site.overwrite_warning = True # These are the default ignore patterns for excluding files and dirs # from the _site directory # These can be strings or compiled patterns. # Strings are assumed to be case insensitive. site.file_ignore_patterns = [ # All files that start with an underscore ".*/_.*", # Emacs autosave files ".*/#.*", # Emacs/Vim backup files ".*~$", # Vim swap files ".*/\..*\.swp$", # VCS directories ".*/\.(git|hg|svn|bzr)$", # Git and Mercurial ignored files definitions ".*/.(git|hg)ignore$", # CVS dir ".*/CVS$", ] from blogofile.template import MakoTemplate, JinjaTemplate, \ MarkdownTemplate, RestructuredTextTemplate, TextileTemplate #The site base template filename: site.base_template = "site.mako" #Template engines mapped to file extensions: templates.engines = HC( mako = MakoTemplate, jinja = JinjaTemplate, jinja2 = JinjaTemplate, markdown = MarkdownTemplate, rst = RestructuredTextTemplate, textile = TextileTemplate ) #Template content blocks: templates.content_blocks = HC( mako = HC( pattern = re.compile("\${\W*next.body\(\)\W*}"), replacement = "${next.body()}" ), jinja2 = HC( pattern = re.compile("{%\W*block content\W*%}.*?{%\W*endblock\W*%}", re.MULTILINE|re.DOTALL), replacement = "{% block content %} {% endblock %}" ), filter = HC( pattern = re.compile("_^"), #Regex that matches nothing replacement = "~~!`FILTER_CONTENT_HERE`!~~", default_chains = HC( markdown = "syntax_highlight, markdown", rst = "syntax_highlight, rst" ) ) ) ### Pre/Post build hooks: def pre_build(): #Do whatever you want before the _site is built. pass def post_build(): #Do whatever you want after the _site is built successfully. pass def build_exception(): #Do whatever you want if there is an unrecoverable error in building the site. pass def build_finally(): #Do whatever you want after the _site is built successfully OR after a fatal error pass blogofile-0.8b1/blogofile/exception.py000066400000000000000000000001051203633570100200540ustar00rootroot00000000000000# -*- coding: utf-8 -*- class FilterNotLoaded(Exception): pass blogofile-0.8b1/blogofile/filter.py000066400000000000000000000144401203633570100173520ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import print_function import sys import os import logging import imp import uuid logger = logging.getLogger("blogofile.filter") from .cache import bf from .cache import HierarchicalCache from . import exception bf.filter = sys.modules['blogofile.filter'] default_filter_config = {"name": None, "description": None, "author": None, "url": None} def run_chain(chain, content): """Run content through a filter chain. Works with either a string or a sequence of filters """ if chain is None: return content # lib3to2 interprets str as meaning unicode instead of basestring, # hand craft the translation to python2: if sys.version_info >= (3,): is_str = eval("isinstance(chain, str)") else: is_str = eval("isinstance(chain, basestring)") if is_str: chain = parse_chain(chain) for fn in chain: f = get_filter(fn) logger.debug("Applying filter: " + fn) content = f.run(content) logger.debug("Content: " + content) return content def parse_chain(chain): """Parse a filter chain into a sequence of filters. """ parts = [] for p in chain.split(","): p = p.strip() if p.lower() == "none": continue if len(p) > 0: parts.append(p) return parts def preload_filters(namespace=None, directory="_filters"): """Find all the standalone .py files and modules in the directory specified and load them into namespace specified. """ if namespace is None: namespace = bf.config.filters if(not os.path.isdir(directory)): return for fn in os.listdir(directory): p = os.path.join(directory, fn) if (os.path.isfile(p) and fn.endswith(".py")): # Load a single .py file: load_filter(fn[:-3], module_path=p, namespace=namespace) elif (os.path.isdir(p) and os.path.isfile(os.path.join(p, "__init__.py"))): # Load a package: load_filter(fn, module_path=p, namespace=namespace) def init_filters(namespace=None): """Filters have an optional init method that runs before the site is built. """ if namespace is None: namespace = bf.config.filters for name, filt in list(namespace.items()): if "mod" in filt \ and type(filt.mod).__name__ == "module"\ and not filt.mod.__initialized: try: init_method = filt.mod.init except AttributeError: filt.mod.__initialized = True continue logger.debug("Initializing filter: " + name) init_method() filt.mod.__initialized = True def get_filter(name, namespace=None): """Return an already loaded filter. """ if namespace is None: if name.startswith("bf") and "." in name: # Name is an absolute reference to a filter in a given # namespace; extract the namespace namespace, name = name.rsplit(".", 1) namespace = eval(namespace) else: namespace = bf.config.filters if name in namespace and "mod" in namespace[name]: logger.debug("Retrieving already loaded filter: " + name) return namespace[name]['mod'] else: raise exception.FilterNotLoaded("Filter not loaded: {0}".format(name)) def load_filter(name, module_path, namespace=None): """Load a filter from the site's _filters directory. """ if namespace is None: namespace = bf.config.filters try: initial_dont_write_bytecode = sys.dont_write_bytecode except KeyError: initial_dont_write_bytecode = False try: # Don't generate .pyc files in the _filters directory sys.dont_write_bytecode = True if module_path.endswith(".py"): mod = imp.load_source( "{0}_{1}".format(name, uuid.uuid4()), module_path) else: mod = imp.load_package( "{0}_{1}".format(name, uuid.uuid4()), module_path) logger.debug("Loaded filter for first time: {0}".format(module_path)) mod.__initialized = False # Overwrite anything currently in this namespace: try: del namespace[name] except KeyError: pass # If the filter defines it's own configuration, use that as # it's own namespace: if hasattr(mod, "config") and \ isinstance(mod.config, HierarchicalCache): namespace[name] = mod.config # Load the module into the namespace namespace[name].mod = mod # If the filter has any aliases, load those as well try: for alias in mod.config['aliases']: namespace[alias] = namespace[name] except: pass # Load the default blogofile config for filters: for k, v in list(default_filter_config.items()): namespace[name][k] = v # Load any filter defined defaults: try: filter_config = getattr(mod, "config") for k, v in list(filter_config.items()): if "." in k: # This is a hierarchical setting tail = namespace[name] parts = k.split(".") for part in parts[:-1]: tail = tail[part] tail[parts[-1]] = v else: namespace[name][k] = v except AttributeError: pass return mod except: logger.error("Cannot load filter: " + name) raise finally: # Reset the original sys.dont_write_bytecode setting where we're done sys.dont_write_bytecode = initial_dont_write_bytecode def list_filters(args): from . import config, plugin config.init_interactive() plugin.init_plugins() # module path -> list of aliases filters = {} for name, filt in bf.config.filters.items(): if "mod" in filt: aliases = filters.get(filt.mod.__file__, []) aliases.append(name) filters[filt.mod.__file__] = aliases for mod_path, aliases in filters.items(): print("{0} - {1}\n".format(", ".join(aliases), mod_path)) blogofile-0.8b1/blogofile/main.py000066400000000000000000000340301203633570100170060ustar00rootroot00000000000000# -*- coding: utf-8 -*- """This is Blogofile -- http://www.Blogofile.com Please take a moment to read LICENSE.txt. It's short. """ from __future__ import print_function __author__ = "Ryan McGuire (ryan@enigmacurry.com)" import argparse import locale import logging import os import shutil import sys import time import platform from . import __version__ from . import server from . import config from . import util from . import filter as _filter from . import plugin from .cache import bf from .writer import Writer locale.setlocale(locale.LC_ALL, '') logging.basicConfig() logger = logging.getLogger("blogofile") bf.logger = logger def main(argv=[]): """Blogofile entry point. Set up command line parser, parse args, and dispatch to appropriate function. Print help and exit if there are too few args. :arg argv: List of command line arguments. Non-empty list facilitates integration tests. :type argv: list """ do_debug() argv = argv or sys.argv parser, subparsers = setup_command_parser() if len(argv) == 1: parser.print_help() parser.exit(2) else: args = parser.parse_args(argv[1:]) set_verbosity(args) if args.func == do_help: do_help(args, parser, subparsers) else: args.func(args) def do_debug(): """Run blogofile in debug mode depending on the BLOGOFILE_DEBUG environment variable: If set to "ipython" just start up an embeddable ipython shell at bf.ipshell If set to anything else besides 0, setup winpdb environment """ try: if os.environ['BLOGOFILE_DEBUG'] == "ipython": from IPython.Shell import IPShellEmbed bf.ipshell = IPShellEmbed() elif os.environ['BLOGOFILE_DEBUG'] != "0": print("Running in debug mode, waiting for debugger to connect. " "Password is set to 'blogofile'") import rpdb2 rpdb2.start_embedded_debugger("blogofile") except KeyError: # Not running in debug mode pass def setup_command_parser(): """Set up the command line parser, and the parsers for the sub-commands. """ parser_template = _setup_parser_template() parser = argparse.ArgumentParser(parents=[parser_template]) subparsers = parser.add_subparsers(title='sub-commands') _setup_help_parser(subparsers) _setup_init_parser(subparsers) _setup_build_parser(subparsers) _setup_serve_parser(subparsers) _setup_info_parser(subparsers) _setup_plugins_parser(subparsers, parser_template) _setup_filters_parser(subparsers) return parser, subparsers def _setup_parser_template(): """Return the parser template that other parser are based on. """ parser_template = argparse.ArgumentParser(add_help=False) parser_template.add_argument( "--version", action="version", version="Blogofile {0} -- http://www.blogofile.com -- {1} {2}" .format(__version__, platform.python_implementation(), platform.python_version())) parser_template.add_argument( "-v", "--verbose", dest="verbose", action="store_true", help="Be verbose") parser_template.add_argument( "-vv", "--veryverbose", dest="veryverbose", action="store_true", help="Be extra verbose") defaults = { "src_dir": os.curdir, "verbose": False, "veryverbose": False, } parser_template.set_defaults(**defaults) return parser_template def _setup_help_parser(subparsers): """Set up the parser for the help sub-command. """ parser = subparsers.add_parser( "help", add_help=False, help="Show help for a command.") parser.add_argument( "command", nargs="*", help="a Blogofile subcommand e.g. build") parser.set_defaults(func=do_help) defaults = { 'command': None, 'func': do_help, } parser.set_defaults(**defaults) def _setup_init_parser(subparsers): """Set up the parser for the init sub-command. """ parser = subparsers.add_parser( "init", help="Create a new blogofile site.") parser.add_argument( "src_dir", help=""" Your site's source directory. It will be created if it doesn't exist, as will any necessary parent directories. """) parser.add_argument( "plugin", nargs="?", help=""" Plugin to initialize site from. The plugin must already be installed; use `blogofile plugins list` to get the list of installed plugins. If omitted, a bare site directory will be created. """) defaults = { "plugin": None, "func": do_init, } parser.set_defaults(**defaults) def _setup_build_parser(subparsers): """Set up the parser for the build sub-command. """ parser = subparsers.add_parser( "build", help="Build the site from source.") parser.add_argument( "-s", "--src-dir", dest="src_dir", metavar="DIR", help="Your site's source directory (default is current directory)") defaults = { "src_dir": os.curdir, "func": do_build, } parser.set_defaults(**defaults) def _setup_serve_parser(subparsers): """Set up the parser for the serve sub-command. """ parser = subparsers.add_parser( "serve", help=""" Host the _site dir with the builtin webserver. Useful for quickly testing your site. Not for production use! """) parser.add_argument( "PORT", nargs="?", help="TCP port to use; defaults to %(default)s") parser.add_argument( "IP_ADDR", nargs="?", help=""" IP address to bind to. Defaults to loopback only (%(default)s). 0.0.0.0 binds to all network interfaces, please be careful!. """) parser.add_argument( "-s", "--src-dir", dest="src_dir", metavar="DIR", help="Your site's source directory (default is current directory)") defaults = { "PORT": "8080", "IP_ADDR": "127.0.0.1", "src_dir": os.curdir, "func": do_serve, } parser.set_defaults(**defaults) def _setup_info_parser(subparsers): """Set up the parser for the info sub-command. """ parser = subparsers.add_parser( "info", help=""" Show information about the Blogofile installation and the current site. """) parser.add_argument( "-s", "--src-dir", dest="src_dir", metavar="DIR", help="Your site's source directory (default is current directory)") defaults = { "src_dir": os.curdir, "func": do_info, } parser.set_defaults(**defaults) def _setup_plugins_parser(subparsers, parser_template): """Set up the parser for the plugins sub-command. """ parser = subparsers.add_parser( "plugins", help="Plugin tools") plugin_subparsers = parser.add_subparsers() plugins_list = plugin_subparsers.add_parser( "list", help="List all of the plugins installed") plugins_list.set_defaults(func=plugin.list_plugins) for p in plugin.iter_plugins(): # Setup the plugin command parser, if it has one try: plugin_parser_setup = p.__dist__['command_parser_setup'] except KeyError: continue plugin_parser = subparsers.add_parser( p.__dist__['config_name'], help="Plugin: " + p.__dist__['description']) plugin_parser.add_argument( "--version", action="version", version="{name} plugin {version} by {author} -- {url}" .format(**p.__dist__)) plugin_parser_setup(plugin_parser, parser_template) def _setup_filters_parser(subparsers): """Set up the parser for the filters sub-command. """ parser = subparsers.add_parser( "filters", help="Filter tools") filter_subparsers = parser.add_subparsers() filters_list = filter_subparsers.add_parser( "list", help="List all the filters installed") filters_list.set_defaults(func=_filter.list_filters) def set_verbosity(args): """Set verbosity level for logging as requested on command line. """ if args.verbose: logger.setLevel(logging.INFO) logger.info("Setting verbose mode") if args.veryverbose: logger.setLevel(logging.DEBUG) logger.info("Setting very verbose mode") def do_help(args, parser, subparsers): if "commands" in args.command: args.command = sorted(subparsers.choices.keys()) if not args.command: parser.print_help() print("\nSee 'blogofile help COMMAND' for more information" " on a specific command.") else: # Where did the subparser help text go? Let's get it back. # Certainly there is a better way to retrieve the helptext than this... helptext = {} for subcommand in args.command: for action in subparsers._choices_actions: if action.dest == subcommand: helptext[subcommand] = action.help break else: helptext[subcommand] = "" # Print help for each subcommand requested. for subcommand in args.command: sys.stderr.write("{0} - {1}\n" .format(subcommand, helptext[subcommand])) parser = subparsers.choices[subcommand] parser.print_help() sys.stderr.write("\n") # Perform any extra help tasks: if hasattr(parser, "extra_help"): parser.extra_help() def do_init(args): """Initialize a new blogofile site. """ # Look before we leap because _init_plugin_site uses # shutil.copytree() which requires that the src_dir not already # exist if os.path.exists(args.src_dir): print( "{0.src_dir} already exists; initialization aborted" .format(args), file=sys.stderr) sys.exit(1) if args.plugin is None: _init_bare_site(args.src_dir) else: _init_plugin_site(args) def _init_bare_site(src_dir): """Initialize the site directory as a bare (do-it-yourself) site. Write a minimal _config.py file and a message to the user. """ bare_site_config = [ "# -*- coding: utf-8 -*-\n", "# This is a minimal blogofile config file.\n", "# See the docs for config options\n", "# or run `blogofile help init` to learn how to initialize\n", "# a site from a plugin.\n", ] os.makedirs(src_dir) new_config_path = os.path.join(src_dir, '_config.py') with open(new_config_path, 'wt') as new_config: new_config.writelines(bare_site_config) print("_config.py for a bare (do-it-yourself) site " "written to {0}\n" "If you were expecting more, please see `blogofile init -h`" .format(src_dir)) def _init_plugin_site(args): """Initialize the site directory with the approprate files from an installed blogofile plugin. Copy everything except the _controllers, _filters, and _templates directories from the plugin's site_src directory. """ p = plugin.get_by_name(args.plugin) if p is None: print("{0.plugin} plugin not installed; initialization aborted\n\n" "installed plugins:".format(args), file=sys.stderr) plugin.list_plugins(args) return plugin_path = os.path.dirname(os.path.realpath(p.__file__)) site_src = os.path.join(plugin_path, 'site_src') ignore_dirs = shutil.ignore_patterns( '_controllers', '_filters') shutil.copytree(site_src, args.src_dir, ignore=ignore_dirs) print("{0.plugin} plugin site_src files written to {0.src_dir}" .format(args)) def do_build(args, load_config=True): _validate_src_dir(args.src_dir) if load_config: config.init_interactive(args) output_dir = util.path_join("_site", util.fs_site_path_helper()) writer = Writer(output_dir=output_dir) logger.debug("Running user's pre_build() function...") config.pre_build() try: writer.write_site() logger.debug("Running user's post_build() function...") config.post_build() except: logger.error( "Fatal build error occured, calling bf.config.build_exception()") config.build_exception() raise finally: logger.debug("Running user's build_finally() function...") config.build_finally() def _validate_src_dir(src_dir): """Confirm that `src_dir` exists, and contains a `_config.py` file. If so, make `src_dir` the working directory. """ if not os.path.isdir(src_dir): print("source dir does not exist: {0}".format(src_dir)) sys.exit(1) if not os.path.isfile(os.path.join(src_dir, "_config.py")): print("source dir does not contain a _config.py file") sys.exit(1) os.chdir(src_dir) def do_serve(args): _validate_src_dir(args.src_dir) config.init_interactive(args) bfserver = server.Server(args.PORT, args.IP_ADDR) bfserver.start() while not bfserver.is_shutdown: try: time.sleep(0.5) except KeyboardInterrupt: bfserver.shutdown() def do_info(args): """Print some information about the Blogofile installation and the current site. """ print("This is Blogofile (version {0}) -- http://www.blogofile.com" .format(__version__)) print("You are using {0} {1} from {2}".format( platform.python_implementation(), platform.python_version(), sys.executable)) print("Blogofile is installed at: {0}".format(os.path.split(__file__)[0])) # Show _config.py paths print(("Default config file: {0}".format(config.default_config_path))) if os.path.isfile(os.path.join(args.src_dir, "_config.py")): print("Found site _config.py: {0}" .format(os.path.abspath("_config.py"))) else: print( "The specified directory has no _config.py, and cannot be built.") blogofile-0.8b1/blogofile/plugin.py000066400000000000000000000156671203633570100173770ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import print_function import logging import os import os.path import pkg_resources import sys from mako.lookup import TemplateLookup import six from . import controller from . import filter as _filter from . import template from .cache import bf from .cache import HierarchicalCache logger = logging.getLogger("blogofile.plugin") default_plugin_config = { "priority": 50.0, "enabled": False, } reserved_attributes = ["mod", "filters", "controllers", "site_src"] def iter_plugins(): for plugin in pkg_resources.iter_entry_points("blogofile.plugins"): yield plugin.load() def get_by_name(name): for plugin in iter_plugins(): if plugin.__dist__['config_name'] == name: return plugin def list_plugins(args): for plugin in iter_plugins(): print("{0} ({1}) - {2} - {3}".format(plugin.__dist__['config_name'], plugin.__dist__['version'], plugin.__dist__['description'], plugin.__dist__['author'])) def check_plugin_config(module): """Ensure that a plugin has the required components and none of the reserved ones. """ try: assert isinstance(module.config, HierarchicalCache) except AttributeError: raise AssertionError("Plugin {0} has no config HierarchicalCache" .format(module)) except AssertionError: raise AssertionError("Plugin {0} config object must extend from " "HierarchicalCache".format(module)) try: module.__dist__ except AttributeError: raise AssertionError("Plugin {0} has no __dist__ dictionary, " "describing the plugins metadata.".format(module)) #TODO: Why does this fail in a test context? Not really *that* important.. # for attr in reserved_attributes: # if module.config.has_key(attr): # raise AssertionError, "'{0}' is a reserved attribute name for " \ # "Blogofile plugins. They should not be assigned manually."\ # .format(attr) def load_plugins(): """Discover all the installed plugins and load them into bf.config.plugins Load the module itself, the controllers, and the filters. """ for plugin in iter_plugins(): namespace = bf.config.plugins[plugin.__dist__["config_name"]] = \ getattr(plugin, "config") check_plugin_config(plugin) namespace.mod = plugin plugin_dir = os.path.dirname(sys.modules[plugin.__name__].__file__) # Load filters _filter.preload_filters( namespace=namespace.filters, directory=os.path.join(plugin_dir, "site_src", "_filters")) # Load controllers controller.load_controllers( namespace=namespace.controllers, directory=os.path.join(plugin_dir, "site_src", "_controllers"), defaults={"enabled": True}) def init_plugins(): for name, plugin in list(bf.config.plugins.items()): if plugin.enabled: if "mod" not in plugin: print("Cannot find requested plugin: {0}".format(name)) print("Build aborted.") sys.exit(1) logger.info("Initializing plugin: {0}".format( plugin.mod.__dist__['config_name'])) plugin.mod.init() for name, filter_ns in list(plugin.filters.items()): # Filters from plugins load in their own namespace, but # they also load in the regular filter namespace as long as # there isn't already a filter with that name. User filters # from the _filters directory are loaded after plugins, so # they are overlaid on top of these values and take # precedence. if name not in bf.config.filters: bf.config.filters[name] = filter_ns elif "mod" not in bf.config.filters[name]: filter_ns.update(bf.config.filters[name]) bf.config.filters[name] = filter_ns class PluginTools(object): """Tools for a plugin to get information about it's runtime environment. """ def __init__(self, module): self.module = module self.namespace = self.module.config self.template_lookup = self._template_lookup() self.logger = logging.getLogger( "blogofile.plugins.{0}".format(self.module.__name__)) def _template_lookup(self): return TemplateLookup( directories=[ "_templates", os.path.join(self.get_src_dir(), "_templates")], input_encoding='utf-8', output_encoding='utf-8', encoding_errors='replace') def get_src_dir(self): """Return the plugin's :file:`site_src directory path. :returns: :file:`site_src` path for the plugin. :rtype: str """ return os.path.join(os.path.dirname(self.module.__file__), "site_src") def materialize_template(self, template_name, location, attrs={}): """Materialize a template using the plugin's TemplateLookup instance. :arg template_name: File name of the template to materialize. :type template_name: str :arg location: Path and file name in the :file:`_site` directory to render the template to. :type location: str :arg attrs: Template variable names and values that will be used as the data context to render the template with. :type attrs: dict """ template.materialize_template( template_name, location, attrs=attrs, lookup=self.template_lookup, caller=self.module) def add_template_dir(self, path, append=True): """Add a template directory to the plugin's TemplateLookup instance directories list. :arg path: Template path to add to directories list. :type path: str :arg append: Add the template path to the end of the directories list when True (the default), otherwise, add it to the beginning of the list. :type append: Boolean """ if append: self.template_lookup.directories.append(path) else: self.template_lookup.directories.insert(0, path) def initialize_controllers(self): """Initialize the plugin's controllers. """ for name, controller in six.iteritems(self.module.config.controllers): self.logger.info("Initializing controller: {0}".format(name)) controller.mod.init() def run_controllers(self): """Run the plugin's controllers. """ for name, controller in six.iteritems(self.module.config.controllers): self.logger.info("Running controller: {0}".format(name)) controller.mod.run() blogofile-0.8b1/blogofile/server.py000066400000000000000000000053711203633570100173760ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import print_function import logging import os import sys import threading try: from urllib.parse import urlparse # For Python 2 except ImportError: from urlparse import urlparse # For Python 3; flake8 ignore # NOQA from six.moves import SimpleHTTPServer from six.moves import socketserver from blogofile import config from blogofile import util from .cache import bf bf.server = sys.modules['blogofile.server'] logger = logging.getLogger("blogofile.server") class Server(threading.Thread): def __init__(self, port, address="127.0.0.1"): self.port = int(port) self.address = address if self.address == "0.0.0.0": # Bind to all addresses available address = "" threading.Thread.__init__(self) self.is_shutdown = False server_address = (address, self.port) HandlerClass = BlogofileRequestHandler HandlerClass.protocol_version = "HTTP/1.0" ServerClass = socketserver.TCPServer self.httpd = ServerClass(server_address, HandlerClass) self.sa = self.httpd.socket.getsockname() def run(self): print("Blogofile server started on {0}:{1} ..." .format(self.sa[0], self.sa[1])) self.httpd.serve_forever() def shutdown(self): print("\nshutting down webserver...") self.httpd.shutdown() self.httpd.socket.close() self.is_shutdown = True class BlogofileRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): error_template = """ Error response

404 Error

Your Blogofile site is configured for a subdirectory, maybe you were looking for the root page? : {1} """ def __init__(self, *args, **kwargs): path = urlparse(config.site.url).path self.BLOGOFILE_SUBDIR_ERROR = self.error_template.format(path, path) SimpleHTTPServer.SimpleHTTPRequestHandler.__init__( self, *args, **kwargs) def translate_path(self, path): site_path = urlparse(config.site.url).path if(len(site_path.strip("/")) > 0 and not path.startswith(site_path)): self.error_message_format = self.BLOGOFILE_SUBDIR_ERROR # Results in a 404 return "" p = SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path( self, path) if len(site_path.strip("/")) > 0: build_path = os.path.join( os.getcwd(), util.path_join(site_path.strip("/"))) else: build_path = os.getcwd() build_path = p.replace(build_path, os.path.join(os.getcwd(), "_site")) return build_path def log_message(self, format, *args): pass blogofile-0.8b1/blogofile/template.py000066400000000000000000000334421203633570100177030ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Template abstraction for Blogofile to support multiple engines. Templates are dictionaries. Any key/value pairs stored are supplied to the underlying template as name/values. """ from __future__ import print_function import copy import logging import os.path import re import sys import tempfile import jinja2 import mako import mako.lookup import mako.template from . import filter as _filter from . import util from .cache import bf from .cache import Cache bf.template = sys.modules['blogofile.template'] base_template_dir = util.path_join(".", "_templates") logger = logging.getLogger("blogofile.template") template_content_place_holder = re.compile("~~!`TEMPLATE_CONTENT_HERE`!~~") class TemplateEngineError(Exception): pass class Template(dict): name = "base" def __init__(self, template_name, caller=None): dict.__init__(self) self.template_name = template_name self.caller = caller def render(self, path=None): """Render the template to the specified path on disk, or return a string if None. """ raise NotImplementedError( "Template base class cannot be used directly") def write(self, path, rendered): path = util.path_join(bf.writer.output_dir, path) # Create the parent directories if they don't exist: util.mkdir(os.path.split(path)[0]) if bf.config.site.overwrite_warning and os.path.exists(path): logger.warn("Location is used more than once: {0}".format(path)) with open(path, "wb") as f: f.write(rendered) def render_prep(self, path): """Gather all the information we want to provide to the template before rendering. """ for name, obj in list(bf.config.site.template_vars.items()): if name not in self: self[name] = obj # Create a context object that is fresh for each template render: bf.template_context = Cache(**self) bf.template_context.template_name = self.template_name bf.template_context.render_path = path bf.template_context.caller = self.caller self["bf"] = bf def render_cleanup(self): """Clean up stuff after we've rendered a template. """ del bf.template_context def __repr__(self): return "<{0} file='{1}' {2}>".format( self.__class__.__name__, self.template_name, dict.__repr__(self)) class MakoTemplate(Template): name = "mako" template_lookup = None def __init__(self, template_name, caller=None, lookup=None, src=None): Template.__init__(self, template_name, caller) self.create_lookup() if lookup: #M ake sure it's a mako environment: if type(lookup) != mako.lookup.TemplateLookup: raise TemplateEngineError( "MakoTemplate was passed a non-mako lookup environment:" " {0}".format(lookup)) self.template_lookup = lookup self.add_template_path(bf.writer.temp_proc_dir) # Templates can be provided three ways: # 1) src is a template passed via string # 2) template_name can be a path to a file # 3) template_name can be a name to lookup if src: self.mako_template = mako.template.Template( src, output_encoding="utf-8", lookup=self.template_lookup) elif os.path.isfile(template_name): with open(self.template_name) as t_file: self.mako_template = mako.template.Template( t_file.read(), output_encoding="utf-8", lookup=self.template_lookup) else: self.mako_template = self.template_lookup.get_template( template_name) self.mako_template.output_encoding = "utf-8" @classmethod def create_lookup(cls): if MakoTemplate.template_lookup is None: MakoTemplate.template_lookup = mako.lookup.TemplateLookup( directories=[".", base_template_dir], input_encoding='utf-8', output_encoding='utf-8', encoding_errors='replace') @classmethod def add_default_template_path(cls, path): "Add a path to the default template_lookup" cls.create_lookup() if path not in cls.template_lookup.directories: cls.template_lookup.directories.append(path) def add_template_path(self, path, lookup=None): if lookup is None: lookup = self.template_lookup if path not in lookup.directories: lookup.directories.append(path) def render(self, path=None): self.render_prep(path) # Make sure bf_base_template is defined if "bf_base_template" in self: bf_base_template = os.path.split(self["bf_base_template"])[1] self.template_lookup.put_template( "bf_base_template", self.template_lookup.get_template( bf_base_template)) else: self.template_lookup.put_template( "bf_base_template", self.template_lookup.get_template( bf.config.site.base_template)) try: rendered = self.mako_template.render(**self) if path: self.write(path, rendered) return rendered except: logger.error("Error rendering template: {0}".format( self.template_name)) print((mako.exceptions.text_error_template().render())) raise finally: self.render_cleanup() class JinjaTemplateLoader(jinja2.FileSystemLoader): def __init__(self, searchpath): jinja2.FileSystemLoader.__init__(self, searchpath) self.bf_base_template = bf.util.path_join( "_templates", bf.config.site.base_template) def get_source(self, environment, template): if template == "bf_base_template": with open(self.bf_base_template) as f: return (f.read(), self.bf_base_template, lambda: False) else: return (super(JinjaTemplateLoader, self) .get_source(environment, template)) class JinjaTemplate(Template): name = "jinja2" template_lookup = None def __init__(self, template_name, caller=None, lookup=None, src=None): Template.__init__(self, template_name, caller) self.create_lookup() if lookup: # Make sure it's a jinja2 environment: if type(lookup) != jinja2.Environment: raise TemplateEngineError( "JinjaTemplate was passed a non-jinja lookup environment:" " {0}".format(lookup)) self.template_lookup = lookup self.add_template_path(bf.writer.temp_proc_dir) # Templates can be provided three ways: # 1) src is a template passed via string # 2) template_name can be a path to a file # 3) template_name can be a name to lookup # Jinja needs to save the loading of the source until render # time in order to get the attrs into the loader. # Just save the params for later use: self.src = src @classmethod def create_lookup(cls): if cls.template_lookup is None: cls.template_lookup = jinja2.Environment( loader=JinjaTemplateLoader([base_template_dir, bf.writer.temp_proc_dir])) @classmethod def add_default_template_path(cls, path): cls.create_lookup() if path not in cls.template_lookup.loader.searchpath: cls.template_lookup.loader.searchpath.append(path) def add_template_path(self, path, lookup=None): if lookup is None: lookup = self.template_lookup if path not in lookup.loader.searchpath: lookup.loader.searchpath.append(path) def render(self, path=None): # Ensure that bf_base_template is set: if "bf_base_template" in self: self.template_lookup.loader.bf_base_template = ( self["bf_base_template"]) else: self["bf_base_template"] = ( self.template_lookup.loader.bf_base_template) if self.src: self.jinja_template = self.template_lookup.from_string(self.src) elif os.path.isfile(self.template_name): with open(self.template_name) as t_file: self.jinja_template = self.template_lookup.from_string( t_file.read()) else: self.jinja_template = self.template_lookup.get_template( self.template_name) self.render_prep(path) try: rendered = bytes(self.jinja_template.render(self), "utf-8") if path: self.write(path, rendered) return rendered except: logger.error( "Error rendering template: {0}".format(self.template_name)) raise finally: self.render_cleanup() class FilterTemplate(Template): name = "filter" chain = None def __init__(self, template_name, caller=None, lookup=None, src=None): Template.__init__(self, template_name, caller) self.src = src self.marker = bf.config.templates.content_blocks.filter.replacement def render(self, path=None): self.render_prep(path) try: if self.src is None: with open(self.template_name) as f: src = f.read() else: src = self.src # Run the filter chain: html = _filter.run_chain(self.chain, src) # Place the html into the base template: with open(self["bf_base_template"]) as f: html = f.read().replace(self.marker, html) html = bytes(html, "utf-8") if path: self.write(path, html) return html finally: self.render_cleanup() class MarkdownTemplate(FilterTemplate): chain = "markdown" class RestructuredTextTemplate(FilterTemplate): chain = "rst" class TextileTemplate(FilterTemplate): chain = "textile" def get_engine_for_template_name(template_name): # Find which template type it is: for extension, engine in bf.config.templates.engines.items(): if template_name.endswith("." + extension): return engine else: raise TemplateEngineError( "Template has no engine defined in bf.config." "templates.engines: {0}".format(template_name)) def get_base_template_path(): return bf.util.path_join("_templates", bf.config.site.base_template) def get_base_template_src(): with open(get_base_template_path()) as f: return f.read() def materialize_alternate_base_engine(template_name, location, attrs={}, lookup=None, base_engine=None, caller=None): """Materialize a templates within a foreign template engine. Procedure: 1) Load the base template source, and mark the content block for later replacement. 2) Materialize the base template in a temporary location with attrs. 3) Convert the HTML to new template type by replacing the marker. 4) Materialize the template setting bf_base_template to the new base template we created. """ # Since we're mucking with the template attrs, make sure we copy # them and don't modify the original ones: attrs = copy.copy(attrs) if not base_engine: base_engine = get_engine_for_template_name( bf.config.site.base_template) template_engine = get_engine_for_template_name(template_name) base_template_src = get_base_template_src() if not lookup: lookup = base_engine.template_lookup else: base_engine.add_default_template_path(bf.writer.temp_proc_dir) # Replace the content block with our own marker: prev_content_block = bf.config.templates.content_blocks[base_engine.name] new_content_block = ( bf.config.templates.content_blocks[template_engine.name]) base_template_src = prev_content_block.pattern.sub( template_content_place_holder.pattern, base_template_src) html = str(base_engine(None, src=base_template_src).render(), "utf-8") html = template_content_place_holder.sub( new_content_block.replacement, html) new_base_template = tempfile.mktemp( suffix="." + template_engine.name, prefix="bf_template", dir=bf.writer.temp_proc_dir) with open(new_base_template, "w") as f: logger.debug( "Writing intermediate base template: {0}" .format(new_base_template)) f.write(html) attrs["bf_base_template"] = new_base_template materialize_template( template_name, location, attrs, base_engine=template_engine) os.remove(new_base_template) def materialize_template(template_name, location, attrs={}, lookup=None, base_engine=None, caller=None): """Render a named template with attrs to a location in the _site dir. """ # Find the appropriate template engine based on the file ending: template_engine = get_engine_for_template_name(template_name) if not base_engine: base_engine = get_engine_for_template_name( bf.config.site.base_template) # Is the base engine the same as the template engine? if base_engine == template_engine or base_engine == template_engine.name: template = template_engine(template_name, caller=caller, lookup=lookup) template.update(attrs) template.render(location) else: materialize_alternate_base_engine( template_name, location, attrs=attrs, caller=caller, lookup=lookup, base_engine=base_engine) blogofile-0.8b1/blogofile/tests/000077500000000000000000000000001203633570100166525ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/__init__.py000066400000000000000000000000001203633570100207510ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/browser/000077500000000000000000000000001203633570100203355ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/browser/test_chrome.py000066400000000000000000000174541203633570100232360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Selenium tests for the builtin Blogofile server. This is only # intended to be run in a virtualenv via tox. Selenium isn't working # for me in Python3 right now. Blogofile will be run using the # virtualenv python but selenium will be run with the system python2. import unittest import tempfile import shutil import os import subprocess import shlex import time import datetime from selenium import webdriver from selenium.common.exceptions import NoSuchElementException def browserbot(driver, function, *args): """Selenium Javascript Helpers""" # Original copyright and license for browserbot.js (http://is.gd/Bz4xPc): # Copyright (c) 2009-2011 Jari Bakken # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software", to) deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. browserbot_js = """var browserbot = { getOuterHTML: function(element) { if (element.outerHTML) { return element.outerHTML; } else if (typeof(XMLSerializer) != undefined) { return new XMLSerializer().serializeToString(element); } else { throw "can't get outerHTML in this browser"; } } }; """ js = browserbot_js + \ "return browserbot.{0}.apply(browserbot, arguments);".format(function) return driver.execute_script(js,*args) def html(web_element): """Return the HTML for a Selenium WebElement""" return browserbot(web_element.parent, "getOuterHTML", web_element) class TestBrowser(unittest.TestCase): @classmethod def setUpClass(cls): #Remember the current directory to preserve state cls.previous_cwd = os.getcwd() #Create a staging directory that we can build in cls.build_path = tempfile.mkdtemp() #Change to that directory just like a user would os.chdir(cls.build_path) #Initialize and build the site subprocess.Popen(shlex.split("blogofile init blog_unit_test"), stdout=subprocess.PIPE).wait() subprocess.Popen(shlex.split("blogofile build"), stdout=subprocess.PIPE).wait() #Start the server cls.port = 42042 cls.url = u"http://localhost:{0}".format(cls.port) cls.server = subprocess.Popen(shlex.split("blogofile serve {0}". format(cls.port)), stdout=subprocess.PIPE) cls.chrome = webdriver.Chrome() @classmethod def tearDownClass(cls): cls.chrome.stop_client() #Stop the server cls.server.kill() #go back to the directory we used to be in os.chdir(cls.previous_cwd) #Clean up the build directory shutil.rmtree(cls.build_path) def testMainPage(self): self.chrome.get(self.url) self.assertEqual(self.chrome.current_url,self.url+u"/") self.assertEqual(self.chrome.title,u"Your Blog's Name") def testChronlogicalBlog(self): self.chrome.get(self.url) #Click on "chronological blog page" link on index self.chrome.find_element_by_link_text("chronological blog page").click() #Make sure we went to the right URL: self.assertEqual(self.chrome.current_url,self.url+u"/blog/") #Make sure there are five blog posts: self.assertEqual(len(self.chrome.find_elements_by_class_name("blog_post")),5) #Make sure there is no previous page: with self.assertRaises(NoSuchElementException): self.chrome.find_element_by_partial_link_text("Previous Page") #Go to the next page: self.chrome.find_element_by_partial_link_text("Next Page").click() #Make sure we went to the right URL: self.assertEqual(self.chrome.current_url,self.url+u"/blog/page/2/") #Make sure there are five blog posts: self.assertEqual(len(self.chrome.find_elements_by_class_name("blog_post")),5) #Go to the last page: self.chrome.find_element_by_partial_link_text("Next Page").click() #Make sure there is no next page: with self.assertRaises(NoSuchElementException): self.chrome.find_element_by_partial_link_text("Next Page") #Go back to the start: self.chrome.find_element_by_partial_link_text("Previous Page").click() self.chrome.find_element_by_partial_link_text("Previous Page").click() self.assertEqual(self.chrome.current_url,self.url+u"/blog/page/1/") #Make sure the unpublished draft is not present. It would be #the very first post on the first page if it were actually #published: with self.assertRaises(NoSuchElementException): self.chrome.find_element_by_link_text("This post is unpublished") def testPostFeatures(self): self.chrome.get(self.url+"/blog") self.chrome.find_element_by_link_text("Post 7").click() self.assertEqual(self.chrome.current_url,self.url+u"/blog/2009/08/29/post-seven/") self.assertEqual(self.chrome.find_element_by_class_name("post_prose").text,u"This is post #7") self.assertEqual(self.chrome.find_element_by_class_name("blog_post_date").text,u"August 29, 2009 at 03:25 PM") self.assertEqual(self.chrome.find_element_by_class_name("blog_post_categories").text,u"general stuff") self.chrome.find_element_by_link_text("general stuff").click() self.assertEqual(self.chrome.current_url,self.url+u"/blog/category/general-stuff/") def testPostWithNoDate(self): self.chrome.get(self.url+"/blog") self.chrome.find_element_by_link_text("Post without a date").click() #Make sure the post has today's date now = datetime.datetime.now().strftime("%B %d, %Y") #I guess this might fail at 23:59:59.. self.assertTrue(self.chrome.find_element_by_class_name("blog_post_date").text.startswith(now)) def testPostUnicode(self): self.chrome.get(self.url+"/blog/2009/08/22/unicode-test-") self.assertIn("私はガラスを食べられます。それは私を傷つけません".decode("utf-8"), self.chrome.get_page_source()) self.assertIn("日本語テスト".decode("utf-8"), self.chrome.find_element_by_css_selector(".blog_post_title a").text) def testMarkdownTemplate(self): self.chrome.get(self.url+"/markdown_test.html") self.assertIn("This is a link", self.chrome.get_page_source()) def testPluginFilters(self): self.chrome.get(self.url+"/filter_test.html") self.assertIn(u"This is text from the plugin version of the filter.", self.chrome.find_element_by_id("original_plugin_filter").text) self.assertIn(u"This is text from the overriden userspace filter.", self.chrome.find_element_by_id("overriden_plugin_filter").text) blogofile-0.8b1/blogofile/tests/integration/000077500000000000000000000000001203633570100211755ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/integration/__init__.py000066400000000000000000000000001203633570100232740ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/integration/test_integration.py000066400000000000000000000023741203633570100251370ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Integration tests for blogofile. """ import os import shutil from tempfile import mkdtemp try: import unittest2 as unittest # For Python 2.6 except ImportError: import unittest # flake8 ignore # NOQA from ... import main class TestBlogofileCommands(unittest.TestCase): """Intrgration tests for the blogofile commands. """ def _call_entry_point(self, *args): main.main(*args) def test_blogofile_init_bare_site(self): """`blogofile init src` initializes bare site w/ _config.py file """ src_dir = mkdtemp() self.addCleanup(shutil.rmtree, src_dir) os.rmdir(src_dir) self._call_entry_point(['blogofile', 'init', src_dir]) self.assertEqual(os.listdir(src_dir), ['_config.py']) def test_blogofile_build_bare_site(self): """`blogofile build` on bare site creates _site directory """ self.addCleanup(os.chdir, os.getcwd()) src_dir = mkdtemp() self.addCleanup(shutil.rmtree, src_dir) os.rmdir(src_dir) self._call_entry_point(['blogofile', 'init', src_dir]) self._call_entry_point(['blogofile', 'build', '-s', src_dir]) self.assertIn('_site', os.listdir(src_dir)) blogofile-0.8b1/blogofile/tests/plugin/000077500000000000000000000000001203633570100201505ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/000077500000000000000000000000001203633570100225055ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/000077500000000000000000000000001203633570100270645ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/__init__.py000066400000000000000000000045231203633570100312010ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import logging import blogofile import blogofile.plugin from blogofile.cache import bf, HierarchicalCache as HC try: import urllib.parse as urlparse except ImportError: #Python 2 import urlparse from . import commands ## Configure the plugin meta information: __dist__ = dict( #The name of your plugin: name = "Plugin Unit Tests", #The namespace of your plugin as used in _config.py. #referenced as bf.config.plugins.name config_name = "plugin_test", #Your name: author = "Ryan McGuire", #The version number: version = "0.1", #The URL for the plugin (where to download, documentation etc): url = "http://www.blogofile.com", #A one line description of your plugin presented to other Blogofile users: description = "Unit tests for plugins", #PyPI description, could be the same, except this text #should mention the fact that this is a Blogofile plugin #because non-Blogofile users will see this text: pypi_description = "Unit tests for blogofile plugins", #Command parser #Ths installs extra commands into blogofile, see commands.py command_parser_setup = commands.setup ) __version__ = __dist__["version"] #This is your plugin's external configuration object. Define all the #default settings for your plugin that the user may override if they #choose. Users can access/modify these settings in _config.py: # # plugins.example.gallery.src = "/path/to/my/photos" # config = HC( base_template = "site.mako", gallery = HC( #The source directory containing photos src = None, #None means use the supplied photos #The path on the site to host the gallery path = "photos" ) ) tools = blogofile.plugin.PluginTools(__name__) logger = logging.getLogger("blogofile.plugins.{0}".format(__name__)) def init(): #Initialize the controllers here, but we can reuse a generic tool for that: tools.initialize_controllers() #The base template is a configurable option, injected into the #template rendrer here at runtime: tools.template_lookup.put_template( "plugin_base_template",tools.template_lookup.get_template( config.base_template)) def run(): #Run the controllers here, but we can reuse a generic tool for that too: tools.run_controllers() blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/commands.py000066400000000000000000000030341203633570100312370ustar00rootroot00000000000000import shutil import sys import os, os.path import imp import blogofile.main from blogofile import argparse ## These are commands that are installed into the blogofile ## command-line utility. You can turn these off entirely by removing ## the command_parser_setup parameter in the module __dist__ object. def setup(parent_parser, parser_template): from . import __dist__ #Add additional subcommands under the main parser: cmd_subparsers = parent_parser.add_subparsers() #command1 command1 = cmd_subparsers.add_parser( "command1", help="Example Command 1", parents=[parser_template]) command1.add_argument("--extra-coolness",action="store_true", help="Run with extra coolness") command1.set_defaults(func=do_command1) #command2 command2 = cmd_subparsers.add_parser( "command2", help="Example Command 2", parents=[parser_template]) command2.add_argument("ARG1",help="Required ARG1") command2.add_argument("ARG2",help="Optional ARG2", nargs="?",default="Default") command2.set_defaults(func=do_command2) #These are the actual command actions: def do_command1(args): print("") print("This is command1.") if args.extra_coolness: print("It's as cool as can be.") else: print("It could be cooler though with --extra-coolness") def do_command2(args): print("") print("This is command2.") print("Required ARG1 = {0}".format(args.ARG1)) print("Optional ARG2 = {0}".format(args.ARG2)) blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/000077500000000000000000000000001203633570100306775ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_controllers/000077500000000000000000000000001203633570100334045ustar00rootroot00000000000000000077500000000000000000000000001203633570100347605ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_controllers/example__init__.py000066400000000000000000000005561203633570100370770ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_controllers/exampleimport os from blogofile.cache import bf import blogofile_plugin_test as plugin def init(): #Any setup you need to do before running goes here. pass def run(): #Run the controller from . import photos photos.copy_photos() photo_files = photos.get_photo_names() photos.write_pages(photo_files) photos.write_index(photo_files) photos.py000066400000000000000000000023101203633570100366420ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_controllers/exampleimport shutil import os from blogofile import util from . import plugin def copy_photos(): plugin.logger.info("Copying gallery photos..") if plugin.config.gallery.src: #The user has supplied their own photos shutil.copytree(plugin.config.gallery.src, util.fs_site_path_helper( "_site",plugin.config.gallery.path,"img")) else: #The user has not configured the photo path #Use the supplied photos as an example shutil.copytree(os.path.join(plugin.tools.get_src_dir(),"_photos"), util.fs_site_path_helper( "_site",plugin.config.gallery.path,"img")) def get_photo_names(): img_dir = util.fs_site_path_helper( "_site",plugin.config.gallery.path,"img") return [p for p in os.listdir(img_dir) if p.lower().endswith(".jpg")] def write_pages(photos): for photo in photos: plugin.tools.materialize_template( "photo.mako", (plugin.config.gallery.path,photo+".html"), {"photo":photo}) def write_index(photos): plugin.tools.materialize_template( "photo_index.mako", (plugin.config.gallery.path,"index.html"), {"photos":photos}) blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_filters/000077500000000000000000000000001203633570100325065ustar00rootroot00000000000000filter_to_override.py000066400000000000000000000001231203633570100366630ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_filtersdef run(content): return "This is text from the plugin version of the filter." blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_photos/000077500000000000000000000000001203633570100323525ustar00rootroot00000000000000Kellie_Mappy.jpg000066400000000000000000005221071203633570100353570ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_photosJFIFHHC   Cw  T !1AQ "aq#2B$3R br%4CScD&5FTst U!1AQ"a2qB#Rb3r $CS4s%c&'D ?['lv_>Y],yQ:cπ8zcVD1#\k );Gt:ֻ3* ptSvFANUP8i]z~$g'aUlt$H޼/>\J$ @z2yPBv*K,OƨE'2Y^Wϧ~7N慳ຢN;v$э$(*6*>]9ڔ$ JR0H*OyZDFƱ?zWP3:9 P*3,+ Hl|=~*ؐ$+ Nͧ8霃8Y$e_m1v3ǃtR`F Ik rԕVԀT|¿tDyP0Pӟ$t>Vt`n(wp'?^ [Fru49in_ xΏJ>(-uMc'aE#j^iU ӌzZ pF~FaR[.IӽmGS 2 P[P9CPz3ӊ\p|1RLVl09Pt\m^_ca8x "~p` AׯʐtJz%>+0!LJ`^2> C2)Jtv/b>!2;"F[WJJG\UeY"ʬV'.G~[RH^*Dex_8KNcoBR:.;H́6˪9ڐgs$} զx=J 9"xcj]{Y'ZwS*!MYG.w]:3urj,ܗ4 -!Ƴ6~vA-/ qT`PGIHȝtN,qDZB'9":y@ސj#[:[qR{t.HWPjg$۶ch0'(ЭKIoL&J~>{VVD8W>qJ[ejHI$O_@ЛL8 ,vij脨+zS42U@@Oj'm =tit3?Tpz{Gu^ciK9}_J.r¤*_:2J ~n Ivӥ4NI tI?˥n| tq:D8GcoTi+SaYP$rW5Ek(B>}XY>#IͪH@^R?ӅL\4`ykQ$ Ŧ*]B@l (AU<~uuÉ%=ϑ@kBTBaГ=Oׯ^Tb6|0#ۀCN)ab_T$vKxqBHeo]R @z~(ZRv:t:q(A3*Pғ!t Ӯ1 QB ]CLt?/_^!ZQQ{bptaƒh떄JV:BԴy}A8zpL" P^@z~fs=6*S)1}8#~fKgDOHw돦;V#JG],e(aod?|`uQX?ֺl 7 G;9oA )^?8$SJB@b/9W֔($󡌠m-[ >^BDQK-IʐdiJZt4+Y#?ı+ >)Mғ[훱͡0#JO_9#=p?R:y17+1A1J--+Ο=*Zs/Y]-6[&-G2O5Dzo8) Pza=7asjPFhO1˔N9ÊRtZs$l򑡫ƿʆ;]U֘Z "dgW{^CXK{Z`-u, LsNb?%3,ʕ&d]5_4ˋV\@=I5i dMQ=׾Q_ϊoΉP*~PS#VÎZS9`,ssŘ)G)hm"P})VխI1hT PzQ 1:~ԣjWeGhu>5EJÍxLg*?"-ق:=_\i0t [Rѽ~|KpuEX8 %JLZ RXU]jL%4JC!-aE=V8A#Î1X݌Ay)5I~Roiӡ/@ )j_ = })J2MmJ Iq{p81ľ@> ojPTJ]|É=Q1ϟA^¬䁎H-j=xWL> P?>^u#Q;JRÝ5Q4Xp3Y:BNi@hP|i5%0Uc'^.JK=׍eP)9郞_{Trs@N=E, aGN?o>$Mba^$GG-:8 :RڼFz+91{NS6JGDjvwU0JJP;Mk꣤6ӿO}ւZ j a Aؓ?O^4YCCMZE:C mUg6c0y*Z&M8n-5@;n'M>i¥j_::R{P-J\ԥl7 #;<5cԌ:w`yKh@G;Fƭ۽@CNWhG< i+[b-irR)!%i$ډ*:dfT FD|tԊ۱Я.ͳ(Q9 Ād==Lirk_eɎDLx*AOL2zpk k JS Aӝߢ.gn/16HHQ +CJE,Շnu2MO )r:pF9Wjj eL1:iƌE6]Q/+S]H$0IlKCۅT*OSI !#Ӱ]ڥ*FYvOrCLiZܡR@BH KNx{StM@BjMX}࿄HwC? ?LU1rz]x[84'Hi $v?ωdJz)3"9JRV@?"TI:0|@$ɗ4ҙn[kJGs؃g})~`5rEQFczF 1 5Y]uuCt89VPHI쒑|6bzب~ Zhl =n VP(͒B,y炕CN}Di @+R YRa) y?È raIF߷T!PɌV !IRV(p)%zJ"zOש L@޵j^ #kN9~&<Ƚ.c1gzq7x I%Uwmݍ#(.4&t(GZ; G>̭-̈\<10$uUoyr'HXIUG=z8腡?\+1șΚjl* $w) ľó6E8yξR&~])Xw)չCb2 $'8br1gklΙߵO΋PJ}lSHBtĄׯ vgnr?SQ/*{onouw8[\.= ?3dfVJXX-;+aC( f{lYfIJ=~KQI*3?4لtGUhsE~C[2'#*6}JlS1tK'iAɐqin4ʰ:z}?Që7ri!9@D!%D\u<.m7նIۗhB:*BȤL?ָxh$ӯNVi&NtJR%}=hA 2ƅ!;:s׿~ *9"7Ƈ7%@lׄQ Wh9Q|j8ORps+VP(AlL~ }tE$ <)i2-Z%8ϟ.vtzC&s_D4 葌;x"">5;>ÈӔ*?i̐HPf3G ԶGK;"\UiQ!T*#I[iw^T$[DmJiOȔ"3㹱N%Ԕ/)<'ֹ7ƱX[Ĥ2O8*ǩ5b6b㤱!J$#9/]ZԐ&I>ׂ?eZإ m$iRV-gǥB~_i%҈ڎ)lT;c<52hNc'1O2kw@*R@Nӧ"tʍoHf f $By7%,T>#؎[c $xIr(-][s%CP#RgԚМ+}41e׊PrFzcWnS<9tʧ-öخwvS,{ IuZȖ馗\`7u6|x9&wEv+ G(څ%Y';ڇq wFNd3sPqW{^3jwR u,i]TG-2Ly-./FSㆾ6wiӪRA>ΠD7-MvP?J~=_inڕQ'hZ]e~P !hgazu8v`7JUGS_6qwex-qo69@tz#)n.GsۯiNSI-R}QS \2a{g1ʼna( 6Lb4Uߦ?.'7'Nun#z☄9:~]]Pӥuj#m|)?A<Nր`;l5!2:uR3^h?yr$M 4R>B`sWv1mߜ?cT{'8 Gݱg>J?|Cvi)g-2ֵP …-Ʒ ܂!ĭ)CK Rz}8dX>zA\DĨF|sBVN1*Ѝv3o7E:WԟgRKȧNVu [oW݀Sopc.(}EMnT9r̞h]WmzˬۗIi4e0Դ!ĀPt9;xxYT~U`Xp ʆ@#pA19T+qu.%AiRBҴ|AI= A0x R~漲mjB#cUG!LZ˵=%r^L֤rR䶔LOd s=Cc{QWCI*dsҮ^ \WOmSƑ"݋! [jBPHRT*AKeAG*^>|ƙ+Sd$~p=>\LtM;$L| <7wp1_S?۞x2"$MzOy~]0F  bR ك4 3o*NKm)V:n#9F>:2Fr4G4JKJs>xmFs@PWS{p ґ${ABfʤͅm<IJqח8BZⴰRRRxR)#ݘ yz#6.'2&O.qr "L72J鱘Zڐ7%eԆVI]:$wf먩o85҈K9u{颧u(Wř6:}+!6s$ d OKѳvMayhJWs 5NG=8;~ml! IdBFaFtAz󽤏HwH\8A`dv.p,l>?H%.gmg>)S_yQhtTiD,IWőaJѫ3Uj% [ANݻO_%ۀ)R5$)1|Hm(h~}bli{7=sFT~K5ЂB$|j|n+r޿{?GΜ*WsV|&r޵ڱR8?[->zn0#eOFDә;̴6&vTGW=\q/ τZ?ZOQ'R{?G=|E4SI%Z,ZS{HSH8ʄI?Sn/Z*"!1 a^ytcȔRSE0{4Uw5Ԥ8е-րNT{E?SSL?n~T&Zy-eFJ^|' DE4.g5URٓ2mI[ٷȉKk}ZA;d\7MS.vե}?WaMY#+Ҕz"pˉ5q hh@' Kj;ROUzIWg*Ð |vت qw?FU sICgr2< Dč􎜠drs>}7<Z);,+rޘ8rgΚH>:Q  6= j)d+o>iӿ _JeS$~-%Yr4P2ȹ4Zl c?3ӧψ)*|8U niդGt)# dcʼnQު&b*駾TݙJ0;>>'ַW6LtD(RO؞/NtDSbFw\<('.TT '<KB#Ba nt8$Mk&(2k9t< iC) T(ltO9fe!{'˾8―`YޜJv'9W^vV)Q֣)ҋIILVp{|h~h~ݴC[Ց\RΧ*6"ف&SFqjiXPZʈRrFNzuO" 4I*JA#NcHnf'O}tW޹l`|Z7`1M]!Sy4%CN2ьa*8sbŵ6)ԑ, U8>[XZeS"$@ #6bv©uv@ˁ:A P̬V^a$RTbc$BcM4kvیА@+u"fyVhY \ ijJݛm*;ttx!z<nJqդh W0'`o+9pjJ< i>ȞX(UWҤ>욝6Zl:♆lW^+]Jj]2vHvX'%d|KC9jo[V]{/Z&O@q'+~:͇h$ ZHm@RԒcxp7Ql[o @45(QӖyecer{9klG ]Z]{tTJU˂xC"DKuo} įy#Q% Y[ B T ѣ[TcF hUW_M4Ě{suKۏM0.?Xe$IґY$`@x˗ORkS}{L-[RΕswm jm.W A %M0RmJJB! i7w4,'\=]q[w-^n(eUl[V}j֊{ m TxQK2*1Or^-% HyMnS`͗INm͔c0$uFx0L9_(A0N>0oV6ZTH׮XL\Ei*L冃i GFn'ć1#ϝH˛\} 5*RQn]ie+2&tBYۋumCm*\_`},Dk'D 1Ę q'b%JQokʦEVnsgiUg^sP[4Z!kmϵkO6ZJ`>q$[Y(xtdb}S *P[AQɎzTμL=ޓM_1;at|:d:m妡1_iiT%^TqwP]#?>$@g5eberawFΚtQֽoQCU]I4w6KR<42K\kKI!łmˢ?N_3`ˀT!:tġ$ݘ$t=Z K6N4N%(.nRVpH:z}TпG(yЃԱoi.ګv.Q(J\AJ+hq`Zi"kPCdx6Fgophf'm8c^;ڥeRVR A!< }j&4Vn+vi3 N saH@~n{K+KB GUT^upJBka͎q[v<{:P~d~]{X1vVsScn+Gr N:Yie\!@${‹I9ڳ/ xj)DTQO\K~kWrT$|Tx{I:>l~|F[N7OiuT#fJ^=8~diqIלE$$9q!H@)u BTq)Ɛ\:Ӯ21~|8lqc좕N1zvZU \@uE.6o N}0dLse8%q (QN{3)&"I]:"3zم oxRE^_/> 8*bǐ}I{mMc~ϋQZamE mJBa]WKԁ?UuQZc(=xp.i4&IQ*۸g珗ep4eBe9ӓpA'Z2F`"A>gxBG0R0H$zg#?>nOZxd OJjJ T5&$2)äʎp@Nޞ|E/>PIZCANINFi`tspoO󏡣xԌ*w~1A <6*Qa#+)xF ox/$!QRNQpKZӮ!9 UsHVwb7(Taq~Yh#UN-d$=ꒂYy S% G^emjRּ,%3Hz}R\p1:U9жb+z @ RVP*c(@̑I +]+ﮜe◊Rz iPIy Ku+?`TjRpI ok#  gI$ 3"TVrʓ'ר?Vư԰!Ӆn#)KkOH@<ܤLBvxN,u3e|ISDi\C P\t{I5"$QR U*F:Q|}&&u>-THRf%wlId~ĮUoGvaNuFh_1%ZwZgW"/NP[=Pua跴.](Z<_XGac8Lj(VwxRim_&UnSi[^ {"Lqr,mLd=2iچ$B JY5U *\OOhb.'D:ơ[3s+b%Ɏ% !*+RD?NGA^ oeI˪ɡ!.o5S' ِ|J;x~ R$!(JD<'5[ot]cTJVR']P Ҥ =֣M՝) SkK CnGXݖÄ"Nf.(*I82<ս{Fj^r>kT`Ӥ6/<l”1Ł\XjO̊;C,V~Qn)0<¨n6ƥԼ,--QDd.]:4y=j>/k^V_V3cNjZS3-i8 ]r!`MG=,ꋘ;6y%?uB٪ e9$>~ϗ ⏹JA?IY%0Jp;)`V[ N>*!@?\h'y#4:ârVñ_ׇEkOUX,IB̙*+u))WL'?+Ci39EJeGSAԸ[oun>~y)eŨa ;N~^@V&̊l)c ` xwI?qXQP\ڔrrUω~b5q%:R sq(Bt TF7cÛJ$Iޑ-U6T=q`^TBκ%I8=<]"3B_A3τnO*?2G ک2HL'?-v9iiOyI۴ӿN5=>t'Q^?ꔃMt\ m?Z+ ~O On$iJbʑ(8%rք Q=p{q*;JS$<>ꯘ7m,q./($P I˧Q;4C\TLϫ'JXt!*I e(BALD.cvS9n龦b%fr=C }|1-&_(plПd?B}ͫv0ŭeL)T Bf#Ω֞n-4vSo/Vɒ>1RTx*YyNڑ.hƒlL꤬$7 +uxd*+fK}$-0%6LU%q&./-kf4 2ߓcUW_o2C^ܵ(v@I5@A`o(úY~;oJP̮eDΎ̓WrcܖvJTzTb!tRP9es}6P2 ʓ2rAPLڑ#ԎeUX,2 Y&ُ4@ TYmHZGh"֤JZUII[3Xl-|9-`wn8-Rm H%KFT'9G:_npn2q-[gβ[J TI A(DofVY]pj$DNӞ+ۑHn'6覟[(X ٱ@lZI/?ax3UrP锤H|k_mjj 9>44a5~inEWzڗLwtzu1!ZZ3\LPvZZm˥%G{>k C8 ,2ӋqEE&Hș+[(N˕8 }?}j)CzJԊѦ;it<ZSZ{Q*k5Y-zܧ_J{wj.Jd.Cimu-C(W)RfݶP-RG< !G:$;E$yZm.urB%RWwFJ2C6ǡ5wmY\>, 6ԖHQ!Y < xm(iOz3!>doxQG a})9Y1`M‰!8Р|Wa)JGAJWj5;K*vdƶnޔ[؅T:]U@~jStt:Z6ĕ.Gܕ4¯x qapj)v߸'մCM 9+uq4e\*C0 tœpc׉bvż5O_#&h-񵭰l8+,-gbMyi@+ $DsK -( ygxpCLF޴x VB;z'mqaJ9trYJ' /׉;V`UI$eZҒTrg"ۅeʂ)g8ϧ߅5PaK@loE%F` %I㎿ϐ (oQY @$y~%pQ Fs*J~\3]{4=H$PޟX'iY Q_n@5#R~s-:8{*&TԞFb"œȩp<GuqƛS2+5*TUEzcp7wM$y. ~:~rݦ=PrNb2"e)L u5jLWpT1/PJ3(!Q\P G\$IV!t lLTIՐ/!|W6zSqرc(֐)}cPv h)QZj{ÉeiߐCd.7q|onnVM$iݤ'IV8ok<WK8$*fRBP+9S3Me{>b.q-ʕ6.[ƗPb\j3˞4VҔVMz^Zτ'w@ںO}9l+ma$CLg@H# 2gaoSr^<nW*HKrYېLBȅpÛ >$4x.2o\j\CEŒbEw,"J!Rӕ'R uN@$U}zLb1aomy^!P@Ic1 Nt/zŚkGփmW.9m)G[\ &4d9BUo ak uK ;u |6t*ڥ5zܭJ"b]j$j\fQe0[e;伳xsKo[/KQYs I! l"t(;FTzwW'vQeJ,v}%TT1i) lT*AS5M7KrZJbH۪P2uF$jҭt=|h_Z~j4a4fsGfIgrWaSPYKk [aJbՖICAΚI~(gCΥmmn$f +ZA%*qI>`\K ;(j}:=sݖ Lf*-I Q*Aoyq-v?hmݲ HVvJ13;\G},s(̶bTL|h129u}lTneCK4,FV%erwp |Km2´00̓ʽ@ҫז-7q<픣3'2B`&N 5ZV2ItrE~f̉Fn33.:0\$$g᱇68zQ֩xWTq_4E 'W|TNX'Y,u#"@RHֽq~W/$QOf:W(HRXOO3O 5[k}WujR*3u̟_ߎOU|eGW[ScM#o`ǧIQ nt=z=pcI3?I ;1ГkI@VKd# 1Onx=ƴY({c8V|0t-AH ց9*ܐ@S8'( ҄)%Igxi@A 4pT1=5'Yޑ5e8Tp=1Cx-Z*ܸUNtJT:Aw& Up8\*JzmIcCsܢ'9ɮ;r m9=ǀBR<9<5No @Q;ʆ:ӄʍA41``|$tu)hĘ֎+q3q׆I$[A={zqiN8$JHX;g>}8gxkI-q ߕԕXWv .pF+o?Z/ooն9lI^:CBeiTD8ⲧSҖMRrk";l @`OÜ>\\Ho)KvN%^=/iQҬB4+r}$%91툩1典yI):@2u^5neN%T\ J@/D`ݪ.TS #>vI?5kzUrf&оMO\(7gФ;K`m>R{ #פF{o2>[nm{GRmڤ&<m22p-ϢP}8# jcPJpZY;svt^b7.ZaL0z=e)5#IP[rR926$%,32d$xpb'(\78]ZS l6ϐ$Ε9;L-ͯl>ƙFJ\y.ãio%8ӫ ⿀ RR֣ @&RD%)yuo͠Q+"I#_i:Hu\hV^=Bbf)V%&ʼΔ<@Bsn=oN J'ӼJ)_)A:sy *F yAļbbEٗ-Z[j12PeM– U$%+P?ӃՈpom*URTjА oI825wG0tӨTim3.(;LhuxUDHd[xe.$ X0`6 qIV݄\!Hȴ90AR6Ra@iQEai{!SDPJz)X`|x`s帻yI;Aۦ:[fo[&Qh 2_*ç} 8yPW/gE;꣐:$Ayʑ|P)|6>L'Q&3f:Iغ3ˆF| uWQhV=JQLݖėTs;SH]X,KH&e@(9ATɓڬFkԜ^ Ea,K3h0t!92I ΂*|Z;!\\ORMBLuT>ּYiE2ДI2U ~Vg~$$)@"9BHjN~Duv[{T>]>s ЩxؔrKjt֘t)͛HOTJJ N+rBILA;u*8-8WˇvA RfJg''Zk e\󍭶7,5ʽ>*0+fSdtog\oPH3 mvwa =|c\6N!(2'OF (p~1yёo .MqAu 3P:E! a*H #^:E9a{U$in)ځU#e7N6ܲZ-T? %?=pNxbUZR#092EJ[VEN :iL;i*2>mm2~`6K{ZMqL Bn_*4J&zIjrb82̿G8HjCMTw5^$$!>δU+^}t2$G7\l"#iNOqF#&$| koH\쟃0fT mZJ*ޡKkAj [ұE*nKV@'kr|3bpS؋[g ?D|TNZ氄mif0+>ȫR;f-AKlZ?\ת Ǡ@?8N(̟?G8_W >5Wӟf}mK&ӫ`륡lJ@!J\dۮҧcqmʜJZb=\pV(pv  0yOJvٛ\AlLlO]Lk:R9,SRx%6]GRPL5 .IJ2qf:rC[\i Iǥmp)2U'zG:|p}@Yuޜ?hDhE6ID qU]yjiN%@4иno_i0oHXḊ!kuNmQ(tgTԩJ͎]!BRHKRv$2FS) ?JSex ` ")Vj:$b71>ӥRW9oYJG1igMb;!ު3=dIT\uYݼ% O8^ؖ$&g] {WHaLZZ%pOviI&0Z9;p.j,mC2$@Z7P}K>aJn:8Ҫ&\)Bw^T{s- ǜ}2$'l.ۗ^>]:IMkSUҚ6?b7w)crooi}8;şdwAL$|}z?X3R-ZZB֙QGE&M#OCLqsY-l |c7w]w֑ܲ&nGe :Q]*F_ R˸eR)rT;N4 3K~XsЌ ӎIϙe^gV2*lʋd`tJ,Ӱ)nJeODӻjԃөz~2Hڔ/$zgJ_!*۸'p>_^6EPU&'>\RVprpR<_JJR228g˃& !*^ h9xR'ʂ„梊T H8BSs$v^ cZm4{p^x5"i:>U)BixRwp,i#u a( iSK%m] P'Ùhk)uGCO h1_b[M=0nt wI8]bw EtV6;6Ur%SҒ; SRhS )#N~]}8H(I*~%|t مN.Ԙxk -uF{($Ғb=SR %Bq!ԶvHGӈ40Rc?!J{EV}wd;b1?)s5ǔD5^=*dbS B\ 9PJU1&TL8d:d1ȱpWDNNύ<赮tGJ5F-3% X'rʉ>jڐTTfLj6Ĩ'(J{Z&T\CqL<2s Qӷ mTgVO+TRrV4V1(RBϧƤun',iL@QnN$@ZN=5},պJ̻Qވ\ShRKZ A?p"%iˑQqi?tpQqH 0RuI PyًbMv7K U]n1#ʕBs2UDT %he HwrbeK-LeQ} i;,Y D0i.)04Dfh92V*Fbe4qܕTv\@V$Hܔ ChZ'~?rVL,Js 'S@IҬ.883on +*;m)!CyJw+k^OufZ[jn<NB ST8vMZZr|כrI@3 24>- pO\B.@˕-ӔH*\!=3R˴eޗ'1:iuM<CJ+p k9!I9>dTl k-naBSޤDfw$Ieǣq]q_c{W)gY%(HS λ}K P\4tGa5ƦiB%jq%GzL Il;1dɯVMMDݺN VҙQy?润%7Uc)z|#ĕ*NXI5㍠:{Oh IS5IRH;3p舢\tlGoրK [yAI9ݾ|)`hI4hƔӅR29ׇAυM!'):h)J9Q Rzc=xTi=mSZU9 IKX=?Qh,~-ϟpsFSjNX{c*>YUZ)IIܰHGOCdEk7*8F]| =ED_#1),}T3'C![XB>')'tmvմ.@@ -BBRzxbkr>e~?ܫys][)NSO^ )4D H۔QG$(m#F Hgk,Z0a-:ӌt=@˴Ҿ-Y*QŠT'^[TJ69\0)~i.$W{QZ XĖW[PI#V3zn]byP.mr>BƑRHH~Fĵ=s~9tmluD- :?i-zUpr:ds7gDZ8^#:ݝ*Ӧ/pu]i9>)8j.cp)?Y-n U:pT*1$="V{Zv~,XqpE$(ǐI3B¸mVŧ3w65Xbǥk?7[W)ѣ(R.=KOˎ{Z;ljGG\pF% h64]q? "ы+m2)嶪fo Am)>-ź =0n.+e+͞ݱ},}^|#jCܔG-]Wj-&${l#o?jKҼ1<VUd[W|>}*Ìtoog#y%Q4$_Ʒ*Cj~jCs!w5]Zg8-%q_/,vDP.1 ~!"*- PHWq׋L LuI(ɨvjuZMܒjJ]*\iȮj\pg["a̸AP*CuJdxHRtsLV:ЍUԛٮѡ[zW-IIkbJ":eC C#Tr((p'^ue9B wN'OlAQkG4+jVhvZp^U:JBDpc[䅀Ɔu¼)re(Jܦ J$boJ3D&(sF(A/5|y( R%9L(*A`6bcaLЏ T_x$ Ϩ̀BY~埨NhK֬Ó26M‰O[,C N&R]*^il#9WW뛛D4Rsv5:"4׌=?;)°L/nr:9B&T R)HBQQs]uiݶ-ne|TJe ϵJ|#9tIA Eyr̕mVurUo ϱXM^kmЩLMy %ȫEme il6xw8BBRYiIbI-(l /򝢒2eũdH$Sg}4Z](:-zuj ^\]ZJj?%0\xcCg7Qe4ᕺa ins m+#HH:9WXoV[9lݨBI,$u%I-as-&KoT왓)z;PCȴi6IS2MܶZ.R b[OD)ĬnLT5$cb6Uǽvy69|uYT)AΒRBAe EKMyrL'Tu5^fr#71,OIake:p(wzC : '5eeVh i#(qntQ@FꎛkͦLtz7˚B63LjUa=׼P)($ c&L$6C6Ð%#\m]cV8ڋXZIZ;.*Y:J KϢ0_OJw Nuxպ:O) +No &34P=-)*e!v:S_;Ґ6I996PdEVjrRcj")5x=sc-u߮<~rdGX>=5M kH&%?|Wg3E [bzIBϐ>|)b3)Ή{ kP,>˴nZjϨI!Ų.z,{-t$*'cQq{-I {ʐTmjmuT#ÛHI;);ӷ~_vo2L ,)J,xp4ZK >B$x\͔Ak%HP;lfkۍde_sHIsO?* g JTQAE'%8HJV1vaZUֻwBuF:)G?o uj˫!ׄv bZ?MX|V4֦ d\[Õ-n̏V0pވJTN!@~leIEFqە4l() Hj4}J rsv9PhYTrjip:@..-h@P꒓Lw`mD8YB۬BPtB12T'BKB|u*iu-97,~ k$ʴ7CnZu`_-]zL?RmͬIfڐr\AYS)uuw^'}|⤾I t&Tg=AZqW pZ6S9C^ "I53ϋDZJT͵?mO/R*E.%>]|\scxU]6K&&3k{dV%Q!igydm@[emt7N<~%*z!]⣡ʃ11SVRQZTTC&LJ*T.LNQ| q!J.X[Zw Ii0E}$qe'q7"2$ DJsy) NLyJ{--^us$ּyU[⩖& q@hZG7\,:j :'Sn SUWٰz8Y)Wz ZID쬹*JbL[Qw1ixTjfڕ6=ݑ:O4T]Ht;,YR|7@]#A$T$AbFMA0I lxtn!Jd qIRrdR%^*JG5~<秘kR#;R)(bitZ )H n^sU$m)LI_GJٵڗp-ư_&|,o4{3"FSզRC)?yxvtssd'-[\'w^aƘ]<㤔5O>tviQ$lUڡ9u?h\v-)=w֙{GO )a#Ea횋s 'Ze>Q`7:DvD+8.N?OԥF/t vڞ.nM5c:XX3:+N$a%0PvTATZJ()$|׋櫐zWJwqӷ1Z&biUA5B1ͲAMΉTGJRIެJ):҅(RPd'> 4$$'%?1y GN5%+pdG'J9T @S89稯eu:I'('`N+ExsF1BHH$zppbO*RZjoCjwn' b^]!H>Ą$`K-2Tdπe !RTr(zSBIGE$/zRGA˂`x[EM<|)LZ!) Gȏ,qZvpw]U?, A|%w]a 6]pˇ0rn@ds)= * \'h++)aQX-%#8|!ko )iXefk0DT}%lMQT(y:wyЙ ^h/<'E.YiTx Pj%8j Ё5oRՒ+N2SkIg$P*AZGጄdBD F FWz5y M ;%_y*+>ΨSgk5J:gzL4J~mHIL#μ 4BG㩺5󩜣mqZW&>G NBKYs:tS4~$uSqyحbrRK L@HR Q^]Q|qŞtLV"d:+HtdyꛨƟQBnpGZRkK5UxԶcK6( Ǝm(I IH\#jy>K4r$ū8Э)PgJlBȓNT{ IT# :c]b,8~onЧk:s bPDs S@A}  0{ʕ+C07)A$iGwyRwJoM!.QeqOuojݖŸxZ~2H73ֈCd>YX%>J')y@5FplwA<!C< *=qbNW/FZ*M*OQt.6Az2rR[JoT?x;2BҠ?,H: &8>勎ɼI!Q%šH4n]y"ʓ#;Hc[j(׮1gbJh'Uup)9oŕBnE7&Ru*RvqIo4pvedAzkpON{Kӆ *&)PB"@;%zSzNk^:9-SJTSᰖ҄!$! pkޡ#s: Oۗv=8jSЅ<4 7!j9hahuIzBzuf I.%NA@%}cȰ BILj`y%Q|bv[jiJtB|D9f@5(os)FVp?8crtO-'[3^tBz%Jƣ"rtTJ a(n mݐ8$|ޔ[KMԒ@.Z=t;;/K/Zݡ`Rz{;.eMD UQ#'P*cJ[Tbw< ;gFnɜGqb9!!)CS%jx$Q3O]-3~ %l^ ;M&A4JBms_udT]z=w!9k1E+A#w F#E Tm)NS+&T,- {|s`Y>SuO*Ԯ[O4W6]-OTgJa[L(A'$tH>[GZrN>7͓:]A > WELtXiDUq6"T؎#am_jA5ʌ&XI RgJf%JQJUuJd Tw#- Þ/H}k/ҘBVvOM1绡ʣAi'ף N4Rz|k wQMJ7! ..CY1y5#JIzVFQt\-"qݠtQ+g>^|`CX־'r@PZЩU lnZ9u AqFnXOڬb[p}) ]~\;YHIڅ)JG@=G׃ғ$A=zC|h,VيHڕ IW_94`o%!́x` 9>yJ(jnde޸ / VSFLq[:k}(ɰI)ې};ܿ*R*dk)Mq(%J ~#v>OxH*;려WKAHR:ӯழMpnCr>~| \ﱀXǟ+b Ɏ(!L9P=@ʌDiSٹqCr+pi28B, VӍ9Cù0?3yfW]͜9.Zd,֩V-ɢOeZ~OeonThۄOz:í!_ u.7aVRَms|oݠ-%Й zJJx$M?܃ bY ط Z%%T2ONR[(*2!-NCkՔf Pܙ ȣˑEݭxqڐR([FuRFaI&\>}XQ5Qb%7eQ-ZU~Wr{)N˟ $x2Q{0A5ae._#.e5ZsHǖSîJ:3YIJT59U9k.lT=R6ʄ60SSi&9Т\$)ү^OZҜa-愩!GP>EYKXeBH{Ґw Q@v_2e zs֔3^贆(TJKS Ma{\Z߄dƖcøoq.,=onκRm@ @$Ϭdt>u/a1wl}ķrI`&dde'bf/]LUi5v'f^v]n:=/m<Hi%’'қ.0G'q#uL,a% hweԒ3^xa4 )M c Hd49 %gq-̓5uջZr;!eTe Q%GZxʮYm(q '2T@5cƸ%_X0a>o}xOҨJ/l"W[b̸Ny'؜ N8v5wnO%3$IDAv5a{^(6e[ȈS QiUʜ 2H)̠+LsLoQ3C:,aURn\қTrDO\~ ,GVIY<%f6Ʈp)U9Ji[`l$8h&+<Ż~1,J1Km, .*c.' IcPtqL])5[4dCjzLq nNɒaQByqُvY[&XRv 17?H3ߜS\E!0i0 * w9v~z|4FYN}_kS#=:'Ð:eXhKZն $'4#C'SEvA裌HHs3p&IQȅ+9P6bnSQKvu.έD|O-KyG~nkKdLBG:rF2Fq2ל)RFZZR;uNDP9M !I^C7ګ :M4eoxͶ!-,nPFR)&$v:bU J%:S~ݍϙ9ׅP4̐@;R4?+a wt* yD>hM\ڈ:aOGˮO>jēoin ?O覣5 %)Gۯ_Sӿ;iFޤ_ͳHCȑCҌ8R6);pUFK ]>a!)z̓<+[[$f ZG40*ܤW#K e.JvPCe!҄q*HS87+^P%cYT;Ԟ^{kJp R!#-S  $UmdYEtEۧzvÁ՚ec BW mPҕu6)}۷DHӤK g'=rHt$tKqXAQ:m9 L!+p UfQΟGogm3ynm;黇8dLi6*yT~)ӆpQxMFtOHRX ru)8~Nnx$“)ݎ2蔕kus!r֖eKHlmJA{w9xO|qJ^R8{ Ѱs֔ wz&,b^F@l"a3k` QÇ0|-XS٭Uʦ.q~2յP5 ;C;JN{3mH Ijt'g!n?e=)%'Y+Cs@l)4Cv2-Q̅>cΣZo2v!*O}_-!Mg^-fS4q0X:lt?/V1:Z3{[jv3r"H=I9XbJJt@{^PiaD+X:"wviN%_I!irUOR߆@:;]2guICS5n~U4hTv%QJv65 4zŰkqU8iK ZJTi>qͳ 9_Q4$wx@!(TkB^[FC E+h56.X~McoPb|,;n ?!O֯ ]ZY[h:zT%qUY8*%S q[s'pvPAmI$SߟTbrZ]rz);7Of:> G}0-6FU @I:5Y{8.*mP_Nu*PΑP@U3RoA%kxVEl MjW3nJWWW.p`MDf2יH!?p|nv%HFdw*^E'[6 bn®])̖ @"LJD^UW|T=VЫ&]*.lIPnj\bL)1ꑜ.K/;Müen9{-G C캵[kk@P#+DFw;q0/\/v|'YBr2 =Xsܕȑ9]RU-Z0"!6-t! O;8a 4Te]#1!-u&k1OIձl=]"~j}T)$x;H`KrӢ78zզ(,U t .[KN}߆QZA%|H[ CnVp pHʤ&HHPk0L$vqv165n嵳!E'8YJa@3$e$V[ϲW4 w->֪fuQ5nd3@\oEr<'Ö\켅ĕOHXM9gx nh%253*WÍ\غ\iIWAAYH0AH"b${2w{Q}Zr P.h\r[E ζ7Q%=cao6p@xlq~ i2s g*ˮ%FLҦԤ()׸x 8lR3Ry5'M'{n.guezGPd+U+k_qݐj4S0ږxr7)̴\$Y.R Uqݏ.Kԭ^3.fP3):e$?g#J}̍hDߚR¶\Jd¦)d)b<-JPBIxH3OkM[,Y1^a$}6ntI#nE@iOhJ.Y?[$Dmӵή]mU%30FH TR=ne4j,~S 4(mg4 lp<*DIRC15>+@ sߘ}.۹NZK?ü7) r=ͭSO,+Pk5[Ѧ=ߤɩIf3R[f:LdhR~'͍h1Cm'1KN{-:P&;{pz‘fdLi|**Z^fuIAKiyeLB%$)*zom6/np4VA̓s*xo~8yiu=-ukQmȼ2tR^4jS&uCũ[Ԅ9Lʏr\CeƟK-3THIB̤;V\V½Gxp  g)Gﮖu:xM)5ǫi}tPĠ4x:{ʲ[UMЗ걢 Ԉѩ[)T*q~sݶʖ֣6#A1 B+pqUxK> ^IZ %erD%U}<˪r|]4֤scӷm@Rm(r@U%RzިLREBuu.~ebr=}35glQا|1wH'a[˴y9mߋI.Gq)'綆~?.9/h^o |$zS^z_sXíy.({kLP\#Nj++lz5O Gӛ7|pZJcڥTSJYk5*;s}ҭ MVõ+VƸhuML*\L0S |yJ7`Xsrs~kyVnK'HnX[Tk1[a:)Kzd7(INRS0CjZQ'{k%O%Dp~4Ǝ?VU*OId=OwTSc>Z[ ;BRVRnK#m)m$nF!+<O3iÓ]$*IH#!}E:ZP#4PS YSifb:;\.8H|I쑔EF#S5%gb#Q=>njFQ#A\KjBxОtX Iܡx~K}{8J҇*tFs*es9%jq(kXSZȮe&\õtm\=%՞S7@W)6]^:ޅ릛Qꔤfur˝ڧ6VU:Mm3]f*̡jqZZ<4NR0Fs*_,)뮻݄LT`AdU˜Eۮb58TyDbs9YaM8QnZ~;@͂LJE0=?PܐriTmE64Ecz䣧Oτx\w4}߉ }+*?Pשb N˽fo=Rv'ڟw:;E/Z[Ď?Kੁw% sf2(5*{)RV;d6G_QWVNR;)J;JφQQkr9$ p8-ŏr l1[Xaz)FOݬe ;w&Ӭm#8;Pۄh$Ø{4Ofz<ެIVyEFGL?2סE:Ҝe8YCAYà||E_CgqG×M'J y:dl 鸫[l]2m %%JC*K)$ \jN ֜^59^\ڙK,@;r׫j=rb1u_ J+@pDq^p۶f4wl2h%%N%+@4RsC*z+هHN߹q%4ҭ׭ͤ%ItfV)tJ坋 J]4^6"Sx]Q^e{ N2l^BřyZ5!v#U@$%6 \U'X(0GKE+ aNP\pȒHSwmj[v%}*:>%Kz hu#i;ѱ?A ͝zS8 Zw|s[na-(T-˶ͯRDHyvIRfLeܥE"TCjRj_i-|AfEt + E* ĥCS 5]` ǍˇaJ)Y !@(1i1liSMSMs/վ`m :`7}:ukFbMZ={j-> uFU>>Iz#e43TyrZk^Y1>E-YQ `n : I`-7/-OʔBBIR(xLH2DʆkމzZMΝv΃kN4OJ[L.B۩"DRBR=QJ4mmӥqoO;śh!JPJI*Ne *wPI:^NytWϕ}ƅcn6i,Vsg @j%m!U!)efX}SOB@$ `3I<vX"*}ƌYsKv2 }SYy'{PTj,V$QLI$bPTTr^`Tt8wkqchIiںyq?ُe+>Y[\bWO[{dVa/:L69Ju ϋӀ'⿤}ǸʰROiҞW~YKMP ՗AxsU#@9z/>_V jSֲ앬*ҏkީh4UCI۟ ;IFOS Iԝ?v.xT}ӷҙrqQy 5|JǶMy0d:T$ÌD)HւR7?}*%Ȳ[ g}x\^NY%E]خ]DøBׂQBy ";So)U|D -VBt̏ KR)°G 4H9dm\IScx- QH~K BR)]A$3T(84 {_WFzu9P1F K/3+9 Bq׷O"i>ZkSq;nZI#W &ƒ?.}RkIWf7^} ~]VĽE搶?MlVnz׉[wO.0KAb$Kp LZ ,=y4Ɲj̕\ShNXħk%KBj:x,|b];\oC$Nk,,gbqk'P0ID@;I)-En[35vVҔ/䰆Zc%-jU⍅7,kbSm>aI(P+sv?ǭ]X[/([(u D-#PG9#*q$Z[MfAޢۚm(q^oʄuUUv['^vM^ܴijImhy-lj_Z1IWN9A]u &-ǐ,6~:֭Q‰ߴF[-% [Xz{I nAIG `gᗒ!ꥻqwÃ6R}p|÷ E.=׹S}yz%%PH#Wr} ? \u2A'{qgp [B!y>G(Tyq&[t~bkn];EՋ_\ іmj75P!Edz !HmaBD' ^uW<:O4y6~oj~DM/.tȬo dJ¤@qKYy/"#o6M0a5j}=)FMɅ,z)bR/ТZb L{GZ㯺9;@uvv]\Ƌ_թ1*騎ް"Tʊn%oXHY{(XIqt ZcxP ̒"{0ۚPT[Bջ2zw%! 4@WƯ7i ZWenP@nU{jؗeQQyɓgǍKI%L-#ubxASJy9Sgte81!H9\A:uQh%|݁dR(t:}{ˎ"CR* &S#:KP̗y)}„^3v v7 绡R0TI:i&L>//;;oM%h !3PZuRtwi\H(ާlʑQ}R 2KmCaH- ѣ IOǛ38 P515fv}Ҍsna%E([3 Ph%EHTkV{3 ;\k:ȡ?pR;ApitY&uj,:fswދT: <t-vwa6m~:ܫs;ϕi jb@2Q"\(t)Q}x2|GB-~ AJxhvXڶ H Ty6&=IETRbUeFEhe*:hsVvQԜ9pc!W S P$ 8)&p'<$nի6%PbN7g+Wͭ+% Q NZՏzSjGJ/-h=:LRhǤ[(FQu- <ϋ%xS%=m =Ez赏ZYcݺ8*oа E*IZ`*9nQ\kr~ь#,xe9'' {VąbWDg?>@Pʋc||RLUO푫?GbsgԩV>֩Ucq( +h! 1|Mfs*7Cu[b]NN[,h)\vȏbհ#~5suy|6lmYUomS]d'߉QP4l=%o8ۨpT8ҫ:0&]ۇ!T-)eI0PH'6m ڼ?T#q#q@4Igٻ؎j4q/yp:,U4!@-mfuR2p0krn lq'P 4$=d(mҠ,L%GP Iu"M./q%RzWB*2kԆ$єSA 6R%d >"\Q3Ct`!BG.g&=]j&' K44ɘP`;s)e/]콪SUg-]6Z%-T}3* ڒHRH$pk3 )uLF}@PE{]UpR&D/0#Sa7$Y+YӔSDC۴3ێxog6hzE/@H@dg{KyDÕ5}^Czpު O ZLb⺗n b`zcK#h6H ֔I*RP#b^ϡ1U>~\Ap8G'>5х _+p.ُx5SܵU]Y •wiiTLQҕM@K^0E5"Or=Wym%- 2!HppeU#vv@ Zjj^}{,:OҔ!'AN]c{MWPgïD]N)c^tDR]v"#{@']D[Y=pTZI j)Ϡn=Ycm"m^t@Ҧ$i jz^kzvR*JC^T|4x; GY* (J-t*8}viSڼRgtgO#j;lY2c+NNW]Rj+iuVć#aPi!͐P%HedUҋQ5?!BAgQ hhd-H՗^-aD/rA98ky譼:&e'AR!5䟆yPo˙I KΠ ;I铐rGuQX#ޒt{j2Hq[XWuÁt;c;RҚ^eb2YfGBZ+lu#E(JJczJ)B}QMkc8Gr9߸=3?I:H#):RbUVec!OKDR3a%*X]ꇒҚq+J AzA(${coҔY:V >>lւT`)w5*{-=O^Yvt=eL)ZRr3 x2=evV]a6U|.(O֦g?W%n)<)ՎaWRnmD[L58{eF9ESH,DQFcmťHa-^vˇ w-6n!29[Xy{ /xOPzk ~ϙh.$\kkUiw.KLਜWx,tZ a<*A翟! šsQ﬿㬥j#ԫ|狶˷$iҜJԐUIq|5hHPGK}_ #"jhZօ>\NO]xiiG (vq[ZVC46l6d64 Lm5ڛ<8^rRDLL'#2vZ-F-1R~ˋiQϽJJT0R(VPFFT8a!BI3=s <)֡˧,0ZlwQ-'~ LtAJQ-OnRA@}FkJSYku( HOO3:?M.ڙLe#|rNR`e}0ZH!@D ߗoUl=B#NʥG&jB$ i"ˇKENTyPFȅ%#@  yEu_ cM+TxbI3>zv9ԈnFZ=S5563q1~R=~Re͈ʋ}.H6Nמ)|V0Fk]RLf+7~W5g,|:i~ʉO:[IIGT5Eҥ&)iu.<&oC[LJNYk g+WU^}h)6B#PD5E]'Ӊ숮v :_Gkӈ}#L^\/mZZbU~%pvh2ȫF벙rˢ%e~ib.eAnJDʬ#qgÖg%(Qt: \Ebi7ΠoY;6Mџ[)ί{Ǝӏ4%MێUz/YuZHQ)HӖ鵿\<{>jɤflf+JPnR^kHk]]m23iQU5h`L԰jZ{gƑZpǣN'ש~(je:[n6_n8Frmx JnRT: H `֫+Rm:Ʊ{Htoj^\-7=,oMl͍1*ۖ=lW*uo֢'i#Rhad' > ހ *ngfީA XU^i $KXǟN_ 3mcY]>wW;Yt鄰t`T\'tJH;ר9Π$u"iW:u\h0O/آ7lRڠ**C~HXd!DRHd39~"ï0K~շ⳶s3A$)/;[dWP9=iՋaF3LzTz~2nQCu~+nJL%MQH^2GeJt\Tu.J4E{_hEF;JR,i#+9%M:'F}l /4'.(H*rO֥*-8Ga:ay |$x#ZKECbXړ XZ1.5^kS)Il:\!$x >(.)1ث B 3(pRO3Q s$7\e -ɕ>YIF|N4^޵&GDS\i+sFpzy$\ U'p5" #Jűہ>vХd@ HZ;MHMJnV-oeexTeIHܜnxf"'ng]5v> [\,fD-`HI:9s-KIZ[b<1`̺mxD)~2qƊ\أ9[TJT0,xUˍ4ۊI$$ "t4} 0 @AJA@H3%qi&[w=ϬUTɊoK.j1,%:4H7.%mwtomTiQ*(05KإHf 0ʒ2*$ă–Ğdoxbݲc(^cϹ诇 GH(no>BNvhHP%<|dYf}IUopɧ?M{k-_I, Zn:-h} [DSK&ݍz?aGn XT)@-d^0lTA\:Dm~:Qpz!:~XVǠuXe"wTuyO>}'k;,w]L:'M&zRm{v>לG>u -ʺwӉ z!boaxSm6()E -BNʢ\]镄|!`";|o)XAЧ)ӧo}|BݷDL :SA)NJ[i$J>%*3wd\BkrӿDgCKhy5Coc{n9)ee~JJʄe@t;_ի En&MGK.%&-;RBU9sd-Җ{R{hӡ lEu/zApnIA PIv &v9ϝjC5i[EbM*rQ ҐԲcYyk;Gc5 a=PQ˓S٧ʈqaa6'ptR#qn+@>uy> j*vӀf{G@=HKRT]QLn_)I@DDe5 دjD DW5 GH;~n9uN_& bi~Q}>QH)sC3EV9SxBan:St1&N[)U@̩E)j/g#CEiz͝#uVkVљR|Ū4V*|vCmse2y޸ J Hl+;Afn[_IY} H>$ I>U9tJ.ո^$fMm `ʣ-#݌ϻ)@ZtJ$')|́ҹ;1wYY0(!$ RcU$ufkKo3Q~v-)J%]Q+\u!g{G8V] ?ָ4^LRy?My} 5웆anK꒜8KL2ʗ!%4JNP9 1m;eڿ.w-BVVUfï+ݢe 31)]Hu\pmq1:L(k/aXuf";J^QP4ƻsWu{K :"lNӉ ƆSx qA?;"XgS'pE _f~ uuVE6Z~ד)V.S"+uwqU2\a_TXꀆh2t(p~۴[w}H 8d0U9G g5Nv^uBږ%v0-”YYxKp;rU &u宕8żK`JDcs )m+w]kD>*06t%d%B$D%KHeHtֲK[;1umv0B }@ʽQ$Q,XMKE=l dҋYa*y@d@Qv@*H1kg4VuqEGzpRKq)pZ؏ 3dKLI*Q*Z^śyb3wz :@z=Og78E :(58Q!#@+Q q-)-j ?I>\t¹LWXy)@Usygܗ[*Ȱ\Dgb4녶V [V)TpGi 6)Y4'^ZLbދ8Վ 3~M!gRTU"6$ͨ'`Ihhu^W xCZہJ]Fv' ڱ:-$jra9=UR*̵ro4 yKiRnUk-bJ5&-/G50*6C!j+pܹ[.t!lBBB15;\AkЄ'2IdO(MldX4[Ǧij,T)\dɊZ\"CO7(7%X"!?`_>MI) !zYU@%* ӰlgǸ2/ AfAPo1E$!DT-P4XucOqw {/ޚZw"k/bV%iWegapņ!fAD)$Đ#@ Zj*l;8sAMͬfKHl*L)s$%iPELrm) nմ(iD22IL$tOTprp"+fU0'8irp}--eDBХn@&){ܲ֩NPj34W$BcrmH6 dxoĮrѲQ>DZHSTnKRʥ)Z ͛sٖߗ̻ LM](srPZS ($'`oba&1"zĉ?`xZa[m el" AV#(:Gt˚r\KtJW7`鄶'W^:\<ZУB;UX؁ JwS>Ug]<߈1IV ђ :=AMy鳃՟PuTu]~ lTuR /N_8䛛c\RS*V(ZWwՏ ͩG~IU`F7c %X*A#G8 n0:Ì-dI`yËy.ۅ$8?$J3JrMXӷ*$̨K.̦-CӨi`e/+m;Mm.fԂJZ̸Q>zƚ꾏[P?24VkA 2S&sKL,8꣣)B_(n{uNe-2nIJ"2fTOc:)<1)BDfJD#ҊG-ZpXMlY\J7NDq\qO2 )թ.ZLKư+\ZBDer@U$FD>5Г1<MsJf1&y OOˏ˪¤Tl؈i;XMZJhm,[ ^&so>#p썥D)H@,qK!2 ;mWm\}>5n6GP= Fn u#C?&I4cn+wɨJykz 2⇩)V2O$C*]h vRQ2:mFU><0BIlza$=iۨZ0&C霾l*jӎ [Mt6[`_F:v)z*RnD:dwrdǴr״ͦTv%4zg)84vʪ )+ [/hlZ|lFQ೩46 @];`)">RXRY( K]BN__|P#<2S@ЊP&_NLoLd^-2ִ͢;2ۈˇ?aHH˿5& 9]AB7ne(,]Pj\{2n]z+p+ץA[T36|~z5BAZ,5:Pfc߶@׬eF^K)SΡ%jYRύcZĄȏʈ'k))e3^JR3`GHGNR.fcB)a{7䂽%58p z64''Zk} !K#^SqN^mW>?ԧECۓ`lOQ\78opT슙ucXu^(RJNۂB4f +"ɦ%窪~ tW GTbT4 ZPP0LLڦ-Y0:DqO'Hh0'x7hEӨ 8Y*) *%E(Q*$3MRzg1U!qfTc6 EJ!R@'ψ@`ԡB;"¸(P6@LSe4}[-6ѨՙQMRKe we,P44ۊu~'Hop|孞2ιq )幈&6n#1 {[l@@#e%ŅBfR%f鴡ÞHP)vMeSJP4RwƝ*FC:S^@Xb×[J R0FS ̒Iyԣ0 Vj/<4Jec.P2%) :}pՋ=1u&tWfc>iϕNu䊣W]}hTp-8Vۥe}Z[(r+)̢N`O(;dN&ınnX)jӹUnaJQuPڒOTbIbӾjNmBE,t*VZ` 3\ ܦ9AA65ðRe dc>0!vxEj[.,Z VԜ$o챵٩kͩ)3l0a?M˄<,•*۠B@=:.>'B*PQM\; ZY.H+ LjvS ]qdv~ZN<EjLڑ#؁>y2,I*=^TyfO_old8긖?:O4XY&=HaļPpBʳy]Apw'8>GI?!\Jqr[n/Ĺ:1HH`?jG\y t="[p禞U̮{JH9\v׊͛}h0&J!)<-kL_ )!~%v؍b)R$(jjS#?x^ *d IN̔ozC8m$B֑J559LXZOɆ\Zw o€_RQgCV\0J_ `A&˼ؖ(\YJ#4HAؑAq՞UoQ)؅> 9{.7,09ɔ8)+jJjq&TD7'@:c*.+V.,,C-J rR/rWpOH5#4ɞAˆvu1{oO*i )ĕ-AJ~ ?x: YsE9Ulބ.AT9{d SʙۖN^xDu: ߶&.|;1YWA8m% ǹ]ƁpJqㅾ0t%w}e+뇡mQ+>a$=ͺID S6 *I=yz\uC6mXUؗ(ӡKuG\v:O8\q'Wm<2'۠: g)Ts$UZ5vvbZEaD# B 24| J_Q>"dC|tUb0aDiR %*[]wlP||@,q@#P 5D?f [Z=3術Nʥ$2)ԪJ'\<, RT^^;Qlъ<)vTD~ΥJЄzuAe0ӎ˯ӀK&4ъM3mN $vCJq<-Tt)ZKͷA;O?.dщBNi MCz3[va}+D>!C`hV`ƿuv_6@TCשW\<ގ*Y2w'h2J %Fs> yVW^tv}FIEϧGzqjGksi{KrC΅% G:Z}qOn L'W}¤)Xsu~5&LU pm H}-!>!aR~:ݾ՛~TuǕ2Pަ A$t;vBG:2D󣙾[kK]AG]^vօw__\5"yĥ.9S7N\ؒC5)[B{O=cbqF)|Vt(%%į"R" +XeֵYX XZ^,)%"0OY_K>/+;[?qŶud Q*T )u0{2:ǻZ# ITj/>:= :4vV2)~ZbӨ=^x e*&TVA*Q<8gG9_i>j*mIrEJPP~x(+2h?b˂ˍr/Q?T}SbNy_}!5f޲hN MmehU;l;)V"?n" |טvvŌ 뵕2TǖE(+\ ~ ν*>&CK̝&b"3zS#.(KKA(Qb7|ʴ9D^Dt8'YXa-% seh6rQQ*VΤiR-=kd:w)%DvQJQuFK#Za?ytBNLk'&]ϯO9"yIm.oU]n~XSPÅz?soN1ЕefBTDdrS!(ʕ*ˆ8EWkZg#`UIhFU]p+)ys[P+^w4*ɋuKrm%SzbuLj874JpzGl8u{,FcH%P*ΰF٤!AKR`+M$T gO#Kho1^SօeeBmSP6"aSк]d@.F&G cirLȠ!kQRe P) %ījx-"!QR%@ ILsCG]*V/-|hRi)T]-6XKIc2"ޒ@*h6`!{ٗqq'pH?_v06)"JboZ JA|Yޣbr>N՛h%儨f+Y2}p>Ҥ5Uu86 HOT8˫m@z BUzke& 9auok6Q?!`Bq pq7M*wFq!6LSHsGDx^$:+QWϯ_ˈm`<"gMA8bidDk͡Nl4w_]SY.ͩ\皕jiJӹU3FhrA+TzM>Bu璀YFlo X{v%!ڂNPK~/]Hd/RyA=jܶƘٲf{jޤjS-Ү䭖w?i܎ܩ)ր! FH H9Ap2券VHJ.ҝCx=@>ѯm=1U=j酄 gveUjNc@f)aty{)%V=x*S]HJܴymR_`n1߀AuخW׷®& eUopO;c>P6SFbcA\$hb;9(Iwκ/n\R]RR:cδ",ȷ$8u31I?ւ&iC]R uAP¨-'=x,DGTM[Sv,ꥅŌ8aīq;rOΆ3Ww*5RIQ&cCq_xxlhљO ) 鼥[\e #44(ˡ YR tA}]2r~<$uchZ_.8rT)<ux RW7,jtOXw{nSaCˠ=q l@FqEkiIZT]˦8,>*0܃44Wtո/mJZNnliLB$uaq2$=QSť ^d`}K5|-AZABΛw'Z/KD*RcӨtKf"SM:} K+9֬gr_h}鷵 @X*CUʽkw8- EyVRؕS BdVE!qΎ׽؈ơ "IUx瓞ŧ&'>pkjXc [P995gOn*ۤNvʦ%C5X+]Aǎ378S.,5*pzݧ5=2;"*HN5}fHuYrWs=ƞTa|(mЄ6͎鴤@P:hĹqA*R*ԓORu֢0WO:jܽ*MfGn x16MuQJhJF_}juMnMJӺ&=aڵ*J o://r|E%н;=v6]{Lh6ֻґ m$6[ I'<CR:3 FfH4.;bfL؎sq$j+BWf[iO>ҿ 销u$sһ$<ۗ Uݕu\E>%)Ɗ5.DDGm;KjqIBVxhBT)JPLF!FlgM{K낑u['!$‰7'i-XEjK5֭[ tBJЩt8z]*5Y(ۏ=3.ԭHz;"K*ykqa)II$4;JCj#q^ܰ26̀`] H V)#(L8iZsH墥Wy$[5XTKCit$LjusZ %hSW7eݔhPC ,3TTu΀<[@<k-6W7œJ iAj*q$x[$HܓFs}_ﭺtKz=f@bR"rmzbjUal&SrRT>EA;nMŖ Z anpBB!AK`D$%kq<=q>éF !J Y Rdw v.61[B+Nv.;Mql%.PEIPؗv'^gůgPDZ RLJ<)I)mn-Ya-63!I TW:<{2WokW/6Ҫ%uS)QmR"C)<"BY[liž.9 ؟F5ٲ0W*ARs$3c Ŝ_~(vD$(H "`ksvxji ͨI?VܵphV?H~KGE3h6 Z1튆>$֮Z]>,| ?ύ5) OsА;dU,zΧs-sJhvdRi,JBj1ГYvwm5bkT~;{Y71l̟>)o훮bLEe>)`CK_! IP`>&l5%IAēu6$ C*\Bw*JjJF0kpV*b̢L_=`},|m|u9PZ7 .lnP peYeЇȕ|ɥ !z4A}-U rJ\qIJXk-АzsW:%Z߶RAGvB\V%A Fp::JYp(!zcq*I[$ȣZ _0K8营7=zy\bK)aP[\z-֖eJvٵuXVT8]8*t!* ZȍTDgM<-ni)$1 9[bS#ǐϢR@JS5?N8JW$rMpʯsS-2_1TP錀ˀP~g077_]G2*DF~%'V%;jY-DPmg; 3>3rR6H&No3T_+WαM;uLV.&tn#0M6;6|e%*p4mANdJ8eP3UF'JvZmt| #T-KT5ϓep9/;Eq:*Tخ)O-S>?ҭ_ 6+y 䀓TL8fiKFjQK*Hf BB$e s)"Do-\,-eUJd9- ~$) T0wIAFDEWv%pV'K[%:DKA'iIe=Yq]%Nڏ!/lJH 0R}qgK\Hޔ(2$v X =^EF:_^6J8s@$F-. (+|hDA$**Ty>ꭽA†:ƅDSBdǩ|}PZԷ  @IC *KRS~Ż@|qPBw^=V4`^s8YYW['>P$0 ?Ҍ <@Ld'x #Fw'P udǤmd8 >4"hƔ*=nNϸ̬S9%Ail; JōgPP/$%'o PAI^1-$tSUm/Tb:ylϙp\ttM+ډo&'ۚg[zqUi(g;K6q ;<OR ̴ Ӡ[mtЩ:C- U" Q#>;JJC q8N(RWĬ^ڦ8ʿ[ j[@?Jj]u_|=˦ʬTb?Hs-,{2wRFTRLq&̓zÎ { y+E^30c9Xm6|IQ;مzŢGXeCBVy[l;a/c>xel:ؓ'k-%+O򹻉ǑPgkB!Q㾴S׿c@JN/+q'sI@_oιߑ0Z)ƍ%maD†m'QA?Bi1EmsO{⴬:sZ)i|[|K2!.oa)osIR @Qߟeɵ%]u0,ʗQiPPi>mr; PTR zhW&=OT\E䅡%cYlnJWD G)$kJfyrھVmը\]Jt鉅RĖ̸ꑽ,퟈{8cud(n^]%|A@)$A ?'%dR"Nі5JFW-cT]b#xR GRLR)YNiKjfG`) LwZo6״/xi;Aq9!.%9JRB)$+]K'[r #8P έiԮ\Kj|XuK.K}2EZPr )nTfC(yEtze߿;E[Jg)( 9dFXˡViisj9P܏TjI5__5J_1J5wov[J'K cLaԺ Knx[*r*u m=\"SY? vx˜y`Fu7O/4]`nZ 8 )*jmyv%/eU2-dISUeD_+uٵIRYB(Jw!%4ʒAl7&\ aXmxR%*@Ng5ҿΥֿ:zVfENBSdRq;BGÇ Y~b#*= Ev9OC8Gs@ L8}5uD^!VWf+) Vq?.Fiٸ}KQӽ:u[[N5(ބf ^!lч91' qW[u?CU[溵k:^m-mm hlZOaSm%t3=tԟ3SJTοZ j9-6clpR\ZNJZ8@!KV=ҥ:w$>Wyrݣ)"/\@ԉJ3<4'TH*>@HC=OT]gRuE>Q @)ڈt&38;B}J')L}/AoN]xV`[Hq;^\Hx)99RugeEZow İ^qnKbXgo%Er_[Rrݺ@R^ߠ !#N>Ov&rPRYږ@ Íza>1GRY?5—)Q;I٫}j.2U&|/@)zp)r y wDS:C]vKF[Kp 셬*6YhEcS̆mX7{UF[ä1AxW 8{] P}:<ڙ` T9 ^CC( x:-Mtzdٯ*߻9]M֥,ܥH!9Hlsan]\ Z'S@RW`ʻKíg!vq0BLJ-PM[ ۀ 5 ^rh}V"RSiO-1LǎI^)"cqeƒbR,> ŰH+,%H:Gi;39$t0dpO5L+aʂԒuJSRLDdVW4 -ɨo9/i!TRԴ7q,2:]tR' UvpmTUؽ/XcD3xJiRvV:Oq2*`ׯS"Ʃ̌0J'=^h@,P1Ĉڄ&'k ` AӷHhDJh? I WPKE|q)P C gp~IXk $vޤ?ˁZ\E*At9U(zoRҶI8JϘ%e$_݅6SҶLv++PQl#ՐSW0̶Oos#S2gbaۀ40 u=t{0_gn-7!} ~de#qIRHKp<̆8Cy|^Ỹk&S*?ʲ}]knj[U$VQ @땆l';qݡ`X|$6$>zr`!Ś폲ێ8YJH˧TI\Z:5K컢ܚ`O ,+Aꞝ13sh2@ Zp۫;[] ֔PҤPK^q߶r.S$RPK= Tq<ryLm7"5/R{JSw*.CCn=N sú"9RQz|Ml>2ӭj %ҫ%+WFr{u#K@Rboҥu҂-{kE>_\W"gʈYu$ҖDE6Ruf/:̗iQMT!̘u}@=h[^jSv*U/Zϻ*c%\^n6*ٜ+.x_afZB ѕEFcY5+n#qJu-XiՆMiNwY.ˎ*_tPdZթAK erHe[v!['h LM`bvMuo Z()NdʜrRX2'HJ:QlVխa7XצR۔Ȑ-kndzu-mSe B]WNtgwliT3=dIyo!F|dzy֑<$]ͼ6/5ۏE,*JDN,%{s$e[NJwoQ/%7!FHq_ϙn)o-J;{䞽@VI;㜏R:7AR1ӶxUZ몵GQEbtEekJZFI2GO8_ 9!U\\n@rަ^ k"ѧуP6GEt$XC× 4J{tUͻv#uy&u]MF_6k{nr#8FFsXSSNZ.)d}~i$e#ZFJZn '?W)N߳R \#SZ+{zEb ]M5C\uCXA^Jg[~:DMƛDeP?]Jޘj5qyo9w; q2ko--6 ŵG +2GUTNNv #ˋ ֔ #3V$֪N&U)'rA.=uHxjHSāXpHsbg@Z,oyW=|Lsm-.t:b^"/4FqM: BL|I2b#+#g;LKӵxSd: PY`u.stfy( Q KB R?'HMVT&ڕ\iaR*/0/olKm-)i@Qz5Yvu[s+𭬝A%ҷιx ԪO:мV &>~UҊ;uU۔KuĮT)93Qش{*Y- 5pb!LR'bI N"Fs~-R#Eemf 5$oYG3(fYݤ-@pZ^m3ٮHѵQBm9׽ѥxa|د]>\Rq&]}- i[ZP*T;&c7 ]_Yʓ-9C4L YVYZRk-gbF߅(t\T'WBpwwyu"w1ιNn]^/8cuu\h\5 C' ç85FXz |j-=px5*8![_98 P)UڇU y,׷Jh@Ҿ$l!*C !M=!S9e~fRv%i@Α\'SEqiƢ״UlzbkΘɢ:Fw|𡻅:r\a*W_?}&$酭$njevgKR@<)d@m/P\# z>v^Y8ƧZ@:: (8Պ)^*yM'! $PUh(rն\a[cNB\ciHMeGփB6o%eXuH[ PPܙ~ qZr EQ3$BA߯^Pk #˷Y]2:@q'yIӖ5\tLߚhlVjCTN1G 9eD%(_$0 jO}8ah } MHiul׽G"J4it攤 Yp m$"Nb=O8[;kETht[.DDwA=xb!kH>Gv$mM$E$2D6kw 2YT K ]N+I/iGՃ6R٥iݧ(5^TQmC,4qmȫqUN1ۨ-m)W/?NҨyboU.^anGl)$` 666>5۱gOP`QzS3\ _z^[|4\%4wƕRDSkx܌x(_XH;Nd M ITw ؟S9C^z+qBqpK?8>$̠CBS\n2.+p3fPGUްrO4Ͷ㰊|s)VGg8e>)_  AL:[z,X-]jAjn~R҆ *%} @JPJU)\GrۨliAsDIs"$ . 2fV-t%^.STĴTamxDw[{VQA 04\(wyqJu녥E*pem@8>$oaŶ|E6ohDwϟqw4EwjH-1#:Y'?JLSޏ*ܡ#gP}3%jZPw, -D~[c?)/q'=hw4tCZOɥC팓FºPKzIڂq 鍽|2dT։1ZTJt9˵Ln~*Ré?%q=.$[j)(j0H+ٿEl;H⣝Q&T[6 ;J5oH5J-TCT n\爪\2!:|j#jm kEZN?¡?71n0 GF-Ty,keں)oRTFGn>Dr<7q~ Cpi!!'n`I9DtJyR6 Iqfk"ClyZuAOKZ;Q_MvQgAbDK~2UQ1׃ڷR~ږ)Ý: w ~H8{yzm6ʳ([O2ȋR20v8pzunG)FΞ* uz+*CZRC|q׃&HJkR4PTjtސPGoDe%4'SA A@``y\Ji ˌ-iN1͢RƶpݗtR|FZpKo 3n )_M+du~QMUJLCDA7wS>ǣj|Oo,rsîӘ|K}<\r:+wZԄ6w1g*}xNF;yeXT(l5}}`!jQ.t7ޤuXE)η+ŪɌ]lQ%H-qCrd#IO3o*V)(`ȫ>R[T)6inNH?%-]IOOVX(yy}I~TiK}A12PԦʗ ,uRs r3XD"Eh:w@֥e>i3+wy!%\t8SNK‚RA(|-7I=DzvMs^rRc)edԢU+g :Ʊ< Iiv=L uzf:2 aMRPAZR^08 R67_m3FU)"S-Jk~?-73ng9Rx'𔧮  {&j+bBtOniZs&M"6$R(ot%#1 w[ Hʯ*q)'Էg~өjڳaӛqL`*ެZ+s?JYq'ަeξZԆ[L)T46͗RTCJfP5Ž:>sixw$ly%ڻi(To xoD+slp&TݽJ*p)glBF2d(CMJ  :w)~Kh3d'ݺwIv~eAjJLڈK 0]\_aИhmZĕ+M5O? d֣`ۺ̤fhI u֔Z;jS,bufsSazO*CeFKi.F+>RCh*j2Ҹf.8IPa e& b5&vcvHI@'1 y*aU4w:|juGGtL*vajIĪz3qi6qxɚZC)*n<aKJ;lM8ia-gSw!gHL,C8[xU(H ,H܀ bߴٕo_o-cИtutB4RBDzQCSq Q#Ñv7ȴABƄt~cX ]vqr?.%/[TYHP@BRqMtu[ԧ\*Y>jVAKO@RKmq)RVۡ+AfK'ۼk;6o|2.y$6PL8?'Hn8c]r<>ڽu߄ոیxq9ARGScWYWeI`!Bںۥs0\yɦƦG;gqml3!`:|ǜ˄--y.-AN^4+=j'17%YJY\(Jc¸𜈲GitN9U%‹w]nUyD'ڽ6K.԰hP`8JjT >@Nzt:**Uop fJn8mmd>H!S$ эnUJ5l5V1w:]+Cjڣkm^k$*RSk2Thb;mJB@kz>@G|ly,(;:uzSˋ:31!3%tZ]?1\Rӏ[fʋ}8NE24p| xM-ޞ"z)=;+d,@iE#CAv;2u T?c"9ކ:PCUYHPTz`ӀLEnL. rp~Jޣ&+oQ =z~׌,䇚+u5YS7-4eV4j|8Tӯ$̒䂧d{OC)9ـE\ɟp}pS{$?m--;vI8.X΃oi$Aj"iFiiP {M'9 ' 8b.R|Ƨx/hA&T;uw A*da 'j|O<$d3+W ZD}Y7Z]&4ҭf-1nٰO\w#.b<ABICdZ_`:Qd_cٶ4ΗqV5O<@HRԪӬkNeJ pێBz~{$ `4vqXzқj0ǥe/ $V?ožuh QrOMҠW'}j<:~aJOPܫCȚu .Biowm,6lղKU><=>Py-Z$.6|9CPZ}Gtz'|;8CO!2" >Bv+X~;nCQzIQ?C^1Ė|*/DY5j\rN0UI) EP ^O=)aqE}`vJZ@:`Jkp5 c5f,1+znZӑpx_cv:V iOb3mOPA-9 yxwhib;G(˖#J%U$w3>sQ+RyӮV^L:TUHJ95#Aܕ@M.VfK-W[ /RPAV$&TIʐIDNDӋXi8$% &MI;5K1d\toq9Q5Tvi ~LJ<*.ԪrzSbLu4#4jGc+([)JUN+:)ea^)AI%Fpa\7sb%lHX*q.$TPaCy[ǷP:ut}fIMĺ Yt<42$RY޴>e=Hm.H.6 B\n0iޮ싰E)kVHԩN# JgA M7wmAO# d QV 󉨲9BEZ۩FJC6i~St7ɏOC 6UIMd>ONqa<-)nfYQ*ᐐ\q;--(h&B`o!2I$G*>nW=Iѻws5%Z(G-.ӎ1Py_yf(o,oF(mRr4)V_QqQZoVZ퓠̪ @I* 39/D@XRZ4!qیVGc4)Z^TR[n "( 31 6?mi2sQϺRdN*u*B(HJzcOtqXwW_\y~ hT @J9Q2V֩]L)t 9mG?hJu)ANn3}0~_d2hn-r3/-B(jU`Hx9=+B<=9ٴ98ĝk",JGn#< SV6v0 :SƠPk`JPIAӯ_ύeQ{jNBpDwNj2 "+Z)PqJBTA_)2x̓iNxvj7nA۹XNDNKWz+~7/ۨf%j%䃝yoj&2CON1̍Ývw pajN:tKvJW<jspֻiZt響R y§+Dmʎwp% 쫊'}R~;K0O #іf:IVG#Dl/.2&PX1݇r[hMU)Z>6ST7{10w/cut2c )UڣVAĴHWk_O6S::iT>x9"ܩ*[eheAcҏKڒ+.$uh. \+GBV<>5F8Cn07[QM'jy_ςW} (25(Y J8P 8<ƕn9A( %6/(} t7+M& &3QbTJGՂRϢJ Q+ ]}RL4 >P00zyUU(>,)D nJ0ƒ3>pL |0e91M(H[XBlj ;@ox Oư$TnBJ߅ 2:YOmlפ I4ze`pr5 R¥JS/s=JqB @^tێ%m$RGT߿7TR?Zb(eD4KnD.\6BԀ[;ڌuQ܌ۗIPh cw;Ӽ֤tʄ]WYuK vu:mVUNB0ҧԑ9Fu#o~ԡ(}<>g}Դ_(EZTꮢ˹"*D #fBK)$]ޒ_9QIә ^՘:zTC_52DW%^\%u$șUy;.S8%IJ$ q Ԛq`A1Nን2hp:;dgIAʾZdwuW!IA1w^µp}|4 T_k*Ub: #ʍPzy2Z^m'm9WCU4 T7ތV0tUY~ӎs`˸u j%b QUO];#Y\lB3R2HA1wvw6'XzkM=솭JnϤѤPMT+[Yis@C/0T4ڐӉuK tp۫P, ʑa 馱ʡ|Y}'W)9s3&c~zQ/X+vj&o=ɬl ɾ 2e*M*3*k(n%W1cJ c¢yj7㸻8rWJ_EyAI TkkDzevsIm@[Jd9’#NR9+ŤQa elV},+!j*)QTS9FFR3'I# ߭N\C}hBKdFEDF"JUdʬZ>Jx25Ƙf N79ęVlYuI'2 !ž▣ Ho}u 9wgmJwp7 Yz^򉧶ֶ ՚.T:tOY_O+eHp\IXh'pK6x{W #bz0&z6a&ԼNH0|*ʬjֲr.`iuh_F!S0Cq JKM$* Ӿ+[n/V;G)'[C1TV.ͥD*;-!eDbfj*J Z z9J1搟zFx\c.YT/7MT=V`|t? $()ACԃ:ئ45vp]h҆8&&W >CL+-;ûʶ|Ӄ:PCC=1|i(Ih{`|nBZɠمH:nʴ Q l{y}8N5ҍ*zL6tdhТUzT:w<( [ZVl%*Q_>l k+S߷Aj`)'?#)IPG\/O4R_P Q;U{3λ؅U@kAɥ%֩-;~4֦kJ}&$5mjRbOƽwf"ô.&TܴeJBOрdG5ӾctzyP C4<ԺJ20xy+xI<[|95j}<}*ˌG]#aQ#+ItRO/ufvwObsEZmzU+s4FnTQygӕ[JG%PgrK [~$?1TJOʻ5ڏg-qcQW/D|@S]ig<\VZi6,K& VjVy(GOЬ;y$yl@yZ`miҮٯ"M&bi8-=Y'9ӄ7 j4L[vS2o';vV]<܎n;k MpA_ Ru ΛٔCe*:>"s'r0GTtAKA[`q3?e]7i:'nn.•KYS1rF;{n?+*];u"0#Fh4}yy$Ӌqoݿp(`txF'( *RA%Y'oL3׀vrO- !TZ&ASDvV$v' F|66BA*2Xir`1,o塩!/-HiHBp\H9Nz乮A4 :ZڤtRg<8HCcH+nD.&ԡ.':6vRhoF.+sݰTa#2F)Uݝ'а <%6 ajRJ|z:t(iYm;AיBϦ= = Ε% P+d-,,''N:rxQRpsz!u `IN0kh2|hՊsR[yHq JIqWbV?r<d9ui(h%;#Xj*!bcXD+aJz;R BJHxu,9Lh#c)EmD j$j yt7&jʅCV9u3+q$\&B{sQJfZkc`8fvRBBT"]@k@&a* RKNp[& ]% +KI`e[.HhHZD(Et@bRPPV%)ehj $, e% N7uk]qqE -/\QY^ZJ;|Rکf4yJ-"81xưwR0F{?1^{h6v,Hʱ<6JtDU{Xzn\|y*k@QħUti ߪ>5O(WYvQT51TT!c\vpITJA8p:.R>#-"[)U`v @hj|iũRT9|kmEik:Pe"b۩:XIPHۀ`tی +"5ɡ3*|o!Cms: hm*N >߿_y|FizdJP ҤA\+9Z+ <#Fxd Y>*e;#?j݀;w'I6/vhPLG9KhI9>X,~,'Ѭ(]>shCo .xT3ӷquL򏿭 % %a yLtB RJFeotȎ)ܕ42 z}8ĀaGXhtsW * %JU(t $~Z3*=fI^8Б>:\RbJkyR Ap܂y/[*I{leML92>~jZo峕[._2ʴejN6 %qIk i"K+<1XeYU+Ĥ%ȳa6 H"TUȀ M{B榃[v`k|s%sT{+nSya=4Ff4^a }|kqPT5oe)PI!JDh_صŘv0 "[[NҮR뵬^tLnZnȲMvqS]>j;J'@.8 RPlx0@إrNT쑠 O!-@s<ܱ&'ToE 2Cdz4 ?3#~;'V02JC+Hէ;|7Cc+=D!?%Y+*ܬUC⤭Hiퟯ+m&Z=j2؅,sq-K>J'%J$I'^4BARYoՠY6[To1-KZMJh ,-I *Q ;BVE¿{t[oiW3O8 >Fȥ2)6r2AUݧg2>Yvx}{H=JjOuu-4;Y={Avka[vņ>ÂyTZ[-aIpy$|鐿 Z4KaJCJ[V3('!Dcχ6WQ۪%B)[,Vo2?vvC*B{i" A~B:ԑA-& pmHT> ֲk*RB$xrTTvג(d~ +4ֹ [JOH88QOӍ+"דMvX2Vak$dqLkBP蜝`:ӯҸUzHRg avw%Euh,RO܏]>HWPvl?~1Iܧ=dλ'C{]5P鄅^_-oi; %zAC I RDŽ9=9-z7Oce$Al2F[+T~vNU^q©S}誕. t_S[m`7\ZɄʹFT/ J?_? iZ `#=Ì&B8یe'{O<.Me&ࡽtS6uWju[d@u{HVdO%ӠPtT"~[Vmz !'i*L+Q׃KkkiRܧӆG(,~ lj iɾ뎸/-y H^]?21<e4@*:M S;Ѵ09ʎ~ 17g ;V'$:F>_njȒb9Jvb4d8=(V-'FQ%w7}?OI# z&9Fl:T()NJc$3ա Cx)[x) ҥ$Gæf̔PRr}x gZOҹ$]cf czqIђd X@en*TB;:c_#455f`()1l;z䤑qۨ4 vtqffRv-돑ु:QAЉ^OpRwNO|p}@B`fF\>bE%~mBm9׀I֏qk@֔i/8$(=}|֬ ·볝>:/(Dcj\gsׯQyePn)ːܦjOGT(~X=zpZbM&*aQ <הIL.Ze8}sEaP:n<8$@*Q_?8ܤ7nzZL +A<7bD6ŵ<${GaK=Kj6>G<:o=E)SjOVld7)C;'oipoJrpxƣb:h̥[p6]y2[<ط%͆Q6]hw߯*$>!6ʝ m9ޠL?us}v#Nd:{+үF,qRmΌ(Ӯ\yת06i8%9J 0a)_xGE<\q^j 'o \V#bz$poq" ,AЄLUt),IPlzOj3ʗ=s_w5 γ([>#Bl;))G Hq(Neh )'*w5xZ5~T%3jl[μ~-M*) q# Q=#ţMN7GO>򫳳E1<#˪oRӘ6HUnV+U?[ A.;+qUvhłSI) Nbbu|)w Z-lTD5~Rtzay^()I&׾>]EGI*Pd!Y=IjVxtϖROUېr\"wsrIib@֠i*HrS[{EӮ80pKMj'Pӯʘ.Ҭ^}U$){^aHWȷi-؝ S~ )杖 ,2x=rn JTml(LSJeV$;PxPAE4Ө@}?~8:yx( q> pϯ*yڬ(/s4a)%CbU I jDiyPhhrUzcQҒ9PD7,sX΀:bf=k6;AN7i@t$|RCvPb,kQ*Ӷm(!gJ Qk 1u6K@y(i())^M$m²LU|knۺV'J 'ik ˥9*7jIyߣAvjR74F UF Tܼ `SC]~P~tv-X$>NH2+IR@ H,?Em$w>mI2Md(kN(IIsgi(6 |揢-yR P sg9qƖ+mumjrs~U@u"}b0^ ݈\]x2}( .!&JQ*ApJƔ Zd+B>.cQlv iSޔ(\($rRS++wp2{ v?dD ^diLRCO$8?$yH="\߭2\&=< 6y@$ ~z@1oF.4m t*?O׮z7MAħ(쟝wR @?R=ðXYƒHHj""O͡-W]nG. bB~baԪR>u< L!l^6KI/}\@APȕ9c#$$_n?p \xOzZT!Q.)>7 sS:6Ku]FrkA Ze)Tve|]OW:TtJw2OJ?3]*Uc0ĕ ɸZ@95N֊KzLN?eP-LQkmqНt%\nV5m_\L، qI䁳H;Ԁ"uÀ7d ӯ Fgէ+HQ5鮘nq! !r$8~X@t$Bs,Bl{z56-C˭Nxs^z$P+Jm*S㯟B;w탶[L Q霨I=O@9$tg#2L#e;i4ӪP[mɕ&S,W"Ku\ E()} =,QGnJԤ4۷%9@?džl7玼Jn[kp'ʡ7c([. >/MtUh%)oZt# `6Ox5/cU'tʑIhFiKi>WUɎuHَ!G&PiXΗ }I4uHBa]: R蔆J*W  ѴLzGIqۭYi!0#}5UٮgܕVo*KT'W1QĮe;I4 *QW)SL *@HE:# :c!&5Ҫъg"ZTD>G:5*<[!+V^([Zw_lT??ą( 8ıg^.7[cAYuԡ{T5WV~z Wֲin[%csg>Ҫ̍Ej4"18;*묗Q dxqGfc,l+y%B@:S زXru JO2Ly4KNjq%Ā+h"bN(qG*w:ҵ'+T;?!視r 7BÉJU!*9צHZ)Y<%(9G|*G4HZcJ\g'{uC4 ҽ[!co==3_ς&MiֲK u9tu'q5g7h5?䤖Dg Bs׮1߂Nh`߾L*7BTZ*' dX=xc8#Sn% ~E4&>wRW! Iޑ>~f"3f^t)a?ӍΆ J4q&76(U(|H~{^$(A&y֤G,\; bɮ&d:\@I*Z760ĝJ?tWoFJ!EBu'U9/S%]:ʻ2$j <2?o5kz_zQ-B0!+Z4Q1%o5^#)#)o B'\R YWCgOBdTRDSOBn+AJz(hԠʢ?@]6Ә]w-n=O}j=5WPA⪁nBKEXz]'1H: t?Ү~^ h"u?s HܙWYWk$=Jv"1xiid%ARO^W%(q+K1~Ф%aZ*F{w )ߴ.+hvW v$K`n& #6梹[ s7NKjnuhw6% +hm7[6pu[JBwAVēڐiq,YkR;4ll"=]\p&($3Kq33Ҷi.;*C*q؅y;#yBwױ T4Q$BH@S(05)^A;z|-СBhKXX[*(#L/{OkU(oC#^xN"|#^%-ݩI Nީ+¾R27 Vzc{0X]'`#]V E4ɷiɟ-ᑯ*GX<(7_V@A8NuRn~RWh}c+7(x*HE @I1%>R5TXR.r؉!gH*7V% Is)IyNFNܯ*ے!* Nqz1c[F%ԎQ H)ԫb+K@R%.!& cTt_L\4и$n{Eé@vؾbhJ1$tQ qh8Hu3L^ӴwM/MuGZt:.R!–ڡ.P1Cvc\b/8-) ']S ޚqWUݻjf j9ɉKAysN9nߥIQ["=Q?#ϯ8y?-삥bRR+3SR-Dn:Q2IK- JBGp"5z, ˩7QК1[?C矧U`jPiYʓdQg9=p ^ny7 Qhrѹq7EQ"kPr+)L_%iRL0m`)LDBCBfU?%-((e{:ccsHmGu\"zF4N3E ģ ᴼ9X-JP=s֘1T!AQ @%EeJ8~LہD7Jd: pJ(r#; wO: ܺI13;4WuE!jȊYmpj9QR u$0CA;wưI֬٥ܡE`s;yʐ ;ZTe#j}@q'"#J2닅,'t!drM3ƈI˛NtgRԅ()ܜ=@=q@J)1h;O#m6DBd-X$'Y40gQ?$H^ߩyPšR/P;H@'?,pDC|Q5ce?9ȧjQ`JD)XOEXHx("P>!!(=;nh2dڙZRTFHcӃVuR&ft֌ 4(xϹ/*$3>| @$*&H>FRm)N*)\OqFu|ٮ͸ʣsn&Ahbܯ̬(%=<14P1$WG%ĶBOq9`dm /wQ#O5 y4iihJ(p9ڤ0Bxn&Pd~ḝ͸/ZAI)P* JT*IN@ MDSuLnR94ijrXYBO]OoHܠ+nTw7WGtO+um$opxX[ZLMˀea E}l|Қ[~;-amnRop# Vw(>xE͙@+":OKRB{㉽&;tk 8f^V\{=L컂¸aoDb7H! ?ZTT?p4>rkBMSXCa$ hO_Ṷ̈́MKiA~극m#$=֜z6+Q)&HmD`OJ&FBF1:eMEV8'wQJ&eⶅH)G8"č}ՌOVy8CvlDZgltS bMq)'{ϐQ,ła !eQ??*gu@`űQ-Z!dT#At":>2:h_U=^S+F\+ylT˯bn n+[`=0wMzYGTq#s~`®<)'@@D$Fޮsoi:>Hx%8\N'ZGH))B201{T&c~fxX-<&z y[̕<>Q6AìaSX`l[x)x߉k_r@#tQ?$IW š^2˕[&9@5ֵv%kNz_"(&grVšSCAjd+iIRboqp*M:Nzb6ܒP&?)u"tBZCW]:)5b]1%!ih 0JZuf&Y1족Kjm|!D*IdM#:)FT> s^yOTgAʌm2⟤S\HPs~]08ӉK򣈲%A+JӃ)*>o<BdI/|:+[ܗTN4QtDFmB|0A!c׮/):iNt`)%%IRWpZ< 9ޟWUK2Q=3kY-ZPҐUqקC^`h4*QOу $n:w另H$'`7ґ(~=~0:+:`) Oϡςu-'L''յ5_UvNrm-wl=Lc)I!nڅ:,!!TE- g*GaO&KQ{7=mq14kHΧO'Va=Ԗ9V|5Oae>(kFUݺNWFce/Tpm.ǁR*j5g3R3QtѫX637% qME-mQ P܄(Rqѻq,/[i'G u$RWrT꠽Jm#~s3Uw5۬ɑ@)my2r$i4uVIR|GYR/x喃}QeII;8|luR6f#cm:> c M0aKb,8! /(i; ){jۍڥդVuUr ʳ($@ RCp1,Yݳ@'+iV! O3Dz765+ɨTdqn*$8MmLu&kRMy1nͯ۝g8gP=TMHJq%8qU4z&#izcmQH(eC>|B3v qթ:Dg'r`h6L8 t_RFhJrI>)׆#.Cq?ҊBO(oƥԻ9.%OJOP4}:uWs8N\^B\$@ވm2v(tA E\xq&wjI u( +j]p}ZIV@WzBe:QQ=Oq_ah޺vcA-uƆz >&`TWJ_`ƠRc-x; [O۟k9-˪cF$|26qSbAIBS۲x0+a ʯs˥Vjg4:DЍ9Q)n)R!{G};-1Sqlq)9UR $$W=a^"P`jz&`54CTs5"Ƽk5qZ;F#cȤ!3h) Ǝ<kqbIl Z&c8u;U~p_SNBF6t5l>Ryb^V}btOOj0M2Kz,ҙ 2N4c〥9vFزVUIif#F\]izmfC)]Rj Wݲvh篺/ˆJRGT(m²@$tF "qEB:)U>5ܑJP7O^RTgCngAp{*T5UoΦ'$[FmOu-t뎰pdrOA:-``S^K)YUR}~#yU~Z4 5:-VaR܈2׵XsZҤ€EÜN% 7:lG=)hK J#QQt"#"~|I$1Qtޖi0zd?ɚӛ :|F_ /j۶=ǖJ`ɡj'RS)En+G|,u8ĨG>z҆яYIGg#=8 I:` +=ce9?ԃ6Ni3:#¡2Uǩܡ܃ʼ3[^%/V}zz`jLִ@R^d]FJNWP:~Zz: A'lP@ϐGƳa[Z)i)Q#aiˣɳRDvC$v]s LcیL* =8ҕKy+qA h(8Y Gt:!yQ9%Do8Wos׌J_kq#!9d|ÂLH*/}F$UY(A t#HǗD j cܦj/T lJFS0PþA m : ѵ)UKA㠦1 *aZVVbHneZE*ʭ@jjr^ 6 mKi Pw9Ψ=Zc!Q)wL;,;@ꏝ(2}jO7?D*K7%)=tjy^IF>bVfE( Oٳ*[mԍPㄯ]xCs ( K ?ӠW40v[{C:<-z[D_}E(+RF@HI$̐8d }6W/#S I; imTS>hHʁ֮ZZ_bU_MރBURJ?:4G\a~[XV8"Ƹ2* }\!ePh&H K|/ wN[pQPYL%Q2y'ͺ[VkX.@]ᦱd rIRRb4g)H/GؖJ př 0Q% Љ" %CE$Gq -z1E&'i1u_mG0]N>tZ Ugh2buJ[z_7㌶6nB[l"A5va=]V۫ A@V"I;M\w+)[ͯcuRhmT"PIBR L6: 'Vx4+PqF!I Mj9XZE) *֩xcFc&EH$+\PU ]>=vlM"Y2SbMo^e5ZlNʺ"͊&9f|g~[mVJ8[q+n((ՌŨ,@&#Tnr b4^}jVDpk GLS&\=SZD䐕{c+*XMyC8KRju;F~EA80q-ybFOT{gX G9z 2~VJ nw;+eI T^^ڏ7Xc(N66e *_U5}p =y+L)>5${blʧhZФPnOBϟD@!7#SKƟRJWf{g?NZ[}+Ckw!#zq~dR,)'|<:VRBʷ}1 xm 9RB B;pH8UrITQsD-w'?~9МiZGAK3˛GpHꣂ^>_>4JAji! (@dSLPixڍO#׿c5$uXrdRIP{?N5:шPRa[~c2<75mMx"33cpGcw zӎ@ kj2eSwcnH9Bƺh5e8% OO8D 7 *١є 3矗n6DAIT};h,HJNsP3 (GOҌmye cH8yQD_tU8iV\tb xweD_"Ji/gq ZP3n!R i?ivnLm>\-i燜XmB(ʦǏ";JتVBiF Jnj84}X^w8$JN,>$;5 W<!)@#cgkWUrYϓ:|ԧTnQ*Q$IQY;I$xl:,04r!6`4I !hiN<[δws-6)~x|Injqѝh6)Pѽj u}pxGTd|̩f~PO "Rj$g>_ÕCOq*/ ;XmCgǙO|*q֧]u]Q*RN~!9SKMÞ9"7:2 d G?i'&(ZϮ~} hS^H븓;]!/-FB~_)4,Dה˟|(`|?/#+5֍ s&Ǧ"v|Ҕ 'gN^ ԟ/f./Yð˗=z$V!nSشߐuI،Iz46*[LLʓ"8Bm<ShZ-?Q͛rљBԳ m" ـH̔m+nqVy"t!jRLםm%pʎjK¸\l)iNR HA{/a]B&AJ#H՗vv$o-ܷ$D4@,}ADV].B*MOmkaoY mH'!m( HAK~%!&B YQ;W1{[L [JTP&4H*c>u$9 WnyMi Tn2rI+a~ (┠ro}U@[ğ(5 qm`ĩB hH3Zuܒrw!~Diߺ5><rNpo,%şGSѥq`vG(V/x!N<:k,K0oeMP$KH%O򶔩 :G')w507޻$cI vɔ{*=3xeWQB:*uMjvn߈◌{ υ ~$S{s{hPV$$<ѵ'-IvxMIM K3"[>4৹ <8`m,'>7bJ?B:s6rBe5MZ -%Dvz1JycB{$qlŵH0A?zڲ yWWz*p'OX؀r15"}:LK]uYuP{2Ǣ錇>]yϳ鹈ZJ;xgZ-rٳɴGI&5݋pc*yN8% G>ڳ/lg2Mrȇ1w^]s|QkeI.FM6Rx,35@t%ix -P@L01Ĕ@E/jFbͭ$(0R_7:9I$ߒZyp9߭ )X5'ʊ{E-tY$rV>S5tb fܠ:mPOQm%y2Tq!GCyXY4㇨ővḖX#7 1#T0aJ/3 kԉ*Fʛp,nm? xjᣑAIPF}Vt( yI]:KKC٘TϮzt=x Q|kXJ\d8Va9 SAyMtƳG!6Z4s@ᲩN2@E$) >s\yǸr,wo> <?=1ZV =S'=q.%:?Z1!(+H$$'>0(U]i/FJ?\ۜg|OG:9$iy!)rq9 >-}kAC<)I :hޙ=Ȳؕ% vnjrF_ʹRDڛ RNwtw?OƠY2NWt0sI?wD3E% <^^ءTۀ%G~w1I;ړ$”4\Op9G6R7r@L(p8?> . R3y8ˋt6tׄ8-on*4&0"agOދ ՝0S/lY0.Z }q*r5E`Hwt{;f摠>&1ǔ͘NdB3NRϕɺ9B+I;Rr VR 1^Jސwc m>pNX]݋k J"JOV鵅FH>̿$UWZ+ClI[H ڂ> \VzB^b߻a@(  TV 8&.}jUAʤQ'v؍ U{652d 8:‘ӧϦ?Â܁J&JOxB8yuޱNPP$Hyx$t'{nF۝ GZ)-@B”'h>ChЍ|o2TlˁeyVI_sݜS0L==s9Rֱ1(Ӕp>vN,+ ^q8>yxUTxXmbܤ@L):Q h-rpIO\t M}S_Q OX'-$w=jX P(a^2IHH|;GQ]\?jQj$”0J7'1Q֬l/[orSyfrw)NXNd(L>TnhDCP)Ueh1 8J.26Y/%I$%[[6TBP2ʠz $Di mۜ-9[`BԱu'PTGJen.M& V=REb>48K>x$$2u[}2𗘵Tu(IPJՔĤmy y0+" Fy<]ꕐ^ޓ@Q)MF3HǙ F8QƖDxA'mviX=ʰ‹^ -#)En,(S^z:zpepIZHP0\ @mDʹ!k8,|̤&(zrmy%J|M>j#BrZItP@$ `xY1A_Zh):dƵMMUyxR WX2ňdJq,j~EF[7$a8WDq퓴XɁ nVD`)CR+MTN}pU aC :Dڶ?pIs HE鶟=TEJ/lt~۽%*J.<OSgl6%%]8D)9$ h'RTO4cW.IQ0A2}z4Sat6HbDa e!v;IxYaJVPH4pkvR+VeSLK.c|U)*S5H%З2Pٝl$}~Muk9jf4Mgu*BR|VԧѴ*a5_^ ɃgCzx ;cY&nʴH̔ؑO姛 N{[&g`oWIC25ДJ8`8vN?q+zw>cA]]DT*L[JR W_*yOwah%=L: scF:@ KmM$Łُn?3%dc7x{#+ TG)#R6?.{ijԗnENZcL(;ձZb&UZيҟ bN %я^H'>tt tNʶ07={[&vģ>,E.;UR.մTP^SXg{ eR8`-)܀>R䫠')m=:x&6iApOb$kmQ(RP ¤-@daC]adORa]+kk.hʨi}QEY%ll-mbuBSqEI.!搭t߆ǍQ^z: ;fVJY# (`JL_iں !GjjRט9bBKr)q%CLMefee'x X_2mCKHP(U0Zg5H̠Iӓxn!-gK2|{q5Ds~̜sYؓjz5=JV`?Jʟ(\72& +pz@TEw?#sĞAh i}u?vMqp`S68W Ou8H r{#bTUTynK*z[’ԩ#ȴqqؼWݸB`BjPyplΩXaKOv\ch"gr6:yiLVio8djyrq"<ܺnLnIg!"kF >km^mIGñZnt_7*r   #MԵaХ!~qLdH[&k7riLʅ6aL'ӟ-XoA[^[ywF,/:@mY†'2DJRWvj鼻d  HAKVl*Lߵq2 KMJC-w\JSф{@z+=~/;/ Rk`~_T Z{K*>-`۴ '!O{Ov̓L(!)[iPNR^ϯ K1NWΠE" R_DuIPURu8¼Ԇ, N~u>c1H>%=۟T^dtl:^rl۱1ۅN@z^!$yJHNZW|ᖮK )Nj*Q Dݡ#Ev*HF'@$ƒJCFcj5ǞhUdONu]4zUsKcƝ:|h -I7" )QRxhBT<=^ZRCŽݨ!ic(e=F)ex8<* Lx)d e˪L !0I[b*uP[k=T\p2O|+d#8U$hN#7ck/<&di )Y@LD%%ŬӉ`Er*aRψ` JXff?~6NčH(YJVFpFH\1=M-F)6*MiRFRzM}z/T,{#HE;S/)NctT}Rݾ"$tm"9IZr`l-*  )8-ƱfTtp V>Lu4:ΆU]E. TPqZTF *N8qUJԅiXPfIZ鵺tIkI,<ۈނA’;Đ5ZTE)i*_| tQ9Cd(IOx$o)A#<˂ʡ:@E cBcFIF'ڌ-ˁP_FI Ghsi7|>O^ ͬUds(OZِ(`Ͽ$ Dv;ؐ6!?^LOOluU%ݒ[X8^n =xtT{G߲W1~Y)P $|/*_ajcy-t /TUO 7MէG~? .~"k*K@JJz⳴AJNXK*IKjZg88>+rDc(PT)H~X ;/JnvC,jI B5Q<ܼ̾:]n;5 CjO%+%NZS·+F{GT A3HTr`NI˱*nʏOK./wAî=@d|BńڶEe\IM=b9!n1.J܏Ev Ƙ=7*En+MN~%d Y__?s!? xn\bVLRz)T.G9ddJ%3bvm<*I 1KU[WTI=꒾k5.3Ɠ*N*'(t>zV"4^kȺYBl7gR?)-BE;UЭG=;ИŔ?db!tһYhP Rn5Wa.BZB%wvxz˶ͳM5ݒcpwEH$-hfrtU$QArw"AV]JV'N(3`5oz$:kQY[v֞a+wiۥ-լKQo KuhYp)y6jIV9m\ Oz3"D"` H B"84|2N$tG]*[Ru_hrK[ly>JMIB!#xOnvOD:NUqWG2? t`L!bз%G1L& rg,Gn:˃`~ EN(L/*up!d}9ďO+SMisNu?VṇìƨE ]Bo\d\+Œ.HN~^#JZDiNB:GA '@hsgÑNnjϠNgT˂"tܵJNx*2HjP>,)F0p@ʄ:Iם}a8'cݞbdʎ׿k ci7N{8Tjdq$NAbEo2u‚ DtCqfXa'uZ:כ\beM6^P+Ryebf]l1OhБ ">? =T㞮-J[WZE#Dy2k/|AKVgQR'vg~u]6mJ!k2k4F+ٳ$5jۮ" RSuPpŗ:iU`殍I"m5)D#  h--6T 6*}\Y;,If;12T^yii$xyTTΏ6H {\؛!ƷD|~G(AYIi-@;$RH C'>~'Qq gDGp!:ƿ^dBN;laM+;_E!4{_cBdiԪDI(Gj\ :|xΥHYJ$ JH8[Kd-SȊH dCB=6 +X')U#kntRL{AEB'蠡?q~Q. u>7k-dmǓ&֕V$`^t[qB-EECU!2BPco[kjßOpPPb16D쒕Cg  @:ߥUf2um"@y,: ;q*"HLMYTWJZaL#zeA~$!H!@#\-g.pG#WC"Ň^HPaBȬ0mt8\Xu_jmjU`:A&8MOn,88<8?M2m3j*Plm<fwpk&\J3MDpKeo?|sb=F',6}?:+OvȝXO.ߊ12rGj"fM-<֮1Ғz(D1U_=cvk2\ Or-Lt Ѽr>Iӫ/;R}}=i&ϊBdCN 9=GcLXq(Ty:1&0KkdK(ܪHZI]r/8GRZH'JGO019I㗸oV[CBRA&cGmk!@T²Z޼֍Ve. C5r-Q :QVٻ>.Mm}CaY*iUTcIq!ERi ۖiВBh Ĺped$D(Gs'c(b KC@L3FeL*&6ǂ!zA:9 u2ZNØ=s]-X*yx``=1ۉB9iLˋ&> TSO{u~RxliRzОu9dN^[Ȉד%e Q/̕lO3^" lBFӠ>☃VQ75Q|ȭH59Vcqr"]kuj_1>vS{ $*W}hkj+ TRàDRw;?݋-)PRtSs鮜a2=D.%6!d)/1ʶx+? W\M.Ĵ` *Jm)9x鮼w 5k# fM#QAkG׺"8uKÎHc#+kѷ$i%*'.ήn4IW@>ua {13+IZԯuNk9w*?RIZ*MZDK2l8V?GGw+e=haTd8RS]A':Rǭ<44khxš+9Rw8'=T@'ςsӖƭG/S΢bGvQ,^vr~^a?J+玤q> @t8Ys%9W@ӧֲeW0wՠJ"&d/2*hPj; RTJ2zg$ >ԛ  #Q'7e,h*mv-`,(-)' pGׇF EC2YjL|F~:R6;kt>sbpFJ#}8#ZbwyPP[B6%YA>w<#_'1խ5:~~\4A˿tK줃 J-ì*;L$cOF[A(oN<  427w¯O,}o7ԖY8 =?Cרe<Q9LcNS[i[~cGkbtjArtW4SƃB4Gh;Qw.=cU#O*4?Z.iQ&3=.D-%IRH{:+ˎcJ^B }ٴ͸P2"+VۥOHY.,^ $sْDaۨǛK=Wgm-2lѮED?Lyt旑4mNR z&s#cvWi=`WxJ)q֜ JQI^S $&|k-gIouEM|WryqV!J_{9-Lˆqh=v;y8B1%2RKKB0XP2Pu"rfUMжuO&ƥԚ`&[qԲ56T%Lƒ6-$Ƹ[ܼvBvO #IV3/q31m%aZҴR |KSt2z]T)%9PEJ D_o\A,8%zڳJ>B.m4Ȑ!nG O͇;u]t*:BYW]J+OF˕()xDX3dEI̦;$*^zE%mos,2IQR? ;JPgsU=w/W%sDv3rBDqJdq-j^8S`X*quI&v,#@K&-BD(-mhT%O'JepXfDٱZ ۨKjyvј0P%3wY-N_hۥy_yIKܤZҪWQSɧJ\m!;TRLX>C׉Uӡy ~ 4 鎆FLPC<<|)+Թi,MS)c $|8IN*n;$2QhPst?bob=mP#Smhsmmd5hVeM.J1y6o$ (cZIn.v8[@Vow2T$|fSG:{ OONP&gy{[PVÀ:e y'8ۍ&sD}EtY*+.g@h7ٺ@俪E:&+<>DI888?>,JSazl{exlu)zh,6S$!:Pn)A$ T RNJ+2%鞡֭)$Jē\nc&1-![:=8(+jH;iKrЁOOp=#JԒwuҒlAi;vȞ.| 4M%^ ק>+A0J~븣tx/%8:g}<@"E;WS_Ke@zqzZiǮɍ]xͫ.m*qwb@>Z9Z:|]@TYZCӨj"|T4Z,?C0ܨ5kkm@!gs}qP=Y|b:c7VU者);:R+=]aDskJ5h<7B|+>nŸ;g W>2>${+Nukʑݭ0PmtNzy}xmJEt+lYmt!yBB('pQ–HIq,]ΫPR)۩u(KclyO˅HT u,`*ڟ% S;TqVd-lgJDz>n|Z.hQeYA$HWnXuǔ[P}jMq54T Kj3VT%@;PW4-h3 +ҭTv-kX %hi V1Tڎ}TmQw{̇ݯǎYi7?2|/xgi=ģS&ہS| >u8_i)AaY =6KQ~D~M (PQcSnƞ[$|빻iOR ];4tj6<6+pnO3Gn_jPQRSO|^R H܎[ CRʇ78Z?V*M |ma Gߦ$~i. Dk W<[/9[׫a7-Rrt"=.Rb Yl}Sy u4ÍK qƓ-ȓΦg@'GA^➈]u*#x&Hi.%!B NR:9ڞ7g/ڹ}¦~L\V3#C.DaZKV(ȹ6In@IW13y:߱.<>zݫ4*IvxB!!2fL(=hg^lhe^ *Eb!PU*@vCKfzJR[y Pn M~KJԬ&O4,Iyk&[!wM)rDie AJ 5\IϘ;:0]-TWt\[㭿# T)uQ}їAFM]YZQ>BBI

xiCqiu?l/{bDTyh>˨P#^ٻ`y|%<>\ݺ[WsZ\SmN$.!@ԅX$.,@hg 'qMɖrzjXq%(jrYu.p_!IwxϭR0T{%[pNXxBvSM'U JR!I9tpVYRƇcw~y51ʷעА@[X`)y[>/vo,2]Y'qN%YGEsQ!)OP}t%,;=z`cq:Z0Sm6mQ2`d`d+GxVF*ziYW*p+]0ħH[Ju  KH1c8[KNmvUʛ *$uxwMr%JaĐ1vSm-)J\'jn"Fa$a )=OJV9JTX= Z^I(dy#iITQj o-Im ,g׋4WXH݃'~GƷU hB(lP/-]GL>^8-GQF%:S+M9''9};:P3R}e)ҭJT(u8=zzB5h`}f%}m(cz ZR{H}@PiC eXw-g*۰sy; 4i%į2L#NތJ**2[hS~#oc}}aߍ} #YbҢS^O@uWm(BTu?,bm6󫆭^2R=~HQEBOEሶKsRJ"z=8@ E4wMz=emPBK Ӊ)ܗBsJ@PmJX-)AЀu(sgD3ʯ3Z1"ZAޘ굫 šS5-rZ|=Vf~w*MKφIyT9k7ۦ*%HX\z]ݰ܄! RO<ßt v~l=rCS [D)(Lp9ّ֖_W[,Os5diI[`6+O!Ԕ((߈ "bxOM* 5&a۹f.>=h>^UN612UdF|Y'{kKͪ7 ld <9z{=9hUjd I4& $Te(eVC-aK*Th@NSa703;.&DKPX#rF񖵬7T|eje\i9c5l^:CE|B_iL,'Mέ[pgup‚%o\\en) sb{Tf9j6Zt=[:R CrMɘPyJ A.Ā'E AD'z'߱LH;=rTݵt\p*!i\%ؒ)#^ch֕c sꦗh]>[̃MfCW"˚qx1 [ZFKgӊ$ˆ̌TbDNQޣ؞1Zxu8Ë O%!-myR3uSOhˉt֫eڸL:hʥF.S%$HBڐpl-;:*c@HHATFx1$KWnHJT!yw"sΠ fT˻V,MIU5^:zf!{ohAAܔn_κ [lI L;wnEjՍ<>?p *03%^ #aOEirW"-+JPc qrmĩ:\BRHbpolGL*!7ñ\Y)RGvI[DB" @Q}7h-+JP7DtR\$>iNmjn$rjĘޫ ,Aԧw?74M8N0hxq9Ymu؅]iO'duvjjuArףC9($-꥕8Qoݠ'lW9<}9;du}ĩDzHQ k0G3Q3:Nj7;:B+e2v <(Q5o ;) "\ѐر0ǿlO-ol:i)moI0x=z+S#'K&谴湴9zPSrD|c=T hMq/2vEfN>PBNaGT#] 3a#s8u@8KiNK" t9A iUi Yw+bW0ߨ֦9%ꋭ *q&Ѕ!` H ]7b.Y䍓[Ӗ((WA\?1sx4N4+Y'bj;{n=َdczAvHeǨθ֟c6]){9vil7NYvr+1ٚoqN%3jچ\v~mfIigqiژJKRIN:g2MŰf?ZS(i,2ꖆI ! FSJ0 Rn9:uCS-YLxn DaL} cXЌrぱe%N]UaڶpVB9yҮ>^ *i:qF8 e-9QC ͎PN4B3Ֆb]0<m B->%!YTvk흼m3*eʢ /0ÜKK:_64>nzY5JZ"p*J֕ǽ0WaV#x4@qi jܫ34șAJ=d{iuٳoUIP=FOyO:>r<Խk[NSPhI%? |uJKn+dg'< r BT=YoZ(;a "ry-%+8H"Dԙߴ JPs=FR$4$-YQt=Ϧx#SF-4Q%z#aA4B*²2Lx[.+H> ,g\2cfԕ%=Kl%Gni)zvut :=#WTNUJLmJI8 NAϨŶȊNQHS83`~\"LIOD|%]{| Dj5VHo3ɡKvz+@;3;fQTQIH|*c}8 s*g)o<Q %*#=zgӁy)Ba4ZT|iRu$|'_ h 9ϻz/\ u{ʿ WspyДZ1ԶOQPWSy|ȝGJ^W|f|;$zч>aQja%!hVsױ s6Ү . N$?ړH֏-r?XS ?$9yvKYw(hKݓv cв $yz?q2r Ip[U4!23LHjƢ\4$Ed8[^))菭CB]),GR_qښy(yf4'QZ1rX0Eܡ6HHrT%&ҒFSVWtCR!2'͙lv365NzK[JdozNb} y4a~ *IJBgz b >)mxFHR +*LAUk/ [R=lϧ_N)ԉ5W~,˳ݳ]\~m\bTi1,7%%=0x[aگ=>~1k⌥ZBoh]"mFfqmbʀ)Sjq#6j=M-`KWByRzdYvIge.$?RB\`IȂHJ(ni^nmK)9 u?IRx^'mg5R1F~e@Pb}d+"cs mBFSd@hCUfϏ;z8#EݪAZ߷.ڲ=eL j~m}:LѶuSLi ]jPZJcHB%8> 9 H[1vG'L`U?opV'&ӊ8aǚ)2':j3$nJT~QV> ՋiV0MU,MbRiRʾ%U u->YA=O˸! H.'/I -M-*86r&\hIk*NQ fD U x\QRns(¤87>AI_2 ROK*_1w=O,h)L"K)qq#nȔ0>iV6!(IPi^G7J '*2ThVya $$>D\X$,Ͱ)l%!$7 Ⴝ:+Qimr3U(Tְ [R%4,8⒵FW E8})>CO^agf|_W #*Aм4*P!("ˇWu?E"mdkwc+-R*r$g,qFs~JfabG_uyjExlT{2Aۥn<9SЙ<{ JÖ#2-KH sc[3buI ۲<uE!X2<q.gSys/X58qs^g%n/R;O:+S >YܹwGߟ:^dm]"LjEhPyKKoK.8f7<aq~Uq Δ 2LBR4+C FDO7jPXh4Ne}\[uZ]}o:RIL74i~D$%JqMξ<~R0[uq$8@ $)a:[ĉLٌ u!kA䙍A\ĊnϋoI",GHJ~JO(|0iW]*ZUr@:ŷvKn> @s-voZ-9E>Cdm HW YX g+ĂGE4/.ߐ*Xx(mTI=IpT:kYVsbX*/)Ha]^R KAԤ,p28n%RAXQ:6kVPj6]d۲?~Ң&Hv S;wdl>z~aY.e$t:|65>7j(a<Ɗirdļ:}.QK0~SŕT nؕ53gS!Ɨ7-?{FQ0fש;T a%1MH%-)V1=3|]ęs`wگ?GuxڌGv!Q@m[P ZJsFz|,ٳ1W(vsaxgBIԄ;SRPUJD(gC[kٲv-#wc& yjH娪 7+m]^|ysGPǑNRG ҁ) 4S>#JSӵz!A'*J5~xypk`:6" 2$@))X1Rۨ>J`it"u۾ FOU@<*ޘS0or#K\kX,'5M'~T'bRj?BL:!^v!Z!@*<է/`sX~(3ۨ(yo5݃IIRQ)!'`yzI*CgaCH81x/,1ӆĆ֐vZ~҄`z]AT(̚Rc F J*gi G^+VBHP?Og΍PIQ)?p~1sNbRt>8i)PG? z5I oGL YZ]ON$6$+,W!J qHBZO_{HiqN+')QD@9O)u=)ǙBG?^7>9FTcJ:%rw$cM(u 3i?ֽ9K֐Vz< }TùR;}*彌1mL9ruЬH3IfڇpSXmCRaMkIQF5iRm$$s0>*BR杴3$H6w랙]Ӕ*Mb2+Yw63]^JknvZ&Rb'Z/hW#VTZV딫J闍5V~C~CNSAJ\\9*C!% RI*uObGb>M#ýᝬ`/ c6)aTe* wInh**4B[Va vݭ89;I1Syk4>-h:Y?ƭǗkU"&j@r 7V̰PT'a=U6eqq'lT1ȏ[Q5½#-ØBIة*#qEG4p9Wz]8?f (A)r?*2=jOgJFucRi]`nn l$}@m'F\yC4#rSj,7xmhR,Q5-jmKBb€ LĶS+NKu0VH¯klܴRY ZժDAsgMYY) e ~w1^ҀݏthuT,zrWpU%(&Z); ¦=)J^ JN*bpI9Lt>.n'd[5D gP*w5; 5zb NTyiF\ 6yNm!Sn6RP)TK$9ʶIԙGE8>r-ʆFRFr8y9wtQ'6MyIQ*Qӣz굩YEEb:vD:TFSf8eq\mHʖsWt7'G:f-cyڴY4ϗ6l^V"n&:ӬJT#,gn}ČSR9~VXD$y Fg̑~*3>qVni9ѡQу=IC-ń%).+xJNNTx8_8jAu\9^ d $3ATj /[0UyD3\vœ{Vh-Ť\ iv3 Kr1eҕW>Sn,*7$쐭T&w~ SQƱN3ݼ(45jېR",HETք)\R(lJB~,q[Bl<Lj|Xx(mc00Fu_eE ֻ,xh6@S 9 HC)@LӢ:Qi3KnF>|*CG͋y!^rn[8o ~`n̯ (ASu{VV(SA0FΤ/&*@`OH##q: ܨ!ⴢctƖߕi m#h.();Y9^\hDcT8z {y1F0BJӷ$+h ی#b H4HZGVrRe5܀2v\Bܥ$OsC4&]x - 9Nc`hb$zsPHq !)%[OP?> lXe*9BR% IRv>>~}8%)v>#Iw5~.gIҩuF$*lu9* MWAw䎙G&-Zӕ[=l̖qMk)ZNPIqRsNE9TtoÔnfTj=R4\ZWR4^D3e4#f0J=||G A;CdM@ SC}?X >Jt`RC&%nc;|g0wKio(Rp+Mĝ}95eilmIL( -Tʷ+ZvT;^㼹EzK4 w3I8 \z;lz'!Z/ŬMw/gEfkB#F9t[~u5{>c46>uJ ރnByKeܸŤm dؼ œB[n4dpO"I'Qc5|Ϧ1Lpjm2 yR mCr]e$0QT?S0f<; Y[ %\q)ZA$΄gDA?`8*<彣 \S!‘2UAF*vΦ-N6ԊuBTjlRa!,PD>={E/rʢ#D=5m q.m0\u!}s&]ZfucN\~D WtZU۔q3PMT]:JTa "]W@T;jB>^*}x ))6O$xԦ%0MA YyIJ)U[=VCy]KP&Fe%OƂmZp. x#ĘӮF]>#Y޻)D%y)=[_ybZU[ZzW]SQ$ͧTҶ\*Ui""ܕȶm0JǤR1 *JS؞6^oiUty}x](̀hl,ӼUNK5ZPY՟o!+$UZ2^.ܣJB4@{=5G/T->1_ ,ʈ ,6{O}% .ʝ[6T'ByUܞx6D<,41_aR.9oRLʚ n~Kd V$CI7v ; %wJ)ISU=].xf3% /O@&UkUƼ8Bʵ 2K:k% wiP1FJ<&WͶY$,V3idsE{x<{!E񔘔 "LΨ*U:2EF'wN-]BR:1'xpmZ 0HkxqqLM$h!\TpQmrRW*٧SiSfuimm=Vִ%)J>)}48ᄤI'@y)*!)[sz:rQDV`o:rE[ߍHw7L-ĩ f+J^!?C's>)UneI%CIyxY6V懈kПeO{bçSbЭ[nO1fC0Bm"yzZ7:ŭ(H}IsL:\K-g$ya\D3ZgeI i BIRnI!9~i66!O|D}-m| p_G[Am.+6vɏ* #s=+NЇJ>6 Pj\܌ScԧTJU'i .bBwJ`)!wB-Zׇ"I =koQM6U>9z n &G29ehB!:6"*dRR@I.d>î\Są)((K[xZc|#%O4)\9HaΩڼ%: k@Z+Je)K[WD#i4p@HiSPJH=E_6o_6d|֭ngl!D:)u Q޻<ʙ2^ jBPJ =WB@pN >OIMJR<|:ySK.mQ'y;:ӡīhA¶pqռ*aӪJρi4#F+J@_OO\c'ѵ&KଖNUI3[LLhߟn] Do ixÁ I”:q1:JC{9lcp%+Ŧ#Aΐ\ <=R; 瀕@BpI5-ғ8? O>xځ2E~\G)OWQI fbLa,ٍQtwH=p:Ct\$(N{+ VgH9bkh!$3 2 -/WL8'CNyw;u,l~WSCڭiwbi66hsmCiR!N:{4aV5 )Jq΂v5܁UϯVT3SuvUvS N+҂N\\'kc5Ij mwejer#q{ǰpM"՗t?}!u9O<\WjZY@iTR(w z*[jlHVWӹ$8+qGz!M1'ql\CaI-$L3n M딘j]7Kly-0fUU#uz,Ikφm:V7awĞ (:9OI_E'1qEn!%)Vi!xDxOE&&Xwe"X >vt2дbSMQ S)( 0%. Zf!r sa]DzVVKAKkܥM ᒤ |M1[;O<Y a3%IԄ(RT@2kfkbMO,xY >\L J0ZO)fh͚lWi?S.y).yJu1' K ;+$#a1J}hCcxµyvrM\U n-"R¼5%ՐtֆRkrYH9VS:iYf@;7M <ZBeYR&rP<2:wμ]WvjEB}SRj=Nmԕ4-GYB:|-(x4mJ[}j.$kUqvR9!2HH'ݪҶAIJ;Q(Qѯ'Gz RU>TN[L+Q^skU/ QSyI%]u Kosܜ̨r"Ք(:rPE%%Xu3=s4U.:CG [D{}*T=>ʬܴw"Qr|c}-mƉ u )X=T2uJ-w]79dz]*LLP1{*ɗ6e"3YFI'\SIl ` <>/!F ԬM8Rm:*G͖mH9n4ܤ6#4ìdAZ\۬kuP7m;LnKKskC 7֪T}O>R)Hky |gf2#ػK)-$m8ȴ!v*|#I$P5X|>aI΀PHT4!*BWJ @xRJV'/kϮȤ5HSމEAKJἤ#DڐRd{ʘۄ޼roM2nuj߿^7i"Akڳ|*/Wo,)M%-xP K͈Nr2[SWIp$JNďnT+ڝJ~HտJQ[@06m*ڒ7wn(;8u3wpbL{dLiiʅ1?QAHVUz)!Ie e'm( 9 >W1P|a3CCYcZQqK7l2ڔ}y$0;Nf8ຮ^i?f j4Un2 V=>KrbT y )M<!MC~ڒrCK)Nđ.F켌<V_Üzv}8 A@ϧetH,LIumշX(u?.[J P\JT!BRWIڛrufJED莴dumFJ d#Sj*QL{S,'VʹV`tgO|IB+Zgi 뛋V>^T ,LjaLb8m*xbGx]YJxhǐ꬝i-407cm=j싴g,1UoY EYJFN.gwM>,-On5hUf* ^mA~LeB\4)uDW;KCEzyVUJ^h޽PY\F݆7wuM8u HI1a -xs)w:MBrޢHbN?QT5pT$9.<$H)Š>siZ%Ɲ 2t ;Mp1F* Y$w)e [ C+j@!Q'4hjͻGßݾKo;nuH)3@ ϧ3.&_nieGO?J[>O$xSiόd^RU7wry"N@rT{HZj.). Z`Sgh+pubٔQvRjF1ܐےp^%Қĝ`O*7 R5e? U?RKSjh{ӎJcYҦ+Xmti{f̪қC(M5q?vȧ4p⵴Tkibe*R@${!7.El6R`IT$sU0{ڕh`©@qThsQZAِR\Hٵb CRgPSK~ʰTN Gb}T{k7zuxkNșjQƜ6!UB)LDs&>8GϭM^HRv;NGOCR R7mtјvDjTUvH LMmIy>gW h:DӞdu+>yWܮ_+rinЛߖ㪎iQʥ~ѵLA puÍ30Vօh̐R|ޯJuC9Xu$&qT75YAJA^*,[$}Ҭ'BDFzR VSáBI\a56Cǐ.JTFw4PD5|hL)c8>ͶޑT{jQ QkȧSX҄6$8b7nRcTp Ëxa+#r)̣; IP1]vͤ4adH(QrBj^KQ-)EJ~Ե(- O 9s'}ZwiܨNUA*CU vScM* PK0%E:T0PO=Ŭw"V`a\:R- :n3#GʳO֯j6iJY~-b9tŲPr[ZfO6❳o1<)?ZA^A $ bb#m1<%۳ I[?(dMQ_L \bA~<8$7.1Pn {RV)R|%:+wWl7aE:voVo;Ķ_p =ʵ ZBc:iʴxJR/ :[v *.c KfnsDIeeךKqbΐ 7>'>jz+܃0 ! o7mf+ơx=jvu2[qDkZ!, x8ӭ4->{k)q. l]z"ZP;(DYi# C>gNo \.H#m NmFϦiP*8Qڼ-x9}_v( >_iju>|k>nY,ˑP(u(|ɤQO߆:ځըl6qe*Jr'(>O_|amy&UbKU\vVKrPbiETW[+YSk U-C|%'McrH {>ºmUЏykGGI*iF[P@I?ǿQOl:\NTGˁw^%$gzgGS4)hͩNBRJwaMcZI&6u%l%Y9$c;yp"6J4b1PVRR RT ~.$O.5Ʊ*NCдeE* :(PNʋuI!%XCq%@ ހ sIںSE R{׿(ϊ4Q}JRSzzW 7ZwJ(n);?g/QpBa;*)1ڛfU[uۋ?%@YsUYNARەh:T/]j4ٖ.pS+'#cn(O8!<ݸ։5Nڈ$o%huؖUb~Ag*hp$ekZtĒiHJ‡Z'v%Tj+-6Kn~ƌ:Nm܅&G޿N2}; rF?:|o6hƔ5/|=13'z0&AN ?/mEңg2|rv2:!fjU}K% \Tyոjj'!7#p_ACP>ӦaW" }v ڊOMFZ,%MY?\6[:OSJMӦ*$$xuN lr0?)GX7$OnBT2gO`B@Nj5Ugy\kE~S*TJ9S.]"y\N%3 hTHˊZ rێ=3rdL#̊0> {¬HPP7Qr4Gs9[*f @Ϗs Qib2+Nd&~eMqPYVBD{qwR'pcM>uzz(oԵ%muk)pִHA{}=y o>ن IQޅԟE෍2y ~U؟uX $ay\sZ^j(zoEM^=%jTɄz% Ka)$Oz@gӗk$M6*+'ZWT[J4]iZͼLhveU#&svLt2)3)/(;Oe% I$J >ϟZښ! ui՚ mVl)> C~;c#w*n6(@BQFP[UЪ(NCJ}+ ();$=U(m>-QVO>َtk#Vgia =GԚjj Ԓ`t=GˎRKJ-U*+)֭lg&R1u[k-+ \`tFLf+!ӻhDd%JY1!mU/5S^h'~jYk"+-LL3!U[ڒ=ui]9%,ӝN!ocx.h^ER)j ZpE-Y&;"4KlKНdczNJ20K'csn%DHluJ?i%zS?r +t-pvJZL3(ئxGfkcjC,@Kiqv2ٷl>"Ee&F|?b_nu'߶Z;I/9/2)lŧƜP7'ƌ ou :5xJ vT$)fͮ7&GZY+X=w* >jT/AeURi $84ڑ'4[Bה{_ qVvAOp-\Jn D& :È/\@!Q "\#!:CjsG\@d,$@  VBq$>LNC“(jӥQGjEĔ4%! \TۊicZ l|9 "%, &5L?*% zG/a_mխi筴9!Pv$t_}Q⌥ i Z2Lpdo0u0F@&b61Gj5vtuF黩MM:ENLY{teԷR.4h)eWR>x=8ڰm45'QOH'ZR6hF_y҈҇Am*a]^^TcCk[vRxmRI~">@Iׂ%#9M~U.y/"]u:'HK8uJH)# pj֟ˆP&CDէTTfzdSU2KѦ8@ZBX6)q$z AZ.~Tw0cC0yißVi妳"aMY)fJWqm<x{836gEA&15?@r+?ͭWr+W$ ՙZPRAptVZ Z}{U^A ZHiwƟYlc(Vz(uG78K?ьV4_VyHy)ˑZ_ae9M)HD tC 7xS PS.bAC,,gi.P<p~/!p Iްjzρ@XPH<;ӌ@E]jF]VEFU}AuLlx)HJ}RJ5&j}r>ERywv-cGB Ɋ0?9p&U ^Jq vWn N|I"ynr GIuiN}&T0*t9Ѥ4)K(mm+vq)ZӅ},.)޽J{c? ]piǻV$H$ Qs= %<9Mz:ufExZRXh}CQ5T4 ֩}6L q7u"P w^QWt HT5ү>xñ|niN^NljQUGϥ_}qK+*d |JBFN@~Xt1Tm9 kW> NrI$̙OoS/ðyԷ'Z5C)?nJK̅ia#- 2uFcP)SB,lYvd-Յ~v4K gUtIhi2d6 ;Й~8/ɝfjG-7Hu&̹nh :2TiM. u \j/}Ubn+Ʈn[ @yk)]A"L㜳wk4z6uM!"h}R SS֔S :t}˭۩k:˥3V9RoSܽ.m*|)Ue6&ݓ%[e*3Uk1jjVҨ`\;xXuj<&Ejj\wA{`IJA֙B# ε: EА9=;+`L+Uv7jE駷CKuW`H^@pCC-O# `pkc6)js9eEbsHG;RLv7@D*:pN#`I99vwl mQnvLtQJtӜ \Yy-^FMGSԨNn*ї*+h^(5^0RFtLO '%e6VtֺJW̮†a@ PH{LT *la;*80-A6 XU ňPQX^tyo]1&lTw5+Jt>kJpNNA|u4\1qnWPDu2.7-@5:PKr2Ի*`QR f1BMZx)?,`0v&W+6&Μ?+q s*>/`jmܩ2j@="b%2]1p:*#ǩ i:_ۊժFK*B,$0J3ǫ;<:Z<ٮxw |=?= 3VVTgS!"4%!gm*;;AIcrbg2;oqSg]t=Kק1qI^q !GC B,$)chq : Rm;ϗ QF(C[u ) UBHIVqG&#Cm~Z̚ RRĘL͇wH ic8qur7W qՊsɈQKrAޜ$u Vc)}iWR.WV aLYBeb@@:aŪTHԁ$_ڗ^j} rޭKuӪVjYS:vu1T\8URH [kܠP~ef0~`_e I]U3:O0ZƞYB&M*+4Z))X*9VWH~$2iD еx^I81D5jMzTQ+DHq0_I\eKxVCC>roo=^IJa  [ӳWIXZu 9նA#Ӟ#P=0xg))0R"N}Lө ɝyox3߿H ‚v?Ɣ=յdW'< &6OY7mͥZO&vsbT4㮳Tn!.HBS k@aIݱ ++m`$i1J*w4}7ցؕznV篊 ƜV$G5oJq!O,FS/j8Sˇ#m LGw=Z:d'T J i ˬ :M撓Oԯh--QNѡXu ^jw1A#^߇Z! *-OjM(Rw[7'q!T8<1"Ćn ! !>@>|Y9f C,%HW5%(RQf(֝`"ۺ@jڃN-LR܉_}UuةSѣ[[Rq&Πo?ҝ-&OSoYC Tk'ܮT(^dKĸ6dmEi%)h.c4Sjĕ$?5'2LN\AMo,hs{@_Mi}&v*PC_efCV'د |&ujyxމ Z.|U]?oˍ zOVدO&.1jR8zAn)֐6'Җe8a*RgI3AS;RR;#IڢhXzYƭj/ɮh]R })JZqrfW%8 ;zZI zZYIRw'=?MNFI'O4]j9󣠗V[ܯޔ_3]؛hˑDnb䐊ZܴŌj* 4cGRl9]bE^Vո\HJ}gTs؋':#7+nԏLp۬Jvnn4:<άLUioswd{-sUE2ORYRnHDc opGe?¯gĶ[W&wy}^Y9RzaBC1B5fr-im՜a2J8cxU%GD3l\̓̒IMd,)IN@$xxq"NiޔVnUXLy,LCji)L[%'€V:ĉwY.۵4%w3]wrH+΃FS$*ir**Pl'Jt*8+޲MoeJn끒 &Mgmad$̒a+ KJO1rN_.0h7#Q|KXإR>D>݆GʽbNV$BP[2 y76)GB׷oޝm;zܨ<ϕQ|mWVfY(_*cTdPs"7IsjTJ% [@(BVN׏l5+9GGҬ7|PrF}ʤ0IuT!,cvRצ@:S:z6O"V\+*1%mg۱ϟSjs%~aZOUKw]ڎZR_@w1S )1$E +)9Pfm92Ҥ3Qӧ~0\}РJI!*6:V%n{Fw G 簾r>B ˁgy[Ăl~,yX z9RJ틚# 2:KZPoܤ%)!' \IxrR ŵ)J[)$I [$´ԗ|iDHpx5fIONNW^kJN`(\qZljMT.]V׸eʎdxVT+@$ldĩ@-;GΠ7!k %tkVݥKBV$g6$UnDRmnqTRP&L^!BVPRGN^wRWel>ѧv9vSH 0w1ԛe\]B\כyȮ-KSj (A-z|Xl' ڴLIlHO"CQHB7:n%9RT22@ׁ&(1W9~rˎ5}Q.E]k-R%6**-<ꥨ#rzOXj5JcyJY*1Ԋ}Ty:"Rnd~5BH-i q|HM 9An 7?n,TlmגFq>=~ ғyU3ݫIZWP!ơnR -,%Km6mC^83"1秼hs595=61n;L\y Iڙ xۜQ:IB+6qbxmWW/:Dz%w4yn*n*'&UH9{\WFN$Da2O~[Oƹi0)'!;N_tҾj/T.Qu-U&Q ;zl{2IG1 ayc+IViA&:OV.~2~i6漡V%]'>Wϲ^JxXZUtu *t|0Ɯm^"J 59FNQVg;_~ +m`qwV_v+V4 E.ʡSV4uhu!6bӻqGPOIX OxZlkZBiO>`ZXwHU"ߟTgRc&D'(FY)hRХ$ܶs(9Ơnp>uSyh#qSTfˀmQr%`xrTEu K'*k Oh}+;>旒+2&͛k 6&KIq(uKB_8k{Ḩ!$@<:sJͯsJuH=¨,׆i+NaO6~tB^E>uT["j0(#!ME,1ua~!Rۻw|1*PKX Od+5ei֦m#OOm sSOKR)fe9jS.۸]8 $'rlڊn0hUY\aKRCissr:[#hJq!v u'SIyWju uNvs5{;5*KDW'hOE祾853i>UJ}e4A>WUi4 r Q=,-'9.6,u,Uq&B1ظt>b *&FHSd380 gjr/:|7u(a>BBGt'>DԨs@nwAJGd[>WM 'y;)8Jr'JzRƕyY ze2ͩޔ0b)nԊkgr)X-exOI)ܤ$Ru$L;LXjS]J/6ўoWJR9~4X,Z7(ٔrMoÒӓ-Y"2quڤ:khB4se*h]і*i Gq祲߈RHm!~ Vz0@i}YI%f 0$M[>RZO}IQ[S( <AhBL?TZq7$Xiz,vh{)'{%nm)J!!<<0;sAksFLʐ[hJAX0z74AIt|f}*Lw3 $#)tcdmX |KOsɴi5l !C[p F{p)uùLmQkFNIW >01'KmJxNŻ۬HW:{^nkOUZ`,\q9^A'<5p?fRʷh'^J22 ΄kXZJ, ֢!Vo#:XHA#9~$%>Ubxy/[x4 z?̝:O]XNAzwt̾d d:3e'>%zĝ[:tH)Tϥ)9[S rmTҩL% %%EAծYZK%9蔀eA$̚W-}j=n{}W DfT^Nի4p#%*|I7I|CaKޖCi $IOag7ɡ[/a\4ᴻsp lAHz a$}މ)ӄ27_$Y>aWJ̣q 8]PQg sQ1t;jP<|+ Vvnp)a+' gu㕸xW8^Ebΰt?oA 6T~юqmŢlR(+逧-ضU)[BG9x糜-7 ̟A^w%}ޙu? T'xx C`, ,v%~렟% >1®e&DD׭53펻t׫fz.U9RGuT8mI nRTdXӂ$hm?RH C1Fۧ:%֊r3m]W/W8|ԫ*vuڝo.R'UʕFbBJu3RLf}M%ōVaחOenH+i@^s>4 >ϝ WL"iMUJ*-GwJ(Mv<2l/W]}M3\[+%&9=uΑV)7qɍaZjYrkfN;MveX}i:Z:ifQfY1+RERSc9GYBY2Yksʎ&d*e O:+ qw! dtNkI6zmtB Rޒ%mTw~'yXqZ*Ky &i)NN ӃT> J]2SS߇5ie B<\)q9V$Q:ՙ Ik'][Yfׄ3Oe߫rN! ~koUfZQgCyU\-ȄG#{Dڸs??d)#H֐sەce D!ifؔk R$'>R1A)K;ExUј0>󦊑nzYx[jԾ1RxJvFzq`YedBiZ.5pĤ@v3_m)5Ql *J2w sZP:Rk5jbLj?2Vp\42}gzSD$mΟX̓}MnT^*. ל)D(I>}BB'*ֈ'WdSOk) NB {u„$F_҂n6V)P #Pn>~l$(NѨdRJ:m) ({#q[IT;kyUS\M?]2n!LlJV )vF2@TvNj=stsJ mҊ9Q!GtSA25 P:cԢϤt;z+%S w"e4ښ9?(<+q+c/-uFpB=*5ɫRnJ5 4;ODD #gl, KMjަ9"q<ϝIf~U`1A2:M<JĶ$SNPcjvg=N[$8uʏ퇅mq7 2H #qޙDߵdZҪ!eGb+shwSE%=@fW6R:A Jҝ6$kTLC % LjRJt;RT'Ni3)ZFATSڪ) Zk8OLr^Ӊ.8b8+?RF??;"+pe/?fm C}oiVB(o<Ր~^=#_s} !ξUpn2U8_;}:.z95,ԬyN,Wۺio84ZJ$k݂4í*yVa!er<$kFSX:͠.%>i*UXg?hW5__HYb.ZKkq$$8Vx4Z…6J$*#P EDf3Ch# f@]>ᇌWQ!XCyVqÞZuL7=lLJ;؏Y{X^yceiPjK:4SL>ʫI"zH;$x#QuoSS 2:M;:Fq tTŖ%Ƥͧ4 e Qe7! y'OO_h>FEB\@O3zry}=^j;@.e][PVIIn2:)F·YY91O':TB;6:{ݡ>RGwHYԊ4>Wu׾CF{cx.)֒!12#VQS4F{'^bZڙ FʚƊW^7-]ԍxk5Zjl纷!SpYS)Z!"Fִ.eQ]4OS@ӻ˧28mĶiZuԆćOd&&>KC;&;VlP RY9;3gY0a 3ˇF'zoiHw*F\q;Ǘn2+)aj޷(ɡ[-)@;/ ~LφC b0vΗYb.aNSLKR[iRcjIrI⥽a)tfÐ{?JP=LZ`RLNՔI3Npz|Og?js[y4;-Ps{{ ZәDM6q1SE([JO0~FxZQ*Fs׋ i$+qC7ˎxDaڔs$Vl+9I[鵰3ztrx@쏐GA&u?Bi椩fjcTE~o{?SAtj!G )Bu=R$>H| ;U{Mj&%Xϟ"nSݝ;˄vR N>3w`.l-$A=})d#T>gr2q}xKk3n*%P;uH KRm)jLI-~*.VIl['xH7m?σIֲv>X#kbЦSeͧ“ ٕKE +GN܃A;1Z44h}{n/F۷ӿG.(RdDCpgձ_P Ő6$/N,\^v>tS;FqvA"M&6VS!RIP8'i^GWr8C ؂"Gpm_Q^u+/2nQCT7MNe'jM |'|~iec* AUl6e7&2-}.^*NāHim DJ=Ko-)@RR3I|08a A\ D:iHZ@y~BFdy?956iңcߊXYFR}:Բ@~xֳ>_Ae6G^ icQ_J!DƎ +aC' %jvA]Τՠ36 PFy*iTA:lSCqlU!/G+|z˽F+R#t Ae31 81R@R H9pӘ~=`) :rҠwt_S5[yu&Rj4W:UA'ŧ>f2H$j*6+z[l&wB&u9s k$(iOuruWl(.[\{QnF6aTnFΡ t6SC'(_8NC]膳%C9:sSR PvbuZkzmjLHyuِz3ү ʜKJOAuL:'Uͷ3HJgUbU*=bMp] KeLVa/-$5|]r:D}Mj BXQz-sk諳L$Wۏ*sP٬S^ej5P\H8Jf^Y ip.ĉ徒@1hj5qz4i4JA ecHm2ֳku(Z,Bs0~dRfJf 4V7!-7B; c4MmB "*qlR"Q7$ l SI3#DX*ZV9*J 5hTIԢ)ys9o Ґ,DɶA k΀BPh\k#`-j1*R=͵ӈKHV$Zr\.͛ɳFy+]%}?6<)|Tu)"y߇^ѾM-9v x!v_LUUAS _2`*B~Vdf ~zag1[*VQ_ITҰ-8VI綇'.bV=Cj%vǿ–rs4gG0( Ӝ[m] ru fe{g}{:pkSOk~b=u}7ោ)s!lޛß 1Y\7@#+`PML1x& 0[Ugk}⫚wU-i[ M?|ZBҢSR3( @#j (.6[l#kidrrt$L7Bs^I䔩ȸ)0pvl(p$ӥV ǡ0|0)bJ&hԅtI uuGT )@):jrԔDc~ afTp jJZg}!QEo Y` :)X!Ra-ԨI ;XIkHhSBl/JtkVeMp><;HYMr (*dzbQ!VOD;Jϑ6wT/kg7 ۡ:r$FL8:DPY(iHai: H $LӪ)D% !'˪MA[! u*uA7$|JmKR%DI1P ZH騪Ln:֗ fnSp&䙿t,ҐR(=v;7 ^Z @*%R}#ă/ | [HBS5E\HPi QL o>D!ŭp2+)P"(XueHa!JnHBD!JqRɷCt(Z:TiTFxzGOLHKBIY RSl(Ɲ:izOoLu=:܅*[ <9fF0:'9({i3*L~Q萻J%J]$XHlo  qo㍂$nA1pzMuT#/rXQR`$ b -aLHRVi%%!& *V4-@4`BS=Ϳ N%-YRT؝i@Jy$ 6`iI@typ$DY e#Dw"#nؙuQ>[I Yq0#P@#5d )mJBҴU3 H6MBY4"` H\#ZKMN•s$nI&OH& )4)T,L`%AtC#I+- [7=4-ʖ] PxW}o&nu_%AZqG f"7^F{ Tk|~+A2EJ"d6S,nDϯMMͪ\zMe:@JHO`KP\Ui&[As/9 -;Q%6^dp-)]ռZq&a<]$,$EQ4%D9ꛇRs(q P@$ji;1ȦuS65OjBSy{l SA$Xx)IM*\ҙ'q뿹*n>ptIQQɹfY+ @[D*)tso1,MbH7a 4<A#>;fvJ!,oŒ A Ru \?L8e؄x{3pTHޖ6cBRu)e()& 'G e+q)A0U DLϒ)R G&7r;t RS,w~wI* +T#8kZ6 EWg.r)UN$+DN:dBӓ$)`'RV?6_5-;XQ5X G$O!KZJA'n~q0jy-; F:su]4d̄éEEciw#_ V4~ct`lN oA0%ȩ_ZXXfc>˷֕[\#<7KO!AjH:`LYaήg,)rhpԲr m67B84DBېJAVlb{p{JsXҥ!y'1Mtʇ-)\ 6 'F[i!k$m~Ar~'@< Ky/ HI}?L9f*7λ%ޥRJTT}:zalSIpIAܩ @%%*$۶c(< JˍN,dMoW$ K%#PiD(ނ@8M*IIRu Qk>quJ׀=)JV+1q;~_鉢u%AP@ j6טIcs^$cM7I+̥ $Ϧ}J u 4 %S)Jt¹S3!8T8HuvBDo{%ejTV.J ]sJ!n)B N,SݖuKLun-ipaPNmLMISQ3*0I6DV ʦ֗I%S,ЀS&<1y }2K0mF1 u< Ө;bh2n`bA:T ւz 6;~E!?~<[Zea=&tJ}}0Tn`O4 u"Q΢J@)ŷIh%cK*\ IfoaP]*\7H X[xtBæIv&g&>zXFYYmc`Kay!3zt-Kn)@p&rwK5[\b-Jw#PqiFzz#pڕBm5$(”u(e4 tíȩaUd~So|by[Bb,z߶9vITdi-UP#PX|1Չ5а ):VPeD_~޽ @E3L%\D[U; *(Ҕ7# $cV\؎Ӹn k /äjR#FZf]% BJx*h=ó !lXZKʔ4~mȼL1#ZABƅ塴$In'-aBQ BzĘR/~?i$)ȟF2 =TRI^*Rz}pƸBo^xwl׊k^IPJ]כ^ߋPSu $45`/wQΧ+^hN8]O]&\TCyJI:<9ug5 m|7jv>>F_i橗mƑM`㙂8UO;@0$I3b]Őya%RAJ á*<'Q#ZÛ &z,RAJϭ 9vFf PQ`n u1: )x:xz#ͬZL0xh#VTegf@0?aXuMy֘NQm6õx0a7)(PB L@1!'pTHci +NDE*Zֈ3JcML%\~^=~3#PIVzL״N1aaHC! \*}kL#DMKqMƧ16y8ej55vDmZ "#L KUTq[pА nOcbPZ sUPmK`kRd)GIJ'{ `#D%[H Yt nLЏ8efrOttIC> &AooC{[IqCi2yGbLf#{gP5W-N_ pȟ|?Fm/xe1IVta ܶoOS k{#aVZ*4*LŅ<4A(>ǀ#u7)c;5BUzpA&35`SY\ô<0ܕ( ])`(?JJuamGtvHi8GS&F'h-?fdg8GrL(W$}I [NMOzڃLg^qIX=}\c]OYc`|W_pwS5+:d#4eR)ЩˢbOIǛ6;K-kV1͎;%"p =kAxT 1p JJHDmᘎ*;b9:½pd+6h'Mvrr-ԡJI* *A ;fTC>{O"B `ZI&dbcEh3 [)&lw=0] jʤ n7釗HKWm l;J7[8+leP8b Xa\#+%ajBS}ngH2?ȤHNGZ>ù[Fn: IF$m{\+1I/sRl$MFX[$Jm* ,X$脣hi!D%)H2lb=`脊K~qHJ~r`d ŦPK]B9 ,B>h~ʻh0dD"ӗT!IQ H a}a0ԡqMLrz?/]ۏF@J ΰ~'7@B,|ۀ+z͡ dX̉/w6X5~KÍpb#_vVq3$$60`XjUyrBƫQ]&Kԏ/3PXVc[^cQТ$sXiөYQ0G4WSOL7Y Pcˮ:g`}Se'V$@>IERnw ѩ)o".i`(j&GE;ˈ"O(aqXrx|9OÃU)BBHLDyA1c˯*+,mοT!H_TwI*m+4lW8tK]M %NuK!4;UYR)E-wu)hty 7 xOcuϓʋ4fb7,@61ߵ<r~2xN֒όeR8q@hH ~K+ૺen*Sp\ǟ 5 ՍE %9>bd>%W ^>1 ?px;>.|qُo0y81{|BNiYAqE*gEfZŗFHqP?0 vǭX(Vȯkkʔ-k ۳U xX;D#"'ӧIt/gSӚsR㔭]A]1jMU1)Ko(ZN׎tq3$BD[XeEd@Mn޿/t  ˧ZT@RG y!R )N"č p4v)>:Mlc;))J*\ , čfd(!*ޙXY\>\FX h* Rc~ۧm51.ԂO#Qmk2F=ZnpV/[ZV]SKlϔ-KImSiRW"f:XwEXYNz<9Q-9 -WcI*H6=>vY2 ׫AG3Y_ԘbPB\Դ(Q*,F[y!DG&%Zm|,%P)kW, T"$u\CkmNfXB*>/78t86-W},-JSc(NUFl#/zTnZ̦q 2tbkV@JB+iS'~`4S.btSmmtd+q+Tp'ziNżx[X UrE̾҇*XjSMs(ps\efwA|c}CIx#QuB3CZ)nQINd9naAÊW@% KfJuDH@  H?ʓnYԭ\vf2-V[5oVƑ@U%Ez+kNWJӊqwn#"H#+Tcyn eCqoy ;Dq[\áJ+d) u&l`DF@OehXw44T9 f @n7*B\)GdAMBf5ꌯ`m%|nֵ̲l~ĔDBdT?BMJA VP pv{Ӯ4)P]}'Y Bzulb?\Ѥ\tjINYFgARb^U'(o?0+X}v::>k>!h-{i#]G0~$MWm7*x:4yn{8w?#Ϸ`P.@:3ǦUi/Ë"?| yV^M@-v7A'/O⍊ IօYqq:=?+3vY+NITTI K2Lz&n-Mq>d%.&_J'?i7#DyKm(I:#{dM.Jnva]>3HOR㿳F-xE,SSAL9٦u1 j gQD0l{F؃C pfD/y*?c7VOWĹmWGNӵ ՆR5)R%"I(Nc_Mȴsj: '`3锟O5b#h^LtcSn'X^G%lURB,#פc r-?JtDI2N7)B4q (U_4LǧI3ٴ8 ?y35k(:΂+zTjJA{ϏxtHsL ke{=>*x2򌪚[Z5$JTE¢=1c6 2ӈ)MDw #{Q#$se1-U?.DwoOkzSNU<>lJF [8ӱ­_dQ*sOz,.,ͳ&zAI RH@۰=m\ UJ՛Owk3Q_m#f9NJ\RڪJ$rHZfs(̩*ҿ*w:}goIfn_"$|BЫ@iÜR+Jt1"=d sEVĵOE<>9,J `yz 7(T<ďwTϺ9"k`&8~k*;.0LJ8& pe1.;tV^%捗K LXrGӮ<&R'+A~pVQ<ۆf)[y Bd1$zN62Y_Ze3뢂fET*OHV+Io'h4 H.{4l5䃢 .%hH*:MYy(njUt@xb/jSKONa)-!?2ŌonM^킳z[a6HK1I3QfC<74˪ih$IJJAL ӱ鶅!`mQUvR:uR-jrZJryn455+Jqww}Wxc/NObxsֶCEҿ uNgT临" ; }-8 [qYk@ UŸM=cŕV.\yhFqñҙI'֖ZU$1gw.y x.Q^"ʩilj%GAb=4?.U!cq0uC?"4|8l֑4w׃Y[*qITXgx'ǿ_q/oÔC73oUNgdxg ** PIyb|ߦ='+T˪ow>ҰmZt-Ng (m RA{[\ubxT/VeS2Z-:) +6qa:o>o8ʄ%)~>tO~A 2ARLQ`@H}W@UJWdu?H(Zz1rqAje9ZeD*skpPJ0E"ǿƁ|Ί$BUq ]KL :k\FSHLVJY j$In ob-|' %.4u qo.i[E;*mD9QBD>zAq+j]$ðN8FP|ο/9̱b˴iқqǔ`ml1?5c8. 7^|S)V& W7,.FflVmBf}'h[Uյs9.sYy! `d9=Om*,ʬ-9RZm-k3R*BCRR8]AL5opoP'E"_*o`$%}p)PԯR\S;pfڦ+oǙpow;yx\`EMrHi~e \zc_"qF,>ր 0}N+}ݏqh\4-7qw ߇ͩq쵷R}SZ IiY)*>o<]^ʥv`nֶmxmI~Q|wվ!Vُ$4>K֏OT?d|CIq : =U[/) +XmvyL+pt|קUMwA^Hv|z1.V2_RֆmHHM uoKILell^w>qIVBy@AWyEZMÙj{˾#4 O Ģ#9k j#Կ}r)u pj[Gf<R*aN9NV֠6?%qojUjvu8BH)Ouw^E:=}o4 >0R*sC"`f.[^X< 䁕/8)!nzvxș~oÔpȲ7U:<_pcn_@m:ahNܤB@%!$=:M4/|C=˚KqQ&ʸX?2dki-r5'ıT^ _->3w0[$ kUxpǡLm' rw]N&>{S AIf$,6l N)V,h\74^[ayPn. -˒J#I; qO?mQ;U^]5r§~* MV뫛&dZZ{x3iv[fˆ>ƸN]k`IZTk.hlgPtlhop7+J/>`E Pu-\":+i.z ZiT{sNgݗ~?ok-/DĻQF0Q7%ʇP>lj/+,+w1[YB`t/omQs )`wmn> ey])Cҡ&5hr֌`n[ZUi dh"FCwnk}Cɠ,..yYM:[TP)hAsԩ y2gj-2*t%osdMavQѢ`o޹|GhqHd7璘;ôx9Pj:᧞U=k*B T0yڇ BRT@8!lhv]uZw@86| 3w<߅3|&i-B )#χhL^CV = +9EwW 4֔4CjoCACdyc\QZq pyi$5W2۾}?\Єe'Ƕ; iԴFV2 ([j)iݧ3@ק=^9kGk[Hz%CI2\Ll;4uS V 'x6gPsI#Do=w3F;'iP)fCTZϙIШN=u÷]R:zUT+ndH$nHO_c8p*2 y+0k8ϊRD\^PD8 WE۱YwڝWC'/UȬʲWf2~TQУ<‡>R}[`5gHKyFk쟆./q=\KH[Ä|Y cjgVwrhHXIZ $E/V u\K|FHKʊJvд:ƻj#ZrqqAk8>#'}j2dz:53rvYC͠e6IPl'.kZVf'tA~x\xoysO-vg\L5U2wK[fi_5=L r68 '77_vYAH/CrIXA؃N{ c|pwl9Ayq>kp᦮#Z.+7(M^5w^|e^3/%(kP3 1atU;U-Ƀ!IL&ڐ)=at:5:pגO>39~j23]ͼFqոފL4SԄ,˯;EuJo.n_LP,-`0jգMsh  2Ӽue2J 3['xWq|.|!| Nq',9cUt_9EW@aq-!*x!mq i¥ʫcCO92{Nq$nDwxp5,vָs sNi8$'Go>?| |9puUWuX'i 2!AZR{QDGpqqcx;V]E6oR@!ĺ B|qV ! tL\YQJpr9 T'JH cwp 12}#P129yN׭Iuñ eYUV_6_OCEj:4(--I$F4{,6%y`٢b^k{@oEqiK.<τq >*S Ind+-*b1ngN-sىۋ[+vt{K G1Iq\pUnDӥRLo% 5aj)dF@[`Hٿ:O }4 tǠ1f]q}G=I)To ]q<B1vc, k@t?^9zl$NoU,N~pB:']k2eu0rAtT{̸&n+o6u% TZS m 㱲ӲysNi7DƻHup^.)^,'ղr. +2]9Ujh}5<6ҢOUc2*Ms1 Qybe!cT]Z~AO>p Ͱl aB>xsxRHzjt.xօP_ n mxͺ`ΨC}x'!>#xs /Oys<Ç25/-K?mN4L[CkJ$qELo|KfUqS,t_85|kkqP֘vI~~"Wc7-`3jlʔ;3AJ%Je K92 10m>`uJQɆ;]}gv3n4:~ө0KO@ܒ_?GI3Z:<aKQg*nԩ% ˩@FJc.$Ru*4*ZR`pi+s?:HU/KhRH&';؛ƱSyut_E`U f(^G̨g)JPǸ)=EFd`Oyq!t*[ԅ4]%cJ\WasF_n_qN()p~#B=S=G CiL?H>!rJrѹR!eքX3(ω~ ڝQϒǃ◴7$7g.Mƙ{:̦ :ѩ>ir>"KQXV8 el@~zZwinx06@r$j C:o4-Yu~k5PU.juF ?lYuZV>?^@'O i*۬eaąQEZrp/)צvUvuU4|CR-x~Z eŤ j-MRExy{v^Ę;k][&2%ed85VE=cͺt!H*鹦Ut: ]©.ZepxWRJQ*ԅWbDXcoUo[ y)8eoy3/Ĭ>/x/5i\sey'6,ɸO$ &SBaZS.~L2j%AԝhVN;ĭm,6t"<2bq: e\V$Sq'#H"r  |NAx%ⷊ_9f`Ls]:jWUMSbTiiJoSL|}^!=湻DHO3cqxwWC;Sqe9Z+O/rLɫ3 ߋ򊩧YSӅIuJ)pJOgMls6$Qm=O]wLeLz@:r\XV?Λ<χs^$YhITU0 POH7 ~>YX M&h">+ %j% a`?25YUjTzG)HDs㰙bG]$TnTuRB`;ɰ|FN9\:J9N"n7}Fl8I0jmG4}Z@'TêWs6| SXdnH ؋_9>2m &gi ?=?rz._®!IHȮXB21gԮ{`b4?.^xemo~g QKӸԹBlвNvT+.җT?dtK적PGek\͙Ɛ)!ҮXfDƒA1='ߺ89Lw0*7,CZUFgf!TP)~į'*篂UerdC F*q S&Qu[XFcrB(x28&2˸.=m8!E*'}n1}3 iyuF{=%EmCŒe—+)hțztA[Qƀj.=oyzts 5%)&.}qu\uqa@T>x 8iCUT*1zcҸm wz:n#ԍ~ʕn䫗(s(y5tiZR%GJd$OGia qK^@Vy̨?툜nVZ`manvk⏍޵ZA OG0xKx|5&p}U/d]miO1 qŕ+s^O68 ӼkU݃J0Cii2]{W' 2uTnW _[N6)WpRawm|[N;ksnD*K؎#U»*洏݁"53umWU*ܮ]SJT5&Jk3?\̸~7sZЩni9$KH:1g-fѮjB I 6 =. ܹO#޹xÎ!J KL;@tk|R+8<7ĂQ1ZOR8 X-zbztl[F|/pJX}ε: 'kC/ُ 6j8)nl; U:՝jTs*-2xq:|=.$eOWC| l|,0gTZn 51 if_Fk ȤOKznk88=V`MkJKu*A1vJT"{! HpW{_QQVI5nil;XDj _<>_{௎9/NG EdIpU!*lƞg5>S-ӈV-n89'L3P=I'XDM#8zY*P}~^KRUW3*j G>+RbX崮L --p Aڴcml1oI*p%ėA1/ mh泹LX 0%gYO,f[XXDB䏮?];Å F y~ 4|Lҵ)aA*%Ne k*-醣J$'o RbvLUA`:D+QvA78:`B2Jlu!aɘ9d+Q2A#ԏ0=U [* jy$cDsetVDsw M;nZt.4zn<|HP 25ПwUr}D|JQ]f|tACJEE=jPJ%!j IV6ƟfvҤTɂ`/>J[R~<4 _Ndeo9/P^[[q{eNځ4P)RINBGĩ{I ϞɰnIԋ9A +J׊Mx;2pd =PRVڞR9SrK_cfH;N?7ZaW/X~}~$şfK3CiKI|)?fGMp~qXg6S pi*wb|ӵ2\JWFgbyT^hMk% E\yƽo:s!KcmMh~k~+xÌyN)PPT~Uܒ:IRbVur..@3\x8"?.*))(D$a#q͸'A B LR郝nfc=č!h.K~H)(s|qk<6*Mod8*5:*EG)O%@)ԲΨ 7_j8 ۙٓ1ԟ.aVƭOwhsZٶ`im>"J˺sܥ O 1wh?H|=ϣ+0C{G=\>j~Ξ vo +,^4.ի$˒KքsAZBPFǤ}X v)c39Ĺ h'M";Ty&6Ot& kgTGVw2ek4DHcc0Ae*oˏ1{{cioX45Rz@Pێ&ZpO)jN4kmI_m|/ט_s[acGZykOWjkzϡqĖ?^ڔ$^HiG#":7 8Hf)14Yd|Z(VTqmR¡) &Mfo4s"Zi;1K"<8 Tlus[_t/~/~$8/ӏx/瀛ϳTx4˅@l6eI%&@"é +|J׬ZC[ 'I3.8nx&z+̍tssU/Uf\UĵP/VWw1ҪDf BzNbVr)NyVu#zU!3x{4ZZ+)="$<7q>wINCՋTi:jk)P (%PAV7הX]IkNffå1#_E׽kKX {ơ~xMbOyBZQԠ?D6Mjzo |-<ĜUZubiFVlT`yM&v[7mH1EJӥܐ +KԎ$ۜKc}$6˲*O#M*.  ŐxvWQ&AC$I4[.\v[spztYmg30':k!i_SN 6[ J#oB2籜~c<"u槣oHQ/hVҁxjA)*ߦb8)N7LLnܣq!SnG+%W~,G5/NBehăC$qm_]T0ݩ*0e%P@`?A3^ob=<蹚pyίυ*&_{i+7qEoxyms[7u'K3G𪓆rjYM3A.,TbBsqSf >6<댡 > ;mŮiͰYm.: Ӿ[9S)֗7{PE8'Iv!\Etv94H{3ΨVZ%*mT>d)$LDmaÞ2vuEСi un}a`it0 e$,c؋a̭fWyu:A:J`I@čg4ڑA z0k$P;ud`׶#,_tM崾`H"\t:D DĩQpJQ.@?Yœ?3$BpihԅI !2:؟HW(TgI)_-$mT/׸}kQ'(Z Jߋ~<]""\JY*c2`Tӭ() jI@wwE ] V;Qy!ꏈjevhӏuu8*aHH)\iL:wpZ ?2/3ƶ^vLt?i͖Os8- Plkܘ+?%U5?,˪>!r 6MEdAtSR4Iəs&=OCP\"Q? N>UK)Sy&SRHðn ʌ1KwwWKO&83 xC.p³)RO8~J!ΈN6ey̢ՓN?F-{L_<'~\+nji5+>ٱY̩v)xDy|BqoVs?/k- Tchw6,~½Щǫ|>R`b8u_RIfߒlff4h^e=K C̭H[)IX ARVF=^4h mtSw=pR &Nnc)(牙씑U.3s'2g5geԟYK)f0oAT-fTN<4pK.Z9ORN4uXq'oLPW(`6*ߍϋ8_Ēx~ 1OfmNa%TIID01c);ےScI!D~'m{pvq _qqsUfwSSX򄀵>pԘ$c oL? ݯh vRM֊k@kHS3ÉZzps,ڔDtQ`vݺdJvtMs:l`uZ9_^U?|KfUw%RDuLIho.™AZV5GRΛ԰8 z* wLn6I+}#))% -@`= (:'pyY%qY"_y GO2:{Mov:7,fΚe|F p?.#~..'ST 7>N/)Zw=WWQ'Ɨ2q7x&xѳ5gy骬M+JZdFu(R!". t"~ _kٶ-\YT0N :SIW HJ̉H~~ZiS1N5>N3ӲI$ꑿYGY39쇮@eTB*RuQ uu<0VugpjfOg5nj >JҤNm'y>&i5͒#qnU;=i 5/-(Ⱥ.h <$i:nftp8g& 2-_ї4;'Dod&'5/ԧS5H&FlNmdl^(̿2FI_@Ą"`>nVOPNE sq~P@CWC駄uR!Ci:PL8)I$\_80gO]P:8Qj<3q&d)_7jo$N÷اC\'N^\ ΰ`ѣC#9g>2Oa[LK[Q$`~**Tpz&J85Yؔdt_S4(I>EQ}L6p%qpbc7'Ubv}1,cN¨JB"M'*44M '`3}LQŇ'bGU(8I 쇂VJp:_,@Vޓ'8'LQU Sut1Sj  p%u] ?{,}aS!4S%S-j"Q7?\@P2*UbH6=iAl(4V 4 L/ImC <'4Gc'RMA3A]0-a+kkɶHB\'HHO>qy䀈mI8HR_%KYH[D+Ś*iR{?ZI&G=FM}cW 2-hoZ x̪g#rW%76?cs.}Xu[]SLhsJezAJT2zvz^~Yu7FDcIFL>T,@&6 amd͔N5F`֗5jR^1joq53[,n6PH"'m퉝OHQ,׍Ydbi ,ؕo>:dsO,]5[JRUP|sm ^HIZRzŵTTSE`kJ9:%"7n=5ir%y %C":b\> QI HAQӼt5fPZI"gW&R8upXu+H;l;@#])Yx@:.m`KtŦӝ)>KLz_nMlAIH!3"u &JDl8Ӭ"f֓\4 {J_+!YF`Zm0gl}q IfkBZ #q)Y: qX#kTv!sS*ngR'l|U6Ưٱx7+ʆ@*ԓrk qJQA>rHoXIx;B!lTj2fݽ}(JxEejB+`9^R-LeHKn#3WUG@ Fȁ펠h:a zR)Ө cvZ QTXU<) !%@$KȀ %l@}u9* PIli|U:R,˨\ =H[QTS)RF @[_P3r-v~_3+K\RJX8Y%NuRS_\#BϐϢS.R'4JfwKˊd%taKRI*& A S]J$䯆ZNFR@m7`dӒjutQTTp톆CEx|miH`I< W*h6G)P<֑/%(JAN20BZyG9~?ASl:jRJaGP$O:u MiSL!nGBM&l4:L&a.Ǖy$A7/sdhm)$!KI DON2 %|^.R`JAF=0 K*-%^MW#| nSKNt&@r夞t.%XtuF,m8ؘJ3o1+,qS1+o/%""b ;~ة^ VerT$ $l`oma3NۦKZJ7k W L12's=&#D&SSbHukwzJs[KJPPRIb}zb|ʉ8*@})"6="?vO2ZmAҭ0N6=m;!iNXJBVZX)Nۢ~w%zO _0eB@R!i$ 1i:=SV0 ^]R |H 0{A=A)µ%Dp:%;6"g킼=S;=Xu ^a=c8{ UMSv"RQ"ȷjXSS@R*h\MBb ݨyz3šQN%@Y)o܏24$-.f3 6B٧K,P!3 X~rW4옐⎂҄w~bB ug@V"pBW t$G ]VKIL@'^^S:\ITTP$+p7YH^ ]u.O%WҹW<[ҢFɼAb'Mu% 3U~zdSք*!I+.Glse0#Ǥ[گ5C产lӡ)STY&78.jqum] ̖??5͎% D"-Ah#+\}OjjKi6$lO퇗y' #Q !1!!."&b":63uĸ,I$(-  6)^ y*͘$*"?]pXӝ몵fw I MGU-HTRv dxqO?<98 ӪhJc=a#ը)Z~k:*Kjt\j-{_!6 -%UlY3q}p&ʙ`]4wu*qajAQSkI#Q wos_8%|E$h))'@>t8wBza.RLz===q")P$6 8 ?mS6d :H$l{mL!)Z (`J-  j6UKu @eImM}+kugP|3haaD V(l nhJm}I. Wg\OCm0MFXO%O]$=h b?{I';szHu:J*ѠԆAap 0N' Bq"4S,Ȣ+[)Ž #{yJA-M2jK*9Z>7Uq[^Uh(M+H@T HƞT9m>*-r*[`6N\,US1n$aa 5:ʜ, Ԥs7!9Ry&u#oX™ԅ/V}D}!e"Ba (* Gq7JmlnLz`Br]­B*Rs bS;~He%/lzfUѸ]Ѕ! ) ݱҺ.}u]ՌÇU*epTS6K#y#kwԺLy❫:sUAh %:jI#P7֍ILUd-Iҕ)WT>V jMrO8R[ (7~T;28iTP p@h}%J#Z`Rqpdtc0쑪vrGtW]+{w*)BĥZm1vc|֦ .9CqwᚥH"RӰ6'g;SOm\ #bM˿\*R6XR+meJR$O@v"ͱ 1  GX:-ԥ,!R":XwcxР TfUe YuB}}=paI[|Ms]:UR "RST4 f XNBABbBHo*Lh)^s R^L,fAYLaTpujRP攥Z`o=bO8ABVJZ$+NROoߩ8Ї)!% M.${޽+85) +rzڿ0t-h+TVePS,Ӱ|.A&/%0t(.d򔄬>$(ScpD@Ꝯm:iy WIb! + 6mb7ӇQ" "S5N@- LHz6v`:vQt<j:l.AMPێ6:DaI>X/`x7=p&)QKET|۩IRɕrS*{8C! 6ԅ/XI lnmGYp$!,(HC{# @P!B@"GU6B~mAAk *+Llzv#BplKZ  (qS oI}(@ԥ "dGI_h9ec"֧5^$دYmPöʂ7<@+$Bw7#J?3Ivttnbn_s:M\]cRlAqs/>BHq œ$깱=w Z.BSax?S%ғTI@o=wm4TBH"&~fRB@JS{HK-Ka%ZnɓI=l.S m2dB){R$,'{w&_ȢKDx]S<$Ymld 'srG<"v?4EV %ZD%w6V@6 NC h@P7$ e"KJTάh % |rV'[xWԪ=$)t%Ǘh2zcXM@ԥRj"L Orz  \Ӌ)ZLi2;v۶"i!F ʓ.N#P>bL"cUvԡ)Z僤A=b?K`h)JrhHbUo@l%$ nc-BIkή |VN!P}6T0^`([NR2Rji$Ȑ aEtOp_|>qThg4ʸ&]!k.4IJEgXJqM:BDP) +:# s +a* րI=nAlP'BҴEJѹO̯1HP/A-RT6'pt#D&ʅHzTR䨕HHaӤ/B\v(@KJDk %F﹃q$N N`Jv?cPoKJ<ļb&ؒ!"QPC>Sp7LXG$ !5!Jf Q=ol>СHYsO-5*zW.ᲔW4 z :)% K8>r@ߧWSlJZҖTca*1:-kz⦙me2BX$Z,N ΞwEyUs[̤( q5|Q@c3R&Ht=cX{6̱{mJcxT'a-RsJX#mq潂 2tW8́U\A qECUऐDx=ҙ e+QڵcZ) 1 Z&mjDT攩DXp`.n) j'H = ĬN!ϑkXu %$QMl6 Hҹ^(q-Ki%7 Zt^xcjO|KemG3n53y'P^TM>)Sm.\y.3CPԅZd(oҧ':IKH'a;n0!5:VB|Idx5:Z T?4b!?G}VgRȶmԂHxI=Ԭe#PaԑW{V5Hizi_b:sa҂!iTkJT/?IATNpkGQZ꒖f76"[E %zj h}C~¯ۤ{3o7̹0C*qЄ&U;bg t˺HiWUxi֡T5xt#MI ^Rw:JAMnӍ&|5pGC%[3V-Od"/@y><+kO-8U#|wRK:%'6^"9$*+F[; oJSEyQ0▝:uZaG$Nu Iu:VNJ1(;}d^}qT"RS.8H#P}$O\[Bҩ!i.Ze"SO^m(ր9aɞGlޤajjVTsXtBAš-%-iQ&R$p$K )rF$%_,t' YYR]V2&6TiL+'D-]e?T$?}@E 1(ul2:7=oŊbGԎ4CI1;h)TmxL߼L5ly jӦMc KVS)jY%™=ۑ3+46"ťJW%/@ "OCj3Y(w QN2~/SF"3%j@BJNp kP#ӡg)ye3LjyP[AZPAY%B`O(kDނ[(Q jJfk}PAB䎷76  ΖѭIZI]Dxߵ!m-ҧYQ  (pM%K%Rԭ@j+=7Y|Un¾4r!_9V 4{{m}qv/j)w2Oԭ2樕Կp~i+۬ւ!r:hkiH XZzGOaM78[Z)HOo0z>DHaKoTSOp=?[$!I.3RD:{o&LXzW|;+K*&h1G.F24sV\I]vM<>__yYJuҝk j$t>.zAV=ƴj$cl#HD͍)Uŝlj%2>Q{p! <|*Sho3 krs ڳ2hVT<V$xc6#LXEyd']P B$\؃a= .)kXNilvz$hH ;o6$$ &48:1##>˘USI^F c/(62HpWM ڂ!u~5ڞ O$򪜻 >\d+X0tz"OT6:A= ӾkQ4F.1a R()%A쫟Bd_v'+GqT_*t8L\q(Vhۃrפ,E:(0zEtsUu@ eҕ!`oc'$T+JKmIRFuH7#*B9x4">"#DtBihԗBe2H[D nܗ9!%)60̋݌@d#E!JsJPH%Ghmc| *SԠ: r 168TSzAZ m1ouk +AuԭƂ|XI$wր&ؕ!r0T 'm#i{ZhM(P5'܉ lvc(>$& LҪwOȩHOh=0UNߙ!N$㮨R6icQ\%Wq ooLCNG5t(q}lG>)OST゚ʔV"VDp'FH& E0"@R0g5Fvt-R+=)w8q*Unͧ0'ub<1yG lR/.xjgVH:ZmrRc׮; Td..ܨ$9T HRUUO\JjJ7Sh{GK7=$m~ܕ›BLA2wa IH\htxapcqGu_vbk[ZJeV!++KN9{>v!:Ђ7#]׋c&!SS+\ܕCZ\wSB Ni$QpSx x_@]^gxj5 *Zu<W7Z9eTcO "dzk=oB}RݯsZu@o*!fyx' <5\Cq&YWӲ+*dPҞVRu6srLѡ]..,7xʬsIԲ8:<40W6xe5KƼ,P᫨]JTm *&w<*!}Y[\GZ歩FP[o-OĕMRnOEQJu`+Z"&bk|z0o06XlQeҟ!$OǩopvR([iP?1XvT'WT73p~rkv_0G{߶LBӁ|p ejZp*=돝S=0ޫ>)tdeb@ &[k*W"gfMD1K^HwUA pF` lg8ç9H~~jsi-jm J;'5-=/k Ee gbF%m ;+x -r-;QD1 #|#:eՏø'R,Noॾ Jvy4D%@&] RwN*]㜵$ (Gh?<gnC[pT|-uWOPl$ ب9JJ&R΢!H=#i{ l  PCp*L@ĥ$ gBж "~LZ4yhlj, dN978-,6%*)M͈Qm,L!7(Fi'Ł-*JKhF` vQ(Bm[ IB S>{XAAj"=:,R.PP&Lto0~7: - S*d,c@'`/6  5hCSP668Pvq-tuHlDwŗjPZ( E<o Qm3]!^x?韂r~sUZrrQu@c>yn k]+-syIifT`j zM>ֆظIQL-jq% vWnPHG1!i$ :_-7)_ZGǶރ&iVԗ^ry IZ6BB%o DjÐ-’mԑޖ#k%Z[̸b96QnI *Ht}N\YKnS@:d9")3n7HT"L8Hh;)Zg @qeSxoĚ/CÊJn8ί5meFLAlxWqsU[oD3йŭ#W.as (eĄd` $FèǩG+AYu,-I: 2a46o⒅ԅPGmmSq)C䮑^Ed*OF !jگRZ@mD)F}Jw'\6a U )Z 66vZd:PP—d m2vXZj[ [7$LMͿ c.6 Z H"ocd`ѶhqLHT?BHQE >VA=M퉍AZ&[a'R .j[{3kCA sPNI/2*Juߦ{Ii퐭n U; t$ H27$75'Z6ni[2U "A$\oc]є WSAR6ҁh]8kEqeyn+C`RV&y9A'uw4j>#\{6BZa)i`H:L}% &C9 [^v5@yi%*JC:==/펕yz&-ZHKO~z`+yr:0ʸW7QR] ]w1 AQvCF-l>Ryw,SO;q"_wm%J[Q2v6=qqTns\9&:0b;f>.>UPЮ9{ZH!m*:\ LA|*}'pdo?|OXk)uV,A k=WďK4 Ӳl$z"p$<1)KAEC2}~-Gk` |ԐZuܵ9OH$ Aa=ÉĔHr ccx"4l5;%:Rv)"`c8K:%{ M͹9ScV(Hrؙ$z eFߢlJ7.'AVIWyEn;kΖ d%r谎6+6(J"M\BVMk}n t76䄸˜8t% W;L{~HwD|(gacJIM`$[wӰw׌'Ի]?hѧ:4}?yuJlFDHߧ>q+h+K )B`-+ ,HSFq]LgJt:PT7&}!\h++QLYfNP!:e|?BF[ҰI) ؉ %I;)th"Zj󀴸|^OCZ iQ~q}Uv_'B1%:Aкq6j-WO]FWKSEP) )P >زڒl6KT=B5Jm&-q:2;!r)iRBNp@ @2 pkYo9I*eKw*=/-jSO$טJ[s6 2DTUҶQPEI&\wտa$LQ殟^<ľjWIOôI ;Q R(myc5jx}\RWyg|>!kb/6v`ykUf~mJ( B "eG̈UzJr+=u&b(ݫSM6 !Is8h5 4. D$_%E3 )%)1r-~~! 8s 14!$]̨2aKdgsp6EANod"XȎ!ܐ~ZMVPHIgxxvyXr6i:W p\فߊuIAo҄Bd:Dc̛{G vi-ՖT) R|@  nE5e: $tL3?Na KmEDk4']1%)X#}t&Pus|ҦA#m*^àSgN.(s+y-!wQO-:č]A=k®ڠ(ʆLLn#e^j3}q,9k*sŬyB,P"f==^/w^_C  \@% Tb@`E}t_.es™99+uuc+m:ec>tBχugTPst&V&aX`7=o)LW @0N*m >vNJ:zumTyJ QONxkP!J1\{>`ְ輒/$ruj+%f@$|Yq }7IEjCHZRwQ0SiE6V@E4*!e*.,\*xM-SR R0/Mh'QӍx*FiH; I}ͅzvǧ"{G]횖xì;n.z!0HְT>۾V_+{UkF/73z[ D WL;I=LZl @&?tanl" RSkM2{. a0$܉A=z`B'3a nIkK*:ӽѲ"[ zBs}`p ˈK"T@&nv t͠<4%EJH16V2< ѡ܍*4mc;uBj4tjtW2Btzɉ6v WQժI$8 ~{B-IRmz%I )`4#_C^V˅I) 1`Ip9b@Sk+Ԁdt߮`G`&< h5iikeNSl1;bB:Ҧrbt#h\+I,SB?v;a*Bsʶ4VDB~Kc֠NnZ>NE1iIZʤBGv<7]%oduy:m!!R$TJ;_E{YČ|9ON7fL^:ۈ[*%iôzM7 &;ZM ͸W2uAik16>oЯ1-PZJ<ښRL L77۷x%|!R4MoS:Hе+?@JPIO7i7&tͱ2p!4zc[;5^4T-|ʊ,L RII&c6=D;wO7iKn]<\O] }#RAN'd0d=QdZR@$IJA&u=cLK<*KRj1&8j (Rt$.$o]_-%5E%Bs+I7 Xb7ζJf%Uς,QSCT4&Ry.|L [qFÕ⍏U_  j9ʁBO&FLZ$;th BfӺ-5,M;e feEFm+A)37aiL NbƏ(6̲ї[oQwT.H1x_ #Ns_5 m%aydJP {bPzOT2TBT2Sk>zF<Ɗj5 >*G8N+ β~hϯ~] ]4R|u",U%1˳jҖY[^Holq ½٫3@Yuѡ foal/n6@v_AK-HQLu,Zj*v,8 *An['{qt_񝓬Ӥ6qIPZʳd)iC\PA Q{x.Ej fLAYsHoeJ >] iT0hro(v+Ë `L4~4.Pdd\ D** *IOTړʩI8r;*܆{72̥:eQZ $a&eki*xpf<='T4JXm"%I#RӰf@" [$h23 +s$0}lBP@ =h_iu]vXS\w3\[nJG鱈-q4 Be%E!)Nƪ:qU|%F/i&t?&UT},PJ1B}pDڵG1Te0 oԟYr3ihu&$ &I>n osGmf*g>^m㟈ws$|6@#HcVmM`S+>uws%s>[I䤓側{w'BJjhQREm T(69PcEp! 2@/U"t-W$(AF&~c.E`Q!)j%VBRPd!Q1/~*H u)*Q-* @NvZ웰.'Ux3"BUDe (G8߽,ϚJDPiUe-U3Iq6VzL^nM\7ڵ7ę]DVޭ^i"Ao\hVLJ~#[49*2e٫K4t%)%@LlA+\;(Wnje3tj9oʼ>͸* QRvѵ+@ػsV1&.z6xmJMgo2'R|:ſGWր eSWN(׭V j4i'uZٚXFUCg;V5nO_ ҩ $9t)Wn ðkik'QWR%]=HCMl63zƙ2Z ht/mT*KiX05ok֧]99(jjO-Z,daZn*\.-ʹuh\((N9`TҩrR2=`mft%٪k /s* 6XDut¹3{)WA 7:`jP,›V$]PP?w k[- Q zoi8 :}Q IոI'mWdBcS6?~O4j5Bp6aL!S:J oIÁYKH! H X( 7bA2:-i"-BPu",R%Pg=-F[6^˫{.@$))Uʧ,~(MT$ ߯407ZQ )E [<2 0`7@$SR*6)&Dob5[^/V<ܚN[N(YHZK2BŢaM}^ YU OP縛$EMMNmӔ$Uj{$JwM7;)T,)*g_@`AJJ4XГkvE~j BFr;[VYT-5<vNU ː\36.{8tfe7RViTT^-vYI;>s ̨J {;ma8JajI r4@G>Q NriJX /&L q @9l.7Ob=@KմkC gN ʁY\7\,+BVBUrIP{b U2Km"P%=6\$kRR5%J"iOMf.uȕGx:46UP-"v`lF7jlApUQ<oL5d:~_9)*a*MwD$y \<2'QQg5PDCNO:SU׏0>;\Bݼ9w:..:s D"n}1] N+zs$^|6z4zJIP9QT.#qCFK _ﰺ 4|*?L9M+;Q:qܥM%@$ ڦDXo\[դiqQk !èUd 6q" J3oޘ}rJO0ѺXIYDN3xt(46z ڨVOtĹbjsHVgR:c{LL74۰7B,7[DIRP*EݮvZfe =xŧYVj|6=a$J2e {/yV( b(BYzSm5}(JH>kHzM.5-L톱Z=DkӸ!_lz1ꢨB!Lk)- GOKtaA+,n'B囑}E*T[Z4E@ "'R岋 .$۾#\*\4jt+ozh'~K5?UMSHRBgy4'UrSNuչ-HLsqZO4ڴӪiiq;|ɀ@R3ڐ^&WV3'Z„}@-:-LTFPzrnTS.@Ae,{_|jR'lsMM_RPaSP" L>g4@s#* ⒴ˊ֛d\7N*%:tR#RsLJ!@4 %d{oB6(RaB D&$v>h1S4@ ÃaF91`!ɫbe\RԎu;TRJہRnƭi*c%+|iAZTd3#QT;Z)N:lAУGXËZdsEJ9- (7+Txԃs>\lp괰2NuBe@fɍi|t7;oVޯOWfe*YQ:PS7 M束S:6_ij1a͍Q4U#& ,]$5DwiLs4[vBۏV!ty lF[¤D& :{:,Vxz0UY85'p.-& v[&m!=TdDmnXJoZAD[J\-ӧ1$HLJA*u'@i"L@v鵻d&h(Z`$I܏K{cMږ`w|?`B^[TH=dXHJ63xXiAϾ$s$&kT') X>}oS VytA A;L TWTH(K pč_qL֦Q j GA0yk=T[tTT&m#͍80nD(VcSRR)m!)vY̢2eFˌ7QQFJO粆+  kLb*@Y~xT8U)Aw*CKNh!jn+i7GU, +F,iQLm*W:;6O\sJ-szs0F_R{3$sQ57f{/i uM7#c⛚ЯVǻ&{RKADyn-PyN)ܠ֘M#mt8l;]w_)c8 k* sPPKK A`:M͆ݱH$kZ]QQ#?zKky*% dGX' l'+]MUBl-2AH (+]67V~s)1IzGr\8Fe攈!=dǀvEŪ]I?;B&d }J0B/~&?-w]%[թi#JgH7TIŢD)ٺԸu y#-WΕ@s msHuPq+` _I>aDsBVx'3T > t!:M'#<߻<}EOt>>k"q^gii!StIl; }0h%ĂC,)Zcr:aP QPBI1Hn_!ly:]H H0"t= '-)i cR*{u_`Bp=:sPl0 .qs%GVhU)|0+*ʤ!N ,Fʟn` UYU7RnO<^\V22➦Rd"a*XRshSkX s9ߢSUTٕAiS6:Fo} A%>r>`fLw\BE$kt:JIJQۦ(T3*j5K]*˚]M)eCi8~%pe;;^ GRZF HARJ2%zL%7/68m#U7\}DmTǜy}ˡJK/B}}'akWst*}d9 ӼkBR ǰqP@ gM L'QcbqcQwn)TYqI(q%"&?k!wZ2 (eB@ya%COcy#kHAQjʛ/ԨI~] Ϧy!!r".*oL~u9U5iqNPR@)H%D(8@^y~A:wū|\8b* ;68Cy7G#7 =7Ӂ G NRTBY#m,(%)W,s򤘒b.-`Bx|(os`A;^zO"ldzX+Q]3D"I$MߧLun:*2b*9H[H^mS:I7H?I>eU=GOUU妡 JV5I *:mqFң\xQ@:PC) &e7LXO}FB%njW|1TT (%i'N @[p<9D3摘pZڛYF* ZP!=n` -$t\%Crڪ~ap l;\~Q#J@))ȀHъU.Y3OU,N%JԀJF= =ݠ*E -/:exŬB+WdNLEy@:).TOB \,f2<,3UJ$`&02vBMnP줨$S"OO€PBQq%zҥ'{vJTqzS_Uۡ^ŌØ(@] 8bGY,=q#$ ;GD)9-A76pẴ ָ \ Ŗ ouÔ(NsAHXLq^+Sr:@*o P|-xhxT:edW<] QEշlymYa֔rDžy֡iR4&DS{F9u[,Y*;0WE pS&<ҒRú[zXiUD\Z1uF2ښ2 [0B%E0%v#17E%'40ײ*[RE 眅l)0|B([5 Y2Y_k}W@R@'~:`U $O6 ZJA> w6nFOVt$OPO[\bfS "P]] Y9`M;iI dFVx[+ըJV- ZĢ$u=MS2CT njJKB~] źt M{kẪ-`\e #O$l+p֞n8~WZ!EHIJ[AK[dl ]]EேO U^-q*VVeYm;9H;~ğW;r|dt_Dvw–-Xiiк`s綪$l6*unA|ܴlI~Xm,h||7UK)nLL\).XBuJf:D $rfu mp'q;ajΤ0iD(qk*^պh[ծmө*RX$iq hWSả; :iUč vkqA@zzl[\?^^Go+guj-\RQWj&*w돭$˝y)JR%E`$ TCn޲i)Y1 ipv6NR\ii!! "rGnwRRPmjZ=& PyN)YťDn5߮M)ef^VuA V'"BP2u;qHHF9IX.n[?ϩγmIy!5n$ v㡡P:[2;<?d6r+}T슊gTR@KPVml -jNM`ur괗J*h3ZO@6m ״FMBxg+Fw U*[N:W"-ԃG7UnZy*t>(TA:I:ncM}\uV"4X +sBۏg1\VO]S$G;c|8:gjiu"UJx*R\q &F337AE*Τ,STiHm` ZbcckfB-xSƴy%NXkBAې%pwl5Z6*:Vi*n:ڒI0=IA$JES$۟)$L|mӨnjsJ8V+OJ 0dZ֏NjUI! RHpNid aPrds=B(Ơ-i!= n6Ԏ\%CжI #H$!mOZZ1钭GϨÐKA*$R XoC qR BK] /e =7Op0!l{->&u@=Hk ,愢yG@y:%Im7m4VO &qv [Pa-ғXIܟ[L .R )Xq5T $LĐA#28s:ϻ{N7nYmTPUSJ[ IfR ϯ\zIna=9/̎-,~)uБ뚝xL5H^KaU(D#l?}7_R;m^Jȸxgs{*w#{U:j;AQʧkR#H"ֵՆF|8LA>ï+IǹhSJ-f# -8tANڂIa |awO/4cW༩Eb*X=A;o{n㺌 ;ZbcWv9Tfqiv'b{\9s4Qм8>e'I_Ivx᭴`yKq?u}{X ?s =U{߆Ÿ6ʢv]]c C-d8Rms.v˛B+`b[[K\܍~C-iOU M tRqOxjӢ4<--87z~I˖&bGp.u\(VAlLR 8-pj++{Ժx-CԵ(>vF҈8E{#*#PjFJxw]+oƢ24;3R&fkcjhTo:(%S:IL>rVnзMkxO`+ZN'Ąy9vy|Wv<51즫1v䭪\> zmJ/ӗrĒS yF<{еaizZ̑BovB=/jT JتDCIF"ZTI "3K9itK<77v8BB`*VԤdHw=WW4fn&9 t'N780t[rTi?vpw+R\r Bvhl[lhtTOqpX-I)gRΩs+ZWkR2 :JU%Q JsõAHSu@tzG0q0qUg~)uI"PƵa]o |J\.*Sx5gS:M{E{ieJn= -)klCIZWޘo#}!P["bhB]@[\ EU o8 zX'{ͺ!n긅A=w(@!dImbgp@## H Z$$ D8%]ޖc u*$iPDA%@b%-S~Yʯ SjqRIJ `Dǔ v͠t]*~}0J[0}mnRq\x`&fEt/u˪]b`H*vU)CmJjPu0QJU)I%[@/3v[!)[I8@ݢk֊j!!H JXHߴ[g1w8RGl8wL:Ω̫]uʚ@m Ԩ $ tm GM5\ӮkONm6Oe=tT\ 4\yظ%Rt6 n` %.6utwqLJ09.[Fֳ2ʑT6" !)=D渎JWe 1SeiW` q%.Q1sS!M?JBTzkQ@JT'X [m*j ŶqTYV##O!l4FZUAITndq |waf|/\el7ҥ&Sh=]'}G0Z9ek_)ਙvguӹpЪo( ӲF HQo\Oڙ`*JK66$*D`}q}X[6[X+ZTҘ"'qX pGm$%m$)A17|@;(i$RQ%@vbGCǸїn` q%Xk.)eP"mZZJ$~;Cژ-w& O %B2S6wD9e .6"1ѯjuD:+Q!:gJtI0T<3ijnUcn6`cN! B!-)hNF"Nŵ"AԄ Q lmi!$~c̛ɀ$[B`*&HH:`BAKRt,\l=#ZR`'p":{`BUKjJy؄I׭[Z̔onO\ZݽdէBn6 H=A`Zu[k儃Tu ]H.RУ$DH؉ %`%0ZI^dm=0!O-KeKR@#o\BY0BQN˭(%M(z;D뇆(N%}Zpa>T?*Tw9OD,՗p[9sR)R|G~q,Np l4j,ri $En펫aΐ%96PBײ(܏~$zb(Mi#p+\8̈́QPvXFψZ[kYX`A lUN!s0 I׾'crJc 4ZІHL$[M1 . 88.+R,8 tyoxgiB٥1g39EJO1b iRڣZAMfĴdUx[U{L'k[k[Y&Y R)H$(60HEyN [3uQ/KY]P8RA$yO:*t-@4[@UFccU][6QLɵ1qLWg]Iqpi0T)c70U ot'Vj .4tp"wa`{c*k$1R,] &6î+q5\Qv8| R`⬰~5)kJ:|gpӫaNHr{SS7jmA^"5ךs(fv>>˩I $$Bzmn#|#H;+fQ &i%8JԄqr&ۋ\Xa96d/Q P ɰ=רj MGߦBjJڈեE1d݉RP(@YTȈ00!h*T*cրgP)R򝾝48 $a'}[ϙJ!:%6&`[a 3pPy2 Em`B PVJ"Ѹ*{`B-N(8V8h @%"l45@ :@ RV"",z 0!K1T4BgLq;-"ir+Re'ۧrD"h T"+8~i!: R_?p!9xPZR[ym!4q$.z'e:jw [RB#3z{ "׆\#1q4! }d*ӿBgckBX!G[^*4v0}=qa=[^ @ǜ恫I(bM&׳*G^w+PqG[QWEs$/b@x (WVi*=+a.}=1]ñ|媘c+z~TyDaqb6G nG~iU4%ITos߿=@"IXPqU)uKI)J@ d[$c  hBЅ%a‚>]12D[틬"&V )Tj&pm% EYZ JBLb c %Yq LuĈC_̙ @$NmV,-ƢH)k[p!|Z ԠL R6 EimJZzgZe:AeJǿ[,6ˀ`1"=Dw +[ZI h*$ߥ Zv6cWDh=wlGHKj()(D#j $rD\HP,o?徶+, hmK;b#%WHhBTu4Bn (㏫b3Ss]Y";2 K i BAE/퍜G|P5/5PbjT@mîFJO:0Nˆ{Jvfma'Rm`8$aroc5?@J]H)p%IL I1<4QBmG O m"0fNks;^JR<MJYҨ$hOnߦ8+w58̪NJTyFIxO\EV81"UMGyQ:BԂ$NG[l9N؝Тu"@0O72%*JOQo\!B<ƣ3VRh u_S`'16n5xޤըFQ\.R[p~X$^䋈s\ RRDBRJV`iN.9 c.`JRA>-֤K~l[1NO@ ؈!fQR9UTBS,>eԤbBh~?s Pd i% ZUԶGYH|6*(n!6㟯olڡS PgYrC'2K(뎲AuKeW07=EKzU tr6Q;թ̞Je(iya JE_N{u?\*_ ^R8iw4-Afܵ܃֙kl핊ORdqSQTEh 0Gr;bj %Ã,@/-h&"{>ݕ*7?z 1)|RT|@=<~"YT@$BmL*A*D Q:yFֈ%q)*Z&&v p(ZCnj]>JfR m=p-R./FS&I& !t!78d$I *Km::ĐO $q|/xPH cPQ-2Bf\A 7ZY:g$ߡ)-OE'c v^Pb @#`Bٕjialwm`C(I{Db/{HJ)!*RAJu))v18B4Buy""܀}7Wh-p#uw-fa5js\#m6$q߹fFteZNbuR'hXR@mb,`EQȮY6}3[>vA"tzЋ懴M78j4S\6s2sBLD(tMYUI:)ŭn)]gБo51PduҤd֎pѢ}2y*Ғ Ҧ)q6`1 Nj=xd%T5*l$2fd y ߤ w0ЕPmʉl*ұE7=-IR& T97%+`FNF]u\渹E ϡC:Cl8( O$$'P[ Bz h{'ƩjET(4)#coNoDhiT0ZTg\J): %uIe?LkJn@Aws2nX D-.%*Jt|&z+獊9z\Hma /vVt>עѻppc蘳J* ũPn X$^{]Tt2 I3Lj'%R)Sm[nk.֐dkLnwIJړ%A{GKO!5JPFvn$BI(Z8I$(Ow[cBJR Mʢln$|ߦe ԙ$Z߮\T5Ǜq{m  P nqm_ĕ!ˈ0?m0{`B!› t)⠫]Cn8-JEU&N1 VjRB)XJJq7Qcp])@an-iI&pH/=qW]NywP ˉ30mCkNqi[2>NiT=TκRۏKjP;Mc´[\>1P:Lݖi3zDڒH6ick,%ɸ=mO)R1`wb: sMsLiL,MTV icwL+]L1eƪ6?1Z}[ۑ=O {eAFbj~̚bxB4dѤq? !O $ʈ?*v3cWR`lDC #֓A6P~~XlSR < Ymy/gͺ-??8_q ]-I*e B@NT!2T i&#zN-2%Z],,Lo:P% TJ W11;LBR*Zk)HsױE&.:e&0@2EuzDy oIҞ$>coO-1im)a{}#i:aR@?7$!MڐJHUr.O8ps$ :dǽJ5 :UKq lwL.r~PNlw;ȶM)TB-AR #дIXz^ĸB CdGAԏ[ Zmi{T/\T%7 zbB)X+qM-}#9v5ŧUכ.t(zGyJ*7(W`9A|>*ZsZ>~˙fbU$)JhN@¦.Rڋ6hW)|=g깗c+#I(ۯ^ю9N<3KϦ}Ɛ•BZf'hUO$DlHY{{vJ\1<\x^ČBĩN cQfWhWvNϑ.hJV&,@2{ teQˣ74 J^XiN&2w%2 }rBT_昼N"2"LLz_ҔsL⹵ A8k y i1'S> M6[qf8ܔ?02⃇ʔ!I,G\0V "RHsSA%lґW=f7I' M*]$ -zYGJRNHDh70I1GCm)$)'d=0s$'Q *Qw?惹BeNF.D 8 2[e6Jt ;[ : ҠBl#xz\ﴍz!Q DT$PHIu(/ͤ6@yB$1 31h|IuVJA 'II$=l !,թe:%@\Gi34R@:7&&: D%ǚ( ~oԋ!_Y=5~oHqFUNR"{O9KO]fcZS鲕QTj_2:`LiQe C ɯn$VgVǵh%UUَV3"fV, X _: H\"UJKi H*^c=mB LG<C;JQ?u+aBq) C7BikR C검b@Y#aZG*@HSD^U=L{O 5)E>ၯ*3haI0۠P%t8CDNrHĊf !hp$ ~rY\C-AV$&Xg n^PUƔr\Q$ y iido`R|V%tkT=:S/JQe:)RClv9/V< M!()(zOCq Iqłu)@-[ Բ۬8BF2 FMFB-+J $0i Aj"BH6z.i\%KQJ* B{}wKyTR#UPy̏An{48-%hKe&N{u=G  4 NIR#Kl;Yq+A*Ҵ7O&*l"s 6 ,Y xZ-.;OHCyvjoa'm:50GYFB閽P/:Z7 :U,@h =MWW:+ CHiR ~JŎ?ّT%79 奠 Q(l78.Uԛ/7R5JW=D߮m*i+*X`:ʐU#pO_9T4 .S7fF{D}`R,e l%PIUѽB NBd6m`BHi);F 2o qI)J \lGiPR֝k"33|!BY)u4Li*i$T$*ޕ<=!'TJImI=OJ Mx 58 sZզlo*l\)T%=5u-E05~$.@=&o`B!8-S 1&L0hWHd*)ti$6!h@,HA>m2 JS5ڜG3dw'oC{D+i0/C)IXWl@=Lh$9)`[RrtH-{6ժVVj!]IRˁ?,i㭙Y yP!5.E@OP0&n JG#p+)>B, ϦPAʯ$^g̎ /puRRnjLtFRP,zn21JZL)iR nqiIB 9A G۶5m!JRF Qu¥Dwy )P؋-"pkRM^hB&tR4,J^kH$B iNS  ؋[ӉlHL(T?:=Mo+tM"PKilOJ&wO6ĴەTVl)]Nl(PRlR!:N!k2櫒֋w7#FPK./ʕZ6$ ߮Тy jytICgJlSqlKB[O&+BAbUcIABVuGG%pb*Z&HH95*0 :|?N)uR>fe D}:FS})2T-ihk+BФIi2jjjkZ~/eR++t;EPއI!&zSZN2U]ZE JǡLoU*CRZObF/ B,i @'zZ)l΁77ai-T,Ɣ$BSaw}0!BQoI\kPpHi &w >;`4JI YczK dH)="$z{`Bj$ $]JQWp@`.EQ ^[P`nF'n\K0!m)hfD'go`BiJT9l:RTI>LuRR*|6}v *36XSM'JaiIokZͫ~.uÒڋ%%I:Hny޽Sm7_J$$@VVq)(]&JgmaP#N=HH\\^uj0G'>fB%MUR[m Lj0xM)*iHCrLAQ6_a00CvAY\hC#.8 'v-}юUi&UUQƹEI-XD̘X`rYJ!kt urg߶EqZhyzo>H*ΤO {|6eSd$zە. ٪TRxrFcP `㢵]>MꙂR4) *$ذJCE !!),A?S ,T*@B`B@>PiҨӪIb}"U8?1$ TI=`B9QRHHHJT _J4Pp4IyӤ`B4--nJfL xq;b XZPe:%s)Q Y";vq')aQS)Jy@ dȷؐ[(A<]t:DZA Th+H{za@kPdTcQXP+i))P%DH62O_m BBXi|ahIcc'! ( eSeTI#bSPBk'zVuJSb?t<+U r]R<R6+ܽ );9Xj4>lob1.+=WhCJC2t6dͽ=N5IR/-:R 5[2O\L&8t)m:ҒgTA%Gk3틍2)< *%Dɱ¡(\KBXi&4D )q*'Q$B{M-|CUz-JNCi)PB1{㳐[e ;HJ$(:->zbT%T)X&ARe fm=:4`-@*%@ܓBBc-pa*o'a%PuV@j:p!($//\H",S"OB##Ao-pHICWMNlғR[i( t6c$oG&ZFqZǖ2g2gk6b 6Ä dl7q#3 ^aRTbߡSD4"PS$P Ɂ$wЮnS%ġ0!RTzo\$jZ6ZKFHLiĈ66NP ᳬy7U< ^1 z~jLUF!h YuBnqԪ 'Z ӤÄ$+ EJ T dv1)KQXEN.b^!)եHL&Aը.86tX*?Z*k:ǂJIV8XAU*qM ۥ!RVeFGʨtbbݵ SKi.6VU*)z.ZRyA[%V`,0!֝Z԰py޶MR쁢L Bz({mǗH'tզb6P)ͩR)VB;&޸B@ SKL+iYV"bf'7.5ZOMruęUb7eQ)7Tt;(:y XF:B|Hm%)*.)F0Iou ZNUAT'eH8Rg M (CkN$|c>#D$NphC+mȀ`{a%l2s:yNo){lq+W2W\}=f^ l(j&̴kNq (IJ@RW`ǧB(M6N@$)mhPQ $J5nzX`BU Hh-Ap=[(ݐJ 0zl7Djl1V#isCe@@AQm#?K\դB '}# o,a!DRT ۶%ݼԕv){-e5`-'@I:Bv=qV Bn"γ&B/+JGjycf9lAz2 Wr5=Hm d7P2uj1c2vo\D㪁R5$ y#~e7@ښJ I'?փ'x0p]: А`I I?9 V֪^h3`DL mPaO2\SP ҁ:QJ^$DrDhzHM] VCʛ&ҟ 5^慉PM(IPn O=c,}8G ^#,*)]V~SV|uSkzTN*PH*F&#xkO&HBytY@+.NH&GdbO. RʀZT`$i|Ҥ`[̓{^!5LB) lM7}>+We3=m:BjX ОBְ%6I!s.u pR$@ ;Iz@[np0bF/CĪ)s(%SAJRf W&زwTS VRTAATŽ׶uP%R݅$i$ԣmA 0BEJyRD26TN"PYCPQ(֒n dϢN: )j*KaA JNl&݅ЪdWf )ұᛓ/R30QZ_EZzJNd b&G$Tޛ.X2r(!)uO@*f& t>Pr .5!3p}gmCB $^lG!ӭOZU&X<6ضѦ7I].?5JL 1$c=~r7T⿚ԥJhs)Qo;c fZG+DjN BBM7j&*eRZQ6R#k7~vB|v@:ݧqB('1 +Ks-US mjU!+"dHѵY([+,V &g wJ -yaą)) 06BAgXMS币y0p(Xnոt!iZ z~1 [ed]C4S,%@/" z{:v3h)n-֢DⓀ.eldG%ϼ\OAAna:w$>Yu} +[Jd6i%p JnSa*\--B @ ?]UwHt3J}%:RlyNߚa)rب+JU*n ͣO(J[9S=g&+drk$[3W] *mJ2n&c9TY֕S* )7ȎJ:u$hMz8ZB*R*/1 ~Bs~0Ե""N%RF{4\>C ':v *5yaV4y( 6 ;fK8![԰Q:l}ؑ5CH!)$iAzuL`B5:@*}A.h`)~c&v3o&YuĆVtz 7_LScMkZ'D<5 њ*W\QRЦj*Cyк-*W"M~lst$]a#N8ޅҎît[l<0jX+.Є 4*=cSy l_ac.( DDM3BV&ʖE-ԧ{q4V"4(mD%u wLHDGVHRВBʝ~D!nj!eN0Fc;|G ΨJ[ÚRir) u^ њ53rSUo<#r'Jf{mCZyjJBP']^*Ϙb.&B\u*[*QIQ`{z0RөB (RI8p 4s.Ji)I 7=#vM-H)>u;e1 6`I?|+)p!jR?R"߽h5TeIT ڊ$$jDAb@7ːrUͽC #h &T>wf4rκ;hyA*#{LBT`t!2 - .`} Ġ3u*B4 )KN} kRդ( DF;SK|`D"㧡 kuB*dONt'Z p#La~ÚDR]2qm$i Z cx:Ex:Z.?2) Dz"ֿcnl*7OI IBNE73Hc^ʯ++ZYA>iBvPm~W n i\db q Đ-#cUA:.;_HĄLJӪHX(%-."T|{ Ky^:*-. $G O;mѪfUv|4 o`Seq@ R v  $Ӿ`n>8 3(iiKk,,s??}VP 4 T)F߭lwpKJB\ P s$#M{3%KH{3& ~`BФ  SAI5y">!BԴ*(9hPO: ;%`yTD ?VAt-u~zB)#X':aM8N8Ĵ!*!?]c3ougmZdJ&;é ʳ 3{E mΊ Qm9JRBP1sEo8m6szIh)g07$7*h wt:mWʕXȴl\k¢ZB*qRddN6%IlkPn LsxBQh 6֔ͫTm0v;׾!Cg(JeO)8L6hOTΚ!B_wTdĝW1o&JI[JB΢`ۭѱġ2:](Y.a~3L/")JP*d鄞hJ%mBPBBT* bNz SBF&ډF$GlJ(L.oB*NpBY>e/Hg1}BW 1~v*T kVHJwzٲD%C2<.'Β"ql:!twV)~"q>T) Ե;ґN5map:| {LYa$ )NN$@;o7#L)Y^m^0`+[pbl#QH8uJJh+,7s^J|6nu5 ydCrT(tILooNFgr_3njGIF++qJJ[A&>qDzE5n!) ]U&z@\mظasCBVQ TQWZ}uWWFIk/ FrP=`'(Z*mmHQT 7r$j}¤2R @ *y-6hTUUZ:Ro&A>bw1q8t!Sjoah2R7 IL'i뎁TyIR 3 q暙 I-j)6:\;!!lZJBTb#pժ.TDDIlm$'TBDeImL!j ܝsZ8[ -*u{P~DoRщX: )HRW)O`nIfZw$`zA"tB@kfjبnWKLLݱ#bm082tAaUIH.6 1a1 biɲPe~Zt?}YNJJ2 Tthš(i;'>ةxSa\L;[ V>рγ&K@HyIOy26í\?j&nmII+>_';)Pkԁd!U#DAI)@ ߨ0"5evAxLeu KN$eD#  k!c^&a#Pt)$vas?Q^K FO-:RAmw&磌P)ȒREIHr6 ͎a PEjJI.x,NTJUr!C i,J́;|>$*{KjlP"BLʀoSQ5ZK;TNH'l cym^S"bGo_\A:R癰VbI\2LH3$~=q]62?\f.RlҒ$ ߿*iu!D)J0TEPmI@@\y>X6ͮI)֥)757nfH!Ts]XiR-hm V L:Bl ߬_\$%n%kN]H I)d[s=7n-=!w3d!|ĵ=ѿ8]S̙j)O-k$ֲLy\ :y(4n$!(" n o1,PUqԡY[P/JI2t[~qcLIsPq*y1?E4BB 7dwS\1aD:3:vצJRYK*y+@L {*uUse%J)Ga* 'q-S>l6)@U<x'oTTZT5T9̃4׏i4isV$1i㽰D!, 7fR2-q ' 0*oۯm]+p?lC ԠϫTJ@DIzu>So8 *)$ƪ<S FCJHl[/۶] V>BloÙPB#Һv.#ME>Oy'TftkͭNJH}2G[o[A!ճY5N|A$GMqF2pa] Cd _LL )C=V+Cn4\J "Iia<-9&Q϶ZVmci&\$<>=obSjd}&I!Ԡ)U6}f꤈UgU9#U(yE`&A;ulψ7aThmZC0RL(-b.o%D+.@g/uBH'$ ܺʨEBF"f:Hu=2d4%T#&I"DDHmq,*N%0R|y^o)lx-.n+@&=;BA2IW8s)mItj3AJIg DuC)6+&zZRZR!)VH10IlIŇʒ$j-3= `BHZ!Z,F/k{[!7V" INA`Bd5Ye(Ѕ@ P* |f;*˹1TįQ$6 ;[Ġ![<%3JdtGBTǓP:Bo!naU ih%8AU-1MHì֊$] q K)q"Jmc&N'uhTX( AiO.{\=1 h " I$\| CVt\ !JT7R\HRBPҦСi&J=sX3* ֦խeL•d-1";WL k;/q;^ Tv!>``JV!kPxSz۬bV㎡*)B:O?$ ӄF3bI&:=tJ.T 6*O'RNbvm0̄-('W/Zu\؆4'\m*(}dx@vABlu!d4JLld}PRuJ(l-D@;BOZv% d$%)S*1v=:!))l :#)EBTjQbH{ƲU"?cJLx^$^dJ}n\rk5m@ykKjCGPD( :Qk+򹜣FC&m;ȉ u-`y#A-@T$ɐd&j٨ ڴh0Zdytmkm *PM`B@ԉeYblcB( i$n'kw8f.;_6h<8Uu"Tzwhĭʋ GucINLXv>zu ILoRZSj yaJJQ Q^zcӚ:y, q%C~1ܒo0$*@>SI; ܨsHi`AP1"=0dAua, 'd6 =pG6RjJ|ԨAHBfCzq0T`#R[ÁPnus x؋pNeX6W's%bWe%2I+H'K\} abw]_ͱHz ϸ;UTB!BAR :Zvc6,L SaBJ6ﰽAP HmUzVΨA9PQmAT\q؞,!3G=M)eD+Ru+)M-CۉDq([korJ }-Z"R5} a!n]ao~)@F'ޗ¡&*u+P.T^,?^}е* L$t2Ph|܃#G4!vj?s 1B7)@{+IZ唈$sm>F֎ggJIȾֈJS!P&g`-yY΄JKTLL, ; g#MKƉBPA\mLBtSJ kKkH+RAv(R@ &{q{R\um ZnH vi*BзH9N֤)š`k$`vgm+ݺ)8QQpq.`Z:5JU&?PA֢TPa}6ƒU|ibZ)۠37ۮa5:u@C^r@ZIQQENۛt"̡VQC)ho`/$ߨi&AWEG*'V4&j(2tNW)*@ҕh2 :DĊS!%(:ʁLn@1ԉ-m֜[m8, ͓ dX8] idm>BiR-&bp]ԣq6IJc>fqm]] X[neًJeBRz[MEptB%tyˠ\ŵt|?u'5@J_Q^m->㮶as|z b5 /6IF6#c; !~5ԆXP D$\{bd#RNF=T3( "%JWz)*U׼X{Ag>aT:B@(–i! }i 5PBDW2-BUNZ"{p$n)!ujYWn.'Lm\G Vm AM-5(M|t6$H mR%b@aiJ "Do:#t {L)RӦ@̎e&Bd5Jv$۾*^%>,*^w))IZ[d_{oo^8h%|R":[%B| rg?^)sTQ-,dl/`,-MOx-E'DAu&gČ?mJ[lr'P;m!IB~!fMLV)QAY'kX)RmL8(2 FHC1ZZ#PJ`}}!|Kล? 0H{,p!iTR$7'蝷MIuPsʛZ "[YoӷbI>OB?- R<MZBQfb6={_h!Ҟ>$E^ oYIB| *"ߧl*u%*JlLjgĞBTʒVTBTT;\hͥl,JA 717aΰqJZ8 ԍ{@"pw<$㒢dDtx:JsvD'|8QQLs$_&p"r ֒%j'U%kHIU#$X) ;e`4˽ OrGU)KyԛH"gȾGPKۯEỸ|E% Js<)s 1[:yiJ)P 37>Sr{.%in!ԣ$)Vԭǐ[Fu"o0!N6$V%r@QН6^'$꥞9<j ۧe"mgmfaTҜ@|l(͓R¹|DH>oiH2*Hp!+R|PX.62; K.)iJSz{,T_4(ɑOQa~RРBN 'jV(#_h.$\/(jVzߨ9san[}*EAIۥ׼24dE+Tu:Q?#t媞}%PJIbAa". J>DDA` +CV)*)mQbdci SZ:% \hĉX'G%h09% ) ZuLy`z{a BQ- Vw腐-$,,t(b&| 2t \Ozu+P7רK  X:HXz,B$2{JH}upjB_HR%$[Qw+ ZS2+EPi– 9N@#ejˢ19021p|K+A*T#7O*F\cLB?U RQ2$ X13,VRUm]AK] XKw:sŚG]vP(}@ (6P7v=H ;!⠤>o)Hv3$BpWM?r6-K#˩8 Q:0RJIS $Ƕ;\%0ǎsJ6 ]Y'YHU}m|J/ +ez|0> i#q>zLsEqT p4Rd4 ljnP]wJS.B [LI}0%mJo|#(^\_7H^$-Ϋ}OCAvnbq%j-p;AB9$S)uIG[{Zc|99JRTI{nEOXZ i:STBD E1*uXp9!dn6؏LK  , $.vos|ڥ0A36OanII/[F0gv826%.d]m!Z ;oi}+#S-өE*ҐH|GO6 d\:P%$*6hS 0e'P+ gbFZ2A`h^=zGL(<Ѕq)oJiS:—orw|䅇(yN<*kŭZR:!28$*Dt Bҥ\8TSh"PAJ<ˁ@BuzzH{b Y %\x+J}烚-n`HwQxJPgQL}:E3P0ؙvHKNΔ,|s0p!aԔ(8IF;Zw~KծJJy @>a465j3P"wBA,H$;>$:(RFA 3B4%KBP5״F?ZZN'֧I) F%KV`ƩTI؟o]aKi@'RDj":{ah6)BƇWסL $Án&TU!JukN=N֐SqXu R[0d;pTBb}0'5+URV()!D(y3)x*k64B(H;N o'l9!S\֍BmRB~]B'H&.;̂ġ(gHF0P)mJ!P_o#a(-%L 6=,o JeeIVJVbcV*$~w”;y,]RX\&/iR:vZBڜ$C%H) Fĥ%3 K:R&Q Y"cĠ*i7q(u Ԣ sh NY:SiR'L$Q ښ%e@'bGg !&ZZN-D):aoN[M K:K%ӣHb 3?8;-ZsIoZd'Rt eZִ!JHEH{(CqImJIl 0OF'*|kЧY T݁ngj1Nӡ(:4HAc3&ɴIiO]Ά(5]5/y/6I2 |]o*C ee K:Q)2`ؐ>ޑ;2h!)H )Lo}->nC9y-IReU Baq{ﺆBQl!)HI &\ILjJ]Cl P 6"DHGk2ZJBI;=Eץp+xKp!LT)D!]}HLfG+y0TeY9ԸCWUϵЇIYJRR@" 0$ByJC#H$ic%\%N@$ ؟?FJ(pvzzX+O-5UVE2B։m m/{[,8!(L_n~L#jJu*pXNְm#dR(`$ )x `'0VY.(-G'DVń̕GwzH,Ni #~~p Tj^%7QD\D! R!MB4X{=Z4! ^R!7$HaSa6(IRRINpT!y BR AOOi(Eeh]KC̐P@sa;Da:ˁkF>h\ ߩ`I<a) ( 3yoBKICQJ${p>J: 'w¡DKaJQ0+ s xTtvh đ$A$uBZ>d#p=ą[E A \m>a3 JHJЂ@:!hCB:zHA !!s̗`@=tGX*/IliBO{+7p4(AJ&nBjhU<*Cߨm2t(QuQRI&I>G&8[IV Rpok@s.[:' UГm";-m:J $*,`[qhYuSuHQ.$}'hT2,)>BJLM *Wr@h `%<0D;H;uqkJ?Q) 6dM!tێWQJ;ON`o ʔntyl:sl54KT(['̂gL4A;_yhSmH$M& la@s NJ\ ):T *J'a{>fL$5UeqKS녨-)s aq-;Pץ!Ԡ\ >ê4@Ni&!` K)JӉY^ u$`&Ш0UwSvBl@1ll ) XT!PJe(*{nBKEE q琕%鱹#GY[FgJLčWkKpn`oBU5.FF$I/8@7) QkHHLߧAJ)\ʐ+Q:NݶN3H>GanFbwA=!! CҺuPZ@3h^`!*-TS`遮 [Z ʖ RN?l,bKSg, O#" $DӲj\I@YBJŽI=m8TJ~VUp@t۩sNJ(N%(\ߦLlq Հ$$L΍ IJR#hT@XLd$,+ &;amKIBbDjPRJQng|慣N%N9 $j"LL~i>WCN+ +0p*1BXe3*$~L҈pدw$Pm)I^" `BT@(#Jdd\#PڔUД{{)Ki*!M벍*8'u1Q)CI+b>CIyIJ֝I JCeK):G\_blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_templates/000077500000000000000000000000001203633570100330345ustar00rootroot00000000000000photo.mako000066400000000000000000000001241203633570100347540ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_templates<%inherit file="plugin_base_template" />

photo_index.mako000066400000000000000000000005251203633570100361500ustar00rootroot00000000000000blogofile-0.8b1/blogofile/tests/plugin/plugin_test/blogofile_plugin_test/site_src/_templates<%inherit file="plugin_base_template" /> This is just a stupid photo gallery as a demonstration plugin for Blogofile. It's meant to be as simplistic as possible so as to be easy to read. % for photo in photos: % endfor
${photo}
blogofile-0.8b1/blogofile/tests/plugin/plugin_test/setup.py000066400000000000000000000035471203633570100242300ustar00rootroot00000000000000###################################################################### #### Instructions for creating a new Blogofile plugin: #### 1) Set module_name to the name of your plugin using only alpha-numeric #### characters and the underscore : module_name = "blogofile_plugin_test" #### 2) Rename the blogofile_plugin_example directory to this same name. #### 3) Edit module_name/__init__.py and configure the __dist__ object. #### 4) Create your plugin's controllers, filters, and other files in #### module_name/site_src #### 5) Run "python setup.py develop" to start testing your plugin. #### (You may need to be root or run virtualenv) #### 6) Run 'blogofile plugin list' and you should see your plugin listed. #### #### The rest of this file is boilerplate, and you can probably leave as is. ###################################################################### from setuptools import setup, find_packages import os import imp def find_package_data(module, path): """Find all data files to include in the package""" files = [] for dirpath, dirnames, filenames in os.walk(os.path.join(module,path)): for filename in filenames: files.append(os.path.relpath(os.path.join(dirpath,filename),module)) return {module:files} #Setup the application using meta information from #the plugin's own __dist__ object: plugin = imp.load_package(module_name,module_name) setup(name=module_name, description=plugin.__dist__['pypi_description'], version=plugin.__version__, author=plugin.__dist__["author"], url=plugin.__dist__["url"], packages=[module_name], package_data = find_package_data(module_name,"site_src"), include_package_data = True, install_requires =['blogofile'], entry_points = { "blogofile.plugins": ["{module_name} = {module_name}".format(**locals())] } ) blogofile-0.8b1/blogofile/tests/test_build.py000066400000000000000000000170001203633570100213600ustar00rootroot00000000000000# -*- coding: utf-8 -*- try: import unittest2 as unittest # For Python 2.6 except ImportError: import unittest # flake8 ignore # NOQA import tempfile import shutil import os import re from blogofile import main from blogofile import util from blogofile import template from blogofile import cache import logging @unittest.skip('outdated integration test') class TestBuild(unittest.TestCase): def setUp(self): main.do_debug() #Remember the current directory to preserve state self.previous_dir = os.getcwd() #Create a staging directory that we can build in self.build_path = tempfile.mkdtemp() #Change to that directory just like a user would os.chdir(self.build_path) #Reinitialize the configuration main.config.init() def tearDown(self): #Revert the config overridden options main.config.override_options = {} #go back to the directory we used to be in os.chdir(self.previous_dir) #Clean up the build directory shutil.rmtree(self.build_path) #Clear Template Engine environments: template.MakoTemplate.template_lookup = None template.JinjaTemplate.template_lookup = None main.config.reset_config() cache.reset_bf() def testBlogSubDir(self): """Test to make sure blogs hosted in subdirectories off the webroot work""" main.main("init blog_unit_test") main.config.override_options = { "site.url": "http://www.yoursite.com/~username", "blog.path": "/path/to/blog"} main.main("build") lsdir = os.listdir(os.path.join(self.build_path, "_site", "path", "to", "blog")) for fn in ("category", "page", "feed"): assert(fn in lsdir) def testPermaPages(self): """Test that permapages are written""" main.main("init blog_unit_test") main.config.override_options = { "site.url": "http://www.test.com/", "blog.path": "/blog"} main.main("build") assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "blog", "2009", "07", "23", "post-1")) assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "blog", "2009", "07", "24", "post-2")) def testNoPosts(self): """Test when there are no posts, site still builds cleanly""" main.main("init blog_unit_test") main.config.override_options = { "site_url": "http://www.test.com/", "blog_path": "/blog"} shutil.rmtree("_posts") util.mkdir("_posts") main.main("build") def testPostInSubdir(self): "Test a post in a subdirectory of _posts" pass def testNoPostsDir(self): """Test when there is no _posts dir, site still builds cleanly""" main.main("init blog_unit_test") main.config.override_options = { "site.url": "http://www.test.com/", "blog.path": "/blog"} shutil.rmtree("_posts") logger = logging.getLogger("blogofile") #We don't need to see the error that this test checks for: logger.setLevel(logging.CRITICAL) main.main("build") logger.setLevel(logging.ERROR) def testCategoryPages(self): """Test that categories are written""" main.main("init blog_unit_test") main.config.override_options = { "site.url": "http://www.test.com", "blog.path": "/path/to/blog"} main.main("build") assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "category", "category-1", "1")) assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "category", "category-1")) assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "category", "category-2")) assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "category", "category-2", "1")) def testArchivePages(self): """Test that archives are written""" main.main("init blog_unit_test") main.config.override_options = { "site.url": "http://www.test.com", "blog.path": "/path/to/blog"} main.main("build") assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "archive", "2009", "07", "1")) def testFeeds(self): """Test that RSS/Atom feeds are written""" main.main("init blog_unit_test") main.config.override_options = { "site.url": "http://www.test.com", "blog.path": "/path/to/blog"} main.main("build") #Whole blog feeds assert "index.xml" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "feed")) assert "index.xml" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "feed", "atom")) #Per category feeds assert "index.xml" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "category", "category-1", "feed")) assert "index.xml" in os.listdir( os.path.join(self.build_path, "_site", "path", "to", "blog", "category", "category-1", "feed", "atom")) def testFileIgnorePatterns(self): main.main("init blog_unit_test") #Initialize the config manually main.config.init("_config.py") #Add some file_ignore_patterns: open("test.txt", "w").close() open("test.py", "w").close() #File ignore patterns can be strings main.config.site.file_ignore_patterns.append(r".*test\.txt$") #Or, they can be precompiled regexes p = re.compile(".*\.py$") main.config.site.file_ignore_patterns.append(p) main.config.recompile() main.do_build([], load_config=False) assert not "test.txt" in os.listdir( os.path.join(self.build_path, "_site")) assert not "test.py" in os.listdir( os.path.join(self.build_path, "_site")) def testAutoPermalinks(self): main.main("init blog_unit_test") main.main("build") #Make sure the post with question mark in title was generated properly assert os.path.isfile(os.path.join( self.build_path, "_site", "blog", "2009", "08", "29", "this-title-has-a-question-mark-", "index.html")) def testAutoPermalinkPagesRespectBlogPath(self): """Test that default auto_permalink.path incorporates the configured blog.path setting""" main.main("init blog_unit_test") main.config.override_options = { "site.url": "http://www.test.com", "blog.path": "some-crazy-blog"} main.main("build") assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "some-crazy-blog", "2009", "07", "23", "post-1")) assert "index.html" in os.listdir( os.path.join(self.build_path, "_site", "some-crazy-blog", "2009", "07", "24", "post-2")) blogofile-0.8b1/blogofile/tests/test_config.py000066400000000000000000000077241203633570100215420ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Unit tests for blogofile config module. """ import os try: import unittest2 as unittest # For Python 2.6 except ImportError: import unittest # flake8 ignore # NOQA from mock import ( MagicMock, mock_open, patch, ) from .. import config class TestConfigModuleAttributes(unittest.TestCase): """Unit tests for attributes that config exposes in its module scope. """ def test_bf_config_is_module(self): """config has bf.config attribute that is a module """ from types import ModuleType self.assertIsInstance(config.bf.config, ModuleType) def test_bf_config_module_name(self): """bf.config attribute is blogofile.config module """ self.assertEqual(config.bf.config.__name__, 'blogofile.config') def test_site_is_hierarchical_cache(self): """config has site attribute that is a HierarchicalCache object """ from ..cache import HierarchicalCache self.assertIsInstance(config.site, HierarchicalCache) def test_controllers_is_hierarchical_cache(self): """config has controllers attribute that is a HierarchicalCache object """ from ..cache import HierarchicalCache self.assertIsInstance(config.controllers, HierarchicalCache) def test_filters_is_hierarchical_cache(self): """config has filters attribute that is a HierarchicalCache object """ from ..cache import HierarchicalCache self.assertIsInstance(config.filters, HierarchicalCache) def test_plugins_is_hierarchical_cache(self): """config has plugins attribute that is a HierarchicalCache object """ from ..cache import HierarchicalCache self.assertIsInstance(config.plugins, HierarchicalCache) def test_templates_is_hierarchical_cache(self): """config has templates attribute that is a HierarchicalCache object """ from ..cache import HierarchicalCache self.assertIsInstance(config.templates, HierarchicalCache) def test_default_config_path(self): """config has default_config_path attr set to default_config module """ self.assertEqual( config.default_config_path, os.path.join(os.path.abspath('blogofile'), 'default_config.py')) class TestConfigInitInteractive(unittest.TestCase): """Unit tests for init_interactive function. """ def _call_fut(self, *args): """Call the function under test. """ return config.init_interactive(*args) def test_init_interactive_loads_user_config(self): """init_interactive loads value from user _config.py """ args = MagicMock(src_dir='foo') mo = mock_open(read_data='site.url = "http://www.example.com/test/"') with patch.object(config, 'open', mo, create=True): self._call_fut(args) self.assertEqual(config.site.url, 'http://www.example.com/test/') def test_init_interactive_no_config_raises_SystemExit(self): """init_interactive raises SystemExit when no _config.py exists """ args = MagicMock(src_dir='foo') with self.assertRaises(SystemExit): self._call_fut(args) class TestConfigLoadConfig(unittest.TestCase): """Unit tests for _load_config function. """ def _call_fut(self, *args): """Call the function under test. """ return config._load_config(*args) def test_init_interactive_loads_default_config(self): """init_interactive loads values from default_config.py """ with patch.object(config, 'open', mock_open(), create=True): self._call_fut('_config.py') self.assertEqual(config.site.url, 'http://www.example.com') def test_load_config_no_config_raises_IOError(self): """_load_config raises IOError when no _config.py exists """ with self.assertRaises(IOError): self._call_fut('_config.py') blogofile-0.8b1/blogofile/tests/test_content.py000066400000000000000000000221061203633570100217360ustar00rootroot00000000000000# -*- coding: utf-8 -*- try: import unittest2 as unittest # For Python 2.6 except ImportError: import unittest # flake8 ignore # NOQA import tempfile import shutil import os from blogofile import main @unittest.skip('outdated integration test') class TestContent(unittest.TestCase): def setUp(self): #Remember the current directory to preserve state self.previous_dir = os.getcwd() #Create a staging directory that we can build in self.build_path = tempfile.mkdtemp() #Change to that directory just like a user would os.chdir(self.build_path) #Reinitialize the configuration main.config.init() def tearDown(self): #Revert the config overridden options main.config.override_options = {} #go back to the directory we used to be in os.chdir(self.previous_dir) #Clean up the build directory shutil.rmtree(self.build_path) def testAutoPermalink(self): """make sure post without permalink gets a good autogenerated one """ main.main("init blog_unit_test") #Write a post to the _posts dir: src = """--- title: This is a test post date: 2009/08/16 00:00:00 --- This is a test post """ f = open( os.path.join(self.build_path, "_posts", "01. Test post.html"), "w") f.write(src) f.close() main.config.override_options = { "site.url": "http://www.yoursite.com", "blog.path": "/blog", "blog.auto_permalink.enabled": True, "blog.auto_permalink.path": "/blog/:year/:month/:day/:title"} main.main("build") rendered = open(os.path.join(self.build_path,"_site","blog","2009","08", "16","this-is-a-test-post","index.html" )).read() def testHardCodedPermalinkUpperCase(self): """Permalink's set by the user should appear exactly as the user enters""" main.main("init blog_unit_test") #Write a post to the _posts dir: permalink = "http://www.yoursite.com/bLog/2009/08/16/This-Is-A-TeSt-Post" src = """--- title: This is a test post permalink: %(permalink)s date: 2009/08/16 00:00:00 --- This is a test post """ % {'permalink':permalink} f = open(os.path.join(self.build_path,"_posts","01. Test post.html"),"w") f.write(src) f.close() main.config.override_options = { "site.url":"http://www.yoursite.com", "blog.path":"/blog", "blog.auto_permalink.enabled": True, "blog.auto_permalink.path": "/blog/:year/:month/:day/:title" } main.main("build") rendered = open(os.path.join(self.build_path,"_site","bLog","2009","08", "16","This-Is-A-TeSt-Post","index.html" )).read() def testUpperCaseAutoPermalink(self): """Auto generated permalinks should have title and filenames lower case (but not the rest of the URL)""" main.main("init blog_unit_test") #Write a post to the _posts dir: src = """--- title: This is a test post date: 2009/08/16 00:00:00 --- This is a test post without a permalink """ f = open(os.path.join(self.build_path,"_posts","01. Test post.html"),"w") f.write(src) f.close() main.config.override_options = { "site.url":"http://www.BlogoFile.com", "blog.path":"/Blog", "blog.auto_permalink.enabled": True, "blog.auto_permalink.path": "/Blog/:year/:month/:day/:title" } main.main("build") rendered = open(os.path.join(self.build_path,"_site","Blog","2009","08", "16","this-is-a-test-post","index.html" )).read() def testPathOnlyPermalink(self): """Test to make sure path only permalinks are generated correctly""" main.main("init blog_unit_test") #Write a post to the _posts dir: permalink = "/blog/2009/08/16/this-is-a-test-post" src = """--- title: This is a test post permalink: %(permalink)s date: 2009/08/16 00:00:00 --- This is a test post """ %{'permalink':permalink} f = open(os.path.join(self.build_path,"_posts","01. Test post.html"),"w") f.write(src) f.close() main.config.override_options = { "site.url":"http://www.yoursite.com", "blog.path":"/blog", "blog.auto_permalink.enabled": True, "blog.auto_permalink.path": "/blog/:year/:month/:day/:title" } main.main("build") rendered = open(os.path.join(self.build_path,"_site","blog","2009","08", "16","this-is-a-test-post","index.html" )).read() #TODO: Replace BeautifulSoup with lxml or use Selenium: # def testFeedLinksAreURLs(self): # """Make sure feed links are full URLs and not just paths""" # main.main("init blog_unit_test") # #Write a post to the _posts dir: # permalink = "/blog/2009/08/16/test-post" # src = """--- # title: This is a test post # permalink: %(permalink)s # date: 2009/08/16 00:00:00 # --- # This is a test post # """ %{'permalink':permalink} # f = open(os.path.join(self.build_path,"_posts","01. Test post.html"),"w") # f.write(src) # f.close() # main.config.override_options = { # "site.url":"http://www.yoursite.com", # "blog.path":"/blog", # "blog.auto_permalink.enabled": True, # "blog.auto_permalink.path": "/blog/:year/:month/:day/:title" } # main.main("build") # feed = open(os.path.join(self.build_path,"_site","blog","feed", # "index.xml")).read() # soup = BeautifulSoup.BeautifulStoneSoup(feed) # for link in soup.findAll("link"): # assert(link.contents[0].startswith("http://")) #TODO: Replace BeautifulSoup with lxml or use Selenium: # def testCategoryLinksInPosts(self): # """Make sure category links in posts are correct""" # main.main("init blog_unit_test") # main.config.override_options = { # "site.url":"http://www.yoursite.com", # "blog.path":"/blog" # } # #Write a blog post with categories: # src = """--- # title: This is a test post # categories: Category 1, Category 2 # date: 2009/08/16 00:00:00 # --- # This is a test post # """ # f = open(os.path.join(self.build_path,"_posts","01. Test post.html"),"w") # f.write(src) # f.close() # main.main("build") # #Open up one of the permapages: # page = open(os.path.join(self.build_path,"_site","blog","2009", # "08","16","this-is-a-test-post","index.html")).read() # soup = BeautifulSoup.BeautifulStoneSoup(page) # print(soup.findAll("a")) # assert soup.find("a",attrs={'href':'/blog/category/category-1'}) # assert soup.find("a",attrs={'href':'/blog/category/category-2'}) def testReStructuredFilter(self): """Test to make sure reStructuredTest work well""" main.main("init blog_unit_test") #Write a post to the _posts dir: src = """--- title: This is a test post date: 2010/03/27 00:00:00 --- This is a reStructured post =========================== Plain text : :: $ echo "hello" hello """ f = open(os.path.join(self.build_path,"_posts","01. Test post.rst"),"w") f.write(src) f.close() main.config.override_options = { "site.url":"http://www.yoursite.com", "blog.path":"/blog", "blog.auto_permalink.enabled": True, "blog.auto_permalink.path": "/blog/:year/:month/:day/:title" } main.main("build") rendered = open(os.path.join(self.build_path,"_site","blog","2010","03", "27","this-is-a-test-post","index.html" )).read() assert """

This is a reStructured post

Plain text :

$ echo "hello"
hello
""" in rendered def testUnpublishedPost(self): """A post marked 'draft: True' should never show up in archives, categories, chronological listings, or feeds. It should generate a single permapage and that's all.""" main.main("init blog_unit_test") main.main("build") #Make sure the permapage was written rendered = open(os.path.join( self.build_path,"_site","blog","2099","08", "01","this-post-is-unpublished","index.html" )).read() #Make sure the archive was not written assert not os.path.exists(os.path.join( self.build_path,"_site","blog","archive", "2099")) #Make sure the category was not written assert not os.path.exists(os.path.join( self.build_path,"_site","blog","category", "drafts")) blogofile-0.8b1/blogofile/tests/test_main.py000066400000000000000000000557541203633570100212270ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Unit tests for blogofile main module. Tests entry point function, command line parser, and sub-command action functions. """ import argparse import logging import os import platform import sys try: import unittest2 as unittest # For Python 2.6 except ImportError: import unittest # flake8 ignore # NOQA from mock import MagicMock from mock import Mock from mock import patch import six from .. import main class TestEntryPoint(unittest.TestCase): """Unit tests for blogofile entry point function. """ def _call_entry_point(self): main.main() @patch.object(main, 'setup_command_parser', return_value=(Mock(), [])) def test_entry_w_too_few_args_prints_help(self, mock_setup_parser): """entry with 1 arg calls parser print_help and exits """ mock_parser, mock_subparsers = mock_setup_parser() mock_parser.exit = sys.exit with patch.object(main, 'sys') as mock_sys: mock_sys.argv = ['blogofile'] with self.assertRaises(SystemExit): self._call_entry_point() mock_parser.print_help.assert_called_once() @patch.object(main, 'setup_command_parser', return_value=(Mock(), [])) def test_entry_parse_args(self, mock_setup_parser): """entry with >1 arg calls parse_args """ mock_parser, mock_subparsers = mock_setup_parser() with patch.object(main, 'sys') as mock_sys: mock_sys.argv = 'blogofile foo'.split() self._call_entry_point() mock_parser.parse_args.assert_called_once() @patch.object(main, 'setup_command_parser', return_value=(Mock(), [])) @patch.object(main, 'set_verbosity') def test_entry_set_verbosity(self, mock_set_verbosity, mock_setup_parser): """entry with >1 arg calls set_verbosity """ mock_parser, mock_subparsers = mock_setup_parser() mock_args = Mock() mock_parser.parse_args = Mock(return_value=mock_args) with patch.object(main, 'sys') as mock_sys: mock_sys.argv = 'blogofile foo bar'.split() self._call_entry_point() mock_set_verbosity.assert_called_once_with(mock_args) @patch.object(main, 'setup_command_parser', return_value=(Mock(name='parser'), Mock(name='subparsers'))) @patch.object(main, 'do_help') def test_entry_do_help(self, mock_do_help, mock_setup_parser): """entry w/ help in args calls do_help w/ args, parser & subparsers """ mock_parser, mock_subparsers = mock_setup_parser() mock_args = Mock(name='args', func=mock_do_help) mock_parser.parse_args = Mock(return_value=mock_args) with patch.object(main, 'sys') as mock_sys: mock_sys.argv = 'blogofile help'.split() self._call_entry_point() mock_do_help.assert_called_once_with( mock_args, mock_parser, mock_subparsers) @patch.object(main, 'setup_command_parser', return_value=(Mock(), [])) def test_entry_arg_func(self, mock_setup_parser): """entry with >1 arg calls args.func with args """ mock_parser, mock_subparsers = mock_setup_parser() mock_args = Mock() mock_parser.parse_args = Mock(return_value=mock_args) with patch.object(main, 'sys') as mock_sys: mock_sys.argv = 'blogofile foo bar'.split() self._call_entry_point() mock_args.func.assert_called_once_with(mock_args) class TestLoggingVerbosity(unittest.TestCase): """Unit tests for logging verbosity setup. """ def _call_fut(self, *args): """Call the fuction under test. """ main.set_verbosity(*args) @patch.object(main, 'logger') def test_verbose_mode_sets_INFO_logging(self, mock_logger): """verbose==True in args sets INFO level logging """ mock_args = Mock(verbose=True, veryverbose=False) self._call_fut(mock_args) mock_logger.setLevel.assert_called_once_with(logging.INFO) @patch.object(main, 'logger') def test_very_verbose_mode_sets_DEBUG_logging(self, mock_logger): """veryverbose==True in args sets DEBUG level logging """ mock_args = Mock(verbose=False, veryverbose=True) self._call_fut(mock_args) mock_logger.setLevel.assert_called_once_with(logging.DEBUG) class TestParserTemplate(unittest.TestCase): """Unit tests for command line parser template. """ def _call_fut(self): """Call function under test. """ return main._setup_parser_template() @patch('sys.stderr', new_callable=six.StringIO) def test_parser_template_version(self, mock_stderr): """parser template version arg returns expected string and exits """ from .. import __version__ parser_template = self._call_fut() with self.assertRaises(SystemExit): parser_template.parse_args(['--version']) self.assertEqual( mock_stderr.getvalue(), 'Blogofile {0} -- http://www.blogofile.com -- {1} {2}\n' .format(__version__, platform.python_implementation(), platform.python_version())) def test_parser_template_verbose_default(self): """parser template sets verbose default to False """ parser_template = self._call_fut() args = parser_template.parse_args([]) self.assertFalse(args.verbose) def test_parser_template_verbose_true(self): """parser template sets verbose to True when -v in args """ parser_template = self._call_fut() args = parser_template.parse_args(['-v']) self.assertTrue(args.verbose) def test_parser_template_veryverbose_default(self): """parser template sets veryverbose default to False """ parser_template = self._call_fut() args = parser_template.parse_args([]) self.assertFalse(args.veryverbose) def test_parser_template_veryverbose_true(self): """parser template sets veryverbose to True when -vv in args """ parser_template = self._call_fut() args = parser_template.parse_args(['-vv']) self.assertTrue(args.veryverbose) class TestHelpParser(unittest.TestCase): """Unit tests for help sub-command parser. """ def _parse_args(self, *args): """Set up sub-command parser, parse args, and return result. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() main._setup_help_parser(subparsers) return parser.parse_args(*args) def test_help_parser_commands_default(self): """help w/ no command sets command arg to empty list """ args = self._parse_args(['help']) self.assertEqual(args.command, []) def test_help_parser_commands(self): """help w/ commands sets command arg to list of commands """ args = self._parse_args('help foo bar'.split()) self.assertEqual(args.command, 'foo bar'.split()) def test_help_parser_func_do_help(self): """help action function is do_help """ args = self._parse_args(['help']) self.assertEqual(args.func, main.do_help) class TestInitParser(unittest.TestCase): """Unit tests for init sub-command parser. """ def _parse_args(self, *args): """Set up sub-command parser, parse args, and return result. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() main._setup_init_parser(subparsers) return parser.parse_args(*args) def test_init_parser_src_dir_arg(self): """init parser sets src_dir arg to given arg """ args = self._parse_args('init foo'.split()) self.assertEqual(args.src_dir, 'foo') def test_init_parser_plugin_default(self): """init parser sets default plugin arg to None """ args = self._parse_args('init foo'.split()) self.assertEqual(args.plugin, None) def test_init_parser_plugin_arg(self): """init parser sets plugin arg to given arg """ args = self._parse_args('init foo bar'.split()) self.assertEqual(args.plugin, 'bar') def test_init_parser_func_do_build(self): """init action function is do_init """ args = self._parse_args('init foo'.split()) self.assertEqual(args.func, main.do_init) class TestDoInit(unittest.TestCase): """Unit tests for init sub-command action function. """ def _call_fut(self, args): """Call the fuction under test. """ main.do_init(args) @patch.object(main.os.path, 'exists', return_value=True) @patch('sys.stderr', new_callable=six.StringIO) def test_do_init_not_overwrite_existing_src_dir(self, mock_stderr, mock_path_exists): """do_init won't overwrite existing src_dir and exits w/ msg """ args = Mock(src_dir='foo/bar', plugin=None) with self.assertRaises(SystemExit): self._call_fut(args) self.assertEqual( mock_stderr.getvalue(), '{0.src_dir} already exists; initialization aborted\n' .format(args)) @patch.object(main.os.path, 'exists', return_value=False) @patch.object(main, '_init_bare_site', autospec=True) def test_do_init_wo_plugin_calls_init_bare_site(self, mock_init_bare_site, mock_path_exists): """do_init w/o plugin calls _init_bare_site w/ src_dir arg """ args = Mock(src_dir='foo/bar', plugin=None) self._call_fut(args) mock_init_bare_site.assert_called_once_with(args.src_dir) @patch.object(main.os.path, 'exists', return_value=False) @patch.object(main, '_init_plugin_site', autospec=True) def test_do_init_w_plugin_init_plugin_site(self, mock_init_plugin_site, mock_path_exists): """do_init w plugin calls _init_plugin_site w/ args """ args = Mock(src_dir='foo/bar', plugin='blog') self._call_fut(args) mock_init_plugin_site.assert_called_once_with(args) class TestInitBareSite(unittest.TestCase): """Unit tests _init_bare_site function. """ def _call_fut(self, args): """Call the fuction under test. """ main._init_bare_site(args) @patch.object(main.os, 'makedirs', autospec=True) def test_init_bare_site_creates_src_dir(self, mock_mkdirs): """_init_bare_site calls os.makedirs to create src_dir c/w parents """ src_dir = 'foo/bar' with patch.object(main, 'open', create=True) as mock_open: spec = six.StringIO if six.PY3 else file mock_open.return_value = MagicMock(spec=spec) self._call_fut(src_dir) mock_mkdirs.assert_called_once_with(src_dir) @patch.object(main.os, 'makedirs') def test_init_bare_site_writes_to_config_file(self, mock_mkdirs): """_init_bare_site writes new _config.py file """ with patch.object(main, 'open', create=True) as mock_open: spec = six.StringIO if six.PY3 else file mock_open.return_value = MagicMock(spec=spec) new_config_handle = mock_open.return_value.__enter__.return_value self._call_fut('foo/bar') self.assertTrue(new_config_handle.writelines.called) @patch.object(main.os, 'makedirs') def test_init_bare_site_writes_config(self, mock_mkdirs): """_init_bare_site writes expected lines to new _config.py file """ with patch.object(main, 'open', create=True) as mock_open: spec = six.StringIO if six.PY3 else file mock_open.return_value = MagicMock(spec=spec) new_config_handle = mock_open.return_value.__enter__.return_value self._call_fut('foo/bar') new_config_handle.writelines.called_with('# -*- coding: utf-8 -*-') @patch.object(main.os, 'makedirs') @patch('sys.stdout', new_callable=six.StringIO) def test_init_bare_site_prints_config_written_msg(self, mock_stdout, mock_mkdirs): """_init_bare_site prints msg re: creation of _config.py file """ src_dir = 'foo/bar' with patch.object(main, 'open', create=True) as mock_open: spec = six.StringIO if six.PY3 else file mock_open.return_value = MagicMock(spec=spec) self._call_fut(src_dir) self.assertEqual( mock_stdout.getvalue(), '_config.py for a bare (do-it-yourself) site written to {0}\n' 'If you were expecting more, please see `blogofile init -h`\n' .format(src_dir)) class TestInitPluginSite(unittest.TestCase): """Unit tests _init_plugin_site function. """ def _call_fut(self, *args): """Call the fuction under test. """ main._init_plugin_site(*args) @patch.object(main.shutil, 'copytree') def test_init_plugin_site_gets_plugin_by_name(self, mock_copytree): """_init_plugin_site calls plugin.get_by_name w/ plugin arg """ from .. import plugin as plugin_module args = Mock(src_dir='foo/bar', plugin='baz') mock_plugin = Mock(__file__='baz_plugin/__init__.py') patch_get_by_name = patch.object( plugin_module, 'get_by_name', return_value=mock_plugin) with patch_get_by_name as mock_get_by_name: self._call_fut(args) mock_get_by_name.assert_called_once_with(args.plugin) @patch('sys.stderr', new_callable=six.StringIO) def test_init_plugin_site_msg_re_unknown_plugin(self, mock_stderr): """_init_plugin_site shows msg if plugin arg not found """ from .. import plugin as plugin_module args = Mock(src_dir='foo/bar', plugin='baz') patch_get_by_name = patch.object( plugin_module, 'get_by_name', return_value=None) patch_open = patch.object(main, 'open', create=True) # nested contexts for Python 2.6 compatibility with patch_open as mock_open: spec = six.StringIO if six.PY3 else file mock_open.return_value = MagicMock(spec=spec) with patch_get_by_name: self._call_fut(args) self.assertTrue( mock_stderr.getvalue().startswith( '{0.plugin} plugin not installed; initialization aborted\n\n' 'installed plugins:\n'.format(args))) @patch('sys.stderr', new_callable=six.StringIO) def test_init_plugin_site_plugin_list_if_unknown_plugin(self, mock_stderr): """ """ from .. import plugin as plugin_module args = Mock(src_dir='foo/bar', plugin='baz') patch_get_by_name = patch.object( plugin_module, 'get_by_name', return_value=None) patch_list_plugins = patch.object(plugin_module, 'list_plugins') patch_open = patch.object(main, 'open', create=True) # nested contexts for Python 2.6 compatibility with patch_list_plugins as mock_list_plugins: with patch_get_by_name: with patch_open: self._call_fut(args) assert mock_list_plugins.called @patch.object(main.shutil, 'copytree') @patch.object(main.shutil, 'ignore_patterns') def test_init_plugin_site_ignore_dirs(self, mock_ignore_patterns, mock_copytree): """_init_plugin_site calls shutil.ignore_patterns w/ expected dirs """ from .. import plugin as plugin_module args = Mock(src_dir='foo/bar', plugin='baz') mock_plugin = Mock(__file__='baz_plugin/__init__.py') patch_get_by_name = patch.object( plugin_module, 'get_by_name', return_value=mock_plugin) with patch_get_by_name: self._call_fut(args) mock_ignore_patterns.assert_called_once_with( '_controllers', '_filters') @patch.object(main.shutil, 'ignore_patterns') @patch.object(main.shutil, 'copytree') def test_init_plugin_site_copies_site_src_tree(self, mock_copytree, mock_ignore_patterns): """_init_plugin_site calls shutil.copytree w/ expected args """ from .. import plugin as plugin_module args = Mock(src_dir='foo/bar', plugin='baz') mock_plugin = Mock(__file__='baz_plugin/__init__.py') patch_get_by_name = patch.object( plugin_module, 'get_by_name', return_value=mock_plugin) with patch_get_by_name: self._call_fut(args) mock_plugin_path = os.path.dirname( os.path.realpath(mock_plugin.__file__)) mock_site_src = os.path.join(mock_plugin_path, 'site_src') mock_copytree.assert_called_once_with( mock_site_src, args.src_dir, ignore=mock_ignore_patterns()) @patch.object(main.shutil, 'copytree') @patch('sys.stdout', new_callable=six.StringIO) def test_init_plugin_site_prints_config_written_msg(self, mock_stdout, mock_copytree): """_init_plugin_site prints msg re: creation of _config.py file """ from .. import plugin as plugin_module args = Mock(src_dir='foo/bar', plugin='baz') mock_plugin = Mock(__file__='baz_plugin/__init__.py') patch_get_by_name = patch.object( plugin_module, 'get_by_name', return_value=mock_plugin) patch_open = patch.object(main, 'open', create=True) # nested contexts for Python 2.6 compatibility with patch_open as mock_open: spec = six.StringIO if six.PY3 else file mock_open.return_value = MagicMock(spec=spec) with patch_get_by_name: self._call_fut(args) self.assertEqual( mock_stdout.getvalue(), '{0.plugin} plugin site_src files written to {0.src_dir}\n' .format(args)) class TestBuildParser(unittest.TestCase): """Unit tests for build sub-command parser. """ def _parse_args(self, *args): """Set up sub-command parser, parse args, and return result. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() main._setup_build_parser(subparsers) return parser.parse_args(*args) def test_build_parser_src_dir_default(self): """build parser sets src_dir default to relative cwd """ args = self._parse_args(['build']) self.assertEqual(args.src_dir, '.') def test_build_parser_src_dir_value(self): """build parser sets src_dir to arg value """ args = self._parse_args('build -s foo'.split()) self.assertEqual(args.src_dir, 'foo') def test_build_parser_func_do_build(self): """build action function is do_build """ args = self._parse_args(['build']) self.assertEqual(args.func, main.do_build) class TestServeParser(unittest.TestCase): """Unit tests for serve sub-command parser. """ def _parse_args(self, *args): """Set up sub-command parser, parse args, and return result. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() main._setup_serve_parser(subparsers) return parser.parse_args(*args) def test_serve_parser_ip_addr_default(self): """serve parser sets ip address default to 127.0.0.1 """ args = self._parse_args(['serve']) self.assertEqual(args.IP_ADDR, '127.0.0.1') def test_serve_parser_ip_addr_arg(self): """serve parser sets ip address to given arg """ args = self._parse_args('serve 8888 192.168.1.5'.split()) self.assertEqual(args.IP_ADDR, '192.168.1.5') def test_serve_parser_port_default(self): """serve parser sets ip address default to 127.0.0.1 """ args = self._parse_args(['serve']) self.assertEqual(args.PORT, '8080') def test_serve_parser_port_arg(self): """serve parser sets port to given arg """ args = self._parse_args('serve 8888'.split()) self.assertEqual(args.PORT, '8888') def test_serve_parser_src_dir_default(self): """serve parser sets src_dir default to relative cwd """ args = self._parse_args(['serve']) self.assertEqual(args.src_dir, '.') def test_serve_parser_src_dir_value(self): """serve parser sets src_dir to arg value """ args = self._parse_args('serve -s foo'.split()) self.assertEqual(args.src_dir, 'foo') def test_serve_parser_func_do_serve(self): """serve action function is do_serve """ args = self._parse_args(['serve']) self.assertEqual(args.func, main.do_serve) class TestInfoParser(unittest.TestCase): """Unit tests for info sub-command parser. """ def _parse_args(self, *args): """Set up sub-command parser, parse args, and return result. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() main._setup_info_parser(subparsers) return parser.parse_args(*args) def test_info_parser_src_dir_default(self): """info parser sets src_dir default to relative cwd """ args = self._parse_args(['info']) self.assertEqual(args.src_dir, '.') def test_info_parser_src_dir_value(self): """info parser sets src_dir to arg value """ args = self._parse_args('info -s foo'.split()) self.assertEqual(args.src_dir, 'foo') def test_info_parser_func_do_info(self): """info action function is do_info """ args = self._parse_args(['info']) self.assertEqual(args.func, main.do_info) class TestPluginsParser(unittest.TestCase): """Unit tests for plugins sub-command parser. """ def _parse_args(self, *args): """Set up sub-command parser, parse args, and return result. """ parser_template = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(parents=[parser_template]) subparsers = parser.add_subparsers() main._setup_plugins_parser(subparsers, parser_template) return parser.parse_args(*args) def test_plugins_parser_func_list_plugins(self): """plugins list action function is plugin.list_plugins """ args = self._parse_args('plugins list'.split()) self.assertEqual(args.func, main.plugin.list_plugins) class TestFiltersParser(unittest.TestCase): """Unit tests for filters sub-command parser. """ def _parse_args(self, *args): """Set up sub-command parser, parse args, and return result. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() main._setup_filters_parser(subparsers) return parser.parse_args(*args) def test_filters_parser_func_list_filters(self): """filters list action function is _filter.list_filters """ args = self._parse_args('filters list'.split()) self.assertEqual(args.func, main._filter.list_filters) blogofile-0.8b1/blogofile/tests/test_plugin.py000066400000000000000000000153451203633570100215710ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Unit tests for blogofile plugin module. """ try: import unittest2 as unittest # For Python 2.6 except ImportError: import unittest # flake8 ignore # NOQA from mock import ( MagicMock, patch, ) from .. import plugin class TestGetByName(unittest.TestCase): """Unit tests for get_by_name function. """ def _call_fut(self, *args): """Call the fuction under test. """ return plugin.get_by_name(*args) def test_get_by_name(self): """get_by_name returns plugin with matching name """ mock_plugin = MagicMock(__dist__={'config_name': 'foo'}) with patch.object(plugin, 'iter_plugins', return_value=[mock_plugin]): p = self._call_fut('foo') self.assertEqual(p, mock_plugin) class TestPluginTools(unittest.TestCase): """Unit tests for PluginTools class. """ def _get_target_class(self): from ..plugin import PluginTools return PluginTools def _make_one(self, *args): return self._get_target_class()(*args) def test_init_module_attribute(self): """PluginTools instance has module attribute """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='./foo') tools = self._make_one(mock_plugin_module) self.assertEqual(tools.module, mock_plugin_module) def test_init_namespace_attribute(self): """PluginTools instance namespace attr is plugin module config var """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='./foo') tools = self._make_one(mock_plugin_module) self.assertEqual(tools.namespace, mock_plugin_module.config) def test_init_template_lookup_attribute(self): """PluginTools template_lookup attr is mako.lookup.TemplateLookup """ from mako.lookup import TemplateLookup mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='./foo') tools = self._make_one(mock_plugin_module) self.assertIsInstance(tools.template_lookup, TemplateLookup) def test_init_logger_name(self): """PluginTools logger has plugin name """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='./foo') tools = self._make_one(mock_plugin_module) self.assertEqual( tools.logger.name, 'blogofile.plugins.{0}'.format(mock_plugin_module.__name__)) def test_template_lookup(self): """_template_lookup calls mako.lookup.TemplateLookup w/ expected args """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='./foo') with patch.object(plugin, 'TemplateLookup') as mock_TemplateLookup: self._make_one(mock_plugin_module) mock_TemplateLookup.assert_called_once_with( directories=['_templates', './site_src/_templates'], input_encoding='utf-8', output_encoding='utf-8', encoding_errors='replace') def test_get_src_dir(self): """get_src_dir method returns expected directory name """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='/foo/bar.py') tools = self._make_one(mock_plugin_module) src_dir = tools.get_src_dir() self.assertEqual(src_dir, '/foo/site_src') def test_add_template_dir_append(self): """add_template_dir appends to template_lookup.directories by default """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='/foo/bar.py') tools = self._make_one(mock_plugin_module) tools.add_template_dir('baz') self.assertEqual( tools.template_lookup.directories, ['_templates', '/foo/site_src/_templates', 'baz']) def test_add_template_dir_prepend(self): """add_template_dir prepends to template_lookup.directories """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='/foo/bar.py') tools = self._make_one(mock_plugin_module) tools.add_template_dir('baz', append=False) self.assertEqual( tools.template_lookup.directories, ['baz', '_templates', '/foo/site_src/_templates']) def test_materialize_template(self): """materialize_template calls template.materialize_template w/ exp args """ mock_plugin_module = MagicMock( config={'name': 'foo'}, __name__='mock_plugin', __file__='/foo/bar.py') # nested contexts for Python 2.6 compatibility with patch.object(plugin, 'TemplateLookup') as mock_TL: tools = self._make_one(mock_plugin_module) with patch.object( plugin.template, 'materialize_template') as mock_mt: tools.materialize_template( 'foo.mako', 'bar.html', {'flip': 'flop'}) mock_mt.assert_called_once_with( 'foo.mako', 'bar.html', attrs={'flip': 'flop'}, caller=mock_plugin_module, lookup=mock_TL()) def test_initialize_controllers(self): """initialize_controllers calls controller module init function """ mock_controllers_module_init = MagicMock(name='mock_init') mock_controller = MagicMock( name='mock_controller', mod=MagicMock( name='mock_mod', init=mock_controllers_module_init)) mock_plugin_module = MagicMock( __name__='mock_plugin', __file__='/foo/bar.py', config=MagicMock( name='mock_config', controllers={'blog': mock_controller})) tools = self._make_one(mock_plugin_module) tools.initialize_controllers() mock_controllers_module_init.assert_called_once_with() def test_run_controllers(self): """run_controllers calls controller module run function """ mock_controllers_module_run = MagicMock(name='mock_rub') mock_controller = MagicMock( name='mock_controller', mod=MagicMock( name='mock_mod', run=mock_controllers_module_run)) mock_plugin_module = MagicMock( __name__='mock_plugin', __file__='/foo/bar.py', config=MagicMock( name='mock_config', controllers={'blog': mock_controller})) tools = self._make_one(mock_plugin_module) tools.run_controllers() mock_controllers_module_run.assert_called_once_with() blogofile-0.8b1/blogofile/tests/test_server.py000066400000000000000000000062351203633570100215770ustar00rootroot00000000000000# -*- coding: utf-8 -*- ## Mechanize isn't supported on Python 3.x ## How can I force nose to run these tests as Python 2.x? # import unittest # import tempfile # import shutil # import os # import mechanize # from .. import main # from .. import server # class TestServer(unittest.TestCase): # def setUp(self): # main.do_debug() # #Remember the current directory to preserve state # self.previous_dir = os.getcwd() # #Create a staging directory that we can build in # self.build_path = tempfile.mkdtemp() # #Change to that directory just like a user would # os.chdir(self.build_path) # #Reinitialize the configuration # main.config.init() # #Build the unit test site # main.main("init blog_unit_test") # main.main("build") # #Start the server # self.port = 42042 # self.url = "http://localhost:"+str(self.port) # self.server = server.Server(self.port) # self.server.start() # def tearDown(self): # #Revert the config overridden options # main.config.override_options = {} # #Stop the server # self.server.shutdown() # #go back to the directory we used to be in # os.chdir(self.previous_dir) # #Clean up the build directory # shutil.rmtree(self.build_path) # def testBuildAndServe(self): # br = mechanize.Browser() # #Test the index page # br.open(self.url) # #Click the title # br.follow_link(text_regex="Your Blog's Name") # assert br.geturl().strip("/") == self.url # #Go to the chronological page # br.follow_link(text_regex="chronological blog page") # assert br.geturl() == self.url + "/blog/" # #Go to page 2 # br.follow_link(text_regex="Next Page") # #Go to page 3 # br.follow_link(text_regex="Next Page") # #Go back to page 2 # br.follow_link(text_regex="Previous Page") # #Go back to page 1 # br.follow_link(text_regex="Previous Page") # assert br.geturl() == self.url + "/blog/page/1/" # #Go to a permalink page: # br.open("/blog/2009/08/29/post-seven/") # #Go to one it's categories: # br.follow_link(text_regex="general stuff") # #Go to the next category page # br.follow_link(text_regex="Next Page") # #Come back to the 1st category page # br.follow_link(text_regex="Previous Page") # assert br.geturl() == self.url + "/blog/category/general-stuff/1/" # #Go to a archive page: # br.open("/blog/archive/2009/08/1/") # #Go to the next page of this archive # br.follow_link(text_regex="Next Page") # #Come back to the 1st archive page # br.follow_link(text_regex="Previous Page") # assert br.geturl() == self.url + "/blog/archive/2009/08/1/" # def testServeSubdirectory(self): # #The site was already built in setUp # #Rebuild the site with a new config: # main.config.site_url = "http://www.yoursite.com/people/ryan" # main.do_build({},load_config=False) # br = mechanize.Browser() blogofile-0.8b1/blogofile/tests/test_util.py000066400000000000000000000043631203633570100212460ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Unit tests for blogofile util module. """ try: import unittest2 as unittest # For Python 2.6 except ImportError: import unittest # flake8 ignore # NOQA from mock import ( MagicMock, patch, ) import six from .. import util @patch.object(util.bf, 'config') class TestCreateSlug(unittest.TestCase): """Unit tests for create_slug function. """ def _call_fut(self, *args): """Call the fuction under test. """ return util.create_slug(*args) def test_create_slug_ascii(self, mock_config): """create_slug returns expected result for ASCII title """ mock_config.site = MagicMock(slugify=None, slug_unicode=None) mock_config.blog = MagicMock(slugify=None) slug = self._call_fut('Foo Bar!') self.assertEqual(slug, 'foo-bar') def test_create_slug_unidecode(self, mock_config): """create_slug returns expected ASCII result for Unicode title """ mock_config.site = MagicMock(slugify=None, slug_unicode=None) mock_config.blog = MagicMock(slugify=None) slug = self._call_fut(six.u('\u5317\u4EB0')) self.assertEqual(slug, 'bei-jing') def test_create_slug_unicode(self, mock_config): """create_slug returns expected Unicode result for Unicode title """ mock_config.site = MagicMock(slugify=None, slug_unicode=True) mock_config.blog.slugify = None slug = self._call_fut(six.u('\u5317\u4EB0')) self.assertEqual(slug, six.u('\u5317\u4EB0')) def test_create_slug_user_site_slugify(self, mock_config): """create_slug uses user-defined config.site.slugify function """ mock_config.site = MagicMock(slugify=lambda s: 'bar-foo') mock_config.blog = MagicMock(slugify=None) slug = self._call_fut('Foo Bar!') self.assertEqual(slug, 'bar-foo') def test_create_slug_user_blog_slugify(self, mock_config): """create_slug uses user-defined config.blog.slugify function """ mock_config.site = MagicMock(slugify=None) mock_config.blog = MagicMock(slugify=lambda s: 'deprecated') slug = self._call_fut('Foo Bar!') self.assertEqual(slug, 'deprecated') blogofile-0.8b1/blogofile/util.py000066400000000000000000000226741203633570100170520ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Blogofile utility functions. """ from __future__ import print_function import re import os import sys import logging import fileinput try: from urllib.parse import urlparse # For Python 2 except ImportError: from urlparse import urlparse # For Python 3; flake8 ignore # NOQA from markupsafe import Markup import six from unidecode import unidecode from .cache import bf bf.util = sys.modules['blogofile.util'] logger = logging.getLogger("blogofile.util") # Word separators and punctuation for slug creation PUNCT_RE = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') html_escape_table = { "&": "&", '"': """, "'": "'", ">": ">", "<": "<", } def html_escape(text): """Produce entities within text. """ L = [] for c in text: L.append(html_escape_table.get(c, c)) return "".join(L) def should_ignore_path(path): """See if a given path matches the ignore patterns. """ if os.path.sep == '\\': path = path.replace('\\', '/') for p in bf.config.site.compiled_file_ignore_patterns: if p.match(path): return True return False def mkdir(newdir): """works the way a good mkdir should :) - already exists, silently complete - regular file in the way, raise an exception - parent directory(ies) does not exist, make them as well """ if os.path.isdir(newdir): pass elif os.path.isfile(newdir): raise OSError("a file with the same name as the desired " "dir, '{0}', already exists.".format(newdir)) else: head, tail = os.path.split(newdir) if head and not os.path.isdir(head): mkdir(head) # print "mkdir {0}.format(repr(newdir)) if tail: os.mkdir(newdir) def url_path_helper(*parts): """ path_parts is a sequence of path parts to concatenate >>> url_path_helper("one","two","three") 'one/two/three' >>> url_path_helper(("one","two"),"three") 'one/two/three' >>> url_path_helper("one/two","three") 'one/two/three' >>> url_path_helper("one","/two/","three") 'one/two/three' >>> url_path_helper("/one","two","three") 'one/two/three' """ new_parts = [] for p in parts: if hasattr(p, "__iter__") and not isinstance(p, str): # This part is a sequence itself, recurse into it p = path_join(*p, **{'sep': "/"}) p = p.strip("/") if p in ("", "\\", "/"): continue new_parts.append(p) if len(new_parts) > 0: return "/".join(new_parts) else: return "/" def site_path_helper(*parts): """Make an absolute path on the site, appending a sequence of path parts to the site path. >>> bf.config.site.url = "http://www.blogofile.com" >>> site_path_helper("blog") '/blog' >>> bf.config.site.url = "http://www.blgofile.com/~ryan/site1" >>> site_path_helper("blog") '/~ryan/site1/blog' >>> site_path_helper("/blog") '/~ryan/site1/blog' >>> site_path_helper("blog","/category1") '/~ryan/site1/blog/category1' """ site_path = urlparse(bf.config.site.url).path path = url_path_helper(site_path, *parts) if not path.startswith("/"): path = "/" + path return path def fs_site_path_helper(*parts): """Build a path relative to the built site inside the _site dir. >>> bf.config.site.url = "http://www.blogofile.com/ryan/site1" >>> fs_site_path_helper() '' >>> fs_site_path_helper("/blog","/category","stuff") 'blog/category/stuff' """ return path_join(url_path_helper(*parts).strip("/")) #TODO: seems to have a lot in common with url_path_helper; commonize def path_join(*parts, **kwargs): """A better os.path.join. Converts (back)slashes from other platforms automatically Normally, os.path.join is great, as long as you pass each dir/file independantly, but not if you (accidentally/intentionally) put a slash in If sep is specified, use that as the seperator rather than the system default. """ if 'sep' in kwargs: sep = kwargs['sep'] else: sep = os.sep if os.sep == "\\": wrong_slash_type = "/" else: wrong_slash_type = "\\" new_parts = [] for p in parts: if hasattr(p, "__iter__") and not isinstance(p, str): #This part is a sequence itself, recurse into it p = path_join(*p) if p in ("", "\\", "/"): continue new_parts.append(p.replace(wrong_slash_type, os.sep)) return sep.join(new_parts) def recursive_file_list(directory, regex=None): """Recursively walk a directory tree and find all the files matching regex. """ if type(regex) == str: regex = re.compile(regex) for root, dirs, files in os.walk(directory): for f in files: if regex: if regex.match(f): yield os.path.join(root, f) else: yield os.path.join(root, f) def rewrite_strings_in_files(existing_string, replacement_string, paths): """Replace existing_string with replacement_string in all the files listed in paths""" for line in fileinput.input(paths, inplace=True): #inplace=True redirects sys.stdout back to the file line = line.replace(existing_string, replacement_string) sys.stdout.write(line) def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): #pragma: no cover """ Force a string to be unicode. If strings_only is True, don't convert (some) non-string-like objects. Originally copied from the Django source code, further modifications have been made. Original copyright and license: Copyright (c) Django Software Foundation and individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Django nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ if strings_only and is_protected_type(s): return s if not isinstance(s, str,): if hasattr(s, '__unicode__'): s = str(s) else: try: s = str(str(s), encoding, errors) except UnicodeEncodeError: if not isinstance(s, Exception): raise # If we get to here, the caller has passed in an Exception # subclass populated with non-ASCII data without special # handling to display as a string. We need to handle this # without raising a further exception. We do an # approximation to what the Exception's standard str() # output should be. s = ' '.join([force_unicode(arg, encoding, strings_only, errors) for arg in s]) elif not isinstance(s, str): # Note: We use .decode() here, instead of unicode(s, encoding, # errors), so that if s is a SafeString, it ends up being a # SafeUnicode at the end. s = s.decode(encoding, errors) return s def create_slug(title, delim='-'): """Create a slug from `title`, with words lowercased, and separated by `delim`. User may provide their own function to do this via `config.site.slugify`. `config.site.slug_unicode` controls whether Unicode characters are included in the slug as is, or mapped to reasonable ASCII equivalents. """ # Dispatch to user-supplied slug creation function, if one exists if bf.config.site.slugify: return bf.config.site.slugify(title) elif bf.config.blog.slugify: # For backward compatibility return bf.config.blog.slugify(title) # Get rid of any HTML entities slug = Markup(title).unescape() result = [] for word in PUNCT_RE.split(slug): if not bf.config.site.slug_unicode: result.extend(unidecode(word).split()) else: result.append(word) slug = six.text_type(delim.join(result)).lower() return slug blogofile-0.8b1/blogofile/writer.py000066400000000000000000000134461203633570100174060ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Write out the static blog to ./_site based on templates found in the current working directory. """ __author__ = "Ryan McGuire (ryan@enigmacurry.com)" import logging import os import re import shutil import tempfile from . import util from . import config from . import cache from . import filter as _filter from . import controller from . import plugin from . import template logger = logging.getLogger("blogofile.writer") class Writer(object): def __init__(self, output_dir): self.config = config # Base templates are templates (usually in ./_templates) that are only # referenced by other templates. self.base_template_dir = util.path_join(".", "_templates") self.output_dir = output_dir def __load_bf_cache(self): # Template cache object, used to transfer state to/from each template: self.bf = cache.bf self.bf.writer = self self.bf.logger = logger def write_site(self): self.__load_bf_cache() self.__setup_temp_dir() try: self.__setup_output_dir() self.__calculate_template_files() self.__init_plugins() self.__init_filters_controllers() self.__run_controllers() self.__write_files() finally: self.__delete_temp_dir() def __setup_temp_dir(self): """Create a directory for temporary data. """ self.temp_proc_dir = tempfile.mkdtemp(prefix="blogofile_") # Make sure this temp directory is added to each template lookup: for engine in self.bf.config.templates.engines.values(): try: engine.add_default_template_path(self.temp_proc_dir) except AttributeError: pass def __delete_temp_dir(self): "Cleanup and delete temporary directory" shutil.rmtree(self.temp_proc_dir) def __setup_output_dir(self): """Setup the staging directory""" if os.path.isdir(self.output_dir): # I *would* just shutil.rmtree the whole thing and recreate it, # but I want the output_dir to retain its same inode on the # filesystem to be compatible with some HTTP servers. # So this just deletes the *contents* of output_dir for f in os.listdir(self.output_dir): f = util.path_join(self.output_dir, f) try: os.remove(f) except OSError: pass try: shutil.rmtree(f) except OSError: pass util.mkdir(self.output_dir) def __calculate_template_files(self): """Build a regex for template file paths""" endings = [] for ending in self.config.templates.engines.keys(): endings.append("." + re.escape(ending) + "$") p = "(" + "|".join(endings) + ")" self.template_file_regex = re.compile(p) def __write_files(self): """Write all files for the blog to _site. Convert all templates to straight HTML. Copy other non-template files directly. """ for root, dirs, files in os.walk("."): if root.startswith("./"): root = root[2:] for d in list(dirs): # Exclude some dirs d_path = util.path_join(root, d) if util.should_ignore_path(d_path): logger.debug("Ignoring directory: " + d_path) dirs.remove(d) try: util.mkdir(util.path_join(self.output_dir, root)) except OSError: pass for t_fn in files: t_fn_path = util.path_join(root, t_fn) if util.should_ignore_path(t_fn_path): # Ignore this file. logger.debug("Ignoring file: " + t_fn_path) continue elif self.template_file_regex.search(t_fn): logger.info("Processing template: " + t_fn_path) # Process this template file html_path = self.template_file_regex.sub("", t_fn) template.materialize_template( t_fn_path, util.path_join(root, html_path)) else: # Copy this non-template file f_path = util.path_join(root, t_fn) logger.debug("Copying file: " + f_path) out_path = util.path_join(self.output_dir, f_path) if self.config.site.overwrite_warning and \ os.path.exists(out_path): logger.warn("Location is used more than once: {0}" .format(f_path)) if self.config.site.use_hard_links: # Try hardlinking first, and if that fails copy try: os.link(f_path, out_path) except Exception: shutil.copyfile(f_path, out_path) else: shutil.copyfile(f_path, out_path) def __init_plugins(self): # Run plugin defined init methods plugin.init_plugins() def __init_filters_controllers(self): # Run filter/controller defined init methods _filter.init_filters() controller.init_controllers(namespace=self.bf.config.controllers) def __run_controllers(self): """Run all the controllers in the _controllers directory. """ namespaces = [self.bf.config] for plugin in list(self.bf.config.plugins.values()): if plugin.enabled: namespaces.append(plugin) controller.run_all(namespaces) blogofile-0.8b1/converters/000077500000000000000000000000001203633570100157405ustar00rootroot00000000000000blogofile-0.8b1/converters/blogger2blogofile.py000066400000000000000000000242231203633570100217030ustar00rootroot00000000000000#!/usr/bin/env python __author__ = "Seth de l'Isle" #### Usage: ## You can generate a Blogger export file by logging into blogger, ## then going to Settings -> Basic -> Export Blog. You will get a ## file to download with the current date in the name and a .xml ## extension. Running blogger2blogofile.py in that directory, with ## the filename of the export file as the only argument will generate ## a _posts directory ready for use with Blogofile. import sys try: import feedparser except ImportError: print >> sys.stderr, """This tool requires the universal feedparser module. Depending on your tools, try: apt-get install python-feedparser or: easy_install feedparser or check out the download files at http://code.google.com/p/feedparser/downloads/list """ sys.exit() import yaml import time import os import codecs import unittest import pickle import shutil import base64 import tarfile import io import urlparse class Blogger: def __init__(self, dumpFile): self.feed = feedparser.parse(dumpFile) self.entries = [Entry(entry) for entry in self.feed.entries if self.is_post(entry)] @staticmethod def is_post(entry): # tag.term looks like 'http://schemas.google.com/blogger/2008/kind#post' return any([tag for tag in entry.tags if 'kind#post' in tag.term]) def write_posts(self, targetPath): for entry in self.entries: entry.write_post(targetPath) class Entry: def __init__(self, feedEntry): self.feedEntry = feedEntry fileNameDate = self.blogofile_date('published').replace('/', '-') self.build_header() dateNameFile = fileNameDate + self.feedEntry.title.replace('/', '-') + '.html' if self.data['draft']: self.postFile = dateNameFile else: permalink = self.data['permalink'] bloggerSlug = os.path.basename(urlparse.urlsplit(permalink)[2]) self.postFile = time.strftime("%Y-%m-%d", self.feedEntry.published_parsed) + '-' + bloggerSlug def build_header(self): allTags = self.feedEntry.tags tags = [tag.term for tag in allTags if not 'schemas.google.com' in tag.term] data = {'tags': tags, 'date': self.blogofile_date('published'), 'updated': self.blogofile_date('updated'), 'title': self.feedEntry.title, 'encoding': 'utf8', 'draft': bool('app_draft' in self.feedEntry.keys() and self.feedEntry.app_draft == 'yes'), 'author': self.feedEntry.author_detail.name} if 'link' in self.feedEntry.keys(): data['permalink'] = urlparse.urlparse(self.feedEntry['link']).path self.data = data def write_post(self, targetPath): entryPath = os.path.join(targetPath, self.postFile) if os.path.isfile(entryPath): print >> sys.stderr, "Skipping. Target file already exists: " + entryPath else: targetFile = open(entryPath, 'w') print >> targetFile, '---' print >> targetFile, self.blogofile_header() print >> targetFile, '---' targetFile.write(codecs.encode(self.feedEntry.content[0].value, 'utf8')) def blogofile_header(self): return yaml.safe_dump(self.data) def blogofile_date(self, dateType): dateStruct = {'published': self.feedEntry.published_parsed, 'updated': self.feedEntry.updated_parsed}[dateType] return time.strftime("%Y/%m/%d %H:%M:%S", dateStruct) #base64 encoded test files gzipped tarballed as a python string testData = ( """H4sIAJVWaU0AA+1ZbY/buBHOZ/0KwkG6G3RtkdT79jZoNrkURosiSK57KHrFgpZoSWdZ0klUHOfX d2Yk21pnnUva3AUFloBlWSKH8z7Dx0utk1o1rW6mSaOWZlbn8arQj77m4DB818VvEXh8/I3DcTl/ JITnCCmlwHnC893gEeNflYsTo2uNahh71Ca6yNvTkv/a+//TEcdVvb1tdGrBJa7K1jRdbKrGqrl1 Hi/33mG9gtvXdPsyj41VCyu+vV10eWHy8vbWSuihtM6T2rHenqm6vgVqpqmKM6t2rRur9qz27VlX J8roBJ751o3kgk95MOXeDyK8dPilK2fCk1MeXnJu1QEuaLuFyU2hYUVopWMat8QYkoqs2ORrbfW8 39J9LYD/8zluYc0Da+5ZcxFZcwd+udKaw5cIfbhYphYCuRYSbx3rTS1c3KXIy1ULxIVnnRe18K0U CKbCSklGEYCQWaOXOCO0bjJj6kvb3mw2s0VRpaluZnG1tlF/re15kRCBwwPHiRxX+JEf2XXVmtZO 9FJ1hbE91w0gAlzf9UTk+tyNQH7gF/gw2xqFl9y6AaUWeaxMXpW2MtX6j+/XBbwROK3RqGgprRud 5GgJB9RRSxfkkZ6l7jAv/QPzMvhtmJfhmPnoFPNgjz3zjrBuWl0s4U4i8w4aw3EthYRUisZwyBjO kTEcNIbRzRpnHIzRxpleq3aWVlVaaBJpEM+WnIf2Ki+TxygKrCJV0wLk1+WfIpLicu8xLoeppP1C LUgEcK2/A+cucu66R2p3PStF9sCzuvcw17fadLTTsfpRSXbZPoaZAc4cSIdIOiKlDCb0TtOom2qZ F9rmIgIrOl4QSumDIT3pebCQeDdZc2sqo5B/D/wHjOc5+EJ1Jqua20QbldNL967agUQKe6c9mVKR 6jyI67faZCzRrDibQ8KEZxTJoMaeDKig/VCZ7M/dQjdGx1mZ/9KRauFlhJb3OQjpE9mccgWwBR5w ORLtAiSK6MH0Hg+doVmn9zqmT7LV3QKyeTbOIj6Id3/SgCjChDHOGb6HGvB9vA2Q2d7dh0zlg7+b aqOahBWqSfW0jVWhGbC9riA6t6yFDKtSUE3AD5qGhYE41h47P6GrpzBb0mJItVS6cb1j3Wx1CzeU wzAD65JeUOAER4ETYOAsVIs8ByHc4/c464ScIuu9sTOzRuOFg7uXaQf84wN0y7dn71TR0U+HkrSp Q8w8odcHL6rl4EfhERdhYKWwe4o7p4cd60LlJU4P8TFukyJ11BiQj4B8xO+YEiuBOK4q0oVaMoMu Y1dVIsotEUZo5Fqzb11/v/U4VPgpeEqz/f37PyGEL6n/44Hn+NLD/g/C+aH/+z3Gb9T/Hfo8d4hI AX1d8AP3L93gUgYzHoRTHlFEekd9HlSQ54YpBhtDqgSuwDGLLUs7DV0Hg3QHj3JoJMqUmUyzuqje sfOfOu46S7ou6KrpKi9YnRdq+ZQVOmGmArJJ3sZd20Ifwqoly6oN+8eHhV71dPKWtV2tm7xqcHbV Neyv6oNaZfT6pw5EcfsHwElDKX1ZNUYtCv2HctHWf1Lxqqw2sFkK/PWPTAZUlyo2bKFj1bX6gm2r juG8C7Yjlql3er91v66Gogx8dusZ6x/8qIviAhUAO5NKqlKzqsaWShUM9mug94M3cHskE+lC0JXj 1eUnnoR0L2bsDapIlSCl6bW8M0aVlvkH1CU8xJqPSiQJoUA12wu26AwrK2JtVeoNzfu+TDFFM9MA xYJ6wBn7btEw+9lwJQ0yVeRpeTVBg+tmwmKQt1ZJApJdTXj/u61VvP9dgH6uJqaZxoq0MMWCB1UD FgPJRZVs4auBT/LsO8WwXbqaDJ0S1eKynW3yFRwXklzNqia18Zf9CjqmS2jx/Cm4rZTXugHR8pY7 s5/rdMLITa8m11v2Y14kS93phv3rL69e/o2dY/uVlh2Rwrgq9NLYy6SYYfV8esFevJhe/3P69vnU mfF+dtxoUMc7vWMHV0KbrMtWt/ZiO22VDXPtpwz8sV8tZ95nLbVh4tN/X7BlU62B00FK9qKfDfrJ 1yko3FxN7hN1wjKdpxm+lf6EtU2811wHTqWSexRX46+dYm2TdeuFHdphbJ/Spe0JWb+fnlb1Jk9M djVxORgbnMRW8EFb2mRUMuzHTjB5dq2QTJPrFqI/qyCOF2NjQeB/pI8RXXtwHJt8cnDQOdtU5ZnZ JQ/yajjcdui1c5ZWGHO4Tx/gFBPNljJUBUHTatXEWR8mkOh+1pALgIvneaLOWpa/zjBaNlmOIbDP fH2ygQ6+bilxFUUfiLBrwj6osktUCwmKwrPRuxwxh99dU+7fwO6qhb1AANWiBBRoQG8JxxeatK4g j8FLmA2bz6FjjVcQ36CabZMX4FEgWo3zoC+tkv0+/ZK9kIOIS8ybpBuM9BzCGMsAKA9a4ckC7KLA vLPJQOT5eNnkS5PUpFco8F8iE7902LlAncBMCnkJ2TiO+qab7T31EPBPXvInkcDrNX/yUjwJOd1/ /CSkezF5NmRH8iMixqh5Ijm+VAx06wu0PuhhzDvmyXKUYjXUkHVeYIrtwyRRsVZ6txTr25ose5/g urxP8F28TZ7tMvSusx9JBr6Yx+AzKNuwc4s8jzyurboyaVmRr3rW13D66k875e6L5MAq0cfmdrQ6 bapNS/5dvdO9+4C/FAmVElhSEwcdnKTAuvm6xnrbF7k5eNlsAIo+QoXC+1Ch6HC+E5LOeHCgg/P1 XAZ4sJvD+REPeobgowShIYPI0BtEhsagkEugkHcECo1wFXHAVfCQOgMdrPS2VibOSP8DuML9yIWT qRf5gRN60ncdSqDgTXt8hQCmw5FMnMRS5AhLkXAQajTMwsOglOPDqYQz4ms00IthIwthIgSLPASL /GOwaIR0yfDTQqFibSHsnZGnvcdO7zgsFcPHg5RT8Ko1AUQHCR0Op+fdmZNgoQNEJEdiOc5YLAca TT6SyaFjqOMjhhQcyeSEB5mc6H8HwD62ImFIB5HAq04YzZUH6WDZgN65ZBAXDeIeG8QdGcT9CtDj vcyP7eGdhB69kWkQOurRO4+gRw8BAO8YevRGIeL9Soh8vjcRqDRiOLrjQP4oLnw0RAFlqYRkQcDS GLYB/WNOFl+QvS/Y0G1AXn5F/LHrMX+EK7UEGL1BvGgEZvoBJhE/vJtE/OgAZgafxCFPgZmBuANm BvKzwczAGYOZgUuIY4CcB8deGAQExdys+vMLZGSCkNp0tN1pRJOgnoF+iGBfKI7oh7Knv6yqhGCl z6QcuiPKyHnoj7HS8DTc/WmsNAx3+R+pRFbae9wxRhrxu9aM4AbxrX75gJFG8mOMNHLGGGnkfgIj jSizRZjZomCPkUbhf4eR3hf+UXQCIxVwXk/HNRSqZUg1FBLtgJE6Qu5KaP/HCqd/Vjj9tcLdccAJ 7v0WESe4fwdVFTz4ElhV8PAueCp4ROVeHBlXgPh7/FSA2ASgir5V2JVraBXuQqhCeEcYqoDGYQyi gqXA+dHIQiDcL0R0H44q5DE/UPVT5CMlHtLR3jswVWAvkPYbprQPKQubHLKPdI8hVQFtwYDgiCkP EcFx/B5T9XYIjpD+8T8JAv9YoldhTz6iJoo/IK4P42E8jIfxMB7Gw3gY33b8ByuzBgEAKAAA""") testDataFile = io.BytesIO(base64.b64decode(testData)) testDataTar = tarfile.open("tar", mode='r:gz', fileobj=testDataFile) entryPickle = testDataTar.extractfile('feedparser-entry.pickle').read() draftPickle = testDataTar.extractfile('feedparser-draft.pickle').read() class MockBlogger(Blogger): def __init__(self): self.entries = [Entry(pickle.loads(entryPickle)), Entry(pickle.loads(draftPickle))] class TestBloggerfile(unittest.TestCase): def test_entry_header(self): entry = Entry(pickle.loads(entryPickle)) header = yaml.load(entry.blogofile_header()) assert 'barberry' in header['title'].lower() assert header['date'] == "2010/11/08 06:36:00" assert 'barberry' in header['permalink'].lower() assert 'food' in header['tags'] assert header['updated'] == "2010/12/07 06:47:27" assert header['author'] == "Seth de l'Isle" assert header['draft'] == False assert header['encoding'] == 'utf8' def test_draft_header(self): entry = Entry(pickle.loads(draftPickle)) header = yaml.load(entry.blogofile_header()) assert header['draft'] == True def test_write_posts(self): if os.path.isdir('test_data'): shutil.rmtree('test_data') os.mkdir('test_data') targetPath = os.path.join('test_data', '_posts') os.mkdir(targetPath) blogger = MockBlogger() blogger.write_posts(targetPath) assert os.path.isfile(os.path.join(targetPath, blogger.entries[0].postFile)) assert os.path.isfile(os.path.join(targetPath, blogger.entries[1].postFile)) if os.path.isdir('test_data'): shutil.rmtree('test_data') def display_error_and_usage(error): print >> sys.stderr, error print >> sys.stderr, "Usage: bloggerfile.py BloggerExportfile.xml" sys.exit() if __name__ == '__main__': if '-t' in sys.argv: try: del sys.argv[sys.argv.index('-t')] unittest.main() except AttributeError: display_error_and_usage("Error: bad test option(s): " + " ".join(sys.argv[1:])) else: if len(sys.argv) > 2: display_error_and_usage("Error: extra options after Blogger export file: " + " ".join(sys.argv[2:])) try: dumpFile = sys.argv[1] if not os.path.isfile(dumpFile): raise IOError blogger = Blogger(dumpFile) if not os.path.isdir('_posts'): os.mkdir('_posts') blogger.write_posts('_posts') except IndexError: display_error_and_usage("Error: Please specify a Blogger export file.") except IOError: display_error_and_usage("Error: Couldn't read Blogger export file: " + sys.argv[1]) blogofile-0.8b1/converters/wordpress2blogofile.py000066400000000000000000000157741203633570100223250ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """Export a Wordpress blog to Blogofile /_posts directory format This file is a part of Blogofile (http://www.blogofile.com) This file is MIT licensed, see http://blogofile.com/LICENSE.html for details. Requirements: * An existing Wordpress database hosted on MySQL (other databases probably work too, you just need to craft your own db_conn string below.) * python-mysqldb (http://mysql-python.sourceforge.net/) On Ubuntu this is easy to get: sudo apt-get install python-mysqldb * Configure the connection details below and run: python wordpress2blogofile.py If everything worked right, this will create a _posts directory with your converted posts. """ import os import re import sys import yaml import codecs import sqlalchemy as sa import sqlalchemy.orm as orm from sqlalchemy.ext.declarative import declarative_base ######################### ## Config ######################### table_prefix = "wp_" #MySQL config options. # (Other databases probably supported, but untested. Just craft your own # db_conn string according to the SQLAlchemy docs. See : http://is.gd/M1vU6j ) db_username = "your_database_username" db_password = "your_database_password" db_host = "127.0.0.1" db_port = "3306" db_database = "name_of_wordpress_database" db_encoding = "utf8" db_conn = "mysql://{db_username}:{db_password}@{db_host}:{db_port}/{db_database}?charset={db_encoding}".format(**locals()) # End Config ######################### engine = sa.create_engine(db_conn) Session = orm.scoped_session( orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)) Base = declarative_base(bind=engine) session = Session() class Post(Base): __tablename__ = table_prefix + "posts" __table_args__ = {'autoload': True} id = sa.Column("ID", sa.Integer, primary_key=True) author_id = sa.Column("post_author", sa.ForeignKey(table_prefix + 'users.ID')) author = orm.relation("User", primaryjoin="Post.author_id == User.id") term_relationship = orm.relation("TermRelationship", primaryjoin="Post.id == TermRelationship.id") def categories(self): return [r.taxonomy.term.name for r in self.term_relationship if r.taxonomy.taxonomy == "category"] def tags(self): return [r.taxonomy.term.name for r in self.term_relationship if r.taxonomy.taxonomy == "post_tag"] def __repr__(self): return u"".format( self.post_title, self.id, self.post_status) def permalink(self): site_url = get_blog_url() structure = get_blog_permalink_structure() structure = structure.replace("%year%", str(self.post_date.year)) structure = structure.replace("%monthnum%", str(self.post_date.month).zfill(2)) structure = structure.replace("%day%", str(self.post_date.day).zfill(2)) structure = structure.replace("%hour%", str(self.post_date.hour).zfill(2)) structure = structure.replace("%minute%", str(self.post_date.minute).zfill(2)) structure = structure.replace("%second%", str(self.post_date.second).zfill(2)) structure = structure.replace("%postname%", self.post_name) structure = structure.replace("%post_id%", str(self.id)) try: structure = structure.replace("%category%", self.categories()[0]) except IndexError: pass try: structure = structure.replace("%tag%", self.tags()[0]) except IndexError: pass structure = structure.replace("%author%", self.author.user_nicename) return site_url.rstrip("/") + "/" + structure.lstrip("/") class User(Base): __tablename__ = table_prefix + "users" __table_args__ = {'autoload': True} id = sa.Column("ID", sa.Integer, primary_key=True) def __repr__(self): return u"".format(self.user_nicename) class Term(Base): __tablename__ = table_prefix + "terms" __table_args__ = {'autoload': True} id = sa.Column("term_id", sa.Integer, primary_key=True) def __repr__(self): return u"".format(self.name) class TermTaxonomy(Base): __tablename__ = table_prefix + "term_taxonomy" __table_args__ = {'autoload': True} id = sa.Column('term_taxonomy_id', sa.Integer, primary_key=True) term_id = sa.Column("term_id", sa.ForeignKey(table_prefix + "terms.term_id")) term = orm.relation("Term", primaryjoin="Term.id == TermTaxonomy.term_id") class TermRelationship(Base): __tablename__ = table_prefix + "term_relationships" __table_args__ = {'autoload': True} id = sa.Column('object_id', sa.ForeignKey(table_prefix + "posts.ID"), primary_key=True) taxonomy_id = sa.Column("term_taxonomy_id", sa.ForeignKey( table_prefix + "term_taxonomy.term_id"), primary_key=True) taxonomy = orm.relation("TermTaxonomy", primaryjoin="TermTaxonomy.id == TermRelationship.taxonomy_id") class WordpressOptions(Base): __tablename__ = table_prefix + "options" __table_args__ = {'autoload': True} def get_published_posts(blog_id=0): return [p for p in session.query(Post).all() if p.post_status=="publish" and p.post_type=="post"] def get_blog_url(blog_id=0): return session.query(WordpressOptions).filter( WordpressOptions.blog_id==blog_id).\ filter(WordpressOptions.option_name=="siteurl").\ first().option_value def get_blog_permalink_structure(blog_id=0): return session.query(WordpressOptions).filter( WordpressOptions.blog_id==blog_id).\ filter(WordpressOptions.option_name=="permalink_structure").\ first().option_value if __name__ == '__main__': #Output textile files in ./_posts if os.path.isdir("_posts"): print "There's already a _posts directory here, "\ "I'm not going to overwrite it." sys.exit(1) else: os.mkdir("_posts") post_num = 1 for post in get_published_posts(): yaml_data = { "title": post.post_title, "date": post.post_date.strftime("%Y/%m/%d %H:%M:%S"), "permalink": post.permalink(), "categories": ", ".join(post.categories()), "tags": ", ".join(post.tags()), "guid": post.guid } fn = u"{0}. {1}.html".format( str(post_num).zfill(4), re.sub(r'[/!:?\-,\']', '', post.post_title.strip().lower().replace(' ', '_'))) print "writing " + fn f = codecs.open(os.path.join("_posts", fn), "w", "utf-8") f.write("---\n") f.write(yaml.safe_dump(yaml_data, default_flow_style=False, allow_unicode=True).decode("utf-8")) f.write("---\n") f.write(post.post_content.replace(u"\r\n", u"\n")) f.close() post_num += 1 blogofile-0.8b1/docs/000077500000000000000000000000001203633570100144765ustar00rootroot00000000000000blogofile-0.8b1/docs/Makefile000066400000000000000000000015041203633570100161360ustar00rootroot00000000000000# Makefile for Sphinx documentation # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build # Internal variables. ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . .PHONY: help clean html linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " linkcheck to check all external links for integrity" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." blogofile-0.8b1/docs/conf.py000066400000000000000000000171711203633570100160040ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Blogofile documentation build configuration file, created by # sphinx-quickstart on Mon Aug 17 21:05:43 2009. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.graphviz"] graphviz_output_format = "svg" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Blogofile' copyright = '2012, Blogofile Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.8' # The full version, including alpha/beta/rc tags. release = '0.8b1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Blogofiledoc' # -- Options for LaTeX output ------------------------------------------------- # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ ('index', 'Blogofile.tex', 'Blogofile Documentation', 'Blogofile Contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'blogofile', 'Blogofile Documentation', ['Blogofile Contributors'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Blogofile', 'Blogofile Documentation', 'Blogofile Contributors', 'Blogofile', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' blogofile-0.8b1/docs/config_file.rst000066400000000000000000000227271203633570100175060ustar00rootroot00000000000000.. _config-file: Configuration File ================== Blogofile looks for a file called ``_config.py`` in the root of your source directory; this is your site's main configuration file. Blogofile tries to use sensible default values for anything you don't configure explicitly in this file. Although every site must have a ``_config.py``, it can start out completely blank. ``_config.py`` is just regular `Python`_ source code. If you don't know any Python, don't worry, there's actually very little you need to change in this file to get started. .. _config-context: Context of _config.py ||||||||||||||||||||| ``_config.py`` is run within a context that is prepared by Blogofile before executing. This context includes the following objects: * **controllers** - Settings for each controller (See :ref:`controllers`). * **filters** - Settings for each filter (See :ref:`filters`). * **site** - General Settings pertaining to your site, eg: site.url. All of these are instances of the `HierarchicalCache`_ class. `HierarchicalCache`_ objects behave a bit differently than typical Python objects: accessed attributes that do not exist, do not raise an `AttributeError`_. Instead, they instantiate the non-existing attribute as a nested `HierarchicalCache`_ object. This style of configuration provides a seperate namespace for each feature of your blogofile site, and also allows for Blogofile to contain configuration settings for controllers or filters that may or may not be currently installed. For example, your ``_config.py`` might have the following setting for a photo gallery controller:: controllers.photo_gallery.albums.photos_per_page = 5 In the above example, ``controllers``, ``photo_gallery``, and ``albums``, are all instances of `HierarchicalCache`_. ``photos_per_page`` is an integer that is an attribute on the ``albums`` `HierarchicalCache`_. Because this setting is contained in a `HierarchicalCache`_ object, if the photo gallery controller is not installed, the setting will simply be ignored. .. _site-configuration: Site Configuration |||||||||||||||||| In Blogofile, the "site" corresponds with the ``_site`` directory that blogofile builds. Even if your site is primarily used as a blog, think of the "site" as the parent of the blog. The site has it's own namespace within ``_config.py`` called ``site``. .. _config-site-url: site.url ++++++++ String This is the root URL for your website. This is the URL that your blogofile site will be hosted at:: site.url = "http://www.xkcd.com" .. _config-file-ignore-patterns: site.file_ignore_patterns +++++++++++++++++++++++++ List This is a list of regular expressions that describe paths to ignore when processing your source directory. The most important one (and one you should not remove) is ``".*/_.*"`` which ignores all files and directories that start with an underscore (like ``_config.py`` and ``_posts``):: site.file_ignore_patterns = [ # All files that start with an underscore ".*/_.*", # Emacs temporary files ".*/#.*", # Emacs/Vim temporary files ".*~$", # Vim swap files ".*/\..*\.swp$", # VCS directories ".*/\.(git|hg|svn|bzr)$", # Git and Mercurial ignored files definitions ".*/.(git|hg)ignore$", # CVS dir ".*/CVS$", ] Blog Configuration |||||||||||||||||| The core of Blogofile actually does not know what a blog is. Blogofile itself just provides a runtime environment for templates, controllers and filters. A Blogofile blog is actually built by creating a blog controller (see :ref:`Controllers`.) A default implementation of a blog controller is provided with the Blogofile ``simple_blog`` template and should be sufficient for most users. All controllers in Blogofile have their own seperate namespace in ``_config.py`` under ``controllers``. For convenience, you would usually reference the blog controller like so in ``_config.py``:: blog = controllers.blog .. _config-blog-enabled: blog.enabled ++++++++++++ Boolean This turns on/off the blog feature. Blogofile is obviously geared toward sites that have blogs, but you don't *need* to have one. If this is set to True, Blogofile requires several blog specific templates to exist in the ``_templates`` directory as described in :ref:`required-templates`:: blog.enabled = True .. _config-blog-path: blog.path +++++++++ String This is the path of the blog off of the :ref:`config-site-url`. For example, if :ref:`config-site-url` is ``http://www.xkcd.com/stuff`` and blog.path is ``/blag`` your full URL to your blog will be ``http://www.xkcd.com/stuff/blag``:: blog.path = "/blag" blog.name +++++++++ String This is the name of your blog:: blog.name = "xkcd - The blag of the webcomic" blog.description ++++++++++++++++ String This is a (short) description of your blog. Many RSS readers support/expect a description for feeds:: blog.description = "A Webcomic of Romance, Sarcasm, Math, and Language" blog.timezone +++++++++++++ String This is the `timezone`_ that you normally post to your blog from:: blog.timezone = "US/Eastern" You can see all of the appropriate values by running:: python -c "import pytz, pprint; pprint.pprint(pytz.all_timezones)" | less blog.posts_per_page +++++++++++++++++++ Integer This is the number of blog posts you want to display per page:: blog.posts_per_page = 5 .. _comprehensive-config-values: blog.auto_permalink.enabled +++++++++++++++++++++++++++ Boolean This turns on automatic permalink generation. If your post does not include a permalink field, then this allows for the automatic generation of the permalink:: blog.auto_permalink.enabled = True .. _config-blog-auto-permalink: blog.auto_permalink.path ++++++++++++++++++++++++ String This is the format that automatic permalinks should take on, starting with the path after the blog domain name. eg: ``/blag/:year/:month/:day/:title`` creates a permalink like ``http://www.xkcd.com/blag/2009/08/18/post-one``:: blog.auto_permalink.path = ":blog_path/:year/:month/:day/:title" Available replaceable items in the string: * :blog_path - The root of the blog * :year - The post year * :month - The post month * :day - The post day * :title - The post title * :uuid - sha hash based on title * :filename - the filename of the post (minus extension) .. _config-disqus-enabled: blog.disqus.enabled +++++++++++++++++++ Boolean Turns on/off `Disqus`_ comment system integration:: blog.disqus.enabled = False .. _config-disqus-name: blog.disqus.name ++++++++++++++++ String The Disqus website 'short name':: blog.disqus.name = "your_disqus_name" .. _config-syntax-highlight-enabled: blog.custom_index +++++++++++++++++ Boolean When you configure :ref:`config-blog-path`, Blogofile by default writes a chronological listing of the latest blog entries at that location. With this option you can turn that behaviour off and your index.html.mako file in that same location will be your own custom template:: blog.custom_index = False .. _config-post-excerpt-enabled: blog.post_excerpts.enabled ++++++++++++++++++++++++++ Boolean Post objects have a ``.content`` attribute that contains the full content of the blog post. Some blog authors choose to only show an excerpt of the post except for on the permalink page. If you turn this feature on, post objects will also have a ``.excerpt`` attribute that contains the first :ref:`config-post-excerpt-word-length` words:: blog.post_excerpts.enabled = True If you don't use post excerpts, you can turn this off to decrease render times. .. _config-post-excerpt-word-length: blog.post_excerpts.word_length ++++++++++++++++++++++++++++++ Integer The number of words to have in post excerpts:: blog.post_excerpts.word_length = 25 .. _config-blog-pagination-dir: blog.pagination_dir +++++++++++++++++++ String The name of the directory that contains more pages of posts than can be shown on the first page. Defaults to ``page``, as in ``http://www.test.com/blog/page/4``:: blog.pagination_dir = "page" .. _config-blog-post-default-filters: blog.post_default_filters +++++++++++++++++++++++++ Dictionary This is a dictionary of file extensions to default filter chains (see :ref:`filters`) to be applied to blog posts. A default filter chain is applied to a blog post only if no filter attribute is specified in the blog post YAML header:: blog.post_default_filters = { "markdown": "syntax_highlight, markdown", "textile": "syntax_highlight, textile", "org": "syntax_highlight, org", "rst": "syntax_highlight, rst", "html": "syntax_highlight" } Build Hooks ||||||||||| .. _config-pre-build: pre_build +++++++++ Function This is a function that gets run before the _site directory is built .. _config-post-build: post_build ++++++++++ Function This is a function that gets run after the _site directory is built .. _config-post-build: build_finally +++++++++++++ Function This is a function that gets run after the _site directory is built OR whenever a fatal error occurs. You could use this function to perform a cleanup function after building, or to notify you when a build fails. .. _timezone: http://en.wikipedia.org/wiki/List_of_zoneinfo_time_zones .. _Disqus: http://www.disqus.com .. _Pygments Styles: http://pygments.org/docs/styles .. _Emacs: http://www.gnu.org/software/emacs .. _Python: http://www.python.org .. _HierarchicalCache: http://github.com/EnigmaCurry/blogofile/blob/master/blogofile/cache.py#L22 .. _AttributeError: http://docs.python.org/library/exceptions.html#exceptions.AttributeError blogofile-0.8b1/docs/controllers.rst000066400000000000000000000172571203633570100176120ustar00rootroot00000000000000.. _controllers: Controllers *********** Controllers are used when you want to create a whole chunk of your site dynamically everytime you compile your site. The best example of this is a blog. The whole purpose of a blog engine is to make it so you don't have to update 10 different things when you just want to make a post. Examples of controllers include: * A sequence of blog posts listed in reverse chronological order paginated 5 posts per page. * A blog post archiver to list blog posts in reverse chronological order listed by year and month. * A blog post categorizer to list blog posts in reverse chronological order listed by category. * An RSS/Atom feed generator for all posts, or for a single category. * A permalink page for all blog posts. All of these are pretty much a necessity for a blog engine, but none of these are included within the core of Blogofile itself. One of Blogofile's core principles is to remain light, configurable, and to make little assumption about how a user's site should behave. All of these blog specific tasks are relegated to a type of plugin system called controllers so that they can be tailored to each individual's tastes as well as leave room for entirely new types of controllers written by the user. The simple_blog sources (which you can obtain by running ``blogofile init simple_blog``) include all of these controllers in the ``_controllers`` directory. But let's look at an even simpler example for the purposes of this tutorial. .. _controller-simple-example: A Simple Controller ------------------- Suppose you wanted to create a simple photo gallery with a comments page for each photo. You don't want to have to create a new mako template for every picture you upload, so let's write a controller instead. The controller will be really simple: read all the photos in the photo directory and create a single page for each photo and allow comments on the photo using `Disqus`_. While we're at it, let's also create an index page listing all the photos with a thumbnail and the name of the image. First create the controller called ``_controllers/photo_gallery.py``:: # A stupid little photo gallery for Blogofile. # Read all the photos in the /photos directory and create a page for each along # with Disqus comments. import os from blogofile.cache import bf config = {"name" : "Photo Gallery", "description" : "A very simplistic photo gallery, used as an example", "priority" : 40.0} photos_dir = os.path.join("demo","photo_gallery") def run(): photos = read_photos() write_pages(photos) write_photo_index(photos) def read_photos(): #This could be a lot more advanced, like supporting subfolders, creating #thumbnails, and even reading the Jpeg EXIF data for better titles and such. #This is kept simple for demonstration purposes. return [p for p in os.listdir(photos_dir) if p.lower().endswith(".jpg")] def write_pages(photos): for photo in photos: bf.writer.materialize_template("photo.mako", (photos_dir,photo+".html"), {"photo":photo}) def write_photo_index(photos): bf.writer.materialize_template("photo_index.mako", (photos_dir,"index.html"), {"photos":photos}) When a controller is loaded, the first thing Blogofile looks for is a ``run()`` method to invoke. It never takes any arguments, each controller is expected to know what it's going to do of it's own accord. In this example the ``run()`` method does all the work: * It reads all the photos: ``read_photos()`` * It creates a page for each photo: ``write_pages()`` * It creates a single index page for all the photos: ``write_photo_index()`` The ``bf.writer.materialize_template`` method is provided to make it easy to pass data to a template and have it written to disk inside the ``_site`` directory. The ``write_pages()`` method references a reusable template residing in ``_templates/photo.mako``:: <%inherit file="site.mako" />
blog comments powered by Disqus The controller passes in a single variable: ``photo``, which is the filename of the photo. In a more complete photo gallery, one might pass an object that held the EXIF data. The ``write_photo_index()`` method references a reusable template residing in ``_templates/photo_index.mako``:: <%inherit file="_templates/site.mako" /> My Photos: % for photo in photos: % endfor
${photo}
The controller passes a single variable: ``photos``, which is a sequence of all the photos filenames. In a more complete photo gallery, one might pass a sequence of objects that had references to the full jpg as well as a thumbnail and EXIF data. This example is included in the `blogofile.com sources `_ and can also `be viewed live `_. Controller structure -------------------- Controllers can be single .py files inside the _controllers directory, as in the photo gallery example above, or they can be full python modules (Python modules are directories with a ``__init__.py`` file). This second method will let you split your controller among multiple files. Controllers are always disabled by default, and must be explicitly turned on in your ``_config.py``. For example, to enable the photo gallery example:: controllers.photo_gallery.enabled = True Controllers have a standardized configuration protocol. All controllers define a dictionary called ``config``. By default it contains the following values:: config = {"name" : None, "description" : None, "author" : None, "url" : None, "priority" : 50.0, "enabled" : False} These settings are as follows: * name - The human friendly name for the controller. * author - The name or group responsible for writing the controller. * description - A brief description of what the controller does. * url - The URL where the controller can be downloaded on the author's site. * priority - The default priority to determine sequence of execution. This is optional, if not provided, it will default to 50. Controllers with higher priorities get run sooner than ones with lower priorities. These are just the default settings, a controller author may provide as many configuration settings as he wants. A user can override any configuration setting in their ``_config.py``:: controllers.photo_gallery.albums.photos_per_page = 5 Controller Initialization ------------------------- Controller's have an additional optional method called ``init()``. Like the ``run()`` method, it doesn't take any arguments, it's expected that the controller knows how to initialize itself. The initialization is useful when you need to perform some preparation work before running the main controller. Typical use cases are where two controllers interact with each other and have cyclical dependencies on one another. With an initialization step, you can avoid chicken-or-the-egg problems between two controllers that require data from each other at runtime. .. _Disqus: http://www.disqus.com blogofile-0.8b1/docs/developers.rst000066400000000000000000000102411203633570100173760ustar00rootroot00000000000000.. _ForDevelopers-section: For Developers ============== If you would like to contribute to the Blogofile project, these instructions should help you get started. Patches, documentation improvements, bug reports, and feature requests are all welcome through the GitHub projects: * `Blogofile on GitHub`_ * `blogofile_blog on GitHub`_ Contributions in the form of patches or pull requests are easier to integrate and will receive priority attention. .. _Blogofile on GitHub: https://github.com/EnigmaCurry/blogofile .. _blogofile_blog on GitHub: https://github.com/EnigmaCurry/blogofile_blog Python Versions --------------- Blogofile is developed under Python_ 3.2 and tested with Python 2.6, 2.7, and 3.2. .. _Python: http://www.python.org/ .. _SettingUpADevelopmentSandbox-section: Setting Up a Development Sandbox -------------------------------- Using a Python virtualenv_ is strongly recommended to segregate Blogofile and the packages it depends on from your system Python installation. .. _virtualenv: http://www.virtualenv.org/ Create a virtualenv and activate it:: $ virtualenv blogofile-dev $ source blogofile-dev/bin/activate Grab the Blogofile core, and the blogofile_blog reference plugin repos from GitHub:: (blogofile-dev)$ cd blogofile-dev (blogofile-dev)$ git clone git://github.com/EnigmaCurry/blogofile.git (blogofile-dev)$ git clone git://github.com/EnigmaCurry/blogofile_blog.git Install the packages for development, and install the extra packages that are used to build the docs and run the test suite:: (blogofile-dev)$ pip install -e blogofile (blogofile-dev)$ pip install -e blogofile_blog (blogofile-dev)$ pip install -r blogofile/requirements/develop.txt Building Documentation ---------------------- The Blogofile docs are written with reStructuredText_ markup and built using Sphinx_. Sphinx and its dependencies are installed as part of the `development sandbox setup `_. .. _reStructuredText: http://docutils.sourceforge.net/rst.html .. _Sphinx: http://sphinx.pocoo.org/ The :file:`blogofile/docs/` directory includes a :file:`Makefile` to help build the docs:: (blogofile-dev)$ (cd blogofile/docs && make html) sphinx-build -b html -d _build/doctrees . _build/html Making output directory... Running Sphinx v1.1.3 loading pickled environment... not yet created building [html]: targets for 12 source files that are out of date updating environment: 12 added, 0 changed, 0 removed reading sources... [100%] vcs_integration looking for now-outdated files... none found pickling environment... done checking consistency... done preparing documents... done writing output... [100%] vcs_integration writing additional files... genindex search copying static files... done dumping search index... done dumping object inventory... done build succeeded. Build finished. The HTML pages are in _build/html. The output version of the docs ends up in :file:`blogofile/docs/build/html` in your development sandbox. Running Tests ------------- The test suites for Blogofile and blogofile_blog use tox_. Tox and its dependencies are installed as part of the `development sandbox setup `_. .. _tox: http://tox.testrun.org/ To run the tests under Python 2.6, 2.7, and 3.2, run :command:`tox` in the top level :file:`blogofile/` and :file:`blogofile_blog/` directories. To run tests under a single version of Python, specify the appropriate environment when running tox:: $ tox -e py27 Add new tests by modifying an existing file or adding a new one in the :file:`blogofile/tests/` and :file:`blogofile_blog/tests/` directories. Releases -------- Blogofile and blogofile_blog releases are hosted on PyPI and can be downloaded from: * http://pypi.python.org/pypi/blogofile * http://pypi.python.org/pypi/blogofile_blog Source Code ----------- The source repositories are hosted on GitHub: * https://github.com/EnigmaCurry/blogofile * https://github.com/EnigmaCurry/blogofile_blog Reporting Bugs -------------- Please report bugs through the GitHub projects: * https://github.com/EnigmaCurry/blogofile/issues * https://github.com/EnigmaCurry/blogofile_blog/issues blogofile-0.8b1/docs/file_overview.rst000066400000000000000000000124611203633570100201010ustar00rootroot00000000000000The Makeup of a Blogofile Site ****************************** Blogofile is a website `compiler`_, but instead of translating something like C++ source code into an executable program, Blogofile takes `Mako`_ templates, and other Blogofile features, and compiles HTML for viewing in a web browser. This chapter introduces the basic building blocks of a Blogofile directory containing such source code. An Example ========== The best way to understand how Blogofile works is to look at an example. Create a new directory and inside it run:: blogofile init simple_blog This command creates a very simple blog that you can use to learn how Blogofile works as well as to provide a clean base from which you can create your own Blogofile based website. For a more complete example, you can checkout the code for the same website you're reading right now, blogofile.com:: blogofile init blogofile.com This command downloads the very latest blogofile.com website source code, which requires that you have `git`_ installed on your system. If you don't have it, you can just download the `zip file`_ instead. The rest of this document will assume that you're using the simple_blog template. It is the defacto reference platform for Blogofile. Directory Structure =================== Inside the source directory are the following files (abbreviated):: |-- _config.py |-- _controllers | |-- blog | | |-- archives.py | | |-- categories.py | | |-- chronological.py | | |-- feed.py | | |-- __init__.py | | |-- permapage.py | | `-- post.py |-- _filters | |-- markdown_template.py | |-- syntax_highlight.py |-- index.html.mako |-- _posts | |-- 001 - post 1.markdown | |-- 002 - post 2.markdown `-- _templates |-- atom.mako |-- base.mako |-- chronological.mako |-- footer.mako |-- header.mako |-- head.mako |-- permapage.mako |-- post_excerpt.mako |-- post.mako |-- rss.mako `-- site.mako The basic building blocks of a Blogofile site are: * **_config.py** - Your main Blogofile configuration file. See :ref:`config-file` * **Templates** - Templates dynamically create pages on your site. ``index.html.mako`` along with the entire ``_templates`` directory are examples. See :ref:`templates` * **Posts** - Your blog posts, contained in the ``_posts`` directory. See :ref:`posts` * **Filters** - contained in the ``_filters`` directory, filters can process textual data like syntax highlighters, translators, swear word censors etc. See :ref:`filters` * **Controllers** - contained in the ``_controllers`` directory, controllers create dynamic sections of your site, like blogs. See :ref:`controllers` Any file or directory not starting with an underscore, and not ending in ".mako", are considered regular files (eg. ``css/site.css`` and ``js/site.js``). These files are copied directly to your compiled site. Building the Site ================= Now that you have an example site initialized, we can compile the source to create a functioning website. Run the following to compile the source in the current directory:: blogofile build Blogofile should run without printing anything to the screen. If this is the case, you know that it ran successfully. Inside the _site directory you have now built a complete website based on the source code in the current directory. You can now upload the contents of the _site directory to your webserver or you can test it out in the embedded webserver included with Blogofile:: blogofile serve 8080 Go to `http://localhost:8080 `_ to see the site served from the embedded webserver. You can quit the server by pressing ``Control-C``. Understanding the Build Process =============================== When the Blogofile build process is invoked, it follows this conceptual order of events: * A ``_config.py`` file is loaded with your custom settings. See :ref:`config-file`. * If the blog feature is enabled (:ref:`config-blog-enabled`), the blog posts in the ``_posts`` directory are processed and made available to templates. See :ref:`Posts`. * Filters in the ``_filters`` directory are made available to templates. See :ref:`filters`. * Files and sub-directories are recursively processed and copied over to the ``_site`` directory which becomes the compiled HTML version of the site: * If the filename ends in ``.mako``, it is considered a page template. It is rendered via Mako, then copied to the ``_site`` directory stripped of the ``.mako`` extension. See :ref:`templates`. * If the filename or directory starts with an underscore, it is ignored and not copied to the ``_site`` directory (other ignore patterns may be setup using :ref:`config-file-ignore-patterns` in ``_config.py``.) * Controllers from the ``_controllers`` directory are run to build dynamic sections of your site, for example, all of the blog features: permalinks, archives, categories etc. See :ref:`controllers`. Build Process Flowchart ----------------------- .. graphviz:: graphs/build_process.dot .. _Mako: http://www.makotemplates.org .. _zip file: http://github.com/EnigmaCurry/blogofile.com/zipball/master .. _compiler: http://en.wikipedia.org/wiki/Compiler .. _git: http://www.git-scm.org .. _Python: http://www.python.org .. _timezone: http://en.wikipedia.org/wiki/List_of_zoneinfo_time_zones blogofile-0.8b1/docs/filters.rst000066400000000000000000000113171203633570100167030ustar00rootroot00000000000000.. _filters: Filters ****************************** Filters are Blogofile's text processor plugin system. They create callable functions in templates and blog posts that can perform manipulation on a block of text. Ideas for filters: * Markup languages. * A code syntax highlighter. * A flash video plugin helper. * A swear word filter. * A foreign language translator. .. _filter-simple-example: A Simple Filter --------------- Here's a swear word filter that replaces nasty words with the word ``kitten``. The file is called ``_filters/playnice.py``:: #Replace objectionable language with kittens. #This is just an example, it's far from exhaustive. import re seven_words = re.compile( r"\Wfrak\W|\Wsmeg\W|\Wjoojooflop\W|\Wswut\W|\Wshazbot\W|\Wdoh\W|\Wgorram\W|\Wbelgium\W", re.IGNORECASE) def run(content): return seven_words.sub(" kitten ", content) This filter (once it's in your ``_filters`` directory) is available to all templates and blog posts. Filter Chains ------------- Filters can be chained together, one after the other, so that you can perform multiple text transformations on a peice of text. Suppose you have the following filters in your ``_filters`` directory: * **markdown.py** - Transforms `Markdown`_ formatted text into HTML. * **playnice.py** - The swear word filter above. * **syntax_highlight.py** - A code syntax highligher A filter chain of ``markdown, playnice, syntax_highlight`` will apply those three filters (seperated by commas) in the order given. Using Filters in a Template --------------------------- The filter module provides the method called ``run_chain``. You can use this directly in your mako templates:: The following text is filtered: ${bf.filter.run_chain('playnice, syntax_highlight', 'some shazbot text')} However, it's kind of a pain to always wrap the text you want to filter in that function call. Writing a mako ``<%def>`` block can create some nice syntactic sugar for us. Define the following in your base template so that all templates that inherit from it can benefit from it:: <%def name="filter(chain)"> ${bf.filter.run_chain(chain, capture(caller.body))} How to use it in a template:: <%self:filter chain="playnice, syntax_highlight">Belgium: Less offensive words have been created in the many languages of the galaxy, such as joojooflop, swut and Holy Zarquon's Singing Fish. All the text between the ``<%self:filter>`` start and end tags is filtered by the specified filter chain. Using Filters in a Blog Post ---------------------------- Filter chains can be applied to blog posts in the post YAML:: --- date: 2009/12/01 11:17:00 permalink: http://www.blogofile.com/whatever title: A markdown formatted test post filter: markdown, playnice --- This is a **markdown** formatted post with all the frak words filtered. Filters on blog posts are applied to the entire blog post; you cannot apply a filter to only a portion of the text like you can with templates. However, there is nothing preventing you from writing a filter that looks for special syntax in your posts and filters selectively (the syntax_highlight filter from simple_blog does exactly this). This allows for more end user customizability. If no filter is specified for your post, Blogofile looks at a config option called :ref:`config-blog-post-default-filters` which maps the file extension of the post file to a filter chain. Defaults include ``markdown`` and ``textile``. You can turn off all filters for the post, including the default ones, by specifing a filter chain of ``none``. Filter structure -------------------- Filters can be single .py files inside the _filters directory, as in the ``playnice.py`` example above, or they can be full python modules (Python modules are directories with a ``__init__.py`` file). This second method will let you split your filters among multiple files. Filters have a standardized configuration protocol. All filters define a dictionary called ``config``. By default it contains the following values:: config = {"name" : None, "description" : None, "author" : None, "url" : None} These settings are as follows: * name - The human friendly name for the controller. * author - The name or group responsible for writing the controller. * description - A brief description of what the controller does. * url - The URL where the controller can be downloaded on the authors site. These are just the default settings, a filter author may provide as many configuration settings as he wants. A user can override any configuration setting in their ``_config.py``:: filters.playnice.zealous_and_vigorous_parsing = True .. _Markdown: http://en.wikipedia.org/wiki/Markdown blogofile-0.8b1/docs/graphs/000077500000000000000000000000001203633570100157625ustar00rootroot00000000000000blogofile-0.8b1/docs/graphs/build_process.dot000066400000000000000000000054171203633570100213360ustar00rootroot00000000000000 digraph { start [shape=circle, label="Blogofile build starts"]; default_config [shape=box, label="Default configuration is loaded"] filters_read [shape=box, label="Filters are discovered in the _filters directory,\nimported, and placed on bf.config.filters cache"] controllers_read [shape=box, label="Controllers are discovered in the _controllers\ndirectory, imported, and placed on\nbf.config.controllers cache"] user_config [shape=box, label="User's _config.py is loaded"] user_pre_build [shape=box, label="User's pre_build function is run\n(if present in _config.py)"] site_dir_created [shape=box, label="The _site directory is created.\n(contents cleared out first if necessary)"] fatal_error [shape=box, label="A fatal error occurs, and the\n_site directory is incomplete"] filter_init [shape=box, label="Filter's init methods run\n(optional; sets up context before running)"] controller_init [shape=box, label="Controller's init methods run\n(optional; sets up context before running)"] controllers_run [shape=box, label="Controllers run in order of configured priority."] files_processed [shape=diamond, label="Rest of files in source\ndirectory are processed:"] files_are_ignored [shape=box, label="The file or directory name matches one of the\nsite.file_ignore_patterns and is not copied"] files_end_in_mako [shape=box, label="The filename ends in .mako; it's rendered as a\nMako template into the _site directory"] files_are_other [shape=box, label="All other files and directories are\ncopied into the _site directory"] user_post_build [shape=box, label="User's post_build function is run\n(if present in _config.py)"] site_dir_success [shape=circle, label="User's _site directory\nbuilt successfully"]; user_build_finally [shape=box, label="User's build_finally function is run\n(if present in _config.py)"] start -> default_config; default_config -> filters_read; filters_read -> controllers_read; controllers_read -> user_config; user_config -> user_pre_build; user_pre_build -> site_dir_created; site_dir_created -> filter_init; filter_init -> controller_init; controller_init -> controllers_run; controllers_run -> files_processed; files_processed -> fatal_error; files_processed -> files_are_ignored; files_processed -> files_end_in_mako; files_processed -> files_are_other; files_end_in_mako -> site_dir_success; files_are_other -> site_dir_success; site_dir_success -> user_post_build; user_post_build -> user_build_finally; site_dir_created -> fatal_error; filter_init -> fatal_error; controller_init -> fatal_error; controllers_run -> fatal_error; fatal_error -> user_build_finally; } blogofile-0.8b1/docs/images/000077500000000000000000000000001203633570100157435ustar00rootroot00000000000000blogofile-0.8b1/docs/images/wordpress_security.png000066400000000000000000000104061203633570100224310ustar00rootroot00000000000000PNG  IHDR8#sRGBbKGD pHYs  tIME7ktEXtCommentCreated with GIMPWaIDATxytTU[ukHU2@B S #`@dj@xHNVPqQ46p@ EQdd"sj5$ UtI;o;sMI rcc@ M"iD'~DHp@ @ '" Hp@ @  N D_a4鳰=~ MB //Y5R6oQѴSMT?<5w wP"~s]d-<~,f>Ƣ]lCypC4o)ڏT4<{V1mkls׃Dԏsp_ '[;c4 ޿=~7K .;yfZڔ:'{0X]Js [1n+恂 jG *f 'ȜOcZUI/#үA?3))RM~l+\ m49>kPYN1jh8+jwU$)ә3ЌYqr%ZpGޮDa=YɠsZ^ǚJ\}_3 `eihbD~ /IlqRsIc3Kb WƜ tjsZJ}O22(6ɛ2SoY) ܞ[;8ώ!6\Wtj.kxkZwu^YDz1!a'p r4MP3Mxɖub%ݙ⹹$=%Dƶ+]q"F(o-|oc`#h <^ %qyB>ȫ9ɻl,7K*i SsPY*nzyfpHm@mhjߢ!Vb$ѕmEޔbǞc-3/_ |H’)o_muڱ 뷁oҩ3{xF& abggo_6$eU4$I f0}DZNKA>XG⻳i(Y(6lfI]'/4* =DcBG X@:r%H?^e`m![PV8{H7^.V/\7ݺ޾l!Їe#gdOYP[^ŝ_H:?dK v_3W> VP{n&s{ap(Wez9ŀG](|凅<Jṱ6Ncn-,HbңwwM}I 7L΄,xz&+;)~#6J$KZlQ[cɘ#uP&䥤θndq/̓8.֕`=ŹxY4\kxYeI*C=,x1n ɚr|vU }QSZG5|2#!G+c/EX;:5677c<]CAvU9|z[7.:Zܯ(hu!I4w+4jmP7T 3WzY,`fa1fzǘƇMG\pનg [IIU˝;m470}rwI-QJXe%0Z%v[n%7L9خ/wQ)kxJHƜ*f(zAJdd%p), G[9r4lz 5F=;!z)?-kVFEq |VVQQB ]Z(EUa@Af|ڥ4M 6N®$]DYDYo)O)2wT`@ f2ksMulW9_i0= M!E]b9&d[J,ٮ:ifr(;)53havh~( }$H0Wh|b/g*=2X)l_\9zAWRqh3Ryr}^N]4i9 SuDG$c!+v8iO _PԶq*cRvɨǴ{:ȻPfkϾ*rw57`v]ixT U,s]K24O<7T4|)2wDt+ltD[略SZjӠBE` !q2rPL2FŏC mɽKz걷Y S9F쬸='G#sxXAsX~c|D3x:$ .T]#9TtuU9L0eݮC "oI"d%8UXg+V ]ɛ݇8}[Bkx)2;{{q))Mdѣ\ڑ]>-W |Lka܍9 n4ОN~BV?ʩnO% I<5֢g*_CҀS[ ~B}kQ7-ZJG=w6tt]30aX;HSԖ0qa6A`09+[<:7ִFhnbQo#<Ϡl2vDu7:= ; }4uXN`MK Hw[zl0wBo=כk\TnX)k?o9V,\v>1̖o E٘{M W2x (!Y]t'"2[9uz&Sm̾+-g~4:0}| m%W ՉǯQڷOwZe2T'x(3cGe1ܺyjl+Zv38)C wQ&Zw,z qnBI:;SPnhO}X&350/u>zk؅I*'|[_ez 3%kﻄW_5Hӻ3=[a;xrۖNOƜ뺲pM{DwMabX g)^SQR4!}7|znI\\ 7Ool1}Wv"ȤQ]?ӓ/'gG90d薇i(Z x*8P`#'_8-dy0}R9un_E &Bo N ﻂ_#DHp@ @ '" Hp@p,R)5@Lp'U D_k6Y) IENDB`blogofile-0.8b1/docs/index.rst000066400000000000000000000013611203633570100163400ustar00rootroot00000000000000.. Blogofile documentation master file, created by sphinx-quickstart on Mon Aug 17 21:05:43 2009. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Blogofile Documentation ####################### .. note:: This documents the 0.8 version of Blogofile_, the current stable release. There are also `docs for 0.7.1`_. 0.8 is not guaranteed to be backward compatible with 0.7.1. .. toctree:: :maxdepth: 2 intro quick_setup file_overview config_file templates controllers posts filters vcs_integration migrating_blogs developers .. _Blogofile: http://www.blogofile.com/ .. _docs for 0.7.1: http://blogofile.readthedocs.org/en/0.7.1docs/ blogofile-0.8b1/docs/intro.rst000066400000000000000000000071071203633570100163700ustar00rootroot00000000000000Introduction ************ * Definition: **Blogophile** (n): A person who is fond of or obsessed with blogs or blogging. * Definition: **Blogofile** (n): A static website compiler and blog engine, written and extended in `Python`_. Welcome to Blogofile ==================== Blogofile is a static website compiler, primarily (though not exclusively) designed to be a simple blogging engine. It requires no database and no special hosting environment. You customize a set of templates with `Mako `_, create posts in a markup language of your choice (see :ref:`post-content`) and Blogofile renders your entire website as static HTML and Atom/RSS feeds which you can then upload to any old web server you like. Why you should consider Blogofile ================================= * Blogofile is **free open-source** software, released under a non-enforced `MIT License`_. * Blogofile sites are **fast**, the web server doesn't need to do any database lookups nor any template rendering. * Blogofile sites are **inexpensive** to host. Any web server can host a blogofile blog. * Blogofile is **modern**, supporting all the common blogging features: * Categories and Tags. * Comments, Trackbacks, and Social Networking mentions (Twitter, Reddit, FriendFeed etc), all with effective spam filtering using `Disqus `_. * RSS and Atom feeds, one for all your posts, as well as one per category. Easily create additional feeds for custom content. * Syntax highlighting for source code listings. * Ability to create or share your own plugins in your own userspace (see :ref:`Filters ` and :ref:`Controllers `) * Blogofile is **secure**, there's nothing executable on the server `to exploit `_. * Blogofile works **offline**, with a built-in web server you can work on your site from anywhere. * Blogofile is **file based**, so you can edit it with your favorite text editor, not some crappy web interface. * Seamless :ref:`Git Integration `. Publish to your blog with a simple ``git push``. This also makes **backups** dirt simple. .. _MIT License: http://www.blogofile.com/LICENSE.html .. _install-blogofile: Installing Blogofile ==================== Python Versions --------------- Blogofile is being developed under Python_ 3.2 and tested with Python 2.6, 2.7, and 3.2. .. _Python: http://www.python.org/ Prerequisites ------------- Apart from one of the Python versions listed above, you will also need one of the Python packaging libraries that supports :mod:`setuptools` style entry points; i.e. distribute_ or setuptools_. .. _distribute: http://pypi.python.org/pypi/distribute .. _setuptools: http://pypi.python.org/pypi/setuptools Using a Python virtualenv_ is strongly recommended to segregate Blogofile and the packages it depends on from your system Python installation. It has the added advantage of taking are of the packaging library issue for you too. .. _virtualenv: http://www.virtualenv.org/ Installing the Stable Release ----------------------------- Download and install Blogofile and the blogofile_blog plugin with:: easy_install Blogofile easy_install blogofile_blog or use your favourite :command:`pip` or :command:`python setup.py` incantation. The releases are hosted on PyPI and can be downloaded from http://pypi.python.org/pypi/blogofile and http://pypi.python.org/pypi/blogofile_blog. If you prefer to use the development repos from Github, or want to hack on Blogofile and blogofile_blog, please see the :ref:`ForDevelopers-section` section. blogofile-0.8b1/docs/migrating_blogs.rst000066400000000000000000000051431203633570100204020ustar00rootroot00000000000000Migrating Existing Blogs to Blogofile ===================================== Unless you're starting a brand new blog from scratch, you're probably going to want to migrate an existing blog to Blogofile. When migrating, you have to consider several things: * Migrating existing blog posts. * Migrating existing blog comments. * Migrating permalinks and other search engine indexed URLs. Wordpress --------- Comments ++++++++ Before you bring your Wordpress blog offline, install the `Disqus wordpress plugin`_. With this plugin, you can export all your comments from your wordpress database offsite into your Disqus account. In your blogofile config file, set the :ref:`config-disqus-enabled` and :ref:`config-disqus-name` settings appropriately. Posts +++++ Download the converter script: * `wordpress2blogofile.py`_ Install SQL Alchemy:: easy_install sqlalchemy If your database is MySQL based, you'll also need to download `MySQLdb`_, which you can apt-get on Ubuntu:: sudo apt-get install python-mysqldb If you're using some other database, install the appropriate `DBAPI`_. Edit ``wordpress_schema.py``: * ``table_prefix`` should be the same as you setup in wordpress, (or blank "" if none) * ``db_conn`` point to your database server. The example is for a MySQL hosted database, but see the `SQL Alchemy docs`_ if you're using something else. In a clean directory run the export script:: python wordpress2blogofile.py If everything worked, you should now have a ``_posts`` directory containing valid Blogofile format posts which you can copy to your blogofile directory. Permalinks ++++++++++ You're probably going to want to retain the exact same permalinks that your wordpress blog had. If you've been blogging for long, Google has inevitably indexed your blog posts and people may have linked to you; you don't want to change the permalink URLs for your posts. The converter script should transfer over the permalinks directly into the :ref:`post-yaml` of the blog post. You may also want to configure the :ref:`config-blog-auto-permalink` setting in your configuration file to create the same style of permalink that you're using in wordpress. Moveable Type ------------- to be written. .. _Disqus wordpress plugin: http://wordpress.org/extend/plugins/disqus-comment-system .. _wordpress2blogofile.py: http://github.com/EnigmaCurry/blogofile/raw/master/converters/wordpress2blogofile.py .. _MySQLdb: http://sourceforge.net/projects/mysql-python/ .. _DBAPI: http://www.sqlalchemy.org/docs/05/dbengine.html#supported-dbapis .. _SQL Alchemy docs: http://www.sqlalchemy.org/docs/05/dbengine.html#create-engine-url-arguments blogofile-0.8b1/docs/posts.rst000066400000000000000000000113321203633570100164000ustar00rootroot00000000000000.. _posts: Posts ***** Posts are interpreted by the blog controller that you get when you instantiate the simple_blog; they have no particular meaning to the core runtime of Blogofile. If you wanted, you could reimplement the blog controller yourself and use whatever post formatting you wished. It's expected that most users will just use the default blog controller, so this Post documentation is here for convenience. Blog posts go inside the **_posts** directory. Without the blog controller enabled, the **_posts** directory is ignored because it starts with an underscore. Each post is a seperate file and you can name the files whatever you want, but it's suggested to prefix your posts with a number like ``0001``, ``0002`` etc. so that when you look at the files in a directory they will be naturally ordered sequentially. It's important to realize that this order is not the same order that the blog controller uses in chronlogical listings. Instead it sorts the posts based on the date field described below. An Example Post --------------- Here's an example post:: --- categories: Category One, Category Two date: 2009/08/18 13:09:00 permalink: http://www.blogofile.com/2009/08/18/first-post title: First Post --- This is the first post The post is divided into two parts, the YAML header and the post content. You can see more `examples of Blogofile posts `_ on the project site. .. _post-yaml: YAML Header ----------- The `YAML`_ portion is between the two ``---`` lines, and it describes all of the metadata for the post. You can define as many fields as you like, but there are some names that are reserved for general purpose use: * **title** A one-line free-form title for the post. * **date** The date that the post was originally created. (year/month/day hour:minute:second). * **updated** The date that the post was last updated. (year/month/day hour:minute:second). * **categories** A list of categories that the post pertains to, each seperated by commas. You don't have to configure the categories beforehand, you are defining them right here. * **tags** A list of tags that the post pertains to, each seperated by commas. * **permalink** The full permanent URL for this post. This is optional, one will be generated automatically if left blank. (see :ref:`config-blog-auto-permalink`) * **filters** The filter chain to run on the post content. (see :ref:`filters`) * **filter** A synonym for filters. (see :ref:`filters`) * **guid** A unique hash for the post, if not provided it is assumed that the permalink is the guid. * **author** The name of the author of the post. * **draft** If 'true' or 'True', the post is considered to be only a draft and not to be published. A permalink will be generated for the post, but the post will not show up in indexes or RSS feeds. You would have to know the full permalink to ever see the page. * **source** Reserved internally. * **yaml** Reserved internally. * **content** Reserved internally. * **filename** Reserved internally. This list is also defined in the blogofile source code under ``blogofile.post.reserved_field_names`` and can be accessed as a dictionary at runtime. Note that `YAML syntax rules`_ apply to header fields. For instance, if you want to include characters like colon (:), apostrophe or single quote('), etc. in your post title, you must enclose the entire title in double quotes ("). .. _YAML syntax rules: http://pyyaml.org/wiki/PyYAMLDocumentation#YAMLsyntax .. _post-content: Post Content ------------ The post content is written using a markup language, currently Blogofile supports several to choose from: * `Markdown`_ (files end in .markdown) * `Textile`_ (files end in .textile) * `reStructuredText`_ (files end in .rst) * or plain old HTML (files end in .html by convention, but if it's not one of the above, posts default to HTML anyway) Adding your own markup formats is easy, you just implement it as a filter (see :ref:`Filters`) The content of the post goes directly after the YAML portion and uses whatever markup language is indicated by the file extension of the post file. Referencing posts in templates ------------------------------ All the posts are stored in a cache object called ``bf``. This object is exposed to all templates and you can reference it directly with ``${bf.config.blog.posts}``. They are ordered sequentially by date. See :ref:`adding-blogofile-features-to-our-templates` for an example. .. _YAML: http://en.wikipedia.org/wiki/YAML .. _Markdown: http://en.wikipedia.org/wiki/Markdown .. _Textile: http://en.wikipedia.org/wiki/Textile_(markup_language) .. _Org Mode: http://orgmode.org/ .. _reStructuredText: http://docutils.sourceforge.net/rst.html blogofile-0.8b1/docs/quick_setup.rst000066400000000000000000000027671203633570100176000ustar00rootroot00000000000000A Quick Tutorial **************** .. note:: This documents the 0.8 development version of Blogofile_ (also known as the *plugins* branch). There are also `docs for 0.7.1`_, the current stable release. .. _Blogofile: http://blogofile.com/ .. _docs for 0.7.1: http://blogofile.readthedocs.org/en/0.7.1docs/ Ok, if you're impatient, this is the short *short* [#f1]_ version of getting setup with blogofile. * Install Blogofile and the blogofile_blog plugin, (see :ref:`install-blogofile`). Use a Python virtualenv_ or :command:`sudo` as you wish. :: git clone git://github.com/EnigmaCurry/blogofile.git git clone git://github.com/EnigmaCurry/blogofile_blog.git cd blogofile python setup.py install cd ../blogofile_blog python setup.py install .. _virtualenv: http://www.virtualenv.org/ * Initialize a blog site in a directory call :file:`mysite`:: blogofile init mysite blog * Build the site:: blogofile build -s mysite * Serve the site:: blogofile serve -s mysite * Open your web browser to http://localhost:8080 to see the rendered site. * Explore the :command:`blogofile` commands with :command:`blogofile help`. * Create some post files in the :file:`_posts` directory (see :ref:`posts`) The next chapters explain this process in more detail. .. rubric:: Footnotes .. [#f1] * **Priest**: Do you? * **Vespa**: Yes. * **Priest**: Do *you*? * **Lone Star**: I do. * **Priest**: Good! Fine! You're married! Kiss Her! .. _git: http://www.git-scm.org blogofile-0.8b1/docs/templates.rst000066400000000000000000000201371203633570100172310ustar00rootroot00000000000000.. _templates: Templates ********* Templates are at the very heart of Blogofile; they control every aspect of how the site is structured. Blogofile uses the `Mako`_ templating engine which has an active community and `great documentation`_. Blogofile doesn't try to limit what you can do with your templates, you've got the full power of Mako so go ahead and use it. Blogofile makes a distinction between two basic kinds of templates: * **Page** templates * **Reusable** templates Page templates represent a single unique page (or URL) on your site. These are files somwhere in your source directory that end in ``.mako`` and never reside in a directory starting with an underscore. Page templates are rendered to HTML and copied to the ``_site`` directory in the same location where they reside in the source directory. Examples: an index page, a contact page, or an "about us" page. Reusable templates are contained in the _templates directory. These are features that you want to include on many pages. Examples: headers, footers, sidebars, blog post layouts etc. Reusable templates do not represent any particular page (or URL) but are rather `inherrited`_ or `included`_ inside other templates or :ref:`controllers` and usually reused on many diverse pages. A Simple Example Using Just Mako -------------------------------- It would be redundant to describe all the things you can do with Mako when `great documentation`_ already exists, but a few simple examples of templates are in order. The first thing a website needs is an index or 'home' page. Here's how you create one in blogofile: In the root of your source directory create a file called ``index.html.mako``:: <%inherit file="_templates/site.mako" /> This is the index page contents. Well, there's not much in there, but that's by design. To effectively use Mako, you're going to want to use `inheritance `_ to allow you to abstract out the things that you want to repeat on every page (headers, footers, sidebars etc) from the things that don't repeat: the content of the page. The ``index.html.mako`` inherits from a **reusable template** called ``site.mako``. Create a file called ``_templates/site.mako``:: ${self.header()}
${next.body()}
${self.footer()} <%def name="header()"> This is a header you want on every page
<%def name="footer()">
This is a footer you want on every page At the bottom of ``site.mako`` there are two ``<%def>`` blocks: header and footer. Think of these ``<%def>`` blocks as functions that can be reused multiple times. Each of these defs are referenced inside the ```` tag above (eg. ``${self.header()}``). Referencing the def block simply deposits the contents of the ```` wherever it's referenced. You could simply write the header and footer inline in the HTML and you'd still get the same effect of having them appear on every page that inherits from ``site.mako``, however if you create them as ``<%def>`` blocks, you can redefine these blocks on child templates so that a different header or footer can appear on some pages while retaining the rest of the look and feel of the ``site.mako`` template. One special reference is also made to ``${next.body()}``. This deposits the contents of any child templates that inherit from this template. In our example, ``index.html.mako`` inherits from ``site.mako``, so the text ``this is the index page contents`` is deposited inside the ``
`` in the resulting HTML file which looks something like this:: This is a header you want on every page
This is the index page contents.

This is a footer you want on every page .. _adding-blogofile-features-to-our-templates: Adding Blogofile Features To Our Templates ------------------------------------------ In the last section we introduced a simple template called ``index.html.mako``. This template is the home page of our site, and so far only includes regular mako functionality. Now let's introduce some Blogofile action! Let's say we want to include on our home page a list of the 5 most recent posts from our blog. As long as :ref:`config-blog-enabled` is turned on, each template can get access to our blog posts through a cache object called ``bf``. We can modify our ``index.html.mako`` to get the list of recent posts:: <%inherit file="_templates/site.mako" /> Here's the five most recent posts from the blog: If you're familiar with for-loops in Python, this should look somewhat similar. We create an unordered list tag and inside that list we iterate over a special Blogofile object containing all of our posts. We limit ourselves to the first 5 posts by slicing the list of posts from 0 to 5. Each post contains various metadata (see :ref:`posts`) about the post. In this example we are interested in two things: the relative URL to the permalinked post as well as the title of the post. We create the anchor containing the relative URL ``${post.path}`` and we name the anchor the same as the post ``${post.title}``. The rendered HTML file will now look something like this:: This is a header you want on every page
Here's the five most recent posts from the blog:

This is a footer you want on every page .. _required-templates: Template Environment -------------------- In the last section we introduced a special Blogofile object called ``bf``. This object is a gateway to all things related to Blogofile and is provided to all your templates. You can also import it into your :ref:`Controllers` and :ref:`Filters`:: import blogofile_bf as bf Blogofile modules +++++++++++++++++ ``bf`` holds all of the core Blogofile modules, for example: * ``bf.util`` * ``bf.config`` * ``bf.writer`` Controller configuration ++++++++++++++++++++++++ ``bf`` holds all the controller configuration, for example: * ``bf.controllers.blog.enabled`` * ``bf.controllers.blog.path`` Filter configuration ++++++++++++++++++++ ``bf`` holds all the filter configuration, for example: * ``bf.filters.syntax_highlight.enabled`` * ``bf.filters.syntax_highlight.style`` Template context ++++++++++++++++ When a template is being rendered, it's sometimes useful to be able to maintain a context available throughout the time that a given template is being rendered. If, for example, you are rendering a template called ``my_cool_template.mako`` which inherits from ``site.mako`` and includes ``sidebar.mako``, a single context will be maintained that can be accessed from all three of those templates. ``bf.template_context`` is a `HierarchicalCache`_ object and is available inside any template and you can put whatever data you want on it. The one peice of information that is included by default is ``bf.template_context.template_name`` which records the original template requested to be rendered. In the above example, this would be ``my_cool_template.mako``. .. _Mako: http://www.makotemplates.org .. _great documentation: http://www.makotemplates.org/docs/ .. _inherrited: http://www.makotemplates.org/docs/inheritance.html .. _included: http://www.makotemplates.org/docs/syntax.html#syntax_tags_include .. _Mako syntax: http://www.makotemplates.org/docs/syntax.html#syntax_expression .. _HierarchicalCache: http://github.com/EnigmaCurry/blogofile/blob/master/blogofile/cache.py#L22 blogofile-0.8b1/docs/vcs_integration.rst000066400000000000000000000046401203633570100204320ustar00rootroot00000000000000.. _vcs-integration: Integration with Version Control ******************************** You Want Version Control ------------------------------- You might not know it yet, but you want your blog under a `Version Control System `_ (VCS). Consider the benefits: * Regular and complete backups occur whenever you push your changes to another server. * The ability to bring back any version of your site (or any single page) from history. * The ability to work from any computer, without getting worried if you're working on the latest version or not. * Automatic Deployment. Automatic what? Even if you're a veteran to VCS, you may not realize that a VCS can do a lot more for you than just provide a place for you to dump your files. You can have your favorite VCS build and deploy your Blogofile based site for you everytime you commit new changes. So why are you procrastinating? Get `git`_. Automatic Deployment in Git --------------------------- You need to have the origin server (the place that you 'git push' to) be the same server that hosts your website for this example to work. [#f1]_ On the server, checkout the project:: git clone /path/to/your_repo.git /path/to/checkout_place Create a new ``post-recieve`` hook in your git repo by creating the file ``/path/to/your_repo.git/hooks/post-receive``:: #!/bin/sh #Rebuild the blog unset GIT_DIR cd /path/to/checkout_place git pull blogofile build Configure your webserver to host your website out of ``/path/to/checkout_place/_site``. Now whenever you ``git push`` to your webhost, your webserver should get automatically rebuilt. If Blogofile outputs any errors, you'll see them on your screen. Other VCS solutions ------------------- Most VCS should have support for a post recieve hook. If you create something cool in your own VCS of choice, let the `blogofile discussion group `_ know and we'll add it to this document. OJ wrote up how to do `something similar with mercurial `_. .. rubric:: Footnotes .. [#f1] If you deploy to a different server than the one hosting your git repository, you could just craft your own rsync or FTP command and put it at the bottom of the post-receive hook to deploy somewhere else. But that's beyond the scope of this document. .. _git: http://www.git-scm.com blogofile-0.8b1/requirements/000077500000000000000000000000001203633570100162715ustar00rootroot00000000000000blogofile-0.8b1/requirements/develop.txt000066400000000000000000000002671203633570100204750ustar00rootroot00000000000000# Packages required to hack on blogofile -r production.txt Sphinx==1.1.3 mock==1.0b1 py==1.4.9 pytest==2.2.4 tox==1.4.2 virtualenv==1.7.2 # unittest2==0.5.1 # For Python 2.6 only blogofile-0.8b1/requirements/production.txt000066400000000000000000000003111203633570100212130ustar00rootroot00000000000000# Python packages required to run blogofile Jinja2==2.6 Mako==0.7.2 Markdown==2.2.0 MarkupSafe==0.15 PyYAML==3.10 Pygments==1.5 docutils==0.9.1 pytz==2012d six==1.1.0 textile==2.1.4 Unidecode==0.04.9 blogofile-0.8b1/setup.cfg000066400000000000000000000001271203633570100153670ustar00rootroot00000000000000[nosetests] detailed-errors=1 with-doctest=1 #with-coverage=1 #cover-package=blogofile blogofile-0.8b1/setup.py000066400000000000000000000041411203633570100152600ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys from setuptools import setup import blogofile py_version = sys.version_info[:2] PY3 = py_version[0] == 3 PY26 = py_version == (2, 6) if PY3: if py_version < (3, 2): raise RuntimeError( 'On Python 3, Blogofile requires Python 3.2 or later') else: if py_version < (2, 6): raise RuntimeError( 'On Python 2, Blogofile requires Python 2.6 or later') with open('README.rst', 'rt') as readme: long_description = readme.read() with open('CHANGES.txt', 'rt') as changes: long_description += '\n\n' + changes.read() with open('requirements/production.txt', 'rt') as reqs: requirements = reqs.read() install_requires = [line for line in requirements.split('\n') if line and not line.startswith('#')] # TODO: There has to be a better way... dependency_links = [] if PY3: textile_index = [i for i, item in enumerate(install_requires) if item.startswith('textile')][0] install_requires[textile_index] += '-py3k' dependency_links = [ 'http://github.com/EnigmaCurry/textile-py3k/tarball/2.1.4' '#egg=textile-2.1.4-py3k'] if PY26: install_requires.append('argparse') classifiers = [ 'Programming Language :: Python :: {0}'.format(py_version) for py_version in ['2', '2.6', '2.7', '3', '3.2']] classifiers.extend([ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: Implementation :: CPython', 'Environment :: Console', 'Natural Language :: English', ]) setup( name="Blogofile", version=blogofile.__version__, description="A static website compiler and blog engine", long_description=long_description, author=blogofile.__author__, author_email="blogofile-discuss@googlegroups.com", url="http://www.blogofile.com", license="MIT", classifiers=classifiers, packages=["blogofile"], install_requires=install_requires, dependency_links=dependency_links, zip_safe=False, entry_points={ 'console_scripts': ['blogofile = blogofile.main:main']}, ) blogofile-0.8b1/tox.ini000066400000000000000000000002761203633570100150660ustar00rootroot00000000000000[tox] envlist = py32, py27, py26 [common] deps = discover mock [testenv] deps = {[common]deps} commands = discover [testenv:py26] deps = {[common]deps} unittest2