couchapp/0000755000372000037200000000000012276056621010741 5ustar gfagfacouchapp/config.py0000644000372000037200000001127712276056621012570 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import os from .client import Database from .errors import AppError from . import util class Config(object): """ main object to read configuration from ~/.couchapp.conf or .couchapprc/couchapp.json in the couchapp folder. """ DEFAULT_SERVER_URI = "http://127.0.0.1:5984" DEFAULTS = dict( env={}, extensions=[], hooks={} ) def __init__(self): self.rc_path = util.rcpath() self.global_conf = self.load(self.rc_path, self.DEFAULTS) self.local_conf = {} self.app_dir = util.findcouchapp(os.getcwd()) if self.app_dir: self.local_conf = self.load_local(self.app_dir) self.conf = self.global_conf.copy() self.conf.update(self.local_conf) def load(self, path, default=None): """ load config """ conf = default if isinstance(path, basestring): paths = [path] else: paths = path for p in paths: if os.path.isfile(p): try: new_conf = util.read_json(p, use_environment=True, raise_on_error=True) except ValueError: raise AppError("Error while reading %s" % p) conf.update(new_conf) return conf def load_local(self, app_path): """ load local config """ paths = [] for fname in ['couchapp.json', '.couchapprc']: paths.append(os.path.join(app_path, fname)) return self.load(paths, {}) def update(self, path): self.conf = self.global_conf.copy() self.local_conf.update(self.load_local(path)) self.conf.update(self.local_conf) def get(self, key, default=None): try: return getattr(self, key) except AttributeError: pass return self.conf[key] def __getitem__(self, key): try: return getattr(self, key) except AttributeError: pass return self.conf[key] def __getattr__(self, key): try: getattr(super(Config, self), key) except AttributeError: if key in self.conf: return self.conf[key] raise def __contains__(self, key): return (key in self.conf) def __iter__(self): for k in list(self.conf.keys()): yield self[k] @property def extensions(self): """ load extensions from conf """ extensions_list = [] if not "extensions" in self.conf: return extensions_list for uri in self.conf.get('extensions'): script = util.load_py(uri, self) extensions_list.append(script) return extensions_list @property def hooks(self): hooks = {} if not "hooks" in self.conf: return hooks for hooktype, uris in self.conf.get("hooks").items(): scripts = [] for uri in uris: scripts.append(util.hook_uri(uri, self)) hooks[hooktype] = scripts return hooks # TODO: add oauth management def get_dbs(self, db_string=None): db_string = db_string or '' if db_string.startswith("http://") or \ db_string.startswith("https://") or \ db_string.startswith("desktopcouch://"): dburls = db_string else: env = self.conf.get('env', {}) if not db_string: # get default db if it exists if 'default' in env: dburls = env['default']['db'] else: raise AppError("database isn't specified") else: dburls = "%s/%s" % (self.DEFAULT_SERVER_URI, db_string) if db_string in env: dburls = env[db_string].get('db', dburls) if isinstance(dburls, basestring): dburls = [dburls] use_proxy = os.environ.get("http_proxy", "") != "" or \ os.environ.get("https_proxy", "") != "" return [Database(dburl, use_proxy=use_proxy) for dburl in dburls] def get_app_name(self, dbstring=None, default=None): env = self.conf.get('env', {}) if not dbstring.startswith("http://"): if dbstring in env: return env[dbstring].get('name', default) elif 'default' in env: return env['default'].get('name', default) elif not dbstring and 'default' in env: return env['default'].get('name', default) return default couchapp/errors.py0000644000372000037200000000242012276056621012625 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from restkit import ResourceError class AppError(Exception): """ raised when a application error appear """ class MacroError(Exception): """ raised for macro errors""" class VendorError(Exception): """ vendor error """ class ResourceNotFound(ResourceError): """ raised when a resource not found on CouchDB""" class ResourceConflict(ResourceError): """ raised when a conflict occured""" class PreconditionFailed(ResourceError): """ precondition failed error """ class RequestFailed(Exception): """ raised when an http error occurs""" class Unauthorized(Exception): """ raised when not authorized to access to CouchDB""" class CommandLineError(Exception): """ error when a bad command line is passed""" class BulkSaveError(Exception): """ error raised when therer are conflicts in bulk save""" def ___init__(self, docs, errors): Exception.__init__(self) self.docs = docs self.errors = errors class ScriptError(Exception): """ exception raised in external script""" class InvalidAttachment(Exception): """ raised when attachment is invalid (bad size, ct, ..)""" couchapp/macros.py0000644000372000037200000001077712276056621012613 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import glob from hashlib import md5 import logging import os import re from couchapp.errors import MacroError from couchapp import util logger = logging.getLogger(__name__) def package_shows(doc, funcs, app_dir, objs): apply_lib(doc, funcs, app_dir, objs) def package_views(doc, views, app_dir, objs): for view, funcs in views.iteritems(): if hasattr(funcs, "items"): apply_lib(doc, funcs, app_dir, objs) def apply_lib(doc, funcs, app_dir, objs): for k, v in funcs.items(): if not isinstance(v, basestring): continue else: logger.debug("process function: %s" % k) old_v = v try: funcs[k] = run_json_macros(doc, run_code_macros(v, app_dir), app_dir) except ValueError, e: raise MacroError("Error running !code or !json on " + "function \"%s\": %s" % (k, e)) if old_v != funcs[k]: objs[md5(util.to_bytestring(funcs[k])).hexdigest()] = old_v def run_code_macros(f_string, app_dir): def rreq(mo): # just read the file and return it path = os.path.join(app_dir, mo.group(2).strip()) library = '' filenum = 0 for filename in glob.iglob(path): logger.debug("process code macro: %s" % filename) try: cnt = util.read(filename) if cnt.find("!code") >= 0: cnt = run_code_macros(cnt, app_dir) library += cnt except IOError, e: raise MacroError(str(e)) filenum += 1 if not filenum: raise MacroError("Processing code: No file matching '%s'" % mo.group(2)) return library re_code = re.compile('(\/\/|#)\ ?!code (.*)') return re_code.sub(rreq, f_string) def run_json_macros(doc, f_string, app_dir): included = {} varstrings = [] def rjson(mo): if mo.group(2).startswith('_attachments'): # someone want to include from attachments path = os.path.join(app_dir, mo.group(2).strip()) filenum = 0 for filename in glob.iglob(path): logger.debug("process json macro: %s" % filename) library = '' try: if filename.endswith('.json'): library = util.read_json(filename) else: library = util.read(filename) except IOError, e: raise MacroError(str(e)) filenum += 1 current_file = filename.split(app_dir)[1] fields = current_file.split('/') count = len(fields) include_to = included for i, field in enumerate(fields): if i+1 < count: include_to[field] = {} include_to = include_to[field] else: include_to[field] = library if not filenum: raise MacroError("Processing code: No file matching '%s'" % mo.group(2)) else: logger.debug("process json macro: %s" % mo.group(2)) fields = mo.group(2).strip().split('.') library = doc count = len(fields) include_to = included for i, field in enumerate(fields): if not field in library: logger.warning("process json macro: unknown json " + "source: %s" % mo.group(2)) break library = library[field] if i+1 < count: include_to[field] = include_to.get(field, {}) include_to = include_to[field] else: include_to[field] = library return f_string def rjson2(mo): return '\n'.join(varstrings) re_json = re.compile('(\/\/|#)\ ?!json (.*)') re_json.sub(rjson, f_string) if not included: return f_string for k, v in included.iteritems(): varstrings.append("var %s = %s;" % (k, util.json.dumps(v).encode('utf-8'))) return re_json.sub(rjson2, f_string) couchapp/localdoc.py0000644000372000037200000004073712276056621013106 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from __future__ import with_statement import base64 import logging import mimetypes import os import os.path import re import urlparse import webbrowser try: import desktopcouch try: from desktopcouch.application import local_files except ImportError: from desktopcouch import local_files except ImportError: desktopcouch = None from couchapp.errors import ResourceNotFound, AppError from couchapp.macros import package_shows, package_views from couchapp import util if os.name == 'nt': def _replace_backslash(name): return name.replace("\\", "/") else: def _replace_backslash(name): return name re_comment = re.compile("((?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:\/\/.*))") DEFAULT_IGNORE = """[ // filenames matching these regexps will not be pushed to the database // uncomment to activate; separate entries with "," // ".*~$" // ".*\\\\.swp$" // ".*\\\\.bak$" ]""" logger = logging.getLogger(__name__) class LocalDoc(object): def __init__(self, path, create=False, docid=None, is_ddoc=True): self.docdir = path self.ignores = [] self.is_ddoc = is_ddoc ignorefile = os.path.join(path, '.couchappignore') if os.path.exists(ignorefile): # A .couchappignore file is a json file containing a # list of regexps for things to skip with open(ignorefile, 'r') as f: self.ignores = util.json.loads( util.remove_comments(f.read()) ) if not docid: docid = self.get_id() self.docid = docid self._doc = {'_id': self.docid} if create: self.create() def get_id(self): """ if there is an _id file, docid is extracted from it, else we take the current folder name. """ idfile = os.path.join(self.docdir, '_id') if os.path.exists(idfile): docid = util.read(idfile).split("\n")[0].strip() if docid: return docid if self.is_ddoc: return "_design/%s" % os.path.split(self.docdir)[1] else: return os.path.split(self.docdir)[1] def __repr__(self): return "<%s (%s/%s)>" % (self.__class__.__name__, self.docdir, self.docid) def __str__(self): return util.json.dumps(self.doc()) def create(self): if not os.path.isdir(self.docdir): logger.error("%s directory doesn't exist." % self.docdir) rcfile = os.path.join(self.docdir, '.couchapprc') ignfile = os.path.join(self.docdir, '.couchappignore') if not os.path.isfile(rcfile): util.write_json(rcfile, {}) util.write(ignfile, DEFAULT_IGNORE) else: logger.info("CouchApp already initialized in %s." % self.docdir) def push(self, dbs, noatomic=False, browser=False, force=False, noindex=False): """Push a doc to a list of database `dburls`. If noatomic is true each attachments will be sent one by one.""" for db in dbs: if noatomic: doc = self.doc(db, with_attachments=False, force=force) db.save_doc(doc, force_update=True) attachments = doc.get('_attachments') or {} for name, filepath in self.attachments(): if name not in attachments: logger.debug("attach %s " % name) db.put_attachment(doc, open(filepath, "r"), name=name) else: doc = self.doc(db, force=force) db.save_doc(doc, force_update=True) indexurl = self.index(db.raw_uri, doc['couchapp'].get('index')) if indexurl and not noindex: if "@" in indexurl: u = urlparse.urlparse(indexurl) indexurl = urlparse.urlunparse((u.scheme, u.netloc.split("@")[-1], u.path, u.params, u.query, u.fragment)) logger.info("Visit your CouchApp here:\n%s" % indexurl) if browser: self.browse_url(indexurl) def browse(self, dbs): for db in dbs: doc = self.doc() indexurl = self.index(db.raw_uri, doc['couchapp'].get('index')) if indexurl: self.browse_url(indexurl) def browse_url(self, url): if url.startswith("desktopcouch://"): if not desktopcouch: raise AppError("Desktopcouch isn't available on this" + "machine. You can't access to %s" % url) ctx = local_files.DEFAULT_CONTEXT bookmark_file = os.path.join(ctx.db_dir, "couchdb.html") try: username, password = \ re.findall("", open(bookmark_file).read())[-1] except ValueError: raise IOError("Bookmark file is corrupt." + "Username/password are missing.") url = "http://%s:%s@localhost:%s/%s" % (username, password, desktopcouch.find_port(), url[15:]) webbrowser.open_new_tab(url) def attachment_stub(self, name, filepath): att = {} with open(filepath, "rb") as f: re_sp = re.compile('\s') att = {"data": re_sp.sub('', base64.b64encode(f.read())), "content_type": ';'.join(filter(None, mimetypes.guess_type(name)))} return att def doc(self, db=None, with_attachments=True, force=False): """ Function to reetrieve document object from document directory. If `with_attachments` is True attachments will be included and encoded""" manifest = [] objects = {} signatures = {} attachments = {} self._doc = {'_id': self.docid} # get designdoc self._doc.update(self.dir_to_fields(self.docdir, manifest=manifest)) if not 'couchapp' in self._doc: self._doc['couchapp'] = {} self.olddoc = {} if db is not None: try: self.olddoc = db.open_doc(self._doc['_id']) attachments = self.olddoc.get('_attachments') or {} self._doc.update({'_rev': self.olddoc['_rev']}) except ResourceNotFound: self.olddoc = {} if 'couchapp' in self.olddoc: old_signatures = self.olddoc['couchapp'].get('signatures', {}) else: old_signatures = {} for name, filepath in self.attachments(): signatures[name] = util.sign(filepath) if with_attachments and not old_signatures: logger.debug("attach %s " % name) attachments[name] = self.attachment_stub(name, filepath) if old_signatures: for name, signature in old_signatures.items(): cursign = signatures.get(name) if not cursign: logger.debug("detach %s " % name) del attachments[name] elif cursign != signature: logger.debug("detach %s " % name) del attachments[name] else: continue if with_attachments: for name, filepath in self.attachments(): if old_signatures.get(name) != \ signatures.get(name) or force: logger.debug("attach %s " % name) attachments[name] = self.attachment_stub(name, filepath) self._doc['_attachments'] = attachments self._doc['couchapp'].update({ 'manifest': manifest, 'objects': objects, 'signatures': signatures }) if self.docid.startswith('_design/'): # process macros for funs in ['shows', 'lists', 'updates', 'filters', 'spatial']: if funs in self._doc: package_shows(self._doc, self._doc[funs], self.docdir, objects) if 'validate_doc_update' in self._doc: tmp_dict = {'validate_doc_update': self._doc["validate_doc_update"]} package_shows(self._doc, tmp_dict, self.docdir, objects) self._doc.update(tmp_dict) if 'views' in self._doc: # clean views # we remove empty views and malformed from the list # of pushed views. We also clean manifest views = {} dmanifest = {} for i, fname in enumerate(manifest): if fname.startswith("views/") and fname != "views/": name, ext = os.path.splitext(fname) if name.endswith('/'): name = name[:-1] dmanifest[name] = i for vname, value in self._doc['views'].iteritems(): if value and isinstance(value, dict): views[vname] = value else: del manifest[dmanifest["views/%s" % vname]] self._doc['views'] = views package_views(self._doc, self._doc["views"], self.docdir, objects) if "fulltext" in self._doc: package_views(self._doc, self._doc["fulltext"], self.docdir, objects) return self._doc def check_ignore(self, item): for i in self.ignores: match = re.match(i, item) if match: logger.debug("ignoring %s" % item) return True return False def dir_to_fields(self, current_dir='', depth=0, manifest=[]): """ process a directory and get all members """ fields = {} if not current_dir: current_dir = self.docdir for name in os.listdir(current_dir): current_path = os.path.join(current_dir, name) rel_path = _replace_backslash(util.relpath(current_path, self.docdir)) if name.startswith("."): continue elif self.check_ignore(name): continue elif depth == 0 and name.startswith('_'): # files starting with "_" are always "special" continue elif name == '_attachments': continue elif depth == 0 and (name == 'couchapp' or name == 'couchapp.json'): # we are in app_meta if name == "couchapp": manifest.append('%s/' % rel_path) content = self.dir_to_fields(current_path, depth=depth+1, manifest=manifest) else: manifest.append(rel_path) content = util.read_json(current_path) if not isinstance(content, dict): content = {"meta": content} if 'signatures' in content: del content['signatures'] if 'manifest' in content: del content['manifest'] if 'objects' in content: del content['objects'] if 'length' in content: del content['length'] if 'couchapp' in fields: fields['couchapp'].update(content) else: fields['couchapp'] = content elif os.path.isdir(current_path): manifest.append('%s/' % rel_path) fields[name] = self.dir_to_fields(current_path, depth=depth+1, manifest=manifest) else: logger.debug("push %s" % rel_path) content = '' if name.endswith('.json'): try: content = util.read_json(current_path) except ValueError: logger.error("Json invalid in %s" % current_path) else: try: content = util.read(current_path).strip() except UnicodeDecodeError: logger.warning("%s isn't encoded in utf8" % current_path) content = util.read(current_path, utf8=False) try: content.encode('utf-8') except UnicodeError: logger.warning("plan B didn't work, %s is a binary" % current_path) logger.warning("use plan C: encode to base64") content = "base64-encoded;%s" % \ base64.b64encode(content) # remove extension name, ext = os.path.splitext(name) if name in fields: logger.warning("%(name)s is already in properties. " + "Can't add (%(fqn)s)" % {"name": name, "fqn": rel_path}) else: manifest.append(rel_path) fields[name] = content return fields def _process_attachments(self, path, vendor=None): """ the function processing directory to yeld attachments. """ if os.path.isdir(path): for root, dirs, files in os.walk(path): for dirname in dirs: if self.check_ignore(dirname): dirs.remove(dirname) if files: for filename in files: if self.check_ignore(filename): continue else: filepath = os.path.join(root, filename) name = util.relpath(filepath, path) if vendor is not None: name = os.path.join('vendor', vendor, name) name = _replace_backslash(name) yield (name, filepath) def attachments(self): """ This function yield a tuple (name, filepath) corresponding to each attachment (vendor included) in the couchapp. `name` is the name of attachment in `_attachments` member and `filepath` the path to the attachment on the disk. attachments are processed later to allow us to send attachments inline or one by one. """ # process main attachments attachdir = os.path.join(self.docdir, "_attachments") for attachment in self._process_attachments(attachdir): yield attachment vendordir = os.path.join(self.docdir, 'vendor') if not os.path.isdir(vendordir): logger.debug("%s don't exist" % vendordir) return for name in os.listdir(vendordir): current_path = os.path.join(vendordir, name) if os.path.isdir(current_path): attachdir = os.path.join(current_path, '_attachments') if os.path.isdir(attachdir): for attachment in self._process_attachments(attachdir, vendor=name): yield attachment def index(self, dburl, index): if index is not None: return "%s/%s/%s" % (dburl, self.docid, index) elif os.path.isfile(os.path.join(self.docdir, "_attachments", 'index.html')): return "%s/%s/index.html" % (dburl, self.docid) return False def document(path, create=False, docid=None, is_ddoc=True): return LocalDoc(path, create=create, docid=docid, is_ddoc=is_ddoc) couchapp/commands.py0000644000372000037200000003274512276056621013127 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import os try: import desktopcouch except ImportError: desktopcouch = None from couchapp import clone_app from couchapp.autopush.command import autopush, DEFAULT_UPDATE_DELAY from couchapp.errors import ResourceNotFound, AppError, BulkSaveError from couchapp import generator from couchapp.localdoc import document from couchapp import util from couchapp.vendors import vendor_install, vendor_update logger = logging.getLogger(__name__) def hook(conf, path, hook_type, *args, **kwargs): if hook_type in conf.hooks: for h in conf.hooks.get(hook_type): if hasattr(h, 'hook'): h.hook(path, hook_type, *args, **kwargs) def init(conf, path, *args, **opts): if not args: dest = os.getcwd() else: dest = os.path.normpath(os.path.join(os.getcwd(), args[0])) if dest is None: raise AppError("Unknown dest") document(dest, True) def push(conf, path, *args, **opts): export = opts.get('export', False) noatomic = opts.get('no_atomic', False) browse = opts.get('browse', False) force = opts.get('force', False) dest = None doc_path = None if len(args) < 2: if export: if path is None and args: doc_path = args[0] else: doc_path = path else: doc_path = path if args: dest = args[0] else: doc_path = os.path.normpath(os.path.join(os.getcwd(), args[0])) dest = args[1] if doc_path is None: raise AppError("You aren't in a couchapp.") conf.update(doc_path) doc = document(doc_path, create=False, docid=opts.get('docid')) if export: if opts.get('output'): util.write_json(opts.get('output'), str(doc)) else: print str(doc) return 0 dbs = conf.get_dbs(dest) hook(conf, doc_path, "pre-push", dbs=dbs) doc.push(dbs, noatomic, browse, force) hook(conf, doc_path, "post-push", dbs=dbs) docspath = os.path.join(doc_path, '_docs') if os.path.exists(docspath): pushdocs(conf, docspath, dest, *args, **opts) return 0 def pushapps(conf, source, dest, *args, **opts): export = opts.get('export', False) noatomic = opts.get('no_atomic', False) browse = opts.get('browse', False) dbs = conf.get_dbs(dest) apps = [] source = os.path.normpath(os.path.join(os.getcwd(), source)) for d in os.listdir(source): appdir = os.path.join(source, d) if os.path.isdir(appdir) and \ os.path.isfile(os.path.join(appdir, '.couchapprc')): doc = document(appdir) hook(conf, appdir, "pre-push", dbs=dbs, pushapps=True) if export or not noatomic: apps.append(doc) else: doc.push(dbs, True, browse) hook(conf, appdir, "post-push", dbs=dbs, pushapps=True) if apps: if export: docs = [] docs.append([doc.doc() for doc in apps]) jsonobj = {'docs': docs} if opts.get('output') is not None: util.write_json(opts.get('output'), util.json.dumps(jsonobj)) else: print util.json.dumps(jsonobj) return 0 else: for db in dbs: docs = [] docs = [doc.doc(db) for doc in apps] try: db.save_docs(docs) except BulkSaveError, e: docs1 = [] for doc in e.errors: try: doc['_rev'] = db.last_rev(doc['_id']) docs1.append(doc) except ResourceNotFound: pass if docs1: db.save_docs(docs1) return 0 def pushdocs(conf, source, dest, *args, **opts): export = opts.get('export', False) noatomic = opts.get('no_atomic', False) browse = opts.get('browse', False) dbs = conf.get_dbs(dest) docs = [] for d in os.listdir(source): docdir = os.path.join(source, d) if d.startswith('.'): continue elif os.path.isfile(docdir): if d.endswith(".json"): doc = util.read_json(docdir) docid, ext = os.path.splitext(d) doc.setdefault('_id', docid) doc.setdefault('couchapp', {}) if export or not noatomic: docs.append(doc) else: for db in dbs: db.save_doc(doc, force_update=True) else: doc = document(docdir, is_ddoc=False) if export or not noatomic: docs.append(doc) else: doc.push(dbs, True, browse) if docs: if export: docs1 = [] for doc in docs: if hasattr(doc, 'doc'): docs1.append(doc.doc()) else: docs1.append(doc) jsonobj = {'docs': docs} if opts.get('output') is not None: util.write_json(opts.get('output'), util.json.dumps(jsonobj)) else: print util.json.dumps(jsonobj) else: for db in dbs: docs1 = [] for doc in docs: if hasattr(doc, 'doc'): docs1.append(doc.doc(db)) else: newdoc = doc.copy() try: rev = db.last_rev(doc['_id']) newdoc.update({'_rev': rev}) except ResourceNotFound: pass docs1.append(newdoc) try: db.save_docs(docs1) except BulkSaveError, e: # resolve conflicts docs1 = [] for doc in e.errors: try: doc['_rev'] = db.last_rev(doc['_id']) docs1.append(doc) except ResourceNotFound: pass if docs1: db.save_docs(docs1) return 0 def clone(conf, source, *args, **opts): if len(args) > 0: dest = args[0] else: dest = None hook(conf, dest, "pre-clone", source=source) clone_app.clone(source, dest, rev=opts.get('rev')) hook(conf, dest, "post-clone", source=source) return 0 def startapp(conf, *args, **opts): if len(args) < 1: raise AppError("Can't start an app, name or path is missing") if len(args) == 1: name = args[0] dest = os.path.normpath(os.path.join(os.getcwd(), ".", name)) elif len(args) == 2: name = args[1] dest = os.path.normpath(os.path.join(args[0], args[1])) if os.path.isfile(os.path.join(dest, ".couchapprc")): raise AppError("can't create an app at '%s'. One already exists here" % dest) generator.generate(dest, "startapp", name, **opts) return 0 def generate(conf, path, *args, **opts): dest = path if len(args) < 1: raise AppError("Can't generate function, name or path is missing") if len(args) == 1: kind = "app" name = args[0] elif len(args) == 2: kind = args[0] name = args[1] elif len(args) >= 3: kind = args[0] dest = args[1] name = args[2] if dest is None: if kind == "app": dest = os.path.normpath(os.path.join(os.getcwd(), ".", name)) opts['create'] = True else: raise AppError("You aren't in a couchapp.") hook(conf, dest, "pre-generate") generator.generate(dest, kind, name, **opts) hook(conf, dest, "post-generate") return 0 def vendor(conf, path, *args, **opts): if len(args) < 1: raise AppError("missing command") dest = path args = list(args) cmd = args.pop(0) if cmd == "install": if len(args) < 1: raise AppError("missing source") if len(args) == 1: source = args.pop(0) elif len(args) > 1: dest = args.pop(0) source = args.pop(0) if dest is None: raise AppError("You aren't in a couchapp.") dest = os.path.normpath(os.path.join(os.getcwd(), dest)) hook(conf, dest, "pre-vendor", source=source, action="install") vendor_install(conf, dest, source, *args, **opts) hook(conf, dest, "post-vendor", source=source, action="install") else: vendorname = None if len(args) == 1: vendorname = args.pop(0) elif len(args) >= 2: dest = args.pop(0) vendorname = args.pop(0) if dest is None: raise AppError("You aren't in a couchapp.") dest = os.path.normpath(os.path.join(os.getcwd(), dest)) hook(conf, dest, "pre-vendor", name=vendorname, action="update") vendor_update(conf, dest, vendorname, *args, **opts) hook(conf, dest, "pre-vendor", name=vendorname, action="update") return 0 def browse(conf, path, *args, **opts): dest = None doc_path = None if len(args) < 2: doc_path = path if args: dest = args[0] else: doc_path = os.path.normpath(os.path.join(os.getcwd(), args[0])) dest = args[1] if doc_path is None: raise AppError("You aren't in a couchapp.") conf.update(doc_path) doc = document(doc_path, create=False, docid=opts.get('docid')) dbs = conf.get_dbs(dest) doc.browse(dbs) def version(conf, *args, **opts): from couchapp import __version__ print "Couchapp (version %s)" % __version__ print "Copyright 2008-2010 Benoît Chesneau " print "Licensed under the Apache License, Version 2.0." print "" if opts.get('help', False): usage(conf, *args, **opts) return 0 def usage(conf, *args, **opts): if opts.get('version', False): version(conf, *args, **opts) print "Usage: couchapp [OPTIONS] [CMD] [CMDOPTIONS] [ARGS,...]" print "" print "Options:" mainopts = [] max_opt_len = len(max(globalopts, key=len)) for opt in globalopts: print "\t%-*s" % (max_opt_len, get_switch_str(opt)) mainopts.append(opt[0]) print "" print "Commands:" commands = sorted(table.keys()) max_len = len(max(commands, key=len)) for cmd in commands: opts = table[cmd] # Command name is max_len characters. Used by the %-*s formatting code print "\t%-*s %s" % (max_len, cmd, opts[2]) # Print each command's option list cmd_options = opts[1] if cmd_options: max_opt = max(cmd_options, key=lambda o: len(get_switch_str(o))) max_opt_len = len(get_switch_str(max_opt)) for opt in cmd_options: print "\t\t%-*s %s" % (max_opt_len, get_switch_str(opt), opt[3]) print "" print "" return 0 def get_switch_str(opt): """ Output just the '-r, --rev [VAL]' part of the option string. """ if opt[2] is None or opt[2] is True or opt[2] is False: default = "" else: default = "[VAL]" if opt[0]: # has a short and long option return "-%s, --%s %s" % (opt[0], opt[1], default) else: # only has a long option return "--%s %s" % (opt[1], default) globalopts = [ ('d', 'debug', None, "debug mode"), ('h', 'help', None, "display help and exit"), ('', 'version', None, "display version and exit"), ('v', 'verbose', None, "enable additionnal output"), ('q', 'quiet', None, "don't print any message") ] pushopts = [ ('', 'no-atomic', False, "send attachments one by one"), ('', 'export', False, "don't do push, just export doc to stdout"), ('', 'output', '', "if export is selected, output to the file"), ('b', 'browse', False, "open the couchapp in the browser"), ('', 'force', False, "force attachments sending") ] table = { "init": (init, [], "[COUCHAPPDIR]"), "push": (push, pushopts + [('', 'docid', '', "set docid")], "[OPTION]... [COUCHAPPDIR] DEST"), "clone": (clone, [('r', 'rev', '', "clone specific revision")], "[OPTION]...[-r REV] SOURCE [COUCHAPPDIR]"), "pushapps": (pushapps, pushopts, "[OPTION]... SOURCE DEST"), "pushdocs": (pushdocs, pushopts, "[OPTION]... SOURCE DEST"), "startapp": (startapp, [], "[COUCHAPPDIR] NAME"), "generate": (generate, [('', 'template', '', "template name")], "[OPTION]... [app|view,list,show,filter,function,vendor] [COUCHAPPDIR] " + "NAME"), "vendor": (vendor, [("f", 'force', False, "force install or update")], "[OPTION]...[-f] install|update [COUCHAPPDIR] SOURCE"), "browse": (browse, [], "[COUCHAPPDIR] DEST"), "autopush": (autopush, [('', 'no-atomic', False, "send attachments one by one"), ('', 'update-delay', DEFAULT_UPDATE_DELAY, "time between each update")], "[OPTION]... [COUCHAPPDIR] DEST"), "help": (usage, [], ""), "version": (version, [], "") } withcmd = ['generate', 'vendor'] incouchapp = ['init', 'push', 'generate', 'vendor', 'autopush'] couchapp/autopush/0000755000372000037200000000000012276056621012611 5ustar gfagfacouchapp/autopush/handler.py0000644000372000037200000000316612276056621014606 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import os import re import time from watchdog.events import FileSystemEventHandler from couchapp.autopush import DEFAULT_UPDATE_DELAY from couchapp.util import json, remove_comments log = logging.getLogger(__name__) class CouchappEventHandler(FileSystemEventHandler): def __init__(self, doc, dbs, update_delay=DEFAULT_UPDATE_DELAY, noatomic=False): super(CouchappEventHandler, self).__init__() self.update_delay = update_delay self.doc = doc self.dbs = dbs self.noatomic = noatomic self.last_update = None ignorefile = os.path.join(doc.docdir, '.couchappignore') if os.path.exists(ignorefile): with open(ignorefile, 'r') as f: self.ignores = json.loads(remove_comments(f.read())) else: self.ignores = [] def check_ignore(self, item): for ign in self.ignores: match = re.match(ign, item) if match: return True return False def maybe_update(self): if not self.last_update: return diff = time.time() - self.last_update if diff >= self.update_delay: log.info("synchronize changes") self.doc.push(self.dbs, noatomic=self.noatomic, noindex=True) self.last_update = None def dispatch(self, ev): if self.check_ignore(ev.src_path): return self.last_update = time.time() self.maybe_update() couchapp/autopush/command.py0000644000372000037200000000236212276056621014604 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import os import sys from couchapp.autopush import DEFAULT_UPDATE_DELAY from couchapp.errors import AppError from couchapp.localdoc import document if sys.platform == "win32" or os.name == "nt": from couchapp.autopush.winwatcher import WinCouchappWatcher as \ CouchappWatcher else: from couchapp.autopush.watcher import CouchappWatcher log = logging.getLogger(__name__) def autopush(conf, path, *args, **opts): doc_path = None dest = None if len(args) < 2: doc_path = path if args: dest = args[0] else: doc_path = os.path.normpath(os.path.join(os.getcwd(), args[0])) dest = args[1] if doc_path is None: raise AppError("You aren't in a couchapp.") conf.update(doc_path) doc = document(doc_path, create=False, docid=opts.get('docid')) dbs = conf.get_dbs(dest) update_delay = int(opts.get('update_delay', DEFAULT_UPDATE_DELAY)) noatomic = opts.get('no_atomic', False) watcher = CouchappWatcher(doc, dbs, update_delay=update_delay, noatomic=noatomic) watcher.run() couchapp/autopush/winwatcher.py0000644000372000037200000000237612276056621015346 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import time from couchapp.autopush import DEFAULT_UPDATE_DELAY from couchapp.autopush.handler import CouchappEventHandler from pathtools.path import absolute_path from watchdog.observers import Observer log = logging.getLogger(__name__) class WinCouchappWatcher(object): def __init__(self, doc, dbs, update_delay=DEFAULT_UPDATE_DELAY, noatomic=False): self.doc_path = absolute_path(doc.docdir) self.event_handler = CouchappEventHandler(doc, dbs, update_delay=update_delay, noatomic=noatomic) self.observer = Observer() self.observer.schedule(self.event_handler, self.doc_path, recursive=True) def run(self): log.info("Starting to listen changes in '%s'", self.doc_path) self.observer.start() try: while True: self.event_handler.maybe_update() time.sleep(1) except (SystemExit, KeyboardInterrupt): self.observer.stop() self.observer.join() couchapp/autopush/__init__.py0000644000372000037200000000027312276056621014724 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. DEFAULT_UPDATE_DELAY = 5 # update delay in seconds couchapp/autopush/watcher.py0000644000372000037200000000607412276056621014627 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import signal import time import traceback from couchapp.autopush import DEFAULT_UPDATE_DELAY from couchapp.autopush.handler import CouchappEventHandler from pathtools.path import absolute_path from watchdog.observers import Observer log = logging.getLogger(__name__) class CouchappWatcher(object): SIG_QUEUE = [] SIGNALS = map(lambda x: getattr(signal, "SIG%s" % x), "QUIT INT TERM".split()) SIG_NAMES = dict((getattr(signal, name), name[3:].lower()) for name in dir(signal) if name[:3] == "SIG" and name[3] != "_") def __init__(self, doc, dbs, update_delay=DEFAULT_UPDATE_DELAY, noatomic=False): self.doc_path = absolute_path(doc.docdir) self.event_handler = CouchappEventHandler(doc, dbs, update_delay=update_delay, noatomic=noatomic) self.observer = Observer() self.observer.schedule(self.event_handler, self.doc_path, recursive=True) def init_signals(self): """\ Initialize master signal handling. Most of the signals are queued. Child signals only wake up the master. """ map(lambda s: signal.signal(s, self.signal), self.SIGNALS) signal.signal(signal.SIGCHLD, self.handle_chld) def signal(self, sig, frame): if len(self.SIG_QUEUE) < 5: self.SIG_QUEUE.append(sig) else: log.warn("Dropping signal: %s" % sig) def handle_chld(self, sig, frame): return def handle_quit(self): raise StopIteration def handle_int(self): raise StopIteration def handle_term(self): raise StopIteration def run(self): log.info("Starting to listen changes in '%s'", self.doc_path) self.init_signals() self.observer.start() while True: try: sig = self.SIG_QUEUE.pop(0) if len(self.SIG_QUEUE) else None if sig is None: self.event_handler.maybe_update() elif sig in self.SIG_NAMES: signame = self.SIG_NAMES.get(sig) handler = getattr(self, "handle_%s" % signame, None) if not handler: log.error("Unhandled signal: %s" % signame) continue log.info("handling signal: %s" % signame) handler() else: log.info("Ignoring unknown signal: %s" % sig) time.sleep(1) except (StopIteration, KeyboardInterrupt): self.observer.stop() return 0 except Exception: log.info("unhandled exception in main loop:\n%s" % traceback.format_exc()) return -1 self.observer.join() couchapp/hooks/0000755000372000037200000000000012276056621012064 5ustar gfagfacouchapp/hooks/compress/0000755000372000037200000000000012303145327013710 5ustar gfagfacouchapp/hooks/compress/yuicompressor.py0000644000372000037200000000127112276056621017215 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. """ simple backend to use yuicompressor to compress files """ __about__ = "yui compressor v2.4.1" import codecs import os from popen2 import popen2 import tempfile def compress(js): cmd_path = os.path.join(os.path.dirname(__file__), 'yuicompressor-2.4.1.jar') fd, fname = tempfile.mkstemp() f = codecs.getwriter('utf8')(os.fdopen(fd, "w")) f.write(js) f.close() cmd = "java -jar %s --type js %s" % (cmd_path, fname) sout, sin = popen2(cmd) data = sout.read() os.unlink(fname) return data couchapp/hooks/compress/__init__.py0000644000372000037200000000752712276056621016043 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import os import re from couchapp.config import Config from couchapp.hooks.compress import compress_css from couchapp import util logger = logging.getLogger(__name__) class Compress(object): def __init__(self, path): self.appdir = path self.attach_dir = os.path.join(path, '_attachments') self.conf = Config() self.conf.update(path) def is_hook(self): if not 'compress' in self.conf: return False return True def compress_css(self, css): re_url = re.compile('url\s*\(([^\s"].*)\)') src_fpath = '' fname_dir = '' def replace_url(mo): """ make sure urls are relative to css path """ css_url = mo.group(0)[4:].strip(")").replace("'", "").replace('"', '') css_path = os.path.join(os.path.dirname(src_fpath), css_url) rel_path = util.relpath(css_path, fname_dir) return "url(%s)" % rel_path for fname, src_files in css.iteritems(): output_css = '' dest_path = os.path.join(self.attach_dir, fname) fname_dir = os.path.dirname(dest_path) for src_fname in src_files: src_fpath = os.path.join(self.appdir, src_fname) if os.path.exists(src_fpath): content_css = \ str(compress_css.CSSParser(util.read(src_fpath))) content_css = re_url.sub(replace_url, content_css) output_css += content_css logger.debug("Merging %s in %s" % (src_fname, fname)) if not os.path.isdir(fname_dir): os.makedirs(fname_dir) util.write(dest_path, output_css) def compress_js(self, backend, js): logger.info("compress js with %s " % backend.__about__) for fname, src_files in js.iteritems(): output_js = '' dest_path = os.path.join(self.attach_dir, fname) fname_dir = os.path.dirname(dest_path) for src_fname in src_files: src_fpath = os.path.join(self.appdir, src_fname) if os.path.isfile(src_fpath): output_js += "/* %s */\n" % src_fpath output_js += util.read(src_fpath) logger.debug("merging %s in %s" % (src_fname, fname)) if not os.path.isdir(fname_dir): os.makedirs(fname_dir) output_js = backend.compress(output_js) util.write(dest_path, output_js) def run(self): conf = self.conf actions = conf.get('compress', {}) if 'css' in actions: self.compress_css(actions['css']) if 'js' in actions: if 'js_compressor' in conf['compress']: modname = conf['compress']['js_compressor'] if not isinstance(modname, basestring): logger.warning("Warning: js_compressor settings should " + "be a string") logger.warning("Selecting default backend (jsmin)") import couchapp.hooks.compress.default as backend else: try: backend = __import__(modname, {}, {}, ['']) except ImportError: import couchapp.hooks.compress.default as backend else: import couchapp.hooks.compress.default as backend self.compress_js(backend, actions['js']) def hook(path, hooktype, **kwarg): c = Compress(path) if hooktype == "pre-push": if not c.is_hook(): return c.run() couchapp/hooks/compress/jsmin.py0000644000372000037200000002001412276056621015406 0ustar gfagfa# This code is original from jsmin by Douglas Crockford, it was translated to # Python by Baruch Even. It was rewritten by Dave St.Germain for speed. # # The MIT License (MIT) # # Copyright (c) 2013 Dave St.Germain # # 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. import sys is_3 = sys.version_info >= (3, 0) if is_3: import io else: import StringIO try: import cStringIO except ImportError: cStringIO = None __all__ = ['jsmin', 'JavascriptMinify'] __version__ = '2.0.9' def jsmin(js): """ returns a minified version of the javascript string """ if not is_3: if cStringIO and not isinstance(js, unicode): # strings can use cStringIO for a 3x performance # improvement, but unicode (in python2) cannot klass = cStringIO.StringIO else: klass = StringIO.StringIO else: klass = io.StringIO ins = klass(js) outs = klass() JavascriptMinify(ins, outs).minify() return outs.getvalue() class JavascriptMinify(object): """ Minify an input stream of javascript, writing to an output stream """ def __init__(self, instream=None, outstream=None): self.ins = instream self.outs = outstream def minify(self, instream=None, outstream=None): if instream and outstream: self.ins, self.outs = instream, outstream self.is_return = False self.return_buf = '' def write(char): # all of this is to support literal regular expressions. # sigh if char in 'return': self.return_buf += char self.is_return = self.return_buf == 'return' self.outs.write(char) if self.is_return: self.return_buf = '' read = self.ins.read space_strings = "abcdefghijklmnopqrstuvwxyz"\ "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" starters, enders = '{[(+-', '}])+-"\'' newlinestart_strings = starters + space_strings newlineend_strings = enders + space_strings do_newline = False do_space = False escape_slash_count = 0 doing_single_comment = False previous_before_comment = '' doing_multi_comment = False in_re = False in_quote = '' quote_buf = [] previous = read(1) if previous == '\\': escape_slash_count += 1 next1 = read(1) if previous == '/': if next1 == '/': doing_single_comment = True elif next1 == '*': doing_multi_comment = True previous = next1 next1 = read(1) else: write(previous) elif not previous: return elif previous >= '!': if previous in "'\"": in_quote = previous write(previous) previous_non_space = previous else: previous_non_space = ' ' if not next1: return while 1: next2 = read(1) if not next2: last = next1.strip() if not (doing_single_comment or doing_multi_comment)\ and last not in ('', '/'): if in_quote: write(''.join(quote_buf)) write(last) break if doing_multi_comment: if next1 == '*' and next2 == '/': doing_multi_comment = False next2 = read(1) elif doing_single_comment: if next1 in '\r\n': doing_single_comment = False while next2 in '\r\n': next2 = read(1) if not next2: break if previous_before_comment in ')}]': do_newline = True elif previous_before_comment in space_strings: write('\n') elif in_quote: quote_buf.append(next1) if next1 == in_quote: numslashes = 0 for c in reversed(quote_buf[:-1]): if c != '\\': break else: numslashes += 1 if numslashes % 2 == 0: in_quote = '' write(''.join(quote_buf)) elif next1 in '\r\n': if previous_non_space in newlineend_strings \ or previous_non_space > '~': while 1: if next2 < '!': next2 = read(1) if not next2: break else: if next2 in newlinestart_strings \ or next2 > '~' or next2 == '/': do_newline = True break elif next1 < '!' and not in_re: if (previous_non_space in space_strings \ or previous_non_space > '~') \ and (next2 in space_strings or next2 > '~'): do_space = True elif previous_non_space in '-+' and next2 == previous_non_space: # protect against + ++ or - -- sequences do_space = True elif self.is_return and next2 == '/': # returning a regex... write(' ') elif next1 == '/': if do_space: write(' ') if in_re: if previous != '\\' or (not escape_slash_count % 2) or next2 in 'gimy': in_re = False write('/') elif next2 == '/': doing_single_comment = True previous_before_comment = previous_non_space elif next2 == '*': doing_multi_comment = True previous = next1 next1 = next2 next2 = read(1) else: in_re = previous_non_space in '(,=:[?!&|' or self.is_return # literal regular expression write('/') else: if do_space: do_space = False write(' ') if do_newline: write('\n') do_newline = False write(next1) if not in_re and next1 in "'\"": in_quote = next1 quote_buf = [] previous = next1 next1 = next2 if previous >= '!': previous_non_space = previous if previous == '\\': escape_slash_count += 1 else: escape_slash_count = 0 couchapp/hooks/compress/default.py0000644000372000037200000000052112276056621015713 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from __future__ import absolute_import __about__ = 'jsmin' def compress(js): try: import jsmin except: import couchapp.hooks.compress.jsmin as jsmin return jsmin.jsmin(js) couchapp/hooks/compress/compress_css.py0000644000372000037200000000544712276056621017006 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import os import re import sys sys.path.append(os.path.dirname(__file__)) __all__ = ['CSSParser'] re_selector = re.compile("/\./") re_comments = re.compile("(\/\*).*?(\*\/)") re_sep = re.compile(':\s*') re_line1 = re.compile('\n') re_line = re.compile('(\n)') re_comma = re.compile(',') re_comma2 = re.compile(', ') def strip_space(string): """ strip space after :, remove newlines, replace multiple spaces with only one space, remove comments """ if isinstance(string, basestring): string = re_line1.sub('', re_sep.sub(':', string)) string = re_comments.sub('', string.strip()) return string def strip_selector_space(string): """ remove newlines, insert space after comma, replace two spaces with one space after comma """ if isinstance(string, basestring): string = re_comma2.sub(', ', re_comma.sub(', ', re_line.sub('', string))) return string class CSSParser(object): def __init__(self, css_string, options=None): options = options or {} self.namespace = options.get('namespace', '') self.raw_data = css_string self.css_output = '' self._compress(self.raw_data) def __str__(self): return self.css_output def parse(self, data): data = data or self.raw_data css_out = [] data = strip_space(data.strip()) for index, assignements in enumerate(data.split('}')): try: tags, styles = [a.strip() for a in assignements.strip().split('{')] rules = [] if styles: if self.namespace: tags = strip_selector_space(tags) tags = re_selector.sub(self.namespace, tags) rules = [] for key_val_pair in styles.split(';'): try: key, value = [a.strip() for a in key_val_pair.split(':')] rules.append("%s:%s;" % (key, value)) except ValueError: continue css_out.append({ 'tags': tags, 'rules': ''.join(rules), 'idx': index }) except ValueError: continue css_out.sort(lambda a, b: cmp(a['idx'], b['idx'])) return css_out def _compress(self, data): self.css_output = '' for line in self.parse(data): self.css_output += "%s {%s}\n" % (line['tags'], line['rules']) couchapp/hooks/__init__.py0000644000372000037200000000000012276056621014163 0ustar gfagfacouchapp/generator.py0000644000372000037200000001735012276056621013307 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from __future__ import with_statement import logging import os import shutil import sys from couchapp.errors import AppError from couchapp import localdoc from couchapp.util import user_path, relpath __all__ = ["generate_app", "generate_function", "generate"] logger = logging.getLogger(__name__) DEFAULT_APP_TREE = ['_attachments', 'lists', 'shows', 'updates', 'views'] def start_app(path): try: os.makedirs(path) except OSError, e: errno, message = e raise AppError("Can't create a CouchApp in %s: %s" % (path, message)) for n in DEFAULT_APP_TREE: tp = os.path.join(path, n) os.makedirs(tp) fid = os.path.join(path, '_id') if not os.path.isfile(fid): with open(fid, 'wb') as f: f.write('_design/%s' % os.path.split(path)[1]) localdoc.document(path, create=True) logger.info("%s created." % path) def generate_app(path, template=None, create=False): """ Generates a CouchApp in app_dir :attr verbose: boolean, default False :return: boolean, dict. { 'ok': True } if ok, { 'ok': False, 'error': message } if something was wrong. """ TEMPLATES = ['app'] prefix = '' if template is not None: prefix = os.path.join(*template.split('/')) try: os.makedirs(path) except OSError, e: errno, message = e raise AppError("Can't create a CouchApp in %s: %s" % (path, message)) for n in DEFAULT_APP_TREE: tp = os.path.join(path, n) os.makedirs(tp) for t in TEMPLATES: appdir = path if prefix: # we do the job twice for now to make sure an app or vendor # template exist in user template location # fast on linux since there is only one user dir location # but could be a little slower on windows for user_location in user_path(): location = os.path.join(user_location, 'templates', prefix, t) if os.path.exists(location): t = os.path.join(prefix, t) break copy_helper(appdir, t) # add vendor vendor_dir = os.path.join(appdir, 'vendor') os.makedirs(vendor_dir) copy_helper(vendor_dir, '', tname="vendor") fid = os.path.join(appdir, '_id') if not os.path.isfile(fid): with open(fid, 'wb') as f: f.write('_design/%s' % os.path.split(appdir)[1]) if create: localdoc.document(path, create=True) logger.info("%s generated." % path) def generate_function(path, kind, name, template=None): functions_path = ['functions'] if template: functions_path = [] _relpath = os.path.join(*template.split('/')) template_dir = find_template_dir("templates", _relpath) else: template_dir = find_template_dir("templates") if template_dir: functions = [] if kind == "view": path = os.path.join(path, "%ss" % kind, name) if os.path.exists(path): raise AppError("The view %s already exists" % name) functions = [('map.js', 'map.js'), ('reduce.js', 'reduce.js')] elif kind == "function": functions = [('%s.js' % name, '%s.js' % name)] elif kind == "vendor": app_dir = os.path.join(path, "vendor", name) try: os.makedirs(app_dir) except: pass targetpath = os.path.join(*template.split('/')) copy_helper(path, targetpath) return elif kind == "spatial": path = os.path.join(path, "spatial") functions = [("spatial.js", "%s.js" % name)] else: path = os.path.join(path, "%ss" % kind) functions = [('%s.js' % kind, "%s.js" % name)] try: os.makedirs(path) except: pass for template, target in functions: target_path = os.path.join(path, target) root_path = [template_dir] + functions_path + [template] root = os.path.join(*root_path) try: shutil.copy2(root, target_path) except: logger.warning("%s not found in %s" % (template, os.path.join(*root_path[:-1]))) else: raise AppError("Defaults templates not found. Check your install.") def copy_helper(path, directory, tname="templates"): """ copy helper used to generate an app""" if tname == "vendor": tname = os.path.join("templates", tname) templatedir = find_template_dir(tname, directory) if templatedir: if directory == "vendor": path = os.path.join(path, directory) try: os.makedirs(path) except: pass for root, dirs, files in os.walk(templatedir): rel = relpath(root, templatedir) if rel == ".": rel = "" target_path = os.path.join(path, rel) for d in dirs: try: os.makedirs(os.path.join(target_path, d)) except: continue for f in files: shutil.copy2(os.path.join(root, f), os.path.join(target_path, f)) else: raise AppError( "Can't create a CouchApp in %s: default template not found." % (path)) def find_template_dir(name, directory=''): paths = ['%s' % name, os.path.join('..', name)] if hasattr(sys, 'frozen'): # py2exe modpath = sys.executable elif sys.platform == "win32" or os.name == "nt": modpath = os.path.join(sys.prefix, "Lib", "site-packages", "couchapp", "templates") else: modpath = __file__ if sys.platform != "win32" and os.name != "nt": default_locations = [ "/usr/share/couchapp/templates/%s" % directory, "/usr/local/share/couchapp/templates/%s" % directory, "/opt/couchapp/templates/%s" % directory] else: default_locations = [] default_locations.extend([os.path.join(os.path.dirname(modpath), p, directory) for p in paths]) if sys.platform == "darwin": home = os.path.expanduser('~'), data_path = "%s/Library/Application Support/Couchapp" % home default_locations.extend(["%s/%s/%s" % (data_path, p, directory) for p in paths]) if directory: for user_location in user_path(): default_locations.append(os.path.join(user_location, name, directory)) found = False for location in default_locations: template_dir = os.path.normpath(location) if os.path.isdir(template_dir): found = True break if found: return template_dir return False def generate(path, kind, name, **opts): if kind not in ['startapp', 'app', 'view', 'list', 'show', 'filter', 'function', 'vendor', 'update', 'spatial']: raise AppError( "Can't generate %s in your couchapp. generator is unknown" % kind) if kind == "app": generate_app(path, template=opts.get("template"), create=opts.get('create', False)) elif kind == "startapp": start_app(path) else: if name is None: raise AppError("Can't generate %s function, name is missing" % kind) generate_function(path, kind, name, opts.get("template")) couchapp/util.py0000644000372000037200000003465212276056621012302 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from __future__ import with_statement import codecs from hashlib import md5 import imp import inspect import logging import os import re import string import sys from couchapp.errors import ScriptError try: import json except ImportError: try: import simplejson as json except ImportError: raise ImportError(""" simplejson isn't installed on your system. Install it by running the command line: pip install simplejson """) logger = logging.getLogger(__name__) try: # python 2.6, use subprocess import subprocess subprocess.Popen # trigger ImportError early closefds = os.name == 'posix' def popen3(cmd, mode='t', bufsize=0): p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=closefds) p.wait() return (p.stdin, p.stdout, p.stderr) except ImportError: subprocess = None popen3 = os.popen3 try: from importlibe import import_module except ImportError: def _resolve_name(name, package, level): """Return the absolute name of the module to be imported.""" if not hasattr(package, 'rindex'): raise ValueError("'package' not set to a string") dot = len(package) for x in xrange(level, 1, -1): try: dot = package.rindex('.', 0, dot) except ValueError: raise ValueError("attempted relative import beyond top-level " "package") return "%s.%s" % (package[:dot], name) def import_module(name, package=None): """Import a module. The 'package' argument is required when performing a relative import. It specifies the package to use as the anchor point from which to resolve the relative import to an absolute import. """ if name.startswith('.'): if not package: raise TypeError("relative imports require the 'package' " + "argument") level = 0 for character in name: if character != '.': break level += 1 name = _resolve_name(name[level:], package, level) __import__(name) return sys.modules[name] if os.name == 'nt': from win32com.shell import shell, shellcon def user_rcpath(): path = [] try: home = os.path.expanduser('~') if sys.getwindowsversion()[3] != 2 and home == '~': # We are on win < nt: fetch the APPDATA directory location and # use the parent directory as the user home dir. appdir = shell.SHGetPathFromIDList( shell.SHGetSpecialFolderLocation(0, shellcon.CSIDL_APPDATA)) home = os.path.dirname(appdir) path.append(os.path.join(home, '.couchapp.conf')) except: home = os.path.expanduser('~') path.append(os.path.join(home, '.couchapp.conf')) userprofile = os.environ.get('USERPROFILE') if userprofile: path.append(os.path.join(userprofile, '.couchapp.conf')) return path def user_path(): path = [] try: home = os.path.expanduser('~') if sys.getwindowsversion()[3] != 2 and home == '~': # We are on win < nt: fetch the APPDATA directory location and # use the parent directory as the user home dir. appdir = shell.SHGetPathFromIDList( shell.SHGetSpecialFolderLocation(0, shellcon.CSIDL_APPDATA)) home = os.path.dirname(appdir) path.append(os.path.join(home, '.couchapp')) except: home = os.path.expanduser('~') path.append(os.path.join(home, '.couchapp')) userprofile = os.environ.get('USERPROFILE') if userprofile: path.append(os.path.join(userprofile, '.couchapp')) return path else: def user_rcpath(): return [os.path.expanduser('~/.couchapp.conf')] def user_path(): return [os.path.expanduser('~/.couchapp')] # backport relpath from python2.6 if not hasattr(os.path, 'relpath'): if os.name == "nt": def splitunc(p): if p[1:2] == ':': return '', p # Drive letter present firstTwo = p[0:2] if firstTwo == '//' or firstTwo == '\\\\': # is a UNC path: # vvvvvvvvvvvvvvvvvvvv equivalent to drive letter # \\machine\mountpoint\directories... # directory ^^^^^^^^^^^^^^^ normp = os.path.normcase(p) index = normp.find('\\', 2) if index == -1: ##raise RuntimeError, 'illegal UNC path: "' + p + '"' return ("", p) index = normp.find('\\', index + 1) if index == -1: index = len(p) return p[:index], p[index:] return '', p def relpath(path, start=os.path.curdir): """Return a relative version of a path""" if not path: raise ValueError("no path specified") start_list = os.path.abspath(start).split(os.path.sep) path_list = os.path.abspath(path).split(os.path.sep) if start_list[0].lower() != path_list[0].lower(): unc_path, rest = splitunc(path) unc_start, rest = splitunc(start) if bool(unc_path) ^ bool(unc_start): raise ValueError("Cannot mix UNC and non-UNC paths (%s " + "and %s)" % (path, start)) else: raise ValueError("path is on drive %s, start on drive %s" % (path_list[0], start_list[0])) # Work out how much of the filepath is shared by start and path. for i in range(min(len(start_list), len(path_list))): if start_list[i].lower() != path_list[i].lower(): break else: i += 1 rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] if not rel_list: return os.path.curdir return os.path.join(*rel_list) else: def relpath(path, start=os.path.curdir): """Return a relative version of a path""" if not path: raise ValueError("no path specified") start_list = os.path.abspath(start).split(os.path.sep) path_list = os.path.abspath(path).split(os.path.sep) # Work out how much of the filepath is shared by start and path. i = len(os.path.commonprefix([start_list, path_list])) rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] if not rel_list: return os.path.curdir return os.path.join(*rel_list) else: relpath = os.path.relpath #TODO: manage system configuration file _rcpath = None def rcpath(): """ get global configuration """ global _rcpath if _rcpath is None: if 'COUCHAPPCONF_PATH' in os.environ: _rcpath = [] for p in os.environ['COUCHAPPCONF_PATH'].split(os.pathsep): if not p: continue if os.path.isdir(p): for f, kind in os.listdir(p): if f == "couchapp.conf": _rcpath.append(os.path.join(p, f)) else: _rcpath.append(p) else: _rcpath = user_rcpath() return _rcpath def findcouchapp(p): while not os.path.isfile(os.path.join(p, ".couchapprc")): oldp, p = p, os.path.dirname(p) if p == oldp: return None return p def in_couchapp(): """ return path of couchapp if we are somewhere in a couchapp. """ current_path = os.getcwd() parent = '' while 1: current_rcpath = os.path.join(current_path, '.couchapprc') if os.path.exists(current_rcpath): if current_rcpath in rcpath(): return False return current_path parent = os.path.normpath(os.path.join(current_path, '../')) if parent == current_path: return False current_path = parent def get_appname(docid): """ get applicaton name for design name""" return docid.split('_design/')[1] def to_bytestring(s): """ convert to bytestring an unicode """ if not isinstance(s, basestring): return s if isinstance(s, unicode): return s.encode('utf-8') else: return s # function borrowed to Fusil project(http://fusil.hachoir.org/) # which allowed us to use it under Apache 2 license. def locate_program(program, use_none=False, raise_error=False): if os.path.isabs(program): # Absolute path: nothing to do return program if os.path.dirname(program): # ./test => $PWD/./test # ../python => $PWD/../python program = os.path.normpath(os.path.realpath(program)) return program if use_none: default = None else: default = program paths = os.getenv('PATH') if not paths: if raise_error: raise ValueError("Unable to get PATH environment variable") return default for path in paths.split(os.pathsep): filename = os.path.join(path, program) if os.access(filename, os.X_OK): return filename if raise_error: raise ValueError("Unable to locate program %r in PATH" % program) return default def deltree(path): for root, dirs, files in os.walk(path, topdown=False): for name in files: os.unlink(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) try: os.rmdir(path) except: pass def split_path(path): parts = [] while True: head, tail = os.path.split(path) parts = [tail] + parts path = head if not path: break return parts def sign(fpath): """ return md5 hash from file content :attr fpath: string, path of file :return: string, md5 hexdigest """ if os.path.isfile(fpath): m = md5() with open(fpath, 'rb') as fp: try: while 1: data = fp.read(8096) if not data: break m.update(data) except IOError, msg: logger.error('%s: I/O error: %s\n' % (fpath, msg)) return 1 return m.hexdigest() return '' def read(fname, utf8=True, force_read=False): """ read file content""" if utf8: try: with codecs.open(fname, 'rb', "utf-8") as f: return f.read() except UnicodeError: if force_read: return read(fname, utf8=False) raise else: with open(fname, 'rb') as f: return f.read() def write(fname, content): """ write content in a file :attr fname: string,filename :attr content: string """ with open(fname, 'wb') as f: f.write(to_bytestring(content)) def write_json(fname, content): """ serialize content in json and save it :attr fname: string :attr content: string """ write(fname, json.dumps(content).encode('utf-8')) def read_json(fname, use_environment=False, raise_on_error=False): """ read a json file and deserialize :attr filename: string :attr use_environment: boolean, default is False. If True, replace environment variable by their value in file content :return: dict or list """ try: data = read(fname, force_read=True) except IOError, e: if e[0] == 2: return {} raise if use_environment: data = string.Template(data).substitute(os.environ) try: data = json.loads(data) except ValueError: logger.error("Json is invalid, can't load %s" % fname) if not raise_on_error: return {} raise return data _vendor_dir = None def vendor_dir(): global _vendor_dir if _vendor_dir is None: _vendor_dir = os.path.join(os.path.dirname(__file__), 'vendor') return _vendor_dir def expandpath(path): return os.path.expanduser(os.path.expandvars(path)) def load_py(uri, cfg): if os.path.exists(uri): name, ext = os.path.splitext(os.path.basename(uri)) script = imp.load_source(name, uri) else: if ":" in uri: parts = uri.rsplit(":", 1) name, objname = parts[0], parts[1] mod = import_module(name) script_class = getattr(mod, objname) try: if inspect.getargspec(script_class.__init__) > 1: script = script_class(cfg) else: script = script_class() except TypeError: script = script_class() else: script = import_module(uri) script.__dict__['__couchapp_cfg__'] = cfg return script class ShellScript(object): """ simple object used to manage extensions or hooks from external scripts in any languages """ def __init__(self, cmd): self.cmd = cmd def hook(self, *args, **options): cmd = self.cmd + " " (child_stdin, child_stdout, child_stderr) = popen3(cmd) err = child_stderr.read() if err: raise ScriptError(str(err)) return (child_stdout.read()) def hook_uri(uri, cfg): if isinstance(uri, list): (script_type, script_uri) = uri if script_type == "py": return load_py(script_uri, cfg) else: script_uri = uri return ShellScript(script_uri) regex_comment = r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"' re_comment = re.compile(regex_comment, re.DOTALL | re.MULTILINE) def remove_comments(t): def replace(m): s = m.group(0) if s.startswith("/"): return "" return s return re.sub(re_comment, replace, t) couchapp/client.py0000644000372000037200000004151312276056621012575 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from __future__ import with_statement import base64 import itertools import logging import re import types try: import desktopcouch try: from desktopcouch.application import local_files except ImportError: from desktopcouch import local_files except ImportError: desktopcouch = None from restkit import Resource, ClientResponse, ResourceError from restkit import util from restkit import oauth2 as oauth from restkit.filters import OAuthFilter from couchapp import __version__ from couchapp.errors import ResourceNotFound, ResourceConflict, \ PreconditionFailed, RequestFailed, BulkSaveError, Unauthorized, \ InvalidAttachment, AppError from couchapp.util import json USER_AGENT = "couchapp/%s" % __version__ aliases = { 'id': '_id', 'rev': '_rev' } UNKNOWN_VERSION = tuple() logger = logging.getLogger(__name__) class CouchdbResponse(ClientResponse): @property def json_body(self): try: return json.loads(self.body_string()) except ValueError: return self.body class CouchdbResource(Resource): def __init__(self, uri="http://127.0.0.1:5984", **client_opts): """Constructor for a `CouchdbResource` object. CouchdbResource represent an HTTP resource to CouchDB. @param uri: str, full uri to the server. """ client_opts['response_class'] = CouchdbResponse Resource.__init__(self, uri=uri, **client_opts) self.safe = ":/%" def copy(self, path=None, headers=None, **params): """ add copy to HTTP verbs """ return self.request('COPY', path=path, headers=headers, **params) def request(self, method, path=None, payload=None, headers=None, params_dict=None, **params): """ Perform HTTP call to the couchdb server and manage JSON conversions, support GET, POST, PUT and DELETE. Usage example, get infos of a couchdb server on http://127.0.0.1:5984 : import couchdbkit.CouchdbResource resource = couchdbkit.CouchdbResource() infos = resource.request('GET') @param method: str, the HTTP action to be performed: 'GET', 'HEAD', 'POST', 'PUT', or 'DELETE' @param path: str or list, path to add to the uri @param data: str or string or any object that could be converted to JSON. @param headers: dict, optional headers that will be added to HTTP request. @param raw: boolean, response return a Response object @param params: Optional parameterss added to the request. Parameterss are for example the parameters for a view. See `CouchDB View API reference `_ for example. @return: tuple (data, resp), where resp is an `httplib2.Response` object and data a python object (often a dict). """ headers = headers or {} headers.setdefault('Accept', 'application/json') headers.setdefault('User-Agent', USER_AGENT) logger.debug("Resource uri: %s" % self.initial['uri']) logger.debug("Request: %s %s" % (method, path)) logger.debug("Headers: %s" % str(headers)) logger.debug("Params: %s" % str(params)) try: return Resource.request(self, method, path=path, payload=payload, headers=headers, **params) except ResourceError, e: msg = getattr(e, 'msg', '') if e.response and msg: if e.response.headers.get('content-type') == \ 'application/json': try: msg = json.loads(str(msg)) except ValueError: pass if type(msg) is dict: error = msg.get('reason') else: error = msg if e.status_int == 404: raise ResourceNotFound(error, http_code=404, response=e.response) elif e.status_int == 409: raise ResourceConflict(error, http_code=409, response=e.response) elif e.status_int == 412: raise PreconditionFailed(error, http_code=412, response=e.response) elif e.status_int in (401, 403): raise Unauthorized(e) else: raise RequestFailed(str(e)) except Exception, e: raise RequestFailed("unknown error [%s]" % str(e)) def couchdb_version(server_uri): res = CouchdbResource(server_uri) try: resp = res.get() except Exception: return UNKNOWN_VERSION version = resp.json_body["version"] t = [] for p in version.split("."): try: t.append(int(p)) except ValueError: continue return tuple(t) class Uuids(object): def __init__(self, uri, max_uuids=1000, **client_opts): self.res = CouchdbResource(uri=uri, **client_opts) self._uuids = [] self.max_uuids = max_uuids def next(self): if not self._uuids: self.fetch_uuids() self._uuids, res = self._uuids[:-1], self._uuids[-1] return res def __iter__(self): return self def fetch_uuids(self): count = self.max_uuids - len(self._uuids) resp = self.res.get('/_uuids', count=count) self._uuids += resp.json_body['uuids'] class Database(object): """ Object that abstract access to a CouchDB database A Database object can act as a Dict object. """ def __init__(self, uri, create=True, **client_opts): if uri.endswith("/"): uri = uri[:-1] self.raw_uri = uri if uri.startswith("desktopcouch://"): if not desktopcouch: raise AppError("Desktopcouch isn't available on this" + "machine. You can't access to %s" % uri) uri = "http://localhost:%s/%s" % ( desktopcouch.find_port(), uri[15:]) ctx = local_files.DEFAULT_CONTEXT oauth_tokens = local_files.get_oauth_tokens(ctx) consumer = oauth.Consumer(oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"]) token = oauth.Token(oauth_tokens["token"], oauth_tokens["token_secret"]) oauth_filter = OAuthFilter("*", consumer, token) filters = client_opts.get("filters") or [] filters.append(oauth_filter) client_opts["filters"] = filters self.res = CouchdbResource(uri=uri, **client_opts) self.server_uri, self.dbname = uri.rsplit('/', 1) self.uuids = Uuids(self.server_uri, **client_opts) if create: # create the db try: self.res.head() except ResourceNotFound: self.res.put() def delete(self): self.res.delete() def info(self): """ Get database information @param _raw_json: return raw json instead deserializing it @return: dict """ return self.res.get().json_body def all_docs(self, **params): """ return all_docs """ return self.view('_all_docs', **params) def open_doc(self, docid, wrapper=None, **params): """Open document from database Args: @param docid: str, document id to retrieve @param rev: if specified, allows you to retrieve a specific revision of document @param wrapper: callable. function that takes dict as a param. Used to wrap an object. @params params: Other params to pass to the uri (or headers) @return: dict, representation of CouchDB document as a dict. """ resp = self.res.get(escape_docid(docid), **params) if wrapper is not None: if not callable(wrapper): raise TypeError("wrapper isn't a callable") return wrapper(resp.json_body) return resp.json_body def save_doc(self, doc, encode=False, force_update=False, **params): """ Save a document. It will use the `_id` member of the document or request a new uuid from CouchDB. IDs are attached to documents on the client side because POST has the curious property of being automatically retried by proxies in the event of network segmentation and lost responses. @param doc: dict. doc is updated with doc '_id' and '_rev' properties returned by CouchDB server when you save. @param force_update: boolean, if there is conlict, try to update with latest revision @param encode: Encode attachments if needed (depends on couchdb version) @return: new doc with updated revision an id """ if '_attachments' in doc and encode: doc['_attachments'] = encode_attachments(doc['_attachments']) headers = params.get('headers', {}) headers.setdefault('Content-Type', 'application/json') params['headers'] = headers if '_id' in doc: docid = escape_docid(doc['_id']) try: resp = self.res.put(docid, payload=json.dumps(doc), **params) except ResourceConflict: if not force_update: raise rev = self.last_rev(doc['_id']) doc['_rev'] = rev resp = self.res.put(docid, payload=json.dumps(doc), **params) else: json_doc = json.dumps(doc) try: doc['_id'] = self.uuids.next() resp = self.res.put(doc['_id'], payload=json_doc, **params) except ResourceConflict: resp = self.res.post(payload=json_doc, **params) json_res = resp.json_body doc1 = {} for a, n in aliases.items(): if a in json_res: doc1[n] = json_res[a] doc.update(doc1) return doc def last_rev(self, docid): """ Get last revision from docid (the '_rev' member) @param docid: str, undecoded document id. @return rev: str, the last revision of document. """ r = self.res.head(escape_docid(docid)) if "etag" in r.headers: # yeah new couchdb handle that return r.headers['etag'].strip('"') # old way .. doc = self.open_doc(docid) return doc['_rev'] def delete_doc(self, id_or_doc): """ Delete a document @param id_or_doc: docid string or document dict """ if isinstance(id_or_doc, types.StringType): docid = id_or_doc resp = self.res.delete(escape_docid(id_or_doc), rev=self.last_rev(id_or_doc)) else: docid = id_or_doc.get('_id') if not docid: raise ValueError('Not valid doc to delete (no doc id)') rev = id_or_doc.get('_rev', self.last_rev(docid)) resp = self.res.delete(escape_docid(docid), rev=rev) return resp.json_body def save_docs(self, docs, all_or_nothing=False, use_uuids=True): """ Bulk save. Modify Multiple Documents With a Single Request @param docs: list of docs @param use_uuids: add _id in doc who don't have it already set. @param all_or_nothing: In the case of a power failure, when the database restarts either all the changes will have been saved or none of them. However, it does not do conflict checking, so the documents will. @return doc lists updated with new revision or raise BulkSaveError exception. You can access to doc created and docs in error as properties of this exception. """ def is_id(doc): return '_id' in doc if use_uuids: noids = [] for k, g in itertools.groupby(docs, is_id): if not k: noids = list(g) for doc in noids: nextid = self.uuids.next() if nextid: doc['_id'] = nextid payload = {"docs": docs} if all_or_nothing: payload["all-or-nothing"] = True # update docs res = self.res.post('/_bulk_docs', payload=json.dumps(payload), headers={'Content-Type': 'application/json'}) json_res = res.json_body errors = [] for i, r in enumerate(json_res): if 'error' in r: doc1 = docs[i] doc1.update({'_id': r['id'], '_rev': r['rev']}) errors.append(doc1) else: docs[i].update({'_id': r['id'], '_rev': r['rev']}) if errors: raise BulkSaveError(docs, errors) def delete_docs(self, docs, all_or_nothing=False, use_uuids=True): """ multiple doc delete.""" for doc in docs: doc['_deleted'] = True return self.save_docs(docs, all_or_nothing=all_or_nothing, use_uuids=use_uuids) def fetch_attachment(self, id_or_doc, name, headers=None): """ get attachment in a document @param id_or_doc: str or dict, doc id or document dict @param name: name of attachment default: default result @param header: optionnal headers (like range) @return: `couchdbkit.resource.CouchDBResponse` object """ if isinstance(id_or_doc, basestring): docid = id_or_doc else: docid = id_or_doc['_id'] return self.res.get("%s/%s" % (escape_docid(docid), name), headers=headers) def put_attachment(self, doc, content=None, name=None, headers=None): """ Add attachement to a document. All attachments are streamed. @param doc: dict, document object @param content: string, iterator, fileobj @param name: name or attachment (file name). @param headers: optionnal headers like `Content-Length` or `Content-Type` @return: updated document object """ headers = {} content = content or "" if name is None: if hasattr(content, "name"): name = content.name else: raise InvalidAttachment('You should provid a valid ' + 'attachment name') name = util.url_quote(name, safe="") res = self.res.put("%s/%s" % (escape_docid(doc['_id']), name), payload=content, headers=headers, rev=doc['_rev']) json_res = res.json_body if 'ok' in json_res: return doc.update(self.open_doc(doc['_id'])) return False def delete_attachment(self, doc, name): """ delete attachement to the document @param doc: dict, document object in python @param name: name of attachement @return: updated document object """ name = util.url_quote(name, safe="") self.res.delete("%s/%s" % (escape_docid(doc['_id']), name), rev=doc['_rev']).json_body return doc.update(self.open_doc(doc['_id'])) def view(self, view_name, **params): try: dname, vname = view_name.split("/") path = "/_design/%s/_view/%s" % (dname, vname) except ValueError: path = view_name if "keys" in params: keys = params.pop("keys") return self.res.post(path, json.dumps({"keys": keys}, **params)).json_body return self.res.get(path, **params).json_body def encode_params(params): """ encode parameters in json if needed """ _params = {} if params: for name, value in params.items(): if value is None: continue if name in ('key', 'startkey', 'endkey') \ or not isinstance(value, basestring): value = json.dumps(value).encode('utf-8') _params[name] = value return _params def escape_docid(docid): if docid.startswith('/'): docid = docid[1:] if docid.startswith('_design'): docid = '_design/%s' % util.url_quote(docid[8:], safe='') else: docid = util.url_quote(docid, safe='') return docid def encode_attachments(attachments): for k, v in attachments.iteritems(): if v.get('stub', False): continue else: re_sp = re.compile('\s') v['data'] = re_sp.sub('', base64.b64encode(v['data'])) return attachments couchapp/clone_app.py0000644000372000037200000001754012276056621013262 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from __future__ import with_statement import base64 import copy from hashlib import md5 import logging import os import os.path from couchapp.errors import AppError from couchapp import client from couchapp import util logger = logging.getLogger(__name__) if os.name == 'nt': def _replace_slash(name): return name.replace("/", "\\") else: def _replace_slash(name): return name def clone(source, dest=None, rev=None): """ Clone an application from a design_doc given. :attr design_doc: dict, the design doc retrieved from couchdb if something was wrong. """ try: dburl, docid = source.split('_design/') except ValueError: raise AppError("%s isn't a valid source" % source) if not dest: dest = docid path = os.path.normpath(os.path.join(os.getcwd(), dest)) if not os.path.exists(path): os.makedirs(path) db = client.Database(dburl[:-1], create=False) if not rev: doc = db.open_doc("_design/%s" % docid) else: doc = db.open_doc("_design/%s" % docid, rev=rev) docid = doc['_id'] metadata = doc.get('couchapp', {}) # get manifest manifest = metadata.get('manifest', {}) # get signatures signatures = metadata.get('signatures', {}) # get objects refs objects = metadata.get('objects', {}) # create files from manifest if manifest: for filename in manifest: logger.debug("clone property: %s" % filename) filepath = os.path.join(path, filename) if filename.endswith('/'): if not os.path.isdir(filepath): os.makedirs(filepath) elif filename == "couchapp.json": continue else: parts = util.split_path(filename) fname = parts.pop() v = doc while 1: try: for key in parts: v = v[key] except KeyError: break # remove extension last_key, ext = os.path.splitext(fname) # make sure key exist try: content = v[last_key] except KeyError: break if isinstance(content, basestring): _ref = md5(util.to_bytestring(content)).hexdigest() if objects and _ref in objects: content = objects[_ref] if content.startswith('base64-encoded;'): content = base64.b64decode(content[15:]) if fname.endswith('.json'): content = util.json.dumps(content).encode('utf-8') del v[last_key] # make sure file dir have been created filedir = os.path.dirname(filepath) if not os.path.isdir(filedir): os.makedirs(filedir) util.write(filepath, content) # remove the key from design doc temp = doc for key2 in parts: if key2 == key: if not temp[key2]: del temp[key2] break temp = temp[key2] # second pass for missing key or in case # manifest isn't in app for key in doc.iterkeys(): if key.startswith('_'): continue elif key in ('couchapp'): app_meta = copy.deepcopy(doc['couchapp']) if 'signatures' in app_meta: del app_meta['signatures'] if 'manifest' in app_meta: del app_meta['manifest'] if 'objects' in app_meta: del app_meta['objects'] if 'length' in app_meta: del app_meta['length'] if app_meta: couchapp_file = os.path.join(path, 'couchapp.json') util.write_json(couchapp_file, app_meta) elif key in ('views'): vs_dir = os.path.join(path, key) if not os.path.isdir(vs_dir): os.makedirs(vs_dir) for vsname, vs_item in doc[key].iteritems(): vs_item_dir = os.path.join(vs_dir, vsname) if not os.path.isdir(vs_item_dir): os.makedirs(vs_item_dir) for func_name, func in vs_item.iteritems(): filename = os.path.join(vs_item_dir, '%s.js' % func_name) util.write(filename, func) logger.warning("clone view not in manifest: %s" % filename) elif key in ('shows', 'lists', 'filter', 'update'): showpath = os.path.join(path, key) if not os.path.isdir(showpath): os.makedirs(showpath) for func_name, func in doc[key].iteritems(): filename = os.path.join(showpath, '%s.js' % func_name) util.write(filename, func) logger.warning( "clone show or list not in manifest: %s" % filename) else: filedir = os.path.join(path, key) if os.path.exists(filedir): continue else: logger.warning("clone property not in manifest: %s" % key) if isinstance(doc[key], (list, tuple,)): util.write_json(filedir + ".json", doc[key]) elif isinstance(doc[key], dict): if not os.path.isdir(filedir): os.makedirs(filedir) for field, value in doc[key].iteritems(): fieldpath = os.path.join(filedir, field) if isinstance(value, basestring): if value.startswith('base64-encoded;'): value = base64.b64decode(content[15:]) util.write(fieldpath, value) else: util.write_json(fieldpath + '.json', value) else: value = doc[key] if not isinstance(value, basestring): value = str(value) util.write(filedir, value) # save id idfile = os.path.join(path, '_id') util.write(idfile, doc['_id']) util.write_json(os.path.join(path, '.couchapprc'), {}) if '_attachments' in doc: # process attachments attachdir = os.path.join(path, '_attachments') if not os.path.isdir(attachdir): os.makedirs(attachdir) for filename in doc['_attachments'].iterkeys(): if filename.startswith('vendor'): attach_parts = util.split_path(filename) vendor_attachdir = os.path.join(path, attach_parts.pop(0), attach_parts.pop(0), '_attachments') filepath = os.path.join(vendor_attachdir, *attach_parts) else: filepath = os.path.join(attachdir, filename) filepath = _replace_slash(filepath) currentdir = os.path.dirname(filepath) if not os.path.isdir(currentdir): os.makedirs(currentdir) if signatures.get(filename) != util.sign(filepath): resp = db.fetch_attachment(docid, filename) with open(filepath, 'wb') as f: for chunk in resp.body_stream(): f.write(chunk) logger.debug("clone attachment: %s" % filename) logger.info("%s cloned in %s" % (source, dest)) couchapp/__init__.py0000644000372000037200000000031712276056621013053 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. version_info = (1, 0, 1) __version__ = ".".join(map(str, version_info)) couchapp/dispatch.py0000644000372000037200000001043712276056621013117 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import getopt import sys import couchapp.commands as commands from couchapp.errors import AppError, CommandLineError from couchapp.config import Config logger = logging.getLogger(__name__) class NullHandler(logging.Handler): """ null log handler """ def emit(self, record): pass def set_logging(level=2): """ Set level of logging, and choose where to display/save logs (file or standard output). """ handler = logging.StreamHandler() logger_ = logging.getLogger('couchapp') logger_.setLevel(level * 10) format = r"%(asctime)s [%(levelname)s] %(message)s" datefmt = r"%Y-%m-%d %H:%M:%S" handler.setFormatter(logging.Formatter(format, datefmt)) logger_.addHandler(handler) def set_logging_level(level=2): logger_ = logging.getLogger('couchapp') logger_.setLevel(level * 10) def run(): sys.exit(dispatch(sys.argv[1:])) def dispatch(args): set_logging() try: return _dispatch(args) except AppError, e: logger.error("couchapp error: %s" % str(e)) except KeyboardInterrupt: logger.info("keyboard interrupt") except Exception, e: import traceback logger.critical("%s\n\n%s" % (str(e), traceback.format_exc())) return -1 def _dispatch(args): conf = Config() # update commands for mod in conf.extensions: cmdtable = getattr(mod, 'cmdtable', {}) commands.table.update(cmdtable) cmd, globalopts, opts, args = _parse(args) if globalopts["help"]: del globalopts["help"] return commands.usage(conf, *args, **globalopts) elif globalopts["version"]: del globalopts["version"] return commands.version(conf, *args, **globalopts) verbose = 2 if globalopts["debug"]: verbose = 1 import restkit restkit.set_logging("debug") elif globalopts["verbose"]: verbose = 1 elif globalopts["quiet"]: verbose = 0 set_logging_level(verbose) if cmd is None: raise CommandLineError("unknown command") fun = commands.table[cmd][0] if cmd in commands.incouchapp: return fun(conf, conf.app_dir, *args, **opts) return fun(conf, *args, **opts) def _parse(args): options = {} cmdoptions = {} try: args = parseopts(args, commands.globalopts, options) except getopt.GetoptError, e: raise CommandLineError(str(e)) if args: cmd, args = args[0], args[1:] if cmd in commands.table: cmdopts = list(commands.table[cmd][1]) else: cmdopts = [] else: cmd = "help" cmdopts = list(commands.table[cmd][1]) for opt in commands.globalopts: cmdopts.append((opt[0], opt[1], options[opt[1]], opt[3])) try: args = parseopts(args, cmdopts, cmdoptions) except getopt.GetoptError, e: raise CommandLineError((cmd, e)) for opt in cmdoptions.keys(): if opt in options: options[opt] = cmdoptions[opt] del cmdoptions[opt] return cmd, options, cmdoptions, args def parseopts(args, options, state): namelist = [] shortlist = '' argmap = {} defmap = {} for short, name, default, comment in options: oname = name name = name.replace('-', '_') argmap['-' + short] = argmap['--' + oname] = name defmap[name] = default if isinstance(default, list): state[name] = default[:] else: state[name] = default if not (default is None or default is True or default is False): if short: short += ':' if oname: oname += '=' if short: shortlist += short if name: namelist.append(oname) opts, args = getopt.getopt(args, shortlist, namelist) for opt, val in opts: name = argmap[opt] t = type(defmap[name]) if t is type(1): state[name] = int(val) elif t is type(''): state[name] = val elif t is type([]): state[name].append(val) elif t is type(None) or t is type(False): state[name] = True return args couchapp/vendors/0000755000372000037200000000000012276056621012421 5ustar gfagfacouchapp/vendors/base.py0000644000372000037200000001540312276056621013710 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging import os import shutil import tempfile from couchapp.vendors.backends.couchdb import CouchdbVendor from couchapp.vendors.backends.git import GitVendor from couchapp.vendors.backends.hg import HgVendor from couchapp.errors import VendorError from couchapp import util logger = logging.getLogger(__name__) def _tempdir(): f, fname = tempfile.mkstemp() os.unlink(fname) return fname VENDORS = [ CouchdbVendor, GitVendor, HgVendor] class Vendor(object): """ Vendor object to manage vendors in a couchapp """ def __init__(self, conf): """ Constructor of vendor object :attr app_dir: string, path of app_dir """ self.conf = conf # load vendor handlers self.scheme = self.load_vendors() def load_vendors(self): """ associate vendor to their scheme Each vendor is an instance of `couchapp.vendor.base.BackendVendor`. Scheme is anything before "://" . See couchapp vendors' backends for more info. """ scheme = {} for vendor_class in VENDORS: if not hasattr(vendor_class, 'fetch') or \ not hasattr(vendor_class, 'scheme'): continue for s in getattr(vendor_class, 'scheme'): scheme[s] = vendor_class() return scheme def find_handler(self, uri): scheme = uri.split("://")[0] if scheme in self.scheme: return self.scheme[scheme] else: raise VendorError("unkonw vendor url scheme: %s" % uri) def installed_vendors(self, vendordir): """ return installed vendors """ vendors = [] for name in os.listdir(vendordir): metaf = os.path.join(vendordir, name, 'metadata.json') if os.path.isfile(metaf): vendors.append(name) else: continue return vendors def fetch_vendor(self, uri, *args, **opts): """ fetch a vendor from uri """ # get fetch cmd vendor_obj = self.find_handler(uri) # execute fetch command path = _tempdir() vendor_obj.fetch(uri, path, *args, **opts) vendors = [] for name in os.listdir(path): vpath = os.path.join(path, name) metaf = os.path.join(vpath, "metadata.json") if not os.path.isfile(metaf): continue else: meta = util.read_json(metaf) meta["fetch_uri"] = uri name = meta.get('name', name) vendors.append((name, vpath, meta)) os.unlink(metaf) if not vendors: util.deltree(path) raise VendorError("Invalid vendor, metadata not found.") return vendors, path def install(self, appdir, uri, *args, **opts): """ install a vendor in the couchapp dir. """ should_force = opts.get('force', False) vendordir = os.path.join(appdir, "vendor") if not os.path.isdir(vendordir): os.makedirs(vendordir) new_vendors, temppath = self.fetch_vendor(uri, *args, **opts) for name, path, meta in new_vendors: dest = os.path.join(vendordir, name) metaf = os.path.join(dest, "metadata.json") if os.path.isdir(dest): if should_force: util.deltree(dest) else: logger.warning("vendor: %s already installed" % name) continue shutil.copytree(path, dest) util.write_json(metaf, meta) logger.info("%s installed in vendors" % name) util.deltree(temppath) return 0 def update(self, appdir, name=None, *args, **opts): should_force = opts.get('force', False) vendordir = os.path.join(appdir, "vendor") if not os.path.isdir(vendordir): os.makedirs(vendordir) if name is not None: if name not in self.installed_vendors(vendordir): raise VendorError("vendor `%s` doesn't exist" % name) dest = os.path.join(vendordir, name) metaf = os.path.join(dest, "metadata.json") meta = util.read_json(metaf) uri = meta.get("fetch_uri", "") if not uri: raise VendorError("Can't update vendor `%s`: fetch_uri " + "undefined." % name) new_vendors, temppath = self.fetch_vendor(uri, *args, **opts) for vname, vpath, vmeta in new_vendors: if name != vname: continue else: util.deltree(dest) shutil.copytree(vpath, dest) util.write_json(metaf, vmeta) logger.info("%s updated in vendors" % vname) break util.deltree(temppath) else: # update all vendors updated = [] for vendor in self.installed_vendors(vendordir): if vendor in updated: continue else: dest = os.path.join(vendordir, vendor) metaf = os.path.join(dest, "metadata.json") meta = util.read_json(metaf) uri = meta.get("fetch_uri", "") if not uri: logger.warning("Can't update vendor `%s`: fetch_uri " + "undefined." % vendor) continue else: new_vendors, temppath = self.fetch_vendor(uri, *args, **opts) for vname, vpath, vmeta in new_vendors: dest1 = os.path.join(vendordir, vname) metaf1 = os.path.join(dest1, "metadata.json") if os.path.exists(dest1): util.deltree(dest1) shutil.copytree(vpath, dest1) util.write_json(metaf1, vmeta) logger.info("%s updated in vendors" % vname) updated.append(vname) elif should_force: #install forced shutil.copytree(vpath, dest1) util.write_json(metaf1, vmeta) logger.info("%s installed in vendors" % vname) updated.append(vname) util.deltree(temppath) return 0 couchapp/vendors/backends/0000755000372000037200000000000012276056621014173 5ustar gfagfacouchapp/vendors/backends/base.py0000644000372000037200000000064012276056621015457 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. class BackendVendor(object): """ vendor backend interface """ url = "", license = "", author = "", author_email = "", description = "" long_description = "" scheme = None def fetch(url, path, *args, **opts): raise NotImplementedError couchapp/vendors/backends/git.py0000644000372000037200000000232712276056621015334 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging from couchapp.errors import VendorError from couchapp.util import locate_program, popen3 from couchapp.vendors.backends.base import BackendVendor logger = logging.getLogger(__name__) class GitVendor(BackendVendor): url = "http://github.com/couchapp/couchapp" author = "Benoit Chesneau" author_email = "benoitc@e-engura.org" description = "Git vendor handler" long_description = """couchapp vendor install|update from git:: git://somerepo.git (use git+ssh:// for ssh repos) """ scheme = ['git', 'git+ssh'] def fetch(self, url, path, *args, **opts): if url.startswith("git+ssh://"): url = url[9:] """ return git cmd path """ try: cmd = locate_program("git", raise_error=True) except ValueError, e: raise VendorError(e) cmd += " clone %s %s" % (url, path) # exec cmd (child_stdin, child_stdout, child_stderr) = popen3(cmd) err = child_stderr.read() if err: raise VendorError(str(err)) logger.debug(child_stdout.read()) couchapp/vendors/backends/hg.py0000644000372000037200000000245412276056621015150 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import logging from couchapp.errors import VendorError from couchapp.util import locate_program, popen3 from couchapp.vendors.backends.base import BackendVendor logger = logging.getLogger(__name__) class HgVendor(BackendVendor): url = "http://github.com/couchapp/couchapp" author = "Benoit Chesneau" author_email = "benoitc@e-engura.org" description = "HG vendor handler" long_description = """couchapp vendor install|update from mercurial:: hg://somerepo (repo available via http, use http+ssh:// for ssh repos) """ scheme = ['hg', 'hg+ssh'] def fetch(self, url, path, *args, **opts): """ return git cmd path """ if url.startswith("hg+ssh://"): url = url[8:] else: url = url.replace("hg://", "http://") try: cmd = locate_program("hg", raise_error=True) except ValueError, e: raise VendorError(e) cmd += " clone %s %s" % (url, path) # exec cmd (child_stdin, child_stdout, child_stderr) = popen3(cmd) err = child_stderr.read() if err: raise VendorError(str(err)) logger.debug(child_stdout.read()) couchapp/vendors/backends/__init__.py0000644000372000037200000000020612276056621016302 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. couchapp/vendors/backends/couchdb.py0000644000372000037200000000226212276056621016156 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import os from couchapp.errors import VendorError from couchapp.clone_app import clone from couchapp.vendors.backends.base import BackendVendor class CouchdbVendor(BackendVendor): url = "http://github.com/couchapp/couchapp" author = "Benoit Chesneau" author_email = "benoitc@e-engura.org" description = "CouchDB vendor handler" long_description = """couchapp vendor install|update couchdb://someurl/to/vendor (use couchdbs:// for https)""" scheme = ['couchdb', 'couchdbs'] def fetch(self, url, path, *args, **opts): if url.startswith("couchdb://"): url = url.replace("couchdb://", "http://") else: url = url.replace("couchdbs://", "https://") try: dburl, docid = url.split('_design/') except ValueError: raise VendorError("%s isn't a valid source" % url) dest = os.path.join(path, docid) clone(url, dest=dest) rcfile = os.path.join(dest, ".couchapprc") try: os.unlink(rcfile) except: pass couchapp/vendors/__init__.py0000644000372000037200000000066512276056621014541 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from couchapp.vendors.base import Vendor def vendor_install(conf, dest, source, *args, **opts): vendor = Vendor(conf) vendor.install(dest, source, *args, **opts) def vendor_update(conf, dest, name=None, *args, **opts): vendor = Vendor(conf) vendor.update(dest, name, *args, **opts) couchapp/templates/0000755000372000037200000000000012276056621012737 5ustar gfagfacouchapp/templates/app/0000755000372000037200000000000012276056621013517 5ustar gfagfacouchapp/templates/app/couchapp.json0000644000372000037200000000014312276056621016212 0ustar gfagfa{ "name": "Basic CouchApp", "description": "CouchApp with changes feed and form support." }couchapp/templates/app/views/0000755000372000037200000000000012276056621014654 5ustar gfagfacouchapp/templates/app/views/recent-items/0000755000372000037200000000000012276056621017253 5ustar gfagfacouchapp/templates/app/views/recent-items/map.js0000644000372000037200000000037112276056621020367 0ustar gfagfafunction(doc) { if (doc.created_at) { var p = doc.profile || {}; emit(doc.created_at, { message:doc.message, gravatar_url : p.gravatar_url, nickname : p.nickname, name : doc.name }); } };couchapp/templates/app/README.md0000644000372000037200000000124712276056621015002 0ustar gfagfa## Generated CouchApp This is meant to be an example CouchApp and to ship with most of the CouchApp goodies. Clone with git: git clone git://github.com/couchapp/example.git cd example Install with couchapp push . http://localhost:5984/example or (if you have security turned on) couchapp push . http://adminname:adminpass@localhost:5984/example You can also create this app by running couchapp generate example && cd example couchapp push . http://localhost:5984/example Deprecated: *couchapp generate proto && cd proto* ## Todo * factor CouchApp Commonjs to jquery.couch.require.js * use $.couch.app in app.js ## License Apache 2.0 couchapp/templates/app/language0000644000372000037200000000001212276056621015216 0ustar gfagfajavascriptcouchapp/templates/app/_attachments/0000755000372000037200000000000012276056621016171 5ustar gfagfacouchapp/templates/app/_attachments/script/0000755000372000037200000000000012276056621017475 5ustar gfagfacouchapp/templates/app/_attachments/script/app.js0000644000372000037200000000434112276056621020615 0ustar gfagfa// Apache 2.0 J Chris Anderson 2011 $(function() { // friendly helper http://tinyurl.com/6aow6yn $.fn.serializeObject = function() { var o = {}; var a = this.serializeArray(); $.each(a, function() { if (o[this.name]) { if (!o[this.name].push) { o[this.name] = [o[this.name]]; } o[this.name].push(this.value || ''); } else { o[this.name] = this.value || ''; } }); return o; }; var path = unescape(document.location.pathname).split('/'), design = path[3], db = $.couch.db(path[1]); function drawItems() { db.view(design + "/recent-items", { descending : "true", limit : 50, update_seq : true, success : function(data) { setupChanges(data.update_seq); var them = $.mustache($("#recent-messages").html(), { items : data.rows.map(function(r) {return r.value;}) }); $("#content").html(them); } }); }; drawItems(); var changesRunning = false; function setupChanges(since) { if (!changesRunning) { var changeHandler = db.changes(since); changesRunning = true; changeHandler.onChange(drawItems); } } $.couchProfile.templates.profileReady = $("#new-message").html(); $("#account").couchLogin({ loggedIn : function(r) { $("#profile").couchProfile(r, { profileReady : function(profile) { $("#create-message").submit(function(e){ e.preventDefault(); var form = this, doc = $(form).serializeObject(); doc.created_at = new Date(); doc.profile = profile; db.saveDoc(doc, {success : function() {form.reset();}}); return false; }).find("input").focus(); } }); }, loggedOut : function() { $("#profile").html('

Please log in to see your profile.

'); } }); });couchapp/templates/app/_attachments/index.html0000644000372000037200000000440312276056621020167 0ustar gfagfa Example CouchApp

Example CouchApp

couchapp/templates/app/_attachments/style/0000755000372000037200000000000012276056621017331 5ustar gfagfacouchapp/templates/app/_attachments/style/main.css0000644000372000037200000000154212276056621020771 0ustar gfagfa/* add styles here */ body { font:1em Helvetica, sans-serif; } h1 { margin-top:0; } #account { float:right; } #profile { border:4px solid #edd; background:#fee; padding:8px; margin-bottom:8px; } #content { border:4px solid #dde; background:#eef; padding:8px; width:60%; float:left; } #sidebar { border:4px solid #dfd; padding:8px; float:right; width:30%; } #items li { border:4px solid #f5f5ff; background:#fff; padding:8px; margin:4px 0; } form { padding:4px; margin:6px; background-color:#ddd; } div.avatar { padding:2px; padding-bottom:0; margin-right:4px; float:left; font-size:0.78em; width : 60px; height : 60px; text-align: center; } div.avatar .name { padding-top:2px; } div.avatar img { margin:0 auto; padding:0; width : 40px; height : 40px; } ul { list-style: none; } couchapp/templates/vendor/0000755000372000037200000000000012276056621014234 5ustar gfagfacouchapp/templates/vendor/couchapp/0000755000372000037200000000000012276056621016036 5ustar gfagfacouchapp/templates/vendor/couchapp/metadata.json0000644000372000037200000000017112276056621020510 0ustar gfagfa{ "name": "couchapp", "description": "official couchapp vendor", "fetch_uri": "git://github.com/couchapp/vendor.git" }couchapp/templates/vendor/couchapp/_attachments/0000755000372000037200000000000012276056621020510 5ustar gfagfacouchapp/templates/vendor/couchapp/_attachments/md5.js0000644000372000037200000002072012276056621021534 0ustar gfagfa/* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet * Distributed under the BSD License * See http://pajhome.org.uk/crypt/md5 for more info. */ /* * Configurable variables. You may need to tweak these to be compatible with * the server-side, but the defaults work in most cases. */ var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ /* * These are the functions you'll usually want to call * They take string arguments and return either hex or base-64 encoded strings */ function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } /* * Perform a simple self-test to see if the VM is working */ function md5_vm_test() { return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; } /* * Calculate the MD5 of an array of little-endian words, and a bit length keep */ function core_md5(x, len) { /* append padding */ x[len >> 5] |= 0x80 << ((len) % 32); x[(((len + 64) >>> 9) << 4) + 14] = len; var a = 1732584193; var b = -271733879; var c = -1732584194; var d = 271733878; for(var i = 0; i < x.length; i += 16) { var olda = a; var oldb = b; var oldc = c; var oldd = d; a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); c = md5_ff(c, d, a, b, x[i+10], 17, -42063); b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); a = safe_add(a, olda); b = safe_add(b, oldb); c = safe_add(c, oldc); d = safe_add(d, oldd); } return Array(a, b, c, d); } /* * These functions implement the four basic operations the algorithm uses. */ function md5_cmn(q, a, b, x, s, t) { return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); } function md5_ff(a, b, c, d, x, s, t) { return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); } function md5_gg(a, b, c, d, x, s, t) { return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); } function md5_hh(a, b, c, d, x, s, t) { return md5_cmn(b ^ c ^ d, a, b, x, s, t); } function md5_ii(a, b, c, d, x, s, t) { return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); } /* * Calculate the HMAC-MD5, of a key and some data */ function core_hmac_md5(key, data) { var bkey = str2binl(key); if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); var ipad = Array(16), opad = Array(16); for(var i = 0; i < 16; i++) { ipad[i] = bkey[i] ^ 0x36363636; opad[i] = bkey[i] ^ 0x5C5C5C5C; } var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); return core_md5(opad.concat(hash), 512 + 128); } /* * Add integers, wrapping at 2^32. This uses 16-bit operations internally * to work around bugs in some JS interpreters. */ function safe_add(x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } /* * Bitwise rotate a 32-bit number to the left. */ function bit_rol(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } /* * Convert a string to an array of little-endian words * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. keep */ function str2binl(str) { var bin = Array(); var mask = (1 << chrsz) - 1; for(var i = 0; i < str.length * chrsz; i += chrsz) bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); return bin; } /* * Convert an array of little-endian words to a string */ function binl2str(bin) { var str = ""; var mask = (1 << chrsz) - 1; for(var i = 0; i < bin.length * 32; i += chrsz) str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); return str; } /* * Convert an array of little-endian words to a hex string. keep */ function binl2hex(binarray) { var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; var str = ""; for(var i = 0; i < binarray.length * 4; i++) { str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); } return str; } /* * Convert an array of little-endian words to a base-64 string */ function binl2b64(binarray) { var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var str = ""; for(var i = 0; i < binarray.length * 4; i += 3) { var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); for(var j = 0; j < 4; j++) { if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } } return str; } if (typeof exports != "undefined") { exports.hex = hex_md5; } couchapp/templates/vendor/couchapp/_attachments/jquery.couchForm.js0000644000372000037200000000235512276056621024316 0ustar gfagfa// I think this should go in jquery.couch.js (function($) { $.fn.couchForm = function(opts) { opts = opts || {}; if (!opts.db) { opts.db = $.couch.db(document.location.pathname.split('/')[1]); } var form = $(this); form.submit(function(e) { e.preventDefault(); var doc = form.serializeObject(); if (opts.beforeSave) { doc = opts.beforeSave(doc); } opts.db.saveDoc(doc, { success : function() { if (opts.success) { opts.success(doc); } form[0].reset(); } }); return false; }); }; // friendly helper http://tinyurl.com/6aow6yn $.fn.serializeObject = function() { var o = {}; var a = this.serializeArray(); $.each(a, function() { if (o[this.name]) { if (!o[this.name].push) { o[this.name] = [o[this.name]]; } o[this.name].push(this.value || ''); } else { o[this.name] = this.value || ''; } }); return o; }; })(jQuery); couchapp/templates/vendor/couchapp/_attachments/jquery.couch.app.util.js0000644000372000037200000000575312276056621025232 0ustar gfagfa$.log = function(m) { if (window && window.console && window.console.log) { window.console.log(arguments.length == 1 ? m : arguments); } }; // http://stackoverflow.com/questions/1184624/serialize-form-to-json-with-jquery/1186309#1186309 $.fn.serializeObject = function() { var o = {}; var a = this.serializeArray(); $.each(a, function() { if (o[this.name]) { if (!o[this.name].push) { o[this.name] = [o[this.name]]; } o[this.name].push(this.value || ''); } else { o[this.name] = this.value || ''; } }); return o; }; // todo remove this crap function escapeHTML(st) { return( st && st.replace(/&/g,'&'). replace(/>/g,'>'). replace(/'+a+''; }).replace(/\@([\w\-]+)/g,function(user,name) { return ''+user+''; }).replace(/\#([\w\-\.]+)/g,function(word,tag) { return ''+word+''; }); }; $.fn.prettyDate = function() { $(this).each(function() { var string, title = $(this).attr("title"); if (title) { string = $.prettyDate(title); } else { string = $.prettyDate($(this).text()); } $(this).text(string); }); }; $.prettyDate = function(time){ var date = new Date(time.replace(/-/g,"/").replace("T", " ").replace("Z", " +0000").replace(/(\d*\:\d*:\d*)\.\d*/g,"$1")), diff = (((new Date()).getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400); if (isNaN(day_diff)) return time; return day_diff < 1 && ( diff < 60 && "just now" || diff < 120 && "1 minute ago" || diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" || diff < 7200 && "1 hour ago" || diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") || day_diff == 1 && "yesterday" || day_diff < 21 && day_diff + " days ago" || day_diff < 45 && Math.ceil( day_diff / 7 ) + " weeks ago" || time; // day_diff < 730 && Math.ceil( day_diff / 31 ) + " months ago" || // Math.ceil( day_diff / 365 ) + " years ago"; }; $.argsToArray = function(args) { if (!args.callee) return args; var array = []; for (var i=0; i < args.length; i++) { array.push(args[i]); }; return array; } couchapp/templates/vendor/couchapp/_attachments/jquery.couchProfile.js0000644000372000037200000000723712276056621025017 0ustar gfagfa// Copyright Chris Anderson 2011 // Apache 2.0 License // jquery.couchProfile.js // depends on md5, // jquery.couchLogin.js and requires.js // // Example Usage (loggedIn and loggedOut callbacks are optional): // $("#myprofilediv").couchProfile({ // profileReady : function(profile) { // alert("hello, do you look like this? "+profile.gravatar_url); // } // }); (function($) { $.couchProfile = {}; $.couchProfile.templates = { profileReady : '
{{#gravatar_url}}{{/gravatar_url}}
{{nickname}}

Hello {{nickname}}!

', newProfile : '

Hello {{name}}, Please setup your user profile.

' }; $.fn.couchProfile = function(session, opts) { opts = opts || {}; var templates = $.couchProfile.templates; var userCtx = session.userCtx; var widget = $(this); // load the profile from the user doc var db = $.couch.db(session.info.authentication_db); var userDocId = "org.couchdb.user:"+userCtx.name; db.openDoc(userDocId, { success : function(userDoc) { var profile = userDoc["couch.app.profile"]; if (profile) { profile.name = userDoc.name; profileReady(profile); } else { newProfile(userCtx) } } }); function profileReady(profile) { widget.html($.mustache(templates.profileReady, profile)); if (opts.profileReady) {opts.profileReady(profile)}; }; function storeProfileOnUserDoc(newProfile) { // store the user profile on the user account document $.couch.userDb(function(db) { var userDocId = "org.couchdb.user:"+userCtx.name; db.openDoc(userDocId, { success : function(userDoc) { userDoc["couch.app.profile"] = newProfile; db.saveDoc(userDoc, { success : function() { newProfile.name = userDoc.name; profileReady(newProfile); } }); } }); }); }; function newProfile(userCtx) { widget.html($.mustache(templates.newProfile, userCtx)); widget.find("form").submit(function(e) { e.preventDefault(); var form = this; var name = $("input[name=userCtxName]",form).val(); var newProfile = { rand : Math.random().toString(), nickname : $("input[name=nickname]",form).val(), email : $("input[name=email]",form).val(), url : $("input[name=url]",form).val() }; // setup gravatar_url if md5.js is loaded if (hex_md5) { newProfile.gravatar_url = 'http://www.gravatar.com/avatar/'+hex_md5(newProfile.email || newProfile.rand)+'.jpg?s=40&d=identicon'; } storeProfileOnUserDoc(newProfile); return false; }); }; } })(jQuery); couchapp/templates/vendor/couchapp/_attachments/jquery.couchLogin.js0000644000372000037200000000750712276056621024467 0ustar gfagfa// Copyright Chris Anderson 2011 // Apache 2.0 License // jquery.couchLogin.js // // Example Usage (loggedIn and loggedOut callbacks are optional): // $("#mylogindiv").couchLogin({ // loggedIn : function(userCtx) { // alert("hello "+userCtx.name); // }, // loggedOut : function() { // alert("bye bye"); // } // }); (function($) { $.couchLogin = {}; $.couchLogin.templates = { adminParty : '

Admin party, everyone is admin! Fix this in Futon before proceeding.

', loggedOut : 'Signup or Login', loginForm : '', signupForm : '' }; $.fn.couchLogin = function(opts) { var elem = $(this); var templates = $.couchLogin.templates; opts = opts || {}; function initWidget() { $.couch.session({ success : function(session) { var userCtx = session.userCtx; if (userCtx.name) { elem.empty(); elem.append(loggedIn(session)); if (opts.loggedIn) {opts.loggedIn(session)} } else if (userCtx.roles.indexOf("_admin") != -1) { elem.html(templates.adminParty); } else { elem.html(templates.loggedOut); if (opts.loggedOut) {opts.loggedOut()} }; } }); }; initWidget(); function doLogin(name, pass) { $.couch.login({name:name, password:pass, success:initWidget}); }; elem.delegate("a[href=#signup]", "click", function() { elem.html(templates.signupForm); elem.find('input[name="name"]').focus(); return false; }); elem.delegate("a[href=#login]", "click", function() { elem.html(templates.loginForm); elem.find('input[name="name"]').focus(); return false; }); elem.delegate("a[href=#logout]", "click", function() { $.couch.logout({success : initWidget}); return false; }); elem.delegate("form.login", "submit", function() { doLogin($('input[name=name]', this).val(), $('input[name=password]', this).val()); return false; }); elem.delegate("form.signup", "submit", function() { var name = $('input[name=name]', this).val(), pass = $('input[name=password]', this).val(); $.couch.signup({name : name}, pass, { success : function() {doLogin(name, pass)} }); return false; }); } function loggedIn(r) { var auth_db = encodeURIComponent(r.info.authentication_db) , uri_name = encodeURIComponent(r.userCtx.name) , span = $('Welcome ! Logout?'); $('a.name', span).text(r.userCtx.name); // you can get the user name here return span; } })(jQuery); couchapp/templates/vendor/couchapp/_attachments/jquery.couch.app.js0000644000372000037200000001613212276056621024247 0ustar gfagfa// Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // Usage: The passed in function is called when the page is ready. // CouchApp passes in the app object, which takes care of linking to // the proper database, and provides access to the CouchApp helpers. // $.couch.app(function(app) { // app.db.view(...) // ... // }); (function($) { function Design(db, name, code) { this.doc_id = "_design/"+name; if (code) { this.code_path = this.doc_id + "/" + code; } else { this.code_path = this.doc_id; } this.view = function(view, opts) { db.view(name+'/'+view, opts); }; this.list = function(list, view, opts) { db.list(name+'/'+list, view, opts); }; } function docForm() { alert("docForm has been moved to vendor/couchapp/lib/docForm.js, use app.require to load") }; function resolveModule(path, names, parents, current) { parents = parents || []; if (names.length === 0) { if (typeof current != "string") { throw ["error","invalid_require_path", 'Must require a JavaScript string, not: '+(typeof current)]; } return [current, parents]; } var n = names.shift(); if (n == '..') { parents.pop(); var pp = parents.pop(); if (!pp) { throw ["error", "invalid_require_path", path]; } return resolveModule(path, names, parents, pp); } else if (n == '.') { var p = parents.pop(); if (!p) { throw ["error", "invalid_require_path", path]; } return resolveModule(path, names, parents, p); } else { parents = []; } if (!current[n]) { throw ["error", "invalid_require_path", path]; } parents.push(current); return resolveModule(path, names, parents, current[n]); } function makeRequire(ddoc) { var moduleCache = []; function getCachedModule(name, parents) { var key, i, len = moduleCache.length; for (i=0;i>> 0; if (typeof fun != "function") throw new TypeError(); var thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in this) fun.call(thisp, this[i], i, this); } }; } if (!Array.prototype.indexOf) { Array.prototype.indexOf = function(elt) { var len = this.length >>> 0; var from = Number(arguments[1]) || 0; from = (from < 0) ? Math.ceil(from) : Math.floor(from); if (from < 0) from += len; for (; from < len; from++) { if (from in this && this[from] === elt) return from; } return -1; }; } couchapp/templates/vendor/couchapp/_attachments/jquery.mustache.js0000644000372000037200000002237212276056621024203 0ustar gfagfa/* Shameless port of a shameless port @defunkt => @janl => @aq See http://github.com/defunkt/mustache for more info. */ ;(function($) { /* mustache.js — Logic-less templates in JavaScript See http://mustache.github.com/ for more info. */ var Mustache = function() { var Renderer = function() {}; Renderer.prototype = { otag: "{{", ctag: "}}", pragmas: {}, buffer: [], pragmas_implemented: { "IMPLICIT-ITERATOR": true }, context: {}, render: function(template, context, partials, in_recursion) { // reset buffer & set context if(!in_recursion) { this.context = context; this.buffer = []; // TODO: make this non-lazy } // fail fast if(!this.includes("", template)) { if(in_recursion) { return template; } else { this.send(template); return; } } template = this.render_pragmas(template); var html = this.render_section(template, context, partials); if(in_recursion) { return this.render_tags(html, context, partials, in_recursion); } this.render_tags(html, context, partials, in_recursion); }, /* Sends parsed lines */ send: function(line) { if(line != "") { this.buffer.push(line); } }, /* Looks for %PRAGMAS */ render_pragmas: function(template) { // no pragmas if(!this.includes("%", template)) { return template; } var that = this; var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + this.ctag); return template.replace(regex, function(match, pragma, options) { if(!that.pragmas_implemented[pragma]) { throw({message: "This implementation of mustache doesn't understand the '" + pragma + "' pragma"}); } that.pragmas[pragma] = {}; if(options) { var opts = options.split("="); that.pragmas[pragma][opts[0]] = opts[1]; } return ""; // ignore unknown pragmas silently }); }, /* Tries to find a partial in the curent scope and render it */ render_partial: function(name, context, partials) { name = this.trim(name); if(!partials || partials[name] === undefined) { throw({message: "unknown_partial '" + name + "'"}); } if(typeof(context[name]) != "object") { return this.render(partials[name], context, partials, true); } return this.render(partials[name], context[name], partials, true); }, /* Renders inverted (^) and normal (#) sections */ render_section: function(template, context, partials) { if(!this.includes("#", template) && !this.includes("^", template)) { return template; } var that = this; // CSW - Added "+?" so it finds the tighest bound, not the widest var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + "\\s*", "mg"); // for each {{#foo}}{{/foo}} section do... return template.replace(regex, function(match, type, name, content) { var value = that.find(name, context); if(type == "^") { // inverted section if(!value || that.is_array(value) && value.length === 0) { // false or empty list, render it return that.render(content, context, partials, true); } else { return ""; } } else if(type == "#") { // normal section if(that.is_array(value)) { // Enumerable, Let's loop! return that.map(value, function(row) { return that.render(content, that.create_context(row), partials, true); }).join(""); } else if(that.is_object(value)) { // Object, Use it as subcontext! return that.render(content, that.create_context(value), partials, true); } else if(typeof value === "function") { // higher order section return value.call(context, content, function(text) { return that.render(text, context, partials, true); }); } else if(value) { // boolean section return that.render(content, context, partials, true); } else { return ""; } } }); }, /* Replace {{foo}} and friends with values from our view */ render_tags: function(template, context, partials, in_recursion) { // tit for tat var that = this; var new_regex = function() { return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + that.ctag + "+", "g"); }; var regex = new_regex(); var tag_replace_callback = function(match, operator, name) { switch(operator) { case "!": // ignore comments return ""; case "=": // set new delimiters, rebuild the replace regexp that.set_delimiters(name); regex = new_regex(); return ""; case ">": // render partial return that.render_partial(name, context, partials); case "{": // the triple mustache is unescaped return that.find(name, context); default: // escape the value return that.escape(that.find(name, context)); } }; var lines = template.split("\n"); for(var i = 0; i < lines.length; i++) { lines[i] = lines[i].replace(regex, tag_replace_callback, this); if(!in_recursion) { this.send(lines[i]); } } if(in_recursion) { return lines.join("\n"); } }, set_delimiters: function(delimiters) { var dels = delimiters.split(" "); this.otag = this.escape_regex(dels[0]); this.ctag = this.escape_regex(dels[1]); }, escape_regex: function(text) { // thank you Simon Willison if(!arguments.callee.sRE) { var specials = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\' ]; arguments.callee.sRE = new RegExp( '(\\' + specials.join('|\\') + ')', 'g' ); } return text.replace(arguments.callee.sRE, '\\$1'); }, /* find `name` in current `context`. That is find me a value from the view object */ find: function(name, context) { name = this.trim(name); // Checks whether a value is thruthy or false or 0 function is_kinda_truthy(bool) { return bool === false || bool === 0 || bool; } var value; if(is_kinda_truthy(context[name])) { value = context[name]; } else if(is_kinda_truthy(this.context[name])) { value = this.context[name]; } if(typeof value === "function") { return value.apply(context); } if(value !== undefined) { return value; } // silently ignore unkown variables return ""; }, // Utility methods /* includes tag */ includes: function(needle, haystack) { return haystack.indexOf(this.otag + needle) != -1; }, /* Does away with nasty characters */ escape: function(s) { s = String(s === null ? "" : s); return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { switch(s) { case "&": return "&"; case "\\": return "\\\\"; case '"': return '\"'; case "<": return "<"; case ">": return ">"; default: return s; } }); }, // by @langalex, support for arrays of strings create_context: function(_context) { if(this.is_object(_context)) { return _context; } else { var iterator = "."; if(this.pragmas["IMPLICIT-ITERATOR"]) { iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; } var ctx = {}; ctx[iterator] = _context; return ctx; } }, is_object: function(a) { return a && typeof a == "object"; }, is_array: function(a) { return Object.prototype.toString.call(a) === '[object Array]'; }, /* Gets rid of leading and trailing whitespace */ trim: function(s) { return s.replace(/^\s*|\s*$/g, ""); }, /* Why, why, why? Because IE. Cry, cry cry. */ map: function(array, fn) { if (typeof array.map == "function") { return array.map(fn); } else { var r = []; var l = array.length; for(var i = 0; i < l; i++) { r.push(fn(array[i])); } return r; } } }; return({ name: "mustache.js", version: "0.3.1-dev", /* Turns a template and view into HTML */ to_html: function(template, view, partials, send_fun) { var renderer = new Renderer(); if(send_fun) { renderer.send = send_fun; } renderer.render(template, view, partials); if(!send_fun) { return renderer.buffer.join("\n"); } }, escape : function(text) { return new Renderer().escape(text); } }); }(); $.mustache = function(template, view, partials) { return Mustache.to_html(template, view, partials); }; $.mustache.escape = function(text) { return Mustache.escape(text); }; })(jQuery); couchapp/templates/functions/0000755000372000037200000000000012276056621014747 5ustar gfagfacouchapp/templates/functions/map.js0000644000372000037200000000002412276056621016056 0ustar gfagfafunction(doc) { }couchapp/templates/functions/spatial.js0000644000372000037200000000002412276056621016736 0ustar gfagfafunction(doc) { }couchapp/templates/functions/show.js0000644000372000037200000000003312276056621016261 0ustar gfagfafunction(doc, req) { }couchapp/templates/functions/validate_doc_update.js0000644000372000037200000000063312276056621021267 0ustar gfagfafunction (newDoc, oldDoc, userCtx) { var doc_type = (oldDoc || newDoc)['doc_type']; var author = (oldDoc || newDoc)['author']; var docid = (oldDoc || newDoc)['_id']; function forbidden(message) { throw({forbidden : message}); }; function unauthorized(message) { throw({unauthorized : message}); }; function require(beTrue, message) { if (!beTrue) forbidden(message); }; }couchapp/templates/functions/update.js0000644000372000037200000000002712276056621016566 0ustar gfagfafunction(doc, req) { }couchapp/templates/functions/filter.js0000644000372000037200000000003112276056621016564 0ustar gfagfafunction(doc, req) { }couchapp/templates/functions/list.js0000644000372000037200000000003212276056621016253 0ustar gfagfafunction(head, req) { }couchapp/templates/functions/reduce.js0000644000372000037200000000004712276056621016555 0ustar gfagfafunction(keys, values, rereduce) { }Couchapp.py0000644000372000037200000000031712276056621011254 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from couchapp.dispatch import run if __name__ == '__main__': run() doc/0000755000372000037200000000000012276056621007704 5ustar gfagfadoc/usage.md0000644000372000037200000000606212276056621011336 0ustar gfagfa#Couchapp Command Line Usage ## Full command line usage $ couchapp help couchapp [OPTIONS] [CMD] [OPTIONSCMD] [ARGS,...] usage: -h/--help display help and exit --version display version and exit -v/--verbose enable additionnal output -q/--quiet don't print any message list of commands: ----------------- clone [OPTION]...[-r REV] SOURCE [COUCHAPPDIR] -r/--rev [VAL] clone specific revision generate [OPTION]... [app|view,list,show,filter,function,vendor] [COUCHAPPDIR] NAME --template [VAL] template name help init [COUCHAPPDIR] push [OPTION]... [COUCHAPPDIR] DEST --no-atomic send attachments one by one --export don't do push, just export doc to stdout --output [VAL] if export is selected, output to the file -b/--browse open the couchapp in the browser --force force attachments sending --docid [VAL] set docid pushapps [OPTION]... SOURCE DEST --no-atomic send attachments one by one --export don't do push, just export doc to stdout --output [VAL] if export is selected, output to the file -b/--browse open the couchapp in the browser --force force attachments sending pushdocs [OPTION]... SOURCE DEST --no-atomic send attachments one by one --export don't do push, just export doc to stdout --output [VAL] if export is selected, output to the file -b/--browse open the couchapp in the browser --force force attachments sending vendor [OPTION]...[-f] install|update [COUCHAPPDIR] SOURCE -f/--force force install or update ## Commands * **generate** : allows you to generate a basic couchapp. It can also be used to create template of functions. Ex: couchapp generate myapp cd myapp couchapp generate view someview * **init**: Initialize a CouchApp. When run in the folder of your application it create a default `.couchapprc` file. This file is needed by couchapp to find your application. Use this command when you clone your application from an external repository (git, hg): cd mycouchapp couchapp init * **push**: Push a couchapp to one or more [CouchDB](http://couchdb.apache.org) server cd mycouchapp couchapp push http://someserver:port/mydb * `--no-atomic` option allows you to send attachments one by one. By default all attachments are sent inline. * `--export` options allows you to get the JSON document created. Combined with `--output`, you can save the result in a file. * `--force` : force attachment sending * `--docid` option allows you to set a custom docid for this couchapp * **pushapps** : like `push` but on a folder containing couchapps. It allows you to send multiple couchapps at once. couchapp pushapps somedir/ * **pushdocs**: Like pushapps but for docs. It allows you to send a folder containing simple document. With this command you can populate your CouchDB with documents. Anotther way to do it is to create a `_docs` folder at the top of your couchapp folder. doc/extends.md0000644000372000037200000001217412276056621011705 0ustar gfagfa#Extend couchapp Couchapp can easily be extended using external python modules or scripts. There are 3 kind of extensions: - extensions: allows you to add custom commands to couchapp - hooks: allows you to add actions on pre-/post (push, clone, pushdocs, pushapps) events. - vendors handlers: allows you to add support for different sources of vendor ## Extensions: Extensions are eggs or python modules registered in the config file in "extensions" member, ex: "extensions": [ "egg:mymodule#name" ] Eggs uri are entry points uri starting with "egg:" prefix. To just load python module use an uri with the form : "python:mymodule.myextension". To load eggs add an entry point in "couchapp.extension" sections. More info about entry points [here](http://packages.python.org/distribute/pkg_resources.html#entry-points) An extension is a python module loaded when couchapp start. You can add custom commands to couchapp via this way. To add custom commands simply add a dict named **"cmdtable"**. This dict is under the format : cmdtable = { "cmdname": (function, params, "help string") } `Params` is a list of options that can be used for this function (the -someoption/--someoption= args) : params = [ ('short', 'long', default, "help string"), ] `short` is the short option used on command line (ex: -v) `long` is the long option (ex: --verbose) `default` could be True/False/None/String/Integer ## Hooks Couchapp offers a powerful mechanism to let you perform automated actions in response of different couchapp events (push, clone, generate, vendor). Hooks are eggs or python modules registered in the config file in "hooks" member, ex: "hooks": { "pre-push": [ "egg:couchapp#compress" ] } Like extennsions egg uri start with "egg:" prefix and python module with "python:". Entry point are added to **"couchapp.hook"** distribution. Here is the declaration of coupress hook in couchapp setup.py : setup( name = 'Couchapp', ... entry_points=""" ... [couchapp.hook] compress=couchapp.hooks.compress:hook ... """, ... ) hooks are python functions like this: def hook(path, hooktype, **kwarg): ... path is the directory of the CouchApp on the filesystem, hooktype is the name of the event `pre-/post-(push|clone|generate|vendor)` and kwargs a list of arguments depending on the event: - push: `dbs`: list of Database object - clone: `source`: the source of the couchapp to clone - vendor: `source`, the uri of vendor, `action`, could be *install* or *update*. - generate: None Have a look in [compress hook source](http://github.com/couchapp/couchapp/tree/master/couchapp/hooks/compress/) for a complete example. ## Vendors handlers for vendor_uri in self.conf.get('vendors'): obj = util.parse_uri(vendor_uri, "couchapp.vendor") vendors_list.append(obj) Vendors handlers are used to manage installation or update of vendors. Like extensions or hooks vendors handlers are eggs or python modules registered in config file: { "vendors": [ "egg:couchapp#git", "egg:couchapp#hg", "egg:couchapp#couchdb" ] } (above is the default). Entry point are added to **"couchapp.vendor"** distribution, ex: setup( name = 'Couchapp', ... entry_points=""" [couchapp.vendor] git=couchapp.vendors.backends.git:GitVendor hg=couchapp.vendors.backends.hg:HgVendor couchdb=couchapp.vendors.backends.couchdb:CouchdbVendor ... """, ... ) A vendor is an object inheriting `couchapp.vendor.base.BackendVendor` class: class MyVendor(BackendVendor): """ vendor backend interface """ url = "", license = "", author = "", author_email = "", description = "" long_description = "" scheme = None def fetch(url, path, *args, **opts): raise NotImplementedError - url: is the url of the vendor source - license: the license of the vendor - author: name of author - author_email: email of author - description: short description of this vendor - long_descrtiption: long description - scheme: list of url prefix on which this handler will be use. (ex: ['git', 'git+ssh'] for git://|git/ssh:// urls) the `fetch` function take the url given in console, the path of couchapp. Here is an example for the default git vendor: class GitVendor(BackendVendor): url="http://github.com/couchapp/couchapp" author="Benoit Chesneau" author_email="benoitc@e-engura.org" description = "Git vendor handler" long_description = """couchapp vendor install|update from git:: git://somerepo.git (use git+ssh:// for ssh repos) """ scheme = ['git', 'git+ssh'] def fetch(self, url, path, *args, **opts): .... Full source is [on the git repo](http://github.com/couchapp/couchapp/blob/master/couchapp/vendors/backends/git.py). doc/index.md0000644000372000037200000000046312276056621011340 0ustar gfagfa#Couchapp CouchApp is designed to structure standalone CouchDB application development for maximum application portability. CouchApp is a set of scripts and a [jQuery](http://jquery.com) plugin designed to bring clarity and order to the freedom of [CouchDB](http://couchdb.org)'s document-based approach.doc/imgs/0000755000372000037200000000000012276056621010643 5ustar gfagfadoc/imgs/gettingstarted01.png0000644000372000037200000015311312276056621014546 0ustar gfagfaPNG  IHDR$):iCCPICC ProfilexXy8U_^8p12ye" $Gʔ!2&!R($R$E!V??ߺֽz:{hhpK=M#   =#B5͍a_+ _[7w'9l 0 AhLdހ1]8@#3aX􏍵l Iў<1/hX .F48^_s /kGSp["|Z-|zyk-|h  w['yu8pGz ~$ EI!$) I7ȷ.vB #S@y,}sT}0 P5w#wht~wx R@(u q? r I d(65 A0 & K`lmA"@3 A"B:1d 9@n/EA9(ʁ Ash OwhDtv?B0BX#N |a8D :qьF "&U$@R \H1R itD ÑgȨȔȼbɲFȖiUȭȓȯגϒoRPPpS(RXPSNQObkQKr|~I  GB$CIK)Ni@EHYHLrJʅ**jjZڝ u!uZI3`,*4+D,C"oiqrrsr>L'iHI .6.}(a=nnd:<< <><<=<&ռo~ 0  T K'0B BBEBaYa?BHȘ(ZTQ4DTtJ /!-V- n,,"~+($JJ&KK~*&HJ'JJ%ZVD6MG@N^.\V^v|Q[]De^Z{uu/544ikJhk6iRJFjighult trVnG_џ2`74407L05Y767n7A\553 1m1ffWޚ ?X[Z,[JZ[[ZZUYm[kZg[DR:VӶ˱?f`?uuIӒs'Nxȕݵ fVn^džg窗W'o>*>9>+*W}?k (hXxdT v n !8yXHhj|RصpPĉH:D E FZV.މi/~rs]pWCK¿~d,ߒavό杊_ wv?c6={|x 2a(] „c$>'@eD%coQh `Lv(^ /&W8Z1:${oXLY3ن9h8MHY\<\|UtuIHIܓ"ϧ`TB*fQ١5KǨe f(oela`kl~âްv B NaΉ'.Tv{|By}x}eM݂cBOfgG\jӉQ g'%I;ϑJzya-}6c0-bˌWrb];̗r#}t?Y%Me;wMʣ*.VU W3ېXiywU;CG&Nv9$f_ӖgL.}x1>|g$jTg86n$qrU׆޴ΤZ}|؂{_{?/-|\Ysj/I_7܌Qf~o#?8/`fvfh!E#?xt% KR 8)ةEiVatY I,l9oId\N0EQ3YpXrY Z(٩:9:Y;۝0wttvw615W P a;J F "#֣?̝}7pi|OBۙƳI%I8ߗv.靕yH+9Wms#u/@ތ**_^BWxۥ,N Dx[uֽ:z 6kmk}u?,~<ڍxշLA!rr#rc/E&8&Y^M_MCo|ӻo?,.}Yx eU|Mz׌o߿mq0ގY˳wb` h`M&L` uryBq1D"Ph &s 3PDEZH*eGӔh!>r4kV-6~v,6;'ׂO[-#8),\$rVULW$D9)Gia̔]S&  JqMUDTQ3j45h6t9+|5|fTbkbe*i5{oagie`g}hڶ.AΑq٩9+[{IO/6Mg>A~,p  ڰj\Q j>'TRWixwgzkL:~%jXttnŬ`y)jCnݵ{yewsfi?ʋKP2.鼻ZASXY}^{JSh"4kֶ}}(qĮO8zξ8=1eԄ+״of%ί/^ZȎA]p`By8cNZ @@ ră"X/.B(HցP.8EB - *4 R@JlD+ѷ3z9&ӏa8W[$#$%'"oRR'G$lRQjO}z& 1AI=2#C8c;s4 9K4[(;-{>gɌ7?u(DbE s$*$3beeut LݎĨ&ijkuu}r ^6J23 56 3 :ihe{.߾֡qiЅU=٣7G˯<i_ e o|7/p;왤sA)_R2f^N"Ӑktm2 B뢠%-G*ݩ]tcΑngЀPx7Iz/2.zKרo[7vD[>! |@ hAB*1D"dPn< 4N@ww1X#,jcӱ/ql8?\+#Y5or; FvJe6?[j >%GDYb2m7>2C#+P *V-vUU[F;znN^>5zfBB?E^;&!"ܒZ~)DK%35V6W8=I\\kOyxN;  pq ԗr>)-[.7"SSIO9xʲF朶N>A1%5sK{V 6+vmN9'p.q>oG|Mhv4uq?4f<5s|lNbZkrC4 2nfI]iϵv pV?r%9JW|MGz}_ABSxvk\ë:+={*9@ŋQqʉS|Na{<OfW?|^Z¾μ~F緢~[o'Fw~zk;Wϸ`˿y?GZ(z n܆E,]AG8'|3=?,!ߜ pHYs   IDATx]|TozЋ"E ,OQ@}*"> bAPEA| 4)ҤC !@Bڦ&;޻MB 33g3w3s-@D@" H$D!P$[$H$D@" 0Rؗ@" H$D@"PC~ X,D@" H$H$D@" P_C;V6K" H$D@" }9$D@" H$5.Y25}mD@" H$Gw[.IZRH$D@"  map RoRD@" H$gup^ ײFD@" H$5U{' +#WlMIJ]D@" H.V 0˞k}}sbnD@" H$!`K > f»Y^7V4 'H$D@"PIX@`,M%q6"  FKk>-mH$D@" xʝ%Qx]" zqbU$,']D@" H$#){(U40u &Wo$k~m'h~D@" H$i6֯RnizyT:%W_AA_Oaj~4-]БD@" H$@GPv6,] Wx8ϰ([-x\7=m07T'uO4xQND@" H$/F·^Y6Mzy|U_~%}@.=ӌxҕH$D@" y L_=Z?-"^ > B6,43SftdD@" H$CS6DW2fam_/Na*J*_/ jӼiӥ_" H$D"E+iz~8Fi"]6|$ 驴_IAtqFhggW8ϰD@" H$@ESx sEp"~24E>r++WJ7=imq\E]4-=m^.IW" H$DA@F{sX/k*GS*# jhOL6lOkA rQY_+H$D@"P('@SEp/\bVydmdEVhpg?Ī{g߇]}(W\mPٸ7x믿 4cH$D@" H6EEEGאY/oOe៍"Ny9~U B@g~4"~ov[_bPZZ*KW" H$D@"pVɥ[.] gLa_ڋ0'8kZmoa/5]}f} A_}'=.*q#H$D@" 8W3  r5 >v!+uc<4r.?~ Zb"QaC6jR;q*5 <\~}HVFtͼlDԎElB"7U ǐgg[7`៳z ~(QlHJiK34y@I6<Ж$&z,Il{.*2*#muDW!*bOH$RB%Y(a6"#` pξ䙪k]_8oM8O&hG]huc4^%ppr_aM_zV̏Qs h|EzuG̵a D\ I;2p(D3^in9_m{1 Qh"}Z݂mn(?bW x>PbA=Li2/#%5E9o^=ё꽻ܤ0Z:eXgЧQe0/`c _cqCB%*>7G>^ƃ½Tgr:z~3'bEW*mbίf`jt;_ @|f)܏'|žOѾSAS{vbϦ_Z̜p#4դ^z0rX7q%JD@"p!r+[ l8/˺x׺/¦B^:Bf› bƿSpXyG -xD4,TEif hEcx\QlAҾ;1ea_AqQ@`X m'$|P [D0C#pYNF4uACA pPJJ;A%1Ƒ%(jGLԫGIj(n7z9R32i% M7OM}H}R<|e"7S~QwxXAY68A &^I>{ة|1".r*ykQ}2^"PP` `w `J@ Jݍ^.~ }ڎ`Exem0g6ZN Q( S[^ ^{]7h;_>χW]7] cޘ0o39KoԔfܭ UG\=} <0~4/;cƘՏ|߿#[Q摠lrg%(>Xm;;OvWuC4IEizjLcI47o?-̹.6e*L wac/>F,Bfs.-RwKcM]$܁dzk*~0+]ҜE)]DCw XnB> B1"/9fa᪑?QYD0.\d5iD f?ZihX(BЈH 诎]ڍ'+8s̼Lh>ڇY_n}Pl Bj 0&viJvjQJp!BHXʢ fFACZĄE"$ЂPf AX (EaRɵօƢM#P0dyElj9v=p$5] ?Ay(*Gmw vDNq.pCᇧB^~Ѡgd:SZu+R-!(P BǮ@љLdŴF':'z8đ&}V)\g176mFKQ'P@7aT;V{^l܉q(<3FMk9؅yV0eT5]KAizto{K] G)coJzͱy[e ]2 OkZ`P`åq/sT>#T4ËAd'QJ%'z#dM°۞ Po}75EF][kjǺ 2Dm7DHjZS4!k{s=^yeR|U,%t,pZP8N~z@7LjEqƏ!?|u9C_0>C6;Cv4olcnT-t~`-ߗ~ywt9ik$[P쵢Mi6i0A~T S< lp ZHa,-P'$1$zy3f`{E9w4(ގ$"! 䶉+%59n/UfQ8Io E0@YJٔ8wQwNErzhEPd%w;>:>\CJ-Z'n4! Z*aBҌXԡõfFAMeqmGhh_= m5S$Xѵ^)͙&hކ,@o._{_+ \v}^'AMg G3t[Go?q=xLԻZ׫PjC6FŌFwcxth҂?1eN꿝xiϣ{vb޿p.GȎ'|O-۞BφYHFjƚ#G?2`ݘpk\]̟qZ;H.2TQgWȴ8Fɪqe8j]Gn?}}P;]!k-jz~h^퐞|&!m h'_"kS9rUe3 :#+$o Y5t-*tv)2zz~˱ԯVJB~#hgd.ӴoE(FǸv4mJj:|/¾v`W[Ƃ'xR?fC~Bz vDL5ܫ" I&=Blˋ ;ZT'RUah&kSBzBg_UE"Zg?;pOv ,d!;5y1 :8**֔d].toy?Mu]M$n?B6)it f̽+๯:鴛jg[h:vaBoGKO#CeJq&9"eZPّ|Q[4 ~0HAJnzẙy>҃쉘b`~2J~z V=Yi)tt q# %!2<%pX0bt9]đW52/ʹ$엣!t83] W~Ҙ}î;]y84-IǞ[ Ù:mEiLېjCDs }u~짮T>d 7Jx툣qX>j"9$?n`ÂD( 36Tb&"#в֕[45) lOb:>UN";Ze,a6,3-;fSj pܨ.,xr ,ei$¨]dXG-뤥\?%LIB+쟇ME|=z\b9?Ge/Evӂ}y:db'֭ڂWCxt":C}h2:^D$Dςck KQkt+mܨWӿBS4]7;.lByQ|G_L]3-Veqox@4[v:&(9ĴTG~,'w'^r?RwP͘T DbD%RCeZ6rxlT?~LUyiZ ׆ yۯ7NbK ;χtI>0t^5C6 fynL^`:߂ҹ8Ho"d!Ғ(jr7]H;v!ޫc{"dFZIj$ܗ}\B;ttDfRrk [~)MA[A^SE-룖 x|B4g̦ v];tAӧsiE؛Q ^Jj~xNA\.v^6CtfTb"`;Dkze,1hܴ+(܈t8}|MKJoBEN߭<}̯Nƈ*:ciyMhG@Lt Z`[.+kCiUG7Ơ~}ͨJI,I0rN$#o [TvKd  E|%lmZTH/14,Rkw=jw([6޲uDZvd1છy / ɄAKqD7SZj^ IDATt3SNsȄK_Xq"16f qK ȳߛ4ŌAn$kTX~ODv="T:rO6] M"N̔'=}fѢEe[vLÈ6-I[[Lw,cA -`<(hFwrAXqҶMrsBmSĵt*L3V$flWƇIÈѢ8v]&'eKDF 7evfW̄ R w6Mp0Yb,n*,HgIu0H%QDtCc:hz3ՊW 1meŕZR v'GV,-[Ȳ //S0&}xsZ)1kˈ4'\! ]~3&ʆdggJ|, [t`h?ټ<8ulj^I秌&:-w #:v'M+zWϼ͌ڹ:(E@iqyb:TNݽ6KPX a尔*|>7^1c/ YD@"Vea\Ȳ,Lˮî#\R9FqʇEZ =,s>v=JcX =Wlf80/Ea_D@" H$@!Fg⫒YgaU~|Nus! V20 ºy a#8B> |.*^Ʈs>ֺF~YIj*,ɳn.\mt6u*k*LW.6FI&\.ZW)&/?k%O;K1L4 ie2YUct &FU4ӂ`ino <{,ѹ_x0ϝ{s4v-l|ȂW6UܿLk'!lTPe`D!aDŽ~}cEVSƳ2IY"Pѿp 1` 8PeZU0k"O ! +#\C}*r CG mF [4:QuAl; M":a$DWhHӳ tR ~q*R_+gA,بa4umc|D Qnd0_EY }BHcBNԽ?c\ur2ᛣGH '߽=jechz|z6s>3{Ҝ<<})熧߄^ڡK#!o_=ɯ"#K7 3-6u|W7#88pȯsw|O<LF}U;!clzϯ.- 嚉*6m#]aہ&+ 147~] le }vV8#beh=kiޗflY>e-`qZ1y{R份*10&x?и]Ӈ7s[q!<#b L֨o#dž#x#"dLR (*FAD<ڶ>s!/ƄIj<M&ö́-o޵ SN8w;B8W DRf.RwnDXi:&Lj|]IV[X󌪾vKvlkkLiF圕E-@O[QF/`~O>۷sM75!j]Na׺gy̌tTѾS9$0`l++Ѱ ',"F!Gd-Fd6#ۂllGv!{ aē)U+?dߔ)?{nn\qJ3J\n*:m{TZC hrZtT~r\U2ԠM9n2t믻]~9˕X s/h3![rqZoS]uP[%W*˩MQJ(I:s4(s/N(>'I%0n6SC W>fV03MIOREfr& EBʄT~qRzcec*IƠ}kĨoE|EilfE_EUh{M' N ?Ǻ6wm(حLLX?b~U+SHsf|.|̓4{̞Sgn>Z(hb_)ͺx\^)#{e׮]JNNB=ܣ|g̙3iӦ)3fP>#eʽޫ }xKadYneXgYe\we`Y.fdYnfhY26Be%S~z.<[b#\_0fiN71YZ>Hu[vt=8hx9ӏl5̡S4CHj(\U-RurRݺR Ʀݏr/V"}n؂x{4&bѨo 8 3<׳/#R! VS<,է\ +Ej#} j;hy?B[~ޱ;ދ[??Cyn17vǾ>V5ӌB= {27=1 ŭ׎F…9+Fy: 3=JD̛Y9g=+̓4{4ԴSf%/i9RFq>ctG ! gH S=A^T 4t&xgf'(1&鋑뀢4lYGU o)x _or:]9d]%-Va "Ln!-T1)Ѳ6\.1)X;GM.9oZzKP+N\92FiѵQ^=7[Q]b#< 0Kwg#*.+d`oĻr?i\n$Z1 =GF u0H0H$+SsѬPSG~,%E-t),dr2O, <0fW=zuŠc#c:<.b ^ac߾5Ó54|l G1w"ZA9BMM6\(YCt<Þ _&m%ޘO3\|i4g1]4Ѻ7N~ 3 _1quhMʙA"})*˾8wQN@SvcΜ92:|ZVOvզ8ry m8głA[f,!ï"x_K![:H{6!ۜl+mɶ#ہd;l7/&{"^ɺ2mU&Ԩ ^R9%u?Vb6eԿ҆_ q26P;&(S+ܰȗLAq>&Z̸?*29 ([UyLjӲʗ߾5ӓOQo3qZIo+-9VtkjFFmP1Oh牵nTsi6Gr96Fc¬>G97Fs5JbsVK}ҙjqh'ƼsY~.QDWO%;;[)((PUyMMMU:?~\IIIQ233<5NӃdYneXgYe\wYe`Y.fdYnf5Y.2:PGM̺C_¯rgW0s_J"c#gaÌʂh^h ]=UP~{ngf0T¸ƐfLx˴6w1h!Ҙ8qɘ9rxڲprxx ᅂ[2xȇ;ݜo2Tq}n8`5`L@: U`d\J7Dny[#+u&cɔ˙-̈́UE\IQ$|hԨ7o~)A0u,@gS6K0[+~7Kv *\BA=ԭ\~7K3m }퉟p%w#D@" 8Xd ]kCڵA;/ S`>yv~B? ׺g]gS-xi5ƯDtfi5ِrYZ9BaF~M3fnfc5,' O"fkx[G2M" TbG,GEEo>3f$n> 6]¾KOWO:OZf8Ꮎ(ξX_o}{<4y ~Lor7Go{VݩiD>=qHڻ{(&cƎ};"ve$GƢ$n_@d(12+^b1KhP5!}>L̋Y*sѷfkd 2CBFfíWo)_IWW_m@Fd6#ۂlmO#٫,__D7n⭜(MAJH3ǙϦlSM/rY;i|/suM{?qr Ex/~֍. ŕon$i!WY=\6F_uT=|;PZҮ5s|)-T?ku+nխ/'GQ7rˣ}x~^N\WLWr?2Z̄ܿHl=In< :^CT}k4-_aYW,ǝRۯJgcY<oy~m >T^UyY46W镮~^jur.UYH1aSآ|^//=;4Kb}eD_.uuVyn(.rAnQԫ7ͤ$ɓӧ]r>B*< _%ـxXneXgYe\wYmF`Y.OV|EfYfyjJIYj, ٜtӫ79gۍiP,g@wťv(-xe: |$7f>/Z(C4fD=חrVÿ]IF`hYvϺ|X\@iGf|"6ZYWDZolX kHJd&@j׾ Sik&= <ٞTlէ10q\'Bq.3OիI-๷~[glرA5Q\&s%z ,~:'__q.Nz%eA7㨛nF*J](\<]cy) 6 \W| ^s8&{6a}LƳ}ZF~>]E<)1T3Wi=&^uh4y /ڷFck84lyxKT/Z+>&< $\7WzsӜ׾s{6}{ j!^)6/V>QQ{?MR5⿜SN|zM \~>ԍCn$nK@ê0eO>${s,?Q '*R^`ʽ7V<"㵜m%?pƀ{)M6SZ.Lcn$ i/CnE#OlGLֈ&O桼UMr4V׮%Ӧh1Ku߯MG%RFނs^p|;㪥ȟa;tQ|LW\gvBy# ˩@fϭӬ>qqPVaYؽ{7#[Xn˗}v'}A n0ЃcI:Z}0CxO *;upiX_hi9M>Or__^ YWm˓"Mr~<%MI5Ga:9j>ovoZ|,hƧ(ȮG$=ŬGa-i 0wiB w!I0sW/@ X WёYnux})F- `|puBLEn+@dX?vfuR*ݭ>q7tU?|8J>4Wa2$\~5YM۰(#OIG*-Ȋ;zh 2ss#:ĒڟMgYy[Wq!<#‹B=|JMNa#p|`Nm*j=K.,sGE|>?kv86TI?b_+'v y ua~.T=->%%V9ӛF =vlkkLڌ^4Z WbJ;bm^Aȷv"t2X(-aHKu^µKbo a:13Bc¤;ՇXqܪZM=J'-zR䭎rwY)']&kyf5?M=!oGbȢMuEƍQNuظq#Ӆ4R T5IBϥT= r^Űm&~'qXgMgxUMi!q<C=x J7v+﫺Z]y5#U[t3Y%7+Wzۈ5O}qi:׶Azmw'z:̅iJzzRʬ]d#GӘkW)MRZiM"Z=>j$`r.(cA3C5; 网^~1ӥg<ŇdĉJrc?^ك |㤋uxÞߦ ug\}M5At,wČ ǧtQ~gӌOcwIr`謸J6&D_y>+"T۪+72z;0:dnz@=e §hdIMQ'eG?Tx1k/\nU 3>Rrte{)__q)z}(q-vիW+$+7oVmۦlݺUٴiB;ʪU5k(iii> ImZa_]i QQLp")#/[pvw2&ո&E]A.aڐx"߸K2Y !N]~t"O6hMF2~T^]="ZAa|jWoGNtiRf]݀kǿ1uZ:Sn~p݀Xg%C<mGv #cfB+\~30v(*u )dK(KoHbsPxwbJfa,s|+ڏހ;Vw{q+wX:es 1ghԷYi78?.cdƔ&g6BB4iE:țBoQ.4.ܙmap+5h_O|yOQ0O5i UA˧\&SAszKh6Pa3^GJoxiGJ9_\[CʒcHD}#C+sɸ6K<{=2r.B1bM+΋=ϭ٪ n٬[h7Vӳ)1E03h3HäG#'7co8]DyG| ䷯TY#y)$t[۬Fivu HΣ9 @NFf.bk^/G=MDiMǩZP8hhP:ZCdʅڰxF\1ؖNm<<n=:]bЈ9]1X;QqqjnhrI#>9-;0{nb3&퐂&̺!hKh,Poˣv.dqawe;My '┈yH7t]햕{Tۨ}{0c 8]TL'ّ޷{Ď]Yx1k/Jp=t"ek3Աv>۴uf4] x itm]bRvVw<&f1 ӵ9cf:9.cFQ3^XWygL/{D^we(Hzif㺺Mn/ :2:[ě^˾8ξ(7tѤJÇ#=FY}}FX[]е=خ9ޢ EXXn}V+5e+Z tyo*E%K5kmv,S<Z4:leW+\_2~=UУ%hAsʕ~b*_}I jImȈfO\}}*;kK*̂(N-K~r3J͑XuIvcޤ+Ҝ*M㝪^%Ce18+LOQyq.V[H!J\K]Ab-Q*!/x8 ؔ]G1P| 5nÉ2euuS1^ƬMS> SZc9;3E!^3+|/hrV(/ڶ>Lm'u[L۪LY+/Tq1k5o'WhD=qVz|z5_3^iƼؔSs=T1ަ"}eXJFGjL(MmɏcI]Ug[ t(bZkh3\lGƆՇ~>u,c,h\6T峩2i9#NY2qm𜗌0 ֢l͟F|r9S\<<uVvZUEWUvC +GQ;F'TTMNNVUK}Y>:?T$t J\к–_/\~Ir"2؏)↮v*(Qw,+Rl3_^hVGZi-l;y7V6.ZI5:c"xsy&+#_ރCΖ`wŇfdx 4:45ℯ+h盐E9(U. T.Ў?=>ISit\״~={i*E 9|m!*W^<̈́,F `<t/Q˄!ZbJm!blB-DLx'c߱CVbM7!# Uɸv[N3NRbʻˬ6w,x7N|"9bg9綌I}bRS%=nVG|-}Ko-,E<uٯPaE#ŋ0yULZ+$DAnD GϵYӂ 3<.^zlK-Dy޵ga^Xq0WeWY9)&sT" H&a[@F"`Y)QXFGKV9eIXg#} _9 9s!V1)u4\8NϜJ$D@" 1unMd!^u0|չ_5F/$^][~8 > ^u4Y|wD_ii+2&e9z}[پRi~Xң}%=eD@" T,k~I"^+/ha 1;m GO L4 ie2bKajBino <{Tqt)>0ϝ{s4v-Nl[$;27ӿI,,3FBKFxD@" Xx b_kD\pRuT/hao_Eahpu!_}XMWBIS] B>`V0K3\_!M6)Kiv+1vVpEgؿ_Y>>F/u֞N^΃LakĘ0gPBԱw<9>f4>J逺{L€K]N&_R4NI$D!p }PPa~.ԡ*BBM=ƫN@bie||B[`ƄƣGeBJ€Oc} 뺽kS/qvipH\5OsVPp?߰(#OIAswؕPF]bgwaS;x,Vv,MlW%~֧|YlD!X'szW(+ [ݛ"UIՀ1h\w5 p\͜l:v}k'5ۈñ!ާ;y>ǒxT$H$rYd¿S @Z>u[vt){AZm gb9tʽѴsиX.urRݺR Ʀݏ7PKk5x.N,ētNJLĜQf7Agx#>^ϾܮKPLƃXӣx9H7)n_}ZٰR/*bQcJD{! Ah?zvXŭҡ-Bj'*׷fx1UH_(W h>4^;hEҩs,UO2J" H$ vgwqy ޙYU(*1&鋑뀢4lYGU o)x _o:nsȺ+ Jp=t"ek3IݦCZ0)s ipe`]49,v`jݿ.A;qsG(-06իf6ZlwA`lDy 0 pMT1Ov#Q92WAA"Yfh4?BQNv;,%E-(]cy}b(f06:fp=fW 9tA#^DzG ʟu`,< b ^ac߾5Ó64|l ٘c!9;"1YsT03 ;" (""[TD F&*_F5K"! $#& "0 ݙ9}S3sg˩穩S:d=dm?qˣ̪ϭqIa}+luËv֓ë1j\o g|JM2\ 2t[vǽϫR؊'<(jwԏ?]-S}`}>rfXE}}T53ǖDvǑ`.G$q| '3#]/~mAdrh֋ncIm؊{_UfkMgE|.^X:_>lgyQF+%@?W_Y˖-֮]km޼ڹsgYˡ -zheUVVZVqqUPP1z,볬ײ~z.뻬"z0ì~z2ˬ7z4ӬW~z6۬w-涮Nd=GzZqsq7+r9,.7f9NGRYpz uZi`%g"++>+~;Ci&-Jf6eNy2rC6Vp!M}wEyE:_\(Ei<89##ܞR'c\~P#AW&x_̉ҲyEh:Rk!/ڱYƏ>Kc_yS ,+ycxzr%M ,@jj*H7eY .NNNy.)j~c} jY_Kzk9,7]s GfSG\‛{e߭Sq!+@B g9h62*~Uep-|g9?F (%!~Yr4X4OcXiF8}*/1uj&7~qݫ+ IDATi3}5J@ (%@Mb!GŹfPaBwvB (%h^^PJ@ (%P H }"o٬gp-?׬qy=6듼Aiv}$xgshxga=q#W[nO̩1~q Җq0hƏx)ڧe(%@Mq|8]0 ?g aoS{^r|?v7ݴfq=M[4ni#M,cSZ|"qsn…-[nD}ki iKK̗]EUƷ l-?9ʌ%z>o:ڋ,-E0=F<w*PZZ$GJR0L[Z:ɱ<=9']sCO:۫j#T&"+{N{;v6"T"I\HfdR [O>$1%LŅ6%joRZ Q}՘ghKF1`f׭O1!^kOS7D}D׹zfո\8D5uy@*"OKp n8f.}^핚JLYۜMN#Ĕ5O-X1 gRU;¿6=|$gS%:zrzn!ˆ˯lpod]_2>%ylw+-K |xhSD2z!"6W/ d^K'_,g_VB|gqx)7\Mdsu=jz7r/%%c9>zry?u܉]]e@xJ 1 ^z<,OC5? 'IJRkgk}aDNc)v,[`}!' 3Ǭitܙ[2y]v_hv[ &[uޭE˭/'J;>8iOrOץOX; *bkNAISoےkŮ2oQK C8gUJ'T|JO}Sdd`g\eK֬iFϲʭ|kA4>}wV>9hs^UyFI7_}009?}y_| x]!^5~,r-^?cF>7r:r׻u4Qw-~e6Y D>A*)1ni)dKۤg ΧS?yEgL@Vr;)%P× ~~wXV{t.C퇍=;Ǔq٠\f O'~k|CΛᛢq8M 2_݌{z:C 1&%zHrX+Z@ZPhit,ׇ.W}<}Cqz [˝qeiɜGϰ< x&݆Ea0!)]QE|< Eٖ:K8[;ӏu$떿sp5MIkR}yM\dF^u\7OS߬PI(ti5ΧvU $,8U o ३)X )IӓNj./5L+-7^odwQq3x-tw/vS}b=a>V,#\^YYcQJfFXIOCJi>X_qWy ^= s^y5,R;Cjg^mg~1s^en~CUz^ӎ|U|&zs?okPqK >7wEE`0<[0VMn.2R~Ě/֓oҺi ?o)9mk)kbҴv!+>}m/Qx':ۯ;[mio趀/]Z6Àg/E>&>DO}d硬,7p2z8ͅ{iN>qN=hgKއ?1no n;},|A֖K}igDoH߿؅}KD~#s}5߸8y2֞+"@p~h xsU<gX %k_E7cKK[׉|> wK7vyҩMo V[OnǨq-B>G;s1z_9s[֞/&Y? +pWb©g]}ݟOfLe\s^qkg#9g[B36\nή'|v]L2{kHXw\Cnj~XVs=p#7~v=HY5m W,)[t4ⷤpe^e/%m?D^ nd1n|k-oҙ2"?<[py#mͩ)+܃b+yB(*P[k΢Жy%&|>1;?dw=$8n5q@rxo+GU{uχgK^{,XTٻ2˖e)e8%%%.VVVo;-;;*Udy$"g~Ӳb.ȃJ~ ˷5p3T1 ЇJ $0->5!  ?]#6%C^&=n8z%6Q,btBMU}6On'yLF(]1 !;~M/^:JJ[Egmj{ϖ V 6Uq|شv#rbl'fAẲ`'Fnہ]aR9j@ (%K@"~Уw(rj%@HC 8]Ҟ(%oS?)>xyݻY5J@ (%PD }VB;=ٵߘv/ V'wt3C[|66r xbjN3﹇&ِD;_Z^/s²엹6%̗?qKne7\v֯5->{Oٽ9[d8*%@ke[g`W 79(ݎg:i}M@?~?;n{G>-97Ö[7ا.OLIܹ͜n9_6-Rق=sk4\vշ7-܌[qiJ>;uKPJ9o߾;TdKږ<2vivL+uva*- xnYVXVYYky7@Rzk>xSgBoZZ_23]2άXE|bj=sՂas5CZZ|i؆k>/tnEk.1dj;G<5\j8MŅ;ڵ"Om\ډ)#D+wvGζS*%@3'G6`5j)k"9#N {yϯ)'j:'>1w >~w\4vV$_٘ɷAr filwXShd}K#JM1_9 o+~q^Ni8ˮ/._ïa[\vn8r܌H_Q0c7saCKi+Qu4I [t\<ރEeU~sb+Ay\rsߜLs74_>9HCz2ƶxsCc&21ϲc)~&]g|f>>k$_]nOFz\Y`B+pag_V!|o}^eϋTSzʪ%|^mY7o ?SrXv|v0_8 kL~%hqԩD\W VXyk~i"Ğ3f;g5 b4>,rD(ދ )2õoDSpI]puinOE?s= e.x˻ngð᝻24#"e-XOo ϽC`W>{^Q5mV,}5U_ 7Ӽ35~Z8g~8/unb68/܀g(>xޜQ,_Q}N̙6[3zN^;K6mc^}K|Xk~+]7 [bDz|VU;@s XԌ?{и%WPJ@ 4V,U!M[>-gwޝu|UkYdɶ%˚xv!ۍlwJpG=Ǒ=P^Mms5Uf^[w-*ǫ_?h{ZJ*DzR u(&[{)W&ǝֿB|v~Ay/Yןu)ZN{x!R&WV:ΰ~)-nKn-B7 N:ot_+Z2tk'݈X%`UY?XںpB"^kA8 3EZqI6V+ݳڵ+*_" CϽf}^234Y-D`cSZ5"o5eE5 ӭRU\j O?HCv̿ޞgD^;Jukg7wE~/9݆I%k&wZ6f%@?W_Y˖-֮]km޼ڹsgYV0\Nϵ˭buɲ+뱬ϲ^-빬ۋ,^1ɬ/3ѬO^5٬o⢛ۺ:9SldxY @P|.3$9k(N?NVWZ&Ca%,^[6 ^8]Z^G\bרOv+i=N@`1HAdd0N>[F 4V,#\^YYcwH.=okQ)e825~O׭8㐱i8 X:sq< |YζQsOg܀Yo=ӥ_W^x pnJ% O ?<4菿ق{y%hr|W?;_pV\p` ĢVKg2zv܇7^o{6zz+#s;0w:-Rʔzܒtv٭c"er,Ťi++>}m/QKj;ְu@6ldt-Y+&7WkXO6m cp3w={& pΌ7Tdce:T`y "HwT,REf=W>^~1t˾ 7~0'ېס301j`:T<{QoCuvlxrcÀg/E> ;CL}ؚ eFdz0B˸ͥ><"3nb48pv P~>?׭-i]c~_1m5oqOoW?C㔀PJ =jWV^slwX#_]c^"KS"/[0\4#Td;#K`r1ޚ*-h}1W _92xɯ/t^ -թZm=9G;{ěvU\WϖyŸBmia67dD;*_1r'_n7Zjfv-^喏[@^,< W1~sm2\BPobNȠmG=.u2754q}5n%(-+@Iq+m;c.UY[5o`kd?AWXiF8}j_~hVB/7!N~q^$+X!yVg߽oyv֋u}n[jLDC5?9g}~ͭ8p $#>s 큚B&#pXap cb;/ίLl/]+3r?ԊɦSy'>r f l̘`Ӛ_z ўmMIi8C:¿6_c qc2unW\R1uqg Z1%Lڷ6by C}cp㢱"߱e#F+G =;׀{防ZfJ(qu4%hvz\\ ݯG{k6g_VBjP]F9nFA ܀Y4=\RiC[X]q(8UIPJ a^:)fM[>)-?' ç;dN,=+#,J 7Xϐ~>q6/[k:/X KҼ5ֿ_,rj-{ }nkՔ)+<~VZkkNh}ˢ|n۷3/k;t]2ǧN2ǿ.37֊]ex3KcUfLFϲpR_>C+y=kǧewB.)GAǵ%~wۯˊ~.`2;HEˀn]')ؕˇ}iȜGϰ<hcJj);u7| x̬p{7e7M7EprxNq9;lzRz69[KsȢic9 ʥ'A|R7#kf>݋p<զdxnq~e6@SÔrsHϰ_pkrJ>`s LUAO ,$MOFkT_\[xӋqXCp{e=f=79v:|F_{?hIo^>7E?f=a;Û ҳ|{6zzLR:b }1{sό_~m[Η4m]ĊOEKpTɽ_\x;f0NK|oԣ&MNg$.zh|q,ϯ-z\܌]  I̦oۙٯn}J@ ( %m:2>ٕ,Z'2 wz+ίLs[9#.hyAӔYuƮP*n˅2o>3nƽIZ/KOm;\JoQ׏YF v֝34TzO綋21Nsp7~ckWI޾?E3ʺ_J[ҚYt{|ΊJ3F>\-R 2LY}RI-~}H9 g> zf*gGKkX}.MP@ ?9 3anZVh'U_`K~B Lq7uiԯP ')h;({ AnU@*% 4һOrٲ,RONNFRR"Ǖ!l֬"[Eb<9,7]s ͒8Vh@*P<)=/ijSWu7=mn.hpn itD"4PɌ>bͭGJNmՋıdzjA|@|p36EDk]cåV],m_Q!ڢqIˢKWc״W+%@ ĝݼk J9nDNx/L[)G_7ˬ={w%VJ@ (%@?e%)_@=]Źf |D㔀PJ@ (%dbԂPJ@ (%@lHXeCxomLa'"cG5܂;Cڦ>W;J≀^;Zzh˔P %>o5?ѳٽfܺd{JH眛paK֭ӍkWV9NMv O QA tl[z~t >%]㐮X m!2rNDrƎC ݏ.gz;~Uiie;vW>fhD#UVV-^}?1V )=v}Ku_^x_SJ@ (H'MN#Ĕ52rJ{z)N jjP MFJ` +\9 Ё>ہA\1eM`|?r1z,@ ~w\4vVh9M1_9vZFUx{MF>s _cI^Iumi8ΗaKtk߈#ͨ^>W0cԸwd_֬k_VBl4hI g:3?+P\jsCxnۧt$l~Ər{6?{ e@fq_ϐc`6mc^}ng˼CQJ@ $TAdnj2/۱b2.GIݰf ,ꧾ=ƏBˀnÓ^:㨎ᓔ)OZИ'}hH >"'Z)1q;u&l4|Cw~Ǒ 4>r 'ے _b 5O2_]JkXf߉踻n$Koo$E|?Z6ןE21w.KK??+ hUJ@ ZgTRJQ-ޣ~b,Zxo#tS7Ԩ4 Q|VHs޵k8o}m/Q|]%; ~T;ea<[hZ6HkXOHt_a nQa܄Ϗuft}qx{SGo4N (%2xl_[Rm۝[{~fmM6l6Y~TԎlv!ۍlwJpG=Ǒ=Pjj[Lz} ް--Ye5#Yޠ5q/#gXZ9r1z[ybxlW|5 @O\1juth?.5;7#y]UU'W_p vml9n˼Eb_ [6_]0+ZolZP5_sUf.YC5o)%;ϟo}WֲeˬkZ7oviYEEEVii -z bUVVZVqqUPP񣭷z,볬ײ~z.뻬"z0ì~z2ˬ7z4ӬW~z6۬w-涮N$=GzpkNkV,~rX\n4sȏ=O6NO߆ #);zuGu=(rU7e()nEOr۸lݛhyx`ǔCifmix87QU6*;|5+˃,yk(A^a)Ҕ]kKVGZ~uhP 4_B˖e)))HNNFRR-)J~} Y5_.0b9,rvYn7-+0a%E\m‛qvK[@*P,zon=%(->)PHO#.6_iѦur# Znn8Q476:4Jگ\?:h>q#DA7/o4-Y (( XiF-4PJ@ (%7wV (%;ݳE댦~3ۍ7J@ (%oS?)>xy7ݻc<%PJ@ (@\+wr{Sy o{~Z;:gI r xbjN3﹇ަ`$>"'^e/sml;\v/:[WJ@ (%ZŸh@v<IOC@?~?;n^OKsM%x SRkXC~$sfNTOshN[v O {[sl9f֬PJ 6ĵp3KeiJFq=wivLiԌLPhRSB\_g>xSgB[CKFcggY]@X`AusS+ +՞y^б }l9_h;݊ޟߗGz g\n:J@ (%@*G6`5jn/+3rD?>{L5by\;\ ;.;+ϯl:Mtc:\tB/^Zi?ڳ5sƒ&˯lt\H gr VLYv}v~e7:bsۿođfD9 ,\J[*~]iO ~Wbڋ|u8,rܔ_"̉4YJ` s{~s2|@ Ecɓap cbʚ?ZiWѯ+,tSPJ@ (@*|fA>)uCLS9U{tNi_Zh)猸bf m?K\o+b;HƧp}nLz&~>ZI~ YP:-މ.i+\>ŪQb<\{$c״9XpXV3~ 0'&4O󚶥iK8.㨟s9[cÓ̾0p4{s2>G|*+GIfg=|(N;3gRPJ@ 8VQ2VpWo:8hQPK=w:iIqoEw>&}2%5W8Mȧ?hanG?j+L<3ii8}< PTYpʗ!MDi&|s#)n>A(K=|wјRR_2LvZFF؁ϟ=f2gW~a ОCeKwb$FoaN򗶗3F݄'uQ'L{K F_3Kc['O%yZИ'Cﲲ"'O/be584p ͣ*VJ@ (%p^,عTx d&IG5Ck*3@+S$rsDʻ}%,^[6 ^hQx|n`p}TU$P@jgnW;KS'SX iz22qj'~wYXuzxwx[-FzR,#4$|$s@кƏ97zz10+~.yǬ։4@72w9qE>AkkTi\u>a^F>CƦἓ&` ݴ v*8%P H nS> GnlXJ{LF~؎½޸ y^DJ鈁~ysr!mL-)މNV1w29}bҴvivرc ۡ[dFF؂"mrs{m fzM?oFK-u2&MPVPNJ *i )~"ZOa+/gvZe_\%œoC.^k쫁j,ً2?~ү`cǓTt={)iKh`]ddWPJ@ JWP$7Z᧜,9zdH'-iw!κ7E@[\Q>y>9u(lR}˔ ,8K0q}LH; u5}C{Y Ԋ_Ӿ>/:r!n.3#PҪȲ!#vr ˟Fұ}Τ|5M56>g'__ms?[Ag})$w5[h9)<`|^tB7̇þs_ty֊>rի$.B<;nܶ9klyNe:u"ҲO}?+9I=\nN-~ͪ'PJ@ (%A]2,ҳZ[../~xvY&VGR'Ypz uZi@VVHi$-2J*{PIKҹ)gF$v\dU6GqW _\sŅR=̣32я:J.u2ZH&/Rn3xm,R`N͝㾶<&t/@QJ@ (%Lq웝'?/Eٴv#r\S #7~q %1S7BDx{IDAT>%PJ@ (f'OY߸.;S>@}Zzً>;08wGVIUc}}J@ (%PJ@ڧ 6mQi=Q{߄8#о}fmWS~SP2PJ@ (%+(+cO.J6IB67= m3S YG`*,\ HKK>n<1?D@%PJ@ ($k? ׵A9=/JAFz2Z"ur339h{`lh{mf5IdI0j!J@ (%PJ߬ ] |B {RS:mZT] 滬Oc~J@ (%PIw S=31h4~ŸF~*[m=ꗔ4gl}$%PJ@ (x%`L=2NkKHO : )d9A2٤v}ޖ3֍*>B>%PJ@ ('MF2Zh|R[EJ~RyN+Ҝ[,/ᛄX7'GHۧPJ@ (%1~*>, J2ۺ=iρr=TهE+PJ@ (%PM@ )S}**X̠Rɟ i|'&=('*!W (%PJ@ 4hE|/'M?Z5fl:EhmL556N (%PJ XWw,-ZcUٯ;?@i>Ú}@7'6] (%PJ$}R&ѓ*P7@zHWKmeƺ'>B>%PJ@ (f%OVJ*Y'SV$ V#XU Iѯ6IJh}߬â+%PJ@ (!\Uo7GtR-b_\na_IIIhUdSV*FZZijoRtOՒPJ@ (%@)O&YIqPPTA}R)~< )秵+Ѻuk=1nTُ)%PJ@ 4/VڏU=?⧮iHJF}/E%etlZNYYmކ5A7D-B (%PJ ~ !GaQ}!-NAKD+оp:蠘*1?D@%PJ@ (&0A8kǗdǃh mnI0OBz*(^]0p@dX7iPJ@ (%o١C\pV+I[a]~;vGAq+3~BjV6ca0rHG?mQ•PJ@ (%@m4##<~ZЕعs󗡨y]v81`t (++kڛ}߼|t%PJ@ (' C9Vxǻ)))xm?jOPJ@ (%мXiŝ]}_~qM@eee6JWe@j1J@ (%PA6mا䖖.زbB-j;PJ@ (%@3'R4+ +b9L˲X5hPJ@ (%Z+n߬,_<"_HD {TُѦ)%PJ@ ?6m?7Oi5+%PJ@ ~Z4-1cAי>êhPJ@ (%ZT_`͚5vǢE"S}n*2*%PJ@ ky3gF{ᒒ8ۭ>SPPJ@ (%p@0?ݻk(,,?75h@ُсf)%PJ@ F¹k˳JNNO5o OV>o/MPJ@ (%ↀ*q3TP%PJ@ (%07VJ@ (%PqC@*mPJ@ (%h@a4PJ@ (%@駟m5݋2ATUUٖwIIImff&ǃQe?FIۨPJ@ (%lڷo:ࠃ;s7HFiPJ@ (%@TُfSJ@ (%PN@X!mPJ@ (%*QlJ@ (%PJ >B>%PJ@ (%c̙,ޝUV`{9`̘1-ڠFTOO*%PJ@ $VŜvi<fm7X7iPJ@ (%u t7A]TJ@ (%P-@@U(%PJ@ $EbQ罏j(J@ (%PV}]gWcRm5nJS (7@^i%J@ (%PJ[ƍ'u:U7/ĉ,OQ-dl7h&%PJ@ (%LE}uU):-f[ٗΈ+sEPJ@ (%9}]>+QwYʾ4X6ӛy销PJ@ (%@Xz5f͚e/EXYCt`q듧^iz~*5I(a#zPJ@ (%@`EƏV׮]Qp갢ϊH2>oFJ9'_PJ@ (%"ǹƚꬢ׊UL*ʾQnkV`# {8kdWPJ@ (%h9~m'Oqv$~qӈ1"e߯Ar8kQPJ@ (%hVT߯;}\\/9s/F UgZ kL¦~5J@ (%PJY ؛O%>^bS5M2/C"`YdZϮfArXdN Nj]fE.R^&WPJ@ (%hU٤._Iא$[ApYVdxKelE.[SNLF!BԻH&'~q9 HZv~Yp<ų/7r`ʸ<2_PJ@ (%@b0uLQD YʷE7v5])S~6' eYPA@F;;gT(/.-qrM"i)16"PPJ@ (%@0K蘦;/ =,gLיӉqͰ[9]|4PH 4ŚyD2D.ؘi8 +"g%5K (%PJ@ $S䞉hs)3rƙyo3Δ_F}}ZhX9vn usX,9,Fe\gس_f#RatlUJ@ (%HI_DG4]Q9Y&rSf"7]xtb$F<5HmP:`*\4/q X_y+0Eg9ŚqF\SPJ@ (%L}{dE5]Ų\JXҋ̔SZu8oR4_govD~ N{ Ky~qyX.ӱ$̮Ɣ$W (%PJ 'Q\(aKX\3,g#~g33]YE6/sDqvFȜ.\Ǜ2#mNS2+~ gߔ_ (%PJ )pΰ(z,terJPLuNFsXi3fluE3P]Sm]˓$QJ@ (%H,SJL]Sw+r)%]6.S45hυJTfʸpt"7]30.e(/aNF_ұ_î+~3ΙjPJ@ (%7r0֩sXdfZ9]3 %lPlu%3,ZnCZ}Sі0gaXgX Fæ^wN26"?EƮXgWPJ@ (%@`]fXr?_\\+M٘$$8ӕ&qBƳ-ZP$/y$̐,_W{N'?I~v~q:6f#n(PJ@ (%kJDsEqӲi g:v9aS. rBw&6ǛaS6g˰$/٘|H8O4cr6$ %WPJ@ (%XGd#}SDqfXNo%,FP:\W:I(}n((1"riXiwʸN/qfJa(%PJ@ $.Ř'XxK3~/~q%OC\*6N͟PQlċˀ8"Fd];KtٯF (%PJ 1(.J/.˝~Ivň_\;ݺks@q K/ eA.eӰtP/$=06eȤ.q%tٯF (%PJ 1(.J슕޲*2.i9%50<3,~aia3Ne" ɕ2gXKc̴'e؋26"gYs,<?yk)bJ@ (%PK@tGMEƮ(N']yXƆel BLS&~gzgKb/Ne++R!iVٰ̰#D9,c-uIdˤ> K:QJ@ (%8pɮ7%̮ʹ܌g3l3Dnʤ N6FMۭ!rz1f~I.w(ro*KR缒FXF (%PJ zX_df?LrG#7g#q↤r<qCzFw˕lV.aq8/a78VE0g*"9E첌 n~;2GMPJ@ (%⛀f/X&rӉ.*ʻ)ĕqNNL/a;"?MKCLa5 D7],R(/eu\ M#aq8+%PJ@ 7-ŕޘaK]Q9-٘rI/KtlnHZ-pTn}t*ƚ4^ҺqEYtFʁY,727l 66NPJ@ (%rϺ(0E˕\9δf\_s9qHnm$0[슜]Q,Q98̮N:5ˤH>jPJ@ (%b~0Ly%̺aW~~k2Űى'Ze߯li(ޒVs JnDA0?g)]0N#r +%PJ@ //Ӕ_7{R8 \ʕ2DndʾRn+rL?Ű>T%(dž~3Eӱ^n(Uu: PJ@ (%D_Ht"gWtR35u<%PJ@ (D# _"7]KKX\75ĕr弦q͸z eߥVn+lpN#"3$|"7v??6 iwװPJ@ (%@;oqutE..lj?um5 A2p#鷣]83]+Ҹ8 *%PJ@ /7E)37]$8lf:7g pCs}eHJ5lj4/~iD&i$[l%55f+%PJ@ $>%_¦LL'2ӕNcćB_;)}g0QK'8IXʒY.yD&HXkLFJ@ (%PK@tG9of\_6ʍjX#0h7rIgWDd^-NfYLJ@ (%PM@o"W$\9X 75:F4Kx8_ʾ}1k?[D^Ux6pHZo}̡!%r! ЙG7r5v`%@.9{N~Fmq?'GM< ޏ}^47u~#qy- @bklXuMƲ͹>x|j3rχ9ΜwoknXkNS3%@S`TdXg?Pbu,>o\׳qpX5d^ȏqd=Y# @(pTY?j?>f?m>:u{5O \Wrl~n7wt&oc:LzNun>oL @/Ȼlk?_r.Ͻؼ,xȼt-#ǹ&&b=7՜\%@ "ZKּgb1}Ҟf?Oy?RPY,G\%@ 0x5M~|ZR_R ])WriH o+yYgSZ(/gAZ[s @`_â|r4wE~%>_iWVr<4 @%.+9+~iZ|ӟ>R?A @ ,kߴo9RCQWwc @n*CQ3د Ҙ @K]K~b0~ @S#Q>`"F pO(WK#@ . @vP3 @-f$@Q@ @[(] @`Gߺ; @l!co5IENDB`doc/imgs/gettingstarted04.png0000644000372000037200000015762612276056621014566 0ustar gfagfaPNG  IHDR2:iCCPICC ProfilexXy8U_^8p12ye" $Gʔ!2&!R($R$E!V??ߺֽz:{hhpK=M#   =#B5͍a_+ _[7w'9l 0 AhLdހ1]8@#3aX􏍵l Iў<1/hX .F48^_s /kGSp["|Z-|zyk-|h  w['yu8pGz ~$ EI!$) I7ȷ.vB #S@y,}sT}0 P5w#wht~wx R@(u q? r I d(65 A0 & K`lmA"@3 A"B:1d 9@n/EA9(ʁ Ash OwhDtv?B0BX#N |a8D :qьF "&U$@R \H1R itD ÑgȨȔȼbɲFȖiUȭȓȯגϒoRPPpS(RXPSNQObkQKr|~I  GB$CIK)Ni@EHYHLrJʅ**jjZڝ u!uZI3`,*4+D,C"oiqrrsr>L'iHI .6.}(a=nnd:<< <><<=<&ռo~ 0  T K'0B BBEBaYa?BHȘ(ZTQ4DTtJ /!-V- n,,"~+($JJ&KK~*&HJ'JJ%ZVD6MG@N^.\V^v|Q[]De^Z{uu/544ikJhk6iRJFjighult trVnG_џ2`74407L05Y767n7A\553 1m1ffWޚ ?X[Z,[JZ[[ZZUYm[kZg[DR:VӶ˱?f`?uuIӒs'Nxȕݵ fVn^džg窗W'o>*>9>+*W}?k (hXxdT v n !8yXHhj|RصpPĉH:D E FZV.މi/~rs]pWCK¿~d,ߒavό杊_ wv?c6={|x 2a(] „c$>'@eD%coQh `Lv(^ /&W8Z1:${oXLY3ن9h8MHY\<\|UtuIHIܓ"ϧ`TB*fQ١5KǨe f(oela`kl~âްv B NaΉ'.Tv{|By}x}eM݂cBOfgG\jӉQ g'%I;ϑJzya-}6c0-bˌWrb];̗r#}t?Y%Me;wMʣ*.VU W3ېXiywU;CG&Nv9$f_ӖgL.}x1>|g$jTg86n$qrU׆޴ΤZ}|؂{_{?/-|\Ysj/I_7܌Qf~o#?8/`fvfh!E#?xt% KR 8)ةEiVatY I,l9oId\N0EQ3YpXrY Z(٩:9:Y;۝0wttvw615W P a;J F "#֣?̝}7pi|OBۙƳI%I8ߗv.靕yH+9Wms#u/@ތ**_^BWxۥ,N Dx[uֽ:z 6kmk}u?,~<ڍxշLA!rr#rc/E&8&Y^M_MCo|ӻo?,.}Yx eU|Mz׌o߿mq0ގY˳wb` h`M&L` uryBq1D"Ph &s 3PDEZH*eGӔh!>r4kV-6~v,6;'ׂO[-#8),\$rVULW$D9)Gia̔]S&  JqMUDTQ3j45h6t9+|5|fTbkbe*i5{oagie`g}hڶ.AΑq٩9+[{IO/6Mg>A~,p  ڰj\Q j>'TRWixwgzkL:~%jXttnŬ`y)jCnݵ{yewsfi?ʋKP2.鼻ZASXY}^{JSh"4kֶ}}(qĮO8zξ8=1eԄ+״of%ί/^ZȎA]p`By8cNZ @@ ră"X/.B(HցP.8EB - *4 R@JlD+ѷ3z9&ӏa8W[$#$%'"oRR'G$lRQjO}z& 1AI=2#C8c;s4 9K4[(;-{>gɌ7?u(DbE s$*$3beeut LݎĨ&ijkuu}r ^6J23 56 3 :ihe{.߾֡qiЅU=٣7G˯<i_ e o|7/p;왤sA)_R2f^N"Ӑktm2 B뢠%-G*ݩ]tcΑngЀPx7Iz/2.zKרo[7vD[>! |@ hAB*1D"dPn< 4N@ww1X#,jcӱ/ql8?\+#Y5or; FvJe6?[j >%GDYb2m7>2C#+P *V-vUU[F;znN^>5zfBB?E^;&!"ܒZ~)DK%35V6W8=I\\kOyxN;  pq ԗr>)-[.7"SSIO9xʲF朶N>A1%5sK{V 6+vmN9'p.q>oG|Mhv4uq?4f<5s|lNbZkrC4 2nfI]iϵv pV?r%9JW|MGz}_ABSxvk\ë:+={*9@ŋQqʉS|Na{<OfW?|^Z¾μ~F緢~[o'Fw~zk;Wϸ`˿y?GZ(z n܆E,]AG8'|3=?,!ߜ pHYs   IDATx]|U^z"E((A]VA׵,bYl4QE:RC $y̛{y ¹Mn?wge\A    p8;IY   # dT   aG@a\ A@A@A@2*}@A@A@A# dC.    >    p8%]dcqR   u}~uMfm&   [2*"   pD$^ZPY{D.   'bzh hmgm>%   &at#ML @F    `g0ip$a%AP'rgձjJ    pLsJ8 Hi 7``p`e[A@A@Í@@U(tviuчr2ZD05sJ-  @}GL~5M !#uDB166Lczq    p$%inCZr)=$dDԊ(Ô_Fp)WrA@A@AH"`K J9ū8ezn3UCrQQ"j&f?WaJ­q]'   ;rhf3UafgH딌ւ r+[7 A@A@Ap"`II`ipȯcQhDL~V6W69oqN&iH   VIUfLktsg L=M]:!uDDDm`pe+qŒA@A@AH `$|0c*r]g1'{ CYTT    ;"yYg ʣKIYFI^=Íi90? ehHdWjCɪ"Ҫ0pxTrr<#*FA@A@A 0LJSRR.ҥl0s:es9 )*Hv]Qc 7\Èa, 1?pΛ7XO @b&FDD[@̒Yȟ?kB&n nڬ2ga%c* ݄t$'ӣA@A@A4(y"Q UDLH8)Bf6 _=KtNJ?K.s ϚKbG#)!MZDr*{g ]yڜ}bccvr̂8m v:-ܳywA$\? -sَŘjѬe 4oZ2rp`_(lX.@C"[mR^eލ.'U߷r#hXrH=# p=vd2؋QS1J8")vfTMFVi8,wkN"Rlzg2ƣkx@@yg–YsGas6f},>bNZq& >H:{(\a_YOi4vf2m[OW6 h!w+w|K,N]Yׁ zu(Ry cMۅ/wz9W]RB1wֵlZ0.hS˲J3ނs=Fv Pp%,?P1! vZ*Ʈu+b}8% X~V~?i=_¿l',Wݘ1zӐs.*x>|(77]H/ U|i)S*p565z:U RD$)*"6IK~#+ш'fс 61g;+DˎhwR!bR#|*R ^Z"9*G&uw肘67qc21cd$CGos*ӱj!/, *[N{B*c/(_܂ (4--G+L/}~;˒\HX\,}⡽&,({|ܡ[ [e*,^oWOrUP;3h\x(>'49`wP-mH=q;/t?M߅Qt# P+b!v`aX\Z49Z-ZP#2j'JQVaVzJQ`);fLϋШ= :-Jx0Q+K_#t`D)8On+߸z 29d-4aqqxغzknˁ˜f`S+_Da )٦ؒ =0S%oᑯ=FxB @ ̭5fN?xS|ro&׆*T` 3ۜF "@tc6lA0,(?|b\"2q(^O-ua~oII1i ) CxLMTpTdE"<:mN¾+Hp^Zպk4i:D)2"aep#\h)H_؛QO'B9YUq1 YYSIvlnƻKx`>wLxCOmuoz{׽UNzyD$0nƙQzLڛd?Q08U9>R\`4܎DDsy?]9w$5Cڼr'=wml:uWI(eO AtN xU=7+[q\<>:7"uĽ+#+pExap5.y>{S{l>NhaGce]`:lҧNa 3!y̯(œh&i8oq()RݠɨMѺG<*+nn≠d⫙hD!q@"OG i6#xɧO!F^RBIOYQ"tl&0zBI9bD!KI׸h:HkRL<]&Y1(Ą\.ݤ+$JvYxieS,M,8ɬ `@%Q"$<Ų%~vE'@ۙ}===ciT #b6=()-D-8W>OG^i N$ 10djz*}.V`?vlP^F+ZN=MWF\>\jpGŹ+Ѵy,q3Š B<žQ$ (W*Nz\Ey(ۃ{Sóm2*0^rV ^2aCZ2d3},IJ7 SAbS_g=DVŽz$"|)ڼ\S}-pɈKqˀ$g;.)^d.mx?m&w>TRuia(!b  #2RSĔo̿m.'1KcJF}Vzspȓ:lnySQB2$r)jҕG=Pe#zR7jB}O43FE~EeDlˋQV"]z[2ٳʧ8A@xK_#?j| #j9$;e@;r)ݍD0'sJ%m#ݕ:yk}JEgϡvY1=jIhw6,=m'3t*v͕o?1 )2d yitR%`fV2CNFC 5'wF ab4~9H侀+dwUbWp߫ 83G"uƝ*\[՗sY~&}4-/C (7QEЬ"=4:S\D/>q1QB7ĵCa.j,+WzS}6ׅng}ok *VshuNEdTa%APT'RfG4JBMH n{g8:>ؼ}uex #ƿSVހ;ϻLy=iX5(X!,}>LJ'nK3앴F @(^ygls #ny[U]&?@)j8”(VĪEeFqUӻ>߲-: 'O%2]NFKh $a{a>*oӻAnX(Zie8)" .zM"u75FRiPTgLm go8$^9 RI6hzX,F|dNhtݴK @»gS5J? 'U8E8r30LG-Y6LX=w#Y!$._c#ćɰJo<{<ҿ=Vak-nz"{ rLv#* $'r~7hʵ 3_'=n{+ڽkGg!7x^, NɩArD%Qo"U*VsNF!f [2Y;$WkVfgi4QiؗUȄƴ$RۊG1e$\rZϦɗ@-%9X_KOFӭ_ĦȥA@" DrS]liplҙxR?2jTڮNc8otp? -別YV wL(ό!F38tj`YA.Df1 hJBo-9R;AE/UHvAbdqU~mjm4:ԡX5n2J"7]|:WQuI*?xE"]T(nys'ٸz"7f I4i\Leѱ沈qz/_~RV"?=-< o<ۓ_WиlE:7|Եh9M+.-g|WFF>6=']-<wxsT/6FJO}-r֝]Vx%>vRZs)C:Q ~}S\9/~/)}{J$f{ai\<<{7{oD]Y.nxO礼4 t-A@ V|m)f\pr06ڜNg;h% شLW)e2l8xq+D 3ƸN_}(0LJJBtz؄f'&43dۣٹy4۸e;E*h6Jfd&1}%6PTP}ܞFPѨC2sI?|qh8tR/=AK##ۍɗi  @\h[tPq-Z.F[P72 *߬-řp\ƾ4 kӧcve?aH9]yصN-_qi IDAT8$ΌssN}G5)1Eٮ7^J _ź'ys4ݑ"H݃2U'Щ)FW|J%D"PUvMKiMi/x99[ջ;[QBJx2ڶᏞM%ىTŸO@P`FF֩rKh;:)q2h6 ] þtlP?h Yؽ3@ëWlQve1-# *C n~236Ұmv0al6t L4C%L a8LI[mv+ӧOzeMFՑ iJ\+V#M$/ Ŵ7N9D l*hl1LȨyQtP;t9Xf|xl 9h1A4":kj/, JfA t0ZQNRwХ2'm5;e H:P& *E;"NT>l(;Ґ -yND^d3"R.B1QBA W\q`J.&"*fR~6*NoC!fE`13:<*N)[Res4pL\`؄CLAOezZUSOdJ&}sK2HOCrY&-I Md e9$3+f.j$@,5j(!H\J{ĖзG,ZlRԨ ag԰_Z6,SI3U;  #bL\l4l4ʦ =lT_O||p# 5&&7n6Jysѯc+"vtU!_ dff&4Eh U~c+"Jw A@A@cS |*4w"Ɯ&P>O.Q6c8R6UED}deJ9HPi<*}}PA@A@cj`P1r5Wi9^ű֦.ɨUETfw0"A@A@ABy*E*<0 <7P2uAFYenlǶ89N}F   @*ʦȥb'5(?*|RϨ4`t4]F\ ͕`a| '_+7dL3F 1   p!7ߠu8Say|)FEE!::Z߻*B"i|T=eʶmw.RNǶMA8v+쀟w̨"\t(kWq A@A@A@Ibbb/p0dD넔hLPvGŧ2ڌ$Lݜp6*\0UAʶʣVnNF.ܘ(ӋA@A@A@3$pcrmewSǓhxե2*^ng#fIH 2+д9}kU   1z3<.=裏1b^pVg), [zڅ[ T.&"Vyٯ”[ٜ69$s`LsDV :6z~D49j[@j"^ko~xa8nn<0~>NΪOr MtDUֶtdٗd-}Jw A@A0r[,J+ne8=Zw${YQ~%R)[+Vy{aWT;L!41n28a*ruoO~ t](J)/q<&|*>QqnҐZ҂6^97-o|v# :Z. 6^z) .]t K. #29nen_;T2Rvpz<*ʶǗO ptY[Qbɼ"TYF)bHMPb|lGl+i=)b%l\6*r!/1 -uo}¹UDn|$ض_M[m0ĶhZNV0}x:'q  !|8"6MX+fJ#p'?bl/Q'Vs S+e02ye 13h$ondF-\48$%u X&a3o6f9L|}ߵpQy#Ĵ p=rE"66KNJT~q~IJB|]_juS1P//V9g>c=ݝ8i4_]7׺/ *%ݙ 1ws_Y7{$ z76V_kGDZDگ!=ԯHWL rI5u]O 5Rmk'ɴk[aisD.\?i*k?M[c7MK-}9Rx*OA@A@ [|w1o<̝;g϶mūCUPFyVa܎dQTeW鍶~u-G.=j_7w (لw,%uq1xb^h~> ſt e [FPۏo\f{+G+뤏۱glsWK> :Y[TGCxz!n>~<)K6ll%"ۿzN郮\mѿ2TsY+_ZL8ч*'9u>-Bfk}IwԯKޒ:'砂3:[_݊Nx9[m'k۸.#8;^4cƲyxCQ,FJJQ.{S9SȦx\dA@A HxIp /ԗ^|V2ڜNh q2iRƑ:lg6tͶ0nXje$१GN>(8F\@*<n{-Z|'23KɳN2Y\t#DψzޗO|\Z ( |O֗cQz*Vyٰ]ܦ.Fv)^ o؏Vk 27W~p:>:f\?;jyFyù"bLv*/:i?lvj'xykki܅'~8gɫ~* 9{ a=P6 vmK 6hp]M>퇻'ބ+zARxU$A@A" 21jUmY!ͨ*lNJm&! QFfĬӍE#W3@HvEyNIF8KN'kfN͐6c(?f^U\ǡSy?{vJkm&OenDex;=]9<^3ho#xyA3~ZyFŹpS,A 76( ~\'|)c pfRQ}{E ڵNJ5e~FSm.]?,D{Z, 7i!^A@A@sxݧ]LY*#RMmU޺5k*lc<nlsYi_M_fiqq+PW<$חƶ4<ϗO\ٽ9 Zd3sd鳃 >]Z헷3Tap"_V8~r 4n%ei??3{K}Uu.F T<f=6׉E^[WMUVҦ5q|\V}ɬgx(KA@A@VZjEEE}Bӵ[jvҴl@cyk$]̫_1bż0c˘1OcƼ8s1cǼOq@惊*ȼ~QN5]3 J 'Wn+ W (7۬+Rls8Dҏvڛ0cco rrE!12KNQ>{h@)Aq-W3ҝ2 ~=Ol V>ԥ4e<\2 )AeR bd<)jMIG ~Ѭ-QocD흹KPɮcm5>Q\"wj'LqLLg҆ڶxɯǮ&;S r>Pei)PA@A@o[0&nBtt4+<<(,,|ȍt>?^vƶŧ_r8f748D7 #MF~1AU6Pv.(9:{!\`NfwtVvw FvOK~]_5JA@AB`ƌ(++CƍA34g$ORD݊r^z1D"2d>dI_0g@55IkfYaFqkq=s$() S;Ut DD  HJJ hBB""")OUU*⼶cq-sϢLE=t$Ql) ZK]F'M?‡\n_#ܔGWA(O$&ƥ<9K觇'ӰtUY D>˩*ՑǕ+B~QXPs;<n`zQ(ִ- AHPmv$V) 1FY^^$3< Hh{XaQe+.lDI#|P>go+6QtԤ |r1< ^+ ǤE +oT tE9csvx卹ZD&y䕘*YQ9z9nY6)X6_\7b,}RŹiv./KNJO6ፋtm^׿8 Kv&pI|ZA%']X=^슌CRR \gꥏC³Ta~> Ǽ?0`z=7k]ztSO=mcb_ R]A@ANj mFPۏo!ߞ[˲QVOZLϷglsWK>6CF^9*)naq}~c۽.NI ,ߎ=vl,[ׯSSݟOu/Ovx֏Z7-O<_9H\ˬ>͎k`s!}5jZ[4@w NԬ&!vΥ;ʬzZA@L\?rJ}f+ڴiSwqhҤ M6aɒ%4I^oW]Y-ߌ@xJ-{m& HTQ^;kW8gJoX$F4Fr+&}hoXAG](kf)-A3}F܇v}OC|Zt۸ڳ&,X=S9x,JU#pnB$\҇Z$Y3wjCt$SQ.+C>vыn\~  3u(-^~ t/ёwS?slv׸ai~/e{Ϧ ˅;\ `cvADXd K*RlA@A@ ^˳L@鬙UrQJKKKorCHLG2Zk]I~~Mr,lu=N;ݰ,CUnìVXSB<b4KY@s\ӺARXL.,ߊ&=/Ь;IWB>V#RDu`j|bé~mҌ|gEy>@Ou0U)Ʃ:U{ě#cɽ碓ߟx7毙(/\G;ZSL\vZ摩* 4 (ϊ\>m\r$:29ͧT _է٪zgx"hv/ۜEkkhY˱{pwT}plbA_6k%]| uCXxG^DOn^#JQhJcOc>N.OM;"WF]ߑ>5x'g-&O\ xk|9|czPGG  uSno?7: q\owA:,Sm̿1Ȅ31m}~T IDAT<Evg meKA@:D*3C/ϷX~nZld$qq0?y :=4Әb It"&dJMij1i+hTSKLL4bP`q8܎oX@z9( (ZMq6KV G   PĒmE<Ռ1ٮk i3R( k[@j"^Lmvn8·;%y't|%Ohڦ#wڶ'˾T[u7V1߃kv[w lZtpm?s~Ė,w9`-_k ׆tu2˱{&~YUYUbi$"*H@UXO]\?Ť/i|)1\|O<]t%ՈtLՂt=]H;]zՇI7[SC-Wh&9 3pS/H6JD2me7uA Aoh^Vd ϐ9>QYmi>G鏩Lۿqv_Oqvȵ]GIдz Sqk˴nwch?i=}p3C<&OY~/uwceWۓV{d3BT?s ~~jۋL'0[+]ګA7>흳B{.ph։e6̇+s Ծ/1z@psHT>TL 0 sϜ%u 3_S/Gt,^Xߵ5kh[lvڥeddh999ZAAV\\itzFix˫_1bż0c<6o1c~<>2/퟊+*sIBՖor£2mNσ[6iёͨbFb>)ڔi_;YKP.Vrb_Ֆմ9 \4p@U&Zs \TJ;۶ O8jhQzߍöԴm8iݶ2[٭0ĶhZ]NdyHYf'hژo,.e*J98O 7{|J*r#^goZzȄrI1mA|z mz\\{|7mzǻhpШlex쮅Ar.^b{/PBT?% ÅXTRprϩ/pώ0=g^p~&,r%{a¸oA/IPc/SneLmI8]q7L}?6,4ALPc2.Uyԡԏg+^4N%l8AYL <p1VupϜj뼩[cUWA0#LjYu%p!pԒQ^yվ/O%"b_|]qHJ\m,[μ/&$3 }E56xҋHn)puXmZ*olnx?$%!uOd<)l筛zy9;4ecVŨI#ꊼu~QPiUיRR_T7͓0pLX}]>32ivj'P>"]=0%%w9w=e,-$ӮmBOW ;zpMa~Sn˿?E[99dr:4q©\ߘ.v ˳K`VhluCAq\ڪU-.tl|㯹-LCYd17t< JV5w|[wjgSg+ɻdL{ QvΆP~\9rӘ[Ћ|HtFw6e8ӐDjphUe+pǠEzǛzȱP^k7uCB-pR^4&4:p;۩P؍k]sV$=z꩸3KgXyvSXGktgZ{>[za+'ArOPtj?PY2~c6pIDiڔҧVVSjꕧ.U;"t+t{o'Oiߍ{6k9>]Yޔ;Oyi{5<]9 /=,EDKr[g%Y,A&绊dRtuaɍB+e@nmТϛF.ZمϭjG-Nq:v{=z?_ Z<+=2 ־[6%G-7, Զuص-{|,>iYIo}UVFQ_YP˻l9ԁ1; Ld 8TyVꟖcSqS2]~ jHKֲ*;)V.tޏoGkSU)>n7ߗn{׶Xj>]Tx%E#wj۵{ٿ+Gm>Ã%ϽteU@kfB[;UzjVm_.vuY[˲T*ߘ*aUga;r}zWvY^U?gV[Ob^iA j6[In9G+va .vͧu#YUB۝Yj.Bm*K0uT7e@tdt6q3 ^P791MM旵_Qs>Cfarq]$2ًopv;r {{:SfyUɰSɗ+o\?2m9kgdZ7ovkiݧy5~>T)<탲*/|ZffVk}o5>~ZOAh+\{:{y/P O]۪Yfy;ޗ1<+[սVuVaNuWi~Nm2AʳdrzdZJ1D?_\uǺ2$TVlh TK$Iqge^io Zi;^ҫPN“Ǭlz /ߋZƒaoӞz=M'|K/]cjgN D:9t P} eGOB8q&aEK =Zv{?Ekн X-%;^soɈ] RW^ :YՏ1+%ś9<g)cbNݴOvHMAhFcCѺ0nX(6g KX}4͸ t^+17zseq2}Y;ldNBkY?ɾJP8]X;P6 'U qC2Xdtqc'!# v(o?둶xߌb7nty}8m" O5t.~v/r =hܬYOGQ#L-rg1TᴮmSgdrdĩt~F1WYf~WMԞ؍. t¦_p$'V]n;$uz!)5k c1{mJJ|H'UQ F+eMICİթB-˷I 1@KűxiM~ޫMj:*8mZE/0aOn&J7|G$өG8Ty|VM'\T>+;Ծd%?̺xٍn ƂW)$}`<7kCxF>x<6> "+jb]f?]L˽ z^m]b}ehKn;~"YXm(+`ȣ¬"tOdS=MKp^t|ۊj"nӭg:ޱZӭ]Φ+9fI"Jڈw2[tv l ~u7:pƩn,Wk& Wt4aOlu BBSd)iWyo?v+w<%N$)*ɶSi_ fvHC-UaT]O|^n k/ :M_[f> %=$y ]{}_}9#62r 삗YwgFmB*=.o./R:ϽX]6 㓩˦ \'=ux~1y/Yi8kS/st}P^CJׁpRsV<^ܮK+)k>;ǺYMzmy>;p}3|{Oce@6q{y<wZV{`GHQPgWqXyɩ~NvuPH؍gogzVTu:g>?SpUe:tqj? WeYai3c7e.j+ }(Uhe+j-_F;l^\lsTI߻nkֿOŒQ])7ZD=3tsNQ>{h@)Aq-aU[ՓB<)H4PʖҨp4 I)Zq/|cSY[ƈ}oqy~c~ OF:t c\*S|Jt(]it>)#tJ7q {<]TOl /Dؚ51-jCz8VwmjP*G+i4){4W7}*2/VO#WQvȡ:ӒdaϻER'Af5ڎqx4"@E.(]Rvsdt1YܡB砜c9soK,ATTVEr0ͅ<\EW1))&*O]Wlsfb~/(\ɩ}6~XZUs/+B)ΗH vwkp@ԼB@:w_eNW/+SA# u{)[;ǙZdUaTס嗘rwSH*H2  z⎱zbH6Q).*")sT=佉64Nq `VWHz@Y&>' X<Bok+h/T"мY3|ih7z/+˞ju6vmڌMiG ܍-~W!TwIaE$VUiY>_i \M@^ܠHU\֬e-UM{톴-M.\|Ck{*W/֚͠)>SR|YN_ީoIpRے_%Ճ?!/HT]l QV}E1zb7wZr IDATKS 1kvBbJ3sB3h[]h:KStW7L䃲8ƨcu{qPX9aYƝ/McU\"1ŀv'cV~Ə_gׄJcːlc}Tb)v 3>#GqϪh^S06Lrs;w0}XzPcv"?j{)7ZɻB?!`)ACm^$,HuCڴwLP='mSau3pcQ^aׯ0͕'gLs ]Mg_>.4U Ig_GY cX^:)>+S e)mI19Ez(J>$P:-tcB*r2:\lׇކŸJF𕊓_rqcӯG0iek)sLgD8LYQW.ŧiFȡcu(1GxJr1Z5\F 8o\ot$V pȁay[4܊oicU5*ַ^F8y ੧ sBLu{^/:V}&0]kpMRɗ vVio><.w2 MF tr%`^`.haSY9c{Đ[z >) u]᛽tґ!}_*e9Ag(K?}w9rHn:9tBgg\oE)[}[Y ֑c{Qgl+\1g^H+[ E#Ӷ{$sm:VhWU#m;/Vn}.0O#Vp SyOu*z z܂s}h=\<8kW4y]g,UΩ}׉80(uHJ X}L"vϼ'nLxKm~ES7cIFVg}H:kO~0Tٮ<:П­m; !Ïm)V$@@4Fu. }>Nؓ53W=?i8([F- 3.P##\8Vwy:*/C:q?ڸxC5x߯-NaJ{ ƯGzg~#p$g66ڧİ7_®X ޿Jm{wj!Sli_&vK\Bd䋝 ,Ձ83gR.X'*zyVũ CjqR_-WI\^:pʦI,z:?vؖ1H(yR?uGĖ_ kz^2r^7'`?@]mLFċ95ҥ}MvD.[WežܫzժLw,S_fF0g.p.Yxv4Vu?Z{fϦv#*hxKQOI>R\){m\j%hk6nyrGVyp5Hշ 3=;jqF;uj v-iWs2Vaop$=p.kq)ɈR͟n"ۦ"NafsQ)RjD3>{Fr0_6(޶ϴ/oVCS-KGNTCd<ϴ^NSNV+NϑJ3<Kaһ .27? :l/,;m xԏU1 jP?\ofڎY _gAPs1ohS,4m.em^͙ cCab^~)@^%NϴClu0sTR6+ ~/ÿj?Po'1J~*Q*ebnnٕgWpP8(me m:L$pϞQ5?B q5&a?;ء=8ݺ޳~ڵ|؇'vN-Wxا> ܍t\GtQ5Pv퀣VZGSVD_ aΩ)<Q=}"/,ٜ;J!x3z%ARH տ!#'`mMts-EO,ng%NNAnt$w~ vx06(WiSdYd8 \Yp׹йMX)IlJN'yOtzyܽvK[='k=g 8OIjK c1oQTSBcbˏەR7mr ,i:ϸzwb,?F;aѹΩ/O=o.܉91{;Qɛ'99ӑzٍu s7jxi҆zvF{OIKp4I,Py93i{i%Ӯe%}'.yGV};ǗȪpइp bڵ3_NɩFAV̜œxdԏg H@bi*FV:C>P=]־$c?r?,Zi /{G0қpJVqw+<$]퐶^~fzˋvc?غlxݟFyK&ksdYێIUQȠ<%Q?hswF+~7%}efӷrM_YJDV5l&=gSh?6eӾ|T_je_SLQ_#lm^"q+X]tezmWJ~dQv5RS۵Ptfr깕2mxґj/kɿZwENYx.>ߣa.tgwIz >y{hũQnٶ<1ݐjq3MC/VϴSI]`pxs.1kr8 s|o g2̨9 ,Ny:dd^ X:F4Dgޱ\?͊-%\x4ҡ B}M I%-:V_٥ z(M<%upyAr:0rX|9bcch/g9/&&F&**ʿM~~>BJJJ(mR,(dqȽ/g7|hت{u_bˍƨUmG$PH@z1pũ  JY+pcծPq??lITJ1qUo.{BJY T`-F?H,FskiVVPp!f_gEHd[2}84nEYGHHʞѲgI " 7Y( TTQuluT ^$@$@$P 2_|:i]Ȕ^V,G?-pX{ʝޝdq UǓ^Y-9l!B"^$@$@$@@3O!߿bGQlVR-U]dKZFpZN6j-rS(上lhSYCT|eu(OwԷ؊EQݻhA>YcUC`[ &Y6ZvG99XWOkJ:vyUX_dj.c7^={"ZhϞW8md<.~6dq Úe hŐE޹Ip'_',6:5z s!$@$@$@@^v\pN;ƅ ?|ObԞ;p]8Nz<.U^)[oR2qԞ?1jOhzk}M7GO ⓒPu3B4֌ 3p^[4_:`Y=_݇s 15 ?lT1HNyF}=g:[_=n^vM>UAæWnծ;YsH'X]o{Y`A<\Ty6kJн]Ю æ a}N3!O?bʰ`O}#Yȳ][* Owb',lfZ;-/ci{F2]wMoj驛 ɲmwoz 4}_*nmD$@$@$P9 [cTl=>\ϨS#HsyX89n;6sҷqޡ~)/m"lj5^,)sJ+ڇ>]fZ91IsX߰ :Ǘ>Cdunhm=PƄTpʳ7`?} =0%p`BlX1M; 1¿R]+]$9 .xenk8$խON cY⌝g!%rSV=섺hղ+z]TWJ]3lAX4c#{*ê}i'ޖi]Զu8-GQ R쟅׌sl= ީ}m;O^VD#  ʱ1\ hUxq*3ЪZ⪷DA@؇'12eUEŕzsbfNyG nF,?eUB:?|+SFk ZǨ^ER<xQ?=+԰0Kq8|ڝyRw k._W̔jۮ+:86X}%obPDWm!S`WVr!L~v` ޑ Tt I eh.⮀1kGFX>zӥfHƥ$#J.Z-jc'zlZ5? ZϮ.U,4;m xTo fF-x;9eɋ|aeqx Ճe MD8Wx9 f JEF2 ?)< _Yh;f6~cЦ!a]UJ['NB(8vj瞷p8\}tPFu'ȢT:G&e1.,7|4ݨ5i|;|mɬ~ig4Q_|P\ybtfWb 15gTݛcK'+gJFX rWڄM޺zjvF>wvk+,w6,ԊKo։X|ӛ=]Afr\o{T 9^6ikɿu>#yvrmxsVSx^?zJSG]^0,Po!3CSe[@I h>}O{f&| m/Nn7Ay6(iխ`9t{)_|2Pt}'b}B\Y`9] b4D%TX(°@]{V1w#;6#;3Km'w z=EIY    ˖-~'mݺu֭[]ۿ;vL<~'NhYYYZFF߬)tJ+K/:N{M6Ď{N:{O>{PB [Ѱu[RUtISs%qVg=s_qm.ظ6E@7"ܛ0%[m|;:ުHcТnᢶH{;PNNGN웯mMzw Ee>yVXl\p,I)-hŒgeCZV1}PsWzn>7m"'7YUTJN$=խπKxc?l_\=^HԓJӶpyfen:Yr3#KKFJƽFX=Ɔ'$@$@$@&|r"..N}߫`,Ũ`ш) R}4HKJJ ڤYR|?{_o>˵ѸkCyrcʽ1jUrVtU_dj"s +apһSXXUD2D{hݯ{W, @E#PQ遤; Nع5 )4"ˈޝH.FwU( @!@c4JVlԼeNa Y!8)BT    td& )lqCas),Y$zCmyE"Cҗ1iG`JJ$@$@$pZT:cT:0mDCv\9{1yT:4'IYNB8]eF&`K'7SF$@$@$PT`c499:qpԺU:lԳD*!d[hjj{w  r3|ݦ99(jJ\euf/Wm^rpуtm^ffZ't>AΰkgA\\XC^ԢeH뒾{T='1wqVqPf$@$@$@H2q>;1jhzkҽ6N :0l=))OIMYdc\o^xp:11{7d1^@QTg?gl+:QuH󙪞:`JΧ*9a^Gi|EꞺr 4/RSUJd^tKaSJY%=Eҧ0_q'^ߏ1bؑU{﨣\ڄpӗ%k:4c#}=baQ_R :ǗVv} _ֲTxcaxdzGSoKiOm]3orKۜ3IHH9J. !j~l>ZɪQqv#;z %#ΨCSUxq*¦ahU-Jol*SGImh}={Tu5D씶x~U?{JQWU'݋O੹IbݱGPO lg-:>w4Z1]/Kyꅖ́}˔QR̿͂d~`@ x4\;ʍWE}qE:wO}obPD#C4::TNętE]ݗHHH*: gjiE`<<ߢ6~+n5SiMmcY9*:3͸ԬX帔dexuhg%Xz<5]Yh xwdD͌[#USUd~3v2lH=oǼM”PD`x\FXz@<9(a:ۅߴȉjh־,4ѓGGv\gd]3,Vuh|(xoM5<X~lP坬zU|>8[]JF|i3t$@$@$@aTggG\Z7DRX\dCH3F5w⃪_qTï}uIf'Jdbʌ z}ZΝnZipN:rROj'9 rCBax}R?6/z-T$Kef hn4{v4'WvNW d{sF^ @e'`_SFGs IELA.c@mvaNyJ>Z }:ǮlVb@Ws swv-7v|YbG8rsծP\72cOK#2=;1s׸[C)SYSroOEY\c5л7̚t҃n"՟GZErv>H^gTU@2{|I_8]#.,Fr6ڙ];iQ_:4C˺I$}+r+Yy"+ C^C(a{ԽV ye:,sbk   E% 9r0o\20Qr/g3%[d߷UA.Jl&VӡS4*O=<:RBHW"I4w9Ϊz\ >seH{aF)'#'! I6Ll428[VU/ٸX: R)""^U9RแQG\r=t*;:], &'70d$YԻ%  C`刍E\\ 8,Ũ$![~~>O9F(R!+@K17,\1{:F74=% >#ޕ 4-D4r֓B*RRL ۓd+[PBO[fj@͟sl"arag@4. 0cԔc)\6& 8C͉\ضh%uHYQNìC90ka{p9ϴ] TlE…Y[/]:a4$戚Er 3 b۶ `ڍy1xO$@$@$@$@@3F+^X P5ol)SeY3( #    JA2Bͬ$ D kvKOw9m܁/cmB}np F<Pu_T)OEEn( T$L~7rHvߏ)#R35[ܡ4eB/8< _q,8H$u[ ,W<   Ou!Ö۬"ƉCʿR#hxnGsQjUv7~5ktb0r33Q%1Bϟ^T{ ]8<>c!F>?Kֲ]Q[vϼ'n=@6#  hBkؑU8L`,f#Fh71c Ő) ++0ʖ@V|ad>w4_=^mj{oyu]Ф(_"\1^ޏ=;8]YYNϟ#NZnzU |bfX5JM.釱S!2nSFO|viP܇W}yX?.c¬{pd8;f\\_eJ=hy&  $P1:. Eq=:ªweپ>#v9tS#8錸<@kB\cjiE?/>;t Wbޝ6 }6ޛ f^zVpǑyA* 4a.KWJA:UcR %bh@:aCp5P=XV:OYsQ+څ*jaok鯧\zBTWC Ο/E ㆗uc~Lwd,4F%GSyx.^p;j)?}@4Z(ݺZqvizaגのu܉MM킨w;\0nyxxyhϟk-z'vCイ/_$@$@$@\SrVg >ĸCkYU9w/g3%[c{UGXJl&}pS<,-)IR]y#*'6 7};t$$<+pUx,w6.8lrN߱th$ƛ2/9j"[PX8r0HH ,_S:juCbT[tt4|Gh*MU?ejroYg6b8rmu(oV(i"۹6]񡅘'n KT zSG 7]9BSH I. 0ctO rNϟ ˩eRK20HH*'rg"BRʩ/֚" ޹5 ɡ(Dt=( 'PSτ% I@v6j8eW_- c # 9˷9sw"\zY} #ӋkblCasEmxn"^.sKDoN#xNZ:tluIg=&4miiV-<޲-6o'   85ʵ1ݳ1xچN1ruSf*FjGlƿhijRk35[ۆ#8m!Y늗Έ}M?MmnE[R.}EI{px|43,޲ s k,HHHH rmSw ۬<5Affx]yjj9jEwBmWZE9973s Wcgp) i#q[HV܎ r!ÖkѼGtA{h -VoUcTm{)n}8֭nהW~*Vj}^i"<jsOHHHH"@5FeuD"{M$GG;.x[K_+&Aׅǣ]{~}s3 ÃRՉٛOe:=bԞ@p]<֤*olrK'%6|׷SX֔asy~법<]s1˹p"c!F*3qܠל )15|_w0- ^t&]Ī IMVŸ:`Z1TK}΋UFTWxA$@$@$@$ʭ1*{UgԩD7 G6mGosCAէxjhy04lރ8RWnaeA}WC!̼cYʷ?#Ztэ>)/m"lj5^,)sJ+ڇ>]fZ91IsX߰ :Ǘ>Cdunhmޞ=PƄfy&49?Y%ry`BlX1M; 1¿R]+]$9 .xenk8$խON cY⌝g!%rSV=섺hղ+z]TWё D8rlf!Y/<3 !zK dX}x/SVUT]\yϋ7w)vZ;1^J蔧p]6a4"^8&DS7e=c@nT{*]g ۅmy**rLR]R5JM.釱S!֙Szi>֠%}'.SscFOG;zvFEAj3pVޯYe} U)O$Jhr2btfWb 15gTݛcK'UgJFX rWڄM޺zjvF>wvk+,w6,ԊKo։X|VF?ڂ%=;_= @9q89 jo?6{=;<*E<;KGAR 9ycHW7IGEbw4$\ dgUQZC.)z{@pu3 O-ۄWO端j c 5˗#66qqq^Sg9/FuDGG#**ʿL~~>MF%%%PnRn)dqȽ/g7|h˵աuYsӵ؈.CU-]|RL9Nl^5s\to`*eB*eS9UJIޓ\1MBNry>JzMǩ~w).1[.ցgqeg+=]j10:    Bt2t4$[4:CINa@59]u?$   (s4F@רycҝ,гBpһSX|WCq8#    2_HHHHHHʔ2HHHHHHQ      2'@c̑@      l$@$@$@$@$@$@eNh#g$@$@$@$@$@$@4FHHHHHHʜ2GIHHHHHh 9e e      (s4F9 $     16@$@$@$@$@$@$Ph9rH$@$@$@$@$@$@cmHHHHHH -s,HHHHHH( @1ZY O@4QPP}9ѧXJ$@$@$@$@$@$Pb|z<=qGll~]Nw\[,HHHHHHg~~>rssq1QQQZ*OD1|$@$@$@$@$@\| mI@.!- j@ǖuШQ#(Hw4F#]CHHHHH013v&k39箉qM߰Ng5kF<3" H$@$@$@$@$PY 1'0}Q>vUhe)CqԪTs!9c?T#...q5݈W$    d7k p0- DԌVh '!ժ%"%9 55oȑ#=" H$@$@$@$@$PY a/OƥSPz4pDe XTuGj̯{6=" H$@$@$@$@$PY ȪG~EjWj(]TELt,bAwh$fzhvvv#1*$@$@$@$@$@QV4G|lD+ NMU{Q&*6JU~jhR!*۾D1|$@$@$@$@$@B;JRF:Ԙ(u-Ʃ ϭvrQvQE2WHw4F#]CHHHHHRpk\BC SMٜbzKU8hk CKR ',SS:C`r5]8:     TIj\5 7Z㪂X ʚrEuv8Z 3j^$@$@$@$@$@$)( y28WC}mb|ĩh\1(]5a;DsF @rEP\|ZFPs̙3VRr0*/3Z^4E9IHHHH*%1< ׫W/}u]^βUW]U QƨP#     $`lbr/V#~$iFv( @&PPP9̮K.hժuYgSN~<4FZ5 D v ҽ(1j78mMP     (&ƍcĈhҤnJG19hV,HHHHHJN@6mƧʋAJczg      (Qh^^^CTP##qH#    4;3++9dQqb5S5 @ir!/1j};F( @% FΝ;qQb!&&ٸu%Mnnn31*$@$@$@$@$@gY=@Hw4F#]CHHHHHRhٲe?WӭjeHHHHHH lP:     (۷(ƨ]~rIHHHHHCO> 8"m=gʈ 7@YkHHHHHH@ضm>S >lOY\Nq0ֹ{F sX`"     ̆h͚5C0l0lė2s5*c< C49qoK M c8WoN|_|HHHHH*͛7c޼y\5 Ȱьsq+ɞ3ZBM g k):/IHHHHH}nV!uVaD(2-ln3z41N8ʼnHHHHHH2DŋLsFkSvq.-bS\cJskvqʇ~$@$@$@$@$@$P /EzD5BgAYܚӚ")1ꔷY 9[ٜ$@$@$@$@$@$Pi P?< 6D 8_%pkï%"8q/gTgܛrMG$@$@$@$@$@VGA l{fƆ$q4.Lr6Ř5,G 9~xo\n4\/kKeMx+~ TCGU|DDG~!Vv|D @"Dm?z8?wxuxyx=<9gM38t=t0ڋ_cPg0u`uxjg@"D @M`@6sK4s);}=1r!suf6u;9wR#!}3{߄D @"D O$0qCs0uģMЧ̷zVn9?b]R뀇3רmy 5lP6]·8@"D @"UÙ4<ͬk͵fmVSa,j\]ɽ5kzt-OĞO?9f=A30x@"D @"\" 3$=hSW~ѧ|ՙ' Krv]zM}9<[bMcFm>fZD @"D#p9gz4iajjx,@"D +0`B3w`nxFM4k[0|?u/KHwᔚkFFOY@"D @K`B\t;X%^XyOs ԭ1n|~S'k9l'}ós|9g@"D @3 <4c!k3'g~OG \X}yMH9fZdLjԲD @"D  <Ⱥ=3'nw-XX(zz>b07Sc-6Kcgx78@"D @"l4z#67OMQcP=WoF9Oʭ{Q< *kYGf);}zs<Yzkg@"D @MFyxf<=}ܵ{]C!8`lY!S[s۽fh:܏:~DFSG @"D xg&qRs O=f_#mѽ: u^fXxf>Br UǣyXy}Y"D @"_^c2Ǜީ:u>5zzb >_☃Cԩ{H23g=вD @"D &]bwfN6uUwݡm%ƬOO]y0nˡ wps|ưpJ~sUS?u<ߊ֧VD @"DYhkLå=U'W{Lo~,aԋ́O?4eIDAT}Yg{C)ug#fD @"D "(%Ʀn~VRwѝ?bu wƋ;LG9Gg7`k1OG"D @"/$̤hcf%l+G;Asϻ=k﬽"Fvp!pjxu{яT g=wi?cMD @"DU_5ZsfŮ=쩡cӎ|;K27T ff9bsj_D @"D  7Mx܋ܡs+5Ww_P9atWuCK͘\CEjC)YwӇvϟ~#D @"aXQ3G @"D jM3?[=~K:kk9mnVMnF7Nb {V鱮v\4>oC)ϡ\d5n;e#[Q67֡FucѣfjǵQ7}g<{#D @"wԌƳOmz{Wux)yqņatݖ9]} ˽]>OmƳ8@"D +8pg?>>k9s5S_Wubk<=qڞ_bkj@"D @^Epf~O-~1_]=H[54K~,QD @"D pnOF9@zl"fˑ@"D @~)rkzDèJw_;x~#D @">vCwaTj|_wMυc*G @"D  \.oxMۖk~0ꅮvgϬw(@"D @" w.tگ&tKy;ӹas%RD @"|7 +W /Fe0[G @"D ^=|?_n]/HހE%-@"D 6_mܺo1n]|KkhݢD @"kYW ׾t}@"D @"Z}D @"D ߑ@w@"D @^La?@"D @" 4~ǟzD @"DF_D @"D ߑ@w@"D @^Lrà(IENDB`doc/imgs/gettingstarted02.png0000644000372000037200000012470612276056621014555 0ustar gfagfaPNG  IHDRk>m:iCCPICC ProfilexXy8U_^8p12ye" $Gʔ!2&!R($R$E!V??ߺֽz:{hhpK=M#   =#B5͍a_+ _[7w'9l 0 AhLdހ1]8@#3aX􏍵l Iў<1/hX .F48^_s /kGSp["|Z-|zyk-|h  w['yu8pGz ~$ EI!$) I7ȷ.vB #S@y,}sT}0 P5w#wht~wx R@(u q? r I d(65 A0 & K`lmA"@3 A"B:1d 9@n/EA9(ʁ Ash OwhDtv?B0BX#N |a8D :qьF "&U$@R \H1R itD ÑgȨȔȼbɲFȖiUȭȓȯגϒoRPPpS(RXPSNQObkQKr|~I  GB$CIK)Ni@EHYHLrJʅ**jjZڝ u!uZI3`,*4+D,C"oiqrrsr>L'iHI .6.}(a=nnd:<< <><<=<&ռo~ 0  T K'0B BBEBaYa?BHȘ(ZTQ4DTtJ /!-V- n,,"~+($JJ&KK~*&HJ'JJ%ZVD6MG@N^.\V^v|Q[]De^Z{uu/544ikJhk6iRJFjighult trVnG_џ2`74407L05Y767n7A\553 1m1ffWޚ ?X[Z,[JZ[[ZZUYm[kZg[DR:VӶ˱?f`?uuIӒs'Nxȕݵ fVn^džg窗W'o>*>9>+*W}?k (hXxdT v n !8yXHhj|RصpPĉH:D E FZV.މi/~rs]pWCK¿~d,ߒavό杊_ wv?c6={|x 2a(] „c$>'@eD%coQh `Lv(^ /&W8Z1:${oXLY3ن9h8MHY\<\|UtuIHIܓ"ϧ`TB*fQ١5KǨe f(oela`kl~âްv B NaΉ'.Tv{|By}x}eM݂cBOfgG\jӉQ g'%I;ϑJzya-}6c0-bˌWrb];̗r#}t?Y%Me;wMʣ*.VU W3ېXiywU;CG&Nv9$f_ӖgL.}x1>|g$jTg86n$qrU׆޴ΤZ}|؂{_{?/-|\Ysj/I_7܌Qf~o#?8/`fvfh!E#?xt% KR 8)ةEiVatY I,l9oId\N0EQ3YpXrY Z(٩:9:Y;۝0wttvw615W P a;J F "#֣?̝}7pi|OBۙƳI%I8ߗv.靕yH+9Wms#u/@ތ**_^BWxۥ,N Dx[uֽ:z 6kmk}u?,~<ڍxշLA!rr#rc/E&8&Y^M_MCo|ӻo?,.}Yx eU|Mz׌o߿mq0ގY˳wb` h`M&L` uryBq1D"Ph &s 3PDEZH*eGӔh!>r4kV-6~v,6;'ׂO[-#8),\$rVULW$D9)Gia̔]S&  JqMUDTQ3j45h6t9+|5|fTbkbe*i5{oagie`g}hڶ.AΑq٩9+[{IO/6Mg>A~,p  ڰj\Q j>'TRWixwgzkL:~%jXttnŬ`y)jCnݵ{yewsfi?ʋKP2.鼻ZASXY}^{JSh"4kֶ}}(qĮO8zξ8=1eԄ+״of%ί/^ZȎA]p`By8cNZ @@ ră"X/.B(HցP.8EB - *4 R@JlD+ѷ3z9&ӏa8W[$#$%'"oRR'G$lRQjO}z& 1AI=2#C8c;s4 9K4[(;-{>gɌ7?u(DbE s$*$3beeut LݎĨ&ijkuu}r ^6J23 56 3 :ihe{.߾֡qiЅU=٣7G˯<i_ e o|7/p;왤sA)_R2f^N"Ӑktm2 B뢠%-G*ݩ]tcΑngЀPx7Iz/2.zKרo[7vD[>! |@ hAB*1D"dPn< 4N@ww1X#,jcӱ/ql8?\+#Y5or; FvJe6?[j >%GDYb2m7>2C#+P *V-vUU[F;znN^>5zfBB?E^;&!"ܒZ~)DK%35V6W8=I\\kOyxN;  pq ԗr>)-[.7"SSIO9xʲF朶N>A1%5sK{V 6+vmN9'p.q>oG|Mhv4uq?4f<5s|lNbZkrC4 2nfI]iϵv pV?r%9JW|MGz}_ABSxvk\ë:+={*9@ŋQqʉS|Na{<OfW?|^Z¾μ~F緢~[o'Fw~zk;Wϸ`˿y?GZ(z n܆E,]AG8'|3=?,!ߜ pHYs   IDATx}|Z ElTT)ԟy>SXA, J^#BsSnޛݽ7Ig3̜9ٽݙqhq    eX#   Y~   @D@Z<)b   B֤   O$  5   4@Mf\)E   {9[ۄ5C A@A@5X&Qw1RA@A@h4TנZ Lyl4@A@A@j=5!NjHBR^z(A@A@zAdj&nDžՀCS/=B  @B bL I7!i   pl̷o'UO!kA4+Ren3ꖰ   4<*c3H1'k2KSݣ&2J   'Vqc k* 1#kDҬ16c8<A@A@%2mc .8b,MsH1!k5$jfD?MŕoWd'\_A@A@6c\ɔZt4*{L wfCɒjLSa{˧*]_A@A@'V,.(3*iq=_}+YQ3*V./*(]A@A@)A"Ic7ÌDV}z#kA5ReǍ:XfjNA@A@Av";-Ƽ0efa4eLS_^Z=5#Q2 1*] D,PQ.aA@A@A@h.(3 ^N)9)HedD dU/W+JtA@A@Aa"O~lJSj:TؿNO{:5 Oqi5MbReF}Ƽ*H/  $Y/Y74L)gԧՅ)7 , #8O*{&oi3 43ߘx_Ocaǩh1112N7 r^6߮Պyk (URSU^<x>A@A@A>`ҖtN'J5 )28y8M9WkY36,i*tqz1ЎtpccG8m;ʋKQQh~R/0l5Cdd$@T'ufe`bl!] CT0D6oQcG 2b}d++kC'nY|p:A@A@A@)ߑ0PD_"jg ώIYX_Y3id=*la'Yf]xԍH^m}X4km2B^!dA'^w-ڟv*Vz_Ww4#]G:E7uݿ 7DLb,ZnMZBsHN>gaw#v ~IAOG"7.}!!T_ݛRۣj=9]>Dv5  9A`6H2U?T>wnJ#kV6ưik4G6:n6W/xƨ@r3F͔JnA@;M~7[sN6qƮF&j`~8O0"j7 Y+~ZD$D (Dmڣ;ڵSW:Zw-B^EJnCLB-E+MZ!9(Guv?FG30o{k'D TcM()/F~H&4a?%쌲i/aA@h<hZ4Z"G4dٿqw ccmE[T<6٭F눮:ހhl7ߧgpD8*Ț#4Ȩѭ,hE*?Qޔ;R ZN?mw׷́|N9%]ֽղ’Ш T;H*Db2qz9J_(ٴ Z"9  EX ΐD#+I#ma Rb+P;lB h(FNfv$9,C1[jqv^}?6č'ǽB̍o᛫+֘7w?Q#xo)x7f=s:~ퟘB?k"iC%YݔqQ"u5d$^8ӟ>MpL^^mc1ӌdD@mCkd T"Ř$"}o)LÄ07x?FHG6|pۋ.ͬ׹%0^w d2Qd7ZAd;3Qj'v,]^Iv q DL9t/n:?B,| LVt9wŷwyD&YpDdQaOھL'PE(qG9 sEa2F#U><|Ja>!lqyGвvfMօx~*BQgi>4Nfw\x凫^vwЦ+_åhZ.~=rv49\o;ӯJk[jcg'VB+S 3FKD-|5Q{]=- !5232/KA vL:hݓ*i5[Nq8L4G)ϢNQp5BѡiT9>2rȦRh4msl›2ΝT`GȚ(ѪVhF9 t>O$Tbsn#%%߉A-*⽗poǦ N"ÐpdSu^GG$>viw kq+J]NERSkfgmѭE3Tͯ;sⓉHۺg,d:4ZVJ'3 U ==vs<2Ft<7*]s*tx(<#?a2tO& <( A@ 0Ӕĺro$5"kALT=1N5 +>q0" hJ$f&B#imM, YG~҉C6̕>~yxޮjpJѷfLFr 5pus҉؜µpĄDI@\h  $zYgy(V2KmS_7<_ɚi0uD}i*w>BQF E44-l'{9ʱV<wD ut}yxÇ05tJ*BQk%r^q_I6y'@G@n N9nF\QPw2*d`BE֪iMP\,430 NrZod7fH"FSilMѡ!0= PyBGi#߅t;ś#-݅vI+GpXʬTj,v8u*xuI?~Eh#Oқm"mᴪ;~H5Ր?1d;ϭbIF@5n"' x o__Z"H*K"M3)S1a]wZx&S)%01{`0YT塦ߑUr{:wmw!́Hbt*WdW%*퉧}5:]u.8bK ףDA{a|4s,?yDVcSbZt<7&&?YD2I`]h2Hr Gr-ׄ+@Qe1hsg8Y43 =F=YpY-sN^։3ٯob(;k5^< wNo(s?RьVxhr{@ҩ'_A$Lܘc_?r;|yTޠڐ5VpqcZ!=d$;[hkG߮j<#k}VtZU+Կ!H(XW!2B $wD8uDZKp%(Ss"$/dž9Ȏu"USEUJH<;JiVyM* MkŽo04HeύaƃU\+veΝ -DA;Ѣ f01Lͳ^˧ѪpuVϧ6)=GSZr?:9P\BBd+@Iah9hyo4M#:VBZv_hu[/ i-J ҵxnՌ`hM#BWV V E+H@My@f!Hj|UHށߧ<=7qY8plI]}#b6Uwhہ_]$&eKګ~1b:pW . IDATB>^G&PKy!z2Kرxh @s̡ifnAX4Ey)[N!َX (>ÞA Z`HnbD3V i [V?ùQ?hqe4M+0Wښ&F*Goq  B`ȑSsvo ,Sy4z4v> LjK֘`)aup"[ưJ3Vqd̙SJ59F&lI11p߄b'Xjзe ct~ȡi&j숀i茍DeCCCEhx*w%iTAR4aNc|%4$,gedM3w9r*ʚŕ0a'  )Q2o%$QPP6&j|Lx= Vo(b>cI29xa McщidˀXSDMϻM; Z6׿>{+Mňlyf:݆!@=8|:v}@ٵ`IA@8!jfVq?{J%ovqNXkKԛ5.M|֭|䷣7q_O5¢;8cǩSNwmo]^c{Ly5pP}-ï};sVצmI;o`grj,]J'lC=wJUO.ǫvomRZd>COKltڜ=c>~Q=[}KVzyt{8%r_ZAk[ts^,A@Oy6l?_ y8|*F8 $AznlɚM93AJ_+00s9Qjř OE8SIw tTx#^D+qGyN'~xA3>~t|'% ݌'ιSEoSΠr|$nh%|R/!o=˷c#1MD>fN3;뫁HD:>+څ:f>+WQ&d#,MHʙ)tc6AZ2f$j߬>+u?KI'] 2zZ??r%! j;ogitƛ= a'mTL%?%R 찶;GƹCX}SBuMr/bg7 Erv:mu RR{o.m7#yjs3>ņd=ݐY >qnmlZ|l @cE >bclJWiF>j[RkUCUؿQt Hh)iyU59LFTT=B%xaaƾ pF 3頯{"AH&npF2}vu䁹8raP (j;`0Ȫ>b%3gϿ_?蔔w>ZzpGkRt]Y9}U̷,o4Cկ9o{\ykM%Ϣ 6ǧH\8-0&6t4<̀5{JfLG;qUغi7~tUZ^BAU괱ECr"A@8>4H,2Fn\L離9ojrNS1*]r=Σ(>Ͽ>Qeb6HaWzkoӧFw4<׸>;-7: gK?=܏,v:Uf~Z8w{$a:9"6aƬzѭ?C+pgDJ-Z9m"I5qII^&v܍{߆.O߅~=c"čTʺt:h$|%ùY[X&mXM ``UUYLsp'#xDZэٕ +["[> ;JkGA}]?~Q3 >֧_.؄<'^e;kP^p~btڵCl=#lwۃf[‚Uڭ3?v}\gkzsE @#3f`ڴix[o|_ʷ8g\?L5*w{#/M/]t#,I-Ex8t7}8t cדM.osӦIZ'yӓHJĥmw욱gh!C?,tVڏn5E۞]\emUv\4q˨ܕ>H{\i%k饬 zNluOku{a%YEUs8uSnr,䲟u|6Vm`;dVLԭ4ۗd?rz]v-H+;77ڧOc1p:FCLO.`ڧpQ[ MY]Yw =w֥{,tr6hZ㿇{6L8P=$:}bEeZ p=(  hM۸qkJk{¼ +[0a>ü ;{0b>ļHq$K;).ż~j7foJ!gWa3+W0l2qZO@vۦGbgbލ+Ɂxhi }%{YvJbhC)=h?!NB|lMn .x sP(n59;;G4z-(v#f{nx ȟOzG9mgͪm9pjHJ˪foe|>zS]V&-w|K?]!@J'oWsﱻGBָjvb>X[[]+CZ A@m۶ 'N@dd$#44(**r7 ;xf9ΤJɼ/=8%y&8D͒o I|pG Fys#,,,0ܟgkjA!c݂#_{7CbΎږ)2A@A#0oa'átLA@8$ОL/,dGӘq*!]8IMeWZ5&Xcuk/W,еi[d$QC>a'tLA@80jӦ>A2q)> R5sJ}cW f=d+16['2;~ uDOjM QqWcGB-<-[4sYA@ƃ5&`|(BnGX߬q88c,Y{?ә3/{m-Jۃ~_-\ݙ{Ҕ1Eؘj\S߹y>.@9H aú癯A:-@E9gۊo1y{v%&bU:Mσ^vزe~l]yۻ ~VMf*3OO>fw]:"⊑}ϫ*_p;$ r[7#N_>܃PX0Ikѽtbux d8E@uj~%*c鈧]ќt-DGg:ѓG)t^>vUMd[5LC˵LV{}s'K[3L]~?s[K޽T?weU3'!p9!0%-QQ-bӾ yUj>{/޲few˥3@4hwyvy2.~`nQgګWK,5%"=.=v*s#z>[}˩-x h>7v^ٟ}pmo` B=['D;[s/NIEzبU1[_'fnZ7icBj\3\rg6 lN)  4P~gmK2)))ÇGz}]5TI-??ny!G0?a|yK':0a^y=1b^gļg42Cq+Y)'3MTP U\q#kej_>w~}<{=\B~y.^WElt`e,]p`ȚN6ؘ}7kVTo:͠jWmf2~H*aa,Sɘcbe'mv\۬7zHܥm`qc1:N$ˀgF]4a w_ZmW0<.S<ܜI'Xts,->i/iF%ī|Χ]ްR[ʋi/CS>G~2C>5{rѰj] [yOTD91A@1Wvڥ5&luhF; !LfM&?ڣt^}zZ=X1D~SUOD<w硕{.ٖXεwX[]IN6J-碝NF2 Uy)CZ)^6ku+{rϿg6L֩!yȁ]\_{)>} _f2zq2~/ (:Kؗ;#K({n`?vARYOaxv _G7O箙<:sf܇VNG8G+!9ýUsq9I3};0vNOm^Əp"Keq=Q/lqзz Qp: =][ۼq׮q)٤/Óo#:zUPJK[,J3 IDATkׯ7\[[)FXt"?xLy5dc"Z_۫E>ϫ p@Kmt9vܙլ7`/Ty Ζ0\z} 4',ldA@A>]}9\s7wLk0緷1tt:gލ~ߦGSs*c^>@A#b |ۆ ǧQ6B\Hiq=>]{9U<=۾OY*ķW`E}NMm: 'V)-'EX(ͮi;D& # ? ּysk͚57޹s'h >j?:5Y7/be=HE#S?Kފ)מlBj n D|b3c7/xtwvax`ĠN?:>?5`$K?垚p|$nj'*0 Xm_Oyv:{$&}'ج2ɸ?ցY]&I  Py^^h#xȖRE:IKNN"swoՆcXr 2Qڽ+׆{6Kgߜډ q N4 6'UٔKCd\D A%ɤtu}N#A]oU4z`6RW( +3LL܈]Gl0=#,: .18 {\ _ ]sX'="- f:$%y*gEYKOrϥ c-h^ō 4B.=!܂4#nǣqۢEs7Sq--]H!dw}]pĝtNfu0#?v⤉u"o4<,{4nc8rvo2]1v,?ONun^fd@SqsИyM[`US[~#JdR"}AN33_)D֍=ֹ]yW,mq8)xÉhrKp~Ľ ʙ]^A@< GˢSXDEE@j~} =xd=#RˏwLj2vL¬Td_ G>C?:>ofZJ{\e=tz<]:;gm#+Og4Rsp!zpN6Y=, !FJi*h{M^X[[~B"mv67kUX.ZED LwD;cV=yhu̷biooC_kTA@;<G"""C-D$<-[APbf>C)|( i~w<,s${׻_/{eIi m^F^7.nWtSTN6XIrZSC~:U+~2jV|6z<%tC?G#^ҶΟ^=E^;F"U8J,JKsdϊ}Q^WdyD㊢-{^lܿA}eI) 9wθD_e*+>|^_^o_f,g2ûB[Ur@?go2|NeIj'K0^5cW4ɫZ?Z uڔ3 \>rڢYT׀:H[hyp;S]m_ǽ̤̫$`ej{ʊz/fXۖ# &J  Pg/_b"/[No5>M۷ov-55U;r䈖Z$MW w~}]2ќN5WD+Ri*l)>>+"ȾJEg-ܕ qlٵ鴑U 6j"<'qۑzc^΃Kh3Sv/-= C]5iTy+|>~wijk4j%BۦwA߯AM.AILBpo Cc-,Xꬁ]Us/[a*w2w://F-@om K6A@AYӧ;otͣiS!tHEmxA NM'up\Fƃ_ѪʢHV9bdͬ&"#`::-s[ݹس@pvjT. 4x~EG55EE# y6A@hh]{6uW@={?iչbQ  E`"ƣfLԡQj쫑l!kl@GoХjnn0Wе-v2( 4v)_6Ta ߶[]vi v"jrsde1NX9 /=עi;l67Wjςо ؾ܃_^,H -[Ml=̔qu=GJUY_mه| |0~E/нf»pY}z{Xˬb}|1nȺ tn6Oc9  4|əASFJk-kdM]Z61/-]Rm7c>ɳh GwϟMd8gKЅug/Wmfo `L{9<ϖ7=|6Hihxu.}97Wlgmٵ`V {6~@_'cNl^,6ƘFoùW.ƴb%2ԐnΑ;KR YfFV>!OEHZc9jYLnk>&<ZCΫ;kx˃״'_ Yо\0|v2OvjCAA-)s~~۞#dyl3ZX3?𹵿J=秦xL- S%YcDo щh~fow8I3};0v&mENL:\;LDɎ4j~;a?qxF5bUc6j[:Z_5Y)ؾ+/>ܦ{3wCZq~(]g.nK9.h-k(?ˉ89_1q#r-vxjDAzr3rQbPw+=:Mg Gq2eZ%QZ՞b#V*3?Gopڧ*Z/ W )t-I {<z35h/$<:3Oǿ *ʷ6zfbX0qP# #+<  pS^3wN僇?9oKG< t#uS:G+:̢NGO:N7:q:7m\eڕ?5Z-rjWI>iIV)' N]~jiK{= iE52^WuL}xRRUN47_vUTO(SM|k=T/Nm9Ϫ}*^:k3*uu 5( #x%Qڳ/ܡڪ|jVF~CzϹRǗm&,oFmkFv6[snў&n5ZE2Z`j>ژW}󲮪}k56iŴM*7׹Jg~5Gtoӵwk0O ~C9ͽOWXsרځ_h)Z/UW-ϭdqfxӻh-m>0O*=-Ǭ[u_ÿ[k>v^gW(A@FU~Mۼy{nڑ#GPYPhlX4өO`?0a^y -`|y 9w0a|y#I̗71R\Jq+k|36JG>x`ĠN?:>{[X4"" !Ҥ)"Ÿu>5 NBYPטMG 쇴kqǸKv2.`愅~Yk\&c8j7_,XVLdcRF Qk2XJ}]xekv՗bT߫^CnGFpsk}NW՝A_/ߦ̹vK ݍG18sd_ToǨGbGp`35d40܌O('Th4} I= hg<0-9h-mz}l6VG&DVz&羚W*1A@Aa PY4 Z/x"`-?s[D<0f kٳ1//bo~Ek5#<=_m>ti'>=JGuឨl{sx醓-bgz>tgɺfU K ѳaN7;?[lgxϓqѼy IDATG\;<]/пrbuGG4W1Ik4c}>YCn?sCY_v{--8߿ÄIkq?^(&ϣ&5޽"L%׎5d_9ݫ9U6^NX_* ֤f N5sqs\noOX0gKf{K yOg/~Ϭ%Ę*" " GZƝL|ɤ:}dI$>J&ܵGd;jg_޹zZQ(uivw{ۦu-nP'b D1~5[6]kixc*vܳq nja+va#*֍MVg#{h`[*nVl4Wa`v`{[&/|jٌɹ{`SaG*w3ޭ퍯;p&i}Kz5e SC}jP߰ rhXpXmmmnHUgmHoKF܍?]tw7ICK@ljTKh%kvefavT!)EAȸ޻2{ IҀNT y*}bSo>eo혘T." " "x3'kEmgo!\2kV}a ۍ57ZЏmXeVmۥgS>Y"* {Jz7mcF T6hoy;Jx_|qbtguVMix^K{ڎ^CGY6Wቀ@Y'kH"n;lͿ{ b/Ȗ) /W.۵om^^L#'8xK0IJE|`81 C4O{*)N՛?h`1i. [՗SY,5q&3۾|nvΗqFy{̀Ws1i6(1_UŎa`omGٜ;_wNGWQ0|i~qBfizuVLv̟"bB]7l ٚ~s{ s?q/s_7z0!spJŏsoϲߔ_mNɜiK6nهf~ e>ɯ&'۞Žv?fW,xdR?\ ;u{$-5a54IQ;{+^#vOE~twqݵVo5{BxԤX_koPDh.\wt}\g2m}I؜LٷO=fӮX_Af6]!.{流:Mu݇w0?}|eK?j璐c}}Zi ~v{tk_6ۢ7']mlѵ+%eٗzmήf}$w;J[zmv]'%Eo3?N<.-_ɶ[{fq陼͛퓻_d o'w.s%?a_~~!5i6ogξIPOS??; ًmmmuwRcQ"d-:3@e\sNuW2wڍ69}kϪm~Z{s_=REnuVh}Ia|Gns/o{ƞߊak)$D@D@D@D qfID;} i6k#?rlˋJ:*џlyL>P]ĤmYMCɦǽчZ>< [~=宥BX%wv`ᦺ(MfmuGFN̸{jzIֶ4[K$o6ݗ O,̜lz=xzi x/,y5efkj6.9vls;Vy}'c/|6yo}N? lW2C}!w@2=ԯ-?&4\lqiG®\;%]+oGyrYr8Ko!]%ەWsA G/-v nN9pOx;3u΁], Y._fmėm/]K{y\*{wOEO^nC/Z{Bzѹףݥw+ʥKicr8mhﳛ5Uf]wg~L7"Qj;ԝd%*'ɓvdM.Y2^sm6=-}!p?;m^r8!4\VБ?]]]v睛3kIuG4.M'B+/f I%!:k9>㋎ͷu[m,_u$Rl{>e(l׮(5\WFT,Y\֗~kl!fֻܾkc&PR7.k.~ۅws_*k2N ( GAWXmqVo.`чlZMn'{KrN=<;gnUdvt'nGlSCm\:&'dG>=|Yn[ΜxOJ4[Wm;%-vo<*D#e!K=t kߑ| 0.M.JpjTdgng=|GBjc53\ 2"֐6_-:VaGkx!6ca3d=z\\)Jb{}rO;נ?6n]_w}@Yy>M-rsυnfًm?9]ۅ?D." " " %D Q$<_' $8gݗn$SA!A.˝>dߺ-Ą&:zO}5ݭ鞃{Ў}&{Vij@6^^R_ᄊ aɝZϧk.`70m5\g+jA;6l4WlpoOcK{ۯPWfްO{55mZ\|ʼn.guVM!pBZM$2nwVWt455~=;xވCBKۨN; .~5a0j'ߧfBK q}2v{64Xw;--/>.<ϗ<(ZFc )YJ/eZwln8)wFCe6 Ca"(/ehqYsNUoD@![h1"dmNl1qPj ab*" " " " " " %F@Z#" " " " "  dMׁ %k%xR m8zzz86=YE@D@D@D@D@FNYHζn}g w JyE[Nd6! _va6m@/7n577"nH>^R|" " " " " m+v6q6ۚW`ΚbӦM6촕zQVgH$si?nmZbwu;ZOKU4L*lÊu/{7n6q1KŠdT΄4$k/u흶f2NljsPm.YV{qYSjkk=׶dm[|" " " " " #FϪX~:ٚRkS L~kK/ٔ)SF,Hhո" " " " " N.c;AL3cjΚǹݵKL^{6۰aè6 kGNy7YZb5%#͕6~Yuj[lXZ'" " " " "Prtؾ{۸J1k7W*tܝ*'k4J^R|" " " " " 2XmuUe,[qHj*%iujJjuI^%k~@*Jg-qɘ{VeXq:LWnrRl14̺k0' F?J9$kY˝+(S5{֪;mkWuWwtUغrߩVc.9sG{f3cY+(E@D@D@D@D@TlpϠmųh=Yֹz2ǭ.I;E:]jzf-" " " " " "0R2H.!%#]mk5tXmGU5UH2$^td!'" " " " "H;dM]Ჳlg{dc:Y[a5ՐMx,vtdiAD@D@D@D@DTL%˷XK:]att\-uDmê6Z9X(" " " " "P*++y{V=Ʋ͵=nꬵZUYM{HY'Hቀ$@5k6[o\\mS؝N6ںuE6ZZZ-9 CD@D@D@D@D`pKAo[%h*lcu6dlUUXfbEu;~mCϬmcND@D@D@D@D`d 0{g;M4i5lݶ6wٺ۰՗?a oDxI[a럲{%[&ڊM k޴ۭE-m޼y6sL`$ " " " " " "0~ddvfNVz֯6mq„ smwѪܛ!ݫ!KhgOH&YCk{m޼9#1ν.K#9Â޲eKY\q+Y%5k[ƍ׿uC6=k%zb {vqEϩF_]UUeYmmKڶD@D@D@D@D@FĉmҤI;mywԋn,3D@D@D@D@D@KJֶӮE:%k~vI@vyڵhR'R|" " " " " E;ol6ps1v)D/)z1vM/" " " " "0<HX8lCw\Y%j[(%ى'/1C߯'u/edώbH%/: 5kV}mΜ9䭜6%kYU]D@D@D@D@D`w-,'|r:餓2}KdT΄1ӧO38f̘%j؁îhR6p5@RFə!rIؔ͵YE@D@D@D@D@F@qȮ4CzR|" " " " " fmmmnÁ[CAdͧ0$aL:;; (9qw R/Y+3D@D@D@D@D@ Z|_>Jp#ZH7COGGG☥bPV*gBq ZtR/JJ )>T{W\zd9-" " " " "&dM}z8 /J]޽%NA$jzkvI #Z\6AJeI(?ov͟?aFGY'!-@D@D@D@D@D@D?Q8qb!w(G{N֯IDATJ.,lS/)" " " " " #Jg!Q3ka6F#Yc'l/#d[`At#50Jh0LpzYl\%;E@D@D@D@D@DWO?ݐ=s- G(m,vgm8iO1~ic&" " " " "Krbs%%Z8&kq$$㎸qTO?5$j[0@\>R( ~NI~lS}TD;藘ڽ9%Izac [6YL/ې~uluH$D-.1wh$KsuIZ&Mϸ:$HXD:+{CR AB7S'UD@D@D@D@D@D`/Fҝ9vG;z< 8  qzXJJB6orN $r eӝ:&~QO MI/QW{~h9Bo:$O3Qu?颍:u_>V't#VF"YC@HJ\|ڞs2dFiD6DB߼B1%9/|@č $2KXq>ԅ2_d-A <>I@8> :}:<\5С"" " " " "P:Q3gߎv m~_|=괅aS5w$nCDr=ehG?,QBz 6 >%0\K?E@D@D@D@D@ƚ?3~%-Pί2 X؇PҗV?EHMߡ6?X ,:1!Y`3u>=?_@4~9/CM|s0G}GT@ǯ ޗ8&fls

:۔ uQX|_ПuLچ$G*YOXFPw0쾎6 б/گS]D@D@D@D@D`0g`alA&1m1|6/铳|?چ%!8$=i h@uB=bNH?!sͨmJTD@D@D@D@D@JsF %%^C@_0>L~J1>L: G/Q0@ v¾uqYm/*" " " " " A܏6&bhSRJuQGI9kma?6`SOFymC/LΘ|:X:HC&|GaෙP:_g?)C49m}DH$k ĈzE: H+sJ&gcCu6 e" " " " " cM 4ċO~,샶}Pr$ %zq'c9M'`7C?$'l68w$(:ژxQQ6ǁD̵څ?,9Re:%b:uI+aNo/cbP@i}?wov)g0 A q(?F"YCP~A;% %0gOX\~=gۗ=~DDzD~Ac %a:daPڃM0! 6m3PH`ű|;~V'tG!uq.J|g~JD:$F:} ?lo(1|۬C_¶oP/e]ɸ$.lӝџ1z`ǸбڬDסGgΎc:H&Vaa$~PC̵r?9c=ۆ`;I9C/TxaA)#}@¹d3&H. :ڨP(DwX:HP}?iӨ&" " " " "PJY?z_6٦6q(/a۷]T7CYCvG~_갡lG^" " " " " Gy" ~v >emaPK,U JbfE`HP >Su~Ltч1 K!{诶H8+L6y`}I=%lVccmP2㲻Aus_16ϗSO-M ʵ|+)" " " " "06PY_*uqmߏvog~`@aR |hgC}/~C=ې~Q]D@D@D@D@D 0@~m_:$uoc?Hs? ьDsg=műg8 @igD-ua ~;pܰNa![!թ+F‡~єP$XG?S]D@D@D@D@Dt 0q#6 IÇۃoXGۆr $ 9Y:v *?9J٠L`G 9mh`6G6%թcP6$J>gKB#ZP3 r@ %p 6.)" " " " "PaQ:u~(i Уfkg qq;d̄)/mί'}Y|$E@D@D@D@D@ʏ@\6Xq\=Mq!PF# 񓣤`:T~~hsS]D@D@D@D@D%EqPWLJ?5>Z;k3y* }8f(ڌ!.@?m(NG}hl{ĉ#Ka]tic9B" " " " " I LU$}_GB8hޡ?cw)h?.l]~J  ~~t"G|gQݓt'|GRD@D@D@D@DG L¶GzGAZ $l  d1 W1>isS)" " " " "Pf`%$)F+QࣚPI[ cd֟$E@D@D@D@D@ʓ@jR44[4h&iu$k\zY_1>YD@D@D@D@D`; P0r |&k ȝ6C'1N_?E@D@D@D@D@J@Q VBȃ-v8$YBwG]XHDmTB"͏a̓5?G y T[D@D@D@D@D@ , 䒵0@QND@D@D@D@D`J-1[GY$kqQND@D@D@D@各{6TZ:RPH@xֵf'dO (YϺ," " " " "P)R" " " " " #%kYךE@D@D@D@D@JIENDB`doc/imgs/gettingstarted03.png0000644000372000037200000016651212276056621014557 0ustar gfagfaPNG  IHDR7:iCCPICC ProfilexXy8U_^8p12ye" $Gʔ!2&!R($R$E!V??ߺֽz:{hhpK=M#   =#B5͍a_+ _[7w'9l 0 AhLdހ1]8@#3aX􏍵l Iў<1/hX .F48^_s /kGSp["|Z-|zyk-|h  w['yu8pGz ~$ EI!$) I7ȷ.vB #S@y,}sT}0 P5w#wht~wx R@(u q? r I d(65 A0 & K`lmA"@3 A"B:1d 9@n/EA9(ʁ Ash OwhDtv?B0BX#N |a8D :qьF "&U$@R \H1R itD ÑgȨȔȼbɲFȖiUȭȓȯגϒoRPPpS(RXPSNQObkQKr|~I  GB$CIK)Ni@EHYHLrJʅ**jjZڝ u!uZI3`,*4+D,C"oiqrrsr>L'iHI .6.}(a=nnd:<< <><<=<&ռo~ 0  T K'0B BBEBaYa?BHȘ(ZTQ4DTtJ /!-V- n,,"~+($JJ&KK~*&HJ'JJ%ZVD6MG@N^.\V^v|Q[]De^Z{uu/544ikJhk6iRJFjighult trVnG_џ2`74407L05Y767n7A\553 1m1ffWޚ ?X[Z,[JZ[[ZZUYm[kZg[DR:VӶ˱?f`?uuIӒs'Nxȕݵ fVn^džg窗W'o>*>9>+*W}?k (hXxdT v n !8yXHhj|RصpPĉH:D E FZV.މi/~rs]pWCK¿~d,ߒavό杊_ wv?c6={|x 2a(] „c$>'@eD%coQh `Lv(^ /&W8Z1:${oXLY3ن9h8MHY\<\|UtuIHIܓ"ϧ`TB*fQ١5KǨe f(oela`kl~âްv B NaΉ'.Tv{|By}x}eM݂cBOfgG\jӉQ g'%I;ϑJzya-}6c0-bˌWrb];̗r#}t?Y%Me;wMʣ*.VU W3ېXiywU;CG&Nv9$f_ӖgL.}x1>|g$jTg86n$qrU׆޴ΤZ}|؂{_{?/-|\Ysj/I_7܌Qf~o#?8/`fvfh!E#?xt% KR 8)ةEiVatY I,l9oId\N0EQ3YpXrY Z(٩:9:Y;۝0wttvw615W P a;J F "#֣?̝}7pi|OBۙƳI%I8ߗv.靕yH+9Wms#u/@ތ**_^BWxۥ,N Dx[uֽ:z 6kmk}u?,~<ڍxշLA!rr#rc/E&8&Y^M_MCo|ӻo?,.}Yx eU|Mz׌o߿mq0ގY˳wb` h`M&L` uryBq1D"Ph &s 3PDEZH*eGӔh!>r4kV-6~v,6;'ׂO[-#8),\$rVULW$D9)Gia̔]S&  JqMUDTQ3j45h6t9+|5|fTbkbe*i5{oagie`g}hڶ.AΑq٩9+[{IO/6Mg>A~,p  ڰj\Q j>'TRWixwgzkL:~%jXttnŬ`y)jCnݵ{yewsfi?ʋKP2.鼻ZASXY}^{JSh"4kֶ}}(qĮO8zξ8=1eԄ+״of%ί/^ZȎA]p`By8cNZ @@ ră"X/.B(HցP.8EB - *4 R@JlD+ѷ3z9&ӏa8W[$#$%'"oRR'G$lRQjO}z& 1AI=2#C8c;s4 9K4[(;-{>gɌ7?u(DbE s$*$3beeut LݎĨ&ijkuu}r ^6J23 56 3 :ihe{.߾֡qiЅU=٣7G˯<i_ e o|7/p;왤sA)_R2f^N"Ӑktm2 B뢠%-G*ݩ]tcΑngЀPx7Iz/2.zKרo[7vD[>! |@ hAB*1D"dPn< 4N@ww1X#,jcӱ/ql8?\+#Y5or; FvJe6?[j >%GDYb2m7>2C#+P *V-vUU[F;znN^>5zfBB?E^;&!"ܒZ~)DK%35V6W8=I\\kOyxN;  pq ԗr>)-[.7"SSIO9xʲF朶N>A1%5sK{V 6+vmN9'p.q>oG|Mhv4uq?4f<5s|lNbZkrC4 2nfI]iϵv pV?r%9JW|MGz}_ABSxvk\ë:+={*9@ŋQqʉS|Na{<OfW?|^Z¾μ~F緢~[o'Fw~zk;Wϸ`˿y?GZ(z n܆E,]AG8'|3=?,!ߜ pHYs   IDATx]VE~]jnAD 1[.D[ATRyevom(~=gfΜ9ysg@ii)\p8C!p8@U"Pʜ.C!p8C!pl~p8C!p8Us6R!p8C!p8C!p8C!pT9٬rHBC!p8C!pΦC!p8C!P$UƝHa@Nd3!p8C!pC@>E?-m΁,7]!p8C!1iMT?Ÿf:C!p8B`guBw*g[2[. ;C!p8; ^Z39Y2ew>p8C!p83)m?x#ff,b,25!p8C!@,e,2'ϿٌɌ/2gj&j:q6HyrԐf/$jY1h\i4Oؐ׸C!p8C*嚦)Tm`\ȑ#GrhI̪Wf5QWHS>E5jT!CԤISRRP C!p8CY'No':~Q3UlH{ , *Cjϼ; :~()):C!p87$$$@"Ms4~slꬥƙiMLW٬204AXxu4:^kРA~yyyC!p8C!@qq1x$&&8p,9ʡ"(әtS&r@"nsRTAFpydjRڢҔuO@]p8C!p8xgz,ڡNJ*| nٴtfc4_(+@ŋeL##??[ׯǢϿP MLKC^=PA")) oWbqμukϐ':Yi={΁!auߺ bYRޤ7t ԎMe:C!p8O4:L$ ʓ2h{MNZqMjj4b9N6mU_(Œ&a˯V ou7Jer_Y94<Zr2;l}ydԫ:-["EkI^[-Cͨ=,W:,o,kga&hťXi%6ZܬBw$3Nd:@䮟+kGyϝmѵ7=߹B w v:6ibqBYp8!뮻%XΥgP`$,'Q_eg6ՑVMj*ôh9raȄ&cΓϣUz tdƱQEXGd spDg:NcDӖhѫ&5@B"Mz˦z $& qQuN]#Ny[Dv1اa|KᕟABIvWDaAqC`'G4.K][lJ#U-qmgcJq. {>)Vz2駜/iwK(+T?=r;&wW".*\Z >/+rFaT|u8;##86g̛78Wo98:unݺyOG l*ǔ2]y2]1R4"Z`j:'D}v,kTJ$}j]L"%,dn($OA. gpκu7M;dul5n(Y=SNDb d6F*6!9kE6CCӹAξ59lmSQ+JVc+քu)ؓ䳢OCS:Jp8~ԏC1_8o*EIRUALɺO(o- ^ %g"[dƿyY&$y^*JϾu/eE=xAZtGi%ذ%߷.!p8vrrr7`8CбcG;"KSZwsCcϛ`}^2(F>ASFĈ2i HQ 6i~0(XJ/;d|aIAjm^_i9[ ˜xDX.!pH~)h}/# 5y< Egamr.ÐxaK,A/ąZPZ-8jb$ :`8ods8szg s.)3A֣t8gc>Nwr ƌ߿_CyRVPrA:W~% q6(A;:L7eG::Q)M'z$iA Jdb3G< 9IkrI^gzBֿ~AiHkIuk"YML+Iͥ"Yڅ(NŒd$feuXn<Ҵ=,wwMds!٤(9)I IX%np Re3vpeDsv9r--3y=6]Qߏ#ۻr0.݁{ޟ5z xtȐqp.4ojumCCGwŅ?o-U7z=+y\V w͸3Y0,<{9ƂYX,'lM4ѣ\bɥ]oǨCx6>qԾrOzuxιbp<N8O=-g[N틒=VNx~{{.?twpȥ81(ӯ vC!$veoFrEt uV8M>}`mLq?w*a.*<&hHy3O1=)@kVJ5O㤊.yAbW}?<#yן\ ,k{?.sg}" &.M"%uﱸQ=1ûomt<)./OZY”t}46w~)?^93(57,U@SvFGbM4i=_:uI^#x78HVx~pd$‰OmdÈs&)I;NHZu{%[y0~&z"NZ uqt?}\}at o]r@"ˌ 2Ϊ= F?'4KLiS,kE+lڊ`ri7y%OLSGἥ@@'O)s!oʱU,+Bjm#_!5k"1Ct)GP .2Dq0)uxOm4ެN3mAFR7Y3A6 JJ!7Av #+OכmNC!!4tldu-vF岥Nb2HtClynNN4,RyP]>8>r7x&y3^z'48;jy52LWlr4Fy 3+hQʹ=vJa]8< _Cq-3H'i2U[7 Wψ'ؔ#ZH98rGZ)G3S^%dnRD o'"Yye$2)wJ:e9m>ouz?C!/DI,Ga5pϕW>˝yǡ@8i9[e䜃 k œ|OD_ahm$Eǃi_KKӣg?C|qt{ܗ*ΟDqϼ` Q+KKe/\z(<̮mvþ޻^eC!pT+dɂ 6kժURu?RΥ:e0_W f9>c]JK*PzgkRu4W>}5GIX*%ȋ)̔:rYOq@Ht9@>G GS@ᐥ/ij*y^,KyLЭQJNdž%W1MIHC6 N)65 i~q6j$g[#tyC_\ɑ}kKEl(hYJ}1B6"g<ا} Gވ`=tn2c/_p5 C ŴYYG8nE#o{ʻ9 eh8|o\ |t=}O_S\C!pT=.o6f͚m Xn62,KC0&?JӨJG33ͬ7Mf6-V(e62Qg6廔5&,ߤLE>kr&:7 W:9<(K+ Nê,5n'A̖5|kWari2!)e"L,I'uo`ch)t'e6CE%ظ, b'!Q6"*-@Xc]C!W"YB49|e ji|zZݒWQr8{&7gtJbEKB#3Z9q<4le3kƽMIҒ4q(4&usHjv&nU8XL8mHw 2'|BOs?DҕCqoto13X^1`(8>ɻ7|x:nY⣛G`\ׯpC!pT/2qFrڵ:u**:uBF<56;YM4]/Blp` EC ˨|8DejM5RbIͼ&MD<&eP2}oOMY#ܸe.wEbS8S&sc4hW%dɫ8Y$8+ %Crn1\neZH!;{DYц ۰F1(4hUg]C! "`3; H]Kr/@Z&Iod4@ݺi(ܒ-_&NBکFdlJ|ǐuk v#dr\y\\\˗k6.hPC6 xu%l ʅ6nZ)[>׋@6%fY" Ȕ[$Y{%e26, y ^+\9-7A21Ӽ !p8^&: FG *SX(F>hl80;_=)oS kdyzH`S><մ:t5#i,2_y?4MM?~Q''4]N䖟")k8q5הygX6ݿrE.OޗIj98w" kِ9(Lnz]AnĴDX#̓M 8%BqFN?<\C!E@֘ ?]m!Vux./"GR+nQ|蛃h ?U߄CT;C!p!>X$ïS~РAGHyrБ<38)ϸ9,fOZu05݌׃iJTC.V'O} R`n-D!V׃TgBa| |?d'f:s8H^7 pKupF;C& cYӧLT*C2J%WQe`ǃ̐ 6jPDu6Ma2/UX:Qd%<)l'Blvkc5ݦ*vqC!p8C߉0qXL"Υ:.g~$G LrReu61F5͸QGӌS.jsh3nL4.gM>3.gM,xC!p8C!';IL晲KU8TM3)X xt8C!p8"WoR3qiM'S^P&`TH`3s IDATOHC'gbu@UVg VDMηvvkzEkP!Z{{J(oR>3ϏV_eYHvEK0^[Gg+o}|;noʶ/Zx&oq~7i_\eIS|hvܶηl~Ekk_\eIS|hvܶΗ7`7 8ʘΥ$0C0UlicRMS6%0pW@:n'%DQ-T˅A;0#ʓjJvhY>SVT8ƕh+[6/^m+L^ڪm Pjٲ3O1`mg__}~L;TR晼)ycyfƕww!ZоE+O5˚:LޔUyi11N^Y q6}%Lt|R=;-UaSRMc<'' ,XcCvo[fffR7o,Z?3>stjԨ [pX+l}񖯬(?[?y[4S.V^˒2?Oy):xhWUmoUs|sa/^vy;O닷xZ_+o׎?^yvoxWn[v{?/㕷kǣ2dȩRft6ypu%e\k'e\ӔSأBЋ˩|:q 4ͽ >p6s„ ׯ?|3ٛ(>`wǻヒ>|0222vpNcq^X}sbkd #3ۢR_i5,oڦR[geYNeW^fGG.Mo%nڢR|UR;?^ԣel]M[WjкH|Ujm}M[Wj>Ujǫz+ָiJmZ)]oWJ||4v/iJgWJxSu7mQ^C"eWoη7mQ^]>^W~2X-+uh] vv]^*X-+k]^*O=ZkܴEyη˫^v]>~;_0N3]DѸT IckclIM߁/IRJ%&0h}޲YxtRO9t>,9NN?/u&-4\])o?Y4\uĵ^C1!oq3)SARuq?/e 44N?3hJYFYmk;wOhܤp^ˏKhиI3hJYFYmk>q/Wgz.?/jX`3Ӣ,kiEHF5"R*ӓIkA6mU.駟N:VF7S~}ԬYېyW;$+K)X]<,\˖-CVV6n,v!,p˖-Ѿ}{]*gv͔Q#v}jiQ*U7|_[ Z g߲e &OkCvv-^oeBjj*xp8繿i7o֭[{/7Kƣn4շuVL4ɍ4+,>Ħ+1L,j*G1M7+oWΦ4R e%ʛJ;Z C2q5i]F m? ']w5lE3q{rg6mڄ 6x7_}zꅞ={V3^ͣ3>sL̘18i#Sn]v9{Փm}bСcۨQ#oc-.Z('qrGdga]6tCu8Aާ!%Kpҡ[nЪ nܭ*$z_p#Φ=NM}o<O;=nCr٤H'S)yu,CJWy6Y絟$,olVgxǼ/JjtAS4VvkW>ƏTtԩ.s2w(T l#<{ڥM4L'3kbOs>[e,Mi&cǎEzpעK.jsf}ܸ7iFt Zf9əC!w!@/Ozbd^{͍6HjРAUVr8B S:HLǠ!>l(AG44)N jߏ7j!ݶm. :4MC̹(~ yF8vp㮍H̙O?{ ZڶmC=[~m_|%"k:IQӔW%<4TBiQ@;?%@.{X-gM;<v3'M ,ȑ#OXk'oYfE-6dovs;, Z8ݻ{3ߥe]v(a_y;[ |GsvC޽c@S:tD6)?ӼwŜ X'1&opkEЬY3/+V@v}pW(^{U۽L@v7ǵ){ˏ}bG/! 9E&W횃׋K.I&$AWs8@ M۶\`oKU6ׂ%?y慜^Fy4#)Xv}=k/2e kpm~7 7~rŖ#񕗴cҭVvLuULcBi/m r`6k>,w=SQ~QT("gKG^(K߾`[&\~<EVqWm~cW-;s\7Uh"VmةI?2_ڴ{[m5xzh,¦u+n]EKdVU6X9,y.Bu\wJPa ^y,sj%ba짮 bG\ÿ?ޓ1§yP'mkooƘ80R淕KniMӪ;p|GW|os? m-ی$l\/ y%OGb ˘+j^sr/w+?v0A|Mou9Ã< x"+IR1},*4MxY^˪ie͘+ +J3 NA ]"9nv(v1w.t4-.Yݻ獽Y@:\ϊɄKC˥b~/G9S^K.^{uxE|/‹nCy`S N:h֫p@NdZg O]Pz!HrG7{ӉeW-,ԒOh~ (~YΙs'?^K+-zaܪ`Zðk XN𯷼Zy `'l)y}-;%^hT? <` Fc0ɿzO[0f򢤖j> ~[1ԗ++Zn#o8S&Pp&+\VuʪfRslWO/qE7@ 2*hL4.:<4b~&1"b% L+߀rYØ7钿76r28P -< 3=6ﻶ׬u?߇ioxMʟe"';3ǧ6^fLˣLuSY۶m׭^zkL6L,a}!-P\*?|Y(kk98P>sd˷Ǖw+?Č ٣huGWH?V護\}&93t8ÅpJp%},EB>#wumdQT9o; Ǡ%FX-o\4QDž?˃C>))nb6ٷ`9 .&=1i$n|w|->WS`Ƹ0i6 YN[zy'ἱѹ/ Y^؋ v7wBs^˃ 9Iͥ}0ަ1c5虺/쌍_ d}xa,)q{bW!KJn~s]NSE|Zx݊xq8%v`r!RW׾ן٣#vZhTO9ت`QX/_҈,)AVJK;2~{YR]d;i/Lyyy2iHfK4P)\ΐӸu]>~hLcm,zU|VŖv'U3Xu!8ϤT:^=B /7bBs8 %ׁ@‹oľTn'andyU8Ep;n>Ƴ3)v[7tHha:G gQ*Xhճ7ڷ*.{yiA{”v82q Mn-7+N )M@bMt? ~PsYt"vⓘgڥl8~}`\-?!S, $z7/W>-=j^@e&$Q(Ӫk)noVIGc_!gA[tl 7Km=OWФf*~{h/}SkL ik&>K^.e.9ىrIO¼pUC Ssz*{][[,ƛ~cwokٯWxz-vvHTӯ_}KeW 7 _:MpE6T^8Oάѧ~wutD8cG>щ,wCqg>ӑ  _L&m3uY;E9rN^XF˳^&3K< Qc\"ON:iCR'yM]T2fy|MԱVGịgXU~~f=|č8\̣ eYfyÇ J?4LJ8^{Ɠ]qG͛ˉ(~=Oww~S|f͙~5ú< qH{ ZZ*KD'#{`za 8XQq~czUE8,O3|-"wllo-*[n?ԝyjr .Iqʎfw?1xw3Ϛ<|_Np/nx 2c;qg`3#uǦMxs`e+[ ~y+/@˾?>Fh78_C&wءRݫZYvXlgñG“g;-sZ4۱ܡj?+NB7b[mٶ8ۣ5zM,5^ gUc-¢WƜ?7z.NB~ħξSbp̾'?/fGN m\vO18pLm㺗~e)Y0ϝ[`u p5eX5#Լ(>㳲ŗc qr ^%.9v)v-9m<_ڐҢjy<9Eu |6-~ |սC1{ ]\sOJGrMvp]zÑ?%v}>!Hk?|)_.|㻢F(Fdt?fɎG"AffnfsEСCwHg‰'zʹ9n/b 5y6fowM^ߝmNjESpw:46A,+WI|E6~Yw[K}O>Z?ֲepovM lPi+Ud)CJs Z}iu;~1856ҁ vwz ltxot^bD\DЙU}t Nb wn>Wd)㇓xfvm1 ղlI40֍~eor=j\Δz O9]e`ͷg0 -W'B_9Yx!t˅6vg|&_)I9 :LV )?e2/UH1_n>'&ou`'pl&8eeyufmt,] s_<%rwL!eXZ6\ ˠ[]02T9@E܃;  }hW4yqw3SnB{c򎔴;Egafޤ\4^~m`㝵_}wá=gL/psCwT,Ss]Ѻ@\d S3©xR9lC8o[mz*8oe6qmL[<)3IWT\$wPJ(¯cpxh޴=>ġ0}N>Mc'RMgo.ſ=*mm&=?\W/.3 cA{?{U6op6pm<x%.|' >by9aOzl{p\;?p=6}7|CÇHFz;F`p8[Nh7?? m +p)ץVXl:"t_eI\@o1ZczW^t4~44nY0H,sl#P dAYVӚALқGqZ:+&8i-~iPSyb=ԖoN8]pWг㊑k4aP| J+dP59GM[JyTfP}N >A;v?(M鹠oKȜ@0Wo]3wngcq|NHzj QY7tyMBҶN 6Nu8? XV>29*ҞP2^umJ,ē%v/~3r=5r7f#\b#e MqH>`h4TL'^>/3d.NTل$tIZG?*5 ( 51ԃr{lL{ > =sO)"?0 M-KQQ[f,YϾͮ T{lepnDcfv,en7=>f!fK# ,.p ! W/Ut7y8ڸ[,w7kRwikcpi kVʻr,T[*LAF1{\p.ޕ!q7˜w]+)!ڵHv=VḺczm?G$Z~X־37hPjd{,_W"]a+ZD7\2)߸iX?>΀>!#.p6!:;P,N rmfv>k:qwg%s/\׾}{o6Nh⣶RNTګuIܚ/3pY0 7WBjHԶWm2_-NMRYn -R2Nԣ6j2}:/^[}MIJuc7^2? t6-dHt*QŜ`">'N:Y|Ie dR) lTF{?7X.yvo/ i~{'p3V>$>}*V`D 8mymveA !;| xj>ƈ&?*ʢIا%:Lal 4+3dzcyyv){ +x؊~\^siz 7y˩?(IDKT8/\wx^FVa_b0s`5B($,bro8y; }y6*Q˼wjrqA0T= ü9^bdyxqNH{H 2\$?v@ݽ  f,ԯ9J}큓.8-qe&G {ӆ%eB`/, &p6pXXy@F!\Zy2V 'k|D~!?Ћ׼ЊH`u ɨV>GPh/v~h޹5Fsoc|v74)~`$)O#/]nEC0^.stW=/7N;x~ءcxdq-fcTև-f)3vX?dro܆we]3U?3,4ul]8܋׼bdftŹd߲soͳq7 )nv"\iEkCi%r\~aE'F G"^BY{|7ǹ7b3p3UVm.`Gp"KdvPɭYࡡu;hTF8nqwQދF m=uH/(w">k@rB3_' ZS(LxCn1\6ȸ+cft13:R_Z C>g8J+5%n]T/s"nd|ks8:8e?'wLSAhvR7ewUAiw<<.'|D^X'͟??<8^?{Vk/ӡ:mU tO0¿xU5|ge̙ذanIp5 ύDQ'+)i3u3OQ>Mo 1̳E2x''%c) f0 ܱg;:x+¡ؽٽ=mO 'gO8 -5IY:[OsWӎDZScWZ,N /aUvp6,9>Fo,3iv z?0 _]}Qyݡ* MĮe>Cym?l\8 s7y}zx'Q_Y\ zl ~Ƃ_c8݁tL&m{Gˮ7--]/kӐ%[L:$ `JIO(msOp3&?t%/M4坻Nk1x4},(q++ &x3( . :0{]8Xu~F᏷KU|U!{"w6pbޠV04د|f6xi7Btzk\qNwmRXno MN>p9""mӦMq74 N(<,J~-QQ,k.l̈́\^BE;-ROpzūEg\+_e sOWx0un[`xykgJ D2V~2c(3={n?\q\/fӿ~[F=C-@եC>4:uխ7eo+[\)tuIhƶO_~>z1BMNZYHf/*bwn}FCY:ߝ/$=\6T)dQi:2|yTl$JO|Eǵpbwҥgf"%90ÃKnV'YʖV0˹/J27v]8乾: IDAT-kQw+8_:; r>,xwz7.5 7Gک -sѥ^(wCNߕZ+)x, fܗeTwhv%k$[23%]4EV]7qssbz籾zz,HNo{A{/G˳tԩY-C復&WZ۸enCp0w߸L[RvE<{7_Mwa|b]33UyS^r c"Yd!ӓz2IOX429C/rEmۯ[>L;_2.?ˬƯ~V/֍]4@ζ5Ԏdz) )Osڴi{d;L3}K7t mMuDz_èA~:Kv,z;)^scƫυp3-lMgJ2}ƛiZ'[ֹ5]Od_[0ÊWû.km~]'O|d8``"0dX즾o_Z7ݭ=tm\V.)>pSu}f_:[qo1EhueNr(TXMzO-m]~u\=0903͎sWػ˭K|bT+iMgysȈ 4ݾ՟\/bg}Jkkqq_JJvA6jb01mXϖnwߞVnoIee2r}d/n!4}^gE';y{OZG[ew}/?DŠR|WӤz_se܉~r6aΏT6W&M])hn4u^znB3Ei/d2@ݯ7MY2o]ITBrmܸqQz,uMuu}h`'s(^&mj`߇@3YG%7ipI"?Tm?:n}GDDcn!-mFnoT)]&SU(u_=C}ݣpς ꣗_~9:Я{.X ^y`{!}غ'OGACxG삢3>ACw_]D9?0=⬋ TMkKu]7lMv9$^"g>qYY=&=S;tR\}hic>mۯ1:^ڬ3lvkgK]J=@k,sjfMsAhMFF;XMtk ],yRz'aSSe @nDoAށ_7ԯS{-_Olw7dC(ϼ6l~l 3`64t?UlÇGI4e[&|VY"xSӺEϣ^zc/hXuPOkS^zEǦA~Ukw_,k2u742f=>^ .>EpꝎ-] S{>wHo>7CWoky~3ni٨`mC~:cƌ.-Lo$Ks1yJRG=o5E'nۺT_AUC}d+5hLAmlm]xm 5lK|JY݆K+a5MZt:/kGO@M^M S?صssU/[ﯶg꿽^w7ܙH7_o^ڢև.DhPf/89SN?ic/y>SO=5 dHƛۺNlGckfJl/ju9^?VݶcT0AL~>TLYߖPy}\~^R{I~{IMm%﷗Z_R{I~{IMm%﷗Z_R{I~{IMm%﷗Z_R{I~{I۹пZh_AZ7W4|KKnj{|/em\o{j_г=㏏&=ѥ IJ^>u5x; _'7~ai|u7_vl,6FoiN|3շ}V&x4Vo_>+Ojuqڶog#>zIm׭?~oL>յӤc3xxu+7i|V/[97ϻmk]+g*˾xukcu|V/[gZL6cg*˾xukcu|V/[gZL6cg*˾xukcu|V/[gZL6cg*˾xukcu|V/[gZL6cg*˾xukcu|V/[gZL6cg*˾xukcu|V/[gZL6cg*˾x\궄2ř8TCO3IG{챇 2D^|E=ztt Gح8 G2VmSw3 {~z_n}"~\Xr)l}ҲvT\ƒKo|vL6?-沝O]-4\2_7Č4|ٺ~\eƗKV9W[U>i>u|Vb3u2-6ڟO]-ks6sͳy][֋`3q F׼?>8ߏrpt6l3e]f4 oz KR_Y~mO4V׷rjw|[\fv?ϿΣ,|6_oK3g3׿`]o`SLIx,K]4g}^"^ 3f̈lR+Oyni/_6VR-_V[YKm>i[x~|=[yoe-no~Myni/_6VlkfϳuKVR͏m+kv[~<-M*[=K5?~~.ni<[4o,_߶lںlҤ~ճT~yZkfϳuKVR͏m+kv[~<-M*[=K5?~~.ni<[4o,_߶lںlҤ~ճT~yZkfϳuKVR͏m+kv[~<-M*[=K5?o->T\?q<[nY /8BvCe_}vZxwݎ/S;.ɷ~sO;w|]Ǔo>|wM-|S'}5wOxm?7|kM=|ohǓO2St?>uKo}4iXmgcvl;޿Wއ []#x1GtϷ:Ʀ,~_wOϗ\T/m}o(j,W%շ~k;ͧ|ճԟ_Til귯0>/k^6{Z >FY//.],Ͷ&4SS<[?u˷,3틗͖oxY_|K-Rݟi=ӾxlO4S<[4>f+o-۳gk_nܥߡK]/ל%%%GwfA@#0gadܹi~L[:u$:t~Z_;vn޵6TRmh׮욾@@`߅ܹ*~ŃK]/,,6?_;5g6n7Z[mctmK?  a { O`Spآo@EEEk@o۳VAg  o?[t=m-=mQ<ШzTB [S=llzo[~m,K@hX@S~BUL"   @?5r|=Ct}|| 7DU<|6; l3xP]pVN).y nq}[u!0ٲN\c,ˍֶ9 ۻW|,Fv3yɚ!S.R_?(O~r 4Ǎc;z\ש\%/qL<;k8Fnzs>~w*>Kvk`iT`3L \٠W⑯@@% ؍J[QUU%-PrS6'Q""j~?OFfTn!jR}U~[wu/oc{;Jl!27w s^*8t}*byTS١m*eղ{6gc=n9Wܪp/&Eٛ\+n+mSEj3rҮKyU^>_)G }]kK *O?p";C).MI(,.)[lG%lHDt&TkD??oٴcz y5bkTkLaeͶ-Z @@ A?qM7E5=pk&*[G }XHҒU7R鵊_] m.Ȭ9"w8HZ=l;j%u1.\BF5J.shh~[n<2IdOO>}GTs13#7XFޖ;ZNnV4^qΛoEg\#.SA}{L9 }j~j6gv wɖTjɷ'+]7)F?q<.Od@2Y4z'QY8 IAuܱƝ&?#sWԶ2S## *ǿ9Jn9w|40UI~ 2y7]GM豽);>^ [Iy2iw49J{9rQQC~JGfjrUW˫_>gWXMs'+r^·1E^owԖf~# wVȇo#W\F/:^,߸ e+a}do)?RfEokδf [6S})~ϓvQ{L:/tIy^zwo0enpr=4->-:wqGI~"|N   u]]a嵽qiw?Q Eyz+xQz%zh GJ[RMs/?b6JZr޿q`伫6/T6|*@sӿ^.g[ȇC*ϤC :j3>.Ԧ.Ԭщ[ʁں:j:m,\fO]zx XWkw3n9A5VɼYd2+M>qWIEgI9X6.\&˥Qɳ/U^*3~%S?;Xj-N1;W>; 4G#Uλt,v͢6'Qs {nj>jyar=/|qǷɒ{Jr QkcI`yɿ僯[zI&_*o WHE>/\ipmL 3&/d8_Rfџ˖;mR;L#u˘O<-*`YzJt8ac]]<5_Vo~OS[VzfSnLR~͛5a s9`0L6m2m۫u;u>oQg-wUv7Z}2G?G]*t/NuW- Ok8 [ITHn*_Y:nBzxL0A~2欦?2kiȼv.xlzh0˞\,'\6S_S'd/W͖U5gD_;W2dFYKZ׍ׅ9u?T;w Vtu-ԌlLzϭ'9x6v;_Qh" rbdyUSdHG!n{qD~zɪ>\*HuAC+q3Hi]'^}]˝ KٲʔSzJ-'L9Sʫ2L,Jt6?w<)ohKjJ[ooQzKMw]}eX@@"СC(3>.ݧ-/u[XO; IDATË74\=':$)eJ0Pc%K1C=,,%+g4$Z4}.UJ}))_LZEoZ/YbY͗} [˪eܺ{.RZ_%3_~B3t'.X*U=Ol;ٝi{\0K^X^[U\yܭN*yJAu'7xs)?KUm lI: ۹owc(on5SRRGxv ;_Ž0.\>pg`f 1 h2&`NMuJU g-;:`ǝݽn#w m}xSn dm/Qq栋䞑)_˼eKtj406uݙ+W'ϑ[\$??]5^:R7^>?귍&$6N&ҵގ*j?Ģ3KfTPU:l5ϥ-k'E@r9R}Y.h٧O9d2s]ֻ`22}P͡}зqHgg)]傍mĨUPyЬe޶[IEҩ&]:mt߭.^~- OE ႏo>8K:W2O;;e\c{*r/;HE*vfj,ppkne I]|ɮC7ֲ\ tP)\*kkɹ7)Ǻ2vdeO#r+Ket rYѩD:;;s*ϔO(m.%.\>3 1tShvI^ϫn*;w*pO w6k> pg=Aݓ֊zn! v]dѣDg:w?y)}Eet9k,jjͰ#m7YY>\qMTG7{j]30iʉSEX,r԰ÃlÕۉY^Ul]͛)K˥g+] uKޑ }-yBy(zMD;]_K@@?ϥct kXBf̘F k\o>V\낾7qѢLHރԖ,칓^r'=I.@|9RUrEgF?S|bKr)E뻝y0LP~W@uIzl$YVJAR>U9wwEuz?2'K4kv?v_9eE:fvkɏ呫a\,'3z&ϣϖɌFKUMO_?QV >EnńU>˯x]..tARw/.S6Yv)dӭ~T#j{jtO| Erԟ/zJ7IEթz~*GN/!m_IW]$2K"R3֭xz룠}(~1PyN sAG= )!fwkΓQ&7˩a5XO/jH}iO'ҕn9i۶MyMs9%Os+dԾ  a Sd:B &'N)޾Ƽ:(t[˔>?~RzҦܝ+y_ϓT׍oV@RȬ^cϸ^ξU9ZKdg^$Q[jW2{>7;|˪CMW2YZV%E.^8S-7<˭(~zDO|ݸV/HhuhU,Ok>[HVs˯Y6|%VIugdҪlPڻ3 gϒ%.~\՜uWz칠}rL}sɇr/ʵ]&̶.+dRozS仃zKJ8ڷuOl֝ Θ6W*uҭ <-+7dɌiiPRI6޴~r +;_}VRk>wj^ VϗYsӗ׉6~x)))nb W7smivR_o,_F2nߓ) j첋ZnѢD&M׃ ${wv?2>wpS!}dݶx nw:Fl}U>SBwKI;V՘JkBWUd][k7j'm\XT,>/$)wfR.>(E; .UOҚ!PVRU^n@kfs{l-pߟm]R+𫞲Æ`|γr9wuAUW3h6?9h~;J)r*̩,6@hJٷo_3f|[83gK6cd2;wto Z⢁E+#r|Jm-?8d Y<ﻁdVɢ'<6+QYqε\N5<ՠ)}z,oXov[ |m֦绳\jծ4~M76k+׷IT5U_ה@h464Xf)lL;\e[f;8u sҳgXcjb_> mt]29<ե eNiMDζi}mkR2EZYKC@؀DcOhyGW_}4u8H>u%Z燸 6^@gXk^z6H,X ni42g?izO^9i@Z޷B#/->،Gm۶.]r|Ǣ3wڕ7,dJtVhm׸.YTGҮ]f{k[VVF7[q  h}aF7TlfX0}ө9/6n6YGu'jpk}XgAgyѼyYg75Աh6u'C  /MM^ަ?~o ;=+\oB#@ A^n׹mKhMGMSqX>)  G>6]O>qzڼyȆs<t}k@@`}@Soͧzp    L4i=u!`_{Ĵ  l-i C@@@ha- a8   @!<   6[p@@@ A`sCx9@@@ l'   ,r    @  laOA@@6 Y@@@&@=!KR-ݛ2IvW^VgW `@@(^8=IAAAcu{E*+w*8\RnWD Rbh/! mO<_ՎMrK~Z`娿Wʿޢ6Iu͡VNk`   L`6 8="].@nHs+ߒ#z&-XؼuV?8_Y_\$m+vkەע%p6QYGe'GjF㕢Arcd?s䔭(r_Y =rՃJ5 @@&^P-lW\DU֖;\X/+FǮC0 {uꚱl;w5QYp#yչ50%u-~|!WQyւ?~_!n  4lNqkeK7إGꥭgZ^\:GvquwvnsyNjlk?z?aE:l 5u*.!=ܞY9{<!;*w35{Y:%n^_diTC@@ lPן| &},||1* lc9 YJw&:coxX5V+?v'.Qޗwӗ\)o]FWHQvߢ^r捻 ]]~rO6s b]$y3 =Ld7k'BrNkeɇɸ瞎.+< 9G@@I` 6; 9ceȑ2#䄡pgs20t-;5Y/eb2 ٳ)Q׏=-;D:Pxk.&t2qri"Frdޗ>]F n?Cw&Ų^bsۮyrʼrO*轏q2l!  !A|ǵwSFg O߉V[ˊ ey^2OH:XҊeQ;-궣 jEvwVu^2o+/9}eagFnmr_/24},-te;KR]AgG'ptARUzb@@h.5Bhm[Z?Rw'U 峧EgO:(w{J(IXAELn`MdN"UJVꕬm࣎k2K3;y o/;UIPwZ-Ŭ[;{7=벍  4U`:chK .h8_^Sbo_~9E.~xۦwS ]I]ߧu>پh|?ښ@wY,wȄUNj~YN֔"A@@*P4uYlqL_cݞ/.{_Nq/uɃ嚟t{,',I뜕uu-(MU39A._w&B/^Q O~Gtfaz ]Z*ZG/~*\?kǚ/4yvBY4iwq]m@@@RKnTq飰桧^UC?m.,&{/yr{h=V{lmvvu=q[֥:*jX~3no*U>~%rX/+R>κ752թaۥi-;.yzjkmw!神VJMJ82]GF)TO5 W>O&#Rڞ4k?Kz5fE M|c0#I]^l"  #{hܤQOi\Yoi=4xL24NxM64xN:4xO>,0Vx@3-n˔FxP-Av|g.Ud2i]IdeB+ϒ*]MW9Rkn;/_Lv<㍥t,^J:v(m˩zD@@/5{T~/P?gݶx +gcTCsqѠ۷vu7mwIVD6khjAKᰉ  !gY@@@*@@@@T`y   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@ IDAT@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@Ii@@@`9   \`38) "  l2@@@ l'A@@@M   @p4   @@@@.@@@@6   6    &s@@@ fpRD@@ d    NJ   @@@#Fn@@@[ Je-5R?L>tšj>{|ͭn?    ".\(ҥK9s,X ɓO>{6˸{}ksvJ=tkyT m[3=hmɬe?O-z6p3ʎ;(ݻw&L ү_h;[}kOͪ* Y@@@)еk(,.._ϖٳgСC媫vM 2F^ .F~r%~~~~yۯ'm%x_OKjm^S~}??i/~~Ҷ_R{M-o/omZߟ߾_I~I5??}kjy<~~~yۯ'm%x_OKjm^S~}??iﯡV^-\Z?X{o5kr!2`ywwߕԩSew-i,֖T%k~*`[Zj[+ɷR|'-m\O孟{x%.EO.mѽn> ;Cҍeaȓ6mFy+R{{'x_s}<뷟?nɷ>^|5vsO7;.xmo?5wy|ͽǓo!w='xB:u}/s}I&ʕ+(jQvѢE%6LcB0Tn'UjU+ܝ(U_~n&_7I_1Nn^gG!W?,񯏒So"G#g0RS=E~P}"۾_>||_''ׯT>t?_k~~y?߯T>tK4^~R~??7t4ʹ/m$'ׯT>t?ǿa{?;Su]ңg@>h`~{ߓ^x!fm}sӕ/z|h7@~/ێY?.XYm`SFKlQu=Fk2ղ,NgY6-÷n-s/=Wl|>UX!= v2y?ihLyY&ߓ.X^U򍔗FR^6e6F͏OL|kiRie3⋶gcV7ԯomij~յ4)_?~kԟVҤ|߾/n_?S֖/[]K+~/㷺~LY[گ?~ou-M>3gmi~յ4)_?~kԟVҤ|߾/n_?S֖/[]K+~/㷺~LY[گ?~ou-M>3gmi~յ4)_?~kԟVҤ|߾/n_?S֖/[]K+~/㷺~^ZnH<{<ãuIq'_ϡNmCǩg7_}>]qL3gΔ? 4.\J>}⇚)T)lZ iiqh['P%U,*=%j5O^4VҮY鶒WdE_UJܓ\QEVO+.oeU}UB!?$JGǷ߫eYI&Mb&&Q!(1HH`w^މ:^RM~^h$%iL2-d3F39}<9Z{5g2yas&}rkC'@ +O>dyW_sϕ^z\r빷_>zhStŧ-?\A]nIDATb|uqj/O%O*˵Z'd+vwy|?ܯUy|?ܯUy|?ܯUy|?ܯUy|?ܯUy|?ܯUy|?ܯUy|?ܯUy|?ܯU?7_>{!roo<ٳ+ ?xyW~9߿~k|׃J^J?2CieHqaS7l>orSr rK|ӯ7?}-{wF7]+~rG?_Ǖzvzsj9K(*_o]?S~|;(pcdqڷKR.c-ʽr>k4ŷ?^kKo{[|KrܥϷt=M-}OZ[힦>k-w-ݿvOS|KoNzO=T5477G-oFSe⪇>o+w~fAzܐ:*4OPo8L->5{sj2uirus/}"~YЇO.:♟z{,?/9sO+?|ٟ)?w/o=sߗʽ~T}nȺ|R=\>o{J%r>2Y|KXtywKG`Kwg|GD;rm[o,ϝ;~©AS> nrt_zu4JU;or=DvkX3F׆̢k^Ʒb],#{ ?({,>yzk~X޹Zxr]t=Z_w{m^/f>%|I3^o+{ֱK֘~g[7Z)W+co[!BV?=_voO4y'$|~fM_LS[Gf'9r<{3~ӌz-w+aS0ضd'lr?DǯrEĵC @|?<5Xzh7D*?[ٶ&w wx,KZeu[he뭰ѯ}tm'>IygO |V7i͍תV~_nתV~_nתV~_nתV~_nתV~_nתV~_nQ?K6Z=2oO|e̵Ϻrk+]qiQK 2^9V2W;r @<y0@gl{г_zO216ToTkS .CP/oBu8@zȌR{D}&Ťss@ @3Fmz(%`eQW֣]˱/Z]hbPyW&Q}ӊ9ii_>ٮSme^dA @%8ègMRKξ+bb,;f5o}`rR~5@z/[qP|CKyZ> @<53Jc|G_cQi)5t呸#dl6So c$Kz)m=@/W9R1 @ ⼣^ _[|K;&j]6}P m=E=ވQJ%p,mb-s^^%T}+ @'YZ+@ @`ܢ~іَ1t,@ @;'EN=8hKvI-˕uh?Kja=Y}3QoH ^\S{ޱhg.;cA @X8ȧqΉt_[tٵj,yŰC.qKť g }ՊãuIo;J,@ @s xfT?鶭IG靜=`Zʟu\#e,[̞:ljC p^51I}PI^1:h9oe^smKʎ\cQJgA @K3Y}4g=JɎm6#//l19l^֩nJm}vssx(c?W>ȶީg? @6~I$u6띫y?Γ͂ @ p#xޑҼc[v̍XcGϐrw6yjTWwn\˃W\HWs@ @`iS|in_g-]跞Ct-,W㯊kY 67mc]I/߅|Z58} @%Y'g֕Yãs,_}#b>.M$l:(CdWzrOGS~HגmC @ -6m%=0*WV;CWV+Vr԰9Kjqp[=,*O9%!XIީk`kVvԣO @ x&Tk뚅j|.zoTid΍>)y԰z!/*~I%=ג/+{:_RK5,@ @K|-_Tk[k$mo]sK~,|}mM}H r֕.b Qlֱh{թGtےy>!@ lC`h~鶭G[PYs}w_?wFp,ůCk<*Si<-Uam$ @ 9|٧xk j-MIje:~A @ ;UrGn[-k>ǦHTm\َat!nYO5~.^ʵIWL+1H@ @&H}mܤ;s3Ʋ},c|kSM54lVv4it\9LC&]5>h˗W+!@ l"04Fc=Mz=Gib$Ou%V(zĔ(t}-SL˽Vq\$ @ m싶(3Ȏ51n]Ry++Sj'N1/8nuʱ9\\]u%cߨt@ @K,=n;KZyEs+k+~Ű`ZoBs?>ZzA @ .ue_1_vX'ͺsgɭF[i}8tH8`d>쏽 @RMpI䳿%rZ^y9Z@ @`çq5ݾ>߶֐A=e]9o ]E=m: @ $ @ M6e_[5}/r}j/R_HxΏ>ZS]ԕ/dk9ge}͹GX @4N1wHWP,}Qw\+%g?3ptJkܿy @vI`PWe{LN?/d}pcl(?׸g!g @ p 9%:OgMw~;{ P<]Oŕl-}qh/ۮ5\"!@ 21/꾿O϶;Snx©N9^mu  @F َMraS4>Ir8&giKL@ @7N1&o(goԠ4b;Xtj @NơrMb}%L ͺzC؆0 @ @sx`Lb7tF>trNm<: @ ]5 l8F<lH[bC @ ptf>l| @Jf,k_OŰY;xPZ @4 cYR؛& @%pDz@ @#=C @X€i@ @v$y;~չg@ @ `\0!@ ܎6oǯ: @ @`a 2008,2010 (c) J Chris Anderson Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at# http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. YUI Compressor -------------- Copyright (c) 2009, Yahoo! Inc. All rights reserved. Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. * Neither the name of Yahoo! Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of Yahoo! Inc. 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. Mustache.js ----------- Copyright (c) 2009 Chris Wanstrath / JavaScript Port by Jan Lehnardt 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. couchapp.simplejson ------------------- Copyright (c) 2006 Bob Ippolito 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. couchapp.autopush.watchdog -------------------------- Watchog - Python API to monitor file system events. Copyright (C) 2010 Yesudeep Mangalapilly 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. couchapp.autopush.pathtools --------------------------- Copyright (C) 2010 Yesudeep Mangalapilly 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. couchapp.autopush.brownie ------------------------- Copyright (c) 2010-2011 by Daniel Neuhäuser and aother authors. Redistribution and use in source and binary forms of the software as well as documentation, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - 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. - The names of the contributors may not 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. python-couchapp.spec0000644000372000037200000000312012276056621013130 0ustar gfagfa# sitelib for noarch packages, sitearch for others (remove the unneeded one) %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} %{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} %define srcname couchapp Name: python-%{srcname} Version: 0.8.1 Release: 1%{?dist} Summary: Standalone CouchDB Application Development Made Simple Group: Development/Libraries License: Apache License 2 URL: http://github.com/couchapp/couchapp/tree/master Source0: %{srcname}-%{version}.tar.bz2 BuildArch: x86_64 BuildRequires: python-devel python-setuptools BuildRequires: python-setuptools %description CouchApp is a set of helpers and a jQuery plugin that conspire to get you up and running on CouchDB quickly and correctly. It brings clarity and order to the freedom of CouchDB's document-based approach. %prep %setup -q -n %{srcname}-%{version} %build # Remove CFLAGS=... for noarch packages (unneeded) CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build %install rm -rf $RPM_BUILD_ROOT %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root,-) %doc LICENSE NOTICE README.rst THANKS %{_bindir}/couchapp # For noarch packages: sitelib # %{python_sitelib}/* # For arch-specific packages: sitearch %{python_sitearch}/* %changelog * Mon Jul 18 2011 Pau Aliagas 0.8.1-1 - Initial version README.rst0000644000372000037200000000722712276056621010636 0ustar gfagfaCouchApp: Standalone CouchDB Application Development Made Simple ================================================================ .. image:: https://travis-ci.org/couchapp/couchapp.png?branch=master :target: https://travis-ci.org/couchapp/couchapp .. image:: https://coveralls.io/repos/couchapp/couchapp/badge.png :target: https://coveralls.io/r/couchapp/couchapp CouchApp is designed to structure standalone CouchDB application development for maximum application portability. CouchApp is a set of scripts and a `jQuery `_ plugin designed to bring clarity and order to the freedom of `CouchDB `_'s document-based approach. Also, be sure to checkout our Erlang-based sibling, `erica `_. Write apps using just JavaScript and HTML ----------------------------------------- Render HTML documents using JavaScript templates run by CouchDB. You'll get parallelism and cacheability, **using only HTML and JS.** Building standalone CouchDB applications according to correct principles affords you options not found on other platforms. Deploy your apps to the client ++++++++++++++++++++++++++++++ CouchDB's replication means that programs running locally, can still be social. Applications control replication data-flows, so publishing messages and subscribing to other people is easy. Your users will see the benefits of the web without the hassle of requiring always-on connectivity. Installation ------------ Couchapp requires Python 2.5x or greater. Couchapp is most easily installed using the latest versions of the standard python packaging tools, setuptools and pip. They may be installed like so:: $ curl -O https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py $ sudo python ez_setup.py $ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py $ sudo python get-pip.py Installing couchapp is then simply a matter of:: $ pip install couchapp On OSX 10.6/10.7 you may need to set ARCH_FLAGS:: $ env ARCHFLAGS="-arch i386 -arch x86_64" pip install couchapp To install/upgrade a development version of couchapp:: $ pip install -e git+http://github.com/couchapp/couchapp.git#egg=Couchapp Note: some installations need to use *sudo* command beafore each command line. Note: on debian system don't forget to install python-dev. To install on windows follow instructions `here `_. More installion options on the `website `_. Getting started --------------- Read the `tutorial `_. Testing ------- We use `nose `_. and `nose-testconfig `_. for setting up and running tests. In the ``tests`` directory, copy ``config.sample.ini`` to ``config.ini``, tweak the settings, and then run the tests from the main ``couchapp`` directory (as the paths below are relative to that): $ nosetests --tc-file=tests/config.ini If you're wanting to generate code coverage reports (because you've got big plans to make our tests better!), you can do so with this command instead: $ nosetests --with-coverage --cover-package=couchapp --cover-html --tc-file=tests/config.ini Thanks for testing ``couchapp``! Other resources --------------- * `Couchapp website `_ * `Frequently Asked Questions `_ * `couchapp command line usage `_ * `Extend couchapp command line `_ * `List of CouchApps `_ resources/0000755000372000037200000000000012303145327011142 5ustar gfagfaresources/scripts/0000755000372000037200000000000012276056621012640 5ustar gfagfaresources/scripts/couchapp0000755000372000037200000000034612276056621014373 0ustar gfagfa#!/usr/bin/env python # -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from couchapp.dispatch import run if __name__ == '__main__': run() resources/scripts/couchapp.bat0000755000372000037200000000040612276056621015135 0ustar gfagfa@echo off rem Windows script for Couchapp rem Use a full path to Python (relative to this script) as the standard Python rem install does not put python.exe on the PATH... rem %~dp0 is the directory of this script "%~dp0..\python" "%~dp0couchapp" %* endlocal setup.py0000644000372000037200000001630112276056621010652 0ustar gfagfa# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. from setuptools import setup, find_packages import glob from imp import load_source import os import sys if not hasattr(sys, 'version_info') or sys.version_info < (2, 6, 0, 'final'): raise SystemExit("Couchapp requires Python 2.6 or later.") executables = [] setup_requires = [] extra = {} couchapp = load_source("couchapp", os.path.join("couchapp", "__init__.py")) def get_data_files(): data_files = [] data_files.append(('couchapp', ["LICENSE", "MANIFEST.in", "NOTICE", "README.rst", "THANKS"])) return data_files def ordinarypath(p): return p and p[0] != '.' and p[-1] != '~' def get_packages_data(): packagedata = {'couchapp': []} for root in ('templates',): for curdir, dirs, files in os.walk(os.path.join("couchapp", root)): curdir = curdir.split(os.sep, 1)[1] dirs[:] = filter(ordinarypath, dirs) for f in filter(ordinarypath, files): f = os.path.normpath(os.path.join(curdir, f)) packagedata['couchapp'].append(f) return packagedata MODULES = ['couchapp', 'couchapp.autopush', 'couchapp.autopush.brownie', 'couchapp.autopush.brownie.datastructures', 'couchapp.autopush.pathtools', 'couchapp.autopush.watchdog', 'couchapp.autopush.watchdog.observers', 'couchapp.autopush.watchdog.tricks', 'couchapp.autopush.watchdog.utils', 'couchapp.hooks', 'couchapp.hooks.compress', 'couchapp.restkit', 'couchapp.restkit.manager', 'couchapp.restkit.contrib', 'couchapp.simplejson', 'couchapp.vendors', 'couchapp.vendors.backends', ] CLASSIFIERS = ['License :: OSI Approved :: Apache Software License', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Development Status :: 4 - Beta', 'Programming Language :: Python', 'Operating System :: OS Independent', 'Topic :: Database', 'Topic :: Utilities', ] def get_scripts(): scripts = [os.path.join("resources", "scripts", "couchapp")] if os.name == "nt": scripts.append(os.path.join("resources", "scripts", "couchapp.bat")) return scripts DATA_FILES = get_data_files() def get_py2exe_datafiles(): datapath = os.path.join('couchapp', 'templates') head, tail = os.path.split(datapath) d = dict(get_data_files()) for root, dirs, files in os.walk(datapath): files = [os.path.join(root, filename) for filename in files] root = root.replace(tail, datapath) root = root[root.index(datapath):] d[root] = files return d.items() if os.name == "nt" or sys.platform == "win32": # py2exe needs to be installed to work try: import py2exe # Help py2exe to find win32com.shell try: import modulefinder import win32com for p in win32com.__path__[1:]: # Take the path to win32comext modulefinder.AddPackagePath("win32com", p) pn = "win32com.shell" __import__(pn) m = sys.modules[pn] for p in m.__path__[1:]: modulefinder.AddPackagePath(pn, p) except ImportError: raise SystemExit('You need pywin32 installed ' + 'http://sourceforge.net/projects/pywin32') # If run without args, build executables, in quiet mode. if len(sys.argv) == 1: sys.argv.append("py2exe") sys.argv.append("-q") extra['console'] = [{'script': os.path.join("resources", "scripts", "couchapp"), 'copyright': 'Copyright (C) 2008-2011 Benoît Chesneau and others', 'product_version': couchapp.__version__ }] except ImportError: raise SystemExit('You need py2exe installed to run Couchapp.') DATA_FILES = get_py2exe_datafiles() def main(): # read long description with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: long_description = f.read() INSTALL_REQUIRES = ['restkit==4.2.2', 'watchdog==0.6.0'] try: import json except ImportError: INSTALL_REQUIRES.append('simplejson') options = dict(name='Couchapp', version=couchapp.__version__, url='http://github.com/couchapp/couchapp/tree/master', license='Apache License 2', author='Benoit Chesneau', author_email='benoitc@e-engura.org', description='Standalone CouchDB Application Development Made Simple.', long_description=long_description, tests_require = ['unittest2', 'nose', 'coverage', 'nose-testconfig'], test_suite="tests", keywords='couchdb couchapp', platforms=['any'], classifiers=CLASSIFIERS, packages=find_packages(), data_files=DATA_FILES, include_package_data=True, zip_safe=False, install_requires=INSTALL_REQUIRES, scripts=get_scripts(), options=dict(py2exe={'dll_excludes': ["kernelbase.dll", "powrprof.dll"], 'packages': ["http_parser", "restkit", "restkit.manager", "restkit.contrib", "pathtools.path", "brownie", "brownie.datastructures", "watchdog", "watchdog.observers", "watchdog.tricks", "watchdog.utils", "win32pdh", "win32pdhutil", "win32api", "win32con", "subprocess" ] }, bdist_mpkg=dict(zipdist=True, license='LICENSE', readme='resources/macosx/Readme.html', welcome='resources/macosx/Welcome.html') ) ) options.update(extra) setup(**options) if __name__ == "__main__": main() tests/0000755000372000037200000000000012276056621010301 5ustar gfagfatests/config.sample.ini0000644000372000037200000000014012276056621013522 0ustar gfagfa[host] ;url = http://127.0.0.1:5984/ url = https://bigbluehat:brother7@bigbluehat.cloudant.com/ tests/test_cli.py0000644000372000037200000002351312276056621012465 0ustar gfagfa#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2009 Benoit Chesneau # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. import os import tempfile import shutil import sys import unittest2 as unittest from testconfig import config from couchapp.errors import ResourceNotFound from couchapp.client import Database from couchapp.util import popen3, deltree couchapp_dir = os.path.join(os.path.dirname(__file__), '../') couchapp_cli = os.path.join(os.path.dirname(__file__), '../bin/couchapp') try: url = config['host']['url'] except KeyError: url = 'http://127.0.0.1:5984/' def _tempdir(): f, fname = tempfile.mkstemp() os.unlink(fname) return fname class CliTestCase(unittest.TestCase): def setUp(self): self.db = Database(url + 'couchapp-test', create=True) self.tempdir = _tempdir() os.makedirs(self.tempdir) self.app_dir = os.path.join(self.tempdir, "my-app") self.cmd = "cd %s && couchapp" % self.tempdir self.startdir = os.getcwd() def tearDown(self): self.db.delete() deltree(self.tempdir) os.chdir(self.startdir) def _make_testapp(self): testapp_path = os.path.join(os.path.dirname(__file__), 'testapp') shutil.copytree(testapp_path, self.app_dir) def _retrieve_ddoc(self): # any design doc created ? design_doc = None try: design_doc = self.db.open_doc('_design/my-app') except ResourceNotFound: pass self.assertIsNotNone(design_doc) return design_doc def testGenerate(self): os.chdir(self.tempdir) (child_stdin, child_stdout, child_stderr) = popen3("%s generate my-app" % self.cmd) appdir = os.path.join(self.tempdir, 'my-app') self.assertTrue(os.path.isdir(appdir)) cfile = os.path.join(appdir, '.couchapprc') self.assertTrue(os.path.isfile(cfile)) self.assertTrue(os.path.isdir(os.path.join(appdir, '_attachments'))) self.assertTrue(os.path.isfile(os.path.join(appdir, '_attachments', 'index.html'))) self.assertTrue(os.path.isfile(os.path.join(self.app_dir, '_attachments', 'style', 'main.css'))) self.assertTrue(os.path.isdir(os.path.join(appdir, 'views'))) self.assertTrue(os.path.isdir(os.path.join(appdir, 'shows'))) self.assertTrue(os.path.isdir(os.path.join(appdir, 'lists'))) def testPush(self): self._make_testapp() (child_stdin, child_stdout, child_stderr) = \ popen3("%s push -v my-app %scouchapp-test" % (self.cmd, url)) design_doc = self._retrieve_ddoc() # should create view self.assertIn('function', design_doc['views']['example']['map']) # should use macros self.assertIn('stddev', design_doc['views']['example']['map']) self.assertIn('ejohn.org', design_doc['shows']['example-show']) self.assertIn('included by foo.js', design_doc['shows']['example-show']) # should create index content_type = design_doc['_attachments']['index.html']['content_type'] self.assertEqual(content_type, 'text/html') # should create manifest self.assertIn('foo/', design_doc['couchapp']['manifest']) # should push and macro the doc shows self.assertIn('Generated CouchApp Form Template', design_doc['shows']['example-show']) # should push and macro the view lists self.assertIn('Test XML Feed', design_doc['lists']['feed']) # should allow deeper includes self.assertNotIn('"helpers"', design_doc['shows']['example-show']) # deep require macros self.assertNotIn('"template"', design_doc['shows']['example-show']) self.assertIn('Resig', design_doc['shows']['example-show']) def testPushNoAtomic(self): self._make_testapp() (child_stdin, child_stdout, child_stderr) = \ popen3("%s push --no-atomic my-app %scouchapp-test" % (self.cmd, url)) design_doc = self._retrieve_ddoc() # there are 3 revisions (1 doc creation + 2 attachments) self.assertTrue(design_doc['_rev'].startswith('3-')) # should create view self.assertIn('function', design_doc['views']['example']['map']) # should use macros self.assertIn('stddev', design_doc['views']['example']['map']) self.assertIn('ejohn.org', design_doc['shows']['example-show']) # should create index content_type = design_doc['_attachments']['index.html']['content_type'] self.assertEqual(content_type, 'text/html') # should create manifest self.assertIn('foo/', design_doc['couchapp']['manifest']) # should push and macro the doc shows self.assertIn('Generated CouchApp Form Template', design_doc['shows']['example-show']) # should push and macro the view lists self.assertIn('Test XML Feed', design_doc['lists']['feed']) # should allow deeper includes self.assertNotIn('"helpers"', design_doc['shows']['example-show']) # deep require macros self.assertNotIn('"template"', design_doc['shows']['example-show']) self.assertIn('Resig', design_doc['shows']['example-show']) def testClone(self): self._make_testapp() (child_stdin, child_stdout, child_stderr) = \ popen3("%s push -v my-app %scouchapp-test" % (self.cmd, url)) design_doc = self._retrieve_ddoc() app_dir = os.path.join(self.tempdir, "couchapp-test") (child_stdin, child_stdout, child_stderr) = \ popen3("%s clone %s %s" % (self.cmd, url + "couchapp-test/_design/my-app", app_dir)) # should create .couchapprc self.assertTrue(os.path.isfile(os.path.join(app_dir, ".couchapprc"))) # should clone the views self.assertTrue(os.path.isdir(os.path.join(app_dir, "views"))) # should create foo/bar.txt file self.assertTrue(os.path.isfile(os.path.join(app_dir, 'foo/bar.txt'))) # should create lib/helpers/math.js file self.assertTrue(os.path.isfile(os.path.join(app_dir, 'lib/helpers/math.js'))) # should work when design doc is edited manually design_doc['test.txt'] = "essai" design_doc = self.db.save_doc(design_doc) deltree(app_dir) (child_stdin, child_stdout, child_stderr) = \ popen3("%s clone %s %s" % (self.cmd, url + "couchapp-test/_design/my-app", app_dir)) self.assertTrue(os.path.isfile(os.path.join(app_dir, 'test.txt'))) # should work when a view is added manually design_doc["views"]["more"] = {"map": "function(doc) { emit(null, doc); }"} design_doc = self.db.save_doc(design_doc) deltree(app_dir) (child_stdin, child_stdout, child_stderr) = \ popen3("%s clone %s %s" % (self.cmd, url + "couchapp-test/_design/my-app", app_dir)) self.assertTrue(os.path.isfile(os.path.join(app_dir, 'views/example/map.js'))) # should work without manifest del design_doc['couchapp']['manifest'] design_doc = self.db.save_doc(design_doc) deltree(app_dir) (child_stdin, child_stdout, child_stderr) = \ popen3("%s clone %s %s" % (self.cmd, url + "couchapp-test/_design/my-app", app_dir)) self.assertTrue(os.path.isfile(os.path.join(app_dir, 'views/example/map.js'))) # should create foo/bar without manifest self.assertTrue(os.path.isfile(os.path.join(app_dir, 'foo/bar'))) # should create lib/helpers.json without manifest self.assertTrue(os.path.isfile(os.path.join(app_dir, 'lib/helpers.json'))) def testPushApps(self): os.chdir(self.tempdir) docsdir = os.path.join(self.tempdir, 'docs') os.makedirs(docsdir) # create 2 apps (child_stdin, child_stdout, child_stderr) = \ popen3("%s generate docs/app1" % self.cmd) (child_stdin, child_stdout, child_stderr) = \ popen3("%s generate docs/app2" % self.cmd) (child_stdin, child_stdout, child_stderr) = \ popen3("%s pushapps docs/ %scouchapp-test" % (self.cmd, url)) alldocs = self.db.all_docs()['rows'] self.assertEqual(len(alldocs), 2) self.assertEqual('_design/app1', alldocs[0]['id']) def testPushDocs(self): os.chdir(self.tempdir) docsdir = os.path.join(self.tempdir, 'docs') os.makedirs(docsdir) # create 2 apps (child_stdin, child_stdout, child_stderr) = \ popen3("%s generate docs/app1" % self.cmd) (child_stdin, child_stdout, child_stderr) = \ popen3("%s generate docs/app2" % self.cmd) (child_stdin, child_stdout, child_stderr) = \ popen3("%s pushdocs docs/ %scouchapp-test" % (self.cmd, url)) alldocs = self.db.all_docs()['rows'] self.assertEqual(len(alldocs), 2) self.assertEqual('_design/app1', alldocs[0]['id']) if __name__ == '__main__': unittest.main() tests/test_ignores.py0000644000372000037200000000477412276056621013374 0ustar gfagfa# -*- coding: utf-8 -*- # # Copyright 2008,2009 Benoit Chesneau # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at# # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import unittest2 as unittest import tempfile import os from shutil import rmtree from couchapp.localdoc import LocalDoc as doc import json class IgnoresTests(unittest.TestCase): def setUp(self): # Create a temp dir for the tests to run in self.tmp_dir = tempfile.mkdtemp() # Define some test data self.testdata = {'CVS': True, "dontignorethisCVS": False, "ignore_me": True, "but_don't_ignore_me": False} # Create the ignores file self.ignores = ["^CVS", "ignore_me"] f = open(os.path.join(self.tmp_dir, '.couchappignore'), 'w') json.dump(self.ignores, f) f.close() # Make a UI and a doc instance for the tests self.doc = doc(self.tmp_dir) # I could write these files to the temp area, but that seems # unnecessary since the unit test doesn't interact with the file # system other than to make the .couchappignore file. #for i in self.testdata.keys(): # open(os.path.join(self.tmp_dir, i), 'w').close() def tearDown(self): # Clear up temp dir and the files it contains rmtree(self.tmp_dir) def testLoadIgnores(self): """ If the code works the doc should have a data member containing a list of the regexps to ignore, and this list should be the same as the list stored in self.ignores and used in setUp to create the .couchappignore file. """ assert self.doc.ignores == self.ignores def testIgnore(self): """ Run through the test data and check that the doc would treat it appropriately were it a file/directory the doc was uploading. Really this test checks the re module... """ for i in self.testdata.keys(): assert self.doc.check_ignore(i) == self.testdata[i] if __name__ == '__main__': unittest.main() tests/test_compress.py0000644000372000037200000000420712276056621013550 0ustar gfagfa#!/usr/bin/env python # -*- coding: utf-8 -*- # # This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. import unittest2 as unittest import mock import os class CompressTest(unittest.TestCase): def test_compress_js(self): from couchapp.config import Config config = Config() config.conf['compress'] = {'js': {'foo':['shows/example-show.js']}} with mock.patch('couchapp.hooks.compress.default.compress', return_value='foo') as mock_compress: from couchapp.hooks.compress import Compress compress = Compress(os.path.join(os.path.dirname(__file__), 'testapp')) compress.conf = config with mock.patch('couchapp.util.write'): compress.run() self.assertTrue(mock_compress.called, 'Default compressor has been called') def test_our_jsmin_loading(self): orig_import = __import__ def import_mock(name, *args): if name == 'jsmin': raise ImportError() return orig_import(name, *args) with mock.patch('__builtin__.__import__', side_effect=import_mock): with mock.patch('couchapp.hooks.compress.jsmin.jsmin', return_value='foo'): from couchapp.hooks.compress import default result = default.compress('bar') self.assertEqual(result, 'foo', 'Our module is called when it is not installed in the system') def test_system_jsmin_loading(self): orig_import = __import__ def import_mock(name, *args): if name == 'couchapp.hooks.compress.jsmin': raise ImportError() return orig_import(name, *args) with mock.patch('__builtin__.__import__', side_effect=import_mock): with mock.patch('jsmin.jsmin', return_value='foo'): from couchapp.hooks.compress import default result = default.compress('bar') self.assertEqual(result, 'foo', 'The system module is called when it is installed') if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() tests/__init__.py0000644000372000037200000000007012276056621012407 0ustar gfagfaimport test_cli import test_ignores import test_compresstests/testapp/0000755000372000037200000000000012276056621011761 5ustar gfagfatests/testapp/foo/0000755000372000037200000000000012276056621012544 5ustar gfagfatests/testapp/foo/bar.txt0000644000372000037200000000166512276056621014061 0ustar gfagfaCouchapp will create a field on your document corresponding to any directories you make within the application directory, with the text of any files found as key/value pairs. Also, any files that end in .json will be treated as json rather than text, and put in the corresponding field. Also note that file.json, file.js, or file.txt will be stored under the "file" key, so don't make collisions in the filesystem unless you want unpredictable results. Of course you know that the views, shows, and lists directories will be treated specially, as those files are processed with the !code and !json macros. ps: each design document only has one language key: CouchDB defaults to Javascript, so that's what you'll get if you don't express otherwise. The way to switch to a different langauge is to edit the file in the approot called "language", changing "javascript" for instance into "python". Oh yeah it's recommended that you delete this file.tests/testapp/.couchapprc0000644000372000037200000000000312276056621014102 0ustar gfagfa{} tests/testapp/views/0000755000372000037200000000000012276056621013116 5ustar gfagfatests/testapp/views/example/0000755000372000037200000000000012276056621014551 5ustar gfagfatests/testapp/views/example/map.js0000644000372000037200000000031412276056621015662 0ustar gfagfa// an example map function, emits the doc id // and the list of keys it contains // !code lib/helpers/math.js function(doc) { var k, keys = [] for (k in doc) keys.push(k); emit(doc._id, keys); }; tests/testapp/views/example/reduce.js0000644000372000037200000000031112276056621016351 0ustar gfagfa// example reduce function to count the // number of rows in a given key range. function(keys, values, rereduce) { if (rereduce) { return sum(values); } else { return values.length; } };tests/testapp/views/wrong.js0000644000372000037200000000000012276056621014576 0ustar gfagfatests/testapp/lib/0000755000372000037200000000000012276056621012527 5ustar gfagfatests/testapp/lib/helpers/0000755000372000037200000000000012276056621014171 5ustar gfagfatests/testapp/lib/helpers/foo_rec.js0000644000372000037200000000007612276056621016146 0ustar gfagfa// library included by foo.js applying !code macro recursivelytests/testapp/lib/helpers/foo.js0000644000372000037200000000010012276056621015301 0ustar gfagfa// apply this macro recursiveliy // !code lib/helpers/foo_rec.jstests/testapp/lib/helpers/template.js0000644000372000037200000000210112276056621016334 0ustar gfagfa// Simple JavaScript Templating // John Resig - http://ejohn.org/ - MIT Licensed var cache = {}; function template(str, data){ // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. var fn = cache[str] || // Generate a reusable function that will serve as a template // generator (and which will be cached). new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + // Convert the template into pure JavaScript str .replace(/[\r\t\n]/g, " ") .replace(/'(?=[^%]*%>)/g,"\t") .split("'").join("\\'") .split("\t").join("'") .replace(/<%=(.+?)%>/g, "',$1,'") .split("<%").join("');") .split("%>").join("p.push('") + "');}return p.join('');"); cache[str] = fn; // Provide some basic currying to the user return data ? fn( data ) : fn; };tests/testapp/lib/helpers/math.js0000644000372000037200000000011012276056621015450 0ustar gfagfa// this is just a placeholder for example purposes function stddev() {};tests/testapp/lib/templates/0000755000372000037200000000000012276056621014525 5ustar gfagfatests/testapp/lib/templates/example.html0000644000372000037200000000145512276056621017053 0ustar gfagfa Generated CouchApp Form Template

<% doc.title %>

tests/testapp/lists/0000755000372000037200000000000012276056621013117 5ustar gfagfatests/testapp/lists/feed.js0000644000372000037200000000153612276056621014365 0ustar gfagfafunction(head, row, req) { respondWith(req, { html : function() { if (head) { return '

Listing

total rows: '+head.row_count+'
    '; } else if (row) { return '\n
  • Id:' + row.id + '
  • '; } else { return '
'; } }, xml : function() { if (head) { return {body:'' +'Test XML Feed'}; } else if (row) { // Becase Safari can't stand to see that dastardly // E4X outside of a string. Outside of tests you // can just use E4X literals. var entry = new XML(''); entry.id = row.id; entry.title = row.key; entry.content = row.value; return {body:entry}; } else { return {body : ""}; } } }) };tests/testapp/_attachments/0000755000372000037200000000000012276056621014433 5ustar gfagfatests/testapp/_attachments/index.html0000644000372000037200000000161212276056621016430 0ustar gfagfa Generated CouchApp

Generated CouchApp

    tests/testapp/_attachments/style/0000755000372000037200000000000012276056621015573 5ustar gfagfatests/testapp/_attachments/style/main.css0000644000372000037200000000002512276056621017226 0ustar gfagfa/* add styles here */tests/testapp/shows/0000755000372000037200000000000012276056621013124 5ustar gfagfatests/testapp/shows/example-show.js0000644000372000037200000000054512276056621016077 0ustar gfagfafunction(doc, req) { // !code lib/helpers/template.js // !code lib/helpers/foo.js // !json lib.templates respondWith(req, { html : function() { var html = template(lib.templates.example, doc); return {body:html} }, xml : function() { return { body : } } }) };tests/testapp/_docs/0000755000372000037200000000000012276056621013050 5ustar gfagfatests/testapp/_docs/test.json0000644000372000037200000000005112276056621014716 0ustar gfagfa{ "_id": "test_doc", "test": "ing" } tests/testapp/_docs/.hideme/0000755000372000037200000000000012276056621014361 5ustar gfagfatests/testapp/_docs/.hideme/keep0000644000372000037200000000000012276056621015216 0ustar gfagfaTHANKS0000644000372000037200000000045012276056621010051 0ustar gfagfaCouchApp THANKS ===================== A number of people have contributed to CouchApp by reporting problems, suggesting improvements, submitting changes or asking hard question Some of these people are: * Andy Wenk * Simon Metson @drsm79 * Jason Davies @jasondavies