chaussette-1.3.0/000077500000000000000000000000001253327544200136725ustar00rootroot00000000000000chaussette-1.3.0/.coveragerc000066400000000000000000000001561253327544200160150ustar00rootroot00000000000000[run] omit = *_patch*,chaussette/tests/* source = chaussette include = chaussette/* [html] directory = html chaussette-1.3.0/.gitignore000066400000000000000000000001311253327544200156550ustar00rootroot00000000000000*.pyc lib include bin *.un~ *.egg-info .Python .tox build html .coverage *.egg *.swp man chaussette-1.3.0/.travis.yml000066400000000000000000000006151253327544200160050ustar00rootroot00000000000000language: python python: - "2.7" before_install: - sudo apt-get install -y libevent-dev libev-dev install: - python setup.py develop - pip install tox script: tox notifications: email: tarek@mozilla.com irc: "irc.freenode.org#mozilla-circus" on_success: change after_success: # Report coverage results to coveralls.io - pip install coveralls coverage - coveralls chaussette-1.3.0/LICENSE000066400000000000000000000010621253327544200146760ustar00rootroot00000000000000Copyright 2012 - Mozilla Foundation 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. chaussette-1.3.0/MANIFEST.in000066400000000000000000000002041253327544200154240ustar00rootroot00000000000000include README.rst include THANKS.rst include LICENSE include Makefile recursive-include examples *.py *.txt *.css *.js *.swf *.htmlchaussette-1.3.0/Makefile000066400000000000000000000006751253327544200153420ustar00rootroot00000000000000.PHONY: docs test BIN = `pwd`/bin ifndef VTENV_OPTS VTENV_OPTS = "--no-site-packages" endif test: $(BIN)/pip install tox $(BIN)/tox bin/python: virtualenv $(VTENV_OPTS) . $(BIN)/python setup.py develop docs: bin/python $(BIN)/pip install sphinx $(BIN)/sphinx-build -b html -d docs/build/doctrees docs/source build/html upload: bin/python $(BIN)/pip install wheel $(BIN)/python setup.py sdist --formats=zip,gztar bdist_wheel upload chaussette-1.3.0/README.rst000066400000000000000000000044271253327544200153700ustar00rootroot00000000000000Chaussette ========== Chaussette is a WSGI server. The particularity of Chaussette is that it can either bind a socket on a port like any other server does or run against already opened sockets. That makes Chaussette the best companion to run a WSGI or Django_ stack under a process and socket manager, such as Circus_. .. image:: https://travis-ci.org/circus-tent/chaussette.svg?branch=master :alt: Build Status :target: https://secure.travis-ci.org/circus-tent/chaussette/ .. image:: https://coveralls.io/repos/circus-tent/chaussette/badge.png?branch=master :alt: Coverage Status on master :target: https://coveralls.io/r/circus-tent/chaussette?branch=master Quick Start ----------- Running: .. code-block:: bash chaussette starts a very simple HTTP sample server on port 8080. Starting a WSGI application using chaussette is simply a matter of calling: .. code-block:: bash chaussette examples.tornadoapp.wsgiapp Chaussette can also serve tornado (non WSGI) application: .. code-block:: bash chaussette --backend tornado examples.tornadoapp.tornadoapp The `simple_chat` example can be started as: .. code-block:: bash chaussette --backend socketio examples.simple_chat.chat.app Note that the two previous examples are not backend agnostic, since they are not (pure) WSGI applications. A flask_ based pure WSGI application can be started with most backends: .. code-block:: bash chaussette --backend gevent examples.flaskapp.app In these examples, we start a standalone WSGI server, but the spirit of chaussette is to be managed by Circus_, as described http://chaussette.readthedocs.org/en/latest/#using-chaussette-in-circus Links ----- - The full documentation is located at: http://chaussette.readthedocs.org - You can reach us for any feedback, bug report, or to contribute, at https://github.com/circus-tent/chaussette .. _Circus: http://circus.readthedocs.org .. _Django: https://docs.djangoproject.com .. _flask: http://flask.pocoo.org/ Changelog --------- 1.3.0 - 2015-06-01 ~~~~~~~~~~~~~~~~~~ - Fix gevent monkey patching (pull request #67). - Add a "--graceful-timeout" option (for gevent-based backends). - Fix the tornado backend so that it accepts tornado's WSGIApplication instaces. - Update documentation. - Improve example applications. chaussette-1.3.0/THANKS.rst000066400000000000000000000004331253327544200154140ustar00rootroot00000000000000Chaussette was created by Tarek Ziade. It is maintained by Tarek Ziade and David Douard. Here's a list of other contributors: - Myoung-su Shin - Trey Long - Jannis Leidel - Danilo Maurizio - Emmanuel Raviart - Pedro Romano - Victor Fernandez de Alba - Gilles Devaux - INADA Naoki chaussette-1.3.0/chaussette/000077500000000000000000000000001253327544200160425ustar00rootroot00000000000000chaussette-1.3.0/chaussette/__init__.py000066400000000000000000000001201253327544200201440ustar00rootroot00000000000000import logging logger = logging.getLogger('chaussette') __version__ = '1.3.0' chaussette-1.3.0/chaussette/_paste.py000066400000000000000000000012471253327544200176730ustar00rootroot00000000000000import os from six.moves import configparser from logging.config import fileConfig try: from paste.deploy import loadapp except ImportError: raise ImportError("You need to install PasteDeploy") def paste_app(path): # Load the logging config from paste.deploy .ini, if any path, hsh, app = path.partition('#') parser = configparser.ConfigParser() parser.read([path]) if parser.has_section('loggers'): config_file = os.path.abspath(path) fileConfig( config_file, dict(__file__=config_file, here=os.path.dirname(config_file)) ) return loadapp('config:%s%s%s' % (os.path.abspath(path), hsh, app)) chaussette-1.3.0/chaussette/backend/000077500000000000000000000000001253327544200174315ustar00rootroot00000000000000chaussette-1.3.0/chaussette/backend/__init__.py000066400000000000000000000040361253327544200215450ustar00rootroot00000000000000import sys from chaussette.backend import _wsgiref # make sure OrderedDict is available in the collections package, # this is required for python 2.6 try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict import collections collections.OrderedDict = OrderedDict _backends = {'wsgiref': _wsgiref.ChaussetteServer} try: from chaussette.backend import _waitress _backends['waitress'] = _waitress.Server except ImportError: pass try: from chaussette.backend import _meinheld _backends['meinheld'] = _meinheld.Server except ImportError: pass try: from chaussette.backend import _tornado _backends['tornado'] = _tornado.Server except ImportError: pass PY3 = sys.version_info[0] == 3 if not PY3: try: from chaussette.backend import _gevent _backends['gevent'] = _gevent.Server from chaussette.backend import _fastgevent _backends['fastgevent'] = _fastgevent.Server except ImportError: pass try: from chaussette.backend import _geventwebsocket _backends['geventwebsocket'] = _geventwebsocket.Server except ImportError: pass try: from chaussette.backend import _geventws4py _backends['geventws4py'] = _geventws4py.Server except ImportError: pass try: from chaussette.backend import _eventlet _backends['eventlet'] = _eventlet.Server except ImportError: pass try: from chaussette.backend import _socketio _backends['socketio'] = _socketio.Server except ImportError: pass try: from chaussette.backend import _bjoern _backends['bjoern'] = _bjoern.Server except ImportError: pass def register(name, server): _backends[name] = server def get(name): return _backends[name] def backends(): return sorted(_backends.keys()) def is_gevent_backend(backend): return backend in ('gevent', 'fastgevent', 'geventwebsocket', 'geventws4py') chaussette-1.3.0/chaussette/backend/_bjoern.py000066400000000000000000000010501253327544200214150ustar00rootroot00000000000000import socket from chaussette.util import create_socket import bjoern class Server(object): def __init__(self, listener, application=None, backlog=None, socket_type=None, address_family=None): host, port = listener self.app = application self.sock = create_socket(host, port, address_family, socket_type, backlog=backlog) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) def serve_forever(self): bjoern.server_run(self.sock, self.app) chaussette-1.3.0/chaussette/backend/_eventlet.py000066400000000000000000000015071253327544200217730ustar00rootroot00000000000000import socket import eventlet from eventlet import wsgi from chaussette.util import create_socket class Server(object): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM def __init__(self, listener, application=None, backlog=2048, socket_type=socket.SOCK_STREAM, address_family=socket.AF_INET): self.address_family = address_family self.socket_type = socket_type eventlet.monkey_patch() host, port = listener self.socket = create_socket(host, port, self.address_family, self.socket_type, backlog=backlog) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.application = application def serve_forever(self): wsgi.server(self.socket, self.application) chaussette-1.3.0/chaussette/backend/_fastgevent.py000066400000000000000000000021751253327544200223150ustar00rootroot00000000000000import socket from gevent.wsgi import WSGIServer import signal from gevent import signal as gevent_signal from chaussette.util import create_socket class Server(WSGIServer): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM def __init__(self, listener, application=None, backlog=None, spawn='default', log='default', handler_class=None, environ=None, socket_type=socket.SOCK_STREAM, address_family=socket.AF_INET, graceful_timeout=None, **ssl_args): host, port = listener self.socket = create_socket(host, port, self.address_family, self.socket_type, backlog=backlog) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_address = self.socket.getsockname() if graceful_timeout is not None: self.stop_timeout = graceful_timeout gevent_signal(signal.SIGTERM, self.stop) super(Server, self).__init__(self.socket, application, None, spawn, log, handler_class, environ, **ssl_args) chaussette-1.3.0/chaussette/backend/_gevent.py000066400000000000000000000032051253327544200214320ustar00rootroot00000000000000import socket from gevent.pywsgi import WSGIServer, WSGIHandler from chaussette.util import create_socket import signal from gevent import signal as gevent_signal class CustomWSGIHandler(WSGIHandler): def __init__(self, sock, address, server, rfile=None): if server.address_family == socket.AF_UNIX: address = ['0.0.0.0'] WSGIHandler.__init__(self, sock, address, server, rfile) class Server(WSGIServer): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM handler_class = CustomWSGIHandler def __init__(self, listener, application=None, backlog=None, spawn='default', log='default', handler_class=None, environ=None, socket_type=None, address_family=None, graceful_timeout=None, **ssl_args): if address_family: self.address_family = address_family if socket_type: self.socket_type = socket_type if handler_class: self.handler_class = handler_class host, port = listener self.socket = create_socket(host, port, self.address_family, self.socket_type, backlog=backlog) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_address = self.socket.getsockname() if graceful_timeout is not None: self.stop_timeout = graceful_timeout gevent_signal(signal.SIGTERM, self.stop) super(Server, self).__init__(self.socket, application, None, spawn, log, self.handler_class, environ, **ssl_args) chaussette-1.3.0/chaussette/backend/_geventwebsocket.py000066400000000000000000000024661253327544200233510ustar00rootroot00000000000000import socket from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler from chaussette.util import create_socket import signal from gevent import signal as gevent_signal class Server(WSGIServer): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM def __init__(self, listener, application=None, backlog=None, spawn='default', log='default', handler_class=None, environ=None, socket_type=socket.SOCK_STREAM, address_family=socket.AF_INET, graceful_timeout=None, **ssl_args): self.address_family = address_family self.socket_type = socket_type host, port = listener self.handler_class = WebSocketHandler self.socket = create_socket(host, port, self.address_family, self.socket_type, backlog=backlog) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_address = self.socket.getsockname() if graceful_timeout is not None: self.stop_timeout = graceful_timeout gevent_signal(signal.SIGTERM, self.stop) super(Server, self).__init__(self.socket, application, None, spawn, log, handler_class, environ, **ssl_args) chaussette-1.3.0/chaussette/backend/_geventws4py.py000066400000000000000000000010211253327544200224330ustar00rootroot00000000000000from ws4py.server.geventserver import (WebSocketWSGIHandler, GEventWebSocketPool) from chaussette.backend._gevent import Server as GeventServer class Server(GeventServer): handler_class = WebSocketWSGIHandler def __init__(self, *args, **kwargs): GeventServer.__init__(self, *args, **kwargs) self.pool = GEventWebSocketPool() def stop(self, *args, **kwargs): self.pool.clear() self.pool = None GeventServer.stop(self, *args, **kwargs) chaussette-1.3.0/chaussette/backend/_meinheld.py000066400000000000000000000017711253327544200217350ustar00rootroot00000000000000import os import socket from meinheld import server class Server(object): def __init__(self, listener, application=None, backlog=2048, socket_type=socket.SOCK_STREAM, address_family=socket.AF_INET): self.address_family = address_family self.socket_type = socket_type from meinheld import patch patch.patch_all() server.set_backlog(backlog) host, port = listener if host.startswith('fd://'): fd = int(host.split('://')[1]) server.set_listen_socket(fd) else: if self.address_family == socket.AF_UNIX: filename = listener[0][len('unix:'):] try: os.remove(filename) except OSError: pass server.listen(filename) else: server.listen(listener) self.application = application def serve_forever(self): server.run(self.application) chaussette-1.3.0/chaussette/backend/_socketio.py000066400000000000000000000035341253327544200217670ustar00rootroot00000000000000import socket from chaussette.util import create_socket from socketio.server import SocketIOServer from socketio.handler import SocketIOHandler from socketio.policyserver import FlashPolicyServer class _SocketIOHandler(SocketIOHandler): def __init__(self, config, sock, address, server, rfile=None): if server.socket_type == socket.AF_UNIX: address = ['0.0.0.0'] SocketIOHandler.__init__(self, config, sock, address, server, rfile) class Server(SocketIOServer): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM handler_class = _SocketIOHandler def __init__(self, *args, **kwargs): address_family = kwargs.pop('address_family', socket.AF_INET) socket_type = kwargs.pop('socket_type', socket.SOCK_STREAM) backlog = kwargs.pop('backlog', 2048) listener = args[0] if isinstance(listener, tuple): host, port = listener _socket = create_socket(host, port, address_family, socket_type, backlog=backlog) _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) args = [_socket] + list(args[1:]) else: # it's already a socket.. host, port = listener.getsockname() # socketio makes the assumption that listener is a host/port # tuple, which is a false assumption, it can be a socket. # it uses listener in its constructor to set the policy server # # Let's set it ourselves here afterwards. old_policy_server = kwargs.pop('policy_server', True) kwargs['policy_server'] = False super(Server, self).__init__(*args, **kwargs) if old_policy_server: policylistener = kwargs.pop('policy_listener', (host, 10843)) self.policy_server = FlashPolicyServer(policylistener) chaussette-1.3.0/chaussette/backend/_tornado.py000066400000000000000000000034201253327544200216070ustar00rootroot00000000000000import socket import tornado.ioloop import tornado.netutil import tornado.wsgi from tornado.platform.auto import set_close_exec from tornado.web import Application from tornado.tcpserver import TCPServer from tornado.httpserver import HTTPServer from tornado.wsgi import WSGIApplication class Server(object): def __init__(self, listener, application=None, backlog=2048, socket_type=socket.SOCK_STREAM, address_family=socket.AF_INET): self.address_family = address_family self.socket_type = socket_type host, port = listener if (isinstance(application, Application) and not isinstance(application, WSGIApplication)): self._server = HTTPServer(application) elif isinstance(application, TCPServer): self._server = application elif callable(application): tapp = tornado.wsgi.WSGIContainer(application) self._server = HTTPServer(tapp) else: raise TypeError( "Unsupported application type: %r" % (application,)) if host.startswith('fd://'): fd = int(host.split('://')[1]) set_close_exec(fd) sock = socket.fromfd(fd, address_family, socket_type) sock.setblocking(0) socks = [sock] elif self.address_family == socket.AF_UNIX: filename = host[len('unix:'):] sock = tornado.netutil.bind_unix_socket(filename, backlog=backlog) socks = [sock] else: socks = tornado.netutil.bind_sockets( port, host, address_family, backlog) self._server.add_sockets(socks) self.application = application def serve_forever(self): tornado.ioloop.IOLoop.instance().start() chaussette-1.3.0/chaussette/backend/_waitress.py000066400000000000000000000023771253327544200220140ustar00rootroot00000000000000import socket from waitress.server import WSGIServer class Server(WSGIServer): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM def __init__(self, listener, application=None, backlog=2048, socket_type=socket.SOCK_STREAM, address_family=socket.AF_INET): host, port = listener if host.startswith('fd://'): self._fd = int(host.split('://')[1]) else: self._fd = None self._chaussette_family_and_type = address_family, socket_type super(Server, self).__init__(application, backlog=backlog, host=host, port=port) def create_socket(self, family, type): # Ignore parameters passed by waitress to use chaussette options family, type = self._chaussette_family_and_type self.family_and_type = family, type if self._fd is None: sock = socket.socket(family, type) else: sock = socket.fromfd(self._fd, family, type) sock.setblocking(0) self.set_socket(sock) def bind(self, listener): if self._fd is not None: return super(Server, self).bind(listener) def serve_forever(self): return self.run() chaussette-1.3.0/chaussette/backend/_wsgiref.py000066400000000000000000000043551253327544200216170ustar00rootroot00000000000000import socket from wsgiref.simple_server import WSGIServer, WSGIRequestHandler from six.moves import socketserver from chaussette.util import create_socket class ChaussetteHandler(WSGIRequestHandler): def __init__(self, sock, client_addr, server): if server.socket_type == socket.AF_UNIX: client_addr = ['0.0.0.0'] WSGIRequestHandler.__init__(self, sock, client_addr, server) def address_string(self): if self.server.byfd or self.server.socket_type == socket.AF_UNIX: return '0.0.0.0' return WSGIRequestHandler.address_string(self) class ChaussetteServer(WSGIServer): """WSGI Server that can reuse an existing open socket. """ handler_class = ChaussetteHandler def __init__(self, server_address, app, bind_and_activate=True, backlog=2048, socket_type=socket.SOCK_STREAM, address_family=socket.AF_INET): self.address_family = address_family self.socket_type = socket_type socketserver.BaseServer.__init__(self, server_address, self.handler_class) self.set_app(app) host, port = self.server_address = server_address self.socket = create_socket(host, port, family=self.address_family, type=self.socket_type, backlog=backlog) self.byfd = host.startswith('fd://') if bind_and_activate: self.server_bind() self.server_activate() def server_activate(self): if self.byfd: return self.socket.listen(self.request_queue_size) def server_bind(self): if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if not self.byfd: self.server_address = self.socket.getsockname() host, port = self.socket.getsockname()[:2] self.server_name = socket.getfqdn(host) self.server_port = port else: # XXX see how to get the fqnd with the fd self.server_name = self.server_address[0] self.server_port = self.server_address[1] self.setup_environ() chaussette-1.3.0/chaussette/server.py000066400000000000000000000201061253327544200177210ustar00rootroot00000000000000import sys import os import argparse import itertools import socket from chaussette import logger as chaussette_logger from chaussette.util import import_string, configure_logger, LOG_LEVELS from chaussette.backend import get, backends, is_gevent_backend def make_server(app, host=None, port=None, backend='wsgiref', backlog=2048, spawn=None, logger=None, address_family=socket.AF_INET, socket_type=socket.SOCK_STREAM, graceful_timeout=None): logger = logger or chaussette_logger logger.info('Application is %r' % app) if host.startswith('fd://') or host.startswith('unix:'): logger.info('Serving on %s' % host) else: logger.info('Serving on %s:%s' % (host, port)) server_class = get(backend) logger.info('Using %r as a backend' % server_class) if spawn: logger.info('Spawning method: %r' % spawn) server_class_kwargs = { 'backlog': backlog, 'address_family': address_family, 'socket_type': socket_type, } if spawn is not None: server_class_kwargs['spawn'] = spawn if graceful_timeout is not None: server_class_kwargs['graceful_timeout'] = graceful_timeout try: server = server_class((host, port), app, **server_class_kwargs) except TypeError: logger.exception("Failed to create backend %s, you might be trying " "to use --spawn on a backend that does " "not support it" % backend) raise return server _ADDRESS_FAMILY = { 'AF_UNIX': socket.AF_UNIX, 'AF_INET': socket.AF_INET, 'AF_INET6': socket.AF_INET6 } _SOCKET_TYPE = { 'SOCK_STREAM': socket.SOCK_STREAM, 'SOCK_DGRAM': socket.SOCK_DGRAM, 'SOCK_RAW': socket.SOCK_RAW, 'SOCK_RDM': socket.SOCK_RDM, 'SOCK_SEQPACKET': socket.SOCK_SEQPACKET } _NO_UNIX = ('waitress', 'fastgevent', 'eventlet') _SUPPORTS_GRACEFUL_TIMEOUT = ('gevent', 'fastgevent', 'geventwebsocket') def serve_paste(app, global_conf, **kw): port = int(kw.get('port', 8080)) host = kw.get('host', '127.0.0.1') backlog = int(kw.get('backlog', 2048)) backend = kw.get('backend', 'wsgiref') address_family = kw.get('address_family', 'AF_INET') address_family = _ADDRESS_FAMILY[address_family] socket_type = kw.get('socket_type', 'SOCK_STREAM') socket_type = _SOCKET_TYPE[socket_type] loglevel = kw.get('log_level', 'info') logoutput = kw.get('log_output', '-') logger = chaussette_logger configure_logger(logger, loglevel, logoutput) if address_family == socket.AF_UNIX and backend in _NO_UNIX: logger.info('Sorry %r does not support unix sockets' % backend) sys.exit(0) pre_hook = kw.get('pre_hook') if pre_hook is not None: pre_hook = import_string(pre_hook) logger.info('Running the pre-hook %r' % pre_hook) pre_hook(kw) post_hook = kw.get('post_hook') if post_hook is not None: post_hook = import_string(post_hook) try: httpd = make_server(app, host=host, port=port, backend=backend, backlog=backlog, logger=logger, address_family=address_family, socket_type=socket_type) try: httpd.serve_forever() except KeyboardInterrupt: sys.exit(0) finally: if post_hook is not None: logger.info('Running the post-hook %r' % post_hook) post_hook(kw) return 0 def main(): sys.path.append(os.curdir) parser = argparse.ArgumentParser(description='Run some watchers.') parser.add_argument('--port', type=int, default=8080) parser.add_argument('--address-family', type=str, default='AF_INET', choices=sorted(_ADDRESS_FAMILY.keys())) parser.add_argument('--socket-type', type=str, default='SOCK_STREAM', choices=sorted(_SOCKET_TYPE.keys())) group = parser.add_mutually_exclusive_group() group.add_argument('--host', default='localhost') group.add_argument('--fd', type=int, default=-1) group.add_argument('--backlog', type=int, default=2048) parser.add_argument('--backend', type=str, default='wsgiref', choices=backends()) parser.add_argument('--use-reloader', action='store_true', help="Restart server when source files change") parser.add_argument('--spawn', type=int, default=None, help="Spawn type, only makes sense if the backend " "supports it (gevent)") parser.add_argument('--graceful-timeout', type=int, default=None, help="Graceful shutdown timeout for existing requests " "to complete, only for backends that support it " "(%s)" % ', '.join(_SUPPORTS_GRACEFUL_TIMEOUT)) parser.add_argument('application', default='chaussette.util.hello_app', nargs='?') parser.add_argument('arguments', default=[], nargs='*', help="Optional arguments you may need for your app") parser.add_argument('--pre-hook', type=str, default=None) parser.add_argument('--post-hook', type=str, default=None) parser.add_argument('--python-path', type=str, default=None) log_levels = itertools.chain.from_iterable((key.upper(), key) for key in LOG_LEVELS.keys()) parser.add_argument('--log-level', dest='loglevel', default='info', choices=sorted(log_levels), help="log level") parser.add_argument('--log-output', dest='logoutput', default='-', help="log output") args = parser.parse_args() if is_gevent_backend(args.backend): from gevent import monkey monkey.noisy = False monkey.patch_all() application = args.application logger = chaussette_logger configure_logger(logger, args.loglevel, args.logoutput) if args.graceful_timeout is not None and \ args.backend not in _SUPPORTS_GRACEFUL_TIMEOUT: logger.info('Sorry %r does not support --graceful_timeout' % args.backend) sys.exit(0) if application.startswith('paste:'): from chaussette._paste import paste_app app = paste_app(application.split(':')[-1]) else: app = import_string(application) if args.fd != -1: host = 'fd://%d' % args.fd else: host = args.host # pre-hook ? if args.pre_hook is not None: pre_hook = import_string(args.pre_hook) logger.info('Running the pre-hook %r' % pre_hook) pre_hook(args) # post-hook ? if args.post_hook is not None: post_hook = import_string(args.post_hook) else: post_hook = None address_family = _ADDRESS_FAMILY[args.address_family] if address_family == socket.AF_UNIX and args.backend in _NO_UNIX: logger.info('Sorry %r does not support unix sockets' % args.backend) sys.exit(0) def inner(): try: httpd = make_server(app, host=host, port=args.port, backend=args.backend, backlog=args.backlog, spawn=args.spawn, graceful_timeout=args.graceful_timeout, logger=logger, address_family=address_family, socket_type=_SOCKET_TYPE[args.socket_type]) try: httpd.serve_forever() except KeyboardInterrupt: sys.exit(0) finally: if post_hook is not None: logger.info('Running the post-hook %r' % post_hook) post_hook(args) if args.use_reloader: try: from werkzeug.serving import run_with_reloader except ImportError: logger.info("Reloader requires Werkzeug: " "'pip install werkzeug'") sys.exit(0) run_with_reloader(inner) else: inner() if __name__ == '__main__': main() chaussette-1.3.0/chaussette/tests/000077500000000000000000000000001253327544200172045ustar00rootroot00000000000000chaussette-1.3.0/chaussette/tests/__init__.py000066400000000000000000000000441253327544200213130ustar00rootroot00000000000000# coding=utf-8 """ Test package """ chaussette-1.3.0/chaussette/tests/support.py000066400000000000000000000016501253327544200212740ustar00rootroot00000000000000try: from StringIO import StringIO except ImportError: from io import StringIO import functools import sys from chaussette import logger def hush(func): """Make the passed function silent.""" @functools.wraps(func) def _silent(*args, **kw): old_stdout = sys.stdout old_stderr = sys.stderr sys.stdout = StringIO() sys.stderr = StringIO() debug = [] def _debug(msg): debug.append(str(msg)) old_debug = logger.debug logger.debug = _debug try: return func(*args, **kw) except: sys.stdout.seek(0) print(sys.stdout.read()) sys.stderr.seek(0) print(sys.stderr.read()) print('\n'.join(debug)) raise finally: sys.stdout = old_stdout sys.stderr = old_stderr logger.debug = old_debug return _silent chaussette-1.3.0/chaussette/tests/test_backend.py000066400000000000000000000013771253327544200222140ustar00rootroot00000000000000try: import unittest2 as unittest except ImportError: import unittest import sys from chaussette.backend import backends IS_PYPY = hasattr(sys, 'pypy_version_info') PY2 = ['bjoern', 'eventlet', 'fastgevent', 'gevent', 'geventwebsocket', 'geventws4py', 'meinheld', 'socketio', 'tornado', 'waitress', 'wsgiref'] PYPY = ['tornado', 'waitress', 'wsgiref'] PY3 = ['meinheld', 'tornado', 'waitress', 'wsgiref'] class TestBackend(unittest.TestCase): def test_backends(self): _backends = backends() if sys.version_info[0] == 2: if IS_PYPY: expected = PYPY else: expected = PY2 else: expected = PY3 self.assertEqual(_backends, expected) chaussette-1.3.0/chaussette/tests/test_paste.py000066400000000000000000000015341253327544200217340ustar00rootroot00000000000000try: import unittest2 as unittest except ImportError: import unittest import os import sys from tempfile import mkstemp INI = """\ [composite:main] use = egg:Paste#urlmap / = home [app:home] use = egg:Paste#static document_root = %(here)s/htdocs """ @unittest.skipIf(sys.version_info[0] == 3, "Only Python 2") class TestPasteApp(unittest.TestCase): def setUp(self): self.files = [] def tearDown(self): for file_ in self.files: os.remove(file_) def _get_file(self): fd, path = mkstemp() os.close(fd) self.files.append(path) return path def test_app(self): from chaussette._paste import paste_app path = self._get_file() with open(path, 'w') as f: f.write(INI) app = paste_app(path) self.assertEqual(len(app), 1) chaussette-1.3.0/chaussette/tests/test_server.py000066400000000000000000000122101253327544200221170ustar00rootroot00000000000000# coding=utf-8 """ Tests for server.py """ import subprocess import sys import time try: import unittest2 as unittest except ImportError: import unittest import minimock import requests import socket from chaussette.backend import backends import chaussette.server from chaussette.util import configure_logger from chaussette import logger from chaussette.tests.support import hush @unittest.skipIf(sys.version_info[0] == 3, "Not py3") class TestServer(unittest.TestCase): """ Test server.py """ def setUp(self): """ Setup :return: """ super(TestServer, self).setUp() configure_logger(logger, 'CRITICAL') self.tt = minimock.TraceTracker() try: self.old = socket.socket.bind socket.socket.bind = lambda x, y: None except AttributeError: self.old = None def tearDown(self): """ tearDown :return: """ super(TestServer, self).tearDown() minimock.restore() if self.old is not None: socket.socket.bind = self.old def test_make_server(self): """ Test all backends with default params :return: """ # nose does not have great support for parameterized tests for backend in backends(): self.tt = minimock.TraceTracker() self._check_make_server(backend) minimock.restore() def _check_make_server(self, backend): mocked_backend = minimock.Mock('Backend', returns='backend_impl', tracker=self.tt) minimock.mock('chaussette.server.get', returns=mocked_backend, tracker=self.tt) server = chaussette.server.make_server('app', 'host', 'port', backend) minimock.assert_same_trace(self.tt, '\n'.join([ "Called chaussette.server.get('%s')" % backend, "Called Backend(", " ('host', 'port'),", " 'app',", " address_family=2,", " backlog=2048,", "socket_type=1)" ])) self.assertEqual(server, 'backend_impl') def test_make_server_spawn(self): """ Check the spawn option for the backend that support it :return: """ for backend in ['gevent', 'fastgevent', 'geventwebsocket', 'socketio']: self.tt = minimock.TraceTracker() self._check_make_server_spawn(backend) minimock.restore() def _check_make_server_spawn(self, backend): mocked_backend = minimock.Mock('Backend', returns='backend_impl', tracker=self.tt) minimock.mock('chaussette.server.get', returns=mocked_backend, tracker=self.tt) server = chaussette.server.make_server('app', 'host', 'port', backend, spawn=5) minimock.assert_same_trace(self.tt, '\n'.join([ "Called chaussette.server.get('%s')" % backend, "Called Backend(", " ('host', 'port'),", " 'app',", " address_family=2,", " backlog=2048,", " socket_type=1,", " spawn=5)" ])) self.assertEqual(server, 'backend_impl') def test_make_server_spawn_fail(self): """ Check the spawn option for a backend that does not support it :return: """ self.assertRaises(TypeError, chaussette.server.make_server, 'app', 'host', 'port', spawn=5) class TestMain(unittest.TestCase): """ Test server.py """ def setUp(self): super(TestMain, self).setUp() self.argv = list(sys.argv) configure_logger(logger, 'CRITICAL') def tearDown(self): super(TestMain, self).tearDown() sys.argv[:] = self.argv def _launch(self, backend): cmd = '%s -m chaussette.server --backend %s' cmd = cmd % (sys.executable, backend) print(cmd) proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) for _ in range(10): time.sleep(0.2) try: s = socket.create_connection(('localhost', 8080), 1) s.close() break except socket.error: continue return proc @hush def test_main(self): for backend in backends(): resp = None server = self._launch(backend) try: # socketio is not a WSGI Server. # So we check only it can be started. if backend == 'socketio': continue resp = requests.get('http://localhost:8080') status = resp.status_code self.assertEqual(status, 200, backend) self.assertEqual(resp.text, u"hello world") finally: server.terminate() if resp is not None: resp.connection.close() chaussette-1.3.0/chaussette/tests/test_util.py000066400000000000000000000025471253327544200216020ustar00rootroot00000000000000import unittest import os import socket import tempfile from chaussette.util import create_socket, import_string class TestUtil(unittest.TestCase): def test_create_socket(self): # testing various options # regular socket sock = create_socket('0.0.0.0') try: _, port = sock.getsockname() self.assertNotEqual(port, 0) finally: sock.close() # fd-based socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('0.0.0.0', 0)) _, port = sock.getsockname() sock2 = create_socket('fd://%d' % sock.fileno()) try: _, port2 = sock2.getsockname() self.assertEqual(port2, port) finally: sock2.close() sock.close() # unix socket fd, path = tempfile.mkstemp() os.close(fd) sock = create_socket('unix://%s' % path) try: self.assertEqual('//' + path, sock.getsockname()) finally: sock.close() os.remove(path) def test_import_string(self): self.assertRaises(ImportError, import_string, 'chaussette.calecon') imported = import_string('chaussette.tests.test_util.TestUtil') self.assertTrue(imported is TestUtil) chaussette-1.3.0/chaussette/util.py000066400000000000000000000201621253327544200173720ustar00rootroot00000000000000import os import time import sys import socket import threading import select import random from six.moves import queue import logging import fcntl import six LOG_LEVELS = { "critical": logging.CRITICAL, "error": logging.ERROR, "warning": logging.WARNING, "info": logging.INFO, "debug": logging.DEBUG} LOG_FMT = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s" LOG_DATE_FMT = r"%Y-%m-%d %H:%M:%S" def close_on_exec(fd): flags = fcntl.fcntl(fd, fcntl.F_GETFD) flags |= fcntl.FD_CLOEXEC fcntl.fcntl(fd, fcntl.F_SETFD, flags) def configure_logger(logger, level='INFO', output="-"): loglevel = LOG_LEVELS.get(level.lower(), logging.INFO) logger.setLevel(loglevel) if output == "-": h = logging.StreamHandler() else: h = logging.FileHandler(output) close_on_exec(h.stream.fileno()) fmt = logging.Formatter(LOG_FMT, LOG_DATE_FMT) h.setFormatter(fmt) logger.handlers = [h] class ImportStringError(ImportError): """Provides information about a failed :func:`import_string` attempt.""" #: String in dotted notation that failed to be imported. import_name = None #: Wrapped exception. exception = None def __init__(self, import_name, exception): self.import_name = import_name self.exception = exception msg = ( 'import_string() failed for %r. Possible reasons are:\n\n' '- missing __init__.py in a package;\n' '- package or module path not included in sys.path;\n' '- duplicated package or module name taking precedence in ' 'sys.path;\n' '- missing module, class, function or variable;\n\n' 'Debugged import:\n\n%s\n\n' 'Original exception:\n\n%s: %s') name = '' tracked = [] for part in import_name.replace(':', '.').split('.'): name += (name and '.') + part imported = import_string(name, silent=True) if imported: tracked.append((name, getattr(imported, '__file__', None))) else: track = ['- %r found in %r.' % (n, i) for n, i in tracked] track.append('- %r not found.' % name) msg = msg % (import_name, '\n'.join(track), exception.__class__.__name__, str(exception)) break ImportError.__init__(self, msg) def __repr__(self): return '<%s(%r, %r)>' % (self.__class__.__name__, self.import_name, self.exception) def import_string(import_name, silent=False): """Imports an object based on a string. This is useful if you want to use import paths as endpoints or something similar. An import path can be specified either in dotted notation (``xml.sax.saxutils.escape``) or with a colon as object delimiter (``xml.sax.saxutils:escape``). If `silent` is True the return value will be `None` if the import fails. For better debugging we recommend the new :func:`import_module` function to be used instead. :param import_name: the dotted name for the object to import. :param silent: if set to `True` import errors are ignored and `None` is returned instead. :return: imported object """ # force the import name to automatically convert to strings if not six.PY3 and isinstance(import_name, unicode): # NOQA import_name = import_name.encode('utf-8') try: if ':' in import_name: module, obj = import_name.split(':', 1) elif '.' in import_name: module, obj = import_name.rsplit('.', 1) else: return __import__(import_name) # __import__ is not able to handle unicode strings in the fromlist # if the module is a package if not six.PY3 and isinstance(obj, unicode): # NOQA obj = obj.encode('utf-8') try: return getattr(__import__(module, None, None, [obj]), obj) except (ImportError, AttributeError): # support importing modules not yet set up by the parent module # (or package for that matter) modname = module + '.' + obj __import__(modname) return sys.modules[modname] except ImportError as e: if not silent: six.reraise(ImportStringError, ImportStringError(import_name, e), sys.exc_info()[2]) def hello_app(environ, start_response): status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [b'hello world'] _IN = _OUT = None _DBS = queue.Queue() _ITEMS = """\ Hello there. """.split('\n') class _FakeDBThread(threading.Thread): """Simulates a DB connection """ def __init__(self): threading.Thread.__init__(self) self.read1, self.write1 = os.pipe() self.read2, self.write2 = os.pipe() self.running = False self.daemon = True def send_to_db(self, data): os.write(self.write1, data) def get_from_db(self): data = [] while True: rl, __, __ = select.select([self.read2], [], [], 1.) if rl == []: print('nothing came back') continue current = os.read(self.read2, 1024) data.append(current) if current.strip().endswith(''): break return data def run(self): self.running = True while self.running: rl, __, __ = select.select([self.read1], [], [], 1.) if rl == []: continue os.read(self.read1, 1024) for item in _ITEMS: os.write(self.write2, item + '\n') def stop(self): self.running = False self.join() for f in (self.read1, self.read2, self.write1, self.write2): os.close(f) def setup_bench(config): # early patch if config.backend in ('gevent', 'fastgevent'): from gevent import monkey monkey.patch_all() elif config.backend == 'meinheld': from meinheld import patch patch.patch_all() # starting 10 threads in the background for i in range(10): th = _FakeDBThread() th.start() _DBS.put(th) time.sleep(0.2) def teardown_bench(config): while not _DBS.empty(): th = _DBS.get() th.stop() _100BYTES = '*' * 100 + '\n' def bench_app(environ, start_response): status = '200 OK' headers = [('Content-type', 'text/html')] start_response(status, headers) # math for i in range(10000): 10 * 1000 * 1000 duration = float(random.randint(25, 50) + 50) time.sleep(duration / 1000.) # I/O - sending 100 bytes, getting back an HTML page result = [] # picking a DB db = _DBS.get(timeout=1.0) try: db.send_to_db(_100BYTES) for line in db.get_from_db(): result.append(line) finally: _DBS.put(db) return result def create_socket(host, port=0, family=socket.AF_INET, type=socket.SOCK_STREAM, backlog=2048, blocking=True): if family == socket.AF_UNIX and not host.startswith('unix:'): raise ValueError('Your host needs to have the unix:/path form') if host.startswith('unix:') and family != socket.AF_UNIX: # forcing to unix socket family family = socket.AF_UNIX if host.startswith('fd://'): # just recreate the socket fd = int(host.split('://')[1]) sock = socket.fromfd(fd, family, type) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) else: sock = socket.socket(family, type) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if host.startswith('unix:'): filename = host[len('unix:'):] try: os.remove(filename) except OSError: pass sock.bind(filename) else: sock.bind((host, port)) sock.listen(backlog) if blocking: sock.setblocking(1) else: sock.setblocking(0) return sock chaussette-1.3.0/docs/000077500000000000000000000000001253327544200146225ustar00rootroot00000000000000chaussette-1.3.0/docs/Makefile000066400000000000000000000127251253327544200162710ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 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 " 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 " 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 " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in 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." 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/Chaussette.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Chaussette.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Chaussette" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Chaussette" @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." 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." chaussette-1.3.0/docs/make.bat000066400000000000000000000117711253327544200162360ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :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. 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. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Chaussette.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Chaussette.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end chaussette-1.3.0/docs/source/000077500000000000000000000000001253327544200161225ustar00rootroot00000000000000chaussette-1.3.0/docs/source/conf.py000066400000000000000000000172711253327544200174310ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Chaussette documentation build configuration file, created by # sphinx-quickstart on Fri Jun 15 09:47:15 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.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 = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Chaussette' copyright = u'2012, Tarek Ziade' # 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. CURDIR = os.path.abspath(os.path.dirname(__file__)) sys.path.append(os.path.join(CURDIR, '..', '..')) sys.path.append(os.path.join(CURDIR, '..')) import chaussette version = release = chaussette.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'haiku' # 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'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Chaussettedoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Chaussette.tex', u'Chaussette Documentation', u'Tarek Ziade', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'chaussette', u'Chaussette Documentation', [u'Tarek Ziade'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Chaussette', u'Chaussette Documentation', u'Tarek Ziade', 'Chaussette', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' chaussette-1.3.0/docs/source/images/000077500000000000000000000000001253327544200173675ustar00rootroot00000000000000chaussette-1.3.0/docs/source/images/chaussette.png000066400000000000000000002324621253327544200222560ustar00rootroot00000000000000PNG  IHDR^ CiCCPICC profilexڝSwX>eVBl"#Ya@Ņ VHUĂ H(gAZU\8ܧ}zy&j9R<:OHɽH gyx~t?op.$P&W " R.TSd ly|B" I>ةآ(G$@`UR,@".Y2GvX@`B, 8C L0ҿ_pH˕͗K3w!lBa)f "#HL 8?flŢko">!N_puk[Vh]3 Z zy8@P< %b0>3o~@zq@qanvRB1n#Dž)4\,XP"MyRD!ɕ2 w ONl~Xv@~- g42y@+͗\LD*A aD@ $<B AT:18 \p` Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut}c1fa\E`X&cX5V5cX7va$^lGXLXC%#W 1'"O%zxb:XF&!!%^'_H$ɒN !%2I IkHH-S>iL&m O:ňL $RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t ݃EЗkw Hb(k{/LӗT02goUX**|:V~TUsU?y TU^V}FUP թU6RwRPQ__c FHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ! {--?-jf~7zھbrup@,:m:u 6Qu>cy Gm7046l18c̐ckihhI'&g5x>fob4ekVyVV׬I\,mWlPW :˶vm))Sn1 9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/ >B Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$ =sl3Ttcܢ˞w|/9%bKGD pHYs  tIME 66jY IDATxyeUϽosdFњlY<`HA6MuC2 E)pWbpƘS` Fy2S12ŋ7{ǍLɀklV\/n}9{ 3CNNNҐKCNNN. 9994ҐKCNNN. 9994ҐKCNNN. 9994ҐKCNNN. 9994ҐKCNNN. 9994ҐKCNNN. 9994ҐKCNNN. 99994ҐKCNNN. 9994mO!'X:_-{Ou> 9Zp|#ǾգC|>4-]RGY> Y\n6gȃ_y e7^WO-Fo}Mn=rp^÷3gCϝ}暏ݍ}[n-GV`C힞Yعy$k؉O?s>wd |p/ݺy|dtS/^ /gfE/cp|0sr_1SS3+|snyW\vapx-7o]R>pOB@)1Ԍ]0c&ap3gΞΥ!'},-79c?sHjlp5C@Ro1@eUUx[^;qo=|A/N]8~gsݍ~`}sp& gHk0iCpDfDD`O=XL _u;w~x|d(kV_qեGm,|ij_wuOmlm֛_uE6KC$]e5w4d"3bU3@ȌAd0@"5`ELM=co-W;wCg S>'?0PzM}nF LR !@f#* fLb`&Dj/ LD G? o0{{bnzNP,57f}pmbJAE8{I$, 2T  36@P@Fcva~/'\:vO-,շh7Ɨٻu2L$UA+@HS,bjN!5`#R)@d DْD9$ܾKܝ[^NpfrϞ<3qW2FϞ<~w?5ǀiP$ZPG j1:CqfxẌ1(A_p 8XE [ y_zwȷrri517./ڝ[^y+FG oݍɈ&E3R Œ@`"p+%! aHd@Df@`d0fPl܅ eknW^wKCN. hz'N;5㏦B/kW/=z3$Ij |aFf&> $Dჶۭ("30vUj^?Y7| BS.†p_A Z-|G=/w-<]wo"QH FF03q,jjJ$fl d @U%3+ss[ ire^/kaC'EfÅmcf PV??m'I9sɥaɩ3?>1y{;G{APyࡦX46 S%AMnq7w≧'no{~wnޏ([v m'D`'j8@RFjXz}b`I'FĦŢS3$eQIαJyL!V*}Ջ˄t!^¦6nņGb’pKھ{bn|9[MӧN>Ο~g^v}w~-o}͖=}4S-U2Yo1 30A3E<^[T9(v9BBcc^b8Lj)L?2 "O"#mH5h <ù\Eo}kK⊕/ArI74{葵$i$JtaDFXzj +M24(KX@.,.v;{\4$ HLi0NDfLR~[5ez/Mzu}.z"v'LC|??^/>{}jů>v#&ICƏi?7Iqdz~lIRUڈ56J))!ܹIꃪT*e9UP.0fjZ0f03,f  NfRƔAB`{}ڂ/Af& GD86qc~C?g:xOsg*r?~}JC,|׏OyZ 0@TPt1,7OLGWI^k斗 0S"hJYU z_$I  v šK;wI~lrʗ~'?~MyԯpȍokI|suW\1<1]>W^M|?x۾/կxWN=~胿JÖ}kg X6!Ǐi׭[/,:AOD! 8%I)z!*^CTZ[[ނ@VzI0o038Iα0]j5^IPvA3q=|1ί|w\su޿ޟ]y[=Gw]~?:]?ܰ{}ϊo4ǟx¦`#l 3UjK Y\, K @ غe0^/X0Q)WVV!, A<9UpPt' bQZ0<:Z_#"FDDz] ^6l#ⅽKZ@! \{u^zw7wH?]X+kSS{׻^XGusN>$ֻie8/KSgk_9o -7{Ump D0'jÃ&P# qT,}Wi0Xf,)`54hjL@Rv(v"wɛZXh00Cu#z#aC< v" "ffZ\zc݋ ߗxt5 RŷsBs}Cvꥧzxӝ2_|/HCc?0"5%ʥ3%6 (i ^GGϞ;/ zsW<̈#Nbjjb BLjT7Z(IRS#7ڱԴM5j7L1SK<*312L5d,$"IalxC-ٱzу>7{/ۿ;}+O/s_9}_);6|~?m}O~![# W? 2 DjW-E-7W WkVgEraj$i;Jl.\)-gpUu!d`&DXiERTFi !@L`*JV >FV6zknaHClDU Aj0./ٿo?P|ɷ?oOǯ8?w K[vV>]}6|oxٗ>/lA~g}){n9󭗆׿S'РFP`P &ݎH:rnwQ/MG֚X%B|lqjJnn,.,EgE]T S#ڱex뮿©~vpkBx~~~O﹠#/Fr( 6mzpzoJ0a(jƔRKb "c!b__ػo_Wcf&hmuM@4amKKBTKtZb@V-[NNNeQޫCCKKJ!;Ӏ啕«9H.YFla$M?f'D `BYOzο"i{Ͼ>7S FJFΉ:5 mXЈ{t 37=H̲_VN=Wtd9j/N^YY"&"C0qjs"B*O2)+v3gD 9ZRY_k!%x53b͵1)&>R #rX?_wO<_>5?uáC|;Hۀ+EDMSό:0 0q7IHA @ຩ.;t`vvrvė+e!@ (DZyi(#gfB5x ,Ԫ奞4o,BU̠;"l=w^D ,AX!F?jt㻿?GCxGo~Zn9&Ұ}7g"L[N00Cs c2s,)yIIAٌ%*VGT5( % J! !h! uYXLH/Fc dfڽy4旉Šk0 s,"I}R$I/xcE(JD=U7r}_>5ĝzݏ?ple_y,7ٜoOiz4!8L4FÈ`$`v>UuSZZl,tQo>=5J8*n#1"GT^ 1R;QUS+ щ؁I##s,ΔLPP뫭1`Ţ3ipU )}ǎ}hvbrK6c'=xw $L眩۶N2"b"8r+MjbfNP*[ZNwfa>jׄ4 PW 0Ս`g3SVT>lr#x#0#DFKĉvJD82CNzJf1@i"`ߞ==SՇK:s.!iR4c Fcʊ7jXN4l;Z674LE Ґ՗ Tj+uHeBjX^i:lKFGG[VH4;w`gXV͘~eu58r eR1ff19?=ؗWι=JߩN>DdLJLb"a#ROp}hz~Q0!); Rk,Gzcd8eUA`B<"41gYy[X\b,D8?5lwAK6Wkfj Jijz&biN`j23 .xwzbz27Ĝ\p 7W<("bz+I,ECZS3焈sQvB䜪vu{EP(r84qN7M333qE":W(MT0S-;ww*6F͐%BZJz,k3nW_\i Q"=:]\m1RT,fv8K|` }9t陙sssK9ιq>sDHҴj9U%X,4Vff6̡CBz\"vhw:0~/UM 'QS'Bc\h"S۵cىÇ! ʃϞ\iY0#AUշOun94\}ՕrD((+U*NB&k;!4e;0CO>T2W*4m0װmۖdUT"5wl#Y;vl_\\r@$τ^(iFq\v]^Z֍Y RK_vnfviy57ǜKZڱ}ئJ4XY".Ca}1# ∀jvM-ʄιg* ipB.NLXLX8.n޼ aرcr%}tltxff50߿jjJL!u W\qr,Kddh(I. &:oቯ>sIKֱO[U> RR^X\7 A[7#$4Pέ[V;vLv}YqpښAj6αCT b!U]TNGBY >t;mSh:rK,͜XD PB%zrQ39\7j$YUOݐ&I9& /$79{& lKRr# ERv:^mN 11 IDAT#&q}pNB[n?9GY8'dKbD%cm߮p Nڦ͛W[48aU+ז@ TbX!d!U|8,0\w]Scf*'ONSKD8a^۲eԩS@ꭿ^ouY[ݍS `@@~g?[d%- "ȡzq23k1"dPP(H`0s0 UϞ>彦{jl:6<4?^`8oiyY D%!T=L1ZNđH1?``LMSN .QZ}($\_>p4F  g5 MT,[-JD2&ް|P /GNLFsIKC&zٳNfYaUXouX}{ӑ: y`Jz_ye1G.b!ڣO͎BS8 wE%\!Z\\TE\(5b3|j@P'l0FGFjƪ՝ڝNɚn,+裂fV9Vcccg')Pt.IS"Tk/}Rm7Z}KQ\ҰgϞ=ccgΞ%(nInFހ4MVV`Li``Ͼݫ^}nZ;?=ˀ:RCChPa%Z#ZmQ^B7oV!(,(RjFGZ@Y}'fR `z{n9Zm~yyݍ'^r(s.ii0R[lF v"!aaRtCcO&05%Ҁ&$G)u̎%KbDic&>ŕfs @> +̲Zx`(&R_5r'NDSrRi1l;sRh(]ה]w?QF@ veFsKõW^} =z`>j! hP4jf 3uÐt|R/`Y_ KXYY vBLMY!^3ԧóAk쩧z2jfj(%ZY]X\yӦ8!"^{5G>ʀ:R=o>yS]\`ݞ+?lI+;UJݤbfQBT#\dَ-Vkjz !BX)|`!ai)TZuy3Soi*3{biuHV!+΂r(BA}(K cӽ`C9Wt8?5#L,d;vngRgϮBĜuz˯*&vsIKhdNUV(OO̓KqZ[k3g蔀-[R˃l]o?"vRH^[/vmou ST(f|q֥jtthdDf`" ؾiT*/,.,РsBq`K%cJ!8BN%b6k[7=Gr̹aHT>7?Cz>4KR* u)*KK+D0#09 vz|젪@}r_Xr wɁj*BNVWv(MJQ<3@@PeeX T*;c+ƀPqUW>shH*++3)|w~3O5@97o ˜KZ+U$Tc3 R(LO8A9P(DAMnG!B0BЁJX,4[-0e@Xk 8rnd!T#k둋RoQz@ @cy4U2al۲F0g?Kzvc`nunjHC'):95D^-2Җ;w.,/UIYAb|iJ3KB!LZ"-- ,6i4*6m WlݶurrRQIsBˍcJӠ{;w#jAV+>y*UȠކkj0<4XjRP+58Cl41>\`k^ѣ'&>{"Y^g,Npl35R;n__͛!v\XXt¥HjE4(JcA}Ttک"4[ni4YD,.ě7on6["Llڼi]^23Rrc%RP_\! @ `禙sIK˶:pPP| L RR-1 ">vӬ K $LsG9RʥbZuyV;˅f!. "CP,’W4U KKqe)Μ!* Zm3Zx礿^oښeP:ul̇ӭwxn94X8},s#X\ GzWR5"W솇I% Ѯ={ OF`VV:ǮTk&m *8'!X,90,-/kWBPu{+Rī%M7p#4J bn94l{o<3q6Ȉ /_pªPլ’s!suۖ-ssnW ^-U 8tHL,rv* @]^Z j&*;Itjj2`U[`5O>SU5(s*B ;mM9G`*iu$II$4m6ϟ˭3璖{Bng2aN73'vZ[[4k4WjqqP"+kVwr'N<1A `"f l߾mqq,@ 8x੧v{Hg$08X75qH f#Cv)8&0qH`c6$ jC! .kPh0/|?KΥ- /.eQ6\6f!"\TRj6QΜ9Ǽ,1s"Bl**%@ AlnUmpl,J5M}Z$M19Z_FD  LC1&fsAv%D$ hj^3ȑY;f鴻4%&BVmav릾 Ya+~铧r̹6x^"zޒtz!&o=qD%rDLopxni^f!022t~n:LTe$BBh( T,(bA>L f:EBj{휚>4xWcF'$XljDMHEfvijr͹amiu/h8 r@? VNY֚ސAQ,Wβ0G''gc@ElP8:}LVslze3 lE j-[4LfYLk6W( N5!N͌ c~[}@s.iiE^rOM-fl`3&%5-,.iAQU* ,/-'^!(&&&ׅcSk&++k"PI}wB1N=lp9^o%\R+/4eHԢji곢Oq0ALni$8`A5(의˺\P*])j3֐D V]]]g[`;v5:i#j[7oi:Q0#Skw:![*TfDƎJbcQeAX"G#03 0+e( j.⸗ Lj<95EfB0öY f:kno?hΥ ɇmԞ,W\r,2;;/ DV]7s"LcǏQ,+9?9 {4IS V*DB$nL, "cKR"׉ +, B0Ҡq\YX\s(Iv`fljripp@A\s^{}1ќKW8Z@|RJt۩ ٩}w|\sޙѨK;6^$@H,Y&d7I6 BR(t`"ٖmYqˌdMМ`4<< |QGʦ ©0{ɒtT*%B6 @DJ)x<~a&&-D뙮݁jTaِhc]]".K= @s6";;;E@L1ȂY@Pr*)q@p>C4i *Ѳih=gӺC "a?Ɔ:JN1Jys6l@(Vښ:9\AkKK֭*rԴmێP "K4?4dC:$޾Tں1a3i8s"T ADD"_(3T6 ,0ąRDENB))z[r+-W"6wQ95DN;;`X ~[k`.qCCɫzgOZI{{T O1 "* (qru:0_pw/&" Tt|e爙5Ik`KDhQHd(as8**;;vi04֥2Ћz)d |W[TUulۑJe}ui&p,t2[g5pS$_]W;884;"2BWl6lR' 5Ш[)Mq֚UUU–g(e DTDP 8aa1 @g͙ݹySϞ=cZ [[2cO<ְp~s[۸5$UL*? #N R)͝OLWS90<14(={ B2 NnHJ~^#tYOM:HEJL$؉0S@*!81E%ʚ;ﺫ|L6v ,zq./4+0@X%\JdT`B q{2Cd87'V &+d])Cu5uU5 y?`/RH1عp958bGŸ!DDUNI` 119Y6r2։1!@)pA="Mqдtu|L6a9}z7mLX)'еcNy\?n9WBfY464 %'@}m]DDΉ硯o_`7wttR PeBeeEMm1Pc!]FA1O `XP(cPQ%E(k؀jS 1~/oZZmBʃ=$]YfZK%e2!]wvu4ha5a<Ɔh  6r6%,|%[xoog]0֐ҞR pj ӎS"հ`v`4$Ӡ *0`rlW } eА斦G};DwR[S;<[=9_-Q``T@EaU2^{Kˢ[yRmBmZG=p# c}?rʆUIk}}j mh۾}uN 29'iڦ{z N ==Ksy1g9]D @T#41qʐ@:yz+w6MG[&4Tf2YkL|G^qN:"Xk4̄ j3&?3Z1 /V<;`R"Suj"l0ޅ *IDY紩64gOqQf;wy\؂袅 A۔UUX{;vtm&۪u]['Ts1mw\_xn}ԌV /RY :lLa^JdW;'ɨ&(|HϽ|^6vaZs%Oڦ9a lbƍ<6F̂`ب 0;`} @#{op{p`O=ɩ헝Ɔ5mr;r0bx$Ќ2 pPsbab M]kD s)@< *D xD} ,ײ]hZs3gquaX$pLBp) V|Q QV8*,DUķdBd߁و3& @0ou6<ۺws[8iHgRϚwRE$tnXA. C0 # y(&^(N4VIN?w]߽s˧lc{^M(K$ں-[;gM")AL(&jUCypKkW_ݻoŹp:= .;}~&eթ2E9r^T)j@!#o,h~I8`68H$y7^ն57wwe]6:mokwuX8Eh{{ۦM[@8LERR&гo`./憿o-:/|j"Hy D6E˺c7 4.FA $&:#FGVGE 0 nR,BDV*|U˧lc ً<& BY!Pʀxxxh0?\SvNuNp綎'xrΝ -ږuuu{6󹝻vlT Q34{JDPG@* XY"C'WPJD * b)BFeǨM?.BCUJG~mg}g=-)ܲŨesCsO?fSss΁~Gԓ?>Хo=3ӧOb PȚ@ 6".B QvP0]:ĕd,lw8;T uJB+PҌH҇0kH:Ũ4PU"'TT X7}{c6F@s{[CsӶ]]Wʛng-ǟtYy)Ͼo 5H~ BXA@T1"QGFHJ`RdL1Fq`$`ΥR)eSނ֬-`m CCuUg3\|tB^ ~8e@X1y^U@]$42j 4D'āqA10A1\&c<--؅m܌{c@T4S5ZZH_C\i/RD :P1Wha5"ğ^ J#Rt23KC( 0JT3q毸㶧}񰥋glcRԦ|sߚ\p\ F %C6dB14JJak(EYFCШ~QMbJ"L䜂3z~._>e6dgL]; Ku`'9Ycc9rZI [r@Qh- mRol,_;>ޯ$(>Q S*CHU}Yణt}yGn04dZZv޼08ҪQb;Ś`/BLt:d(I⏴v#X PQ S= X )(ڋ jD,lcL\U[sazg`@ipσu89`G]C)D`BCXt}1HQK/tc<_>e02]_|GKq;@b5-8YFcKiqF| -@)$grlc̜6oxhݖr nFMZR ɪ2(T"%èMJIGzE YMSd64SX&3%RgR'QKڔG?z,ݲ]hP'޾L "8MBRSJ!XU)iw=6*e(Q<\ȃ,₈P&fJ$%t针+g<N>^{uCyf*4L10<4{}K3r6ra'yEo@LS}(b;ԈSEIo2,@%FIISMR$JEXwkʴȲUhXVUQ`Μn1D1eőWڒ>nۂ #Ex, |Yd TUDPRm@Lݿ(,(1Jh4Mj_rQ:o:U7ˆxMÀR6sXTHX:"XQܠū}4]O*"\M* K(72M]8-U5,ٹ}GY}5[][w=ÁƩ2$ I^e($-J|M *E 8~|gg'([0QBEI0}̻[>e{ AFGl$/ IM(a+[4rTDJ()}6&. >zy?*ر#:B@!hkwޛJ_ҞMϒPT p 'lzMC\1 &4lQеH"]]CAfF3bbSIG1)=#IXVaUU""*¤VU &9f둸 _Yܠ+VWe&5Һ`zYgڹ WZL !b("|{D|6FaJUMs[6Yg IUi) y!$ %Ѐt& % 윋$6Dž q ܮpe֫ {;å25 u;+pGMC'>'j7_5Q\ 2a>\mBC;ܳ!F$^SRhP I,BF4"PE=섆bR& B5"l:'dwcÖ/ 9b0c\= WMH@a86eI'jޡa ݽ{993MzO/n5F1BXAL?}9wZ~cmBÑ8ܖ-/. J$E7(-P$gU\yI76T\FXR? r+oO k~d<]=kfo!ֿ{vg_;'xBC]@ﴀTV9 'C (EĢP]kc*㲍Qh0{캶Wּ*@u0;Qy`YS6E9SUY)y.;=FpQ{Ie^TQU1w<}0=O~[4nH;{CߥD-aD-VuK0 GE4㪪[z(㲍]h8^y򩧟x3k :2Q9Cu%3~wr`A;>b0p/՛_`,\^?St\te]eh:3N۞?BYvZJrA:/%d̈́0Y#Y#6 Sά.㲍]h|別:;`D -8"<9Q DWJ2oMmt;&iF_@_]s/Sd<ٿy 9#s˭suo5?u |췜@A VM9R)KLN!$\E 9W]H{F5U5>LmB;4ewޱ+1Bg @%qI%#K% a/kÁsaV|?'/)j=hؾoۮrkY+>~{ƙgP^2JV49/HEFB54(nđ:Sh*^$S4?Tx. 'W-岍QhX i6&Ԑ$Ow@Q”h%fC'i@DU&ZXuغs'>ޱa`ϯo=_M;n+􏾨&!mSZaN۴c6JV8M#C2T bDʬ&tR.ѿ D"4ko;\^ojϜm~8l!:qp0J!Rw}ཿzV75t_Oii᧿ïww=p_5`8 *$>3XZ4DI#~V$BMk$ Qr@غ|6FIS 1 }DtBG  Xu?ȃ?u|`&47X: 9_}P@j،ơxNOGؒ+ָkWWKSHV)g z~NitElv\~VtÏؠ "JBX)we PNXN z@{ N5[YA!gZ]89TH(`[qKUIJ8'D p]ފF ptbb%;(좷I;wK|e*5}s?͚sGttuY."Jʊ %ىsuɼ҆HAtor lEM6KMYc4gƀAt ɨ'%x1z9F.B0\$@56Uݽ7>|x;v5?){f XP6{z{pAPZ"#{vED+g!)h)95BHâV < D9ʆ4P,X~ T5ԑNToc)#bk UU Fs`aȗOgo|cIS&0Ͻ_xa?|e,CßWmnkyG 9%&Q yD(J !DH RQTC !Xat@@">_ov*" tp~DCdygQ i8CzQ~5S]O߀G&Y]\uU.z觿]]{/ozaOY3UTy?USSdaf"e06& p Ig3@.d]hq$oHŊ3Ʉڽvuնm*_'~Z[\7t򮡡N;i=??O/^0;>Ķgmܼ! C"c40@<\|-T‘Džow_&/ n (J˱֛҆f48vH~Φة2'#?ں{>+pĒ`_?Ǿp{>ɛoM_}ϰ䆇oWRdxL~ lݺMD%N%XR$1Q6VK۞Bn(G%~_0D J`&f*UeTI8OKa!*`IQIxcK;_ǟsʇu5};|.9hŊG^޾}?^;wvѷs5Wo/hm.;g 4㏫oi}5UP( +Ff3KFd㝸2(Ǣp L8H ޻S!!D틒vQ :E8^Z>mٙl9zS/FOM/^~eI-?_OnmO|}0m8[249Ŵ)^e掎r$\3cgK6#8ps]z Sqp3~Hj* BGUQ!X#'nd .\`Y7raNkٍ7] s_Z_7Oh*}ou_.`Pߡo9s9C;.:Փi}SkY5V/YښJ1ZBB0^gF(WY&1F=_˗%/ebH 0~\O]^9uŸn/׶46șoz//_?s⃗.<ښL@fп?j"ZſP_`n㕣~ }jU`!Bx\A'U ]SXD2iw!Qqu,z(!yB =AABTЈ,HkH*1+e Lꂋ޹`O^}kV?ȓo:~`W0|תGn~ி? sGZd.W);/<{<f㰖A Td(ԃ /,Ģ >DL19JJEVH*c$c[܎A}]Q2?~D|9e7oYwg-}d+|G?ɌiӖ}8Dt IDATu~3/Toq'~;ySO8@ʳe7+CuuHgR+V4!P>NATƣad)zeiJTcW/yJbDJ1Xo+6b3ƈ \3i/5V<_͟Yd|ߞ3{3O \b喎mN8_^} /u6ve͟2鐙3wON^~e+^׶vuMj:UӧN0~H2 5Fw-\,FAii`TQr?ܡ׹GQğ\ep- |J@D`Ȧl0}\`Z*m3&_O8y`u3fJ{vysTKeho?ohs6npn|m?յڮySX6}ICݩ e+C_M޵.[ܳ *l8^P߲RObvihTk$ 8)]]Ď;(4+JJ3QpBˢy~hhW]wݿ_cߠn8 3AED`:n&*R(jDߍ}; O>FEԇ$(kg(:wȹg7߸}?7?޲K ̛2uΎ1KdMQ 1yt!QQPxJU.q @GU̪-4~zHB‰ ZX6k/[Vvy_쳏?MJeG9сIP,Qm:^̓H_ʔ$$PqRiLe/)~{N?M 5_4 zvN@R'Bu%w8(^H.C rÊ#K !#8c(TH4 [NUչ$ (@F`Qph.Pr)Iڴٳ'ؾqgUy葷崲 LbM>BM4@ɜ$HxDhJIMQM0H!Ae_|P$P )ߪHs2pL5r'Zskw{m}>w{wzzu+Dw<Bޤ0oÏ8[|dŅ\Fn N;|93ffZ'n9sF{ߏ%QR,0GrPHHJHaRrJpa'Zfa2J.ٽ5/:G-Bаe%?=P(sR+gqPp0 \1pfbOI} A1 Q2D8@5&#B{~ժ׬{u;=sǝ|igO~25 r!Wyi` /Ǟ<ʇlorhpТze:F50PAJc_8إisJမG1sBUɕ0U" uL`&8f;I!B'ϳ W6޹?ĐM͚?з1w얊 @аr6Q&o⁌ >w/.Yzy]nhW+릴O޴аwg;:!wHH%?*[nTJtt!1V!dL"y_lZP/ώ{^+6[O6ar{}n6T{@ RƦ] ?|LPbK 9SN<w[z3ʇY^>e{C)SZNzeOw[[ $P,t*iԄ&#5NRᢠSNa(*Zށ? wtĪyg5VwQ?x czNL`ـH> ՒBDHT:_?~S>-hhoXӽM[`Dv&5gYZ,! ^HrR9ң9\)f4b%WW7kW>NVrIK+*2gRê̬rì)lH],G)h6T Z TBO}C~gLj/Jm @V?s26PbHr^EwE7 +, q /D<;ɀ7lXPYkm7ئ͙׾7 90CUTD Q8bN_UA>ls@$ˬЗ?{9s~~z#>d:M@֤6L `!&i:X W˪Dd8`*j 2̪0B)C_er)&`ڳ;7k6zvoٹw{桇v!yiPU4AYD,m\8^|Z# gF`UŨ9/<]rͿ5?8r:/;P2]w{Q7zl@$!"fr.ә-LTIRB)8b`pGOM= .XpВ埾t4cAư.eL*"l``XHCzfE Qn "5 }=C~g(Wxлs? e{Ak}ӛ9|9 T zUb"'4%\5A+ /0{MEjJ`N6҃VdS:O]|qZϳ 5p֚O/ZrMJ*Ս؏2/KGDo"Jӷ0\ #t}u7޴>Tg'9b܏r~Kv֋O=|Na]Аeĩ { *0Y@Y`?==k'Onl%S&N3gFM:]@@(.[s{Ͽ_{B38_`Q;$ڦCI5Q^ԀS B=,Y<xa{}WxxŃ쪭埻5OXvv=O/ʿ:eij@YȱsTRl`B((;6nضf=߹sG=wa:Ėl%ù1G:1dإϚ:ѻ=3 %_2 D5@` eCy3ĉg])Jdgʷ~"!d& :M+ogߗ5WvԢ%;ϚCR~fW|epJs#){Np#7{=\0CN=C.'EC!0QQI:n$3,sTUw_OL {M`!9V!B^4 :pa} "L1KKkk8sY~o.;|?o۳O]tIL,Cж_𑋇6X~5EEJkE) u]º;^5M{\hcY6{¥C }("9coM@k7SO|֍o &"mNu^~^UU`@)$8׽,5Yl*EyB%4 S5?‹guڸl @X{]z?ӻ}rE'g=cܲÔ foōȇ+?O}ʔYSLh * γpl4C@[|SkCxo!ȲH:C J8e%sˍ7Y0X F\MPڋ~f\_oD`v qxM;v|lɘc;>锷_;ΪϹNa`}(T: DP@M)bÚK41D}Kјb{vQ Vl*H.eg~S I^^^bf sk]i9Xÿ4UՏ<:^t[~r9:θ>戎Ei4j19R.e=@ȘcFaxT`P ܤȚo֊6 vZv]^&gH)#(@ UG+.;^y{ǹBVk.@ `@ 55aa#`DXd%EmJjZ'? [ŗo]v3Y͜&lM]AA0ba 9KDb¾b]E{$=lP( TU@4k?ݼ瞙3NT[}Uw^ͅFsh'^=O:n>pA&SbD,bX3v:P[5*@,[ؠXG_-l@ v "JF7\XK/=y ɓךhÅ)v_w%W\~#'0uʴ*q'ӥ2-& }$B8)|bC73,.*&j}4!@OBr\~whCt)M԰# ԜCvGY}Oat,*aKZ3*:+ĎC P [o}Gzv|=*6Y9٭{l8 rb B$!x$ A*ϵ}O6ӺEtQ1@***}U3h#\Žۯ~UM<:q7I?Z](Z%M@l+"BFdXbzٳWΊHE00м$Š%\Mh3e@2R2:UoSǎ WG~ﺥ6y̳~u7o*Θ9}vm 2 q F`{&2)(@"#"]4ݯtF:M;20џ._M2\Dr & t01ګw]Jrzú5C9hWMEka0dFAb$hvN+W@6#uGKVo'6PHh Xm9E%@*$W!AIKh)4a9+17O>:Gu ?ibͫ94)ǟ0Q/F|05AFG 6k,Uvӵ.,ssw~y5Ð{'?}鹕}g=F"hȧ3<ƌelQR};QPs dd..=z}}Ҷ}sU|;6VX=adTA][?\ѣnO.B6T13Zg+p@B6. ~OF;ݠ^kanݺulX@c $%ϱ~+0V 0"KZH9v">۹zzՓqyvم2NVg[o:jgS =O 0 g!T50W 4݂"=Ąӵv?d@QHh? ᜃ (%h4SMǏ;z N_L7^sP6 &"f"/,͌'ыQ#+Vw7?Yv? rەfIA((AbBAXKNX*JJ' q̳O<}qːawcshl:lNj~{֬sBĎ!X$^[}W/,2lpt#Ѡ{((B f wٻ荅'vZJ9 $۷WU3w޼/dAARZ42(BWnc;zOf]~^7;tF@lD9̀M@767Fl "gCzU\!`"J!2L"@gg'~IYkF%h==EZsȠ]\a/w.jѲMYl >Eu?x YQE&˜kf`6 >/b oE$HFAzg;8%D(`:tI}͠$DxY lKle,\=Z;tfeS?>-6/k&Co[oaN=” Eb0al3 IDAT'qA 1AT@ +$!kg8J/| ǎWJ 3)mwY~C>\Xq Q8 9o!13/iNm\t%Jq;`,EdXկ\jcrFXoƻֽ'L1 Ipp-ogB"zӎ;z˗xy!S` 6)rb Crq&hS΢E/v)t^__SdF -}06@왒[אK\mE\W5x(ߴ[_l֗!ې5)'rGp7lؼ+. d J f)P…s} G4 (rRo|)'Oݥ{]Su\hɒU;r7?\Z5翺sX8( J""lPi4ҹAw/YdI'͆#ex&kOe^D->:ڟSja@AFojxssho2I\PUΜ{-۷=< 䴫BBD, ʠ6n RFڜ޻o9 ř̽ҫ4K!O׮E)E0 aC?|"@hBt*C@><7{uU{U.{~ת xYUz\43~TiOɉ]KsFFPH@#"v\k +Vt4iMDDH >`Zg-27,S|Wah!ؘm;uu~🿽{[׽\ ٽy#זjr衃Z~woڥ3{q]{v~?O:v+f! ڪ@u4|ȾwmM9TզŒe+4ΉDPA6KپmwYr$V:@B XX cz/y}9}k׷hlρ;pw:ǏdRFY/Mn#bfj*4(ebCE)joj#4pI#B^uE9$*GmDs|^j=8)?/lCo!X?syGwuӲ"iuo^l]?YCU͔N>azkeg^֮ ; ^Ct 6g0ctI p^ЈaW)ZS3yw_ 4X^{ T.mջ,ݵk7MPT@ ِu0 Ewn՗^Y{Z|ͪө}C4qRFKY@͙(1؇K6,"w<_6?zMN#"[o޴_u,rj0#L:u~sԱctoo5ݺwKS.dR99}_g(ZaÆK 3Cx"11{%9="$ƴȄ!bm$H2y%vu:];,ϡ^7p J K7) iWU5 w欙J)2бkVe[hwq_y53Θ^Ƙs2KJi , a Z_`T[@)2 @Sʅ瞑e˧Dаa S2m/~y|ԈʀB*s(i[\f^{{x'+;v?^\ 7uԩuN']0\ >Brv>Dă.ŊCJ )`?8wg^('5Wݻ5xgk >Z֢}E펝sLrE(6zQL ,rLRdCB,Q<1{'M_!g`#''(E-T?I;taQV-FBhLi)ءCRv 4CXOc1F@ "bS8,Q@'h$$?J"M^ڈ V |ͅDj6\[vLG_r?x?<״h՜5|U:w+Ϟ mЬH\ozadt_ǝ~!rAEeޙmYKц43*;D3 0!$|t23:4~LMdR1xFa[]D!P)xn]9t1y.$Ef0$J`#"uL*.l?!nq/ܗĿ8u*q Adu9E(? D dѪsmZxN27nl>p#GGjjmU5k5/8rtbX(’KNp&b%\<Àxh];vھ}ݿW7c\Q8*L#h8J }q , @b q|/H4/2Z6DϺL^%)j@/Ob{ƪN{!CL~|liYPA$(${q0e׎k|ټ_6]puTsA/DZ("Ө((1A{aMAՀqCZۋ0J9G;m޶{9D؞B.*4v(MYaر R_]Y\ZhX3^`CŦ{Zi#Jp-W Iy%G8N>4,1c㍼< ѨZ ~̤taFMn|wQFDtHâsRYѵL. <5٧_}w|z2ǏW)LiS4UZX_eS\5yB}. @™&I >_$&Ƥ=X=%~)* B(D)I 9:u̘oO}s?x 2C8~';oB]3%`4.VP|0Lg+|+z lv"la q",lW#l !+ ]iE J1(D$T (DIxÍZ(.-JA#DWfBT/݊} 1kΞ6~'>d?( Nl$?r*8 eP;`׼.fL2Q*:A  al!"F #9q4h-YSNS޺ݯZh": IDATaӏ}?_qsh\z1“/,⨒<.jf (9Wf i! b`o-z:H2(9 ڶ}C \5<杵kYJE%*H<A nFoz@Bn( ѓs'0jqP2߭"\K,qhJb} ք&E L0}8y)O/k94߬k??tC kIwԒA"T.Cxp3ZчjB/83Ԇ01*pʡ\NL9wt.-,0s.}MI*&iitkɻgv,[4 8`0GuЌ n$/QR s~:Ad@b_3&!t O$@$6bs'SO}?npγ|[q^k=Kpե|94oEޯˤShbUM86QcT3?&fhspwWڽG8@bII`C?P ӮwđÆ 5* T\N^`H@A`r*qR66!熀H JBv6 JlOsbs>18c {%Oi&І&. fI6w)P"a0 ͞A}_{UKWU,p3:94w(۟.kӢlKF 0& cg$T'^҆>(+G{4` 0S(yՇY>u`8& ϛ?C[l)"锓m ӞeGȚ 2 ;fN\`A d:a޼$*][D"hV8M K$yMMVHb#B+=NyOQ==r^ďCqK48P5 @D#ԃ)z晧v8Wcǎkݱg8={; ө]5w(˫nܷ˖Q1QN0gM}$o(>Hڶbg3$̠APr pvtRyxd !,8:Nm޶S:ucfQJ$@ůlb  M|;E)GK6n\TT0e¤o5'S3@ᡜLfv{XPD0 D8ZoapcPe4DL(/#N xaJ0 xLQO4sꤗ~ʂG{w{Ǟ:u̱]|#G7ժUʢB Yؤ`2lFl'+KLV+F[Nn:WJ^.T` ڰ]A)*rEdrWTJR)qƛ:4$a M^HVKAbs羴dqPI@ (c̍lc>h NÕIBrBP@)q{|/^bYmJ Һr!"ư:HJ!cwo Uuuuu6U Ş90pFu0dXǜ68Y`) 0 8tq~q&J4WD}l 1=mL]]1z2#(0YH!#ℂ0a@!4(~;==l<PrŊ}EiRO~S63/>p۴ot XI/FU,6 eD? 8.k׮D!i7WINriiqG㎋/ǚCanǎD21'x@>j$l&hū A4>xҕVZ]1&/P-4(N#AfN̓7Eh51R Һ%"ҦqSЈ[{]Ǚ8={i%n,pxW&4& _6Fу(17ѯ0:ij'_$*T vUU:PQZjAGV9Ap1)<6.y.4Hナ+{vj c޺ukaQ"Q%<{- Rc] .)T֐\Lb9oچƩu<>#Ei㦍9;ieUc,!15ĥ>i4YT +͛׾"@&f0oێmMlSf_кC!#ya5KJ_喛guF6cRHS?[WE"16w3o^k;^'`W^9|8k J~3X!q2N΍=te.ݶ94VgϞ;vHD 9{A@a;LL =)FI@, 掬1!3Y;0[4nHKp\#3۪Q5ӒɎfS'Y2Zl;GK, $ 03{ (c&Eڰ4~i%RPQ^.-t['?~<-ˠWr˱cG)a?^n!,߷"b@$ g:H@N$lXhFlÇ7hcBLʐ2L(AH BF 4 [0{[3. >߲꫺:=<6T6(EGu1ALV 2:D2#aF}Р%AQ̂D⺴Tn<j/0Im_FbI4Q-L6~x8 # h3p~7\홾=}_1sfyaIr x ?ܶ\2C ='eKBB!^.IlJ-GG%NQk#^`%!aDviUVVZXi4m! OBVˈ`S 8HP4 ѸrcδJ1 7|%M3ҿq@#@ H^k!;bR. <"qyM[Ѡ"*tR-J\szp^]~YeE'2K.iѢp?'-w88BH2h(V! J@_ dy|зWoq dDC6$DaVR;_Z[5sλ>3:-Y7 vNiI%Je5wiW]]8u4*!m9XoDG}>f"۳e5E} `LVh<-^!Ei`iW^8k/1' sH48hyh!&7@i`v}yͯCf/r!u ؘ| aYTQWj{iõ55 ڪYus0XX!ϦmLI':*ۿ-i:$ =&#D ܲ&IP4q5IƿfĉƐPz};l:%ia)rDWBD1##04D9۴׿|]sho.eU{š"r\W*E.L~B1-oůE I͌ڣ[BJ|7mR5>`10{ᅣƌjqA-g"@3rI o&|]hLD-3ID1 D#4k%Ĕ*)S|@{$(s"N-&H(+W3vLYx \ jVDE%+_4ƪf0FD崵<2&>/ AG%6D! /D$DF([h!!S{g PcE`Qٷw_۶m]u]AcjM!y;$S'n(~2Q/wGOEL~~ Pb/+jhyA IBu cVD \<-L$8M~b{ĄBb"8:ǟ{6#ذ (ػ5 nb'5c&  J˲¶)˳.ѽz붭q`ay+Es0\ZAj6DP[($\ٸ(fVɋ mL[d!*`ҟ~:#b Zk!  acoѸWz#ٛf&aN?$ s/&6\b4BXa!2@ZÊKeڄKȈW4¿~HQs-R!,X6рظ⢂ pfpk"Wrx뇖'q\z2k^3B *Pc1 ]tiw!15L$0Sê!DH>-ϾAef q#ƍK)g`HuHN`$^XHN N(է9^pI' h8of&TQ҈ M̸BٛI 5"H:Ԏ$2Dv 2mtЖTL=,uffAQoZ  Y׿6[:/D#LC!\?1{y gH2 )?\h/ mfW(;=ok8'fQ'#;:| kg42uZYF$iخ+nJnCw&-] 3Dl!afYcoF,A)""H Qiaa#b 0y2/] t$mջE0L$ېq uB(?a8B}LyQФtAAsק"t g0<00_ hd4% Smrd&[\X4b]Qt{~½q13*uC$\i9͆ y>xkeq qvgyAn;: Im1Đ84g߆ 洈1H7ː&ֶp:sqU>; ƖhALUn|d,x&B@{{4͢ˆUL\l,+;2k 0 N >F,<Ʉ*s1,=__&͘et}1mCs>3QHAl4* 0022 =?.fаoڽgK[kkK<",S[Еq ݌K D)YmPqZtwץWV Te[ϟ;G)]r3!b0 Ӂqæ{2$miP !ιpN)j!;OĐ uW828TeD&QfB+3=3{@:IgFV{(+DFY ct3; Gr!`HO;п??xnzlݼW_dCH.q.wbǕ0ߝ1hHGcH6)`6,QRV,o8e&K DN^yֺMmE\DQӘsB\!zM 0 Iwg1ijy%>EesgC6{ ,V2m6n}~7Ɍ{ {S0` S:Z ~?v!'pЈCCGJ9zvJKd!ܽDBwd Ġ@(S[Bb4 H߸is6eKtRYZX4x䈺^uy @ &wĦsfΒJ%ɋ> !4^B ~[XTh㳳Zi n<;β]Y42p. fVgtNy'6 ={ ꗜ 3+PJW2"2\qäc'1CԔJ &)#8cNF,\u?dZ+vߵ[g~8uhK78w`DtȬm3bpR"3ض6P"ATR D b+׭i IDAT߿CH("MDڳrW8 @!mCnju(1 ڤ a{áZ!1Y1ɂ&ln % i 5vM>2VViju8δrN9l49LLS6,fɕ\"p4 0W9cfonAhM)"!Y!bYg83N.l鎝m[mU@w?Ԃ `n4풆xӺ̝L]CGf"S)`)40Ds<^ND }%\Ҧ("PSݧz Ńn <'#ѕ0 AĒDV=d䈉?:i{ cGz7g/Xp#M2 pH#J2M`3p"aʕ+ׯYaѵkUͤ|p3Sֆ)p5P'I}hðQ̢ЧB8w߇|WC:MLJ92RSTZbAsVΙ[e(NGW fv䰓o/{|拻$)W)l5w) ˢf߳IOOT*`/1to3 iر-U5 "JoGj$AQZ8e͢dv8RALO܏G"j!~/_2z|]Sg}6bذ+mCJGC<oغmΜ*J^{xQp"h‘@X#n\G9660kQ S CG ;:fH@b>@+bKW,mi]SVRd `ҁ!X>"S|=4 aǙPVoZJgڙ*wdjch6ـsZ7w0]&0)Ě̆$|Z"4VYۧO_ ^nǢ*[4\pwN6AfAI  6]6sYsϾtִ{;|;|k׮*(>jȠ>4(gz!y$@:n]y@"n !s@"݌Z몫УzcLlR`^ݔ+S9½S8,4!|Yn{K#3ifRYmK˘3(d8 %T9-,`Sn2,lA",GZDiaqG)~ŦcΦgy"ek0RJL:"$ݢr,4`K8 ^yPz.XL8b̨0c&kGzy#JJ}-\7lm~th8{t w5KW f;If2 q-/ =G+cP*rޜyo'&֔2P (/&_ + $ZqɅ떯h;/JJ3妍[֭= L]} @ӆieZx";e:w|Le]RX+-tiiZ!-ϡAK tDFٰ `RH +nO9g-mb((vYRR9-l` w5M6uIF"M]u'D/3 ǂZ'6@2t!c3#RO PvdGtՀāG&= [/`>ѥM 2VAYQiDtnK)I $=掇_^uUlRά֮n JĆ@ @P߽OO` (`7Hﰁ)r }+v5$` %)NS$rR1X>/0D!$HL<@7ڀ6ɓNYn3??4O;&{k~*,G?qˮXČXKC);uvXlo~{gKCi#>(f QX }Xj RAZf1y fVd P#ơ!Z 2=nZfMu =N2?]gxta˄Ψ!TQdB3>gYQ!-ͰO&>p %{0(B0 A/ϲB3矡] ># NǮMҐUt3y7`P"JӲ>|_g)a0 (L"m먴,J9raZifp@u{oCC1h{ȄХ_wR)bqwNܧ[ rℿi -O?[6hZrҬ#sʴͭ- M#2YcB R%MPd<kj~  >L=aÆ^[5b$F MYij^Ym5r{hZx#Пqi5B E9~!y@Fœ#qʩ2r{=^rn6x=x5g#b1042YƆʋ #,QҥۓGth(½j#w(6#1a(@vGh0fpȁq|~)-D q5SN3^Ξz56*\-)"/C~۷jt%St̑ Enc]`F1Q\XG})KްbҤ_b G^xŗM[i(\qo~}E@L Ɛ1 onlz'>>@Lj2#?p㎆>omH$DFO+[̀SڦUzxO}{USu39u;qC"Nغ#E)1m#/5^Q{#^}UzAce-ry~ШjkKhܻߜuQGq 'NO:z'3>u_:bd*mS¡󆆝8sJ'Z'NN(+J(J(N(Jj;Pw(P?HhJ(NhJN%N%z:u( ~W*I3פ{ZqEqڽ6 !^gBq\q¡I)IM Do}nBy?)ty4%5?(u'8'%5'5꨹VGJ˟z%f~ʺx]=ʴawT>c愢/Z֣}aʕqbVĶɔNڔr8(xKc[ۙq|VvIg=ezٷc;q[TTAܡ)8%MSB{rRsһ%T\kf~6yf/^$r,^y7/+iɔCi|8%QD_8r%7]rƏN,-9߮_{eWw>:r]Mof޽f͘jutӯ6m>f~}ŗ^Rӥ*+",qSӮ3:_hEIl '6oW; 6XO0`H&=ܬAY zGJ n3:iEuS-b(2\UΥ_, T)-mkƉ%B8Ou^[UPQ/s˝_}aX6u&LlL:i}ܰ1`䰑-2 !(\\`)1cRjڶ՚ p^~]juŅ3",3Daa0#J[f >jL~;8'^5?674<|W_uE;>z'Cƈi#QjGIpȨ=k>,2v}>_. 7O珏?ӫ~tђkYc9eWY0@''biIFVPhʕTIx2/se@t&nKpCgu^.7gҚXΦ)aӧ26^ U(\l@Y6<!`pt, l&20ӬCa(3IJR fWi R TZV:㇟0qF=՗޳[CGʼbI#KV8 ={$eϋ} @!SCJF ZEGu!2M!zq󛿾57޶|ɿKҶqRf8#,("Κ8`wD ٸsc=[ =iS?+bL~ٙ_=}ݵqe5!H)Bk ̏GݯRJ 3kU\Xл4ID`PbJ#gsIG>f Cֱ=6 4yo ˬF'̡O7Pa@=50 .IÉ4*Bٷw}g+{oc>l6 wniZ߭H9J=z[]tU///{И|KBF alalR7Bfi Z##'tۯԻW\5?5ZV{pϝwM8iY62,p4G"LcJb>@{d2SϞO:7^_Rd jNܶ Zhv8N4j>e+,;܋.HiWLJݽ3Dx.BotAA YSI8)Rw$iD]drY8 uL^!"CX2UbC+ZscSӪv$,ռ;D U* Xhɔ>[=ia#PK#lD&CDr͚53g~+;pA[s}7WEECDR$$#F^Nq`]/V1)Z,*$x0p DXk71(JV%yKn/-.;֭7=_u6g?y_}N:SN?sЈQ@H9@! YK5RF 2ŇYQ`WD6T>Y @׾Y*b:N̘\`:h")G9 i8Dֈ۵!,XcOsZU]jZ @bX>YެK,)Iǎ' t}@nkK455]6ёZ][TPXX5]<4`"te , gO334 ~,Iu3_/|:pؑ=| g BK% H) P$PNH7ENͧ 3+Į! v/Vg wxbvYeP 4:I 9zDC5WoO?ZʭO?-,-?ztZ: KK i*-! Ҭj&8j6l'rNxL,bm4=ECJrTSK;L٭*?-̏if""8Qځι.ъmm-c~kI~^[~u?WoؿWWM1O8^7hA FQ(Fo' .s3MGa0>K?u9SN9q.qBr졅jfN2  Fm hYI d释 3_=쳄#ˋF"k(#A+W?D(@s<.ip矏8a֭uzH BCDԴ1Oٳ ȕDB!V`I)"Z¥9do?4dQ#Z[3 rϦϜC?'sD^a>8 P ݎhkB' {C!\ x*$ȕ? Ӷ!dl<}zf/;*dYI 1H7op9t(lu!v{G2b”A( 4&0G6n^j-ZI@k;R("LJ4:ɕ+Wbh=++-Chb"RZH)H'Yfң`‘ +-;J+[X٣gUQrWSӎ:$*6?[_UUT3E@rH @Ă5?̕"x @?*,ni;X_`&~F! aD5d6 o0-q*lu-C z1 x3vlI#@n3@u ;ˏHiZ+4 (GkiE&@JL04]VV*/[:R=HkTA L)4 DbÆ؁bL "KvaW|ÖgyҢEzVېࣿ_j1_hnCr7hj"4d geտ6|Wnxԏ?mA=z?~|a SΓ&iB c ah 0bp 2|;|M/14oX\$|>*6)w*(oY`*E,3lt+fQ~aT4RIF$bBrJ94i105`8t)+M)R;W:S+"RIp虹ZѣەS);VS]XTT\5 c׶x<`ފUm.Xό8b\}?e)N%/mmyw-\g#뀱cֿ'$I ߙәDfy[YpeoM`1aצ`tAF af'>{C@ي0!ŋ2D aʴA`@PB~vҥ=픤&6P q$F%3H`E  `+5R6D4P+&JvҶ䙑<+Vח^|2 AkB,@VMͻvk#XQR$>_~wlZQ\C8 j]{\{5mvjc'\سZǎ^ ҫ&#eXiK*^]Y7zCB_֛oWI&1?=Vu؉@ 4$q_ H3K39nV!S^&K0@K?4vi :=01d*ddx3GmmRyA3Z  @ GҮqkM ێŢNq4#Pʟ2hd~$ґL HĊ0j !!Ǖcf̊st9?N;ԓO8A:b<Ӱm8FI;yҨ:k,j+(aQ{Z[~(~ϧ1fL6"b7˖/Zҧiةd ,(ȫk)qWK)ۣHbU|/hmM6O9m54D 菹{=`M)AxNF@~%52(0MO250a^4-kaVxD0[%}}<؄b}{ՑG~hn7'̆!RʉEW}wgnPqX Z1KZ dQaaEE@ Ȓ"ҐnǞ3닡CTwoV[[#Mi枽{hqwT:jw|)/Rj3?аfnuw|~n2{-?O>dɬˋ wv$:"9Ҕ}5Msoּgwk{0YVSo̳mཀྵo2ŕc>+@6=>iWDB(4d"a&-B ##Lwrv!"3zYgYi*4M7PI.TeMwuuO_xJH!>& T &(J @:ް$Tul)&DB"hGY'>|};%Ņi(aDD)ZWt¸Cx=e>4l:o޻ߨ#kYİxfϞ9gΒϧU[RR>z&N^յڈ.u.YM3#vt8Ň 0e)/6N9aj!B3E*eH,HFI`H8N' [iMd{zO"M12di#$pO2niؾtɲS0 &g5Fa/f~XAzR3]r'ԲcuW^5)I[)`6 $&$8?8ʉ7njnjӻOaAAEy)82#$"̊t2757+~zI%S]",-lŎ"@0,j͆xσ啗oC¶]M?ݷ‡nSw.\hŒ|pҤ#5zwӮ"~2hoѲ0LCHԻߣ0mofwР!W\qYgQB` 4 pB01Zp {8frBC\{ID0{G I@pCVUVI <`Gi _^_D@JEMqw}ߤ@?v)v 4DDHi&@l1h`AJ)!$LBDCHغ~{}ֺGUUCJM @Lڪ~{}u)Cc~jMcsC,Ͽʷk֎=O~>kmcsƎۭkmQQQmuSݪ*;Z˻T4|wux[:*iVKZZYÿ騁gwީ?:EG2E/!,3 ٌ3 d'hpC]yR_|qY䃘_TIXnk}ѐSxտظxi]˻+Wְ2ĴOg|1닻QZ &0t B8.2J ͻ[hΝa޽iŠrTz:L@mojl:hȔ)S7W^}yO">hϿ`hߨV45M;v{t;vQGQHYKMEE.zh#H.n+_o9c{~7ѣ6PydD!%"01L.v ЁHzvp,$M]½UC: |QP1cʍo?|h&@`[! {IXx&?ĔgK矧 ˊ{UV3HH \a֭['}4(Bl^}f`ԡBHQ Je()@3ٸuX^8bDhl0 @"iZbW_֛=M{Ϝ?4]|̐Akg[n7/Y2)ؑ8炋{tGc{CUeEA,6heOk4Zf>)˿b7]ɧ^TV FB!]%|}uS|: 2Ҟ;!Ҋ^~'?4rn]^=j/w1o^ګo>wm}9؉%ũjiv 1l=}Vػ[ͿǸxێG%նg숑M8ƛo*l`.@D4H!@$"&f)X %3u= >eatQYþ;֨@);`q2NY1B4]ଓOL@!" 򫮺Wם}v~|݆yim۴~ܹ_v$WXYݵm'4%^gʟM:aһoG{P\L H5޽{玆=-b*J֚){|huKׯ_1mw~?c;M<tEqJϾ//?yrg^tƯ~dGSvzKVǓv)+,ۭ֍ w5mߵ_޵GJhOgL g*e h!mBHm]$XiRE¯)ӊE;zZ&ٚVLM!7#]Rʏ*o؃5i"CD*/6λ[3~E?c?qW**A+)ea+֬^|C{M/9c.zY$1@D0T 8SB3!dvv˯/KoZ~;|m3z3.]=UkWvR]ճ{Q5DEq12+ᵛ͛޿kjk=hĈ9 ~ %%"ah"%!z|0}ˤ22A^ܮG&BwħN~'u84 Aa㮴 m[L~يk0o(2-Ck2 ޑHTWt1%^0o¨SDQi)D dOKaÿYLJ'ڛےvkܴER$`۶°@Z@Tw67^+/N *O {!^s]fтn1td}*ToX^a޽⻃ݦ%myyyeERߦϜxz/1A$ ,@QhD'N @-3kr%a y ["2!K"H]N4 %?ݷϐ13 {ЮcV)!-w=or]N?>s'qJⲟ_yuY?hf&Na(kâ8w&ҋ*(`WP{/=j&أ^cFl"HEzoݙw $j}{nwvv/ $I!@ <} jne{`M+(oRr Š+#nQPqOu376RjokP[oWa1[O!OïA;'yzr$p28H1Jb(-=n; @y~*i` @4CQ&cysfs0402ЂAHAr @}"e8$CA^qI~}"# ۩.Z֯kicQ%9[&r I`RP8LMfMW&0=ߠ쭪 NCSSPWygNREy1f۫ 7[]w乐{7 ":Y,8Ť%WAYcrT=b Ar$!0=E 7W f3 Dͺw~H" CD7O0H _:.1`C"Z`H"+]q`& 22$#42̙;lC42rb 8B*F$-(<&0\9ݻb#bMWkQ)uuI)cNTo̡:[XD<5=-sNrr䘨PSn-\7|@CǏlNdhf0"0IpH F85 ߷RHqd$g$8@%())u CRc  DۓA$$0$IoheHu#$&d LP@"$@рIQ RˋL;cxMUuZ_dᐈA,^KR}ŀ Di$+ϙdAܮ>qsvdM׍"K'Ϥ䤧qppZ& b%[kkSSFW'@kW~N:䚺sO;*;~c{wSq4M$^սy)2 ,hDn}e2@HJI X P:^|{pYY!0ɕRu\5) '&n $F FCҘ#8 d\!Mc*> #1v!D``DTWEKN 1#(.E^u|c/ZɻT0 9+7lqy@1?#%/,//72Ѩ~W5wH1-~ȠG F:}̝3KAY!A̡H&1 $1 FI$bXnshs IDATz4g/y[z'$>`ׇ?`(%)XAbt94C`I$-h.$0D S$%(h1)ؗTճKII*I1@QN~V{,M#O^x44tVl/G^7 ԡIG @!HD%eΣ<:E )I^=XvY#@fKQq k/.L'"(9=ѣlȬsgWffFF_yŠ~uiYL6mD޽! @f./C/]RZe!  377mh 8N f},KgJ`31K:&Мvќݲ%n&\ xe:p(O>5UQif 2*v㶁cW/뚚 Ј`XW^0!>^ 3 >-} ^B i,`'\׋:Չh i$Db18%ɸE)uرoy ^N]fY)O A0_zO~۰a) zgdڻi!'߾uzH yogO?}bGs3 `#F@HGAD$}SK&) 0Ғ̜{2 #qKj0)U /*#"!L4η@V"׀{$M&>{ o5)-@` "#d*&W[v7si$VX؄y ٳ=߹$05:!%61W,414d)yr:0ĉ#-<liikyQQaAS}Xk Bu |%O?w_ڳP+ ?@14B"RGb! F%_F).(QG#=]BXBU%q2 Rpv{M^={P1ӴUjix][;*@S\MAO[UPF3wnܺ~D\#7qރ{ 41& Ya"B1Ȗt  fV'$AJM\ąKCx)(*r) 0C xWRu.x))IDx>  EW?~z.g2h6Cdko.G^k ' Q.?MW;=\4s棇qU h s&O_rVj[66&&V7̄aY/rn= "єONV(*~ӒsN䦤^tpO #@[0ʼn߶u˗$2d+L#ɡ"&6] `CkZ[-_;VQ[2Juy$A` ٴOe$I !!&*QR~_DؼvΛߺVLØqx^4sxa@ddDH`pYZ9; }"=*-(ʬn7˗qU?|P^IoF=ܽoko; +ZRSggg} /7r3?FUV E*mM=yjTDsbl: cJ@4`$I`Ef֣z6 LB$ 0!DXԅa$ E]`x#a.ƒTIKUrbiRƀ0q9"0 Båk3Krn߯9Ao.~޶Y2j -d^9 5TfϚ5fH=㎟1EP8oo&Oc^> Vf~g*+}__+l>ƚ@(#++,:~,ᘒW^8|8کˤÇS3nI3߷!\XVF0fC-#RK^uUQ^$ 0l~(DmhIRlr'Plp  `D1$GZ)Gqf69EFZFiiխZd~l{vS321{Gj_|5?}ޢlɛs}1>v!a-Z\P\7Ewh6INښ;{4XlO?o"$=(1#QXV@3*<#k㾽J=ۿ/#% {øV@t%(4L#L#$dP~Qp-u" O4/4npJ|i$@FbADG:1P(tvlʫ͛\jfcW< PSVqG_GgQAQnjb2z^Z`ɓ&)*)MlCC=z攖[(|zls+ ۷So=cajlgmaqҌr;?.-S,()j+}Çdl阘H>FáiE$2$$(-1h_EIsq Jk:;3A IMr@Qvrڅ=lڵCwSkZ 5uw5 l Ck-MУG֜>|aDB2INV0kѷ}]~]vn޽֮YSRThlYKwpߖ>JI~$ɓRUyyGsVO~]CBHME]k~Nm||NVCFyyih i PD+rVyw+))LEJ 2;"DK{ BH./1qxDzY1({ h쮎ٱyϾgOn/O32 :jk?33qc%e-MvuuҢi#Z#qdzڡ']@5+!{qހ4 @@2@1I` $DIq" `hEdAcT䩪ʪͻ$DVUV5RLqvVGM٫gOsO_3VL[wE"dџsQs[KkYYo1&˿"B4VΚ:G#Odz "ƍ[c1#mqf[+^q }Bî_\VQD".zC@Auqɲ_eٕA 1Қ:9kOW~ްIUEe* Pp֭Astux%TTG1fNFj %wUºڊ7oDby(*ش<~Ưk[1 ƍ8qҤy ^|œРVODFZA^^Zjk6 2^UsKLo}l 3WV vf! ƆFڭ߾637G]V9޷3aq5}Ƞcccc_V!)848amrjjӲjjkUUUu tT;k'9`ѲU6Opxuu~|d4g/>||6C 3}ox|^ '5dK0Օ5h 'YqHr{۷lv7$‚xy ݿI(Jgik{jencmo)N=/ oUX\fB>ieղ2222Ou{KNIaĉS$bdh \}u޽ah;r ư#l=yzر <-+VR+pmM%M(4Y[84-ԕZ%ݣG63cM7Wt3msOvݝgek)Xi{~`{nFm鋆RyYk׭)~QU' x"Y"d5Ti"b7'\3yCgN h Sx514*mm[\Tr٭kVr 0:J*43j䴴.^]WNLYh[vRSW12iUYy3Fi+WBCې[>c\psNK%{ CYyEYUl ²]b/FN{p]|qh` ƴO'<:{z4aWoƍ.^ ť$OĴ7kjkv25oæ79 8))8=Ȥq:ۮکqƝ`âCC.["/rWv.=ziU SCe99[K*4YF7H9YL= )FX]?cdG+ RV4}|ż{uhu22z &)~:~;Rӳ'͘sN{ oč/޸A8*:VFAaيga{oB²EK;`ocǍmaj"+^㆏ܥKi勜⢜bm=^u}gE T vv3M=!:1Cpji87۞$tvkӏwx"\\&L5st%ǟ޼1`iWvvtKx}K),_g0+6o2(,j$/ϭ5[%w QIUԴ؄ػN`\!cdvWc\!`x{9i'Ϣ kSVLCLR|Fnοi?4lÎF&~umACiK \9yvn=l+>>Xdh}/<ƘXqX,c|ߘis1.%c\yKKj/o@+6V6'Ο``_dhh*5yݥ;>:I7}<Əht붝&G`l@fG?}vܼ"77nN=s7׾ |%UNNdKtڵʄ'QVLvA}/a!1KQ\/kx? ߳ǻĴetՏ "N?k9ʄw˗Hpp~'.XZQGMАg)YYnXXԄ:#F7[ PQV>yG+5+ٮ=<﫦ɹɩqmi(HIJ~D7+ץgcO83puB1smjǑW_wڵ˾Q5xg ?lپ}ˮޭ n[v-xƢYu!IU"ݺmǀc,8yHZzV^bTf~iS.]jYQZu豍|1?m*J ,o"RYw'gg4xʔA{-)aQFOfaf<@KKҗG!^I_ʚpbE/?uPNkjM ||3.>k<}/w QH|ԵWk4' 1|6GORSc''dhdhog7ss:#yٶeMç]vVWVzo[0spM}CNZ[wc'v[wk8z,5%erk^^ҹ K##{+Fv&ͻ/_г_Ok׮nX`d(~iYѣEETv,_Q`à@m=]k׾yJ~aq|?O  z޲~3fv8tdIqe_-Z~׶[]6$ybMvtnVɋ6̝[]'={3ZӳWRtO?YY}lٝk>9xG2_WgZX|ʪ!37ʥ×1G  ;?iRGvoQ|2yc#[yBhY: ̺s硟obR"`<})cZY*}I BL;+s > uc znzs#gvvN]~̤ɓKgc%u;|萱Xf q3*rJ]tc0a_Rr N9.FڐiKK9k|Y=NM11ջS7!gΘ1a,=|4>&{wyfG?s 7!"nǶoz^X{xx{ 356Y'hGO&R {Iu\}•NyIx/s߁^XԵ[gϢ11qɳ|QmeuqIVt/:a2QJPܸdJN޺m_|>-v}6n !n*U%Ć-;J_Wlac7+j`ۘ¯}pvFfzޑc2cuU{4G/G iYi֊Xy֫j6jUUyz&&tut\M=sn 'M~II]N)_=e xYYYQ11!ajgOX_ g񉆆-"D|eE/vuU5 ,Zr);rM[7u~vq?Nj1C*By^^qQ1Q* %+.3fXD?O=~ǻkFs*+*-_^)(VVTPWzuЩmK.hsF+ŋ{va1ag^sH/q(Yж84yZڂYR|3w=gOJ Jv>vֽ;]6Ys1^xժE/^ >48:%Ӻ.)8vHYAajh0O/Eg@~ÆS,e&Z,c={VzQ8='ĘūWc=faSs"R|qwFuۂC>"P$~(k̬Os!vBFu%E}^Y[S[T\Zu0k{K_ҹw>^xϞ>s\U[UU S;jX~ںCGWwiW\K3oڞ?XJNvok˗+kV/Z0ii˗1'9/*KJOJKN~.[LAFf@?W+K ?fԳN(|bͱ٣Y@\^CGc9wۭ֭5ر99"g'(*1də3gf{ E_]x3q܄';곦ŗOqꦠ{Q~~nsfL< ҍA~!h⥫dбow9)rXq] B@È?/1ӧ2DܥKWGOAe,ryGۏCFi.ǚ5%Eϊz1!͜:kW1!b Eꚸ }PӼa*j JMsOHK/:Y+nboUsj}2/kX9V7S KkfzvuvW3boh|kXȪkrj}qYcesEֺC/+,95ݹn(XLO{$8 ׿KC)5eΎo? V`M  U)ŇNNOp0ĭ`obe~sv>kXRҼtz8t:HӦlwEjUE㇁O0kssFF {7˚,Qa'O렯=zywkX`XT42$77_Ol=|55 ,X|Ÿϻ{^VMW;kTEucg<ؾqkX/0%yϞKʭcg0}hc[ }$pv:ov4`_²a.w젯-zIӈ^.]xsGbM eD^y(?'EjNv&fNNNz:}/]eM e%ёёյ5ʈr:|n<}#? SsK4`Fq#2, ,~Ds /"DƎbeɚ,4=ܰ~NLtfl3v>XBܼo}qyCtg} ,Xx\f4`- !`kX`,X i`kX`,X i`׃‚M) IENDB`chaussette-1.3.0/docs/source/index.rst000066400000000000000000000202561253327544200177700ustar00rootroot00000000000000Chaussette WSGI Server ====================== .. image:: images/chaussette.png :align: right **Chaussette** is a WSGI server you can use to run your Python WSGI applications. The particularity of **Chaussette** is that it can either bind a socket on a port like any other server does **or** run against **already opened sockets**. That makes **Chaussette** the best companion to run a WSGI or Django stack under a process and socket manager, such as `Circus `_ or `Supervisor `_. .. image:: https://secure.travis-ci.org/circus-tent/chaussette.png?branch=master :alt: Build Status :target: https://secure.travis-ci.org/circus-tent/chaussette/ .. image:: https://coveralls.io/repos/circus-tent/chaussette/badge.png?branch=master :alt: Coverage Status on master :target: https://coveralls.io/r/circus-tent/chaussette?branch=master Usage ===== You can run a plain WSGI application, a Django application, or a Paste application. To get all options, just run *chaussette --help*. Running a plain WSGI application -------------------------------- **Chaussette** provides a console script you can launch against a WSGI application, like any WSGI server out there: .. code-block:: bash $ chaussette mypackage.myapp Application is Serving on localhost:8080 Using as a backend Running a Django application ---------------------------- **Chaussette** allows you to run a Django project. You just need to provide the Python import path of the WSGI application, commonly located in the Django project's ``wsgi.py`` file. For further information about how the ``wsgi.py`` file should look like see the `Django documentation`_. Here's an example: .. code-block:: bash $ chaussette --backend gevent mysite.wsgi.application Application is Serving on localhost:8080 Using as a backend .. _`Django documentation`: https://docs.djangoproject.com/en/1.4/howto/deployment/wsgi/ Running a Python Paste application ---------------------------------- **Chaussette** will let you run a project based on a `Python Paste `_ configuration file. You just need to use to provide the path to the configuration file in the **application**, prefixed with **paste:** Here's an example: .. code-block:: bash $ chaussette paste:path/to/configuration.ini $ Application is $ Serving on localhost:8080 $ Using as a backend Using Chaussette in Circus ========================== The typical use case is to run Chaussette processes under a process and socket manager. Chaussette was developed to run under `Circus `_, which takes care of binding the socket and spawning Chaussette processes. To run your WSGI application using Circus, define a *socket* section in your configuration file, then add a Chaussette watcher. Minimal example: .. code-block:: ini [circus] endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:web] cmd = chaussette --fd $(circus.sockets.web) --backend meinheld server.app use_sockets = True numprocesses = 5 [socket:web] host = 0.0.0.0 port = 8000 When Circus runs, it binds a socket on the *8000* port and passes the file descriptor value to the Chaussette process, by replacing *${socket:web}* by the file number value. Using Chaussette in Supervisor ============================== `Supervisor `_ includes a socket manager since version 3.0a7, released in 2009. It was originally developed to support FastCGI processes and thus the configuration section is called *fcgi-program*. Despite the name, it is not tied to the FastCGI protocol. Supervisor can bind the socket and then spawn Chaussette processes. To run your WSGI application using Supervisor, define an *fcgi-program* section in your configuration file. Minimal example: .. code-block:: ini [supervisord] logfile = /tmp/supervisord.log [inet_http_server] port = 127.0.0.1:9001 [supervisorctl] serverurl = http://127.0.0.1:9001 [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [fcgi-program:web] command = chaussette --fd 0 --backend meinheld server.app process_name = %(program_name)s_%(process_num)s numprocs = 5 socket = tcp://0.0.0.0:8000 Notice the ``--fd 0`` argument to ``chaussette``. Each *fcgi-program* section defines its own socket and the file descriptor is always ``0``. See the `Supervisor manual `_ for detailed information. Supervisor will create the socket before spawning the first Chaussette child process. When the last child exits, Supervisor will close the socket. Backends ======== Chaussette is just a bit of glue code on the top of existing WSGI servers, and is organized around **back ends**. By default Chaussette uses a pure Python implementation based on **wsgiref**, but it also provides more efficient back ends. Most of them are for Python 2 only, but Chaussette can be used under Python 3 with a few of them - marked in the list below: - **gevent** -- based on Gevent's *pywsgi* server - **fastgevent** -- based on Gevent's *wsgi* server -- faster but does not support streaming. - **meinheld** -- based on Meinheld's fast C server - **waitress** -- based on Pyramid's waitress pure Python web server (py3) - **eventlet** -- based on Eventlet's wsgi server - **geventwebsocket** -- Gevent's **pywsgi** server coupled with **geventwebsocket** handler. - **geventws4py** -- Gevent's **pywsgi** server coupled with **ws4py** handler. - **socketio** -- based on gevent-socketio, which is a custom Gevent server & handler that manages the socketio protocol. - **bjoern** -- based on Bjoern. - **tornado** -- based on Tornado's wsgi server. You can select your backend by using the **--backend** option and providing its name. For some backends, you need to make sure the corresponding libraries are installed: - **gevent** and **fastgevent**: `pip install gevent` - **meinheld** : `pip install meinheld` - **waitress** : `pip install waitress` - **eventlet** : `pip install eventlet` - **geventwebsocket**: `pip install gevent-websocket` - **geventws4py**: `pip install ws4py` - **socketio**: `pip install gevent-socketio` - **bjoern**: `pip install bjoern` - **tornado**: `pip install tornado` If you want to add your favorite WSGI Server as a backend to Chaussette, or if you think you can make one of the backend Python 3 compatible, send me an e-mail ! If you curious about how each on of those backends performs, you can read: - http://blog.ziade.org/2012/06/28/wgsi-web-servers-bench/ - http://blog.ziade.org/2012/07/03/wsgi-web-servers-bench-part-2/ Rationale and Design ==================== Most WGSI servers out there provide advanced features to scale your web applications, like multi-threading or multi-processing. Depending on the project, the *process management* features, like respawning processes that die, or adding new ones on the fly, are not always very advanced. On the other hand, tools like Circus and Supervisor have more advanced features to manage your processes, and are able to manage sockets as well. The goal of *Chaussette* is to delegate process and socket management to its parent process and just focus on serving requests. Using a pre-fork model, the process manager binds a socket. It then forks Chaussette child processes that accept connections on that socket. For more information about this design, read : - http://blog.ziade.org/2012/06/12/shared-sockets-in-circus. - http://circus.readthedocs.org/en/latest/for-ops/sockets/ Useful links ============ - Repository : https://github.com/circus-tent/chaussette - Documentation : https://chaussette.readthedocs.org - Continuous Integration: https://travis-ci.org/circus-tent/chaussette chaussette-1.3.0/examples/000077500000000000000000000000001253327544200155105ustar00rootroot00000000000000chaussette-1.3.0/examples/__init__.py000066400000000000000000000000001253327544200176070ustar00rootroot00000000000000chaussette-1.3.0/examples/flaskapp.py000066400000000000000000000002221253327544200176570ustar00rootroot00000000000000from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" if __name__ == "__main__": app.run() chaussette-1.3.0/examples/tornadoapp.py000066400000000000000000000021761253327544200202370ustar00rootroot00000000000000from tornado.web import Application, RequestHandler from tornado.httpserver import HTTPServer from tornado.tcpserver import TCPServer from tornado.wsgi import WSGIApplication class HelloHandler(RequestHandler): def get(self): self.write(b"Hello, World\n") class HelloServer(TCPServer): def handle_stream(self, stream, address): stream.write(b"Hello, World\n") stream.close() # serve a tornado app like: # chaussette --backend tornado examples.tornado.app.tornadoapp # test is with: # curl http://127.0.0.1:8080/ tornadoapp = Application([('/', HelloHandler)]) # serve a wsgi app: # chaussette --backend tornado examples.tornado.app.wsgiapp # test is with: # curl http://127.0.0.1:8080/ wsgiapp = WSGIApplication([('/', HelloHandler)]) # serve a tornado HTTPServer: # chaussette --backend tornado examples.tornado.app.hellohttp # test is with: # curl http://127.0.0.1:8080/ hellohttp = HTTPServer(tornadoapp) # serve a tornado TCPServer: # chaussette --backend tornado examples.tornado.app.hellotcp # beware this is NOT a HTTP server, test it with: # nc 127.0.0.1 8080 hellotcp = HelloServer() chaussette-1.3.0/setup.cfg000066400000000000000000000000261253327544200155110ustar00rootroot00000000000000[wheel] universal = 1 chaussette-1.3.0/setup.py000066400000000000000000000043171253327544200154110ustar00rootroot00000000000000import sys from setuptools import setup, find_packages from chaussette import __version__ if (not hasattr(sys, 'version_info') or sys.version_info < (2, 6, 0, 'final') or (sys.version_info > (3,) and sys.version_info < (3, 3, 0, 'final'))): raise SystemExit("Chaussette requires Python 2.6, 2.7, 3.3 or later.") PYPY = hasattr(sys, 'pypy_version_info') PY26 = (2, 6, 0, 'final') <= sys.version_info < (2, 7, 0, 'final') install_requires = ['six >= 1.3.0'] if PY26: install_requires.append('ordereddict') try: import argparse # NOQA except ImportError: install_requires.append('argparse') with open('README.rst') as f: README = f.read() tests_require = ['nose', 'waitress', 'tornado', 'requests', 'minimock'] if not PYPY: tests_require += ['meinheld', 'greenlet'] if sys.version_info[0] == 2: tests_require += ['PasteDeploy', 'Paste', 'unittest2', 'ws4py'] if not PYPY: tests_require += ['gevent', 'gevent-websocket', 'eventlet', 'gevent-socketio', 'bjoern'] setup(name='chaussette', version=__version__, url='http://chaussette.readthedocs.org', packages=find_packages(exclude=['examples', 'examples.simple_chat']), description=("A WSGI Server for Circus"), long_description=README, author="Mozilla Foundation & Contributors", author_email="services-dev@lists.mozila.org", include_package_data=True, zip_safe=False, classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "License :: OSI Approved :: Apache Software License", "Development Status :: 3 - Alpha"], install_requires=install_requires, tests_require=tests_require, test_suite='nose.collector', entry_points=""" [console_scripts] chaussette = chaussette.server:main [paste.server_runner] main = chaussette.server:serve_paste """) chaussette-1.3.0/sitecustomize.py000066400000000000000000000000531253327544200171510ustar00rootroot00000000000000import coverage coverage.process_startup() chaussette-1.3.0/tox.ini000066400000000000000000000020731253327544200152070ustar00rootroot00000000000000[tox] envlist = flake8,py26,py27,py33,py34,docs [testenv] commands = {envbindir}/python -V {envbindir}/python setup.py test deps = nose waitress tornado requests minimock meinheld greenlet [testenv:py26] deps = nose waitress tornado requests minimock meinheld greenlet Paste PasteDeploy unittest2 ws4py gevent gevent-websocket gevent-socketio eventlet bjoern [testenv:py27] deps = nose waitress tornado requests minimock meinheld greenlet Paste PasteDeploy unittest2 ws4py gevent gevent-websocket gevent-socketio eventlet bjoern [testenv:py33] deps = nose waitress tornado requests minimock meinheld greenlet ws4py eventlet [testenv:py34] deps = nose waitress tornado requests minimock meinheld greenlet ws4py eventlet [testenv:docs] deps = Sphinx commands = /usr/bin/make docs [testenv:flake8] deps = flake8 commands = flake8 chaussette