pax_global_header00006660000000000000000000000064127627760230014526gustar00rootroot0000000000000052 comment=92c552977e7e7102a1297c718701fde942810a88 .gitignore000066400000000000000000000005121276277602300130600ustar00rootroot00000000000000venv *.egg *.gem *.swp *.pyc *.so *.pyo Couchapp.egg-info couchapp.egg-info build dist setuptools-* .svn/* .DS_Store .project .settings/ *.so distribute-0.6.8-py2.6.egg distribute-0.6.8.tar.gz debian/python-couchapp debian/couchapp couchapp/autopush/select_backport.so .coverage cover/* tests/config.ini *~ *# docs/_build/ .eggs/ .travis.yml000066400000000000000000000003661276277602300132100ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" services: - couchdb sudo: false before_script: - pip install coveralls unittest2 nose-testconfig mock script: - python setup.py install - python setup.py nosetests after_success: - coveralls Couchapp.py000066400000000000000000000003171276277602300132070ustar00rootroot00000000000000# -*- 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() LICENSE000066400000000000000000000227721276277602300121110ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONSMANIFEST.in000066400000000000000000000003001276277602300126210ustar00rootroot00000000000000include LICENSE include NOTICE include README.rst include THANKS recursive-include couchapp/templates * recursive-include tests/testapp * recursive-include resources * recursive-include doc * NOTICE000066400000000000000000000202111276277602300117720ustar00rootroot00000000000000Couchapp -------- 2008,2010 (c) Benoît Chesneau 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. README.rst000066400000000000000000000101141276277602300125560ustar00rootroot00000000000000CouchApp: Standalone CouchDB Application Development Made Simple ================================================================ .. image:: https://img.shields.io/travis/couchapp/couchapp/master.png?style=flat-square :target: https://travis-ci.org/couchapp/couchapp .. image:: https://img.shields.io/coveralls/couchapp/couchapp/master.png?style=flat-square :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 `_. .. contents:: 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.6 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://bootstrap.pypa.io/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 before each command line. Note: On debian system don't forget to install python-dev. To install on Windows follow instructions `here `_. More installation options on the `website `_. Getting started --------------- Read the `tutorial `_. Documentation ------------- It's available at https://couchapp.readthedocs.org/en/latest Testing ------- We use `nose `_. and `nose-testconfig `_. for setting up and running tests. :: $ python setup.py nosetests Config ++++++ Our ``nosetests`` will run with options listed in ``setup.cfg``. In the ``tests`` directory, copy ``config.sample.ini`` to ``config.ini``, tweak the settings, and then modify your ``setup.cfg``:: [nosetests] ... tc-file=tests/config.ini Coverage ++++++++ If you're wanting to examine code coverage reports (because you've got big plans to make our tests better!), you can browse around the ``cover`` dir :: $ cd cover $ python2 -m SimpleHTTPServer or (if you prefer python3):: $ python3 -m http.server Debug +++++ If you want to debug the failed run with ``pdb``, add the following option to ``setup.cfg``:: [nosetests] ... pdb=1 Thanks for testing ``couchapp``! Building the docs ----------------- We generate the document via ``sphinx``. First, prepare our building env. We need ``sphinx``:: $ cd docs/ $ pip install sphinx To build it, just issue:: $ make html And sphinx will generate static html at *docs/_build/html*. We can browse the site from this dir already. Other resources --------------- * `List of CouchApps `_ THANKS000066400000000000000000000004501276277602300120040ustar00rootroot00000000000000CouchApp 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 couchapp/000077500000000000000000000000001276277602300126745ustar00rootroot00000000000000couchapp/__init__.py000066400000000000000000000003171276277602300150060ustar00rootroot00000000000000# -*- 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, 2) __version__ = ".".join(map(str, version_info)) couchapp/autopush/000077500000000000000000000000001276277602300145445ustar00rootroot00000000000000couchapp/autopush/__init__.py000066400000000000000000000002731276277602300166570ustar00rootroot00000000000000# -*- 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/command.py000066400000000000000000000023621276277602300165370ustar00rootroot00000000000000# -*- 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/handler.py000066400000000000000000000031661276277602300165410ustar00rootroot00000000000000# -*- 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/watcher.py000066400000000000000000000060731276277602300165610ustar00rootroot00000000000000# -*- 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/autopush/winwatcher.py000066400000000000000000000023761276277602300173010ustar00rootroot00000000000000# -*- 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/client.py000066400000000000000000000415131276277602300145300ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000171511276277602300152130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import base64 import copy import logging import os from hashlib import md5 from couchapp import client, util from couchapp.errors import AppError logger = logging.getLogger(__name__) def clone(source, dest=None, rev=None): """ Clone an application from a design_doc given. :param source: the http/https uri of design document """ try: dburl, docid = source.split('_design/') except ValueError: raise AppError("{0} isn't a valid source".format(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', 'updates'): 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 = os.path.normpath(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/commands.py000066400000000000000000000330541276277602300150540ustar00rootroot00000000000000# -*- 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 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, create=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'), doc) else: print doc.to_json() 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)) appdirs = util.discover_apps(source) logger.debug('Discovered apps: {0}'.format(appdirs)) for appdir in appdirs: 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 not apps: return 0 if export: docs = [doc.doc() for doc in apps] jsonobj = {'docs': docs} if opts.get('output'): util.write_json(opts.get('output'), jsonobj) else: print util.json.dumps(jsonobj) return 0 for db in dbs: docs = [doc.doc(db) for doc in apps] try: db.save_docs(docs) except BulkSaveError as 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'): util.write_json(opts.get('output'), 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): dest = args[0] if len(args) > 0 else 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], name)) if util.iscouchapp(dest): raise AppError("can't create an app at '%s'. " "One already exists here.".format(dest)) if util.findcouchapp(dest): raise AppError("can't create an app inside another app '{0}'.".format( util.findcouchapp(dest))) generator.generate(dest, "startapp", name, **opts) return 0 def generate(conf, path, *args, **opts): ''' :param path: result of util.findcouchapp ''' 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.") elif dest and kind == 'app': raise AppError("can't create an app inside another app '{0}'.".format( dest)) 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): if len(args) == 0: dest = path doc_path = '.' else: doc_path = path dest = args[0] doc_path = os.path.normpath(os.path.join(os.getcwd(), doc_path)) if not util.iscouchapp(doc_path): raise AppError("Dir '{0}' is not a couchapp.".format(doc_path)) 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/config.py000066400000000000000000000116541276277602300145220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import re import os from copy import deepcopy 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={}, vendors=[] ) 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 :type path: str or iterable """ conf = deepcopy(default) if default is not None else {} paths = [path] if isinstance(path, basestring) else path for p in paths: if not os.path.isfile(p): continue try: new_conf = util.read_json(p, use_environment=True, raise_on_error=True) except ValueError: raise AppError("Error while reading '{0}'".format(p)) conf.update(new_conf) return conf def load_local(self, app_path): """ Load local config from app/couchapp.json and app/.couchapprc. If both of them contain same vars, the latter one will win. """ if not app_path: raise AppError("You aren't in a couchapp.") fnames = ('couchapp.json', '.couchapprc') paths = (os.path.join(app_path, fname) for fname in fnames) return self.load(paths) def update(self, path): ''' Given a couchapp path, and load the configs from it. ''' 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.get(key, default) 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): ''' We will get the key-value pair from the dict: self.conf ''' for k, v in self.conf.items(): yield (k, v) @property def extensions(self): ''' load extensions from conf :return: list of extension modules ''' return [util.load_py(uri, self) for uri in self.conf.get('extensions', tuple())] @property def hooks(self): return dict( (hooktype, [util.hook_uri(uri, self) for uri in uris]) for hooktype, uris in self.conf.get('hooks', {}).items() ) # TODO: add oauth management def get_dbs(self, db_string=None): ''' :type db_string: str ''' db_string = db_string or '' env = self.conf.get('env', {}) is_full_uri = any(map(db_string.startswith, ('http://', 'https://', 'desktopcouch://'))) if not db_string and 'default' not in env: raise AppError("database isn't specified") elif not db_string and 'default' in env: dburls = env['default']['db'] elif is_full_uri: dburls = db_string elif not is_full_uri: conf_uri = env.get(db_string, {}).get('db') default_uri = '{0}/{1}'.format(self.DEFAULT_SERVER_URI, db_string) dburls = conf_uri if conf_uri else default_uri del conf_uri, default_uri if isinstance(dburls, basestring): dburls = [dburls] use_proxy = any(k in os.environ for k in ('http_proxy', 'https_proxy')) return [Database(dburl, use_proxy=use_proxy) for dburl in dburls] def get_app_name(self, dbstring=None, default=None): dbstring = dbstring or '' env = self.conf.get('env', {}) is_full_uri = re.match('(https?|desktopcouch)://', dbstring) if not is_full_uri and dbstring in env: return env[dbstring].get('name', default) elif not is_full_uri and 'default' in env: return env['default'].get('name', default) return default couchapp/dispatch.py000066400000000000000000000104701276277602300150470ustar00rootroot00000000000000# -*- 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__) 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 as e: logger.error("couchapp error: %s" % str(e)) except CommandLineError as e: logger.error("command line error: {0}".format(e)) except KeyboardInterrupt: logger.info("keyboard interrupt") except Exception as 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 not in commands.table: raise CommandLineError('Unknown command: "{0}"'.format(cmd)) 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 as 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 as 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/errors.py000066400000000000000000000024171276277602300145660ustar00rootroot00000000000000# -*- 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/generator.py000066400000000000000000000173521276277602300152440ustar00rootroot00000000000000# -*- 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 as 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/hooks/000077500000000000000000000000001276277602300140175ustar00rootroot00000000000000couchapp/hooks/__init__.py000066400000000000000000000000001276277602300161160ustar00rootroot00000000000000couchapp/hooks/compress/000077500000000000000000000000001276277602300156525ustar00rootroot00000000000000couchapp/hooks/compress/__init__.py000066400000000000000000000075271276277602300177760ustar00rootroot00000000000000# -*- 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/compress_css.py000066400000000000000000000054471276277602300207410ustar00rootroot00000000000000# -*- 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/compress/default.py000066400000000000000000000005211276277602300176460ustar00rootroot00000000000000# -*- 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/jsmin.py000066400000000000000000000200141276277602300173410ustar00rootroot00000000000000# 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/yuicompressor.py000066400000000000000000000012711276277602300211500ustar00rootroot00000000000000# -*- 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/localdoc.py000066400000000000000000000410251276277602300150300ustar00rootroot00000000000000# -*- 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 to_json(self): return self.__str__() def document(path, create=False, docid=None, is_ddoc=True): return LocalDoc(path, create=create, docid=docid, is_ddoc=is_ddoc) couchapp/macros.py000066400000000000000000000107771276277602300145460ustar00rootroot00000000000000# -*- 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/templates/000077500000000000000000000000001276277602300146725ustar00rootroot00000000000000couchapp/templates/app/000077500000000000000000000000001276277602300154525ustar00rootroot00000000000000couchapp/templates/app/README.md000066400000000000000000000012471276277602300167350ustar00rootroot00000000000000## 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/_attachments/000077500000000000000000000000001276277602300201245ustar00rootroot00000000000000couchapp/templates/app/_attachments/index.html000066400000000000000000000044031276277602300221220ustar00rootroot00000000000000 Example CouchApp

Example CouchApp

couchapp/templates/app/_attachments/script/000077500000000000000000000000001276277602300214305ustar00rootroot00000000000000couchapp/templates/app/_attachments/script/app.js000066400000000000000000000043411276277602300225500ustar00rootroot00000000000000// 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/style/000077500000000000000000000000001276277602300212645ustar00rootroot00000000000000couchapp/templates/app/_attachments/style/main.css000066400000000000000000000015421276277602300227240ustar00rootroot00000000000000/* 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/app/couchapp.json000066400000000000000000000001431276277602300201450ustar00rootroot00000000000000{ "name": "Basic CouchApp", "description": "CouchApp with changes feed and form support." }couchapp/templates/app/language000066400000000000000000000000121276277602300171510ustar00rootroot00000000000000javascriptcouchapp/templates/app/views/000077500000000000000000000000001276277602300166075ustar00rootroot00000000000000couchapp/templates/app/views/recent-items/000077500000000000000000000000001276277602300212065ustar00rootroot00000000000000couchapp/templates/app/views/recent-items/map.js000066400000000000000000000003711276277602300223220ustar00rootroot00000000000000function(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/functions/000077500000000000000000000000001276277602300167025ustar00rootroot00000000000000couchapp/templates/functions/filter.js000066400000000000000000000006111276277602300205230ustar00rootroot00000000000000/** * Replication filter function * @link http://docs.couchdb.org/en/latest/couchapp/ddocs.html#reduce-and-rereduce-functions * * @param {object} doc - Document Object. * @param {object} req - Request Object. http://docs.couchdb.org/en/latest/json-structure.html#request-object * * @return {boolean} True to let the document through; false to prevent it. **/ function(doc, req) { } couchapp/templates/functions/list.js000066400000000000000000000006461276277602300202210ustar00rootroot00000000000000/** * List function - use `start()` and `send()` to output headers and content. * @link http://docs.couchdb.org/en/latest/couchapp/ddocs.html#listfun * * @param {object} head - View Head Information. http://docs.couchdb.org/en/latest/json-structure.html#view-head-info-object * @param {object} req - Request Object. http://docs.couchdb.org/en/latest/json-structure.html#request-object **/ function(head, req) { } couchapp/templates/functions/map.js000066400000000000000000000003671276277602300200230ustar00rootroot00000000000000/** * Map function - use `emit(key, value)1 to generate rows in the output result. * @link http://docs.couchdb.org/en/latest/couchapp/ddocs.html#reduce-and-rereduce-functions * * @param {object} doc - Document Object. */ function(doc) { } couchapp/templates/functions/reduce.js000066400000000000000000000013001276277602300205010ustar00rootroot00000000000000/** * NOTE: * Built-in reduce functions should be preferred over writing a custom one. * Replace the contents of the file with one of these strings: * _sum * _count * _stats **/ /** * Reduce function * @link http://docs.couchdb.org/en/latest/couchapp/ddocs.html#reduce-and-rereduce-functions * * @param {(array|null)} keys – Array of pairs docid-key for related map function * result. Always null if rereduce is running (has true value). * @param {array} values – Array of map function result values. * @param {boolean} rereduce – Boolean sign of rereduce run. * * @returns {object} Reduced values **/ function(keys, values, rereduce) { if (rereduce) { } else { } } couchapp/templates/functions/show.js000066400000000000000000000007441276277602300202250ustar00rootroot00000000000000/** * Show function - use multiple `provides()` for media type-based content * negotiation. * @link http://docs.couchdb.org/en/latest/couchapp/ddocs.html#showfun * * @param {object} doc - Processed document, may be omitted. * @param {object} req - Request Object. http://docs.couchdb.org/en/latest/json-structure.html#request-object * * @returns {object} Response Object. http://docs.couchdb.org/en/latest/json-structure.html#response-object **/ function(doc, req) { } couchapp/templates/functions/spatial.js000066400000000000000000000000241276277602300206710ustar00rootroot00000000000000function(doc) { }couchapp/templates/functions/update.js000066400000000000000000000000271276277602300205210ustar00rootroot00000000000000function(doc, req) { }couchapp/templates/functions/validate_doc_update.js000066400000000000000000000006331276277602300232220ustar00rootroot00000000000000function (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/vendor/000077500000000000000000000000001276277602300161675ustar00rootroot00000000000000couchapp/templates/vendor/couchapp/000077500000000000000000000000001276277602300177715ustar00rootroot00000000000000couchapp/templates/vendor/couchapp/_attachments/000077500000000000000000000000001276277602300224435ustar00rootroot00000000000000couchapp/templates/vendor/couchapp/_attachments/jquery.couch.app.js000066400000000000000000000161321276277602300262020ustar00rootroot00000000000000// 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.couch.app.util.js000066400000000000000000000057531276277602300271650ustar00rootroot00000000000000$.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.couchForm.js000066400000000000000000000023551276277602300262510ustar00rootroot00000000000000// 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.couchLogin.js000066400000000000000000000075071276277602300264220ustar00rootroot00000000000000// 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.couchProfile.js000066400000000000000000000072371276277602300267520ustar00rootroot00000000000000// 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.mustache.js000066400000000000000000000223721276277602300261360ustar00rootroot00000000000000/* 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/vendor/couchapp/_attachments/md5.js000066400000000000000000000207201276277602300234670ustar00rootroot00000000000000/* * 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/metadata.json000066400000000000000000000001711276277602300224430ustar00rootroot00000000000000{ "name": "couchapp", "description": "official couchapp vendor", "fetch_uri": "git://github.com/couchapp/vendor.git" }couchapp/util.py000066400000000000000000000366371276277602300142420ustar00rootroot00000000000000# -*- 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. This function will take the environment var, ``COUCHAPPCONF_PATH``, as the search path list. """ global _rcpath if _rcpath is not None: return _rcpath conf_path = os.environ.get('COUCHAPPCONF_PATH') if conf_path is None: _rcpath = user_rcpath() return _rcpath _rcpath = [] for p in conf_path.split(os.pathsep): if not p: continue if not os.path.isdir(p): _rcpath.append(p) continue _rcpath.extend(os.path.join(p, f) for f in os.listdir(p) if f == 'couchapp.conf') return _rcpath def findcouchapp(p): ''' Find couchapp top level dir from sub dir ''' 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 discover_apps(path): ''' Given a path as parent dir, depth=1, return a list of the couchapps. It will ignore all hidden dir. :type path: str ''' apps = [] for item in os.listdir(path): full_path = os.path.join(path, item) if item.startswith('.'): # skip hidden file continue elif os.path.isdir(full_path) and iscouchapp(full_path): apps.append(full_path) return apps def iscouchapp(path): ''' A couchapp MUSH have ``.couchapprc`` :type path: str :return: bool ''' return os.path.isfile(os.path.join(path, '.couchapprc')) 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 elif path == os.path.realpath('/'): parts[0] = os.path.join(os.path.realpath('/'), parts[0]) 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 :type fname: string, filename :type content: str """ with open(fname, 'wb') as f: f.write(to_bytestring(content)) def write_json(fname, obj): """ serialize obj in json and save it :type fname: str :param obj: serializable builtin type, or any obj has ``to_json`` method """ try: val = json.dumps(obj).encode('utf-8') except TypeError: val = obj.to_json() write(fname, val) 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/vendors/000077500000000000000000000000001276277602300143545ustar00rootroot00000000000000couchapp/vendors/__init__.py000066400000000000000000000006651276277602300164740ustar00rootroot00000000000000# -*- 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/vendors/backends/000077500000000000000000000000001276277602300161265ustar00rootroot00000000000000couchapp/vendors/backends/__init__.py000066400000000000000000000002061276277602300202350ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. couchapp/vendors/backends/base.py000066400000000000000000000006401276277602300174120ustar00rootroot00000000000000# -*- 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/couchdb.py000066400000000000000000000022621276277602300201110ustar00rootroot00000000000000# -*- 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/backends/git.py000066400000000000000000000023271276277602300172670ustar00rootroot00000000000000# -*- 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.py000066400000000000000000000024541276277602300171030ustar00rootroot00000000000000# -*- 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/base.py000066400000000000000000000154031276277602300156430ustar00rootroot00000000000000# -*- 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 docs/000077500000000000000000000000001276277602300120225ustar00rootroot00000000000000docs/Makefile000066400000000000000000000164331276277602300134710ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." touch $(BUILDDIR)/html/.nojekyll dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/CouchApp.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/CouchApp.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/CouchApp" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/CouchApp" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." docs/_static/000077500000000000000000000000001276277602300134505ustar00rootroot00000000000000docs/_static/.PADDING000066400000000000000000000000001276277602300145050ustar00rootroot00000000000000docs/_static/imgs/000077500000000000000000000000001276277602300144075ustar00rootroot00000000000000docs/_static/imgs/gettingstarted01.png000066400000000000000000001531131276277602300203120ustar00rootroot00000000000000PNG  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`docs/_static/imgs/gettingstarted02.png000066400000000000000000001247061276277602300203210ustar00rootroot00000000000000PNG  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`docs/_static/imgs/gettingstarted03.png000066400000000000000000001665121276277602300203230ustar00rootroot00000000000000PNG  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ȨȔȼ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`docs/_templates/000077500000000000000000000000001276277602300141575ustar00rootroot00000000000000docs/_templates/.PADDING000066400000000000000000000000001276277602300152140ustar00rootroot00000000000000docs/conf.py000066400000000000000000000223311276277602300133220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # CouchApp documentation build configuration file, created by # sphinx-quickstart on Wed Aug 5 15:00:02 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'CouchApp' copyright = u'2016, Various CouchApp Contributors' author = u'Various CouchApp Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.0.1' # The full version, including alpha/beta/rc tags. release = '1.0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'CouchAppdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'CouchApp.tex', u'CouchApp Documentation', author, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'CouchApp', u'CouchApp Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'CouchApp', u'CouchApp Documentation', author, 'CouchApp', 'Utilities to make standalone CouchDB application development simple', 'Development'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False docs/couchapp/000077500000000000000000000000001276277602300136245ustar00rootroot00000000000000docs/couchapp/config.rst000066400000000000000000000070741276277602300156330ustar00rootroot00000000000000.. _couchapp-config: Configuration ============= .. hightlight:: javascript ``.couchapprc`` --------------- Every CouchApp **MUST** have a ``.couchapprc`` file in the application directory. This file is a JSON object which contains configuration parameters that the command-line app uses to build and push your CouchApp. The ``couchapp generate`` and ``couchapp init`` commands create a default version of this file for you. The most common use for the ``.couchapprc`` file is to specify one or more CouchDB databases to use as the destination for the ``couchapp push`` command. Destination databases are listed under the ``env`` key of the ``.couchapprc`` file as follows: :: { "env" : { "default" : { "db" : "http://localhost:5984/mydb" }, "prod" : { "db" : "http://admin:password@myhost.com/mydb" } } } In this example, two environments are specified: ``default``, which pushes to a local CouchDB instance without any authentication, and ``prod``, which pushes to a remote CouchDB that requires authentication. Once these sections are defined in ``.couchapprc``, you can push to your local CouchDB by running ``couchapp push`` (the environment name ``default`` is used when no environment is specified) and push to the remote machine using ``couchapp push prod``. For a more complete discussion of the ``env`` section of the ``.couchapprc`` file, see the `Managing Design Documents `__ chapter of **CouchDB: The Definitive Guide**. The ``.couchapprc`` file is also used to configure extensions to the ``couchapp`` tool. See the :ref:`couchapp-extend` page for more details. ``~/.couchapp.conf`` -------------------- One drawback to declaring environments in the ``.couchapprc`` file is that any usernames and passwords required to push documents are stored in that file. If you are using source control for your CouchApp, then those authentication credentials are checked in to your (possibly public) source control server. To avoid this problem, the ``couchapp`` tool can also read environment configurations from a file stored in your home directory named ``.couchapp.conf``. This file has the same syntax as ``.couchapprc`` but has the advantage of being outside of the source tree, so sensitive login information can be protected. If you already have a working ``.couchapprc`` file, simply move it to ``~/.couchapp.conf`` and run ``couchapp init`` to generate a new, empty ``.couchapprc`` file inside your CouchApp directory. If you don't have a ``.couchapprc`` file, ``couchapp`` will display the dreaded ``couchapp error: You aren't in a couchapp`` message. ``~/.couchapp`` --------------- Please see :ref:`couchapp-template` ``.couchappignore`` ------------------- A ``.couchappignore`` file specifies intentionally untracked files that couchapp should ignore. It's a simple json file containing an array of regexps that will be use to ignore file. For example: :: [ ".*\\.swp$", ".*~$" ] will ignore all files ending in ``.swp`` and ``~``. Be sure to leave out the final , in the list. You can check if couchapp really ignores the files by specifying the -v option:: couchapp -v push .. note:: Windows doesn't like files that only have an extension, so creating the ``.couchappignore`` file will be a challenge in windows. Possible solutions to creating this file are: Using cygwin, type: touch .couchappignore cd /to/couchappand then notepad .couchappignore TODO: more information about other templates like vendor, view, etc. docs/couchapp/extends.rst000066400000000000000000000125111276277602300160300ustar00rootroot00000000000000.. _couchapp-extend: Extend couchapp =============== .. toctree:: .. highlight:: python 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, e.g.: .. code-block :: javascript "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 `__. 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, e.g.: .. code-block :: javascript "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 `__ 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: .. code-block :: javascript { "vendors": [ "egg:couchapp#git", "egg:couchapp#hg", "egg:couchapp#couchdb" ] } (above is the default). Entry point are added to ``couchapp.vendor`` distribution, e.g.:: 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): ... :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. (e.g.: ['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 `__. docs/couchapp/gettingstarted.rst000066400000000000000000000045211276277602300174100ustar00rootroot00000000000000.. _couchapp-tutorial: Getting Started =============== .. toctree:: .. highlight:: shell In this tutorial you will learn how to create your first CouchApp (embedded applications in CouchDB_) using the ``couchapp`` script. Generate your application ------------------------- couchapp provides you the ``generate`` command to initialize your first CouchApp. It will create an application skeleton by generating needed folders and files to start. Run:: $ couchapp generate helloworld .. figure:: ../_static/imgs/gettingstarted01.png :alt: couchapp generate helloworld :: $ couchapp generate Create a show function ---------------------- To display our hello we will create a show function. :: $ cd helloworld/ $ couchapp generate show hello Here the generate command create a file named ``hello.js`` in the folder shows. The content of this file is: .. code-block :: javascript function(doc, req) { } which is default template for ``show`` functions. For now we only want to display the string "Hello World". Edit your show function like this: .. code-block :: javascript function(doc, req) { return "Hello World"; } Push your CouchApp ------------------ Now that we have created our basic application, it's time to ``push`` it to our CouchDB server. Our CouchDB server is at the url http://127.0.0.1:5984 and we want to push our app in the database testdb:: $ couchapp push testsb .. figure:: ../_static/imgs/gettingstarted02.png :alt: couchapp push testdb :: $ couchapp push Go on http://127.0.0.1:5984/testdb/_design/helloworld/index.html, you will see: .. figure:: ../_static/imgs/gettingstarted03.png :alt: CouchApp hello world :: CouchApp hello world Clone your CouchApp ------------------- So your friend just pushed the helloworld app from his computer. But you want to edit the CouchApp on your own computer. That's easy, just ``clone`` his application:: $ couchapp clone http://127.0.0.1:5984/testdb/_design/helloworld helloworld This command fetch the CouchApp ``helloworld`` from the remote database of your friend. .. figure:: ../_static/imgs/gettingstarted04.png :alt: couchapp clone http://127.0.0.1:5984/testdb/_design/helloworld helloworld :: $ couchapp clone Now you can edit the couchapp on your own computer. .. _CouchDB: http://couchdb.apache.org docs/couchapp/index.rst000066400000000000000000000007721276277602300154730ustar00rootroot00000000000000CouchApp Command Line Tool ========================== 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. .. toctree:: :maxdepth: 2 :numbered: 1 install gettingstarted usage config template extends multiple-design-docs docs/couchapp/install.rst000066400000000000000000000072541276277602300160340ustar00rootroot00000000000000.. _install: Installation ============ The newest install instructions are always in the `README `__ In case the below is not updated, check out the `release section `_ in GitHub. Requirements ------------ - Python 2.x >= 2.6 (Python 3.x will be supported soon) - the header files of the Python version that is used, which are included e.g. in the according development package ``python-dev`` (may have a different name depending on your system) Installing on all UNIXs ----------------------- To install couchapp using ``easy_install`` you must make sure you have a recent version of distribute installed: :: $ curl -O http://python-distribute.org/distribute_setup.py $ sudo python distribute_setup.py $ sudo easy_install pip To install or upgrade to the latest released version of couchapp: :: $ sudo pip install couchapp $ sudo pip install --upgrade couchapp To install/upgrade development version: :: $ sudo pip install git+http://github.com/couchapp/couchapp.git#egg=Couchapp Installing in a sandboxed environnement --------------------------------------- If you want to work in a sandboxed environnement which is recommended if you don't want to not *pollute* your system, you can use `virtualenv `_ : :: $ curl -O http://python-distribute.org/distribute_setup.py $ sudo python distribute_setup.py $ easy_install pip $ pip install virtualenv Then to install couchapp : :: $ pip -E couchapp_env install couchapp This command create a sandboxed environment in ``couchapp_env`` folder. To activate and work in this environment: :: $ cd couchapp_env && . ./bin/activate Then you can work on your couchapps. I usually have a ``couchapps`` folder in ``couchapp_env`` where I put my couchapps. Installing on Mac OS X ---------------------- .. warning:: This section is out-of-date. We need you help for testing on newer OSX with newer ``couchapp.py`` Using CouchApp Standalone executable ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Download `couchapp-1.0.0-macosx.zip `_ on `Github `_ then double-click on the installer. Using Homebrew ~~~~~~~~~~~~~~ To install easily couchapp on Mac OS X, it may be easier to use `Homebrew `_ to install ``pip``. Once you `installed Homebrew `_, do: :: $ brew install pip $ env ARCHFLAGS="-arch i386 -arch x86_64" pip install couchapp Installing on Ubuntu -------------------- .. warning:: Our PPA is out-of-date. We need your help for upgrading the packages. If you use `Ubuntu `_, you can update your system with packages from our PPA by adding ``ppa:couchapp/couchapp`` to your system's Software Sources. Follow **instructions** `here `_. Installing on Windows --------------------- There are currently 2 methods to install on windows: - `Standalone Executable 1.0.1 `_ Does not require Python - `Python installer for Python 2.7 `_ Requires Python Previous Release ---------------- Please check out both `release section `_ and `download section `_ in GitHub. Note that the download section in GitHub is `deprecated `_. docs/couchapp/multiple-design-docs.rst000066400000000000000000000064231276277602300204130ustar00rootroot00000000000000.. _multiple-design-docs: Using CouchApp with Multiple Design Documents ============================================= .. highlight:: shell Here is what I did to use couchapp with multiple design documents. I want to setup a project for a new database ``test6`` with a design doc called ``design_doc1``. Make sure couchdb and couchapp are installed and that couchdb is started. First check that ``test6`` doesn't exist:: $ curl http://127.0.0.1:5984/test6 {"error":"not_found","reason":"no_db_file"} OK. That was expected. Generate a new CouchApp:: $ couchapp generate test6 $ cd test6 $ ls _attachments evently language shows vendor couchapp.json _id lists updates views Now edit ``.couchapprc`` as follows so it looks like .. code-block:: javascript { "env": { "default": { "db":"http://127.0.0.1:5984/test6" } } } .. note:: It looks like couchapp doesn't pick up the default db in what follows when I do ``couchapp push`` Make a directory for design documents: :: $ mkdir _design Make a directory for ``design_doc1`` :: $ mkdir _design/design_doc1 move the design doc files created with couchapp generate to the ``design_doc1`` directory: :: $ mv _attachments evently lists shows updates vendor views ./_design/design_doc1 review the directory structure :: $ ls _design/design_doc1 _attachments evently lists shows updates vendor views Now push ``design_doc1``. Note that I have to include the url of the database as a parameter. Couchapp doesn't seem to pick up the default db when I push from the ``_design`` directory. :: http://127.0.0.1:5984/test6 is the url of the new db $ couchapp push _design/design_doc1 http://127.0.0.1:5984/test6 2010-08-23 15:47:45 [INFO] Visit your CouchApp here: http://127.0.0.1:5984/test6/_design/design_doc1/index.html Now check to see if db ``test6`` was created: :: $ curl http://127.0.0.1:5984/test6 {"db_name":"test6","doc_count":1,"doc_del_count":0,"update_seq":1,"purge_seq":0,"compact_running":false,"disk_size":106585,"instance_start_time":"1282603665650439","disk_format_version":5} Now go into a browser and take a look at the ``test6`` db :: http://127.0.0.1:5984/_utils/database.html?test6 You should see ``_design/design_doc1`` listed on the html page. That's good, it means that ``design_doc1`` was created. Take a look at ``design_doc1`` in the futon web admin. Open this URL in your browser:: http://127.0.0.1:5984/_utils/document.html?test6/_design/design_doc1 You should see a nice listing of the ``design_doc1``. Try opening the index page in your browser:: http://127.0.0.1:5984/_utils/document.html?test6/_design/design_doc1/index.html This should serve up ``index.html`` from the ``_attachments`` subdirectory ``test6/_design/design_doc1/_attachments/index.html``. Couchapp generate had created a sample view called recent-items. Try querying it:: $ curl http://127.0.0.1:5984/test6/_design/design_doc1/_view/recent-items {"total_rows":0,"offset":0,"rows":[]} That's it. Multiple design can be used to create different interfaces for users with different roles. For example, consider some data and the different ways that and admin versus a regular user interacting with it. docs/couchapp/template.rst000066400000000000000000000036411276277602300161750ustar00rootroot00000000000000.. _couchapp-template: App Template ============ Most of the time, you will use ``couchapp generate`` to create a new CouchApp with the default directory layout, example functions, and vendor directories. If you find yourself creating multiple CouchApps that always contain the same third-party or in-house files and libraries, you might consider creating a custom app template containing these files and using the ``--template`` option of the generate command to create your customized CouchApps. After creating a new couchapp, you will have a project structure that looks something like `this template project `_. The following libraries_ are included with your new CouchApp by default. ``~/.couchapp`` --------------- Custom templates are stored as subdirectories under the ``~/.couchapp/templates`` directory. The name of the subdirectory is used in the ``--template`` option to specify which template files are to be used in the ``couchapp generate`` command. The default template name is *app*, so by creating ``~/.couchapp/templates/app`` and placing files and directories under that path, you can replace almost all of the default files created by ``couchapp generate``. Libraries --------- CouchDB API `jquery.couch.js`_ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The JQuery library included with CouchDB itself for use by the Futon admin console is used to interact with couchdb. `Documentation `_ CouchApp Loader `jquery.couch.app.js`_ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A utility for loading design document classes into your Javascript application Mustache_ ~~~~~~~~~ A simple template framework .. _Mustache: https://github.com/janl/mustache.js .. _jquery.couch.app.js: https://github.com/couchapp/couchapp/tree/master/couchapp/templates/vendor/couchapp/_attachments .. _jquery.couch.js: https://github.com/apache/couchdb-jquery-couch docs/couchapp/usage.rst000066400000000000000000000076511276277602300154730ustar00rootroot00000000000000.. _couchapp-usage: Command Line Usage ================== .. toctree:: .. highlight:: shell .. warning:: There are many undocumented commands. We need your help! Full command line usage ----------------------- :: Usage: couchapp [OPTIONS] [CMD] [CMDOPTIONS] [ARGS,...] Options: -d, --debug -h, --help --version -v, --verbose -q, --quiet Commands: autopush [OPTION]... [COUCHAPPDIR] DEST --no-atomic send attachments one by one --update-delay [VAL] time between each update browse [COUCHAPPDIR] DEST 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 startapp [COUCHAPPDIR] NAME vendor [OPTION]...[-f] install|update [COUCHAPPDIR] SOURCE -f, --force force install or update version Commands -------- ``generate`` +++++++++++++ Allows you to generate a basic couchapp. It can also be used to create :ref:`template ` of functions. e.g.:: $ 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_ 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. :: $ ls somedir/ app1/ app2/ app3/ $ couchapp pushapps somedir/ http://localhost:5984/mydb ``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. .. _CouchDB: http://couchdb.apache.org ``startapp`` ++++++++++++ It's an alias of ``generate app NAME``, e.g.:: $ couchapp startapp myapp docs/design/000077500000000000000000000000001276277602300132735ustar00rootroot00000000000000docs/design/filesystem-mapping.rst000066400000000000000000000060621276277602300176460ustar00rootroot00000000000000.. _filesystem-mapping: The CouchApp Filesystem Mapping =============================== The ``couchapp`` script has a cool way of pushing files to CouchDB's design documents. The `filesystem mapping `__ is done via the `couchdbkit `__ Python library. If you have folders like: :: myapp/ views/ foobar/ map.js reduce.js It will create a design document like this: .. code-block:: javascript { "_id" : "_design/myapp", "views" : { "foobar" : { "map" : "contents of map.js", "reduce" : "contents of reduce.js" } } } This is designed to make it so you get proper syntax highlighting in your text editor. Complete Filesystem-to-Design Doc Mapping Example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: myapp/ _attachments/ images/ logo.png _docs/ sample.json doc_needing_encoding/ _id (the ID for the document as text on the first line of this file) title (same as ID, just for the title field. Repeat pattern as needed) content.html (HTML content that will be encoded when it's added to the JSON doc) lists/ xml.js rewrites.js shows/ preview.js xml.js updates/ in-place.js views/ foobar/ map.js reduce.js validate_doc_update.js The ``_attachments`` folder will turn each file into an attachment on the resulting Design Document. The attachments will be named based on their file path (ex: "image/logo.png"). The contents of the ``_docs`` folder are turned into actual JSON documents in CouchDB. The contents of the .json files will be input exactly as they are in the file. The name of the document with be either the file name or the ``_id`` field from the JSON object in that file. Folders under ``_docs`` will be turned into documents with each file in the folder being a key/value pair in the resulting JSON document. HTML and XML files (and maybe others?) will be JSON encoded before being added to the JSON document. An ``_id`` file will be used (if present) as the ID of the new document. Otherwise the folder name will become the ID. The rest of the folder structure above will become this JSON Design Document .. code-block:: javascript { "_id" : "_design/myapp", "_attachments": { "images/logo.png": { "content_type": "image/png", "revpos": 1, "digest": "md5-GDPL+eLwE7kzEDWY7X4KdQ==", "length": 886, "stub": true } }, "lists": { "xml": "function..." }, "rewrites": "function...", "shows": { "preview": "function...", "xml": "function..." } "updates": { "in-place": "function..." }, "views": { "foobar": { "map": "function...", "reduce": "function..." } }, "validate_doc_update": "function...", } docs/dev/000077500000000000000000000000001276277602300126005ustar00rootroot00000000000000docs/dev/how-to-contribute.rst000066400000000000000000000032671276277602300167330ustar00rootroot00000000000000.. _contributing: Contributing ============ This repository holds all of the code in the project. The Python ``couchapp`` script is the bulk of the repository, but the JavaScript stuff is in there to. The jquery.couch.app.js and jquery.evently.js files are both `in the vendor directory `_. If you have a commit to one of the CouchApp files (JavaScript or otherwise) please let us know on the `mailing list `_ as we don't always get the messages in our Github inbox. Also, documentation and blog posting is **very much appreciated**. Don't be afraid to tell us how CouchApp sucks. We want it to be very easy to use, so giving us a high bar to reach is important. If you prefer developing mobile apps with Titanium, @pegli maintains a module which wraps Couchbase Lite for that platform. If you've built a sync powered app and are starting to hit the point where Apache CouchDB filtered replication doesn't scale for you, you might want to check out the Couchbase Sync Gateway which uses the same sync protocol but is designed to give efficient subsets of a big data corpus to sync clients. So you can sync to CouchDB or Couchbase Lite (nee TouchDB). Or simply use rcouch a custom distribution of Apache CouchDB with a bunch of new features that offers since a while incremental view changes (indexed on the disk) and replication support using a view and allows you to replicate in an efficient manner subsets of your databases. Last thing, PouchDB is the future of browser based sync apps. It can sync with the same sync protocols but uses the built-in storage of HTML5. docs/dev/roadmap.rst000066400000000000000000000013161276277602300147560ustar00rootroot00000000000000.. _roadmap: Roadmap ======= .. warning:: The content is out of date. Developer Toolchain ------------------- ``couchapp.py`` ~~~~~~~~~~~~~~~ - upload only changed attachments (md5 on attachment stubs?) - Coverage rate improvement - Python3 support Node.js CouchApp Tools ~~~~~~~~~~~~~~~~~~~~~~ - try Mikeal's Node.js CouchApp style JavaScript Libraries -------------------- $.couch.app() ~~~~~~~~~~~~~ - Make this responsible only for loading code from the Couch, and bootstrapping the CommonJS runtime. - make $.couch.app.utils a commonjs library. - move into Apache CouchDB's ``share/www/script`` RFC: Please Comment ------------------- This please suggest anything you think is missing. docs/index.rst000066400000000000000000000030471276277602300136670ustar00rootroot00000000000000CouchApp: Web Application Hosted in Apache CouchDB ================================================== .. note:: This documentation is a work in progress. Contribution welcomed. Overview -------- We will introduce the main concepts of CouchApp here. .. toctree:: :maxdepth: 2 intro/index Getting Started --------------- Let's get started with the ``couchapp.py`` command line tools. .. toctree:: :maxdepth: 2 couchapp/install Tutorial - External Resources - The `Standalone Applications `_ and `Managing Design Documents `_ chapters of the O'Reilly CouchDB book User Guide ---------- .. toctree:: :maxdepth: 2 couchapp/index js/index user/garden user/desktopcouch user/list-of-couchapps Design ------ The following documentation tell you how ``couchapp.py`` works. .. toctree:: :maxdepth: 2 design/filesystem-mapping Contributing to CouchApp Ecosystem ---------------------------------- .. toctree:: :maxdepth: 2 How to Contribute dev/roadmap Other Resources --------------- - IRC - #couchdb - #couchapp - `Search The CouchDB Mailing List/IRC Archive `_ - Mailing Lists + http://mail-archives.apache.org/mod_mbox/couchdb-couchapp/ + http://groups.google.com/group/couchapp - `eNotes CouchApp Tutorial `_ docs/intro/000077500000000000000000000000001276277602300131555ustar00rootroot00000000000000docs/intro/dev-tools.rst000066400000000000000000000110321276277602300156200ustar00rootroot00000000000000.. _dev-tools: CouchApp Development Tools ========================== To develop a CouchApp, you will need a way to get your javascript, html and other resources onto your CouchDB instance. Typically this is done with a CouchApp command line tool that maps application assets and CouchDB views, lists, shows, etc into one or more Design Documents. - :ref:`filesystem-mapping` - ``couchapp.py`` and erica_ (mentioned below) implement a consistent filesystem-to-design-document mapping .. note:: The original CouchApp command line tools were created in 2008 / 2009 by @benoitc and @jchris. They still work, and have been feature complete for a long time. ``couchapp`` has been replaced and is compatible with the old ``couchapp`` tool. cURL ---- The simplest way to develop a couchapp would be to use ``curl`` from the command line. `CouchApp command line tool `_ ------------------------------------------------------ The `CouchApp command line tool `_ is used to generate code templates in your application and to push your changes to an instance of CouchDB, among other things. Here is how to get started with the CouchApp command line tool: - `Installing couchapp `_ - `Couchapp configuration `_ - `The couchapp command line tool `_ - `Extending the couchapp command line tool `_ - `Using couchapp with multiple design documents `_ .. note:: There can be confusion with the term *CouchApp* because it can refer to this tool, named *CouchApp*, or a general application served from CouchDB. This is probably due to the fact that the CouchApp command line tool, as known as ``couchapp.py`` , was the first full way of developing a CouchApp. node.couchapp.js_ ----------------- .. _node.couchapp.js: https://github.com/mikeal/node.couchapp.js - http://japhr.blogspot.com/2010/04/quick-intro-to-nodecouchappjs.html This is an alternative tooling to the Python couchapp utility that is instead written in Node.js. It uses a much simpler folder structure than it's Python counterpart and is a generally more minimalist/simplified way of writing couchapps. Note that you cannot use Python couchapp to push couchapps written using node.couchapp.js_ into CouchDB and vice versa. erica_ ------- .. _erica: https://github.com/benoitc/erica erica_ is an Erlang-based command line tool that is compatible with the Python and Node.js CouchApp tools. Kanso_ ------- .. _Kanso: http://kan.so/ A comprehensive, framework-agnostic build tool for CouchApps. The Kanso_ command line tool can build projects designed for node.couchapp.js, or even the Python couchapp tool, while providing many other options for building your app. These build steps and other code can be shared using the online `package repository `_. Compiling coffee-script, ``.less`` CSS templates etc. is as easy as including the relevant package. NPM for CouchApps +++++++++++++++++++ Kanso_ also lets you merge design docs together, which allows reusable components built with any of the available couchapp tools. The Kanso_ tool can help you manage dependencies and share code between projects, as well as providing a library of JavaScript modules for use with CouchDB. soca_ ------ .. _soca: https://github.com/quirkey/soca soca_ is a command line tool written in ruby for building and pushing couchapps. It is similar to the canonical couchapp python tool, with a number of key differences: - local directories do not have to map 1-1 to the design docs directory - lifecycle management & deployment hooks for easily adding or modifying the design document with ruby tools or plugins. - architected around using Sammy.js, instead of Evently, which is bundled with the python tool. Sammy.js is a Sinatra inspired browser-side RESTframework which is used by default. Unlike a traditional couchapp, a soca_ couchapp is one way - your source directory structure is actually 'compiled' into into the couchapp ``_design`` document format. Compile time plugins: - Compass - CoffeeScript - Mustache - JavaScript bundling for CouchDB and the browser Reupholster_ ------------ .. _Reupholster: http://reupholster.iriscouch.com/reupholster/_design/app/index.html Reupholster_ is geared for CouchApp beginners and simple CouchApps. What Reupholster_ does is allows you to experience writing a CouchApp as fast as possible, with very little learning curve. It just feels like you are editing a normal web project. docs/intro/index.rst000066400000000000000000000011561276277602300150210ustar00rootroot00000000000000.. _intro: Introduction ============ CouchApps are JavaScript and HTML5 applications served directly from CouchDB. If you can fit your application into those constraints, then you get CouchDB's scalability and flexibility **for free** (and deploying your app is as simple as replicating it to the production server). There are also tools for deploying browser-based apps to JSON web servers and `PouchDB `_ is the future of browser based sync apps. It can sync with the same sync protocols but uses the built-in storage of HTML5. .. toctree:: :maxdepth: 2 what-is-couchapp dev-tools docs/intro/what-is-couchapp.rst000066400000000000000000000453771276277602300171030ustar00rootroot00000000000000.. _what-is-couchapp: What is CouchApp? ================= .. note:: This article came from a blog post by @jchris. Some contents are out-of-date. We need your contribution! The Basics ---------- A CouchApp is just a JavaScript and HTML5 app that can be served *directly* to the browser from CouchDB, without any other software in the stack. There are many benefits (and some constraints) to doing it this way. The first section of this article will address these tradeoffs. In the bad old days (2008 and earlier), if you wanted to write a dynamic database-backed web application, you had to have an architecture that looked like a layer cake: :: Browser (UI and links between pages) ------------------------ HTTP ------------------------- Application server (business logic, templates, etc) ------------------- custom binary --------------------- Persistence: MySQL, PostgreSQL, Oracle In fact, the bad old days are still with us, as most applications still rely on fragile custom code, running in an application server like Ruby on Rails, Python's Django, or some kinda Java thing. The pain-points of the 3 tier architecture are well known: application developers must understand the concept of shared-nothing state, or else clients can see inconsistent results if they are load balanced across a cluster of app servers. The application server is usually a memory hog. And at the end of the day, when you've finally gotten the app layer to horizontal scalability, it turns our that your database-tier has fatal scalability flaws... CouchDB is an HTTP server, capable of serving HTML directly to the browser. It is also a database designed from the ground up for horizontal scalability. Did I say silver bullet? ;) (Of course it is not a silver bullet -- if you can't fit your app into CouchDB's constraints, you'll still have scaling issues.) If you can build your app *with the grain* of CouchDB's APIs, then you can piggyback on all the work `other `_ people have done to scale. The fact is, 2-layer applications are simpler: :: Browser (UI and links between pages) ------------------------ HTTP ------------------------- CouchDB (persistence, business logic, and templating) Because CouchDB is a web server, you can serve applications directly the browser without any middle tier. When I'm feeling punchy, I like to call the traditional application server stack "extra code to make CouchDB uglier and slower." Aside from simplicity and the scalability that comes with it, there is another major benefit to creating a 100% pure CouchApp: `Replication `_. When your app is hosted by just a CouchDB, that means it can be run from *any* CouchDB, with no need to set up complex server-side dependencies. When your app can run on *any* CouchDB, you are free to take advantage of CouchDB's killer feature: replicating the app and the data anywhere on the network. Have you ever been frustrated by a slow website? Filling out forms and waiting even a few seconds for the response can be infuriating. Many users will hit the submit button over and over again, compounding whatever performance issues that are effecting them, while introducing data integrity issues as well. Google, Facebook, and other large competitive web properies know that `perceived latency drives user engagement like nothing else, and they invest huge sums to make their sites seem faster. `__ Your site can be faster than theirs, if you serve it from localhost. CouchDB makes this possible. Here are `installers for OSX, Windows, and Linux `_ and you can install `CouchDB on Android here `_. The take-home message from this section is: CouchDB can scale. If your app is served by raw CouchDB, it can scale just the same. Also, there's no server faster than the server running on your local device. And fast is what matters for users. In the next section we'll see what it takes to get your app to be served directly from CouchDB, and what you can (and can't) do. CouchDB's built-in programming model ------------------------------------ The CouchDB API is full featured and applicable to a lot of use cases. We can't possible go in-depth here. Instead we'll focus only on the broad outline, and on what is useful and necessary for CouchApps. If you want to learn more, check out `the CouchDB wiki `_ or the `free CouchDB book `_. The first thing to understand about CouchDB is that the entire API is HTTP. Data is stored and retrieved using the protocol your browser is good at. Even `the CouchDB test suite `_ is written in JavaScript and executed from the browser. **It's all just HTTP.** HTML Attachments ~~~~~~~~~~~~~~~~ A common question I get from people starting to write Ajax apps using CouchDB, is "when I try to query the CouchDB with jQuery, it doesn't work." Usually it turns out that they have an index.html file on their filesystem, which is attempting to do an Ajax call against the CouchDB server. After I explain to them `the same origin security policy `_, they start to understand this this means CouchDB needs to serve their HTML (rather than loading it in the browser direct from the filesystem). CouchDB documents may have `binary attachments. `_ The easiest way to add an attachment to a document is via Futon. We'll do that later in this blog post. So, the simplest possible CouchApp is just an HTML file, served directly from CouchDB, that uses Ajax to load and save data from the CouchDB. Map Reduce queries ~~~~~~~~~~~~~~~~~~ What sets CouchDB apart from a simple key value store like Memcached or `Amazon S3 `_, is that you can query it by building indexes across the stored objects. You do this by writing JavaScript functions that are passed each of your documents, and can pick from them a set of keys under which you'd like to locate them. So for a blog post, you might pick out all the tags, and make keys like ``[tag, doc.created_at]``. Once you have a view like that, you can easily get a view of all your blog posts with a given tag, in chronological order, no less. By adding the reduce operator ``_count`` you can also see how many blog posts are tagged ``foo`` or whatever. I'm not gonna try to teach you all about views here. Try the `CouchDB book's guide to views `_ , the `wiki `_ and this `chapter on advanced views `_. Server Side Validations ~~~~~~~~~~~~~~~~~~~~~~~ The second thing people usually ask when they start to grok the CouchApp model, is "How do I keep people from destroying all my data? How do I ensure they only do what they are allowed to do?" The answer to that is `validation functions `__. In a nutshell, each time someone saves or updates a CouchDB document, it is passed to your validation function, which has the option to throw an error. It can either throw ``{"forbidden" : "no matter what"}`` or ``{"unauthorized" : "maybe if you login as someone else"}`` where, of course, you are free to craft your own messages. If the function doesn't have any errors, the save is allowed to proceed. Rendering Dynamic HTML ~~~~~~~~~~~~~~~~~~~~~~ After a new user understands validation functions, they have begun to see that perhaps CouchDB / CouchApps is a good candidate for their application. But maybe something is missing... Search engines don't treat Ajax applications with the same respect they do static HTML applications. Also, a fair proportion of users have JavaScript disabled, or are using a screen-reader type application, which may not understand Ajax. These are all great reasons your application should ship the basic content of a page as real-deal HTML. Luckily, CouchDB has an answer to that as well. `Show functions `__ allow you to transform a document ``GET`` from JSON into the format of your choice. On `this wiki application `__ the main wiki content is rendered as server-side HTML, using a show function. You can also use a show function to provide an XML, CSV, or even PNG version of your original document. Some folks also use it to filter security-sensitive fields from a JSON document, so that only public data is available to end- users. `List functions `__ are the analog of show functions, but for view results. A view result is just a long list of JSON rows. A list function transforms those rows to other formats. Here is `the JSON view of recent posts on my blog `__, and here is `the HTML page that results from running that same view through a list function. `__ We added these capabilities to CouchDB because we knew that without the ability to serve plain-old HTML, we wouldn't be completely RESTful. Rounding out this group, is the ability to accept plain HTML form POSTs. (And other arbitary input). For that, CouchDB uses `update functions `__, which can take arbitrary input and turn it in to JSON for saving to the database. Pretty URLs ~~~~~~~~~~~ "All well and good", you may say, "but I can't really suggest to my clients that their website should have URLs like:: http://jchrisa.net/drl/_design/sofa/_list/index/recent-posts?descending=true&limit=5 I used to respond with skepicism to such claims, *like a total moaf*. But I've mended my ways, and seen the light. It also didn't hurt that `Benoit `_ committed an `awesome rewriter `_ to CouchDB, so we can provide nice pretty URLs like ``/posts/recent`` instead of the above mess. Realtime Updates ~~~~~~~~~~~~~~~~ Lastly, something folks don't usually ask for, but which is insanely useful: `realtime notification about changes to the database. `_ Essentially, CouchDB keeps a record of the order in which operations were applied to a given database. This way, you can always ask it "what's happened since the last time I asked?" CouchDB implements this with the ``_changes`` feed, a JSON HTTP response, which sends a single line, whenever something happens to the database. Since CouchDB is implemented in Erlang, it is not expensive for it to hold open tens of thousands of concurrent connections. The ``_changes`` feed can be used to power realtime updates to a browser UI. For instance, `this chat room `_ updates in realtime whenever a new message is created. The ``_changes`` feed is integral to CouchDB itself (not just a bolted on feature), as it is used to power to replication itself. The replicator listens to the changes feed of the source database, and writes changes to the target database. This is what allows CouchDB to keep 2 database in sync in near realtime. You can also use ``_changes`` to drive asynchronous business logic. There will be a webcast in August on this topic, as well as a blog post with more details, from Couchio's `Jason Smith `_. Filtered replication ~~~~~~~~~~~~~~~~~~~~ One last part of the programming model. You can write a JavaScript function that decides whether to include a given change in the ``_changes`` feed. The possibilities are endless. See `Jan's blog post on new replication features `_ for some interesting use-cases that might stimulate your imagination. Hello World ----------- Now that I've described the theory of CouchApps to you, let's dig into the practice. Before we get into the expert toolchain, let's see what we can do with a little bit of HTML. I'll assume you have a CouchDB running at localhost. If you don't, install one now (or signup for hosting at `Iris Couch `_ or `Cloudant `_). Quick, create a file called index.html, and put this in it: .. code-block:: html Tiny CouchApp

Tiny CouchApp

    Now browse to your CouchDB's Futon at http://localhost:5984/_utils and create a database called ``whatever``. Now visit that database, and create a document. You will be creating what is known as a, ``Design Document``, which is a special kind of document in CouchDB that contains application code. The only thing you need to know now is to set the document id to something that begins with ``_design/`` and save it. Now click the button labeled *Upload Attachment* and choose the ``index.html`` file you just created, and upload it. Now click the link in Futon for ``index.html``, and you should see a list of the databases on that CouchDB instance. (rengel, 2012-09-05: Because of the *Same Origin* policy the ``index.html`` file has to be in the same directory, or a subdirectory thereof, as your whatever database.) You gotta admit there was nothing to that. Make it easy it with the CouchApp toolchain ------------------------------------------- Now that we've seen how you can build a basic CouchApp with the same set of tools you'd use to do plain-old HTML, CSS, and JavaScript development, let's learn how the experts (and the lazy!) do it. Uploading each changed file to CouchDB via Futon would get tedious quick. Alternatively, you could download the entire design document as JSON, and edit that JSON in your editor... but keeping track of proper JSON escaping and formattng is a task better done by a machine. Back in the early days of CouchDB, I solved this problem with a Ruby script that would update my map and reduce function from a folder. This way I could open the folder in TextMate, and get all the proper JavaScript syntax highlighting. To deploy the changes I'd run the Ruby script, and CouchDB would have my new Map Reduce views. That would have been the end of the story, except that for some reason, many people had boatloads of trouble installing the Ruby script. I may have been suffering from a bit of "grass is always greener," because my reaction was to port the Ruby stuff to Python (with a little help from my friends), which I thought would have a cleaner install story. (It almost does!) Since then `the Python CouchApp script `_ has grown in capability. It boasts the ability to `push edits in real time `_, import vendor modules, and more. Benoit Chesneau keeps it up to date pretty agressively, it just got some `GeoCouch features today. `_ So let's use it! Installing ``couchapp.py`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ There is a lot of documentation already out there about how to install the CouchApp toolchain. I'll just link to it. The basic installation instructions are `in the README `_ and in `the CouchDB Book `_ Here are some hints about `installing on Windows. `_ Once you have CouchApp installed, the basic usage is simple. From within your application directory, issue the following command. :: couchapp push . http://myname:mypass@localhost:5984/mydb Replace ``myname`` and ``mypass`` with those you set up on your CouchDB using Futon. If you didn't setup an admin password on Futon, you should do that -- until you do, your CouchDB can be administered by anyone. Also, if you are running a CouchDB in the cloud, you'll need to replace ``localhost:5984`` with something like ``mycouch.couchone.com``. Also, of course, ``mydb`` should be changed to the name of the database you want your program to live in. All this is coverered in great detail in the CouchApp README and the book, as linked above. The Standard Library ~~~~~~~~~~~~~~~~~~~~ We've made it nearly to the end of this post. The last thing to cover are the various JavaScript libraries for making CouchApps. I won't try to document them, just name them, and say a little about their purpose. I have a mental plan to clean up and consolidate some of these libraries, so they are more modular. This should make it so that CouchApp code loads faster, among other things. The jQuery CouchDB Client API ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We already used `jquery.couch.js `__ in the Tiny CouchApp example HTML above. This is the basic CouchDB library for jQuery. It handles things like saveDoc and openDoc, view queries, replication requests, etc. Essentially it wraps the CouchDB API in Ajaxy goodness. This library ships as part of CouchDB, as it is used by Futon. The CouchApp Code Loader ^^^^^^^^^^^^^^^^^^^^^^^^ The CouchApp toolchain ships with `jquery.couch.app.js `__, which is tasked with one job -- loading your application code into the page. This CouchApp jQuery plugin loads your design document (the JSON saved as a result of a ``couchapp push`` command), so that the browser has access to your view definitions, show and list functions, etc. It is invoked like so: :: $.couch.app(function(app){ // app.db is your jquery.couch.js object // app.require("lib/foo") gives you access to libraries }); Essentially, all this function does, is inspect the page you are on, determine how to load the design document, load it, and gives you an object that references it and allows you to require libraries from it. (There is some legacy featuritis in there, but I'm working to remove that.) Examples ~~~~~~~~ There is a :ref:`list-of-couchapps`. docs/js/000077500000000000000000000000001276277602300124365ustar00rootroot00000000000000docs/js/backbone.rst000066400000000000000000000015131276277602300147340ustar00rootroot00000000000000Using backbone.js with CouchApp ================================ Backbone.js_ is minimalist mvc framework for JavaScript, written by Jeremy Ashkenas, the author of coffee script. Backbone is a good choice for creating larger CouchApps, as an alternative to Evently. A robust backbone-couchdb connector that supports realtime updates via the `_changes` feed is supported by Jan Monschke. See this `introduction `__ to CouchApp with backbone.js_ by Jan. Extended version of backbone_ couch connector (with fixing some issues, extending functionality) is available here: https://github.com/andrzejsliwa/backbone-couch An example use case -------------------- https://github.com/andrzejsliwa/couch-watch .. _Backbone: .. _backbone.js: http://backbonejs.org/ docs/js/index.rst000066400000000000000000000011361276277602300143000ustar00rootroot00000000000000.. _js-app-programming: JavaScript Application Programming ================================== .. _jquery.couch.js: https://github.com/apache/couchdb/blob/trunk/share/www/script/jquery.couch.js .. _documentation for jquery.couch.js: http://daleharvey.github.com/jquery.couch.js-docs/symbols/index.html All application logic in a couchapp is provided by JavaScript. There is a library called `jquery.couch.js`_ that is distributed with every CouchDB installation. Here is the `documentation for jquery.couch.js`_ .. toctree:: :maxdepth: 2 backbone Also check out the :ref:`list-of-couchapps`. docs/user/000077500000000000000000000000001276277602300130005ustar00rootroot00000000000000docs/user/apps/000077500000000000000000000000001276277602300137435ustar00rootroot00000000000000docs/user/apps/pages/000077500000000000000000000000001276277602300150425ustar00rootroot00000000000000docs/user/apps/pages/install.rst000066400000000000000000000042071276277602300172450ustar00rootroot00000000000000.. _page-install: Install Pages ============= If anyone else has issues understanding this whole vhosts thing i'll give a *grandma-can-do-it* recount here of my troubles. If you want to set up *pages* on your machine and you are not very familiar with what's what, here it goes: Get pages from github: http://github.com/couchone/pages Navigate to your fav directory and do this in the terminal: :: git clone git://github.com/couchone/pages.git Make sure to install couchapp python helper app, i won't go into the details, `the github instructions are great `__ Hope you have couchdbx installed on osx or the equivalent on your favourite dev/production platform Do this from inside your pages directory: :: couchapp init couchapp push . http://localhost:5984/pages That will get you the app into your db, nothing new here, the git hub instructions will tell you the same thing, what got me (besides a bad commit ;)) is the instruction on http://blog.couch.io/post/443028592/whats-new-in-apache-couchdb-0-11-part-one-nice-urls The section about vhosts is a bit ambiguous for those that aren't in the know... The instructions there are as follows: "Each HTTP 1.1 request includes a mandatory header field Host: hostname.com with the server name it is trying to reach. You can tell CouchDB to look for that Host header and redirect all requests that match to any URL inside CouchDB by adding this to your configuration file local.ini: :: [vhosts] couch.io = /couchio/_design/app/_rewrite" Well, what the hell is local.ini? Who knows, who cares, go to your couchdb app and navigate to the *configuration* section. Go to the bottom of the page and "Add new section" Type into the 3 fields that popup: - ``vhosts`` - ``your-pages-site-name:5984`` - ``/pages/_design/pages/_rewrite`` Now go to the terminal and type: :: textmate /etc/hosts (Notice i'm making assumptions here, basically, get to the hosts file and open it...) Add: :: 127.0.0.1 your-pages-site-name Save, go to a browser type:: your-pages-site-name:5984 Hopefully that worked out OK for you. See ya! docs/user/desktopcouch.rst000066400000000000000000000014711276277602300162300ustar00rootroot00000000000000.. _couchdesktop: CouchApps and DesktopCouch ========================== In **version 0.7**, ``couchapp.py`` has a new feature allowing you to push, clone and browse CouchApps in the local CouchDB installed with `desktopcouch `_, so users of linux distributions where desktopcouch has been ported won't have to install another CouchDB to test and will be able to pair it with other desktop. How it works? ------------- To push to your local couchdb installed with desktopcouch: :: couchapp push desktopcouch://testdb To clone: :: couchapp clone desktopcouch://testdb/_design/test test1 To browse and use your application: :: couchapp browse . desktopcouch://mydb and with push option : :: couchapp push --browse . desktopcouch://mydb docs/user/garden.rst000066400000000000000000000030741276277602300147760ustar00rootroot00000000000000The Garden ========== .. warning:: The original database of garden was gone. Some legacy resources are `here `_. `The CouchApp Garden `_ is a CouchApp designed to make sharing other CouchApps easy. Once you have the Garden installed on your CouchDB, you can use it to install other CouchApps. The basics ---------- Currently, the Garden needs a lot of work, but the basic ideas are there. Essentially, it can copy design documents from your other databases, into the Garden database. As it copies them, it renames them so that they don't have ids that start with ``_design``. This means they can be replicated around without the replicator having to run with admin privileges. Also, the apps don't run code when they are just sitting in the Garden. Once you have a local Garden database, you can install apps from it, into databases on your CouchDB. The garden document will be copied to the target database, as a design document again, and there will be a link to visit that application. Sharing your app ---------------- To add your app to the Garden, install the Garden locally, and use its import link, to add the app to your local garden database. Then replicate that database to http://couchapp.org/garden, and check out the updated `Garden `_. Contributing to the Garden -------------------------- `The Garden code is on Github `_, please fork and contribute. docs/user/list-of-couchapps.rst000066400000000000000000000245031276277602300170760ustar00rootroot00000000000000.. _list-of-couchapps: List of CouchApps ================= Please add links to CouchApps (alphabetical order will help avoid duplicates in the long run). You may also be interested in `the CouchApp Garden `_. Afghan War Diary ---------------- A GeoCouch app that provides a browseable map of entries from the Wikileaks Afghan Diaries. `Code `_ BlueInk ------- The beginnings of a conversion of the `BlueInk CMS `_ to a CouchApp. Currently, it can serve as a view-engine for web pages wearing Mustache_ templates. `Code `_ Bookkeeping ----------- A little CouchApp that helps visualizing expenses for my household. Currently in German only. I am working on a branch that uses views and ``_changes`` instead of a pure client side implementation with jquery. `Code `_ Boom Amazing ------------ Presentation software with a twist. Uses SVG and pan and zoom. Based on Sammy.js_. `Code `_ Brunch-Colors ------------- Brunch-Colors is a simple, addictive color-matching game that was made with `Brunch `_ that utilizes such tools as Backbone.js_, eco and stylus. `Play it here `_ `Code `_ Costco ---------------------------------------------- A small UI for bulk editing CouchDB documents. `Code `_ CouchCrawler ------------ Spiders the web into CouchDB. Uses a Python script for web spidering. `Read the blog post here `_ `Code `_ CouchWatch ---------- Simple logs watcher with realtime view and simple searching. For using witch Rails and JavaScript logger. Written in Backbone.js_ `Code `_ CouchDB Contact Form -------------------- Simple Contact Form CouchApp for CouchDB. Includes simple mail spooler. `Code `_ CouchDB Projector ----------------- For doing presentations. `Code `_ CouchLog -------- Application Logging tool. Uses a CouchDB backend with a CouchApp-based interface for sorting through log entries and troubleshooting/debugging applications. Leverages schema-less approach to allow log entries to contain structured meta-information to aid in troubleshooting `Code `_ csv2couchdb ----------- small app to populate couchdb using data from CSV files `Code `_ Dimensional Drawing ------------------- Collaborative 2.5D drawing space. `Code `_ `Demo `_ Focus ----- A TODO tracker that replicates. Run it on your phone, run it on your server, run it on your laptop. Keep them synchronized. Never forget to do that important thing! `Code `_ Deployments: - `Demo `_ Food Cart Pages --------------- A catalog of all the food carts in Portland. Deployments: - `http://foodcartpages.com `_ HejHej ------ A CouchApp for language learning. Lets you train vocabularies and solve different kinds of games/tests. Has Cucumber tests. `Code `_ Hub List -------- `Open source GTD style productivity app `_. Manage your tasks from bug trackers, pm tools and other online todo lists all in one place. Built with Ext JS 4. `Code `_ IrcLog CouchApp --------------- A couchapp to view irc logs stored in CouchDB. The irclogs can be stored by `gdamjan's ircbot `_ and its `couchdb loging plugin `_ . `Code `_ Li.Couch -------- `Open source LIst notes `_. Easy track of your items. Built with Knockout.js. `Code `_ `Demo `_ MapChat ------- A real time chat app on a Google Map. Points on a map as a chat rooms. `Code `_ `Demo `_ Microanalytics -------------- Personal hackable web-analytics. `Code `_ `Python command-line client `_ Modern Forum ------------ A new project aiming to bring real-time, CouchDB-powered forums to the masses. `Code `_ Monocles (ex-CouchAppSpora) --------------------------- diaspora... as a couchapp! in pure javascript and fully OStatus compliant (almost) `Code and more info `_ `Demo `_ MTG Pricing CouchApp -------------------- A mobile-centric app to get the pricing information for Magic: The Gathering cards quickly and easily. `Code `_ Mytweets -------- A personal Twitter archive. Deployments: - `@yssk22 `_ Nymphormation ------------- A social link sharing tool. `Code `_ Deployments: - `Nymphormation `_ Pages ----- A Markdown wiki. This was the wiki used to create this documentation originally. .. toctree:: :maxdepth: 2 apps/pages/install `Code `_ Deployments: - `CouchApp `_ Processing JS Studio -------------------- Web-based application to store Processing JS sketches and renderings. Storage and service provided by CouchDB via CouchApp. `Code `_ Proto ----- A basic CouchApp for inputing info from a form, and listing it in real time. This is the starting point for many other applications, as well as the `Evently Guided Hack Video Tutorial `_. `Code `_ Or run ``couchapp generate foo`` to get your own version, ready for hacking. Deployments: - `jChris `_ - `Jan `_ - `Goto `_ Random Lecture! --------------- A simple Sammy-On-CouchApp (soca) app that plays a random technical lecture or tech talk. - `Demo `_ - `Code `_ - `List of all lectures `_ Sales Stats ----------- A simple CouchApp Demo that displays sales statistics as a bar graph. It uses the ``_changes`` API together with Evently, so that the sales statistics are updated live (in near realtime). `Code `_ `Demo `_ Scrapboard ---------- A decentralized implementation of the old Orkut scrapbook. `Code `_ Skim - Simple knowledgebase for insightful metabolomics ------------------------------------------------------- The vision behind Skim is to develop a tool that can help analyze vast quantities of peer reviewed and community-provided information on metabolites, biochemical reactions and pathways. Heavily under development - may be unstable from time to time. `Code `_ `Demo `_ Sleepcam -------- Whenever a user's computer wakes from sleep, the software takes a picture with their webcam and posts it to their profile on sleepcam.org. Users can like and comment on eachother's pictures. `Code `_ `Demo `_ Sofa ---- Standalone CouchDB Blog with tagging, Atom feeds, and gravatar comments , used by the O'Reilly CouchDB book. `Code `_ Deployments: - `Daytime Running Lights `_ - `Chewbranca `_ - `Plok Light `_ - `Blog Bleeds `_ Snippets -------- A Couchdb snippets app with a Couchfuse backend. `Code `_ Swinger ------- A presentation engine. Like Keynote in the browser, but simpler. Uses Sammy.js_. `Code `_ Deployments: - `http://swinger.quirkey.com `_ TapirWiki --------- A wiki couchapp. Uses textile as the markup language and has a few macros, templates and support for attachments. `Code `_ Taskr ----- A task tracker. This one got deprecated by Focus. It's got some cool features so it's worth looking at if you are building something similar. `Code `_ The Infinite Maze ----------------- A collaborative maze drawing app. `Code `_ `Demo `_ Toast ----- A real time chat app. One of the first demos of the ``_changes`` API. `Code `_ `Demo `_ Tweet Eater ----------- A Twitter search archive and real time display. Uses a Ruby backend to import tweets from the streaming API. `Code `_ hckr.it ------- A `Hacker News `_ clone built entirely using CouchDB that can be served as a couchapp. `Code `_ `Demo `_ .. _Mustache: http://mustache.github.io/ .. _Backbone.js: http://backbonejs.org/ .. _Sammy.js: http://code.quirkey.com/sammy/ docs/user/videos.rst000066400000000000000000000007701276277602300150270ustar00rootroot00000000000000Video Tutorials on CouchDB ========================== .. warning:: The content is out of date Also check out `Relaxed.tv `_ for awesome CouchDB videos! Get friendly with CouchDB (an intro tutorial) --------------------------------------------- Interactive HTML5 CouchApps using node.couchapp.js -------------------------------------------------- How to host your website in CouchDB ----------------------------------- Eclipse CouchDB --------------- New in 1.0 ---------- resources/000077500000000000000000000000001276277602300131045ustar00rootroot00000000000000resources/scripts/000077500000000000000000000000001276277602300145735ustar00rootroot00000000000000resources/scripts/couchapp000077500000000000000000000003461276277602300163260ustar00rootroot00000000000000#!/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() setup.cfg000066400000000000000000000003321276277602300127110ustar00rootroot00000000000000[nosetests] cover-package=couchapp cover-html=1 cover-erase=1 detailed-errors=1 # Specify config file, this option require `nose-testconfig` installed # tc-file=tests/config.ini tests=tests verbosity=3 with-coverage=1 setup.py000066400000000000000000000145301276277602300126070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of couchapp released under the Apache 2 license. # See the NOTICE for more information. import couchapp import os import sys from setuptools import setup, find_packages 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 = {} def get_data_files(): data_files = [('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 CLASSIFIERS = ['License :: OSI Approved :: Apache Software License', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Development Status :: 4 - Beta', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', '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', 'mock'], 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.contrib", "pathtools.path", "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/000077500000000000000000000000001276277602300122345ustar00rootroot00000000000000tests/__init__.py000066400000000000000000000000701276277602300143420ustar00rootroot00000000000000import test_cli import test_ignores import test_compresstests/config.sample.ini000066400000000000000000000000441276277602300154600ustar00rootroot00000000000000[host] url = http://127.0.0.1:5984/ tests/test_cli.py000066400000000000000000000235331276277602300144220ustar00rootroot00000000000000#!/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.close(f) 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_clone_app.py000066400000000000000000000005351276277602300156100ustar00rootroot00000000000000# -*- coding: utf-8 -*- from couchapp.clone_app import clone from couchapp.errors import AppError from mock import patch from nose.tools import raises @raises(AppError) def test_invalid_source(): ''' Test case for clone(invalid_source) If a source uri do not contain ``_design/``, it's invalid. ''' clone('http://foo.bar') tests/test_commands.py000066400000000000000000000354471276277602300154630ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os from couchapp import commands from couchapp.errors import AppError, BulkSaveError from couchapp.localdoc import document from mock import MagicMock, Mock, NonCallableMock, patch from nose.tools import raises @patch('couchapp.commands.document') def test_init_dest(mock_doc): commands.init(None, None, '/tmp/mk') mock_doc.assert_called_once_with('/tmp/mk', create=True) @patch('os.getcwd', return_value='/mock_dir') @patch('couchapp.commands.document') def test_init_dest_auto(mock_doc, mock_cwd): commands.init(None, None) mock_doc.assert_called_once_with('/mock_dir', create=True) @raises(AppError) @patch('os.getcwd', return_value=None) @patch('couchapp.commands.document') def test_init_dest_none(mock_doc, mock_cwd): commands.init(None, None) @patch('couchapp.commands.hook') @patch('couchapp.commands.document', spec=document) def test_push_outside(mock_doc, mock_hook): ''' $ couchapp push /path/to/app dest ''' conf = NonCallableMock(name='conf') path = None appdir = '/mock_dir' dest = 'http://localhost' hook_expect = [ ((conf, appdir, 'pre-push'), {'dbs': dest}), ((conf, appdir, 'post-push'), {'dbs': dest}), ] conf.get_dbs.return_value = dest ret_code = commands.push(conf, path, appdir, dest) mock_doc.assert_called_once_with(appdir, create=False, docid=None) mock_doc().push.assert_called_once_with(dest, False, False, False) assert mock_hook.call_args_list == hook_expect assert ret_code == 0 @patch('os.path.exists') @patch('couchapp.commands.pushdocs', spec=commands.pushdocs) @patch('couchapp.commands.hook') @patch('couchapp.commands.document', spec=document) def test_push_with_pushdocs(mock_doc, mock_hook, mock_pushdocs, mock_exists): ''' if appdir/_docs exists, push will invoke pushdocs ''' conf = NonCallableMock(name='conf') appdir = '/mock_dir' dest = 'http://localhost' docspath = os.path.join(appdir, '_docs') def check_docspath(docspath_): return docspath_ == docspath mock_exists.side_effect = check_docspath ret_code = commands.push(conf, appdir, dest) mock_pushdocs.assert_called_once_with(conf, docspath, dest, dest) assert ret_code == 0 @patch('couchapp.commands.document', spec=document) def test_push_export_outside(mock_doc): ''' $ couchapp push --export /path/to/app ''' conf = NonCallableMock(name='conf') appdir = '/mock_dir' ret_code = commands.push(conf, None, appdir, export=True) mock_doc.assert_called_once_with(appdir, create=False, docid=None) assert ret_code == 0 @patch('couchapp.commands.document', spec=document) def test_push_export_inside(mock_doc): ''' In the app dir:: $ couchapp push --export ''' conf = NonCallableMock(name='conf') appdir = '/mock_dir' ret_code = commands.push(conf, appdir, export=True) mock_doc.assert_called_once_with(appdir, create=False, docid=None) assert ret_code == 0 @patch('couchapp.commands.util') @patch('couchapp.commands.document', return_value='{"status": "ok"}', spec=document) def test_push_export_to_file(mock_doc, mock_util): ''' $ couchapp push --export --output /path/to/json /appdir ''' conf = NonCallableMock(name='conf') appdir = '/mock_dir' output_file = '/file' ret_code = commands.push(conf, appdir, export=True, output=output_file) mock_doc.assert_called_once_with(appdir, create=False, docid=None) mock_util.write_json.assert_called_once_with( output_file, '{"status": "ok"}' ) assert ret_code == 0 @raises(AppError) def test_push_app_path_error(): conf = NonCallableMock(name='conf') dest = 'http://localhost' commands.push(conf, None, dest) @patch('couchapp.commands.util.write_json') @patch('couchapp.commands.document', spec=document) @patch('couchapp.commands.hook') @patch('couchapp.commands.util.discover_apps', return_value=['foo']) def test_pushapps_output(discover_apps_, hook, document_, write_json): ''' Test case for pushapps with ``--export --output file`` Algo: 1. discover apps #. pre-push #. add app to a list ``apps`` #. post-push #. write_json(apps) ''' conf = NonCallableMock(name='conf') dest = None ret_code = commands.pushapps(conf, '/mock_dir', dest, export=True, output='file') assert ret_code == 0 discover_apps_.assert_called_with('/mock_dir') hook.assert_any_call(conf, 'foo', 'pre-push', dbs=conf.get_dbs(), pushapps=True) hook.assert_any_call(conf, 'foo', 'post-push', dbs=conf.get_dbs(), pushapps=True) 'file' in write_json.call_args[0] @patch('couchapp.commands.util.write_json') @patch('couchapp.commands.document', spec=document) @patch('couchapp.commands.hook') @patch('couchapp.commands.util.discover_apps', return_value=[]) def test_pushapps_output_null(discover_apps_, hook, document_, write_json): ''' Test case for pushapps with ``--export --output file``, but no any apps discovered Algo: see :py:meth:`test_pushapps_output` ''' conf = NonCallableMock(name='conf') dest = None ret_code = commands.pushapps(conf, '/mock_dir', dest, export=True, output='file') assert ret_code == 0 discover_apps_.assert_called_with('/mock_dir') assert not hook.called assert not document_.called assert not write_json.called @patch('couchapp.commands.util.json.dumps') @patch('couchapp.commands.document', spec=document) @patch('couchapp.commands.hook') @patch('couchapp.commands.util.discover_apps', return_value=['foo']) def test_pushapps_export(discover_apps_, hook, document_, dumps): ''' Test case for pushapps with ``--export``, Algo: 1. discover apps #. pre-push #. add app to a list ``apps`` #. post-push #. json.dumps from apps ''' conf = NonCallableMock(name='conf') dest = None ret_code = commands.pushapps(conf, '/mock_dir', dest, export=True) assert ret_code == 0 discover_apps_.assert_called_with('/mock_dir') hook.assert_any_call(conf, 'foo', 'pre-push', dbs=conf.get_dbs(), pushapps=True) hook.assert_any_call(conf, 'foo', 'post-push', dbs=conf.get_dbs(), pushapps=True) assert dumps.called @patch('couchapp.commands.document', spec=document) @patch('couchapp.commands.hook') @patch('couchapp.commands.util.discover_apps', return_value=['foo']) def test_pushapps_noatomic(discover_apps_, hook, document_): ''' Test case for pushapps with ``--no-atomic`` Algo: 1. discover apps #. for each app 1. pre-push 2. push 3. post-push ''' conf = NonCallableMock(name='conf') dest = 'http://localhost:5984' doc = document_() dbs = conf.get_dbs() ret_code = commands.pushapps(conf, '/mock_dir', dest, no_atomic=True) assert ret_code == 0 conf.get_dbs.assert_called_with(dest) hook.assert_any_call(conf, 'foo', 'pre-push', dbs=dbs, pushapps=True) hook.assert_any_call(conf, 'foo', 'post-push', dbs=dbs, pushapps=True) doc.push.assert_called_with(dbs, True, False) @patch('couchapp.commands.document', spec=document) @patch('couchapp.commands.hook') @patch('couchapp.commands.util.discover_apps', return_value=['foo']) def test_pushapps_default(discover_apps_, hook, document_): ''' Test case for ``pushapps {path}`` with default flags Algo: 1. discover apps #. for each app 1. pre-push 2. add to list apps 3. post-push #. for each db 1. db.save_docs ''' conf = NonCallableMock(name='conf') dest = 'http://localhost:5984' doc = document_() db = Mock(name='db') dbs = MagicMock(name='dbs') dbs.__iter__.return_value = iter([db]) conf.get_dbs.return_value = dbs ret_code = commands.pushapps(conf, '/mock_dir', dest) assert ret_code == 0 conf.get_dbs.assert_called_with(dest) hook.assert_any_call(conf, 'foo', 'pre-push', dbs=dbs, pushapps=True) hook.assert_any_call(conf, 'foo', 'post-push', dbs=dbs, pushapps=True) assert db.save_docs.called def test_version_help(): ''' $ couchapp version -h ''' assert commands.version(Mock(), help=True) == 0 def test_help_version(): ''' $ couchapp -h --version ''' assert commands.usage(Mock(), version=True) == 0 @patch('couchapp.commands.hook') @patch('couchapp.commands.clone_app.clone') def test_clone_default(clone, hook): ''' $ couchapp clone {source} ''' conf = NonCallableMock(name='conf') src = 'http://localhost:5984/test' dest = None ret_code = commands.clone(conf, src) assert ret_code == 0 hook.assert_any_call(conf, dest, 'pre-clone', source=src) clone.assert_called_with(src, None, rev=None) hook.assert_any_call(conf, dest, 'post-clone', source=src) @patch('couchapp.commands.os.getcwd', return_value='/') @patch('couchapp.commands.generator.generate') def test_startapp_default(generate, getcwd): ''' $ couchapp startapp {name} ''' conf = NonCallableMock(name='conf') name = 'mock' ret_code = commands.startapp(conf, name) assert ret_code == 0 generate.assert_called_with('/mock', 'startapp', name) @patch('couchapp.commands.os.getcwd') @patch('couchapp.commands.generator.generate') def test_startapp_default(generate, getcwd): ''' $ couchapp startapp {dir} {name} ''' conf = NonCallableMock(name='conf') dir_ = '/' name = 'mock' ret_code = commands.startapp(conf, dir_, name) assert ret_code == 0 assert not getcwd.called generate.assert_called_with('/mock', 'startapp', name) @raises(AppError) @patch('couchapp.commands.os.getcwd', return_value='/') @patch('couchapp.commands.generator.generate') def test_startapp_without_name(generate, getcwd): ''' $ couchapp startapp ''' conf = NonCallableMock(name='conf') ret_code = commands.startapp(conf) assert not generate.called @raises(AppError) @patch('couchapp.commands.util.iscouchapp', return_value=True) @patch('couchapp.commands.os.getcwd', return_value='/') @patch('couchapp.commands.generator.generate') def test_startapp_exists(generate, getcwd, iscouchapp): ''' $ couchapp startapp {already exists app} ''' conf = NonCallableMock(name='conf') ret_code = commands.startapp(conf) assert not generate.called @raises(AppError) @patch('couchapp.commands.util.iscouchapp', return_value=True) @patch('couchapp.commands.os.getcwd', return_value='/') @patch('couchapp.commands.generator.generate') def test_startapp_exists(generate, getcwd, iscouchapp): ''' $ couchapp startapp {already exists app} ''' conf = NonCallableMock(name='conf') name = 'mock' ret_code = commands.startapp(conf, name) assert iscouchapp.assert_called_with('/mock') assert not generate.called @raises(AppError) @patch('couchapp.commands.util.findcouchapp', return_value=True) @patch('couchapp.commands.util.iscouchapp', return_value=False) @patch('couchapp.commands.os.getcwd', return_value='/') @patch('couchapp.commands.generator.generate') def test_startapp_inside_app(generate, getcwd, iscouchapp, findcouchapp): ''' $ couchapp startapp {path in another app} e.g. Assume there is a couchapp ``app1`` :: app1/ .couchapprc ... We try to ``couchapp startapp app1/app2``, and this should raise `AppError`. ''' conf = NonCallableMock(name='conf') name = 'mock' ret_code = commands.startapp(conf, name) assert findcouchapp.assert_called_with('/mock') assert not generate.called @patch('couchapp.commands.util.iscouchapp', return_value=True) @patch('couchapp.commands.document') def test_browse_default(document, iscouchapp): ''' $ couchapp browse {app} {db url} ''' conf = NonCallableMock(name='conf') app = '/mock_dir' dest = 'http://localhost:5984' doc = document() ret_code = commands.browse(conf, app, dest) iscouchapp.assert_called_with('/mock_dir') assert doc.browse.called @patch('os.getcwd', return_value='/mock_dir/app') @patch('couchapp.commands.util.iscouchapp', return_value=True) @patch('couchapp.commands.document') def test_browse_dest_only(document, iscouchapp, getcwd): ''' $ couchapp browse {db url} ''' conf = NonCallableMock(name='conf') dest = 'http://localhost:5984' doc = document() ret_code = commands.browse(conf, dest) iscouchapp.assert_called_with('/mock_dir/app') assert doc.browse.called @raises(AppError) @patch('couchapp.commands.util.iscouchapp', return_value=False) @patch('couchapp.commands.document') def test_browse_exist(document, iscouchapp): ''' $ couchapp browse {not app dir} {db url} ''' conf = NonCallableMock(name='conf') app = '/mock_dir/notapp' dest = 'http://localhost:5984' doc = document() ret_code = commands.browse(conf, app, dest) iscouchapp.assert_called_with('/mock_dir/notapp') assert not doc.browse.called @raises(AppError) def test_generate_inside(): ''' $ couchapp generate app {path inside another app} This should raise AppError. ''' conf = NonCallableMock(name='conf') app = '/mock/app' commands.generate(conf, app, 'app', 'mockapp') @raises(AppError) def test_generate_miss_name(): ''' $ couchapp generate This should raise AppError. ''' conf = NonCallableMock(name='conf') app = '/mock/app' commands.generate(conf, app) @raises(AppError) def test_generate_view_without_app(): ''' $ couchapp generate view myview But outside app dir. This should raise AppError. ''' conf = NonCallableMock(name='conf') commands.generate(conf, None, 'view', 'myview') @patch('couchapp.commands.os.getcwd', return_value='/mock') @patch('couchapp.commands.generator.generate') @patch('couchapp.commands.hook') def test_generate_app(hook, generate, getcwd): ''' $ couchapp generate myapp ''' conf = NonCallableMock(name='conf') kind = 'app' name = 'myapp' ret_code = commands.generate(conf, None, name) assert ret_code == 0 generate.assert_called_with('/mock/myapp', kind, name, create=True) hook.assert_any_call(conf, '/mock/myapp', 'pre-generate') hook.assert_any_call(conf, '/mock/myapp', 'post-generate') @patch('couchapp.commands.os.getcwd', return_value='/mock') @patch('couchapp.commands.generator.generate') @patch('couchapp.commands.hook') def test_generate_view_outside_app(hook, generate, getcwd): ''' $ couchapp generate view myapp myview ''' conf = NonCallableMock(name='conf') kind = 'view' dest = 'myapp' name = 'myview' ret_code = commands.generate(conf, None, kind, dest, name) assert ret_code == 0 generate.assert_called_with(dest, kind, name) hook.assert_any_call(conf, dest, 'pre-generate') hook.assert_any_call(conf, dest, 'post-generate') tests/test_compress.py000066400000000000000000000042071276277602300155030ustar00rootroot00000000000000#!/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/test_config.py000066400000000000000000000273701276277602300151230ustar00rootroot00000000000000# -*- coding: utf-8 -*- from couchapp.config import Config from couchapp.errors import AppError from mock import Mock, patch from nose.tools import raises, with_setup @patch('couchapp.config.util.findcouchapp', return_value=None) @patch('couchapp.config.util.rcpath', return_value=['/mock/couchapp.conf']) def test_config_init(rcpath, getcwd): ''' Test case for Config.__init__() Check following vars: - self.rc_path - self.global_conf - self.local_conf - self.app_dir - self.conf ''' config = Config() assert config.rc_path == ['/mock/couchapp.conf'], config.rc_path assert config.global_conf == Config.DEFAULTS, config.global_conf assert config.local_conf == {}, config.local_conf assert config.app_dir is None, config.app_dir assert config.conf == Config.DEFAULTS @patch('couchapp.config.Config.load_local', return_value={'mock': True}) @patch('couchapp.config.util.findcouchapp', return_value='/mockapp') @patch('couchapp.config.util.rcpath', return_value=['/mock/couchapp.conf']) def test_config_init_local(rcpath, getcwd, local_conf): ''' Test case for Config.__init__() in a CouchApp ''' config = Config() local_conf.assert_called_with('/mockapp') assert config.local_conf == {'mock': True}, config.local_conf class TestConfig(): @patch('couchapp.config.util.findcouchapp', return_value=None) @patch('couchapp.config.util.rcpath', return_value=['/mock/couchapp.conf']) def setup(self, rcpath, getcwd): self.config = Config() def teardown(self): del self.config @raises(AttributeError) def test_getattr(self): ''' Test case for Config.__getattr__() ''' assert self.config.conf == Config.DEFAULTS assert self.config.env == {} self.config.mock # raise AttributeError @raises(KeyError) def test_getitem(self): ''' Test case for Config.__getitem__() ''' assert self.config['conf'] == Config.DEFAULTS assert self.config['env'] == {} assert self.config['mock'] # raise KeyError def test_contains(self): ''' Test case for Config.__contains__() ''' assert 'env' in self.config assert 'hooks' in self.config assert 'extensions' in self.config @patch('couchapp.config.util.read_json', return_value={'mock': True}) @patch('couchapp.config.os.path.isfile', return_value=True) def test_load_from_list(self, isfile, read_json): ''' Test case for Config.load(list, default) ''' default = {'foo': 'bar'} conf = self.config.load(['/mock/couchapp.conf'], default) assert conf == {'foo': 'bar', 'mock': True} isfile.assert_called_with('/mock/couchapp.conf') read_json.assert_called_with('/mock/couchapp.conf', use_environment=True, raise_on_error=True) @patch('couchapp.config.util.read_json', return_value={'mock': True}) @patch('couchapp.config.os.path.isfile', return_value=True) def test_load_from_str(self, isfile, read_json): ''' Test case for Config.load(str) ''' conf = self.config.load('/mock/couchapp.conf') assert conf == {'mock': True} isfile.assert_called_with('/mock/couchapp.conf') read_json.assert_called_with('/mock/couchapp.conf', use_environment=True, raise_on_error=True) @patch('couchapp.config.util.read_json', return_value={'mock': True}) @patch('couchapp.config.os.path.isfile', return_value=False) def test_load_notfile(self, isfile, read_json): ''' Test case for Config.load(['/not_a_file'], default) ''' default = {'foo': 'bar'} conf = self.config.load(['/not_a_file'], default) assert conf == {'foo': 'bar'} isfile.assert_called_with('/not_a_file') assert not read_json.called @raises(AppError) @patch('couchapp.config.util.read_json', side_effect=ValueError) @patch('couchapp.config.os.path.isfile', return_value=True) def test_load_apperror(self, isfile, read_json): ''' Test case for Config.load() reading a invalid file ''' self.config.load('/mock/couchapp.conf') isfile.assert_called_with('/mock/couchapp.conf') @patch('couchapp.config.util.read_json', return_value={'mock': True}) @patch('couchapp.config.os.path.isfile', return_value=True) def test_load_deepcopy_default(self, isfile, read_json): ''' Test case for checking Config.load() deepcopy param ``default`` ''' default = {'foo': {'bar': 'fake'}} conf = self.config.load('/mock.conf', default=default) assert conf == { 'foo': {'bar': 'fake'}, 'mock': True } assert default == {'foo': {'bar': 'fake'}} @patch('couchapp.config.Config.load', return_value='mock') def test_load_local(self, load): ''' Test case for Config.load_local() ''' assert self.config.load_local('/mock') == 'mock' paths = tuple(load.call_args[0][0]) assert paths == ('/mock/couchapp.json', '/mock/.couchapprc'), paths @raises(AppError) @patch('couchapp.config.Config.load') def test_load_local_apperror(self, load): ''' Test case for Config.load_local() with empty `app_dir` ''' self.config.load_local(None) assert not load.called @patch('couchapp.config.Config.load_local', return_value={'mock': True}) def test_update(self, load_local): ''' Test case for Config.update() ''' self.config.update('/mock') assert self.config.conf.get('mock') == True load_local.assert_called_with('/mock') @patch('couchapp.config.util.load_py') def test_extensions_empty(self, load_py): ''' Test case for empty Config.extensions ''' assert self.config.extensions == [] assert not load_py.called @patch('couchapp.config.util.load_py', return_value='mock') def test_extensions_mock(self, load_py): ''' Test case for Config.extensions ''' self.config.conf['extensions'] = ['mock_path'] extensions = self.config.extensions assert extensions == ['mock'], extensions load_py.assert_called_with('mock_path', self.config) @patch('couchapp.config.util.hook_uri') def test_hook_empty(self, hook_uri): ''' Test case for empty Config.hooks ''' assert self.config.hooks == {} assert not hook_uri.called @patch('couchapp.config.util.hook_uri', return_value='mock_module') def test_hook_mock(self, hook_uri): ''' Test case for Config.hooks ''' self.config.conf['hooks'] = {'pre-push': ['mock_path']} hooks = self.config.hooks assert hooks == {'pre-push': ['mock_module']}, hooks hook_uri.assert_called_with('mock_path', self.config) @patch('couchapp.config.Database', return_value='mockdb') def test_get_dbs_full_uri(self, Database): ''' Test case for Config.get_dbs() with full uri ''' db_string = 'https://foo.bar' assert self.config.get_dbs(db_string) == ['mockdb'] Database.assert_called_with(db_string, use_proxy=False) @patch('couchapp.config.Database', return_value='mockdb') def test_get_dbs_short_uri(self, Database): ''' Test case for Config.get_dbs() with short uri ''' full_uri = 'http://127.0.0.1:5984/foo' assert self.config.get_dbs('foo') == ['mockdb'] Database.assert_called_with(full_uri, use_proxy=False) @patch('couchapp.config.Database', return_value='mockdb') def test_get_dbs_env(self, Database): ''' Test case for Config.get_dbs() with env set ''' db_string = 'http://foo.bar' self.config.conf['env'] = {'default': {'db': db_string}} assert self.config.get_dbs() == ['mockdb'] Database.assert_called_with(db_string, use_proxy=False) @raises(AppError) @patch('couchapp.config.Database') def test_get_dbs_empty_env(self, Database): ''' Test case for Config.get_dbs() without env set ''' self.config.get_dbs() assert not Database.called @patch('couchapp.config.Database', return_value='mockdb') def test_get_dbs_short_env(self, Database): ''' Test case for Config.get_dbs() with a short name in env ''' self.config.conf['env'] = {'foo': {'db': 'http://foo.bar'}} assert self.config.get_dbs('foo') == ['mockdb'] Database.assert_called_with('http://foo.bar', use_proxy=False) @patch('couchapp.config.Database', return_value='mockdb') def test_get_dbs_env_fake(self, Database): ''' Test case for Config.get_dbs() with an useless env Assume .couchapprc is (no `db` field in `foo`) ``` { 'foo':{ 'notdb': 'mock' } } ``` Expect behavior: fall back to DEFAULT_SERVER_URI/foo ''' self.config.conf['env'] = {'foo': {'notdb': 'mock'}} default_uri = 'http://127.0.0.1:5984/foo' assert self.config.get_dbs('foo') == ['mockdb'] Database.assert_called_with(default_uri, use_proxy=False) @patch('couchapp.config.Database', return_value='mockdb') def test_get_dbs_proxy(self, Database): ''' Test case for Config.get_dbs() with https_proxy env ''' with patch.dict('couchapp.config.os.environ', {'https_proxy': 'foo'}): assert self.config.get_dbs('https://bar') == ['mockdb'] Database.assert_called_with('https://bar', use_proxy=True) def test_get_app_name_default(self): ''' Test case for Config.get_app_name() without args and env ''' assert self.config.get_app_name() == None def test_get_app_name_default_env(self): ''' Test case for Config.get_app_name() without args but env env: { 'default': { 'name': 'MockApp' } } and env: { 'mockname': { 'name': 'MockApp2' } } ''' self.config.conf['env'] = {'default': {'name': 'MockApp'}} assert self.config.get_app_name() == 'MockApp' self.config.conf['env'] = {'mockname': {'name': 'MockApp2'}} assert self.config.get_app_name('mockname') == 'MockApp2' self.config.conf['env'] = {'default': {'name': 'MockApp3'}} assert self.config.get_app_name('strang') == 'MockApp3' def test_get_app_name_http_uri(self): ''' Test case for Config.get_app_name('http://foo.bar', default) If the dbstring is full uri, return ``default`` ''' ret = self.config.get_app_name('http://foo.bar', 'mockapp') assert ret == 'mockapp' def test_iter(self): ''' Test case for Config.__iter__() ''' self.config.conf['env'] = {'mock': True} ls = list(self.config) assert ('env', {'mock': True}) in ls, ls assert ('hooks', {}) in ls, ls def test_get(self): ''' Test case for Config.get('__init__') ''' assert callable(self.config.get('__init__')) def test_get_default(self): ''' Test case for Config.get('strang', 'default') ''' assert self.config.get('strang', 'default') == 'default' def test_get_conf(self): ''' Test case for Config.get('env') returning value from self.conf ''' self.config.conf['env'] = {'mock': True} assert self.config.get('env') == {'mock': True} tests/test_dispatch.py000066400000000000000000000127301276277602300154470ustar00rootroot00000000000000# -*- coding: utf-8 -*- from getopt import GetoptError from couchapp import dispatch from couchapp.commands import globalopts from couchapp.errors import AppError, CommandLineError from nose.tools import raises from mock import Mock, patch def test_parseopts_short_flag(): opts = {} args = dispatch.parseopts(['-v', 'generate', 'app', 'test-app'], globalopts, opts) assert args == ['generate', 'app', 'test-app'] assert opts['debug'] is None assert opts['help'] is None assert opts['version'] is None assert opts['verbose'] == True assert opts['quiet'] is None def test_parseopts_long_flag(): opts = {} args = dispatch.parseopts(['--version'], globalopts, opts) assert args == [] assert opts['debug'] is None assert opts['help'] is None assert opts['version'] == True assert opts['verbose'] is None assert opts['quiet'] is None def test_parseopts_invalid_flag(): @raises(GetoptError) def short(): dispatch.parseopts(['-X'], globalopts, {}) @raises(GetoptError) def long(): dispatch.parseopts(['--unkown'], globalopts, {}) short() long() def test_parseopts_list_option(): listopts = [('l', 'list', [], 'Test for list option')] opts = {} dispatch.parseopts(['-l', 'foo', '-l', 'bar'], listopts, opts) assert opts['list'] == ['foo', 'bar'] def test_parseopts_int_option(): intopts = [('i', 'int', 100, 'Test for int option')] opts = {} dispatch.parseopts(['-i', '200'], intopts, opts) assert opts['int'] == 200 def test_parseopts_str_option(): stropts = [('s', 'str', '', 'Test for int option')] opts = {} dispatch.parseopts(['-s', 'test'], stropts, opts) assert opts['str'] == 'test' def test__parse_invalid_flag(): @raises(CommandLineError) def short(): dispatch._parse(['-X']) @raises(CommandLineError) def app_arg(): dispatch._parse(['init', '-X']) @raises(CommandLineError) def long(): dispatch._parse(['--unkown']) short() app_arg() long() def test__parse_help(): cmd, options, cmdoptions, args = dispatch._parse(['help']) assert cmd == 'help' cmd, options, cmdoptions, args = dispatch._parse([]) assert cmd == 'help' cmd, options, cmdoptions, args = dispatch._parse(['-h']) assert cmd == 'help' assert options['help'] == True def test__parse_subcmd_options(): cmd, options, cmdoptions, args = dispatch._parse(['push', '-b', 'http://localhost']) assert cmd == 'push' assert cmdoptions['browse'] == True @patch('couchapp.dispatch.set_logging_level') def test__dispatch_debug(set_logging_level): assert dispatch._dispatch(['-d']) == 0 set_logging_level.assert_called_with(1) def test__dispatch_help(): assert dispatch._dispatch(['-h']) == 0 @patch('couchapp.dispatch.set_logging_level') def test__dispatch_verbose(set_logging_level): assert dispatch._dispatch(['-v']) == 0 set_logging_level.assert_called_with(1) def test__dispatch_version(): assert dispatch._dispatch(['--version']) == 0 @patch('couchapp.dispatch.set_logging_level') def test__dispatch_quiet(set_logging_level): assert dispatch._dispatch(['-q']) == 0 set_logging_level.assert_called_with(0) @raises(CommandLineError) def test__dispatch_unknown_command(): dispatch._dispatch(['unknown_command']) @patch('couchapp.dispatch.commands') @patch('couchapp.dispatch.Config') def test__dispatch_inapp(conf, commands): conf = conf() conf.app_dir = '/mock_dir' mock_func = Mock(return_value=10) commands.table = {'mock': (mock_func, [], 'just for testing')} commands.incouchapp = ['mock'] commands.globalopts = globalopts assert dispatch._dispatch(['mock']) == 10 mock_func.assert_called_with(conf, conf.app_dir) @patch('couchapp.dispatch.commands') @patch('couchapp.dispatch.Config') def test__dispatch_update_commands(conf, commands): conf = conf() mock_func = Mock(return_value=10) mock_mod = Mock() mock_mod.cmdtable = {'mock': (mock_func, [], 'just for testing')} conf.extensions = [mock_mod] commands.table = {} commands.globalopts = globalopts assert dispatch._dispatch(['mock']) == 10 assert commands.table == mock_mod.cmdtable assert mock_func.called @patch('couchapp.dispatch.logger') @patch('couchapp.dispatch._dispatch') def test_dispatch_AppError(_dispatch, logger): args = ['strange'] _dispatch.side_effect = AppError('some error') assert dispatch.dispatch(args) == -1 _dispatch.assert_called_with(args) @patch('couchapp.dispatch.logger') @patch('couchapp.dispatch._dispatch') def test_dispatch_CLIError(_dispatch, logger): ''' Test case for CommandLineError ''' args = ['strange'] _dispatch.side_effect = CommandLineError('some error') assert dispatch.dispatch(args) == -1 _dispatch.assert_called_with(args) @patch('couchapp.dispatch.logger') @patch('couchapp.dispatch._dispatch') def test_dispatch_KeyboardInterrupt(_dispatch, logger): ''' Test case for KeyboardInterrupt ''' args = ['strange'] _dispatch.side_effect = KeyboardInterrupt() assert dispatch.dispatch(args) == -1 _dispatch.assert_called_with(args) @patch('couchapp.dispatch.logger') @patch('couchapp.dispatch._dispatch') def test_dispatch_other_error(_dispatch, logger): ''' Test case for general Exception ''' args = ['strange'] _dispatch.side_effect = Exception() assert dispatch.dispatch(args) == -1 _dispatch.assert_called_with(args) tests/test_ignores.py000066400000000000000000000047741276277602300153270ustar00rootroot00000000000000# -*- 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_util.py000066400000000000000000000061631276277602300146300ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os from couchapp.util import discover_apps, iscouchapp, rcpath, split_path from mock import patch @patch('couchapp.util.user_rcpath') @patch('couchapp.util._rcpath') def test_rcpath_cached(_rcpath, user_rcpath): ''' Global ``_rcpath`` is not None already ''' assert _rcpath == rcpath() assert not user_rcpath.called @patch.dict('couchapp.util.os.environ', clear=True) @patch('couchapp.util.user_rcpath', return_value=['/mock/couchapp.conf']) def test_rcpath_empty_env(user_rcpath): ''' Empty ``COUCHAPPCONF_PATH`` ''' import couchapp.util as util util._rcpath = None assert rcpath() == ['/mock/couchapp.conf'], rcpath() assert user_rcpath.called @patch.dict('couchapp.util.os.environ', {'COUCHAPPCONF_PATH': '/mock:/tmp/fake.conf'}) @patch('couchapp.util.os.listdir') @patch('couchapp.util.os.path.isdir') @patch('couchapp.util.user_rcpath') def test_rcpath_env(user_rcpath, isdir, listdir): ''' With ``COUCHAPPCONF_PATH`` set ''' import couchapp.util as util util._rcpath = None def _isdir(path): return True if path == '/mock' else False def _listdir(path): return ['foo', 'bar', 'couchapp.conf'] if path == '/mock' else [] isdir.side_effect = _isdir listdir.side_effect = _listdir assert rcpath() == ['/mock/couchapp.conf', '/tmp/fake.conf'], rcpath() assert not user_rcpath.called @patch('couchapp.util.os.path.isfile', return_value=True) def test_iscouchapp(isfile): assert iscouchapp('/mock_dir') == True isfile.assert_called_with('/mock_dir/.couchapprc') @patch('couchapp.util.os.listdir', return_value=['foo']) @patch('couchapp.util.os.path.isdir', return_value=True) @patch('couchapp.util.iscouchapp', return_value=True) def test_discover_apps(iscouchapp_, isdir, listdir): assert discover_apps('/mock_dir') == ['/mock_dir/foo'] isdir.assert_called_with('/mock_dir/foo') listdir.assert_called_with('/mock_dir') @patch('couchapp.util.os.listdir', return_value=['foo']) @patch('couchapp.util.os.path.isdir', return_value=True) @patch('couchapp.util.iscouchapp', return_value=True) def test_discover_apps_relative_path(iscouchapp_, isdir, listdir): assert discover_apps('mock_dir') == ['mock_dir/foo'] isdir.assert_called_with('mock_dir/foo') listdir.assert_called_with('mock_dir') @patch('couchapp.util.os.listdir', return_value=['.git', 'foo']) @patch('couchapp.util.os.path.isdir', return_value=True) @patch('couchapp.util.iscouchapp', return_value=True) def test_discover_apps_hidden_file(iscouchapp_, isdir, listdir): ''' Test case for a dir including hidden file ''' assert discover_apps('mock_dir') == ['mock_dir/foo'] isdir.assert_called_with('mock_dir/foo') listdir.assert_called_with('mock_dir') def test_split_path_rel(): ''' Test case for util.split_path with relative path ''' assert split_path('foo/bar') == ['foo', 'bar'] def test_split_path_abs(): ''' Test case for util.split_path with abs path ''' path = os.path.realpath('/foo/bar') assert split_path(path) == [os.path.realpath('/foo'), 'bar'] tests/testapp/000077500000000000000000000000001276277602300137145ustar00rootroot00000000000000tests/testapp/.couchapprc000066400000000000000000000000031276277602300160350ustar00rootroot00000000000000{} tests/testapp/_attachments/000077500000000000000000000000001276277602300163665ustar00rootroot00000000000000tests/testapp/_attachments/index.html000066400000000000000000000016121276277602300203630ustar00rootroot00000000000000 Generated CouchApp

    Generated CouchApp

      tests/testapp/_attachments/style/000077500000000000000000000000001276277602300175265ustar00rootroot00000000000000tests/testapp/_attachments/style/main.css000066400000000000000000000000251276277602300211610ustar00rootroot00000000000000/* add styles here */tests/testapp/_docs/000077500000000000000000000000001276277602300150035ustar00rootroot00000000000000tests/testapp/_docs/.hideme/000077500000000000000000000000001276277602300163145ustar00rootroot00000000000000tests/testapp/_docs/.hideme/keep000066400000000000000000000000001276277602300171510ustar00rootroot00000000000000tests/testapp/_docs/test.json000066400000000000000000000000511276277602300166510ustar00rootroot00000000000000{ "_id": "test_doc", "test": "ing" } tests/testapp/foo/000077500000000000000000000000001276277602300144775ustar00rootroot00000000000000tests/testapp/foo/bar.txt000066400000000000000000000016651276277602300160140ustar00rootroot00000000000000Couchapp 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/lib/000077500000000000000000000000001276277602300144625ustar00rootroot00000000000000tests/testapp/lib/helpers/000077500000000000000000000000001276277602300161245ustar00rootroot00000000000000tests/testapp/lib/helpers/foo.js000066400000000000000000000001001276277602300172340ustar00rootroot00000000000000// apply this macro recursiveliy // !code lib/helpers/foo_rec.jstests/testapp/lib/helpers/foo_rec.js000066400000000000000000000000761276277602300201010ustar00rootroot00000000000000// library included by foo.js applying !code macro recursivelytests/testapp/lib/helpers/math.js000066400000000000000000000001101276277602300174030ustar00rootroot00000000000000// this is just a placeholder for example purposes function stddev() {};tests/testapp/lib/helpers/template.js000066400000000000000000000021011276277602300202670ustar00rootroot00000000000000// 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/templates/000077500000000000000000000000001276277602300164605ustar00rootroot00000000000000tests/testapp/lib/templates/example.html000066400000000000000000000014551276277602300210060ustar00rootroot00000000000000 Generated CouchApp Form Template

      <% doc.title %>

      tests/testapp/lists/000077500000000000000000000000001276277602300150525ustar00rootroot00000000000000tests/testapp/lists/feed.js000066400000000000000000000015361276277602300163200ustar00rootroot00000000000000function(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/shows/000077500000000000000000000000001276277602300150575ustar00rootroot00000000000000tests/testapp/shows/example-show.js000066400000000000000000000005451276277602300200320ustar00rootroot00000000000000function(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/views/000077500000000000000000000000001276277602300150515ustar00rootroot00000000000000tests/testapp/views/example/000077500000000000000000000000001276277602300165045ustar00rootroot00000000000000tests/testapp/views/example/map.js000066400000000000000000000003141276277602300176150ustar00rootroot00000000000000// 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.js000066400000000000000000000003111276277602300203040ustar00rootroot00000000000000// 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.js000066400000000000000000000000001276277602300165310ustar00rootroot00000000000000