pax_global_header00006660000000000000000000000064136225367740014531gustar00rootroot0000000000000052 comment=f850b73c1b1983daf9d7f7783e4a20ebe5707cb4 uvicorn-0.11.3/000077500000000000000000000000001362253677400133005ustar00rootroot00000000000000uvicorn-0.11.3/.codecov.yml000066400000000000000000000002031362253677400155160ustar00rootroot00000000000000coverage: precision: 2 round: down range: "80...100" status: project: yes patch: no changes: no comment: off uvicorn-0.11.3/.gitignore000066400000000000000000000001221362253677400152630ustar00rootroot00000000000000.cache .coverage .mypy_cache/ __pycache__/ uvicorn.egg-info/ venv/ htmlcov/ site/ uvicorn-0.11.3/.travis.yml000066400000000000000000000025361362253677400154170ustar00rootroot00000000000000cache: pip language: python matrix: include: # The Windows job is important (obviously) to test on a non-Unix platform. # It also runs the test with a quite minimal setup (no uvloop or httptools), # but it does have websockets (needed to run the websockets tests). - os: windows language: bash name: Windows # The pypy3 job runs a fully pure Python setup, which triggers a part of the # auto-detect tests that the other tests don't. We cannot test websockets though. - os: linux python: "pypy3" # The main tests run on Linux for a all relevant Python versions. - os: linux python: "3.6" - os: linux python: "3.7" - os: linux python: "3.8" install: - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install python3; export PATH=/c/Python37:/c/Python37/Scripts:/c/Python38:/c/Python38/Scripts:$PATH; python -m pip install -U click h11 wsproto==0.13.* websockets==8.*; python -m pip install -U autoflake black codecov isort pytest pytest-cov requests; elif [ "$TRAVIS_PYTHON_VERSION" = "pypy3" ]; then pip install -U click h11 wsproto==0.13.*; pip install -U autoflake codecov isort pytest pytest-cov requests; else pip install -U -r requirements.txt; fi; script: - scripts/test after_script: - codecov uvicorn-0.11.3/CHANGELOG.md000066400000000000000000000037711362253677400151210ustar00rootroot00000000000000# Change Log ## 0.11.3 * Update dependencies. ## 0.11.2 * Don't open socket until after application startup. * Support `--backlog`. ## 0.11.1 * Use a more liberal `h11` dependency. Either `0.8.*` or `0.9.*``. ## 0.11.0 * Fix reload/multiprocessing on Windows with Python 3.8. * Drop IOCP support. (Required for fix above.) * Add `uvicorn --version` flag. * Add `--use-colors` and `--no-use-colors` flags. * Display port correctly, when auto port selection isused with `--port=0`. ## 0.10.8 * Fix reload/multiprocessing error. ## 0.10.7 * Use resource_sharer.DupSocket to resolve socket sharing on Windows. ## 0.10.6 * Exit if `workers` or `reload` are use without an app import string style. * Reorganise supervisor processes to properly hand over sockets on windows. ## 0.10.5 * Update uvloop dependency to 0.14+ ## 0.10.4 * Error clearly when `workers=` is used with app instance, instead of an app import string. * Switch `--reload-dir` to current working directory by default. ## 0.10.3 * Add ``--log-level trace` ## 0.10.2 * Enable --proxy-headers by default. ## 0.10.1 * Resolve issues with logging when using `--reload` or `--workers`. * Setup up root logger to capture output for all logger instances, not just `uvicorn.error` and `uvicorn.access`. ## 0.10.0 * Support for Python 3.8 * Separated out `uvicorn.error` and `uvicorn.access` logs. * Coloured log output when connected to a terminal. * Dropped `logger=` config setting. * Added `--log-config [FILE]` and `log_config=[str|dict]`. May either be a Python logging config dictionary or the file name of a logging configuration. * Added `--forwarded_allow_ips` and `forwarded_allow_ips`. Defaults to the value of the `$FORWARDED_ALLOW_IPS` environment variable or "127.0.0.1". The `--proxy-headers` flag now defaults to `True`, but only trusted IPs are used to populate forwarding info. * The `--workers` setting now defaults to the value of the `$WEB_CONCURRENCY` environment variable. * Added support for `--env-file`. Requires `python-dotenv`. uvicorn-0.11.3/LICENSE.md000066400000000000000000000027651362253677400147160ustar00rootroot00000000000000Copyright © 2017-present, [Encode OSS Ltd](http://www.encode.io/). All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. uvicorn-0.11.3/README.md000066400000000000000000000041051362253677400145570ustar00rootroot00000000000000

uvicorn

The lightning-fast ASGI server.

--- [![Build Status](https://travis-ci.org/encode/uvicorn.svg?branch=master)](https://travis-ci.org/encode/uvicorn) [![Coverage](https://codecov.io/gh/encode/uvicorn/branch/master/graph/badge.svg)](https://codecov.io/gh/encode/uvicorn) [![Package version](https://badge.fury.io/py/uvicorn.svg)](https://pypi.python.org/pypi/uvicorn) **Documentation**: [https://www.uvicorn.org](https://www.uvicorn.org) **Community**: [https://discuss.encode.io/c/uvicorn](https://discuss.encode.io/c/uvicorn) **Requirements**: Python 3.6+ (For Python 3.5 support, install version 0.8.6.) Uvicorn is a lightning-fast ASGI server implementation, using [uvloop][uvloop] and [httptools][httptools]. Until recently Python has lacked a minimal low-level server/application interface for asyncio frameworks. The [ASGI specification][asgi] fills this gap, and means we're now able to start building a common set of tooling usable across all asyncio frameworks. Uvicorn currently supports HTTP/1.1 and WebSockets. Support for HTTP/2 is planned. ## Quickstart Install using `pip`: ```shell $ pip install uvicorn ``` Create an application, in `example.py`: ```python async def app(scope, receive, send): assert scope['type'] == 'http' await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) await send({ 'type': 'http.response.body', 'body': b'Hello, world!', }) ``` Run the server: ```shell $ uvicorn example:app ``` ---

Uvicorn is BSD licensed code.
Designed & built in Brighton, England.

— 🦄 —

[uvloop]: https://github.com/MagicStack/uvloop [httptools]: https://github.com/MagicStack/httptools [asgi]: https://asgi.readthedocs.io/en/latest/ uvicorn-0.11.3/docs/000077500000000000000000000000001362253677400142305ustar00rootroot00000000000000uvicorn-0.11.3/docs/CNAME000066400000000000000000000000201362253677400147660ustar00rootroot00000000000000www.uvicorn.org uvicorn-0.11.3/docs/deployment.md000066400000000000000000000264721362253677400167450ustar00rootroot00000000000000# Deployment Server deployment is a complex area, that will depend on what kind of service you're deploying Uvicorn onto. As a general rule, you probably want to: * Run `uvicorn --reload` from the command line for local development. * Run `gunicorn -k uvicorn.workers.UvicornWorker` for production. * Additionally run behind Nginx for self-hosted deployments. * Finally, run everything behind a CDN for caching support, and serious DDOS protection. ## Running from the command line Typically you'll run `uvicorn` from the command line. ```bash $ uvicorn example:app --reload --port 5000 ``` The ASGI application should be specified in the form `path.to.module:instance.path`. When running locally, use `--reload` to turn on auto-reloading. To see the complete set of available options, use `uvicorn --help`: ``` $ uvicorn --help Usage: uvicorn [OPTIONS] APP Options: --host TEXT Bind socket to this host. [default: 127.0.0.1] --port INTEGER Bind socket to this port. [default: 8000] --uds TEXT Bind to a UNIX domain socket. --fd INTEGER Bind to socket from this file descriptor. --reload Enable auto-reload. --reload-dir TEXT Set reload directories explicitly, instead of using the current working directory. --workers INTEGER Number of worker processes. Defaults to the $WEB_CONCURRENCY environment variable if available. Not valid with --reload. --loop [auto|asyncio|uvloop|iocp] Event loop implementation. [default: auto] --http [auto|h11|httptools] HTTP protocol implementation. [default: auto] --ws [auto|none|websockets|wsproto] WebSocket protocol implementation. [default: auto] --lifespan [auto|on|off] Lifespan implementation. [default: auto] --interface [auto|asgi3|asgi2|wsgi] Select ASGI3, ASGI2, or WSGI as the application interface. [default: auto] --env-file PATH Environment configuration file. --log-config PATH Logging configuration file. --log-level [critical|error|warning|info|debug|trace] Log level. [default: info] --access-log / --no-access-log Enable/Disable access log. --use-colors / --no-use-colors Enable/Disable colorized logging. --proxy-headers / --no-proxy-headers Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info. --forwarded-allow-ips TEXT Comma seperated list of IPs to trust with proxy headers. Defaults to the $FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. --root-path TEXT Set the ASGI 'root_path' for applications submounted below a given URL path. --limit-concurrency INTEGER Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses. --backlog INTEGER Maximum number of connections to hold in backlog --limit-max-requests INTEGER Maximum number of requests to service before terminating the process. --timeout-keep-alive INTEGER Close Keep-Alive connections if no new data is received within this timeout. [default: 5] --ssl-keyfile TEXT SSL key file --ssl-certfile TEXT SSL certificate file --ssl-version INTEGER SSL version to use (see stdlib ssl module's) [default: 2] --ssl-cert-reqs INTEGER Whether client certificate is required (see stdlib ssl module's) [default: 0] --ssl-ca-certs TEXT CA certificates file --ssl-ciphers TEXT Ciphers to use (see stdlib ssl module's) [default: TLSv1] --header TEXT Specify custom default HTTP response headers as a Name:Value pair --help Show this message and exit. ``` See the [settings documentation](settings.md) for more details on the supported options for running uvicorn. ## Running programmatically To run directly from within a Python program, you should use `uvicorn.run(app, **config)`. For example: **example.py**: ```python import uvicorn class App: ... app = App() if __name__ == "__main__": uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info") ``` The set of configuration options is the same as for the command line tool. Note that the application instance itself *can* be passed instead of the app import string. ```python uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info") ``` However, this style only works if you are not using multiprocessing (`workers=NUM`) or reloading (`reload=True`), so we recommend using the import string style. ## Using a process manager Running Uvicorn using a process manager ensures that you can run multiple processes in a resilient manner, and allows you to perform server upgrades without dropping requests. A process manager will handle the socket setup, start-up multiple server processes, monitor process aliveness, and listen for signals to provide for processes restarts, shutdowns, or dialing up and down the number of running processes. Uvicorn provides a lightweight way to run multiple worker processes, for example `--workers 4`, but does not provide any process monitoring. ### Gunicorn Gunicorn is probably the simplest way to run and manage Uvicorn in a production setting. Uvicorn includes a gunicorn worker class that means you can get set up with very little configuration. The following will start Gunicorn with four worker processes: `gunicorn -w 4 -k uvicorn.workers.UvicornWorker` The `UvicornWorker` implementation uses the `uvloop` and `httptools` implementations. To run under PyPy you'll want to use pure-python implementation instead. You can do this by using the `UvicornH11Worker` class. `gunicorn -w 4 -k uvicorn.workers.UvicornH11Worker` Gunicorn provides a different set of configuration options to Uvicorn, so some options such as `--limit-concurrency` are not yet supported when running with Gunicorn. ### Supervisor To use `supervisor` as a process manager you should either: * Hand over the socket to uvicorn using its file descriptor, which supervisor always makes available as `0`, and which must be set in the `fcgi-program` section. * Or use a UNIX domain socket for each `uvicorn` process. A simple supervisor configuration might look something like this: **supervisord.conf**: ```ini [supervisord] [fcgi-program:uvicorn] socket=tcp://localhost:8000 command=venv/bin/uvicorn --fd 0 example:App numprocs=4 process_name=uvicorn-%(process_num)d stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 ``` Then run with `supervisord -n`. ### Circus To use `circus` as a process manager, you should either: * Hand over the socket to uvicorn using its file descriptor, which circus makes available as `$(circus.sockets.web)`. * Or use a UNIX domain socket for each `uvicorn` process. A simple circus configuration might look something like this: **circus.ini**: ```ini [watcher:web] cmd = venv/bin/uvicorn --fd $(circus.sockets.web) example:App use_sockets = True numprocesses = 4 [socket:web] host = 0.0.0.0 port = 8000 ``` Then run `circusd circus.ini`. ## Running behind Nginx Using Nginx as a proxy in front of your Uvicorn processes may not be neccessary, but is recommended for additional resiliance. Nginx can deal with serving your static media and buffering slow requests, leaving your application servers free from load as much as possible. In managed environments such as `Heroku`, you wont typically need to configure Nginx, as your server processes will already be running behind load balancing proxies. The recommended configuration for proxying from Nginx is to use a UNIX domain socket between Nginx and whatever the process manager that is being used to run Uvicorn. When fronting the application with a proxy server you want to make sure that the proxy sets headers to ensure that application can properly determine the client address of the incoming connection, and if the connection was over `http` or `https`. You should ensure that the `X-Forwarded-For` and `X-Forwarded-Proto` headers are set by the proxy, and that Uvicorn is run using the `--proxy-headers` setting. This ensure that the ASGI scope includes correct `client` and `scheme` information. Here's how a simple Nginx configuration might look. This example includes setting proxy headers, and using a UNIX domain socket to communicate with the application server. ```conf http { server { listen 80; client_max_body_size 4G; server_name example.com; location / { proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; proxy_buffering off; proxy_pass http://uvicorn; } location /static { # path for static files root /path/to/app/static; } } upstream uvicorn { server unix:/tmp/uvicorn.sock; } } ``` Uvicorn's `--proxy-headers` behavior may not be sufficient for more complex proxy configurations that use different combinations of headers, or where the application is running behind more than one intermediary proxying service. In those cases you might want to use an ASGI middleware to set the `client` and `scheme` dependant on the request headers. ## Running behind a CDN Running behind a content delivery network, such as Cloudflare or Cloud Front, provides a serious layer of protection against DDOS attacks. Your sevice will be running behind huge clusters of proxies and load balancers that are designed for handling huge amounts of traffic, and have capabilities for detecting and closing off connections from DDOS attacks. Proper usage of cache control headers can mean that a CDN is able to serve large amounts of data without always having to forward the request on to your server. Content Delivery Networks can also be a low-effort way to provide HTTPS termination. ## Running with HTTPS To run uvicorn with https, a certificate and a private key are required. The recommended way to get them is using [Let's Encrypt][letsencrypt]. For local development with https, it's possible to use [mkcert][mkcert] to generate a valid certificat and private key. ```bash $ uvicorn example:app --port 5000 --ssl-keyfile=./key.pem --ssl-certfile=./cert.pem ``` ### Running gunicorn worker It also possible to use certificates with uvicorn's worker for gunicorn ```bash $ gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornWorker example:app ``` [letsencrypt]: https://letsencrypt.org/ [mkcert]: https://github.com/FiloSottile/mkcert uvicorn-0.11.3/docs/index.md000066400000000000000000000312021362253677400156570ustar00rootroot00000000000000

uvicorn

The lightning-fast ASGI server.

--- # Introduction Uvicorn is a lightning-fast ASGI server, built on [uvloop][uvloop] and [httptools][httptools]. Until recently Python has lacked a minimal low-level server/application interface for asyncio frameworks. The [ASGI specification][asgi] fills this gap, and means we're now able to start building a common set of tooling usable across all asyncio frameworks. ASGI should help enable an ecosystem of Python web frameworks that are highly competitive against Node and Go in terms of achieving high throughput in IO-bound contexts. It also provides support for HTTP/2 and WebSockets, which cannot be handled by WSGI. Uvicorn currently supports HTTP/1.1 and WebSockets. Support for HTTP/2 is planned. --- ## Quickstart Requirements: Python 3.5, 3.6, 3.7 Install using `pip`: ``` $ pip install uvicorn ``` Create an application, in `example.py`: ```python async def app(scope, receive, send): assert scope['type'] == 'http' await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] }) await send({ 'type': 'http.response.body', 'body': b'Hello, world!', }) ``` Run the server: ``` $ uvicorn example:app ``` --- ## Usage The uvicorn command line tool is the easiest way to run your application... ### Command line options ``` Usage: uvicorn [OPTIONS] APP Options: --host TEXT Bind socket to this host. [default: 127.0.0.1] --port INTEGER Bind socket to this port. [default: 8000] --uds TEXT Bind to a UNIX domain socket. --fd INTEGER Bind to socket from this file descriptor. --reload Enable auto-reload. --reload-dir TEXT Set reload directories explicitly, instead of using the current working directory. --workers INTEGER Number of worker processes. Defaults to the $WEB_CONCURRENCY environment variable if available. Not valid with --reload. --loop [auto|asyncio|uvloop|iocp] Event loop implementation. [default: auto] --http [auto|h11|httptools] HTTP protocol implementation. [default: auto] --ws [auto|none|websockets|wsproto] WebSocket protocol implementation. [default: auto] --lifespan [auto|on|off] Lifespan implementation. [default: auto] --interface [auto|asgi3|asgi2|wsgi] Select ASGI3, ASGI2, or WSGI as the application interface. [default: auto] --env-file PATH Environment configuration file. --log-config PATH Logging configuration file. --log-level [critical|error|warning|info|debug|trace] Log level. [default: info] --access-log / --no-access-log Enable/Disable access log. --use-colors / --no-use-colors Enable/Disable colorized logging. --proxy-headers / --no-proxy-headers Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info. --forwarded-allow-ips TEXT Comma separated list of IPs to trust with proxy headers. Defaults to the $FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. --root-path TEXT Set the ASGI 'root_path' for applications submounted below a given URL path. --limit-concurrency INTEGER Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses. --backlog INTEGER Maximum number of connections to hold in backlog --limit-max-requests INTEGER Maximum number of requests to service before terminating the process. --timeout-keep-alive INTEGER Close Keep-Alive connections if no new data is received within this timeout. [default: 5] --ssl-keyfile TEXT SSL key file --ssl-certfile TEXT SSL certificate file --ssl-version INTEGER SSL version to use (see stdlib ssl module's) [default: 2] --ssl-cert-reqs INTEGER Whether client certificate is required (see stdlib ssl module's) [default: 0] --ssl-ca-certs TEXT CA certificates file --ssl-ciphers TEXT Ciphers to use (see stdlib ssl module's) [default: TLSv1] --header TEXT Specify custom default HTTP response headers as a Name:Value pair --help Show this message and exit. ``` For more information, see the [settings documentation](settings.md). ### Running programmatically To run uvicorn directly from your application... **example.py**: ```python import uvicorn async def app(scope, receive, send): ... if __name__ == "__main__": uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info") ``` ### Running with Gunicorn [Gunicorn][gunicorn] is a mature, fully featured server and process manager. Uvicorn includes a Gunicorn worker class allowing you to run ASGI applications, with all of Uvicorn's performance benefits, while also giving you Gunicorn's fully-featured process management. This allows you to increase or decrease the number of worker processes on the fly, restart worker processes gracefully, or perform server upgrades without downtime. For production deployments we recommend using gunicorn with the uvicorn worker class. ``` gunicorn example:app -w 4 -k uvicorn.workers.UvicornWorker ``` For a [PyPy][pypy] compatible configuration use `uvicorn.workers.UvicornH11Worker`. For more information, see the [deployment documentation](deployment.md). ## The ASGI interface Uvicorn uses the [ASGI specification][asgi] for interacting with an application. The application should expose an async callable which takes three arguments: * `scope` - A dictionary containing information about the incoming connection. * `receive` - A channel on which to receive incoming messages from the server. * `send` - A channel on which to send outgoing messages to the server. Two common patterns you might use are either function-based applications: ```python async def app(scope, receive, send): assert scope['type'] == 'http' ... ``` Or instance-based applications: ```python class App: async def __call__(self, scope, receive, send): assert scope['type'] == 'http' ... app = App() ``` It's good practice for applications to raise an exception on scope types that they do not handle. The content of the `scope` argument, and the messages expected by `receive` and `send` depend on the protocol being used. The format for HTTP messages is described in the [ASGI HTTP Message format][asgi-http]. ### HTTP Scope An incoming HTTP request might have a connection `scope` like this: ```python { 'type': 'http.request', 'scheme': 'http', 'root_path': '', 'server': ('127.0.0.1', 8000), 'http_version': '1.1', 'method': 'GET', 'path': '/', 'headers': [ [b'host', b'127.0.0.1:8000'], [b'user-agent', b'curl/7.51.0'], [b'accept', b'*/*'] ] } ``` ### HTTP Messages The instance coroutine communicates back to the server by sending messages to the `send` coroutine. ```python await send({ 'type': 'http.request.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] }) await send({ 'type': 'http.request.body', 'body': b'Hello, world!', }) ``` ### Requests & responses Here's an example that displays the method and path used in the incoming request: ```python async def app(scope, receive, send): """ Echo the method and path back in an HTTP response. """ assert scope['type'] == 'http' body = f'Received {scope["method"]} request to {scope["path"]}' await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] }) await send({ 'type': 'http.response.body', 'body': body.encode('utf-8'), }) ``` ### Reading the request body You can stream the request body without blocking the asyncio task pool, by fetching messages from the `receive` coroutine. ```python async def read_body(receive): """ Read and return the entire body from an incoming ASGI message. """ body = b'' more_body = True while more_body: message = await receive() body += message.get('body', b'') more_body = message.get('more_body', False) return body async def app(scope, receive, send): """ Echo the request body back in an HTTP response. """ body = await read_body(receive) await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] }) await send({ 'type': 'http.response.body', 'body': body, }) ``` ### Streaming responses You can stream responses by sending multiple `http.response.body` messages to the `send` coroutine. ```python import asyncio async def app(scope, receive, send): """ Send a slowly streaming HTTP response back to the client. """ await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] }) for chunk in [b'Hello', b', ', b'world!']: await send({ 'type': 'http.response.body', 'body': chunk, 'more_body': True }) await asyncio.sleep(1) await send({ 'type': 'http.response.body', 'body': b'', }) ``` --- ## Alternative ASGI servers ### Daphne The first ASGI server implementation, originally developed to power Django Channels, is [the Daphne webserver][daphne]. It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets. Any of the example applications given here can equally well be run using `daphne` instead. ``` $ pip install daphne $ daphne app:App ``` ### Hypercorn [Hypercorn][hypercorn] was initially part of the Quart web framework, before being separated out into a standalone ASGI server. Hypercorn supports HTTP/1.1, HTTP/2, and WebSockets. ``` $ pip install hypercorn $ hypercorn app:App ``` --- ## ASGI frameworks You can use Uvicorn, Daphne, or Hypercorn to run any ASGI framework. For small services you can also write ASGI applications directly. ### Starlette [Starlette](https://github.com/encode/starlette) is a lightweight ASGI framework/toolkit. It is ideal for building high performance asyncio services, and supports both HTTP and WebSockets. ### Django Channels The ASGI specification was originally designed for use with [Django Channels](https://channels.readthedocs.io/en/latest/). Channels is a little different to other ASGI frameworks in that it provides an asynchronous frontend onto a threaded-framework backend. It allows Django to support WebSockets, background tasks, and long-running connections, with application code still running in a standard threaded context. ### Quart [Quart](https://pgjones.gitlab.io/quart/) is a Flask-like ASGI web framework. ### FastAPI [**FastAPI**](https://github.com/tiangolo/fastapi) is an API framework based on **Starlette** and **Pydantic**, heavily inspired by previous server versions of **APIStar**. You write your API function parameters with Python 3.6+ type declarations and get automatic data conversion, data validation, OpenAPI schemas (with JSON Schemas) and interactive API documentation UIs. [uvloop]: https://github.com/MagicStack/uvloop [httptools]: https://github.com/MagicStack/httptools [gunicorn]: http://gunicorn.org/ [pypy]: https://pypy.org/ [asgi]: https://asgi.readthedocs.io/en/latest/ [asgi-http]: https://asgi.readthedocs.io/en/latest/specs/www.html [daphne]: https://github.com/django/daphne [hypercorn]: https://gitlab.com/pgjones/hypercorn uvicorn-0.11.3/docs/server-behavior.md000066400000000000000000000144021362253677400176560ustar00rootroot00000000000000# Server Behavior Uvicorn is designed with particular attention to connection and resource management, in order to provide a robust server implementation. It aims to ensure graceful behavior to either server or client errors, and resilience to poor client behavior or denial of service attacks. ## HTTP Headers The `Server` and `Date` headers are added to all outgoing requests. If a `Connection: Close` header is included then Uvicorn will close the connection after the response. Otherwise connections will stay open, pending the keep-alive timeout. If a `Content-Length` header is included then Uvicorn will ensure that the content length of the response body matches the value in the header, and raise an error otherwise. If no `Content-Length` header is included then Uvicorn will use chunked encoding for the response body, and will set a `Transfer-Encoding` header if required. If a `Transfer-Encoding` header is included then any `Content-Length` header will be ignored. HTTP headers are mandated to be case-insensitive. Uvicorn will always send response headers strictly in lowercase. --- ## Flow Control Proper flow control ensures that large amounts of data does not become buffered on the transport when either side of a connection is sending data faster than its counterpart is able to handle. ### Write flow control If the write buffer passes a high water mark, then Uvicorn ensures the ASGI `send` messages will only return once the write buffer has been drained below the low water mark. ### Read flow control Uvicorn will pause reading from a transport once the buffered request body hits a high water mark, and will only resume once `receive` has been called, or once the response has been sent. --- ## Request and Response bodies ### Response completion Once a response has been sent, Uvicorn will no longer buffer any remaining request body. Any later calls to `receive` will return an `http.disconnect` message. Together with the read flow control, this behavior ensures that responses that return without reading the request body will not stream any substantial amounts of data into memory. ### Expect: 100-Continue The `Expect: 100-Continue` header may be sent by clients to require a confirmation from the server before uploading the request body. This can be used to ensure that large request bodies are only sent once the client has confirmation that the server is willing to accept the request. Uvicorn ensures that any required `100 Continue` confirmations are only sent if the ASGI application calls `receive` to read the request body. Note that that proxy configurations may not necessarily forward on `Expect: 100-Continue` headers. In particular Nginx defaults to buffering request bodies, and automatically sends `100 Continues` rather than passing the header on to the upstream server. ### HEAD requests Uvicorn will strip any response body from HTTP requests with the `HEAD` method. Applications should generally treat `HEAD` requests in the same manner as `GET` requests, in order to ensure that identical headers are sent in both cases, and that any ASGI middleware that modifies the headers will operate identically in either case. One exception to this might be if your application serves large file downloads, in which case you might wish to only generate the response headers. --- ## Timeouts Uvicorn provides the following timeouts: * Keep-Alive. Defaults to 5 seconds. Between requests, connections must receive new data within this period or be disconnected. --- ## Resource Limits Uvicorn provides the following resource limiting: * Concurrency. Defaults to `None`. If set, this provides a maximum number of concurrent tasks *or* open connections that should be allowed. Any new requests or connections that occur once this limit has been reached will result in a "503 Service Unavailable" response. Setting this value to a limit that you know your servers are able to support will help ensure reliable resource usage, even against significantly over-resourced servers. * Max requests. Defaults to `None`. If set, this provides a maximum number of HTTP requests that will be serviced before terminating a process. Together with a process manager this can be used to prevent memory leaks from impacting long running processes. --- ## Server Errors Server errors will be logged at the `error` log level. All logging defaults to being written to `stdout`. ### Exceptions If an exception is raised by an ASGI application, and a response has not yet been sent on the connection, then a `500 Server Error` HTTP response will be sent. ### Invalid responses Uvicorn will ensure that ASGI applications send the correct sequence of messages, and will raise errors otherwise. This includes checking for no response sent, partial response sent, or invalid message sequences being sent. --- ## Graceful Process Shutdown Graceful process shutdowns are particularly important during a restart period. During this period you want to: * Start a number of new server processes to handle incoming requests, listening on the existing socket. * Stop the previous server processes from listening on the existing socket. * Close any connections that are not currently waiting on an HTTP response, and wait for any other connections to finalize their HTTP responses. * Wait for any background tasks to run to completion, such as occurs when the ASGI application has sent the HTTP response, but the asyncio task has not yet run to completion. Uvicorn handles process shutdown gracefully, ensuring that connections are properly finalized, and all tasks have run to completion. During a shutdown period Uvicorn will ensure that responses and tasks must still complete within the configured timeout periods. --- ## HTTP Pipelining HTTP/1.1 provides support for sending multiple requests on a single connection, before having received each corresponding response. Servers are required to support HTTP pipelining, but it is now generally accepted to lead to implementation issues. It is not enabled on browsers, and may not necessarily be enabled on any proxies that the HTTP request passes through. Uvicorn supports pipelining pragmatically. It will queue up any pipelined HTTP requests, and pause reading from the underlying transport. It will not start processing pipelined requests until each response has been dealt with in turn. uvicorn-0.11.3/docs/settings.md000066400000000000000000000106061362253677400164150ustar00rootroot00000000000000# Settings Use the following options to configure Uvicorn, when running from the command line. If you're running using programmatically, using `uvicorn.run(...)`, then use equivalent keyword arguments, eg. `uvicorn.run("example:app", port=5000, reload=True, access_log=False)`. ## Application * `APP` - The ASGI application to run, in the format `":"`. ## Socket Binding * `--host ` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. **Default:** *'127.0.0.1'*. * `--port ` - Bind to a socket with this port. **Default:** *8000*. * `--uds ` - Bind to a UNIX domain socket. Useful if you want to run Uvicorn behind a reverse proxy. * `--fd ` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager. ## Development * `--reload` - Enable auto-reload. * `--reload-dir ` - Specify which directories to watch for python file changes. May be used multiple times. If unused, then by default all directories in `sys.path` will be watched. ## Production * `--workers ` - Use multiple worker processes. Defaults to the value of the `$WEB_CONCURRENCY` environment variable. ## Logging * `--log-config ` - Logging configuration file. * `--log-level ` - Set the log level. **Options:** *'critical', 'error', 'warning', 'info', 'debug', 'trace'.* **Default:** *'info'*. * `--no-access-log` - Disable access log only, without changing log level. * `--use-colors / --no-use-colors` - Enable / disable colorized formatting of the log records, in case this is not set it will be auto-detected. ## Implementation * `--loop ` - Set the event loop implementation. The uvloop implementation provides greater performance, but is not compatible with Windows or PyPy. But you can use IOCP in windows. **Options:** *'auto', 'asyncio', 'uvloop', 'iocp'.* **Default:** *'auto'*. * `--http ` - Set the HTTP protocol implementation. The httptools implementation provides greater performance, but it not compatible with PyPy, and requires compilation on Windows. **Options:** *'auto', 'h11', 'httptools'.* **Default:** *'auto'*. * `--ws ` - Set the WebSockets protocol implementation. Either of the `websockets` and `wsproto` packages are supported. Use `'none'` to deny all websocket requests. **Options:** *'auto', 'none', 'websockets', 'wsproto'.* **Default:** *'auto'*. * `--lifespan ` - Set the Lifespan protocol implementation. **Options:** *'auto', 'on', 'off'.* **Default:** *'auto'*. ## Application Interface * `--interface` - Select ASGI3, ASGI2, or WSGI as the application interface. Note that WSGI mode always disables WebSocket support, as it is not supported by the WSGI interface. **Options:** *'auto', 'asgi3', 'asgi2', 'wsgi'.* **Default:** *'auto'*. ## HTTP * `--root-path ` - Set the ASGI `root_path` for applications submounted below a given URL path. * `--proxy-headers` / `--no-proxy-headers` - Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info. Defaults to enabled, but is restricted to only trusting connecting IPs in the `forwarded-allow-ips` configuration. * `--forwarded-allow-ips` Comma seperated list of IPs to trust with proxy headers. Defaults to the ``$FORWARDED_ALLOW_IPS` environment variable if available, or '127.0.0.1'. ## HTTPS * `--ssl-keyfile ` - SSL key file * `--ssl-certfile ` - SSL certificate file * `--ssl-version ` - SSL version to use (see stdlib ssl module's) * `--ssl-cert-reqs ` - Whether client certificate is required (see stdlib ssl module's) * `--ssl-ca-certs ` - CA certificates file * `--ssl-ciphers ` - Ciphers to use (see stdlib ssl module's) ## Resource Limits * `--limit-concurrency ` - Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses. Useful for ensuring known memory usage patterns even under over-resourced loads. * `--limit-max-requests ` - Maximum number of requests to service before terminating the process. Useful when running together with a process manager, for preventing memory leaks from impacting long-running processes. * `--backlog ` - Maximum number of connections to hold in backlog. Relevant for heavy incoming traffic. ## Timeouts * `--timeout-keep-alive ` - Close Keep-Alive connections if no new data is received within this timeout. **Default:** *5*. uvicorn-0.11.3/docs/uvicorn.png000066400000000000000000015272721362253677400164430ustar00rootroot00000000000000PNG  IHDRWGsRGBYiTXtXML:com.adobe.xmp 1 L'Y@IDATxdW}};AQD3,K^㈍爽ax͂ Fm0IidP$Q9V|~1TիTkΜJ& ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (ptGiEDQ@DQ@X! +d2LQ@DQ@DQH86U-TMDQ@DQ@~HEY_x{~‹nxKYDQ@DQ@)`}֗:}c_?{~\1FI^( (  h/﮹Ϛs.yy[7zI yDQ@DQ@XW^O2+/~OkRq*F( 3+ gY+ Xs4쨺%v|t[73q MD#P@8xr( 7ejÙZ-$xՖrڸ#7m?DJVdGݲ( Jh!DH%e㶛o/wa.LA:S=;n4F( ϨNQ@X p1\ks2Ύ *<1>9Bgm!rF.H>$ӽk6l6/C,s'DQ@Nnމ RKy;\^~P:/@6s&]}Aw)\WyaBG3׽ӟ~c}B( Lc&4, AHFci,ֽ3o:-xv.M[S~#߁?ceѕ%YDQ@DQ`(`4\7jܯ&YLM)V*$EQ@x ?>WxQ@@/o7N~S.\Z8}2ӳUaO~g|,wޥo ]>!=Cejlޡ( 1Q@*(l ؒt^Kyg~zkoM_t@Rc~2$Q@8`ܢERЍlW6Q@D#Q@#QODQX+[G?lr3]:ݍtt!L͵4+'qx{}泻oP/zx}^DQ~f]d( '\[5oqCj)Ǜ=dAtg=sݡʯ) @ dpCbUCyii;r[p@I%KWDQR;*]DQ`e*P١QNSm 8IU:֜J'_mtutKT5J-5Od^N:.vywGZ 2dԢ(p H#PZDQ*U+>wm[SEQ@XR@zI yDQV`i"/"N ^Ң(pR* }R~-)Q@h7l5/\x\ڦWfkRcq*' T8lԙP5qoN ]nD&zdǗ"rT@z9~2&Q@X ,A4'`]: ISs~hK:Ti>pDGc܏v uwW*Grŕ(V@V"Q@tn7o.9R PչIh*7vJ^\:9~xS:rtoO}zAx;^7sq:urL" ( ( Xn42o3n33s1>2g߿`>dra>2a@@\QU@@8rJc( \ tWUOgFuU|Vʥ*k j_*I|'{r9xkw~\yyYԌ6+1B++Tw QD+E( ϢLyq#Q@Cj(|@1r{ѺJ 3֓cS[8C t.E/Ҟg\h9n4:F\~s-<4jujt.s?6߻c˘^cQU@"W99ODSP.8WɄM#?VPLͽKqQ4# GIeOmGI iFDis Ϋ)3<6m]Tg j pvNjM`<3;ۜ:zs_ZLwkZ؋''c5/i_?o}m*KF2q<驪E 1b518ʄ%$ H( &1>:},\^\pc':KOj۞:pOo=kHtJImDjWK?@wX?ewS5gSupCUftUN-7;=m۲4yߧ' m'AD'\w3r46ӳ6Ssf_.w+7Z^5>@y uۏ~ NCv#3w8]jSXzNϾ139qtvTJQ/}c#˛d+PːEQ@ ]5Y|G[ӵM]Z}nmBMdG=YM&:y3\`-Scqou%wG\9&c3xHYɭ[Q@XF $eePDQ@8B !uK1g- tz.yFS߼rN3tI^'xVgS_҉tgr:FA]7;n?.N.l篏J ޹9z\o6~R##.sʇs`ї(7ښ_@wUKh UdLz|WU # ,/5d( )DR1-4ͦ\Z|Nlt.ꎒJMh#Sq<`RՍwkuBٳ^][?[?~g]3VQiv]ul,A nUu}obnM*fRfJaR4*/:Mv{RR54v碀(pr+ HDQ@8 ,9{Yjs<~U-96I#5Qu+\k&j-.4$ºqILkQ楓U=kOWڢݑN9zr@Lio5>6%1wD\thm23S(DpS4gೊpȸTdav;aqq-lDQ+ X )R_LeStO|]Xi"W{V\+S]@P"mh+<3Uzbb箹vX+z6Fyx)78.6Ј(]7˕mgӳ9чބ(59϶Ze:E0](g|?0Aa9{>0XW&tZX! Ba(^[}фb7t BWGB3֗33 U磌-ze_%Kb !֊dpﯪ9OzibM~&YqDYPo櫏Se-{/Ssu}f:sr3D7:όnp v1uqu#('a^myk ox9fq);n;ّмĞ>:QsVDQ@'<Q@VF.73nVPa=|]I|:-8ϸ]0m ?G"zʂox,ra~0eYk,7ukjT`bà\[ÚqQsl3^ԌO8F7ۄ 47kuƙJ_sf X]A.%G[Yh+*(!iEQpI(nߌb_ynT1QЯꪠiʹN:LxEoxoݘi7!ox}B4$zPi,9p#XBjś/}eC|+ϭ3" 2Q 'ۗ>Z6:`k׍.RCfK7|{rMҊN<$Rgހf di3?~[&StM=-h::7sAH`)}n`& G3ېEQ@V mLt _PzfS*roSPCzWlM"=#.̿K&ԯ?JYV~B(kG[؈ÂhN3iR(u̫ [j92fdw.f+}Θ{F[ p]Իv eWjߋUoTeesEZub)WBEt"KHiLT0 2'`Q(gP:*"O7_*)mP],"L/A ɔOY ( (p `M50^M$QN]3 Lj34c *ժuzzQw+3Ùz|_ld~{Gϋf'O _GA͂/vpS%IKySC/T;9 qL L8q=X{oW* ?ZirfԡZHU UmFC!{}>FOFt#Lynj1b;-.ZP+ԍ2ipWY7AN;fi8R^oIyGQLiJD!=omV\Z:n9d鹦Q ⻞ `㦀D,=rjl+9HwӞ6;׭8!ikA"8C𛋨┓0I΁վ~ٿ[KAw >{f(]LJ!D<C1$1ma3gkxc\pQF;Np덄DљNQ+;AM85#'pq60GVk\hLTҡoLˉ}b@;.@wj 7M!&SIo"SfXn[XwKj\=Hc*wU Юk3xXmD/U1بsp z>ШZ)@,;mƨZGp6H` múX8agT6@͇N;WMըŸh/4!c<g swTii~7 lGK0Pepb#$ԇ9Jݢ EXQ'=G{DQ+I( YFL@W;/˺2_&b ա =yXsíEv: CsĤɶO==;SַU*+u }`T!LeTȢƥN↉k#d~"zɃYo>P8AWC3 WhMƨ l5bpz`ܽ&t`=zn3VXw鉍-Uߍl֠^("*$c&_kI0r܋yfB)mh&rP1Zf3d@& <%t9SDQ@ Zglۮ+k T5V8.k݅p?qϾgo]gՐ E=᠓ߎ,֦`0nJxOf[B0_ e؏ rmwf]ߎ<;jL[hCG0ĸNMFp,I)h ˴c >*z2 vHrQX.9pm,x#etZf 3]keTJ{V U=(몳o FSD础y'( ?,/|?ܹ.K*0j.B6m .Po/3TZxr0q֋庱 $F<3^{-'N4Wd%p ɌuA6lb\n3iӲ|1{7:D.N1ANV$%F'P:<⃯Y(ma~tׄA `&<<*~^mH: *.Ť41A1VE't=S9\g\ϡt`a h~ Й9۷U1v;)s{gQ@ DQ@D6sWQe7@n>|ң| ,ZjLaoy X֮br@d DA'R UN)f9f0Ql@h6vA v"ۊh+ǘG7n5-8(k'}&y}ni71ɉ~@9]moU V[ cCJ& ҥoyGjZsXN';pwf&CB!n0]_GW^eּ 8^:sXBf;f;V+L{ef 购3 x6Y;|EPU!45n4 Y5# !z*)(݉dQ8Gv_z4ʳl( 7DQ@DT\v%3_}(gNybD1 bpd 2^5^سHpv?dD68<lkA@ kl' 5y}F3yd ohZx G莘 9yPk{!DQ@@ Ѥa KA_O4&9 }L3'M%^)@l:hY l@-5e,{.OGrrtq茽&yi~Jhq{r"3hmmssAyfٿo9.G݂.@'8 0OG܎4h<*8EWpovNwd"ƁyFzD9Mk8'Hp^G'b"I~CD@ urD&ڟMal( W0hԎJ es&TV(׀ }tTAӕM0-+l8:xp"'1]elA쁟1?Ĺ> |)mFmDAABgN&K `χ&eF4mIHV`$QX,%AD2hQ ,"kl#)øl}`pPy-Ƶ7vdhd3g0cEB >*wM)?BɅH<<|< h Mt*nMrj8 X'K\~5Ђ.v6`a)m@%  8t@Ɇ5sIL1KA1{cgagHj= h-Q{S .3%O\7E]cI6vB% SsnvhB֝&k&ʇvԋxxa0j׏T@S1dL`Gm#5c/h.Oރ )`{Rbf&Ҋ===nꥮ'm8dV+;(p8_ypc^+j! YLi &ZEeҥ;TS ,>uef]%&8¨5O6'O l3͆Z5 2Qoy@y ׈;l6ÀlgiHOsрA\r>b#oz`b 3.f5L(s$x,50 1# U\^ihd܄ϻ㎡ uCL2sZtqCN/܀$7t+^~?Z@֍vu*ij"P& ( ?AMoGLoZB:sbЌtohv-LHȄ\ @mְ@1as3a7x>< u%&a 4O00ay-y8x .p#cFCj5LgRe/ ̇ߪcs󮻑^[Ȝ _L.7q"^ ~e삙d0g 3a w%\ 5#LeY|ikbF;8 I.*"6b'ƵSڹPN&Nl`R^ 06B<@:xK@O_"V37@NbtY7 Hi3kD䰰 p^[X!̆ 1vgs"kP?)ymB8jZaep,Np9.2 a_]>@>TkXxnތ ki~6<MDQ@8~ Є/?۪uq}@0lm߶̚`8TjU@eŬƀ YuXp 1ҠT y0;Mٌap 8oU0a,6, u.m0?q6<1Lc,t706cԭl~..ˇP"ߝa88Xi0M<s;"E@ *m|@3~CgYI${VMp.L֧k͸ o0$\뒇ݗeQc=O{kmW+0/s駪|זa3hrg, (DQ@D֖37?|,ZFdvC%=zm(mo:hJAtL 1xȉr!9Jh,fk=XYu%18ف&pۊt |p3p0 '&)9PBv^XDmiKaҝ *p]T]}fVK$\gG6@U 7ED[p֦L(U 'yAy c"QmFCF<Š/:&D,B)P]IٞZ6Hb_Z@cSIUspKz x܆n >45`Z()8ЧW&DQU`d+n[(f+EvK;1jn1>2 @δkG;:€<^cE=nâ#p[9IPf )f:*h,ĆX/D;\я.v+8 #+pr#%H + XFGpK1׌V̭>nhc 7<c18$Lt_\tCPɣٴq<.ƍ 2ܾ*ƅᮤ,cy=7ox f"1|5yRCe[WMH A˗ux2X?-z|xѽWⶵ۪Jf-~ ) } }YUQ@NevUtp;b s3( y <@IDATc آdX ϰya8 9f NQ͊|QVN~< XĎ͇>.*ltc! p.'jA;\~8C ָ1amzh:xivs&&8mzBSKB4?c[>PD;Q1P#ganA͎c, E7&8Q7zاݝkrr}ylIweDQ@#F7t6T7q.}i_s 1XY"2R'JҪת_%2u 8ԘgNkqk>\&`n  PYUO7.+]f&c%X8Kbb!nv!RqXփf;`qD$JcNH!pquQ7ppAh&ƍA̚@F&Ջ^Nu!Rxe0Ks$3DoYV8w7&Dx,}ɕfRC+=¤K_환7+y*L\,f,g3\X~&eU FT۩Îhzd;yɲ( DFf[g!p_uF-bdmD9\&=qBq$ ` #ֺX7)d.yrht[1'9f:s"fq^ n+a$HL`FLg1(M M$"сPfif:6ۍ1e|lj Wg6^5J-yTX}5_cC'i04@BO4ɉtٷ'HmWZzҸZǴRv1. ;s{ֽNU]Ȯ)Sjt.9Mw7) Pу3gn%ܬ,ܝf7Re+wXdo T@(\ZDHG-#K\>F9x@:@3d|In7rjN?08 yvinzpMY-Ƭ?Z"t򵵌/0!NUC26EcFX:f}ɨch}k(lc,M2D?varg0M#:-.HcV0F6̩JG")%`=PUz!~TQ6Q( }bt(0(KFx6@JcJ SF=H4@e^ Qu3^; KXpktY]/Nv3t5YШd1{.X4IZ =En cd'б X%&(qeQ~x #$gmihzh6~AU# J' }a D@쳎3 2wTݞ8I.>C^z̹7pp&F5ʉy\]mq(y ͛rN"VqWƄ\@:V4kA-/5SHU`W4od_s( Zdev0q~N̳'@3z-4@g.jڮH:Tg t@"P,g[#tC'a8Wf |uto$'=YMÕ%X^ Z9A~:ͥ~aә"@3Į@3osl?DŽ('.w])We$P%43⒠`6-JMdɣAGޔpn6H3MP<mmMfǹB~B{96F'𖋠0A7BlEe|@|f#@c<8uB[t:I`lqS@I-DQ`+`g/9?%j *!_l]KE =*`2Kt,K %9&0C.,8y PNWe6c\m\±H fI:iE%LQ)@\]юqlvo<'H^/`x` h{77ƁkpB"#_Vq.w x BPan/sئxTq* n;#bA9!%>p#/k59~C0nC pCVOifn:q3cr.ڵ~42U{-pX64|tyexPk~ \781̍QxFۦvp6&c%DZY%y7|L.ƪ%E()!^\q5}fGє]2hodl>8M~1Z,got | Sx~v@.}̻ S|7A ÷MyH q$N4d; @{ ( <}{lP>1: SN$8 1 |1X~@aѺ1`9bTi'(3lobᘓpb PfI^E":θNF|,~JnG3$34]_4,.gl/\څ_袳=Νɒ|Ow> vހp?9["\ۍo0V;p|_m "^bWA\W;d}lGL tvLz!v|g<|Q߉zt3MZ`;Byg9jpY%g3EsDco1Oaf‚-hpu sx8F۾4h P;h|mf$qy{k(4 `񺽸 bl-h+'/{p] ͋sX8QZ 07o*CkVH͋!B:l y2GC:ǔp7N-[:}SGU߻:oEiDc1WDQ`%(pJP懓YR. P1!(ei7BvșCN .)" ҝpT%YYf& blD I#24i Xf3 /K|ow݇<cKx^n$qڞgpg9vv@JA{~8`VZ وRxt>W̼3:<pc8a,C%Dz78A+\?ŤKvfXCŪ"YI\hLp^wq 07~Pʩ0L6&d@ DZuB0~;< D,5/\}]wMN_v8"d; @SyqQ@Ky5 Ϥ#FAiP 0J'Ԓ3'9,*xKMI(ێ4HseA;QF BG)V b˂LMnR:OQ  *DhUmp ]; m&xTpQ|7B)71Q3wx#gR;|c_{)fOD_b?{4qoCk~w$M^/v̵YMښ861W4ELf@/^r-!Џc.-!+\R%F=ڲvllK~F?IsslMɸ67>ѾCm{vRc[&7DF~dmVۀo'%2ΉtP w sW%R_/3?GE߻zZW|Ў0 NL7iI9} & l( U"ig]n;^]9F\WQW"UCUjЖ'=:sHjxdF*B]#e{ySz;75O9'@W^D}0.|?/eB)!\ X@*s e1aRa *tڽhlp+ހWI 9>{DЖIB]ILx:|L"Qi˰͔()57NA&&~`biM faRd+Ɯ3Gje7y'9qQ?:\߇;? ^3Oc@VL7h>ͮW}z;w-/6#Y Y Y Y YZxqՅ?7?`s׎G;W ()<`H,E iyjng> 4@tRW95<;Ha.TDqm 69&lՍHs¹w(hgDAkB*\-:>* ,'GJ: n[Pu?pl'qJ OU}e{E8L~vEĂrd;2/E$\4՝|ǭu D28e=o oL>wqfyk˙o{=CW~©[o)a//,#34̅&ۓF{ӏdTHHHHHx@m?x?-'>^OKN"<x<Jl]6UEg,_%ٌPin-d/R`88˹uzs-ug>p&,lUٙ.`M}yrY icb1(n ˉuC:w}eal-je6mwPMr#_q#f)[7d)Yí_u~6) ,dFH\iKzprt6knKŻ=xөxG,i.cٯ2ٗ%qz,2k8:wocUTH}'nv_owt/yg<:߹m_z5X:^ +dddddgF^2k޳-O=6{(x^x -B0 `qrbLcBْ@lTl?P5l u%9|w.Gח|f8Ԁj\l5d%hP W߁ 7xK4+ >UQIhycIRvaõY2HǨzcΙsѠ6[}ظ?мVUb] oG$N'*_k#ñ][8Jඏfo'|:S}t O9t.W1NDymoc;Noşl<[/S^y}{7iӦ#YX ^Rddd/@ (:ߟ_~|o+t6nːόPT |x+/yTE?`A]xXf5P@.3@4ׂ =жN\w:I_0M)Wv[ߣ㰹]U7)[ l NbV71y=|zE`h)+dж- לmi!vH*AkHf=ݖo\FXceّI.0.\^|2@Tz{ʵo#3h{p.'t]`| bN^|}J#Lx~H [ʕ,,,eglTKRKO=C)蠿ѡ6l'vx*5O,XT9CI]V)I<4D yNJ׶jS:gzK,]:Y Y Y Y πƒ??pp|-:V(}eg 9(#a墇/tw40PH &{&YřBU3@q]p&\`HbY\Kl¹]7o\"s%;,sMnho0ʮHYJ7ql/s2mO8vamZ, T&N{}G5ME7Fm:6oG|ɱ UآG8]ZRzFX%>h{O<XvB$׀iʲp\w&Zjbޤ06I2Ybbo>GܿrDg\2IlN0 x#Y Y Y Yx#_q-??v蓏e.#HV}.yf͊=2\H8ĭ@i[Jc5m\0'3[|((^ς!ઌ*HJ\Yl>Wy.@DH7L /mR 'fO6Վƭ&'/DGGCѯ蟕)ߵϐߌ P[Z5= \GZ8MYqzCu =QG*nCk w=ě sc9'-,}Wq>T̤/ɳLH3KG@@@!P8oe^sR3('PJl D9+w\&.7f$,l`KXg l (*i+KB҅"m2'sAf2 G )y]W@3D%&B^U*|ي8nR3QPkt– ]UGܭPO݉kQm5*6mZ?9]xgd,Ю}jNZvo7>l~,mD86>7N/[' $ $ $ QQ~=O׭([N5^fx q;@U36q|=? ą` upj܆b  `XҖYLJmr $PI^_AY&咉@+ƊS|hSPq%*x(*An(v#@=OP}탐Q>A'iig2r `lO[/V '/=6OrpGwͩ"_`h'#r*|S9&U ʖOݾDSLu_m^b}k"'LQ7H ƮdddZ.>|wl)ꖛp8`<}xd(FQ|?"p9\P5]' cP#LRi׬ 9^GJgd)bh(ٸW[ΎwZJqmMd/eݺf|>E1upk S<ݥ+\j+]6.Ci6_sFt0MwU}W#: P ׍Mڌb:ºLS &"L++FWxT5ʱimcgϯѶ_PWހ:p4%NoxzMo+Ňf}zL:^< $~lJJHHxZ>???z<¯xjw_mυ7]%/Z{D{񓶏1b^ӅJa@5q⡫L4~ZL)36-}v|`'va%7lnɧ I MmŸb;>]iY4wDӥhӺ4itBMUw;.ʃWn{?sR7_ĵx>}Q:((/@(fL$ $ $ t-#m`Bl߿0_̧z} Ye'sSEsA`D@5w. JBCDTt [& <ոM`ۀtT̳g.0.lZ_7,(d5<&ԓ9.5JiAE˖gT@eO&ܧv]l>>]yznF¢~iB7 YY.h<0mdN tquo7e` 5p۱Eݺ"o7 *PN"J gΤ 0mh|[_*>]L$TN.|[}lTzL(3\`Vyuz^9:z'Ì留AmHx^H̖2% $ $ Ѱ÷'7}˅sVY'D8Dz.UK*"n\ $' P $~L^Z=_[M_@Ws$1U.*zf>aQiz#<`*̔r ]p&t@#L$AeTRPx]?KWWTdeIcY2ka b@V, ۾vhcnb}*As$ :`)مoh6j b8F;Jc +pmJ>5OiO>9X IَuDWEzZϙ=r#I#-Ff[4W'@{NhM_ąm7&Uç6f-6=F}opyqIz4߼3W/SmΤ#Y[#Y Y Y Y Ν۸lO_,wWy9^d ^u J3lE =&R@V@* j#7-#ૂ D/k9/e$)+`/%ŷ8֧w'"#j_tPo jư~7 mloEj QUָB5oYV|)D,=n)*ף\Wmn!@:kǞrT2d}WLD d3f+&mKf[7&)>/@VLe$ $ $ D,G>ȩۯ] VA\#H1}dr,huЯ>F: +,J8MYTɵ=C@ wʴ ` |m_kR'9p )xDa?o1U\-?7N(l,C?jYVPsB;&a0܂<%z!$F|*T(A抉B,\ {n{J[plF"䇱c 3zAs߼8G%I!-!+o=yT2 {wdݢ,,y-eJHHxiY(te]x*6^J&9r }fp,'H qn|qB;piJ_1}y%,j Lk[پ q">>ZVRWQp1@*h芬Bj)Y AX]n&)sjnV;H&i,Ÿy ~w\lfۄt.h U跅UZWӶF *&%Te\Qq !Dp`<8o"?1e??1*mpӱ2rjjOZlG7sq ,,&ʣTkLb۰xA6&xQ1}3ܟ58YS|L*na}m!6/ _Rddd?| ~;BB  \Sm6[(غ=c>Cy4i0c`rh<AQn! mmQ4o =#;#}x]B-Ks8*0DJQI5î3 wL'Rj eo|q2UgsXn}2Row[/п+M:4`n/xQ}|jhLmt)c@@6m/TϟiVۃGKB/]4V(ɱ0|ڭ jp* X"<8 > PzpksgIfRy7X+ GFQ-@)>|t.fY^j3@Dk/_?E8#w IYZ gi Š0L X ՘SF3l_ dL*x<ofQCKڝ@IDAT- S@a*ytf␎ tsإ5Cl2M6Fn(+}rk@Iԯcn|]lfͺTՈn}AR6]@(cܒNԅH .Pd[5?4h/gF<ZhVѴMW;EQ<ȁ98~oHvG]揾ц` U1q^OOٰ~bItc&e,iuM)5i6n|V?{XNdjG:ڣ; #c;OJlzӈ^aW8c;|r!9 '^>͆s*ɔ(W\g$Z1Fs:9a\|P.v4n rF=ƝMГI=:տnŏ|Cϝrht$ <' [dddd?*}sooq/<} e[ n猲k:ha]ss ਒ $Kw_ BX(K9R,OxP hYYsf^زK%e؜E/y] Ђ,g ASL]WDDay:N07ƝWǜ*(|sUwLS nuマdHxNIG@@@K*dFWCZUW|'u;/ PUʲvwSE}8mRt B y֊8WZ#W~d?%HS2]^&PxBXi8PO(Ǻp=ZLvR m ' '/(jĺeZJoBPM f!i&(=8䘖І0)'VCI`em _raζg:&0B]w;ñv0w*_kpxN!¤kEy;csB W hОZ ŃQ4y?#{;LGjk'3$E}Dx t INj2k/G'ѨώRޜ<3.DZn^ڊjPaas~5h&JS֝/}~)Cctcnʷ-8}|tIuD b/&,Y;>ʖ(ڈZ)Z_a\Xs:4b֜ŋK]HP?,(z p QanxXcS}U Pa.h@19fI0yy97 pv<ӒʚqnˍZ^R1*ZCo kʧ51P'-d-&R6:Р_|:Ք{c5Φ{mqw%h?(/U vvo>xJ!XoT#!h} GP"dp DxYiOm+jM 99$ZBA΅!}G _OЮx5ˆIVr"O%d Q$/ mTLMn@9 `ΰJ,@qI%U@D(ڮ[ZRԦCs\Cw AtBj $oi ц4Lp2ϰ#?b9P1&#S&C`AYb] GH!|Kx'?{Gߦ{*<; N8\K*ei@Q5c*1@1ow7$f8+0 w7;FK «DF΀5wPM. ;4ו[0.lpddH"J ~ғC`ӽƱr 8G=qGQHO.l v(بh>wńSf9r3">v^O{ojpm{{t4n#Y Y Y Y%b SZʯ=x#Qmm 5.@X*TLVkV}Ga'4OG3]ZEƿ *h惉.W0.]H& \3ڼVAUPM%hM<+b)4/I$͢7UQ7ΘF a &9w ߹Vs͆bЮ놾 Дs@FhZh؅"ۆ^Y}'6Jw BrfK;sMX۞#M?Q;C V xTPhi"Q44_(խ ~ i Lj#9ave+TCQg,TLR]jwqTP2ir"RJ}z~y>E6c buŸsPO^LVsh;ݬ:4d.PuD[nݝPn+=_yǻ|t$ <gNg>;xr{??7}^]Y lP`.5zMOԶ6]I8@p{=Tyc~s<Irb@'=uXn!:6q=@ `%cU]H9TI$P&Vi?DXv\XRJ"ir!՝9*2uq#>eUSAHx FRWs9c|X9`Âz;,bcpG/B,,*\|L[R>MVM{(*+ 8}bt {8о)/4Ub(@Pv<*lݠsl }ƽbMxVyޱ!G(-b;<t@1/ ɀ<«7fL AG+C,TNKT]h8B$wIw3w~}_3a?یNZ_7[ywU?:{\=8+HG,mdddd?L pRO¿ys/_a?w0}D%$4u\+Dc hA/pAމJ6'AP) H'L(G2h\oOs(Vq x>˴6>)q!ĂP))sNP Vc ` P4eCQk!RHWK]X)0 G5@^#c fQ]EGX(?)Oej6=A&;°) (Wy?TXf|ǡ MJZ]IڎNQENT5SAT1u`~L~p]ә|Qa1u/  '4`ΘohBAh*kfُ\?&rPEPfXɳBv}TRÍRh#%q9>K1崰 \HOU=KKr|Fi{>5zP Π!ڝl\&W-|WȊ8ԗ 4.郬<9=CrM4FYu!۞ñs`_ٹ@V\W э15 X\~%x{cEtts ɨB5#x11W|7\1vHdFlhchqhmN..5~[4uUc@ .?zii'zE<|]ľ'Nh7ʵ~/A9.&(!4"egpZ-ï/4`hQ\cfl __von/=r`t|J $fI'>ظlc6Sb  A/d LjQ mSSl%s+2HЖO?qq& "G TfQu|1+Jnyª'q~Y.pqXD̠<.r7 2).n!jlg[u иtL'*|i|hq(jg '3H7)| ȥ*.lV9:f֤?]6b}#Od.QEXt=k}QU'10i҅EJ Q쾈rc3&\L4q\eN&M#n#N0-6!f.u3c  W?N.5-,FЈĵ)k6R֤dזxRn;*We &R =!'1&hU|D=mϳWگDUKU_'՟+W]w>tMk_1t3&`keqc[.~߃z$YOG@@@aγ|+/0`rRP`( p y=X*]?>}f4ʶ ņFxWG4%:7{4I(0ޜ7/]-x#=XT'8ϻ(!-W6#C0»T0&Pǀ&9npV.lx_1Q؈^ Oj6tE߬j/CYt_w Iū$C5f#^`vA}'^%.\vtGlѮ!fHntxQ'KpԊ"ۀ/g&W,t]\HpO%u4'/$K6NW }N6m#ȬsWm\TF:f'jX0hF11¶*xgl!y).14Gq&0gh''*S- _H]f>ffnko'\ ut-gD6Ob{%/I*4vNdz-ddddϓα0?Jt῾PȻEyf@0'ĩ$_vG\dOfmf'9,v>K MpRK'!`Ӈ]Vf[RaMH"irjp[hI ؈=jdy'! n5Nt:@*VIV/'E ԃMjڸ%5E,5ސ0y@~Q朐ɔ<.4|qFb"$Њc`){}(u0HbW }U'E]\t0\\ɸ¾%q"ţ!{(*/߆Ll,cicL_Н|7cc)0c NI;ܥ;Gq=چ>̓zw}45D/CαXݻwMl:?^y] nTh-gZ 3>' $ $ |-ĮGNݲz?%B2RыM6S9 }<\Z[np TDk`GBҁr9"DPw#>3gʡK`gNA= l{=E~^V!?~/ 8BSq36J]$.iJ(k3ˆ&bhPg">ݶՍZah v; (P\@Bjw<*(V_ۈ@:Pi;1O 4"L*&G;ReMchs"ؽO'eT-\F(9h]ռV]y;N_UF츇ou6wPt}R1vVLPbKnPjiin$'iʘ8̌vqBS؞szk=ұږcLýGn3x .;ԁ}pq9J; %\_ Ǹ2;wTQ]D9ab*NnN*N|N1V;f,kXBA0!# Ls1[\_wOgozh?[i~Ϸ2~x?u'l[^|^"r`t| HG@@@!eo]|=|D)c=xE|AB rI'F7fwhRwPlUMq?R >_iq^xZL* ³ $* q`>}ouGr3 cG#)<㚢;pkV %`R&Od5/+@оv|ȴ:nV3US!S"2:;A]\ tq@evPQLPSog(b!퇑4.}C1}0ÇM_ql+1@f{`tvu{E>c x+ HMZxҸϺLyb RNy]׽ώ#nl1Q:>ܺme'$ǨwtP)[{1!.%^n);\v@{PXɤo?nǾ -K#ѹ:G[ϟo SΞ]ly'͏^۽ώd_$gyKG',Vӑ,,,,9,8@ɦ^ /_z<㇅J`tmȪ,~2 :[usVB3U`vyX<@!␰EYQAwa.@4fK* qk,{=@eceץ,GQP#4T'GW* gCզ?py`'x4@/ p. Fv~+a$b]8a\U0h,LTN0EE&*!/%uJ (y(β QE0H۽\_L<^n7 !ߺf̎3ݫvlO#7Sa5=}zB1̫Ui+.OǩG~#n,(QϺb{@v`bM02.I[]*X 3K{]gvۿ][,qXhjˆ^ob}}.mMto|G'_}5:K8ɲHL?o~mg61|s"*uґ,v2B@@@kչsO];>LWC@S8΅&tc@A٨%?#j"W}Wz݆ݕ>yew8wYzkѾvAb`q#-\xJBѷC~4M+|q;M]m;w'ۿn~hD{S {YD_'\̲{,B{[#Y ,:/{厼>N~Tnb֔a0DTSUGUX/{0Pq[ 8qR_jv#(k0EAY W. 2 +NxZQmvH\`&i R e{sN[kU$fjxn,o 5,Hu>na=t1L}]HWpU;;b4JV L-v4.DWME(hlȳJ.eIDP7׺`4MuWp6[. nsK2٣ohg j O^26,c# H؎v+vQ<D[/-j^9}rs0p)CQi3{D ](N؄oڌV+16 ׺+.Ȟbwpo?nd| xi9@]\ N4_b[QܝucRU[a4u7,NjhZ/ЭYMY~ҧd ӝ,,,"Y=OW_8`%Ɯ0B, @# )}@7 2/{NPHQ.4|,!/sy,Ze1|aE  SPXP&Hm0t+@K: thX:v+lê)IGd _C xз?± ij!pQ+<.*LMr q( @ kȌɭuX^_9 sͅY"c GwlIHM=;೯=3]Ei_Þ:,,b[ɫ|W@.mGlC%Y H  (p;~u!!ʄDrt,C "6. -[ Nx"Uf koBNTݙnC34i&Mn22' 6+U[lޡ?E,@YShlLz w)H;"`Kkؚѭb9ԋ9J&1n("_h)yw#|a.sk>EeE i71UˊaԌ tJ)L̎%T$u@݈ܧ[˸̭ːMv:p޻oq8Li'j4\Kj>iÑn 07p%OSn9 &BW˲:ahUDثnZW cqo: ŃlT]JAUT靲3e+p 0.א@.!!溂P +I8κ =<9tZ$||P9*ÂnTuUV@1ld`MEt0nTk4' *ͪsU 0 yG ΊU 8AІA/X;J.d:.&psǼ M.>iOjUQcv %&Ul$ dZDju3ސv8j#~ު,_f3>vxϧz.1C;;p>PQv21ZIwӢۃю~ CWfIa Xc{OE78O[SlP/Iq?}t$ E^~FX9ZTBBGRNݪ+s -(Ցs1/JEH8 @*U lDZ#`Ķpq@eoHi TjGq!z1ԧ`N8zȎ%v/PuI1őօ;ˊAv@;ؓKGz3¾t1 SeL=!h4O d۽l~=*v!9cRBHbh2&ńs)<}D1 nX9}FV Y@,2.dWtGiK[볎ȽTUUQomKTq>O#f?юмco6en=1 ] ^M;I/.X-K|/1  .v LJu%0 ,9!3>T-k0&ry:sPyH6?,y oz!ecRkƢwu iD^NryIN@" `G`3Ю.T\׆dp*(]`xalb Ή+ՊF'YjwRq`ؐϚxP"Fo2EYLl iұ V=3w]n(qhG8429BMc@.͊rdIe{/IcݺPslQP~D] DXKArngFޒHxNH̔% $ $ ~ 5DY?=lT~)̌K?$fT  ]YvBa:47Ωwvs 3^fɧoywznQΨ=xRc7%C^WdІ]Rqh \1eIzO"&)7_l11.9wR\`yu[fj| P= JG~ ]M`1utS;;dיԔ{޺I'$#@w iлgTѲcb̸N<@[}祟~Mߧ!)ڔc&}~naӧиgv6e3ɃJF+p\;WN<@v}e*&g{{>JGg@lt5Y Y YY8w׍{/=Ƣ\~.׸<[O%|gbp݁RmC4윐eg]|\ 9j?'Px&yk &R,CtrM>@/At-*[2u]¶L(FL%S[b٥!F U'YPJ׊(KF8ZI7r(P /]<h4:J ֆ𳍭OpD^m@[Ola.F<H:Nv[twB,}q ""0pn--<i; PeRCDaϒscC}z}L:N` v~1[R/6+:vWk@١ҟ.Tܘc'w[78.3|F!{CU\(\:a4f>P%v*A'Js8:1XF&٦ }ʅ囍`B8 2bHQr"n>Q/Cgxf#Y@?@r@@'[=}TJ6nh\{o /c >x0~*ȭA12AQ`_@n72ή _ (8P髊!:> s^´ȚSW@mC>iwCU]@_рC3gttJwQFi%>qk^'\[W ?D(pźTuPd q_XpU7ƂcX <ش*.t/Z%W`^8*-[n0t8d#I6aK>.k3Nī8S= ڪS 6f_`%!Pnëz)ɆXa> xNT@IDATlF)>n&`aTݷF-|lWu>31aj@9ZxoHxawdb!#{ 6_?7Š839qi HN N۸iOeϯ[mӑ,-&]IHHk}GC֭UuP Y6g\<+ЩHSNcj.@3ĶɌ;IR=P$ЎӏklE~ýKLlZ'uHv7jTl4s>uͤ@\;FWvu0bq ܦl7@QQw% |}Tm[/ hYH> :O|rY-ӞIsN~94l!YJqYȧMS]f/߼:amd \n>k9_"wIٙ8 K;|ioc/8\WG#Y3X g0N,,,w7oͿ}q6g\PH , @([(n-*dʄtPN: } djd# PeE 8=-a0~ PH..}c27K,hO@>p 0ɢ D_9<7;RF1V2&wNhlYwQ޽c|}(|6 yB^aP}p.2/-ϧNt&G? mӑ,-&]IHHַtض[{89wzCwA# DA"ym ۤT (0}TqlBp yOCH0j0z|< -$Q:a}帘*B^u6+t" 8 (|ױw(i-4c&`\JsY*mp#y$Sڮ  y/QX 5|ڄ]cڂ̐ڊom*|̀Xmxn{)Dml' 0:?":nH&Ѳ!\J) `63 ͟}]hskdaj@⇅184c]/y/Tq!Vvwwr>$l2JLE9nyr{i*[RZ;\̨ll#0!C%v.BoMs.:f0~dR$uQ} \tOå_7Vwa5+) aHg%[Xبe o6(ecrvǧbTƃ ٓçִG_JyN?s3 cuBb*Qf"t'>(}jVCxVJ،͉vzYOv8jU^s|8 P]bisn{XZELE6c M44ժNTYW8u#bIYGh fhq3 $'NN?|n'-a6׸3@_RNnT󽔜 9+%Ǵǁ?{p~~DtεfĄg.V.f6q0L@ M"E E E#aoոンi8 cd<5/9Byrȁ%SZntRp+7 0PV@UдY1 P4Ka"V3ӄXg, y۟DYU.kI;.rU̸QtT)b S65w sYXkdk}Ub? ' .ӷm`}UiYTq H-1tܦa. Xs.mTS`~9òUaDnP|XgG6Y 0n8n1C*gƎY_3oI'*LYڶ@@w^,U P = ؼI_, A:sA(^d(U9PÚb-mB)PGwX@Ð8mK{F)sBo3@ko><08vyɌ}_8F#K&09[xWkfX8 ;1GoK?5&O119i.N^ C/їW/'.wx; GW+ex{A[.=R? ?.pssn-N"< z(Jgq: Xq UC,\ɣҴiqq]M%Autq.{jfx$T+Kj~j]j<,<8v(tծ}CZZm( 8-*HM[*¢2`NFCdkWA@XĹ5 v}K[ɜA±Fd\_DeD́t6Oh!ϡ%]=yCY) ox2XXH !8'Iv*M^T c{B-s3.NX~:fb`?[\E>1qs-1;K;_T weVGBVI[RYeU4ƻ&\oMt|7-LIgK@!G5悵:dŦ Ś ,lvvHM RʽjWcPGT{"Eix%o~3Hx$~ްSRR^8._?>Mj ^)P 07V>fP`-Ylb q/ְ"p՗;7׳_tog}b>¡h EcZs_|zPc/ ତ1_SYڠ)TV&i eoI $cx8vN6 G$.T@,_>BSΉь \rQ~wmedKV[Q\߮ ]Ј @ }z`VW!vߒ0@ҟ5]CUV| :1/YU dS-TeXYf}Nw‹8tTX8?pC$(Pp O '\7lDqX,ي\LJX۬,ج#~wP]`Y dNpa8tCfPA`r1-ISsM_$.97#:mTl$)_Lߋ_bʇ92=R7WyIb1 ۚԲp86Ȅpjs E$Ziԥ튘7CLzuo\ȁnqeqcnLs?uVstHNv*un%)7mwd5C%_] t+ Ƅ33, ^n >rMzWcv8#VҒWo3);KYW[zH'(m""" *#gq~~ޛONn.z 8jl"+ .ڄ $cV BKFqvW KbՁe.lsf2d1E'<Q52c1) d=g6{*Db>j`֋Z ak)#[,g- P> zvBݲo ^^7^s`}V(*:Ts0NCT]pV[|Q4PC*,Ԃ4JPĠE'`!*sM<  s,% Xxǀ:ѥ0rTRƪ} 3aO?R,#^'6jե/m'sA"46V:ɘ8bv6;潐2&qHlTK, ` BʔcFʈס˱lg'ddߨ'#Kc(wn&I0>fwEe|9+,lq cSaTrEbo9gTjqpR GMZN5T̚lBUD>^ g1OF #KHHSG"bw]fܒ`䶹XPNm`Ӓʪ1p}W{ pCb o4Zoq4Ձʼn.\T pPIxw)䒇S`"|Zp-9[m۾&6F VujVS5cDz^רpcfXPEr0C)pN[q@KwKs8f$w=*JNO/㈌س SU(k r.rꨥw 䘸4(oA̜XL.8HdG-hoT /M:5g7T,u6& rMh$@lv#<Ok'bBFHV>J9#`@cq@؅mbZb^5'mšGܷa z-ld#.0vGkvlU]}Nt_䍫ش:eنEZFnc9-h2a>Aۨg?wXD(^Bm"c\"M4ZEYChO>웩r0^:yRi8L>q!:EN9w$XDg ֶ/z-`^\yX07·j1_aUXW*΍{,9ID6mzaa 7?33BOM+cWshFpXXfk&6NH%xa89X[cNTwfc*;n&'fffq4!͌3LĚX.&tcam=Zcr4 {w;3@ 5G%o|KwWV3B0O|L+ >3zjٙ&n:^ji(;+`vcLIW;99 !λ\KαqkǙNˋUwOؾ02KM 8yQ-;oE'}sߏnV4F$|JME9I(-ىV!Dzf9ڞjߺvNh:ǽ]  )vd6|[HH#CHHH#^p[g9*b 8 Z(zhc!<.Rӧ;> (p,(R[ڜ$XUTR-*B]X^aGXMv`#Ԣ&6h# ҦGnyܓ7 \(H .Sq,Vv"bm40h8g*'$"fR@^`&ܸ w*zG=좱Hq;A٘5TD㷸e )x=@EjޠȢH]~$f[GdK[J@ Z!چ|:l/ia/ Ԝ'`s @8!zMf0 aT@e:9-NY\D}vE&fa7^3w99ψfYm(|NxV\ 7cwhgV$w*F*7'5g9΋-MïW k5V1,}ޙ=p"\~0GD #IHHDa6>O)]s,3UVh2,X(DӴELI :^S(â1 >D6b;;)!F/p&L&m͚6soKvd+A ]ED -ڢ[B#-5-q|ѫr#B_ +Uj3Fl<%5@&=gl %Lkڶiy֍!pg9Wf| kwQW.3s"|(akFS0Ud/M|Z{fЎ(-9p[1cNeUw B#]$idÒ,C+<?y!Uk wI6'*Ʀs#L[fzgW>j5 !3a&:$޷)P[].zka?Z#bO!'n(ZerI5wcؕ,Ulzע ݃pPRdʊPh1i⧞g֨nFqAm5ENs-7ĞerjIj`dZԽrxo_x\vMf""""猀;ӹ.ȜNJ3AYk3\30-D>*%v7C[E+fxmw8@d|`>muKԨ= Sմ x;toKSB뢓eP)θuݚp@~7 hWe8EȂT>V$<>(h0G9ƢE!W2%ʺv.HdGu,%Nۍ ,cNoi<2K nc h7(^7tNѮ'mI犼ΪJ6򦓜5̼;Tn-[ \nj.sNpucypRjbb;Q',D%Ɠk ѶBж ]beX'9T;ι6H5ygVToЯBq[N; *gSEJwxq1(pksb%Y>nDf2HVi-x'a[&]P'y2sF? ;W gttIi\kj:9N'^8.tS̾)d{KQKۋi" 9_cĆnfJCI&n"5%) >+>/VE#۴yG~E Ñ3]*nКlkWpwSxT@SXҩ4С)Z[zf.D ߌ>?Ӄ ɚHtl⚲,UC,V!UH?gbebUAɱA3 Vw|04fMNpf<]oc5'nh ؋Bw-)8iо8+>HuejծAai-v1Wֱcas >L; >8י r,B`$at[=.e4@:NZBxA-eo>=g'jم+uyAHxN@?'e@@%r}۷:e1l{Zr nn F?-pg^jb xri hpڨV-$t\A< vu/hV_^x+>U3?\R^pߟN7:?wt\ug;|oj=T.0ܪXGBigfبʒa y.1F) DL.9 ,IF^elƫI;qh'pfXn}-Ƿ9>n35XA >+xGs-yCUMgQ9G<@5.wTNv̢αIwkn߮)S߷;K֎Qx?KHF s^"="VI]v 4ᐥ5lz;ab;LX%|sӆ`[s| ]69]p$ 4u%tv{?۟;xZ?PWulW^,'0ma<ҞxNbρ9APhl5#! cl׌#־}ZwܩN8*.A\qs?2Av+ 2iMc>r%^%>\S&7THсN??c)JF IgYs;t5831[3ۤؓb&ӏ}4 G1EVܚ8Rӱo/:<|ІsHH.!.W}/}jgjq+pC#MrS#.x?"-MB9Ma=V@j"*5RTy#5u&lV{h{EJoxP>8C]DȀgٖGiMlKdc|?gKvIt)n͎8|R}pn>-2ar|nΉeKQokPmNL @;>Ԭݮ 4 +e" Gnr\SՀ $O̾AϬ bQ[F%掶(pLω0qa"=gK5&W{lF`R\XXT9.J,<;L6>IZ-d¾,N8Z\5ǯUE^'~\9ʰ~g'҅bߘ~wZ=€No ~_}lkI0}[k%#Eke۠ʴ,H)βK8]j]J=Cn^2A.bp_HxNM%Iu|WguN|/u(/SMQ0}6, hX/jeGA >,[gGT% 5 Tg&.~i?vx"мe̾y1. a;>gw|>;DZ%܇j%_K2*eyapIn~0sS'ۼ`Z9GDF2UnV 'X m[š$66hsXPNozm;w-Ue.FEncQiu@yrɬ,F2zn,$ @2ٜqr_S߰Xr?=;3f?SN 6!VƍOns#0ǥ$-w("Fk" Gq:GY`'6,a^Bl?!O_e(g wcd* <#F rwŊι;UfN[.:e7qUj]o=10 ~w2+#cE?U?뮄 hR INA@@_ShԿ޿^a,7DžZ𶽅Hr>4ERР K :JT:j-E?V\CЎ:{׽p~MƗou$>ą>0jK{͘_ܾ/v8* 1 -j4b䪈*yq_8 iE |9]`i Ò>ep9TrX$̆a9>ﴢ"x Â*o쬆/aF#2 ̊VK$|az_9N[᥋!/#E""""!D@+[Gov;hi}4C"`YD|æ'w?d@mH,Z5vv!^Pi6Pu#>9(gׯƗk@~ֹs#*pWYP"Sb>cdҽܞ7=QsRZUv"v F= ֡k_[fUM;m#Sx ru47/Y-]XzΙ{aL8 Vrx}%&w6̽mDˋ{\ %J]biNklߗ侞rLўiF\,Tmn x.mw\ {1gw92's4.#c8}O`1![O;{I&>mmRX9OԄзrPkL^Uuj&WwYR[N,z9}iE"@@ E E E+ǿ/M~0ovO6K) 7(J;\ g5s,B_TUW΂mJݝ=8[.w/V()1*џw޿nesA95QmxӞsN[lBWY{shļ]s x-,cWFtiP Y,Kx0xMs>Z&*ՔCP;o .Q'R2j YpƢ,P2uۋmU%9yyv9dMSrϱI9eqK~6 y19)Pw30Lܨ}x&){a<:+B~/W1&% 0 CY_ϝ|p]{<6 IIw8QI隸R >3QǛә ${$0WHu}bE~,.ň^߾9 3J>p~l'}B=Є!=@t|"pu|O>/=,d89xxRP Oq5/A=/:ZS}@1:*1/VjEp̟D~|n]$aGO3g(h!4-,inA5]SiS%I&$|PVm~cl(1&-`6hW⼹ʧ-T 7rAXҠ cnfί y+zcEG.)3c!M)'&pe5JR *t8!z&x 3*`Z*'>rNV2Zm^؄O0'[;,4\Gu9ѩƲc;9dqx[ Tjf*r-iwϳ>~CoPE0'\ MJҷ DmK~ozdfC<1,G뢽W}37~ w^L^^n.-QAu:] KpQ_?:QYSOJ玎@;|#X,~{7ū>w(J{yf$W0AjMZ*u ֊FbZJ hSşQ&ѲFۘs>ULJ{l-ዎWՋͬEo굽th!"eYH\ ƒlB9vA0%| 2j\a?azf[Am˙¯l]XViv$%bo]@1dξl'f>RӒ(EH텃F/ۢp^5¾acw?n(l>Pۿa¨ oT?C_{l8d`$oiۼ 04:V3 :o>dZ7Z ATfM鯧ʋ.\8,+~M}^LEu y~zt>/:wC'x""""MU9Oç{矆sl>n_so L, LC5w&ќi[+۸o|ݶ CJ,d|6\W09 QTx#`b,dS5 J.p6UW=DtD!؆YU̡,,樨{v.jr,iKvkox!=Nv-=WxM $nNhbmov1`2[mAS&f @U{GZ7s.ٜXnXн&u' 7:Q~'f@Q`ޤ%i*bhl ྠLVgN2|3xU M2ԙ9Ufش1eѡ#gxt.cN׍~/??l˗|˗we?zߝK;O b\~wp˦G@@@_xbd8G!k[ŧגme=nY]֋jFt~yO7u]?/lgRzz/yGg60< jL .? NZ zf'}m9Ij a!VJ|9_ ,=r Dnb/G7&܄lshJ;rӳv8O}#TclXF&Nh̝'3ĖHKRQ,m]_F)bWٞ81]F&8&dmft13Xa࿸VDG /WnnXeJ +[] /7UĭI~r;gi,xz%ئN8w C%^=ɷN%!/Gq d*Ƃu$N_~p^} &N>zݞ?p3)RRR71Oct= eXvkpwԙ'qylXG5)N3oBe x~,|6^J|u'K->T򌳱D9V4eEY%V䢲s4.Y\Ŭ(%TA>WbpR]H*Q/ b[%dz%كTv)&h:0Kɝ*l 4v c1G5R\qrS2Z!li9mk+ߛICߵUzsa$zS3`“ 3a @6L$L22k| 1 ;_z<8[ sgܘmclOƼ.p^UM-y&4;\p*4ףڿ Gq3ҍyi3+Wsmf| qzܙH yӨSR>" YMJ!~GeKG?1<uZtQ rٚtusaYyͮL~hU賨ðw"5`:`J 1~Ӷ.rF6%-~_S#M9e^|[M?BFcֿO['iN@;Lq|TE@۲`?H Uu2!wkokiF~,wOOHs@93GhO?=gފU&<.(Uev~2bvL(XS0* ؖ(^ѕB`m9y `uYhe[̓0_/J33Tfhk^_C})UNxq}J"{H[*0^9CA֦a6\k YDH΂M8 ɔhٰ A[/Hx/6b2GfQ6 C!ng}ɗ~C}#/d D>@&ӯ4q.^l2~{r{6/=jwoQ;o5?qwb$zvN볍j -4^ZEn-P]&vԨZ9 a ޶ tu5eМg aT~XEՕq2h{-Ă% G;0iʂ͐7Gxo_g?FNopTCwQ![(ʾ71'n)Fq$;ƚ49aܻ식~ @xbaU34Y蟁`'2NN,YD#ρI ˞3^ldb0t\@vn/ 3mѦhoE!F {@@@GnV:t7~JE0^>l@mv ](N>,b&8;ք1<2/Fڌ@hj xT4?-+"9{`(r1͜a-Iޢo'зɽlYqs>j @X74p (H=4`4 ʋ16Pj٦SZ;o&r,Jc0g:2s4E⑞RU[fr}ŕ[юV-Zr{ADIsک3J@ q6S׶B!Rz,ZI9F|0|||'=R7\B"""""#a>ozfzP/IC'RO.me̓wJ E]G~޺=v} @L]0'@p=FTmx{ u t~{ Ǣ'Z?ݘITԍAEE3סƯPGd:ei@8NT[{|{wg Moex{SH7:u5 f٬N.@c,B@m]B))A svL\i¹rRĶ`-ņbQwY?3o?VGl#E" I E E E EQDU.X<R[2y~3o2`*0RB$(F;sm xzPmˈQeZ쯝#|suz!;`Sf ^ܦ)lr#wfMkWaŘ`GbžzEfhR&Px|w֨P0m6zT־A9EoMO؈]M6h0(ߠb{}s8y=ev9P'#'1YDrԧ,I Ʃ F-)Z-QvBR4RVіBgTElb }PM'|gVWdY,LSO{㿹r_-D$xw))))/d4rmm2&d*vBW6KzFumgY0T1`DH!Ju.TS]5 TU͸CtO6Qi0,&U(*,U MH'a^>^F#L8UxR:~{X pϣ44#[i)=(f9 "mO|gsM\uz*G>B+SpPL1bp̀rFpϽa>G6e+TCjF*=7˔> ØopνMJ^ĆIjxy4yU<՟w{K~O@;!I Gs:p:.y8Un] Jp/Y B-lk9V Q43?h.kiypmz+393 r麦_ٖ8vqΝlF66k)6>1D̰m#?}r*Nm>p27t]ϩXMoB!| g14a^'T\AL7DM7i]&"ul5@o w-GOiGjѠe Mh@]$v>x '>^bF(3p\X#%*Ǘ(B+ f!t Ǥʼ( bCɻ@ѐn@- nc2k,~,F~|Qضg{kVfkm ݘ뙾l<=ƀB!z4w vx]S7B+GJmO4)@k 8l7E x : 7 [ S s&cerQRb|B}_Tߣ0!ͨ .$olО^ck}&\A"""""kw;ٍwUE~zT;젒6om5ktp2Q>:#[%WpKgm"GX/:,agmm%^,dX6p_5s>GnIwR Bjynd _z >Žq H9yꗒYW3(vYCXnB%lx@o=XȄ}ٝnxΆsp뽷{}#0 xTTUݻK[VUIr_%Y9^YR-0sl[}f,pcķ(-aݵO@9v60xs& ´kfuˌ& Ɯ~֭Zot5m3@^H#s8^.wP1^a =3Z&~:8Pm#cyfN(aSNhX͂xDR1T|6hȐK9}0>֌ 3(&tV˱E(A j} ȶz!h}R AӠ4*@Jhq" jX-eܥc{.L8FKmEECJ^,& qtZ 4Bk5is|2RŊuTρf ;]paa> `~]( 1FB_{",hs8 M1ټӌrWL}_9 mKZ ,BQ'͑CK  'Kd`tkٳ߻C; ^\?}6'^_ X wh}4;1lMPќA8_CXm2h˸ kɨ\DƢ)~O47.rz@~:獙=_2tתߜ@X~>*}ڶGX.Xk bh୾kgz= yvC,#'B>u (kjNѦ JWX `{G ZYyQNvQL5$=Z8 Q4PaMNÌ6xLm7Jڵ{qvNuя-{lZ^>|ۭw-כd<]gu)<0h[ƨ 4~_^nl<` ㎌dz` )`%jvW#F|w!Rj;(|`` Y9t{SY '5B=j5 GV0K5M 8Y v|bfg+&SJv8N>8lPs4hwXߚ>8BAlit ۅv#SU\z+醏g';/Dݝ6}׀{Vn6 02GX(NHSwz{noz*,V҇7jkQ-9Ć'n2PVVuKTN{3}mOzbYn]qj~gJsOn.#]z# <@\0V`lo`ȲLv hBpb.ɢ- \v.joUma5evXB:/+8[X8zË@sj3uZAuvlSܤM@.Ί-TMmp]p3]`r^!ƪl{Lbs T_+$shbr"Ыq3jz֊ 7GȔp/ pw1M2 BxB ^y@b!>jЗΜ$C7Su| iX`,b vαf@l9 vu-y)%iwF)V7 ~S^=\.dCO|v@@f*WzI01  vg6LUN>X+˙s,IQ0 Z=jͪfxЊ9 \ +f=` %ZT96` v5dQvY fX:⡟n{M#ˀuvh<|V+<܍Ă<$)3PLG8Iw'~^D[d ,.Qw|~*;c}sLFml%dkj)kqm(1]mY5"~|N 8?ks&4LKُ2ܴցKp3Ω xM:2>yK^_ W+~udK[Ou#/ɝȣp`wX;i)))wdrʕwדl^םM-̨\ꢈ97C 9M^n-Q ,9OUh@o5}O[D vX(Poi kb DP%:>UyK5BR%Y 8ՌmԿY[f2KS(JEƑNO v)5yPOxګ>pYu03sF`msUg.6Q͓m)r=W1_Ō(ͪ3 ͝m'xp׽a78;/,sz+i-^hk*_>XD/yoy#GDj,kԨH^,5]a}nV` WnuV] 2 {&@JS3/*EaQw6S5p<\1Z^e=@gD.PȂ5:l@29,m[ض~wO40ǠHf*ѷc֔fls- ;%7Vv[[ >yċEJ{K0+.#?]B55-HZ"f4Nl\D}d?KzO`wcO˰[ĉzh#39z # Ms  To9ifgː)i4P9ZZ]Vjƫ <$x?|#G @<m`M#l>x'a`+`ddx@l4GJa&O:V3H&J#<6 @Z4 ф:\sgWK˱=1:c\'8G"NJ9#,BsŘ>1RdswF8I;ۂ(Zk7VUQҡ%_)&Rn$r裙>^i$6ÌNxpZ8BZ 㱂&R-U,`}YD8Pa1/n w(6Ss*nطigo- hV|5|Ao G֣,.GQu?utpRxQדN N;zg]8Wkl6 ԡNV6O|̿fV~E+p1#G RTppː&}.z4E1: ETJ2ϭV It Peq ")\F@@qdEy!c H<n|{}m n!z'(\=:Z@%cxG$Qst^4iCޅ) ~ :=OSrAIuu%!#Al)yc\ vew,Ж+/p.Czwꡌ Eo<$(FVv,K:Ȉoc)qR*ysllZ<w Ϝ6Ǫow.y 'N)|?pD?|#G @<o`q:NfMƽB 'R  W H \nh } -@2+"/y}s"uT0n4ұjjGG@(ڛXȩR3M\0&>'ak.kJp([wJ5 }KFC7R%@ ~ŰZiQl^n m8OLc)lzVKȺ+vbf f\S@wtdRcHVXDՖ]2XRBt Bӈ O<C8z|)hMWg >+o ۙ3Y…dEwmm_}?67 Y{:q@ksv ]=xkk2х^^JLT,)a!Si+ *q|5)2jkC/-V$tጓ+5s\d$^ ސ +'@ /3 xo}W[τ O-ˈcDTƵݘu Ls0]!A;<Rf1fNhkkWI\YF;;S0G:a r DzFٲ%>W4c9a?-L`߅0__i"q4E} .EXVa+!%}5=N3j^~dv p޲Y:Y8̷C>t<p<yiʅ4J)Ae8^YI[2 M4|E8t2ϝ+'=TYׄ)v7G:QN`4G |@2 ~xUfX\Cއ]&"yNa@`>ۢ-NW\M6aslkϦ9qR7FlVyh7JvDkzwvy}Z6 {%bur8dq@k]5`a9HГoff0 V&575=+{Pͳ@=w:jڎ6V?s n )ւ&b>=#4ʄqM G k/ܟgc?>Y{"Cv#GGj)o^tڐ-4o0 #$j#{ Um(}=B]鹀p'Ψϭsy~uc<䧬x aCx2!x/c]W(.A]us7~Ŝ?X0jBip,7QIfC\Nׂ6R4DB_Fv&q:z=iu@<ϲ?$Uc-gEkA&fa->ĥ h>]ri"<O͂cju-Qbk_6E^/ &n4.7q8דj)3b?/۷:: oyoy#Gz@>`l 'a DjQ1Jf@U[]̺{9s8B)kF;- >* F]5O8[`[@X~XXv6 e#2sW^!)RE]sP]`NJhq5g8x~\k7bwڈ2 gpuj_̚7# `;=UTkE8nYu.&h۰5`e+cp]ukfْ1IŋףԆe;8t49ȷutSq򘡍!Äʔ{KTg[Zoxׅ^ .XB`ͷ<y#pF14^߱Wa>'EѰ5ҁ Yk*%ALLS)(PEs,#@IDATX vK;a6Q{H k,v>}z>WWNnAtG F B<y\8w.Y {Zib82Hu 갦)6_I SElnY969Z$4jx^4vM%3YXZJNvtje rKV ӂ(ٗYۄA /F&[ 9&%7ڑ\wP&7zۈ.zM+0% x4 >eų"jhMrWiv)p ueoJ;}.WqTg?Cl{ Fmn ű&UuȐCvaHG(]s=a'W19{ d)ƲÒSA{`47?Ƨ+a@_@><y\x]MƷ7 'QNƋ`' FB@ss 9%"&2^3oB&B&Lg-6=<6氱 n+A*+wnϝQ:zY3\Yh4pA\ F{?Ѻ'0})= OEPAbuG`nZFvzlſ)2+6%N1dU*CLd`*8w J[qFC,MeV=p[BI{@i#).g*EJe[ǷBvp 1>.2ޏ*:}X NEEslsHZ.3C!ʷ<y#p]F`M<GoI\\, 7$Y9/E~H1dzHC?4 k'h]& NI2)8AngZ3|dC(z RdﻩyhOX2 mOdMf!`W 8N0;I l9}Ȳ)I3}"%i)')ӰaA2 yc*~(pRS2! :q8i2t#ܹApl!@?}<yۡ@G @<8pY%+vOX!pܭɀ90z e=j }6׶JFn˾j}ڜjV@}0/D7 Ҕ* E,B`O֑4ŷ7`}@<ylDVE ,Mw.* ,ܔYKJVe(7 ZYr23|_d|.LFOTX:OaGwCNUQ = {'Y(D ?݋ٔ'yfq|M `_h}eh5ۋJG-X&9^cJ8p`FQ 4RqfP0 )eV#t|<; 6t5@.~>nzykCB\MA`s(3&AA%HjL`z, 0r \q.bاEXG#kYRyy _G}'aB,/3Ї##G aX}Axe{m4]fD9&@O`>"5IF?ZHD\YVVg Sl`Ubnߗ}DHREX$ 쩖t&'ʎU-w,P67^Y42fIXF(Жv,J9A8,>G =^>y[<8fRy`p$jZx]6#o3+pu@ G.v&T\(~QH~izZG0ƥI5[@ $xdYT{CW8flYq:*kP^+'z@o"A4iԈVt?g:;ruH@ ;@<{Z*tk*̢KEzNgrL^ `0~a| \p!iq %Xq=2Q\ RDoYR;B`ڔU3,,k 0S+LD&5iO I|-grv࡞9NbeʰŀIU_R%JR_oc^IDLaaĂ¾%O [hÝ|/TLp611"K8$^Yj۲)ZK@AXX͹N#J-\9@p8H¤l}5 ,cX9([lԚ{gAP6ɓAJdzkV}?w,}&Yr}.x><y#YVӽO8̦,M&6eb$t xF;T Zql2,[ FY|sBm\!x pю ]&Y6WEtM0Cg? }fډm.3{ ~Zc# T`\gQ.Vy_ S5V8 % h#K^ăڋ蒋H(FѦ8P7Z&t};\NǐtԐWj!m%ڏ$iAm#Je]U a'p@"RmTWLcY w.xZAZ&H0ҢLxSѢ\^)5BC^|y#n*UپB`?puyEf5\:`es- MlվM -* Kv0F o-Rߌۆp05 3(1h Taweɠ3N -9@t`S^ȸ(7+{!m)O"`4&7a!+H*`݌A=D9Ӭ~ײʵv8yKpVX6n2p { & t+d\"B8. df8vT3&@XwIPz61Tprn: kpJcL0biWxPeM:+f;RM/x;_ &p!#pG܅ lm[0E-iv]V0l4+2#SXU,.8KfhSe} (PvZ9Uрq 2&@DP-`Yīo6qFNead6v9iO|~2cV`l%'SmoB{m䦙Εh IeaӧHU.Cs`3@fVn0􍎙x~(+L? ۼz#aaP]Nko 0&|,kZL4)cmb_P1YlL#m$2hVLi|px61%[2c!9紖zdI豀p`zg#tjl:-G9p9 M$oy#G-[9l?\/NR?D`U/UNc QIF0 V-eeV[Lg}*qPIMaB N>uk2 ZfPڞ`~ι x홐]6"#Ʊj#s @VFt\U.@EHG@UD&xHQ&82 l H.G ' }T>#1-]h`x8&aK]gvn{zG;0;o +~w٬x<v @#p8" w|qFn:+SXʶZ6[jBJlB[]Pu8i)8N1j%c 5a##@-x'WWRF:XFLPZ&豬Xs`[o b}yVP/=9^06,e'!.Nj $V XY0z|CI=>8&ΎeAqH*A &G*݈Ua,圼>TtLҟϦ_wb_ŹꌋQEۅ!iCk: [52Xi% N*y'Bkd9B=s5:\T(MXx:c:d הoȮZ.9E\,mJ?x߱(;V')!uXD`n]XXyNj7ppm;.]!azA"Hsu 0v?IdkkPI1 ~eYA?4#q\oGA4l4T92Y@랐D yIę"vW{gx<ȷ=9}_||y8$xxI}$76-.}X*Z`Z2J1&U2Ĩ^#(IQ_ hlb5H"̀\@r# ` 2 C1Vh_9|d= _8@xp¿-DDn1:,v[**GMX 0嵧TnuHd IRk92%F+8wϱNh\ ǃy(}^x;IEpt 1 @lՕze$&W`w\lŠU*7cPkloexmPk4ݺ^ێ_wwC|<׮ع@^#yϟ3Q9 |#G @<_yrQRZZY`51Sd fnl 26p)qØ&< Y!Rege2؜ RmG;+;0 +[Fj[GXdEHa 41!HS*vGN72Îi;ζež}q@@M%7;Ndzz?\.=F[!7uFpJ@pc'u'2 kjlZ߭WpZX| yVhQG]QR/dIV+7O?x噷?H_ u|q#GD`B(n\gkk/ ;;(**EB[V{M\LVN=]&܇ +*[Jz(@ xXOBYV6T^҂A8FOmYo%' 1FF vN@ kZKi9pZmL@}ȟpn 0?+N0 {¥w-gG49ʢANF6 zf1\7`IdYw7em%:pY.Xk ,? wޘd1^'QpFѕЮXFJyD N`WVtG6>\-*6T}t@9p={}ơuzPSD&+i|= 0z e,3?gx,4Q8G @<y`#p?q{K{nW5pςO@DŽ=-LC , \zGLz l'Mb] }Y,QN = _i_ 2' 6Q&5rzd-QgjрV,ؕ5"Lyюɒ5^s 2&Di1gU x…* G&D[$`uH`M yh޹ W!.s:ɀNb,C=@rɪIxSj;m7~<ܰ OR=$N4zvk[^(D=1c}gg$~Le;/4M0Tzm5$kuP5r>`yg+ EEL|cb F@T_UNlC:X+aШ{П>{FbY +zds>W[p؉ ލx KJ58Vo4n:ޛ‹[xNO>,bj0uV3Uƹ:Ijlda~>nIy B2voGw]>U#bY>;Nz%i,XNںŋ\ e[1{+)N >N22q+ 9($fZa9@#DyhƝ.9k J#hWUa$Րnx|-7Z!u۱)#[~iHAhq&L d;= -AnB X] j-LF<^0|=ݙO^[,7k~;5NjTl!0a˯'x9G{v=F UǔG @<w?鿹hHsmy Aa>ࠀQQtŰ M%6d̰I2u o/g+@k.=Kl ӂd (x[ cp`B$],RX$>&W-}T@a{(#ṀUS"l5pהNU췒RglGCfn2bjeɶc?Hbk| :Yiw4ϧdȹi 4!pYOPe"~F|]{ t߉Ò'{:*!&Ŵ\kxgxnHΞxg o]{7/]]uCTKcמzs:^G4;}ֹ$yDzg|υGߑŪGɁ}G @C<Co~,)k;tR8Y!x @FX `%=Ul d,RXLSCھ-ɪt2]khApT`t'i`5˜p՝ dTC8:vn`aq`ǶY8s&=,>圁,gh? @7'{GQtaiq_)NG?Wt4ݧqyj[tͬݒ3gW"H@ͫub:k7`pgaWIJ I`0 CX6I L+2 /!/1pːQ E1N WmأW OY C#WWSM\XN1ɨkUXdq `!sO|Br!.iay\9_9stgcܶ+Gx;zD o9P+u&awp=7W;EN̷:b:Z3>f9/ r2P q;-c "֕N-֚{=)aYϷ<y|aE #W{{|X$k b{2@K`v$56J+FihJ#frPPb(h-h- T(\M@lh 6N(0KA dwV[0eʈ`* dkFk㨭_^Ђ?YdAo '5a!J6r\&.6vYYc/uEyENI?"XAT>y)4`ZCځy_* x\F]1"J ,5s#yd<$NQڱ(|rExv#ɰ|l%rXkJ=[" _XsǜO+av6Ʒdl{R@[G -Y.6R$/w%vJ-a|F[8xܷ%h\C;o'_9毙×W;՛_éy 'aO1w$ېqa#ܴcQN4opQVrG__y퇟}9#C#Gyd<7㇮|cl¤0)T uXtfh٨0'‰Z;2r#x=D1@M&t/+hlLmf6YxK}*#e*ƒ2!Fd-2b_vMVwuwMzW.p9Ap,0V"+Ei75Z)Xgl(X"):z'{ܫ8K&^[9 :{8>` äHrǜ`J1I-'E0Υsh,5r YuW --[B25Жϔܮ"_a)_F{ p QyNvpQ:Hh?i&fT <!\pVc>yDSQTSpr?_Dg="omΟ?;b6 Wv_3F_M_3^D3 L]qŠ=3y-t[Cԑא0Fn{cE-qD臊vL9Ukh"C] IhDdJK?ݓ!lMCo<.w2O}|:MVT>]mpr Fa=)R|vKR3QXa~ӌ^=cu'& '2$LpVyZP_+L$ngtoVli^[V'?} v^s}G @6nJK_O V zPzXM+k9 VX<|-V"lϭ6m9j&K%CV0WAxsz&@I+<53os P`lU< ?LksEZ6kFX)åyfx=Xh+5@44QaP7SC5L+U eS29đ @1U@Q,+@^RU&Qrww6Z+$iO]EM߉;;˽ "M,.VZ|& ܬ03gB~>؆/aC8e/xq̛M/?<&oqZ$:aFg3y3ḐL,5t[ZN9Eꊝ Ȗl2f8w; 3_/^ hlXj!0o3v, O)=>[ 3ڤ_5Xhd$+Z:|l+R&#LΙyMr vaYp o$H j" !ǻEapr>}1kcA7};ij %eS@y g Yl}}_f^fMoW6g vqcEm5 *C tKX#c*wiucuZm%\7NY Oo I8߯^m7q8TľoAͱ yܹȩ+?~e._n/E?/_<ʿٟbb.VO'wʺ](W3$I:Pc[M5O~<;h]6dy P$̱mz躸-ʹ bb{-Wν$m5 gn{u 6껞ˎgE#3gΔ.w?I+EXZe<-JByiutq3wCF3J*τJ(,y$D-vVoY[{njJ@#kY+ؖqg%L@1 QqE n 96}L`DZ]bH@?}+FAjN? :PϗYH@ؙɑ٤t=q#AL|sIQcudu-9O7+;(2Y lAALei=ȈIj4^`n1gLaG%t^>/Q% F*^pѯy #[)`Fȷ<yG@?~'o,WuʓsJ:c3s̴ς@ ̧`o;62 _iyU3kdDƺ.`nC7MdnM8 AFΑ';lkA.SW8vƂm'frqD8Ĺ^yJ&5. `ƕXeQi =WEܖ{ ˆ!蟒 "a M[Ʊj=-¹1Ð=cd|cxe*X_+|t$N{t 7;Vb8л8t)lCިϴ_c̾]Q݉1=+PS;Ri)X{> 0 #i߬_XXsHcsZ%$ro\uؙt">wO%"r{ίsb>[Thc)IOJ|"e$U8 ,0 55>U' x@IDATEV|͆Lx9yv5j] q(Jt̢)$B*.b'tV;֛V]; iZ.nn蛿K4x|y 9G[<y"[·įͧvC+ҖR,D9̅C1[YP@.z:]eU/gg 9aVoDbu*_`53x11#QxT\?~{2al2:t2Q<"#) mOJ!@@fo Оn MPڨ*{T1lou(9_CkC#+4 ٷ$Aգ`q, Z|]2ѕ' Oʾ u7~-n9\p$ǃ\*Tb2>#us;+1XqtJ*'`CUX8r^+=0w[G4OpŌm?,&Hs{2Cm-ph#JC]дo3 zdҸeOMF1K}ҫt:_{m_u7_]?<y>8aYG[}cwtII+k:dw|]fZP xft&TߝS`d_QP8Sd[2ZePuH ̮Qlb: kӨ>dvZc/0B'gN4>P1?}EBį =GMB:y`P l~txq9ˀH k+QXA=ӵU]#y1oSF05 Ʊσ/}#=M@rbGRFN5C; 5X߁^#"(59N`-*BD@͋>{pyG>"V ğ;)sk߫Ā~y+TW),RI&ߴ~DCmq+7|2ȭ;|Z,#̍qB*pK"3:0FKK>@f1lx2&{*Ť l3d}&VNLs4qV1Ԉ>2xi޹7}SMתX Tla3'&]ld%WT{QFpx;tD8ka,_i莭7#-*1f #f,BZċ +nh+,N9ܑ/rA\G:? ٴfNcS_[]pΡ!|Jo?ur#o i7=8cQ(B31 ޮ"L Y2} P+Z p?o3ygOGbx kZQ' t0{F&KpH Nb^*[6NDabj&Sk9m}e6 C,`\ۖuT$[I8(0Qm;c"-Z{2Vك`^6`FVkH䎈ytX> =h0VT^҈ |v@:x;ݘySg>aƐrn9r4@\Q{MwJXu4&J({@"j#~qL&z,x2yv~Gq72 qk~!V\/#QSNӷtwO+ZWo '{<6?L}Q>ȣd|'af8%mNMdאcڮXcbRP-'Ed MwmSkQ8Y4`W#^C { (^(wpP7]#I#V"n3bg4C~c0F(:ZWuUL]Ɛk$ <Z-?~5|aXpBNNCBwyRvᑋφWcu䉌,8|$ [hVxs;-]AߜȡFp)B_;sˋvBu=OlR~.!-W]tG @W(pB kp0ڙ$5&N[Ey),m1b'QFq2",mrcQ3t<-9 ?ՙ\(s!5A H8wKL)Dn\&I 9B*JeE*2A <XZ רFfY ƀy6P€Y|{c^k!k̈: ]W-]D?*@:f?+c.蜕q-ATr}mCsc.Z P` Dhģ9^|_be5 }| 7cBG}q2IO>v<>䀿3?ɥo*]ah-$|*|L"5st) >>*IQ(C4#}?~-|ɍѴ7ӚdLuFG͚2F;/#|`V!pMmLڍglcԲAsN_u'A~od Fo"==r>?rۿ}-xޝm\>g!7YZUUtD"y~3Js[BFTC+Q0Ltv[nEY}X_ @[H p$ꡱ@(d3'Ǯr<>L cH]CQ;lof <L $0S~HUkaU,Qm1c5aJC/.SsNL [ bC*ds𕹖^H;_9 Ș j$ClXM505d~UOasr{-^yU5hua Mqh{_޳<"quzz'\_9k m #xNbl2qcUXFMWP=x;|SWGGZ>޷g ѧ7e_o|T@>QDrL PgxvCgc~@ %Lxy_l1Zh0VCo$/eNPD% lPRͅhH'@"Go^R^x @k [M>&QíG6h m0y~]?譯2_s{E醝n,g-1%zZX;\AJO\AXGpzIis5 #s=o1;ē,:;)F~\/Ռؗcul%pʚx.US$Y~!,. @_MDeFpYN0cR\u)$Lj{ Ct>P:aKr.t s[AdcKgx-x;uԷɨ11L T_D}AvqaWSUHSgR @;d͈݇)I#pE'ybV ?Ġfg ]64@CjWÐcFXШp8`wBsw:UvcD^(+zݭo|Tgp Kk8,X"W0ť6rFbbL`VL;{|$vaɟZH9xiXGsӧߖZO?nxYhLUw /gȂcf I  8gAZ 3iS1̈́ f;Vb:u1s'n`FhdLӬ7lc!.^LqF.y uW,ҍTm?RG w޶z #m~ō 2$v/x8tܛ槹K?zܣ>U?{`{࿠؀^A{*g9&ck[{|{sIY+CS^"ȱ$8jr.`Q9n"Pg|BfO1H kUrc2ҦvDZJqaa0.Y|d>SĚjcJV%#8>ct3s<{L48;MeKY\%\e N&<"DZPR zx!(r[Է>" +ttγ{1ATeL'ܣ`ڡox酝E* `~!8mɳ1}ȉ)c98q._c~鵛pȊz=/ZֲN?t0~.DTwXJ:wr3FډMf"3#z4LB#2ݍ-)J/c0>Ac5;:H\me2OI^ 8bb-v:|IP] he%J<|p)4`ȟaGa{008#+3`с9GcDgcP>eMИafz~pd'.KRgO̧tO?ٹp.~tv9s&e/x[Vu |euwcCKv%i *YlW ˋafZ6P9PYY`$/4De|4i0Vķ|vmU,8Zń>:>=m>YM\( :{,<|Wde$~v+!@ vGMҮ6dv7T؀o*RT{D}P4&"eN"5(~(kp*uH4)]<0 l v9IHlg!XL{h&-۲9*Fv._J t@2NC"QF qȐ/M6mZk!OjK0#SH*4aZՏ>^+4 ?ܳF)z&͈Q{Ё$*-bd7R#+%yTjv5~-Gki O|6C"9p{WrFEbh)[9qm`=3XxcslV* S=^?}e q uF\ޏxksqϟ7'w[cGd] VB>$LL ;W3 BX6B7aGz%->wceFˀ44bc#&uǹL Ltf.n*&,tmCk99v;nR ]Wq}Y %OL#Vƈ C܄Mέ]5BA42`e mNN\sq#W[Zx-QCG|\NEm>ĹhܜKZѨ2? 9n{1ƅ<EpeAGc.qM;b2Φf6AMVVqZo._lzVml>N?J+f@_?(R]Bj`ËcS"0d4tCEЎF*vHܕ] " {9fPZ3d-iRY?)u%>(X8vGהcYcH/XNmhYjxɔ HK;9lJa:Un!+rhG?5ߣ=jvr yaYoRxb9)WOΨٱ9}IˆS}߄q{h2ɻcYdLG n&uإ[jt0 kSv?N^6QLneBʼn.: :c "3'e,.')y*3 O05:c  be.Ƙ%Ca344QEnF8#!y4!a@sڱx8_zj/Kz"x%A1`ٳgI]*w"%GSpzhܑ+Ȋ{{ǚ/-w݅(Y{L'T\p'D;Huˮh|#aA].N;vʿUfϓbR*_ٵ~ٟ\g\ԱY;'?x|ϯ $@X\ *n_ T1s5ڶH{0m@gB˚Ub=IݦC`Up/H< 4Bk]u{u>D 9\]οI?Hثcn7o@5ZV5@@8k96^GJD':[i9tk 3w:DJ(N N= W &󓕗կqmrC@aΝ8?nx,q7\2l3tlLht/1Q"3VeɄ-1u-&K=v9 |v6Ni\199߄`@n3S]/oIf9Wm0&a&:ejE@`E.29gFjL7NFdA#sGM!.Xro5ί;'ֲ䬷8Q]ʖ3]}x+zށ==$>N]z3z! d#`d|ưtRE<)\,B&ހߨKyntp} hwyK4>kqѨ?{j{d0y2+)ԭMơkaa_|X_p'=_|+++h5wý=H@66Mնt C: <,U];w4?K!4ȨЍ)6S ]YA>8ҫ @=6 G=6]y@v+\ڹaNNG,4]u<^s鹵I~Gca b"Gě4A,(5:Qf0xrY2 YW<7-;ئ0(=n-kcU`S +B`t1&էF|ݑEz{4Q[-AFL_r^pmz<ڲX0.Q$ u ¶cTG;.GaLѻ ?DPzs`'B5jйzs_ׯ|>rka_ބMTfD7XJ #{cqH&KO 669sw>}Zvާ6?"{b'Գёow_;~n^uB)}əOOZ)GA_EWp~SP a X}vδh_H5B8rf9Y3b(f%4g`Y`˩&:%̼ 7bZ#^ہ-Xppa!#` 1)GVsh"KSGkka\8O>8_^*wgR~c;b3w}3H雄r(%GQ298lwcnC cAͳ&󴛠p1 xH6*ɀq9u(;sa2 iͼNva@\t;UIrdq1+%7K$poT!%:}2iEKޱ`EE7Znn[n^ {"{1\|C/$KV_{<|~AuO $N!<ȯǍ7<~Q{Oh2Y[4:f&L<#ܕDdcŰ0;o L6 Zg&B9[rgo EQsǭ-r1ˠBf>Mp1mJ[H ZHo+ZJ<22rmbP5;C 2@Nj۝xxSk?Nbiο@a}.rגS X˝=KCf3^c4\I̵տ|,t?mkWwirR s; M[c?`S@.J춰":4|Ns9i::!i d̕ sL115U`ZPiea<|8a (eo%:Wց ):K :ns?N4d@JHʀ @ެ#ef%?;t᫄TFG,] *-i㰗]n#elVde=  XӇ'|‘ ["=ssx]3bpr~* qƁWJ~r/c#Kאп݌u15p釧 O?.^LX'ڥ=~\̍_:1W}Kύ~'$GvJ K6yn`ɕDb ;_ , ! vfmka~fh8!{t(f..^ zCa09CEBcF8x`%YR:)\y,ǜ]L*צ/GM,׾#[kqj8vЉR8mDЎx' f\,@Se xs}2 0BO)(~cv$|o>|eFB:w6!tZe3cl&xNLudΖa]0+{r8 *0sGqw'_8 LIf{[MT -浧k"% 9 -dʅXGN 9h1N73+7TwaPjLL`*ʄj1Q 7}_%n|Nerr1?lLfg]]n4I@#(j4p]bЪ땕ꮜc< sڀy6J}Q@cb26 xf0 sxh^ @(rW Ǚa=\4|t5ڠ1T݆,)"'|wwѠ;/^KgFJM9sP[{ IsqJR1qtq2vHFsv9ZdE3&j>bs,q%gAcL|9w42Wjӻh Ck@lcy:j?]<ǝ`)do0z,('-^9N&aax,ǯ_F,S]#fJ0w@%((m}`z=<2N?V<#=*&2l^pƑV=//^5na}Dy8|ݳzآNދq%ne/ uhME)lV/:d,$>WWW2LlLGöLOY(Pi &`LFnǁy$bD"3C8 Hi@IDAT_Ms&L#eD0( ir0-b Nyhf ʹl=s{W#̶w` L-  ցu=XiX7 :ƥZX@?_>/mSo*#2n)va?&=mP㔿m:SC9a-ABJf&t)R@2*qʼߒ#qꌱ/x8_# h}[f_E?91׫Z& ~Lk֩a鐟;:\{O]cSCN I|ay#🰿>CY[7&_\wk:ffshp` b^˺{U>=ltn݄=ƦqTjCHxQlv|4xo?cܠ5moy߁iY ˌvbVl\weX QW2 Nj> 3+k3/&&5J1d]4R Ls4T0W I"' _Ia`ܑhN{Uv?s;>t._?z?Ωk*:ь.2$cp#\%;Zˠ) `f TmB FJZh omCkm7 r=4PwP$ cYly҂!3-#-7{.x t>$\0(vٌ>ÊSaz毀 JBu'6"|6JX9v,RlYTıNx97KGvxyulA0HK.;דj5D,x/kYcg_r!8c [׃iNnrϱϢ+пG`m gLhsOjx=~fSK{qM+0/zG_њ_ib~ke9൫) 4tFTJ 2; ?5JuZb|*`D f[yKx[=ָ^XHMN?vJYR-}q#ڳSK`T-v`dAXH*[r2d[LѿTG ސaʢu=$JF*t㩄k6E@k~ZDds'=(#p(G= Debnk<(_bnԨ Dh`hg0gT-Ku 0La:׸]?h£3JT?ۿ޽=ΥTq>$`팗蒾\'N4F)>710t*mo}`\9^"@:q{2"ޘNsz1ξ)װ_ծ\L 89X<] 攘L=`T!/"1]c(UCP^nn$(e5EdD~lGc9Xr&w@>9X =Ek7Jqaf.ʹy`o80 !hLK0{.vU0BC;> GV + KK؛Ɍ m&k Ⱥah́\նsI5KUz6B^ 3]!3,[@Am'SkؼQhlsxXWd ̯#XCد˃W8;x>͗_#)J#iw؆ DF&ֵ|c!$ rNv%?@MJ2"Q]}t SU»K747ht yщSt"ý#<cu6l _ɰ/#^ Xcy2/C` )h^Vw@ch)Kƀ &Fg#Yd3AdCzg>1`1͉w&wk(‚˴ 2G" @b d6Ġ8]JX%5Cѧj5Yzkҿ=߆ǟ7^8+@a~#Ӱ-L ez05JH2ø $x"LrR~[Xg{^4nh4yE/} 8^v0W'm0]1IGx0^vES5)q\~ )I$Dy $; |x]2u>&Oo~֨t3wڱ~Acg:aQ0<00c0ɴmh}gjʨ< 2xrq\{d=t?e `='i M?U`,kzȎ攷߷KAǯhLTr&,e-.Ç}G”sAC0;~.)a?HmGF,{N|C$: 68Δz\)2D|~4ń.>-2yLx0 #% K) XERl\ /fO- PsBMDn3Z P[<:̕-u\U;L7IuC߄ UJdq;xuIxo/5:=Ͻ)?#\ٽX+t7LYhc> >H2}I&[쁸Y<±^dxw!iTƃ7bW&YmAĀ,Úg2ZTn.w&.3=xgbdV*}0F`K`L`Txu4sn85&>ifnԂ)@g` d81ekp˱L` 'd@e1*PPp8*%DAK h'A<>(GfD 똱UPGwqm,jKS}0fO ğ Bd-\;5Ts:W(l&xƜN`+ }s h@+..=X a|6Xv7a^ʍ@[/`B]xş<[k:@Y~oMY s7am\S.tD9@EGfԓs!" L  4Sn1Õg#ۈQ%jǀ. &sIE6RN2T*L.Lzk?d,A]C+a\':=]_nQs=}P=yxХwS{5IA6 ىa=aq#TJ3 k$?`@7}_Ol\iwbȀ:hwa`/+XĹPXBMf#+ck̑=׷sðB>oi=E?VH\, vfk0mEyQg~MRuaa]ƫ&ct[%ã9@.nXWP@a6:fqG`LVې\-sɖAuhO V0kQکkd(d[ FI[#{}< *wY=c#Xo01^k12iL=ב092@1Gsz2%_$&7 %TFG\ S(vTsTOzle ӗ?ٺv!{k\…֡*sRF6P`ͶcJ?4'zLgAkxzkko~ۿ?~}֍D4̥bT ^w)2^0n\R7g`#)ˬ82~)jlO`% pkh&Tao#@B! -i S$}6Gaky*΁ᵔ\kgMyo>b$A8Bi\ )LS1ٷ{02$ZXi<A쨹P3w{%v!{CC>cC}c;Ts}0fMأ ,:cCvIi\m1~)8-s-rqaq \,n9vYh?Er_nyXhr3j>ݻ1ȑϒ/AeX4kv[3MG=}BV+%y|ŭ\L]e_]0s6ɝȇ.'ȞXgpq>+?X"-yB" 97(?) .}08e/I:n|x|Uiȯή^j HA6 1\`q#Kެ =H<"ќ=0Lg!Tʱql9ƦVm4J*&xTw;']ڄ{Xzn ޛO7e@j{=hG_Hp.C2? g Fv Rl S>83hY2&Z矣?P f[^ :ߕ`c 9ʪ]4zt3pG\ۦ[m5/~`'X>?W,q;Fхl6멒%"U*lReй945en{ c7D <8YٰMpĵRg4WYgo8Cp*x}+ B6BB Xv5#uo1{ysR@o%㛏~7z8K/K`y] UY h-vLùg3-^ƪjL\Ķ2f(;ˡILz͙0sL` `۝,_9qW-0ӁWEsi#\c/!@?#E0}- .~zi<z E׾A 6~rG9ܸ\nTX3.w<͟?3x憜#8pm|ѿ{SVѨm\Gqg&)D;:/ʌ"i&\ ! 4{'WUIbLJq…A:WB>TdgNT1P"lVq ڬB`st#flizYs>*ʸXat8,&7KnnjG %S0]+"/A\Ub\2֨M(]Hh,>jF:z#Bin쏲iu ~oWrz#UO_k_$ii'13 #G3Μ !)->A6펭=K/KH7{h71 T]ϝT5ڥ}m6axӏQI3qL8UCIP9,KUӴb&iE`?U_{f;-25NQ0Z31y`_V~;`tKy7CdfgǞ_^vu[D6l'V+2[-|m #TG',0}wlȸQ\vYF`ш*(M5OdF(`hI] C2YQ [1Yhݶ "[28Z M^ Vа#0y aáBڪ3rhs@[D1-/I >omQJPq'ӐY=a7 u #w:?coJiߓ ?\1neqN ,Jdf{.3:ֿ/$[͵v`;v|B qXn7SIV_dvyz+\d=`q{H*&u19P`V1vL#lub"h2] e],9q-22pd[dx8d]#>xv\xz,0hԛ n,l6>#2c|M}8ʗ. `Ail5YD*[qȺx{ÞQŌCCBxB40a7nG&n?inҟП@}uuPP;ntr2\v`MIB$RIlMx3!b0lP`e~f#1Q4aΙF)i&5F{[en9h6[AmSQ`UH=T^oԑt`;V v}}2`!SrFn;.K{b_?_ Ԙ&G19R`Sh2cڣLZ9Y8F3G/^iQFz R21kH@)S >HZv[ 2-y8'0;000٦xm0ky3n[ ^gxm:֌%:K(]aiyI>`a2uΩ1(9r|J&踻f3ŵ.qVJa YZGb5Ʋ;r9VI XH c5P~nE捗^3uuw~ QFZ)S0epysxDT̸&4.\64,6]c77䩐;zZquJ~b6Ȫi|uv4 $#j@&2~VE3٨ވ0 `x>cjd5T³WvYYX +-a|RqD^MD, N0(=dXJ6ӑe&|&w![w|1k4Kuaa96(~T^{_ɇ+3|]\ZQX{&aZZmՀ ;bWrDJ8 Bܯ6MWb!`x-@^+3h!ZS 0}G=]FS1Y}Y\7k #`=2[`cµH -R= 9:z|>0Ye">pYH7`1aAR]S\xh?D?#ݕ![?fh#l)i &C,7bYĜל*$X<.YC'jogP/`nR8H6tD)98lp{+n֨W/U+_ZZ.altYy:~|BFڧ6)/??r| krK$htl?96nwnf~Zt;\@ 55wn% mAOr/RMU a =yH}Γ~v .x_<e=B2Y5>Aq2J4GntUԍyjϓN\&`Lfs@c- mul+ MIg 40q'Ǽ0}Xn(kp~sJ΁4aĄ'Km\"o~1HwʵP:r讞4mdbfiRDeM3n=a9sS~F`'QGs׍jF RdVH`=pJJOpaVBiF0MK kA1 j&q,nntz\ h'f ǂ^6\B9~M,vs5]Z#iJϸNEWȘ {qPm.N`oa!H:ڵ_!ƢtS(in!78&m Ӏ45U-i eU<*:pɐ"S!ѿ׼biG :qzJ 褏{y@]KWUJoZ,"TLps&!_Kd"fc1H#D],4 P/5(3ٷLt9Q͞/ ݭhi=^qjؤ5dLτ1+40A40_b`ԑCXEtOFcO;&S@d?5veyc2ROA {h7(2NANOӘO/>\E'O2O ܂OHЙ8&0~ ['hF}B_xQ&J;FYhѰBcP$n1 E_r 1)1ւ6 `ޚ(S8)p9*'nWQa{\Y`"ujzy^7`As0jdYi, =3rtd9Mo 7\m[q%gHDɌ#9x8L8(;>d~Vt_tvdUdnpI;29G"MrGbiz>OG+x~jε/v[M7e( p"zah&bj1 (%6qMInIXL:]#Ⲗt-lṋ;1GL߷u|sνc?xc]K=AɓkHc>נҸy8|~i@GϾ'disV+Gs ʨ-3dWcrJg"*,8t9 pkdmY)FBǘ1xC,iqr3dRx4 (.sV\'sa*Y0v cj>'kP3oF9[3遲aAd0t5V7V *50.8]w%0BwIp={tY KfGb%;zM'?7AbB-I-i5܁{E5Kzᵯ-\} sebNS2X 6̑aus,KhWq0ﲀP@jqk:cE[`}7H]h/gh](ʍ *b 74.Xn\  (e# fMZ.%G3L>Ȣ=CEe@@lYlLM ćdވ9df9}93k͋y`{ݬUM%Dlڏ2j3N"h};HA'nt, b6 ߡMrhiis$cfq$q4=<422X878Dt3̷6 D5 >eH)nyԔG`B^s 4cFp-c!8ɖ32+21H*y*!eSC}Cl聠VWq7ʝH 1ˌ161@l9XHGuZA0|Nhh*^XEו[#c^rRŴO1e0^ICKZc[-w>1H06L搶5(b0u,bsCVN Tpb~-3 B3%ZO] h&x)*HY>Ѡ'T)kӼ*İ_-nYIY=OPf|cw!Sy2žK&{ې#E6C{HsM<3| lLR 8:Ӷyq&&%`h2m-#k~zz01-˰9- xecs"F, &A`5a3m0EGty3cYO w/˫ :*curjD%t2A{ 2:Mr} k&%@ li 9xԸm.10~j dJ袡L3^MDe g2;[Ch޸XRG㘇Z7Khѹ!Q \-m+8+7>brfN~emXnaWf`8{1s }JoF4f1V}PFm#ԑEXw5ok!O(1 `m~*/}Ysqt^ֱVK7TZeIgyCW$Uָ=@):7Kz4d81qgQxoHX =P>+,}553PEOx?w14Ase&۷X26Md!ƙW (uAg~!+DșT3s9"sbz&،̘s%nК10z%4]=hE颱w{%D6 hk ni-8eiy2:aKG0A@&<@f5XRqj2sZ=s"!6'W$o#/ 3\|m^,ƃ}Kbi&/-mZ&Œ(dށy(bOXkVpΒ fy"~hnD,S$Pp,Q sc) / ֣120a:*Cce2|ӫ-OO<0GwڀfF`E@T%aLo0ݒ`Xd.80%!?XO0w,SK:_^z /jb" |dhlRHqg~ѿucAM5-RdĤ//ǰ@)[ڲM{[A\9B)WB[b:D)U sJsm ^+T;T | [E7dBx/C@f< mݛ,he/V.b16Vo/DX EjrP6cĝmsumQG$Cbg H5`a x[֊rfI`("P;gwP Fi%Kr@560X360ta!'hʄLףt#4P6dpmT 9 ܓ$jJ8 `7XS˗'8]B\;j/!Erh;Z,2o_fZ>K41lIOR8k#)zUΨzľ#a4guxOd%+LBNz bf9̤7OwQ?ݥ9Aoxx !֙XX>)0#.ಞUm~2^LpoitSzOi/]ˑ m K#j [{&U ZYkP.ݟae/j=kq &v$>xC f2\`RA2{]/̺{og a/ЂC)zCr.@IDAT0LɈ=x&oG5~TBH^cE{[$,XɄv8M ݸ.%x񑆏ZYAC33bWj!@pjE R$g1*5Vˢeau!,wFu+R> `0X<;@pxjU+t :o"zUpJPSql[eϡ8'JcfMJ+pwԼ2t'>woy2gЬ0d 5?}nd윐{XRo[f!']Uŀ._dX,_O dWlj>_G!r9J^FG?vPpnNִMg `ەdMf+@2 u[*=*gaVR,XBj^Xxxs\c&F1#qX 0L bp v =wH\-c_{MpHr1%,yC|\#@9:'fls ˴[x6 /ߠK%b-B @9l7<ǘ0{+'`! {QG!ϲ ?uNpsm臾yLfu ٵLƍMxus_"rLw2Zr9浼ZF0 W۹k6I7"JEh`(K);.uWI& 6iB;șu^6^)v)t^'LdO(L u/@ \LrTYɵEzpT</Z!G@Y!;*hjI4\{bM$ HИU9Ŷ3F&  fZˤ|J@4tB蟰>~cc/~>HEx+`[}BtIpWԮ>@KQo ^@v6GVU~Bbb '|7Qw8s,bv;U]7V8yzB^:kgw??y[>>>73+/~^cəGSMeBX}BL`Ģ>j`V $Xnp!k <(ߺza"84e`3/HyD  dbLi j&ktSe~֨СUpHrG }͞9GDp<27~jJB~z}"e}&[i6@8 M)8XXZ2AoZra)?A etve P'V!,3 d& t]k]AO-6k0euќH/]~#25ƨV-B] ;I'F+Kj%p B*iQq9d-7fk B0+88kG\Խ/h[p z-9EPjΝwڅi=]:;x`q1?Ane~O9o3 X9W>gҜ! < Fw"9֛o4煃(fgj\cZd ۃiж}CVzgG"G0Qz! FƢNLD0NZkg;S@$YM-=P% (4z.VhÚhu2zU1.*8etϱ&t~ӝ}j\:sbPLuc[kHM#@ug뽈k\U,}jYm2. `͹ue3}v_-Q.{MhmmU?cd3 ZjKA)y\>!V >W{UsR395vńtNB7gI52mn ݧy g-iil=* *gP܉ x O2'f#Bmb~lI&\i7@vO|U$Kp~6fk_O_k64_az~>Jg z}h!OAXp)lBX%Fm%#kP72,~x;L@?78m,. Vj\b00 ӱ\̊9?_3SG!͓pb#Mi'lh=C RÓmqGQoV`wG\[c  O˺ `\:C)sLAH ʍ{$WWXזnƣ{?d˃Q3X֫jn^ F8dWZ?3G[-:}Bny"h~X{-Bv;2`Gau3JGy-E({L5}ٻVqk ,Hb(0UMYUpSA@j3 (0eD69V cB:x\{h1̽wgjƩ`|C/؋FI8g`vJOF W51<<[cH#|8Aך9hM?Aj=Ke9'5@^D<7LCL=嬛s=3Ђ* glj_7],f@?rTaKWX-#do ܿyϋP9!@E&t=OY_ ܅?9cvKKC~rC!|"b &ƈv$چ>ǚXƎOR*6$CgB Հ)X[ )hqڈ7nA܌Hf?8,k"+tk%9m#e1F9>Z>o R 6XN42dxq eZ3 :^b1dž!H{ kchiTCF!)0_+hnhBO#.0+CJ1{vYLJt$qin6#Ŵ饇NЛ`_k&;f)!J`V:~6Vk3Ť `.pmP`a1v-S7y9gJ/-Ρsv^a Q KiICw:a7 P(TG/*c*Vp{kkwޑze3jQ殕<`BWzJ< (A{S"xKd}{y,em!Pߣ[kb. ,T{;vh0ŒZh%9{kֈ <p5hЃ<9Jbhw誐X;=8T4a=m:gF{K" N`QI=^ 3_f0rp+|OɳIQ9:X)Is#rbRwOTvF+tS-q<{ ֗QYE ޖ&g@ȧ @#_04F i>I:<@'Hl dbiu%ˢxv#ږO3oM:Osm{'G@)6| q ˲˨ -odkHr;; LA,?Ũ2G8% ڜdo:w9/֡}޹L^%Ǿhf s@޸0^ ~}'}P)y0Iz^{)*)Cz$E?r9+VxK$*mjH~0wd4eXPAQfBڏ6UKYv9aE>@P22y RP.c X G ЃԲ3+re4uO8h*< MC̲uYܻ2R:HdWFOXoSm#{&V0t`jWfG_|']E(ckIp Z)@{HU` #n:8 F;_w00(V`.؀`dTf;p@LR'S0@b<1cnTEb蝅y֭X=.7j) CŸZK$` P8Fhgg+j{ֈ/U٬ūN .Bbf]#U kcU?{Hky>l JŒ#B4P_ .Y2ip}jN\עm&2z*|d(c KWmO NI$q6UP1̣ιrZ!PߡeʫA`FZ!- Sjwނ@Ky!h/ܷdܶ񽀍yEk $G>hohe1̃^ k9XY0]DQp|S`t5ܛ- bfXk(u^s)sƱ.f徬@f-1~ڄF`~z\3c%5 J߼ԫp6K`b:_r~M6_Jc0-*DѦQ9j4\-ng!}ۇlfTհ$!ur^'1DZ֎$0+pp`ClMg˨ l+WZ1Jp3{q:?-yJfİ1ڊQuL Uzhq^ұh7Tz[Rs%qT~ 49%:Du|IBz?C5{VS$43c奲]Q4: ;>ZqE>^zV9=eSta?ܼK rs91d6nq}d uχ_^Gx"g/{# w l9SZMf<Ǟy·rC['YZЕ!\dgH(jS sCb!TLk4ֆ?6=`ZgWnV~c '.@<`܂ 7GÄ>H`eM#FJ!LHA+I%;q`Ud c؟F+ǕAX'wi=[x _yᅗ)Kdק_xoŷfz>xuNbkfR;WΚI{us#K5S0ILR@"`,܂8j֨4O ]S,W\ ng4D^04u H<fyTt@edfr9Ԯ=#^֒q"'-~ZgnSexvvcv  "1F&TtfX@~,-͆D_{fXEqs<C X[tj0?CY) 3#Y/A ߪ L2p7ރSր' *YVȢ'YڵC\Jlv*c$,,G#Ƀ M;SƒH#9N  gxnF\uJMrj!ώy& C"Ophc%& yy^$)&6ץkfk[2M+c !7n0:."KUy>n~#esJ0T"~n.-,@- +d#nAỺE`e,ӈ {*Gӎgi炙~{(F0tb$/iAnM6n:ʮ1;+ݚU`휵.N'~0[:3#ZwQ8V+a9랠yUj|0g^=Ϩ29Ro <ϩ0 PҒ' +J(-QsjEAQ 53A/U=:)`uz Y3Sbeq7x5ݹ{JgB /"0Gq,_+1bzits3Ex]2U%7x=OtLZT̙H{AϞe5JXe5B8gB.qOs&lzۃWS&zFAzP!KNI˺@)g52~7Ґ ]mC7@abUm3zv3 +q짻3PĒz VQi^|}}swk[tf*Y;gْ0-huWa搄,CIkz?788^n ȗcyZ?!eu={NסCh7x߄A{8j3rt`.ʖ2uo9x!zu>ׄv!,JNܠ!W@dF_ukgucZ~O@z7+ {Zpy7hkÚ%J+*Qkf@5̜ۀ LPdʥoRJ2  J2#$㑳/a+2DOU %3]yL3pf1 Fe>jf/k.P|{"O5 8ƚc9-bi @%'6|kZN{b\O h7aEgʂp/SSc~mْLkaw=NPy/TL8'-*cЍ=^9aD=[a.~#$m^'jB[&7!TY-o C*,ȕaE0>1a٘Рi˖Bd5 +ލ39!,#ӞPi#f&0!+w:1e,Zc ( 0иF 'u >3ZEC԰B 0p3ՍXU_ n=kE 2T.`pZv:(nVЊä(x[}ء{! m^,Zz f}V]2y ⵢrҤtC0@Xupq ,Єw-XAH[$~5yW&in{Pv|~HY']BIeLΙ-9?~-KX8][|Fl*bQ)4MPXR,H}609iA6jytA'iti:%B8ާj/ƟW6f9GTiΔ" $R1H@*QX W| o8B.3"E}5P(qbS@4vP31=Y^]xybjJ~rLqNBK<%F_!ֹn'GmF" sU]]HO\1Ȭ,,4T^1Dh59jyc";3B9X"S᳗Ж} `|->ò@ MN׆]0 D$M &z'9.pm  :z_f ܢ$ɹ@X9W y0*@s3~D? 3θԯǓB_$&Ȗ0<ɻ&eizˇi-O *is3%fHs'XezVml|+WT$ L+}Ϥo ҅ot!>3XsXJ"'B[6}4G_-ݙ1C!BX-}JH˫Af)J6U]&,rBiHaBf@Quf*!QGP< e, "ؑaCqZCo^ȍo<=Q)F=jd^<$˖dx|q9x~nm}7%܃΀?&O3 c⧄"8Pj/b!bi.fHNbOdS:D*d)9 :ީw/G0C6m7PJd]79jJb3ɑő`#1ݩe>nLof=T+f?GWrڵ'?P>|?/JՓU2dj?B=?ϐ}Yچgy$2OU:eh"ݣԺGQE>S U\UЌ-ϱ9<zԸ$W (s>xZ$dʶ#4}CI$ov_Fuy<̻C) @y#ۇZ =Z?ʕ06mySur'`p̓ݦ0> _Cܬ1 ˄'(9I Oz)l[q| `/]o k4ǵ0j xcBp'[Z1 t`ygh;jjg Bƍai$YQB9sC7o>M%jZyRS)- Y2[G4PC+bך9jLT[f5V CZ.l,DZ:vh@IDATgMJFP P;C4ȱZ˔8 5@({!4K-TOz8C@kiKÊ5bu~e)KT [x-t51Xϊ5e=[2A -?* zW|{P^@]1^.0.g-pmlJ8kـZC,*_ "cmn~f 0cf%f0<5q_uoHq AtXY#8cCcϫ'6(H[& -M\,v8SP*<]' z4=" ߺĥ/e67xq ~×-3> #z#@d32(m@y$LKEnڎXN 9HCyFi䨹tv+Mʸڃ.Hrax,[ xI0¬EI0E_vMZ u;Ѽu+ &MV$F!pв1i"3wo41* 񥟽9[iʕr6y 8M/ Gbʕg3[[i'>-?#1 CBh524#[a,R$QebcOX\QX;̪79K"DP-ASu2:%0R=Jk 2CWPnkНH!ЙB?+ BaE'pҍG!6dޗu@kuțOV#&!BgCm(e+1i&;A2ubBTzslJrJZFkieA!7<n\M//b 0l#qAuM5[ pD&^^ ` 8wzkaS.I6|p5>hWa3V1[%Q띟i'F-{CCbz.Vm H'`>ބHsh\F?ejb?sVRK}qLI뀥&DppT̩ Mr`*x& c]7jtD-)FGScy,7-u]{}æs0`\'qv6myBT@D2cjm8d*c1g,V׉$hlkhcQ& ۧA*Z;z-S~JD84ܓOɄn qss 9%p)~DɔZy9cP'eX3MĿrerv\7n[Th=H2$Csg|Xse(G6n2ksߛ ?q_BtW,p! so>[hy,]W[[9e(UlN$qDa2<>J(={Q2>KiJhBɠPoD14j9 P2y@oD>c s4`/0xj=F~sϜO=nHBq 7Ij bN(=A.!40ܠ;~}jsֈ> U&lg8#[xT4ZE8zz(!X:V{s}ew V6χPP⬮s=ranmdJ_-j+yc Y<˸3dp;0= mM:Y7`-@_ܬWl\Ī, W7lm5Yz{'~ͷ[P a:bXd:.#k!y KHut/*X*# pZLtZBgYm4DP2GDF!g0&SD\4ѝkP`6މ:PYK&2V#cc&彵ucC"n@դ眢8Rɰ$'3 -Qj[FmigZng,%ʝmom~* е],ՆhX: Legh1iPKX(]r}!W3pm%Z[bЄ&){X `m̍:ЪXKϧE1?jC#4vch-im4SK%O+/5BID!U,򄦍_ h#j>[gmlٙZ|* YPI63@FB{%c$J'jB8cc9P3:)qG'x;h%P%t{jBubU$sъwAϡh{E9dߜS׶#6g,aɓQ z?r;~Ǯ}D,<`d~ɉ%](z.`*G eT9[.d޽,^ϼs?2'Q'2*E3Zʳ [zUcTP':a?;I#I*>CM w*o;05P{h! V۷F< ͕mo߶񪚜rru!|ceo ~?x9}s(T)3Ɣc=NL{-tx|ϙa( *4O5‚ g',T%١{fc]Z ̓gtU 1&pҸtv1\pOBDPU~.fG6kq@7e(}3 x7 mo;pw`G؄n$V06Iɸ.C+\PX#p|ätTP`x\pID''bIQ$tW-xCe v !t7e,MuDV5efr/>]!-[[kS-Oɼ”zνg>))%(l%Avf)mjE_L+^0pAH5pF+>:#NZtCp:&5Qmȹn-:uU[teJ!iu9iHKhUa(׃]b4a"F!/1Hb<4)~^ěmq1LiA̼^hmq_ #`1ʋ-4{ M,mO]^JvG@NT7p+ m* CukSW| @y=$,J1 ;1Ip=h`($Z'Yʯ"dtHfqns*]~l0.qeH$ kMHA4fwXܞϹTI(T#,`KM gZU\ %6*E"C2j=&]rTxlr|*P M^Zfņ<ls׷\.:х)9&׳}jKY룕Y+ 32"* hN2!qlp];O92*gPu},EWEhy&}euHE(մ8`nxgYh]ae𘽪!.;.C4V8U9ß ;eYrg Yy10M_9/H# ~8s_O,bx"'.*(콰,fM1{9 S0R"Q<` u *F < ͹ 2a㝛|ҟxf#SؽT}o}b5bZgW0eeCzl`@9BK fxxAy7Rq"tM5'=:z 031Z\kb>0by9*` (V0kqr!$ƛ q~!5hpNǜ#Rٵ5Wq5>dD?[WH4˹=_.- .{z G㑫| "%G+\wqRv50ЛgZ|;cYTͱYEZF ʻ}7p ..3oqgȃX6M <{@sZ?On]b4MP*>\(Z㙯Sz֙Ǜf?kβL_SYDe{im-*2yVkj7" d:}nY2k2$QbƵc]hfX~PDfc_6W!ozB[ݻ X3(.c&0&~esT͑ݽWg-y 8$|7 YCeѢTgyL7yCV/8/6~1Z0(".С=wuyA}|l;[Z1 cUB(s۩ܻnr"j0 '߇=x; >P'D WGG?M.$?ϳ*$@޴PcP0O5ogDmzc'*p) =;fXж-~Mʹᆡiis::\zl5ɐ0 XTR^'GZ]4nf>իlnf?ܽi@ٹw< E;w2@38TfɪH.n=S5 к$q!"6Sϝ_OкXR%ƁB0-Ng 'Ȑl!SX.]\t#,'6Aؒ[b|C (ʹn?s_OZMs%׍3^__zNgr#v& c +M \ pbgQ(.T S| V v-ec:e M hXG l -`%M sui♺MBiQbkZY!́)rB`a 6xYɇ/hY7;!ḻfGXbψI֊-c֪.s|*0w?y_CO$ ڸxi_#P4jM1c6Xjd 0-Qg-O7,ZtGUf;G [cEH' G!F ]RҒfS.)_(gWxclh3v:!`$@$Cx 'xH5æ)#sBԥU-qބy JH# Z`~_l\U~#$1"[]eR9v-s.)#qېFܾ#ƺ6W\hN_+mP9I+>oki+s 1iwQhFAhiA )ܕr;Q[ \A[G{d8<7ͥaf'g+nt |ͬ/ \Kv\\} ŊGTW+`>$F|EY8 =Jhr#dqxobAg66"KX''4E\a|f#5L6Y[?Zc=:ɗ6J\)V(UsoNn;__@L|^oo-vn;ot9IGi'$펂"@ ɀ2|7ߝ~AjU `- %A]H\Hҥ*S{:R$BǯH^AlsK*K{)*gFޛw[~zw|֏Scp@(4ildfȮ`D IJ;l5ዘcH Vpẋ B=@#OdM1S?%Pw$TdT|g1~8G&J<@)p^PN\\0!s@n dq& A6]mž]L{LxqFj536)ljCzߐ+[V(0H4ˆ4›tٺOЎW]kX ȶ,`E;sJW"< "J]_󌎗Yyp.(P01Ykblr 3luTyoE oOUJ 0Tj^3, ~yƗKzR`>$pu<e[o$~};Νݛ^ƿ0FߙͳpP (( cU"idek0;(֙rDLuZK8ʗ)XVdP=ݚ+Ċ*utnVqKoNRY ¿|cY6ndA*X < <|}* F] |oϣt6i- 5|k((\CFmk Pa+]!$ VWE.>c^ΥA/(8c/Mj  \=xu=^L̥b3t[< &ʀ=pIBjԃd#6cΗhaBONeϞbNm?sƝ/4*޶knx t[xmyX"6^*ÔcX |@vf "gS9d1F}ŀ&Fv d>Zx4`xoް0ZFm\^'b# $XFZry 8]UڨZ0 pKۇ} PIŒl~K>+ uhc #ªyZ yq}H5O{pum]c[~Q9}-^QLοkqXx,cEMMOV-L\>AkM$=*??Ix݄3qih#%9܆y>4=ʀuX4k=uiJR\GMddϘ9KˮG4f!T܃YV3*0O7_:ʟ  0 {ϊy j@-KwƍiyatrPsͧٽ(RNrT9+˞pruykHzv=!! *aED qHai&o9,w5x!`ټ;{̀x j'_H&afOHI^4ݜ0aA3$PQ,_ ]Gt'Oq &ISd*)FtשeǤmՋ'7Prд=[{(lT{Ol"3Oք@a7r`{mB)s{ݹnH[}o ,[(m0d !`@<'"ZKvğsf(e;YAVFɜKCg[K4 %'RЭS (3 ' qp9뤻2+ښnGCYOdx(9%wl2:\m\(¥ғ-RY.ѸP<2[A2&s.,2=be YM3:!wPxQִE˛{HMRq=UjbYG\e| 5.hiCbXWY% ^ƺ`[7/UuNSp$+<<̯iXwlނ2Ho\uzY%xRi^ 2WJ|0_735Oaa "ӭэIg sS )۝֋e-X+;ùfdV a/bs{6*g>ς5J?dղZzpW ,lQrF:">oOL)ĥncg1>׷ji F^;&1t!EJ lř)%_ 6w 4ȵWI۬!Lps#FO8@d?gS I9F^,j9EXc믏Ib;k;t DXUy;qTS]@yYW v0M@g:;A39tH,F)0{VLRFl,J"15R`PAA.3KrFz<]S7yyg5SY"cr\:}kf0,XL]2mh g Y"2t6羈ͱ'Z0Zcۚ1hn$-)3*7/#$KT u$0<0㞢U\+ASY?rj3F@6a$,y&b,?Q;`} $1Nѷo ZuH83.9,#%k$Bi栏br}As8j[MHR.[ky2˺2A_ݤƪ&dx9_ɭ_f܏0WU `& ש s(Vڏa Ʒp:2A=5{ FqH'ZDm r Rq{?Y#a*Wv5 2* vUb|Č[+p&. ̓Lzqo+h[k #<ѻ|0*s I^TFAn2zølM-dgWAR) ])jFhwT-B&p! =Vc8~6Bԕ} ^cߐW)beC%!#<`yV!:xdKCʫQ & \ތi4?2Д!kRDNɈ9w~ #ÄԷ1:6Z`liک%/_]M^4 Z~Zd^8i&, tJv/m A<UҰLنi8cX(3 "lcl͈W'F*`)]qNyU3ZAf7giAIYHA(#P'@fa18 ôVHoY-ۘ@" `6$Du @bVe([% e{8\1?'p ,u#~S[ÿ/xt2Ʈ{)U@9H]46W9A p ,egKX8XP#Ux3Ppd#i= wn 6n,3=nҝG(wC4,5O'KɭWw_ N>y'nfDn`jPJ D>3b~d4%51#{^_4mL=.( 64L+,G"+f|}%n h5lr5=3e6?” -Da@N5$@@ f19abRb;KmXsv~Qӆ˯.4ӀqmDUmavEiKyGT ܢV 5_#ci* T`@O/[ĭWcղ`z5J{Ξ R6q 2&ə39BB )!OG/ |oz۔AD[vLO`hmd{-Y'B([M;488kA !D'.]F-VD (/WVy;q9", eO%B /tA!Y.jR =L8Nz6ɋ: hsn̛Ƃc2t\yc=ۑ&9vޙ+12ןkECj`5'd J'Q(jNY܉FG%ȉD'ﳏ]*j.YS3Cq <)ai| E`^呕uW=p/eRɜ}|Z1dP`|`,F'4hK\)( mE6,>ϛVٯiq54xx~|g ^c..aD;LȥN % fC&ww˜dim^"t?p D, G9 |{~u+k$!ag H|pk< PVu=#. AEISl GY&M`kR0dHD!ruݿV1IUXkn4 aL+<-wco5g`<1m31am1>p'ȣ! hD)je y1̓EX盽 yv'Fߠ/Ғ2ðfO`ܛo2@N/s.c,μb/^c2R Q.p~ Ыx\[ 5BsK%99$Ϋ43t `_ŋa3)#dgnZ #BFwK6_(}rأT=5956t8#PefgqUCpB 6:FK+=$,c@Sf 'w?2 (t! Y RAaasg3dZ%W.7n&_ƥ'%!(aݓ!^GI . 71CܚvS)hiRUΏ!YXLbdM6<[Ԇl# Ě)d-M:l%À"SyrOBo,)/  ѡLq&'mfld$*q[4/֎/S!TL#f/[ýdo\ #i>Sq^ 'nu/XZIE  ] ^`}L[2b:BI\y$422iV|Еkܨ^vo t}&3C&6T-hZ܌{g}Y`iȀ!, ):[vC4:,eR[ԇ0>?NX+̳\1벁(pqL>L:BW.qx7DܛոQhrB.q–J>%Z$΋Lſ-R>=k ǰ>?ӝ}?+M4|\"Micoiĕ|0g 0w$<8$q_iW A>fM]e>M4Ve)gLY4,H *5FFf.>< 0obōBRMAt[#4fJŊ0b41%a_]A(o̚ВT]UfՌo\cMu*eM4e?cK(Kf&*,p-+r d!-ȯoZGAL}oqm> µR#.JUr5Ez9.JŽ5#OPV簂cq*CQR#D q>JS:XeQ峾q#r@x{?zTO)æ?hD}A=S_@:%EgcL% ktL#$|xvJ ǯy ?WW1ny,|c B )1ӈB3'ּĈOˎlxѣZx7He8(FU=@vx (.YpIvѸ#>%̵ !E|srTL.FaC֠ޠBDMb1ksD%bc>`լ+=b昫"d uKxyd@Nf|XI!{868, N\WA{'P\Ux\@;7!zC dsFo6Sֵ5hErZ(eKU .;5CJ(0L1Ҷaba MAINQ{\=1gLx(Ydc;˒HwDy1JBl`cY pcd6,X8x( ?0`= w%}Om#BKX?ҳY l2U1p#e%PH}c$S.R ~deReW^VrulZX<V^( |I@jt. ,.@^iHj7]9TYIfTA:/]1n.$8retek ij U2S@ACr,2c.n ÍŜs}ݺ&;0Iuf2!fV0zNع% y #`:m&J&wAiH ʒ:tV-Hd90 2.>!>@Pg;$)C8>aGE~MJ>7xZhDp`ϓ'[q\Fig} }c-ek7zhtSmInx[ȱ&:rrħwۊs_]54ָaUCD{sA911hƜk qKRsiו\`XkT)̫y6?EiHMi,<.̯YVZ[fbM[d`}̜d̑&깧 j!@kծC7&9|)E^zrgTFc4Lka[&zG cH@,@z&+ƾٌg̠%8L$<̽2^j-B'sqxwkVN-ּ%fukC8c` ؗCNG:O 7K\5q--Azқ_rW;y@y0az>{k bam*ZCfH΋*@ BR;>zA/ |t!L\a/7]\ 2F05(b;5Ubփ3E4Ta1Ҏ"w29etSՃ:bPy2ScP6c vOɓƀxLY,!i+0nRn4P3~w\02LXmqh锼L@ Bxϯ1;)Pm:Y~Јcqx1 OQ2G4_9Wu%נ̙ ^.!N\>dYepyb`e\R{5Qei 0Df0c hQ0f0?܈7{pOrnks2LN# :B#OXz+ Ѫ1X4TjxcRեrB[WmCsc9H2pe<{08M6.#]NeOz#Xc.W)<_sYC#δ7gfB 29 k]BϬ0';]mkkhcDy11z@ &FJ:O0Ypn\;C:QPr&H`\*܌> ϣ]jsa]\-? +{cyaw.lFC>/ |?u=>(efgXLBZSؤtt9:NuSb͐WQFԔ NŸ2>BL PJ#3Xs: x8ȑ4sUEqr fJXP]A4-iwx0!,TAN|$CR|[oIv2߽3|' (aBNf84a §Ofˤ@:EvK- y$>Fؙ =o-|F0u"0OR$;Q39 ݓE$Q/[@I UYjK|pkdy>16dT@zphQD&j|@ <_$&p(ѕ c5v\teb|,{5bWYG(pd#MJcxC`QX_r O-n >F2m`. XcEHcU2 c71Q! V-ogYwf-lC` od=-lwr(*0.5{^-`x\Dqȏ@)@iqk !NyNugz׸ &$u:PP !^|W4Ma0yL馨H[bi0$Bxv{zu!L_t`:{=Gc;eMJu*fR{s !9z4(M+}ۀeJ7IcR }i^`H&LC7AT#c],ȃUn "TJGBP⚙AdB61`(Gr3!Rг{mF :'n ν^C2z Sh 1kaַv_W Xc  4)Rh=ܜs!0+cMBn\ ܽ#:*yp2@,1J/HhpZ8<.y+U5>kY< oPnǺN b{{? CX} ?0t!??KG3np a5E, /Nz)y~pirpU-( (IA2΀$ԅo!-k:Ee&'%#h䁀5AESI +k(dg n6s<}9*? _\f\[ebIHW}I+!C4@YB>+N]B%Udԟd'922/*'1&gemgoٲgߤ[ףsw9]nad{X&X52NeeC$h2܂{D&8䜆=m+߲)+FAF?w3Iǽ= u"JBi"gG{p2qX!>D`l}c$)r#%fֿaJ?sHLr^WdX=_QՁZ 7BXeQe!6H&;- ;[qmocxɿu% ZaæVYJ! j@^Hω]Z.Nԥ]P+(U`lL5m2Qb=8pC:GSS&dLHv)!ʠˀbR}"F'g\eN:ez$殰˓j7<@cj) N#Kd¸eNsct(9lAkVCsI4zB aejVfSzm~49lh0ASR!)Q"ιœ %0g ]j2"WtwcomNd gu` )ki踟9-y>\&+i&γ̞3JrHҲ;gg`V# LDvj)Qge7{k2 C¨P.Hap~9dZh2^_꯿@'am*!96{}iL HeUF5ٛƖ,\aA==}ݑt9+GȤ o6s!Ƿ;m{|ٻ ]:cX; o$b2Q KS\ 5 xn]#t@liA.L!&׶>j1nlcr ba/wחd0ߜ߹s/|BSYNش ?eFi<0G i3摦,.C'7`4'Ê赒+txQ '+/dOA*% ZO/HɴOg[o,▟riB6EYو"EfwYlj@7`!t}1S@3X9U$o. C\ fB7#8wR ݾn_RMH|bc &xMʺǘ#vTR2'*P?i{8> ||۸Xc~B}t>c.` Se1@j3O`'\ːdF} nTAhR}֗b0Je4FdC dk|1 .x%xm`c}`C^S^?9d,#eΦ ˼`ŒA> &C1:ˮMpH;f>ANt9,>ϒLwS6t0$DϽ1t4X h\i3\sU+c% )b3 6ecm`[<g;#1 ; NHd16.#c1 ݇wXdXSoc|F>o4”lNc,\Fy(Vrq;\la 6ӌtUQv2a8ʬ1K!Luxl+.  V^l% M`.]mgXǼ[xUQ !sY&F0(mxA3ϲ6%O}cm{{:h8`p䙹~^[𬍓ϒwzlRy{0A-MyQe] j&M\֘1DCΝ z &mqo}(La7(]: e -cOt,Iт*+z:€QFOS8Պ;t#MzBU@̋#5+_J#ϸfn% 2KF&Cp rD뾍~$7młߟw|^۷ϸ/痯$?{ߧn&]c*(!% E0ous$oe (aO Vp,] Xk(̎|s|cSdJmYe<}Eh%P%X1N&Cyہ}~۷۩'74mN+Zk| {߸#[x%e0Tƺ0|hsKtamrUĸ EQU C'"faaE#F{ 0 kt"7YDzug\bFp3f1'EJ(;wFқ]E$ܸ60Eơq0P1J1>JS4R*003ג+ඎuWĢ] -?k]RJcDFڡEu?>U۬E!q`Y(|=݀s .M+uw bh̊k5 1{T;U@1]j;G7?/3)`!Lkj6IŞa`c cc0X߻9N6_^HlsÍkXrER7I:G#*2=ǿs 't0FyɁl"{J*I):@~SpU[Qc`' 4s@!*3 㞂?s+횥xFK6Bhq.,{Yz_G#ʀI>NPSkcL/@1ru*"Ohk 5fdMf_ 籖 " h5}\l/@riX!i0\ߤd=~5\{hrV%cW܇C6QUC_{&D q5;ýE9t2@l56'99tf#OVkN&)"$ze ZY|0Ը*N};OMrR4.a+SB1lTeK7J"yA|N$8k@cwsL0^wd[C %/u__5¿nZܺuF7i7А0 p##RCY929Pf%?cey ,NI|.|@=!%7ʬ7*, 5(m2dy eeeK_\פK3:4^t?:^LwV^w;A_yv :vw xFV9|O @躵*$lmCT2$\+y_&dwkk^ǥ^랗Sp1A!6-O`$0boSr[-Ʈ & Y@K?K,7j( >kh"fؿS-ku,LE-Oa./䍩E3`N&vubA: L2ˊ)P4vY[y?pۅo-h` A1IQbqXr n6Ze,ʂ7I<dXA_Y#3`Vq.g>sgÜ!duM;^"]ƴF.,F>OaƀK1w 0Z-֚ 5Mb+nU4Dġu0L&8 JW:4t]OlNB:>@^Yt3} 7l Hy+7pVqVNK(G]%gyx5gsCEf\k@ ڞp ]`wfE+lMq{pf bAΟ9Ӕ+Փ5FTgW4XW[O2pMŬ^ m_vn M2f61 XysD9 ]@iwyq3\/J1P,4Tz2$ ZĘoj)5@ eLq>cz9>aPSdLUK.?LK8ۥt>^+ZFx ZcN_=׭ hGOc.mncA9e#TX>4pTIxH)}%q><+ &[flЄŨwM щ^I8.yw?5AbAʶE@[ݗ@wAD sX˗M14N-=ʼn"CX -# BZN1VD֢FDf}`݂\p !ϝۯ3E_k@ ;P{SVv02}2=̽-*K X/#@Я`XQ̸F;W&4̠ĚMCy$**׺L7ʄkTL^D0YMgOE?n-#S F8Yܙ^W7B!l@%` 0)ղ`R q $K6ERX_(, j;m'#4E\;$ޔ,AH L3&hlc_xkJWUډ{Q6Vv]UYdjs`3elT6 CqzraQ֑5!~84F RYV9 ~c1]@g_7 ]-6€R{ Z YQ}CScs g&cSsAMdupи1ria]ko!42"!O)]dhi~p@&%mCÃBaεe'Yn,o~__@n^qZ,~G@"&c=E  0`FRDB!d3t{pAa|XOZjRP ˄"d :'kk]71u2R#eRԄ&@0FΓ@\|wr@9X[p_Yg_6҆HIqP3>`,_;lh;I'8 &>`P)s+ 5Cf1\Vp3BaQ ULtm\˶:8O\ DV<'6Yg/tG &ыaZYKn@U@[Cj;הq:yB&cvjЉ0Z%ƨaa[kj#Iwl=9n<]p^1'['@06Z[40Ox4}0cNY7#z WcR{JYOJzX1ISO'po<\b}mSo9gUZƑ{Dv=<`o1& !'!ʿZs/gÇ nI]' rVz!r5_I.j=<,O$G (=ZW|yM gL6[c0+ʪ+kIcc…D!sMÚ2\O,K6XV$v3 Ee=@IDATt!(q?2½n{kU(jȓMÐ)9yހ+X:}-~^}N=ʯ5>cy=iӈ2DfÜAxBtJ8͘!< t >Ǘ?ϿٖFi+T8(9:lЍrZa<[-y')dEU|K9 !b؃ |"'?AW0F49d_ 8oV;BFy_6u |O >>EOrkAٽ{hd6 +V"6He \Νso:ɧL"O1˸|EexWxR70(+f|uwr'>|%Fsv9fB hk1p@hEAU(,-hnK3 sJ:hy5%YQfhrRWՍTgG"ؤ>LDLt*٘~|y_\n#O8-j6J&XNn/+l#i"1}sRЬ?u .D7x)k.m/3 Jo~g\ݬxeư2bϣWXV-5e^\̋ƀ>&+iCcmTAugco!!TY <@2@xU^F)l,}3DvY `2MULI e |޹ &C6 0ȖHAɏ8#0 :ΟM6*;<}X%̫ c0?kHƀ0Z,a,.:eyb+ bl/__v3gm]`H^N~ċ#JOlrpKQQ6/r")vz6nLW0̫evy !!ek]à܂v@&.S)Ccο{<# S2)4 8]eNnOXC@d?mgW9 .dg-CteD  b!0Gȥ86QAKrM>G+l`mOdtL\VP:2`x«^L vbP#2wn')[*gT(Na캔>3;~{0ŗ ~v2$}5 Q l:仰w @,-@6BX]˹N7 }KA[06(ʑd' ^Q cpaN6q &zv܆MS7Pm6PP\*u)Gl(ٍ~{Ƶ-{/=Tx^7̹2zy⣖[vRrKȚ:w5Tk8c*Le"h[Y5R8Yp-Yï^M_<RBK9_ g" W-!LLPIIqcn  g>Z.Óy~XaЯs3ņw=Ϙ;2 ,Kħw<XBY9ƛ/7 Ŀ &d,1{BrV/c.kx-d4>N.,1ӹdn@&ݲ\$i]H0ZNCM6Cv7B"~ l h:t"k..l!wγ(YvD9vluz%e='iU^Ms1Z[=-gLw|)CZAp' YL13+u@Vȑ mi&etA=  s㥑3SWs%f1e0hA?/5ͤwA ,F9SZ~>ꕃYe0X7Cu%wlդߔ%6Fe@ajm;jXձ AB#k4h.= :MC.=a,ƍr/;2 7g^VpϜ'q 'xkH]nR#;s^BAssnh!*:Th9Mء [ߐmUkV,ˍaM9H 3y;|'o5㟿,3{ao3BАvPRG-HJ$%  VYX?V7%k10=:,-*2)$,`񄺯 .s& YYo4yŤcy8$#(^0oCNA6nR:JSd{lxkv鞡"Q)DVR 0@@@|ŭaGx% ۟9N=`I( rX(e\Cؒ> X4mŀk0D**&=[O= #&)&R@!Y/s*T`Zz `n Rtg*h1bQ}d:5&Q k\]mm hLRM\ ׂmyQ54fɒeI]6n<Иah4^!vJ J$@hS X mc쒍$۲,ɪP%|<JZ"ѹ*UY[w8}x%ZN@\gZ(t|Ic:]T0JfkX\EشͶ#M ݀w> Vt=?;Yo,z6 C3Lxzg:KKPxD#IcsQA"cdvG2x+ы4ӧCiJOhm?coVCʃrÎJc-F@@Z?ߜSjEP#lp \Yg-/r_5@.}seVYkO7vS㫹 c },^IE! ƑF>Z4W$ai`q븷1dI8wL6̳,`00vj$Jסi'^γGf 5ӟ  {75Eۼ//Ҝ`,sOSW+VɺIAgclX]:c>Bi7زk`tVĤIxfay>s?aɭ9I(*5(ǽS|HpA7knq-ׯQ3i/x_H+Qhw>s$jE m|zD&9ٷ! R;LWjA4 )¸0w3)^:j:k;E^zمF"+L`vH7ِlzJ=J$#7Z0d(I0 @ O fEuxn =oK&'?_{!:cjp݋|s83Z<4'n,n$ Ƃ|#TgrE(,x}FecamW] khn6Vs L\ӆvN_g3#_ pNl [Cghك'h+|NXg)j$GGx jܭJ/FDTMχrAg"+pӓ#HbLzNm8 l P+lVυun$KQsx%YZۓe> L൶B<]C}&rgt8=^i"ak7l=|zOk0Nƙ\`"̏K &TϬ h!ɓZ<7Ru+|P@7[LA5c;ՎR :Oz⇎F#@5wd6T 020x~ q%}C*VC/ J++K%1g.p}(h,*hTLɪ1G(km1{Kͳ.ӳgiKR ipI1  c%S5.Hi䌩qLЊPMsy>bQ+ l(._|[ ^Ɓz%A1W'x-U}gJr\ƄZ ʍdF 'U Vp= F4f$>5:9e":Tu\u$5g4x>A+D\og7ߙupɑaܸ뚉shx3auIp%tZ ާOPTDAWid-$[8WXԞw>V2kĤk8wȴPEdbdjuS4&NJT&"1QN& pu,5ƩjcwM2csԘCكF4Uk*VŤK ]mdb{ogVFi3q0ߞ#,,*l§Ow7&ͶSa^=%6ji ŋ7 `_^u \U^ x6p!Xb2GQyyKNH ^3mbat$sim*:׵ʡmGl( z&f$}axVJ iC&)-il RW6%g ! 4j. t; ިbwUg_)x+JWn)J {| [ײ'WA[X`G`5Ȋm ea(Yb{QIie6SÝz2FXEyM^f/R.fWì³nV,Q,xiۇb(iHr6tG/zWr G ThDneHbiDJ\*lCe=7 g%@G0]Rh;9k15#4/}!c%4z1ΤX-C9fǢQl2H的++u%i%$0;KH-6͆/Zve%(D.1KρGS:7^):4 @9}C*L-Ja%lxƸ{#W[xMBs1ß䌌!tSgVpO1ҁ<3# &eC}x%BG[P(%)j0Yj G1d=M1xzy>#6JEc!@F ʕ}"Qja5;-G94,> R&[PxU.J=䄗Xk LRU|Q@ɉ Is B'QۘhDddDEΫ:6r dÌ{vYMq#X.AO/L6MDk&fT bp,t0RI{d6Ti]O5-pÃ=Gȕ4>#YE.sg&QCFY:{Iʢlr6=&1iŞqmu@p5]  Jy^f:]kq ưݬݑ;q"xgo\|OmOL`e7L#H=cp 0VWc W &edag'.5]8rHnƵ#^0Lx^-yvD47*9h}UYv(03[c M=MsIJF sL1I68 w8|b41z[}u, mTm:[ q8V$$_x5L5 = HIǑR43O ]'0u7 eqjFoh?uE։2눲lxIi ")Vm 6qߝ{vߴg0?τG>s<_W1 5D;@O a/<S )uƼГ> ]܇Q0OJF;kǙ4!eXd=9>W_;v<;4xyǕ~~QΆB#9zl`N1Ry`ԣa+kԍC6:Èa2”!@1"ߑ DSuϸRfVk눊%lb2SYWb~[c8BKQRٰ5Sll7|.;K~9yݾ_7>fٞ%lGH\+dY~H|Nـ{"%66O ÏkOĥ]x==~Ӱcb/.MB6 _CNad,ОJڭ r+z'+<7x}-l1]K%01QLJ(o7n$ꪭHXᾚ!ɯw?m|9!Z@TIZ-> ncN4)$~RmxѨpP- IE{O$uNV.LZu.OV9dX]]l=V:Ah[ N7Jruċ:zƣuhnC=h|L5IV`vqHKI}:6ɉRo)!vMkq8]g4b⼇SxX1Gc ڡ ᚻvGVee9[$X,1¤9&dvi+F%P#51ui%T_?Թ%"G̅yV4*S˞зB| |׵W.햛_|LnO}bugu$ 9R8Z1b\Ƨm Shb4$ZVLH bu`< _Qb c~iTz,JIڕx`>H.o^2I|ۘlz;TC͟k1 F`4Sjjprp507 c(%@ Q*:Q%Vp}at tl%/oX GÙ Y^gGO Pu99DJjXXwWMc^'yPPi3LJ6FG(#֍apz"t<ѝxd8Uµ7o ?p+ӽ=I1VU p0'αjds*Ҭ}skh x>枉)|Zbv|}_Gï2l[8 7`2xva|aUKT= 7߲#ׅ>u:XOw sOFTdzq9RV15=wS_>Cx ~'' ![(z"*Ikĝ[XpT"a+ş_~,2,m&Q\XT;ejt%T:}Pu r|/jPz $yT,5=7tطlCoy=<=çOcʫY!!6}wL؂׻C.nE[/⠗gPcgHꙇ*'%<ɢ]~$)t8+ܒ=cތMo6tW隽mo%?Vx!*0aAN y:L#Mt]*-oj-8mjYQ_>>ēhHdHw^ă] Y*KjZ& F@W&b)Lm/)!$yh 2CL4<τA'GHNLR ެGٔ4% Jt޳ G$oC>T^)'RJ'[L4F]]aF;D+P(Q: 2Ko dI8vwb6HkP*g-^]&R9ϳca]|jzSmށ*ҕ@Ǹ/6D+/ȣѺ4G;#J:+i{;N(˥D8\ }6sKx1gW^@+7eb #3Q\LWհћD`cylOn[ns<˄OwO9j2lV=߳w*opt_S% >lv<m{*nΡ]{ RS6Y'x1EŁbMV>>%"g.i?THcy.LO.`>"L x-=€e/ۂ:zdX،HB9|$ 8,aQƳE.Gp!,1v,fqFs!DZlhDhq7s<,pJD0&E-6-Cay:@l-|=?A20-BHUqK@n3 3}&TKnZ9<^`Q&B /K(\o8^[得4/@ՉJ/k"=GCxfHPc'Z/>e,m?,q-PgIn ?TAꀣ bV;(K^ki"Zp7q|U>%(b5BC漩B%Xqy #} ^` yKNhF_N[Qwut%VL#rim55gBb3:\KrQО^(0laL,VPW(A6b߹j x Pmt*~JE>umj60pVCYCw1rS~5#ŢH#^˰}PuX,e7ar5=;ABP4}yt7B8QcLPe9*7ǞO=n{pÇ7qj9y%cC`L=/>vDRX"rwe:GEQ>AfD4%H02Ϛ75(L%`a)˔IҙK5ս+R.FK @KY * }d}' ՚Ld<& e՘ h^ntMwI(Pڲ%@\kHwE [uHl_% &<,`h.dmg uǬA8رꔴ@wPMuO.l [sR,Nw~YT0Q49x-1-B}eW\LS2M޶ WQgLQzi"%331wP! oؖ6tr) _“@7xu(=j4:n1aBՑ^VA$cpIR~K4/>CfWKUJoc< jl{=Lzt.[ TXOI0Ѭ1$B 4I6L1VA v{1ϯ߹=|,eg jh\CF1R8F1T&ȶX9CܕQIg+}!Eab?=*da|IԽ"Ȅs5bL53yi96PeyxHnR:$RxaTԬL`sJPZ\25M6e`/|, t4|) &[ԣ^Ϧz{VJO=rïG4.dGѢ`י I.[w]]]*fn:BȮ(.s+؜ 4ݸ-vpŮs@F*W]P.͊%{W oRVڎa@n5‚3kXTu\Շ^=#C; PXmkT@#W\ FCb\Vos0Sĩ2cUIa_$O*}Տd؞hCeYh2m/``dқ)װCMmCƸ(wy' jsoP="D~W@heXsx}[v_~nD p> %Ma$3$ii3B?' cg_r7Ph]nË0s&$R#藧#I#?ǃS$II>@cS!'dIwbZȃkDW[`"vAHQI@?l0.*N`R1Yx 6(ݹZ^UP_9Wxn+-WVA5#_oUCgH xO=q"(=R.g-0ޤ4hy5]r!^a"v}\s8 Foz♰]ׅ,Im}gIh`Ԇ&(Ӣ!zKG34q횿sB'-%cLLϛ(۫i@IDAT a,{-2`B$-^K;bb׋! ҆MYY<_25w4 / KKU9E<>:Ǜj;Ç:3YE2ԙU{ׯť r\"' K[&wvxu;ys7pC*DGي*H}p,$ay`]U1·?#P9XKX}s5^;*?p}ݣJKF%9=uUlC+ ??5ٚ{Їᑇ_/ ]"5}9QgbU^`lu .+F]q -Hͦ2|JWv|!|.BָK@1$(b<@'>P0i7lĕ2!;,EAЀb p+U$1'BBmb\* Zagќ;85ɹ"'H.䇣G"\BH3k4M`c3IGGr@9w<L&ZV,HT\+o` !5f-L?N֢d2`ʃsSAR=,o~/saYB&y%Jw5zBsk5e 1"&57fLrz˞s$C}kxۇܴP^R ~:D]7FnFzTo x/G?-Gq7U*CdI釂&&<U KޣrQ^w1gpq׎v=X(_R Wr{.L\ZΞ\96nzɠaV7vm"t1Ǭ26uT*r8[b,W;A rll,52h!E@ډxsT ۣO>Myu6 TCMm1[o4ڐցs ^5~( JBY0hF;AAxJRY&ĸcw?): l s1Fń4c H(X'[ɭrPp! BzJ['fxdNMfHE~XR9 iƎXAa}7QKa`s?A cZS(bicTYjzS#xZͰѶST}|̽}w:{7Ճ7ģ9 >2U6*c` Cz˞0ExvM 3,D;(9Q I:M'V=Z D5=l#w ڬ['ɘU&jxԡʨ%)S}qC.otXecU^_s[t(¿ 첼 ċ~PLu ~n9uq[=]-&b[O8*MBo 'MDϯU}4`9xJ=Ej.G4`C~}폗8Fm%97֮f-[UP C݃z|d\"z)^i@;E O=Gz?;zi/)0B>3g(Y< gd Ȯ=a; lDi.FR<dƀ 7Dp37 Ѐ =2L8Bp0R#kuVpmc6 7V}7՟:6~A0| nIxƨČtI*_Yx!G:X1׌YowkB{^\ ]Kp&+0,iH kA0&N8퇶1w"_9T(cxU1ܽ?x{r$aL:FV*!1dBE!shĘ uydkIC^'ԃZY@)^Ao5VlaSϲr,{&xm#)i8$1j-85OEc5>B ڥCTSC/J ?{=Hl;B #;Q-9]N9 ڸp;7m 5n{v8o{WmW}oNly*{"7 `d.PBͼsanz"DTRej2m^^/|l8/1FtY󙿰 nHCf ]>ި=pihRrzÒR \b e׏ `F~/F5Iɧ@)S&;mNɔ,vQg_<~63 f&@#iT6PvA0E,3ʎɱq nx}Z6M&6r~o}WrEK6]o%sQC^'b@ hCC~*x}%*q tЌ (#Yxʳws6Hwxfg)<.l0ɪx;4 z=l[lr݅-[IʋI-7~Pw+X>Jï\)0d%%x6s`'iS3Y"Qm0|Ωp֌Wٞ,*DK)FSUҀ4:䭏ށ96 ^Dϼ'd 1Gv/pΧ'p:yz.\p[v ,ߜ>xSҗyD&jla1U`pv5z'F*Y=Ź =, qc1oӝ,2ǟ _ NN^kb_TiYÓ;splퟢ1¸ܬˏ?aj%Ua$ID=fRi!j8mTPŧ %jfycBz+vd班˔z۫Ǚk"xK)]Ua##vlَ Vd,eT%XXr*޽" Ib7&H34/ WdMƹQB~mRozF0ɩA_FT٠xpqߕ:kHR K>^~<5J YDZr?~1i#"BA &\qƹ~+OP߶eN}/moʇ]< GgaZ[+‡g"^&žM,YܑgpVn;8=t9榤Czx/5 ůLwcصw*}z ֛Q9BH|q=S1j%#zibsyraoQdv<?kThLgHc9kZKъy)bNX;G?L KT'%3¾&2tDs>]c?gC7oغ)۲e8b<6WasxkT fL<ⵁHvi)D^ { (DZY}/msO;ҸwUVx-ܾ"1{z.R7dVCYF@T"uKN!nbSmlz*?,(ҢD ]ĄyRp.b7+Exp*x@=F Elry0rT &;͆Gsb((VTWنCg9華M/Ɂ+2^^}ð*fn YPT:;%2b"c%<)f>6z#|l{~hʩ0GH~Dڹ vun"AW0T1n^ir.LDX:Q&[{ cÎ~C`؜|%x]Uƃ\ h{['?? Hwy[9h#Q*=5d8àMd=Éanټ8qt1?#Q)41,Lr˰Y^=W3sJS`|6YxkFyK ܌;DseޔS+,C/y>y;G_l Ãڷ sQx/7x*TAr8&9w {t\͗4Er 4î+5nb}| ?a0E K%t|;dd®O"O_"L<R<ϳ/.M^Q<֥68k#\(rZb{!<5؄2!m,uo? ͫ˟(z;#/q2d{v#U%|^gN41Y$3*ל2I`Vz ᙁ-$%9`#.Ɣ \c_Nb (&w);^m SP5@x3Ru6p=a9l)sOm$ tܷؾ(0Xox׉4h1U4]1"fzzs$z=ܼA|Mu\ZadܸI}qG#WΊZs2^;K$aZIɰn9VsP;mL<߲#>wC*h+̿9uٿ3Qcqx‹fK* !YR!LrApyAPL 9{`Zy0V!KFZ֡Tv"I[mzৈ ]-hl8ZM)&m\OyIH[b|h2:E ]c(?-OC ]dm; GW^)Hdd=zD^Sq-bXT"@7_{;+Q_VH\ƀa!r (-rH uedQOC6 +WhEPsn&iz/ۭҍuqϦec^Ӹr w\9t>nRMWKMƥ:R Dk fFuЂ=pi藟R?ތ0?+XVE&qfkRLIeQpNX \$t̺3^w__w!>.N'ڂ:%+N*cT`4P(x3QyF:" ytMg e 8:DWm}Wx^sjûo?k"Noo(ғ_]DR+dqlR\bY!R(9y|uen&F9gxG?93psѹb۪:x $ej>ݖ$m=`aş&՜) Wu #x6>Ssh3]B.nE&A|8/$'N+7gadpyo<ϜHʆ!̟_qd&'σQvF4ɰdbB7Rz9n $N I$tz폅ߗL h3 N(ч/4#.t~X)6؃*ن_&?W(Fu9D PImTD,z?ꌳx0aax%~:lEP>3 eCeYH<:Mr x@"P f`D`@ci/oc3?FDav9mt(%JV̓5`.<9G{{=LU‰\K)]B@pRRh]|\uAx蹥0œ*:` _Z 3HĕIttbQ"$W_g~H `i˱ʐ>SZw~/U*،c$Wk']h(Mč-=Mp>Mgy.=7[.Ëxˬ)Xd\g" I:vdUY)h:>6x)2'Fodz[8 й{zIRe" ijS6?cX" zu@ǧ8u[Si,2ԈI@ը@¡L /Eos?~^ߴϜi `0 qhRUm rmJyaDf}&+E=_G[+Gs)$ʙU berz f%HI[ #P"uxFz< K3ǹ{Iu| ^ϟ;3SZt!=EJx zOj3@Ʋw   H±6) f T+s܌C@)9+u畁kF6(%UpwspՐx".=A!@3zܖ wT&|AA$b#ƫguT F:fq?>wԢ]x:8 }4P n!dw-{~B_X5{쮐4 < ;X99T>{SjLJwC>+*3 (U _7ߺ. S&iq0:x>۹ u8]8B|3SqE5"\^<)O=: t|wft&gyyY<],&szܦ%< S_q1y~:]z{?vgOJj{vrv+< F"&kCM0֬.R- å{sl2Mf`2M 9n۽~^,&TG{Ͼ].3]G˂., OkvH vrss+“LAX٧y~ߌ0T!]V]!|j߶:D`drCD(7x먧طi%ϙE+Iá@Kχ$a^"-ؤS$&]]HYHCl ? յjv6Yje8L$"Ӽn§e o z_ 8zzk%6aibک(AӋc)߿;^],19J j F1J)iQ8IlشCۄ2nZ3z/Z%lUF)Ikֻ~6nP8^c,I늲֡AEoz)~/$>&I8Ӄg_|b \yITxT'Swg8+z?!7c@j u%P"m_6}W8@x9+}U5 3͍fxn%h [Y*2D^O1@4pd0Gϝ#9ഺiy.':#HNͽgB?)mQ8Y1rΐY]}$ 6hX w("ts7,PC+Cud2?^>.]ܨ q!g< h{;KQx>{Ytr"+[7@|]˾I bE0˪C&Yܣq2}o"yta.T/P4Ѿ?>4'?^>V8ves$lE}%0t-B>iZ[] Iu6g֡!draJ@I g|EDǍFS dV:c3K\K)#^ӊ{K#` v-ײqp!K/Fݰ2#.|䍷(߻aqmǘ2r^Kɰ&ɹUjr n߂͖AH?U6;.4JXW&йAca],tw5:t8#g8gu`V,JAщIE(@Bit4mx9T ECO^xPzFxVR\rh'(.UA5arƄg:AGޛY~]}{ˬ1n"-Z(%V"EJY$;ĊTHe)eKJ*㲣q.'fIHHB$$v`{>fD%Rh 9~ws9G/mʔMA`3@*`ok['DŨba,[<-K  w}*ߌ+s3_[$PQeA&R00Ry' >C+d`~w}֮!D=]򜛌U1f=q3dWS4F(09圙:Rx(L"eɀ󘱉5(zKXse쳚NσTR-:3( ԖRqΗ۠|8M xQ6WN:}c9vi,okTnŐ|cGįdyZWl c!VLh@ZVu|~v"-oB^ރN4mQ6\+Tќ*wlXA: _ fn7 `2!e7*V*# G'䗷i?@階|tҦRPK,(uCbbsu GW&.νc>P&!HUђݧ W }6h_p} S&/`m s/~BxO?aT+R^Oow4SD)cY,C+:T;{PÊИKp e9 PqXfKämw5\aa9rZ\**f0{)Wqex$H0Ϭؔ^,tdhP9}В2 m5@[&W=@ C[0i3_T6{bE+i7_$͢Lyx}Ӕ)Gh2t 7_hEw9'%"%fbo0s@IDAT[\˕RA!d,R_7n J{経>t%W:X 3GX\iuhT ܠieV8;XWcBEc6V8-5#a]֩R,!SXO2&\X{C%˶Gnð$h`2=kVkuľk&r\UA"^{psic ;8w* 9+ZH P ΓG#xziEuPҏ1q8G`AwokØqh~+T% OHaRZ2\\FRFTT5Rƒfa|iL bn[qJkgf$ΉJL{Q+!SYJL}c M53@Ff{l>) )'Ih:JpOQq˛:v}FC!86cn_` ')y ,(wZf" G`j=+5͌$zԦp/2WY`j]Kl: t3qQ`+iS- &{PR;`.YqHpH0%3fM\6r쀵e Y圉d0*viTPlҿ(҅V&t^ng߈stN^m)w:iV$FB&GlB4dF)8@"#+$Ivl>wij-Lȿ#lpH5M5[9y~ =p" P7 F2nZ c}V6]7*rkLQzAnӓ4s7<74Z6G##<$UN<쵮Yl)os`1oAt(IۄƑ.1AyJ|g2i>&AcNdFP9Am:.J QpMV qpt5>}6qWuRQ٘#m ) erWsXԵ92@wm-eoA#bq S< +=洧M\5T9jSXBg1?q0e@46JֱX.xyE,lԤ33@Qܸ @Di\5k XENU; ~X=U83KU5,|*)jYhT^s&]ֆgQz[ʀ* c ^bk#׃cK y*K1@Wz?ɟOM@@[VÛS /6Pfy#K3q_v,`t eS~/M%3ՙ5oCZa-id@!]RXd҆L.c9d{Iꩥ+|v3tw2P[qu:fXk!Y}1q^"86xѴhZ&Uaσ#35e[ֳBqq&pv>o𩠃xAڐs {D‹[  Y>QdxrU/XĂY*<% u .Kԡ)<ׯ6o%rS`ޤi%uD4ZBэue4-Sa 2~]FFw98J:}1Ace4Kޢ_3YKRfJ%sg769Y4YGqзM9zV F'Pk=?1'އ: ?v$¸ '뗘~Xi=&83qQr娤5żuvBy>6,<ʭGXֿZj).pp akLZ< ݮ*PDpUN[xZʔvp)Gۡ|X=AJXŢdiTk3c؇c\mS4poaܧ\iT Q3g B9Kލ̾hZv)r",4Kа.=@ Sdt$b9 ͜fYEa 4zOa@>K9k5nb!A O TJOEU%LiqSk/* j '4=7Q^ 0t/FqzS;r2v",Ee>4>> _T(BdscekciJS/ipR*VvwyBhڽG.} 2#uRCDoD.vL{4ka-͗bv=_3 Ga&{"m ZUjUN`c:,6nw`E ėVJu~D{Mߦ ט`)q4bZIr"Y /*lY9`m{lyi1=ݲ=yk.gTgn5^! _3Sx,鮇tKqMK MK}i41Fs_' K ep11Raa@`'}`HY}\9T]dz@JG@-W: F2zIkV_ˀC+k(X%wI[~8*RF̳-7Ka*8:в|1 H 9AXAyT*KE|1LJi Z<7:_JXJ!Eo 5.pyٵ ӀyvhQHGb,"g'pg\ƌ/ DfM0s*UcyC~v9TFvg]=-FCB5<ϣZ KTxkvy2 +qQfD.ʆ.^CV>m!-Bu8 v=_7 |*qgMfyXPj!(Lc2+DiVhaS/G]5tb9#\f܇rǏ?; [9zo;NsTM'_ /~=Z}X;Z9LQSM&DŃa-)",3ew%b}OO?A^(mi\o#uSq\3Q7.M+V1V+L9,l4yД+޻NHX[z$bI d7i+a;Ƚc>rULaQW0Rm]h^rj󥫡ymfÕ7hGPa**'l -]{a/?wnp2w εiʤɡayfڮ‡՝KprAfz79zFޕ3U($q̊a֛l33^5@bfMR,©+dAQ#V4,eu@\g+げۀ;7_q3&mmֲ;5"5dj7jc7VQa85'TEDϠ\ @HE-0mb1^9 ;j^j&߈>8ix9-'m^l?*]F n1f7֒lz5+ ֤hkA}7nB O> BZohRwQe2+l(G'fy}3: g0MLyuP;Zx 6TOBc8,&UyKP,muhW) wlq4[̳~d)fիgjsdžXv=_3,[S佅Ǟ ,B̆lH!iTsӛ )G D0Ήi>F-] fiSYx;^eb-ywݱ>!NY޼iR8\G(*qA8GԲQpjUJP x #ӭF#Sܑ路sSm[XA}wԿ6tS9zXeElC+%RXX(8(-;6GV Fo?$j} pyMk5FqA?|o^㹋+"&3%LN#ɢ}ƒ" ?w>{\%*8cvn#l^BQ *5δlx>,v]S]6u拇5gnoGH!tdAR1\gcRmLa4qu_38qYm 3Ӡ-Vg<^A#U&q{0$CDm*`4"u"R VG@Ky δB tS?$-F[5 9S P{kO'QBkc\{eVKYe0GJ`rmI&OiUXGĵ &C\.1 mg1wNA Nkw˒ <1g9B55><1T8cW Q7=iSȡ~׎^l֢jzE`jv{Y,mPQB'sf0c hdN21.Tх.`^@a+(5*A)skēξtVy??i 3τptF@<"$O͒(Tdf5-ghRȼTD8 1Pt .cUy66yc>rfPyt+>EOAV* 8uuj%)d:.{Oo=jmv[8?z76Tig;(Wt@`D@3o21{8?G8r 6v/Ε]ˑkF V*ݧ{5$eJ99D]׾p)ֲrK6COQ G=ft8N>p#pbP ,3h9eQfU2;ژ,y2ot0o-f|Y<W_`>9`gD:I^>6V1SYa&8OS[׮M ^x<4Kix3' DH`KbsE@aZ8Q݈=3>\Mg A bFEJ}}R%iUKXO{MBkoL.sR|6֐4֗Ӓ69_d޺m??) Ve"U;qlT$d)/}a^gac<ԫ12&d)=KS؀q1f`8tiȾdS&kZZ}׃+ˢ{_|b6shqn$"WW^a||&| 5ɥ7q k[26]2!yRc$Hur_Fzp#` 8v5]#$4e+?]PB{\M,YÏ~s8q!㮽|M\d^h\{MR?aci>Y.Xs#{se W caNkř"BӨ.u2o9z&CA %s6@H4߀yF X+hFX xd޲p8M}_8us[5K\9S#&| c> z;BN|0>z~K<,<뙤GO-]"b*bZ mW<^1) 044m(ZϴxݽAdz!κbW*+D pY`~RT0\iyƷ?o$ }b^F؅u#*:΢x(zXSr͢9V/{  Yi'6M'3p(n`cTv ЭbYa.hbYr%aH1f46vZXcs`^26LmKq8g$8O9fh1?R,i5PlVfUmNGfC9Gl_! ^ahr NlE" $;c)xT==q` 1͚z IkVԪ(M x =ZesMLёw7?o,^iʱŽR/?`/(K3W?_y1n<.[=xH9m!IؿӆH{ +>j=>^IКz\DѸ"'x9m @Ek?SٞNpVIAfc><™M & g\ 6}%^Qܸj@?k'֦bXu(Fw^%>+&YK_\1>&5G:B1A ƫWfKx1TY ΡryY+yӅB#EMF GcMA V+D[^v%5}:a-6z$^0p=, e6g4#8-uOXb žH;?IUׁ-Z!1EZy6#OiP83+ϮmKh1.gy cj$gQ7Hh5BځYV͎PKs"i:L4sG[zuucnt&bp*|^/3,5a}oJy@Ϫe]2E B.`J<{hOR!L4d7YjFܰffൎlQ NIZ=BۢQ{So[ X ݢZ':B)2 ]%"9uARrMw|/w<67f[k4ȨDSFȴi@BLƒGùsopPM ytl9|'h3brZo(I(b+SЦ!^\z5J1nJ 9&> a=-N޵Nݷ6(WM*$Q)jlc/8}z \c cO@eu7PpWb6'^[|ZF9 0RW4aogj8QY4R@y''=\o p*ϘL}W|90Sa$T\}г2g62VRl4ئ1/{?G` HhzH_/RUTN݈+ט*Hz9I *\ UdʡY/ jG=I qqi?10q2֢9>ֈá![Tt%# 9R99|b@!:f;yXkΗLY+RqtbNYbm0m+C^q'hZ:Z G!;b* "1#ryqP`Nv=c<N`\D^z>Kx5r?ݦo$]7O.#O*Ӱ~d?+3RMR̓-ܵdLk f ^;ǃʳ-2#jo?\x5ws890{]D'ǹ?6.{ (`!\oDH#6;Ӏi`Lch\׸Sv`kveqOTwΦQ=~%mypi kc:?4΍]7.l.Ӱ@c0a7(9BZ2W?[Xb frzCk"Hr^/@;G:SVMcA'}/50b\ {AD:?]5]#sNgԺ@ZbV>t&SՎ4d/R5%У%U4ϋ'pLYs'G}8|xXJ/V -TǤMPyxUc?%U@FtRKT*X:QYK{~јםʜה1A*SZR[`r2P+(8,`ha&%:L7`-J)` N-"g}9hR3}`<%IE׶hƀ4B> 4_L*W(^ëϾA†N~u>,,bdDu2ceX-=vc_fZ ZLxTH?7Xhl#q8p{ib X Ȝ뤶( n-~>Qm+q*=W>)Jgn3\4uGay^<Zcg+Í(:OuPxc/XgL .ȔYNȧ%=Nؼ1DT=(9֌Ȳ/ ^aXu >hϝ3S&|9ǟʜ9c\Yb|׉6V+hХ%*^b#wlp1+|ӚVmi;KT0TTWPܘ),~V",G򦱃I1f_H7 MuEz.>0vZv=潵IeIVY_JxK9"[ W$'\̝]/b1^& K }),W>+L ̔yHz&_i\y}_}F=`n;Fbj3r|g.Թt^nEVd#c6~ƐcB[\9ЧѶȂc@>V|AtB*EgQ-GKB1Hڢ9rlQG)bhT&9xLqn jkۍ@uy1DQIo\RR%rx̪R^Jc7 0F#S:Qxב`l}_dN 0!pcB#psl~K{==ʥ6rTM@D A&"hp.v4dX9 ~xVrc@N)ǵݨ.b»f%ZЂ;}+LRo"p]1-0y8qͫ# 5׿ØMo(Zȭ.\\Amz:sioZK+"X5@k)^VhǢpĂ)Xc+.(htJZs䭷;Ϯ"yIԌX'^1]Y.a1cQ~KA)nxwR `<78R3oqhCVA?;(!`G+1Hsk@-P2jlk`Yy e!9hC D9eOW}FSV F>\CL;s]L\2I]rX{\2 %>|rh3XQ}6l0*]yvO|DtZI)7kR`loaDd:h# VE%b.X#]洢9r{݌nf`#߁L5O%TנVN=|_שZN6 gAgb5e'W:iJ+-F|C@r&\G1^e\*sX0%#uucp=g|3f 3qcA~8+ly)#٣I[ʴt ZjoCE[X鐒>7k̳)f*=DѺ%)\fi=<%m*B/v3|%KǹۮfL+3lQ<aan/n9.\Ϸ %Ja#1p^%M&~e :vm4aT*@xx- ̳N)oPYǕ +\pᕠjU#'" q('1*jxn'1f{()Sm3ag()Y| d51ƆV4HߘW%(%> @5K^{cX[ d i(ΓН 7~Nf8RZ,6(WrE>nH,LCLX?=\LTƀX6G8Ln0ΦRZGOОVv7ϐud ʹӃ.{}t³qb/A"vw,WAGƖ&4}x2mn8>֛Vil!p0L](\mW?rI*>Oa="#phqRO?^u;GƠHaƆFhpZNˑ&46iiE8Tkf-fC7jk :QG]oX.P_6qL8qrT?VhpHQ5.:?Ç߽qTtLl^mGj;뀑ѥξ|D}rxF-[/W:gn?q]:#r2+&V m >}pQQ@qX7 GTD8zjZ\B`]vnx+gI +( Xx:(*0_s+~POub@IDATЂ7*J8 TZֽ"~#îb K{o1 '=U@Q`sYYeY3(LR2۬3 fw@5rX0JprcЧ'H ȫ(XQKGivf $(#1@NK?6צWΏBgc([)-l; ̽d C=@)-}gi} B㓓+ 45] 664GN`Ip';dY}GI9߼T3t/~T˪Q.t{+:*q_@)ʠu P6 &:zý5k@ '\YslXVÉ>HE>b<*5??Թ4_^pjܿ_za6v/oL;K_V)ymu9ˬ kR@@T!o71Ԍ%@Cı3yϱ6m\(W3g4vw40QjY,a,j%aؾTk} XLkGJaL8@?,WXE@E S| gIlEAF`6Z.򞙅mP5*4T5kC0FΘV0J6Es6c{T# HPȉ\oc!k#aza \1xr'ys>`!U,^X|X"`v +P'M叽^Y v :=@s&IV^j<ڋ(z3!W9YÃRE95<2`lDJ$#F#)@ CA2(A'!-@C]Gwo}ԯ92gg%PvVPXMe= d?B$_^eش>% "'KYPeZzCF42F}QfÁO3c\ EGu&uaDQs<:T25I0TJmignv^+w4}-ܿ9>^L68C8Ff*Fl<@p\ć;lfj1'׼~J7ƌţl~>6vM_Q|c_5ݜjY ;պIc  L~L8{dѱ}E ^gk'c0Cdh11] 'W3p kWÑY[n45As;TƁ܆/Hc,|7wDX& lԢZ cYr,Ϛi [앏ܼ]xy8 Er?'tU 0cwd!ԯ0{L*ca6J #v?mfXW@{_A p l^mK:AX ܻh}BKIac# _0:R,:1 \hkhp/K)rГI1x G\{YL=t9d RCDTLx-.)]{3(y@]a{NwFyB}_cS&AJQeΠcKyϘg/"O>j~:O_k;J AUT}yb5B<"#v hM=߼|9CW3RI@1oe9G&V1680 p/'5''| m`(}@ړ닯R%~H=1HjUnxϖSEK8,uxh(cTADlE%nOVͧJ ˁ{8{GOr@Lc2"Ϝ+=XBl`;\M<$>ұ5`d(w}# 0RU5 5I6tu\'gb&3o~^8'fji6!i^W,}Um1DݼhO"iǴv!'i5Y;HqxZ\M հUK;hq3G6oRD =Z )y2\g Vf@@Պo]`:_ZTF+^HP4;pG߳X]D@hZB-xdz> sDGg/:i" i@Vb_Y~8&(t6ZD)cpy}}?4jv/wXȹޘdiMSh7_{w@BUӕ.;}$]> />{;- xnݸvh-uBdrX>p4/4,L2c3’'_(V]ZwtC_b+Z_ʴj\P!:n2RP# ^0& *Xl/fm3iF(R@Cd**a  o9Ј|pSiE4pJַ``Je?8b3/cF;޽-R V OB1V&_ߎLBF x͝5?kw!~g-,cKڮ^6CvRXZȴ,?DNK wtZ.|k3魑cNf7Zhf_pZ-lr)7 !2}Kq%4BXH13^-,P¯)\pZBl/XџښH^|%j/y  {Of:e`d?ӏ?57Q\F{ЬWշN9u\ Z hrsoOBؗmZ%hzeyrQB ԰UC9Lt]}Y84n(H0?^AJ(iޠ7@kN`nb2˜kTEsmM.]@50ٹPϟؙ;ȱ2ҳjۄZV@m\(4/ƾ"-AqC_KZXҦ"PhM;$,:WfI:=]MH:ߒetxɈVHRV NΦ& 8{Ϡ",JRβ1ℝ,&@X)kŎ{)S@j*UtP 3~§?s. r |W?rW4pq #~ eY@د1JèŰt0w'RGv|Eov {6Χ"?+ZHv|CÔa a R&k&+-o|q=i [ua>‡wS[Ta zZֲNf/UҚuIA7Z>=+@4u)\ˆ̳Gy}Y*p skmS8ҌT=4ǹ  !o"ଏ#08\ZTGTtMa|1YA܏Glw^T9ҜQk)2f.w.# !Tjгˢ9~ݫ0ЛSRY-֦BJT؃Boc%~uFDȽo"(j15FVuIȲҺY?zz}]!po>ӗu͍P!WZhHczB޴Q&*$E' b,6iGpwo/=s;7׭6Wv9bweZtH_;1e7xq]eG@Z ;H-ZIT@qBaõf(G-?i (|v)h9hf_v_xgԛ3ȖLssȓvŁuCH T1;#5 iǹa*N|[@Z8:?&uK{+2FTƢF Gr%APNhK`#mcKo lX#M :6 (O[Z(|N8X.t{}lws|%릠2EAta˯FPg67HpPY|j:i7?U`b2jw  5zVgV= _rVLfMT?3=(r}@@cfm̛9rm T3l KFmfVcya5*>EJ\E/5R.* R 0<e *D2VQغνvjM1O{(atΏ2C6Ǟ@}1Y)؜?-x(u[SəR~b!Y^/9M0ŭ:g@`i8^˄{FNJeZ/mwF~5'8Lţ3QtE5qٽps(A 4範j[p#8E|&95VQ~pﻎw9ٿkFZ[ P+6Jz֌ 2(,]x+>R.Pϓ?״ښ!`.^t#X) M_.s/LhZ=iTʊǗx IXYb bN!6g *8QL#^͡ҤdTMA<#?xgUj,e_qC?~pJ"F&iTziGf-D76.RԦ6WX7Zu^|ȇ<5ˑ',yp??6*oox _H9:ܧV\U]@Ϩq,+tOi$g/\jF߿kkL>yDaS/ybc8g?y0hy v32]681'[ys?Pos:b5@:59k*)ZL,@AP7@I.x]GrOkR0"aKKc#KM|}&eJ&wR*9hs{d-*J$>ab!/c.kQ^<qX|'ZB@F3ޮvWYir~8J\*͊TkL+ -JG2߇>rOyqיkc7o[Ђ"`6xalIg5$,ˍٯ[Zd"t?߼F1"INt "D+dY{`Ze,AĽur0oV5o0"{E~(3';}OIW \NJR,81xAr3ISIqA^5)dmI{<8g{2pl+ÜLkVXe/Ô-ݺo X|?D)!aُR.l1bsBG*^/88yM kq@ Rjr*آ1*a5KgC|>ZӤ36ny܌ҚhD߀KyK" 5zI!7vw*@:Jn*(϶R>Cn'_Ork-.S p [FxR>PX8dR" _ڪX=eL#*Zy(wqoؼqZ8Rpk՚/穔aL-omJ7f2$(cr<;`fPpڌHգ" ]cX@BC2Ejlb.qs)o]E ,c )|S:|1r mL$ t+ѱH 1;Lj|oa1{NaUJ,]ӦX""DowYz IKA ξb')-cP: t cdYM43 ה~!lH3dc{ -K1LQ2$2 9 T"KS.AlxNC`*iz= <-d^ۅS~og[@9#{`wuҌv C7`lH8 *q80)橂]Q(d DžP$&$!y>=~{ۭg{#4ϼ~w[s{IG[@Gd 8J&&`*2&V(yŇ/]93X'[,Y-đW -(<@Q1Y/WΞFm@5P7+\{?ԓ'7>p0|bAlbS_~  Z\{) *_22ۄ@OQ/^&͘:Eak]7Ct=AYgR=NXrvkiXn]NjT۬]p*= &N$˾o0q}<`.ȳi9>h`~m:FH[u>b%_wv}jO cb=CuK,e8Ё>i^#GCyC먲~oMOʎG~>.d)zgYGs @F_x\զuBzٗl*}#2P66 -C.|S:BG֡?:̕-awɔ]9^^Բ_Iuc7j H-׾tbsrӢi>Fd? []2R&K^_42/y-K Mrh3(i\[ ϑ#7tt[&2H9Y @y$>[ 6*J:Z8+1IȂtFܷ5En31.qs=PLe2b#*;H>r,}0& ^ o)^(ߍ"Ck(?V2w#C9UA]ϗHٵ\|6Y `Y^3 /_ц.Ȣ*/ 3ODZϤI'"#˜79b  kNwƨieAj8>GqigA.ӂsy9a,ٛ#.i)[ĂGfy^L~|O>tiW'Eg9p+':1xTA=N1QLM V']~{ʏM?!دqaY D֋WTv9 &jyWʉ5"b6gzAHSrΧ!MP@mL+zzJjlKʱ VI{drAhY~'l#ۏ$5rOg=VG~!$-ceO'גxf_{yF}o“-5A4_LarcD^k 4]"͙ak0kzbUr <^(c`#p(d7@uO6)Pu~g(2/< 4s4 ˒4@l^Р 6M7oH9_ǡex6\kp5,~=u,H"gI_ ٩:8sjs$ԣ)'<2Z2L0]lk1x?Z]K;DdNp(lTiDru s#NWr=cN8UKstO<MSgOxBCK ;  ayb̞` Mga^(}[C {+|\3!<KK(,)N9Q!2W{+.4hFFqr,Շ.1d)XOR#OGp#ZE22\`߲9yZ:~>ϸn\7FP#p類>g˹|KupKz=WY,33㹗O!^gXE.b+(&o /> 8m߾W,ln^7ڮPx߇"рA=P}< GZZxQ/>Ϧ{琡Qi|@kQtj+,k3\bc/B"KɒVFJY kHh$܊91Z4"4d5 #sG?*BƁ"`v^?܇^&X֋,l X,d5_!84vdb\9~5=8 ^.@4rV)׳3?>xT Z}(ɢ$FxvOtX-F:ehS p7=%!R0NWr*!KՏ-F_ 0(2{rggG<܅T rs1%z=B `Xͪ},Pdŋ狷28ǭ(DUOsyGZ 2@CG/HW5:h䌴+~mdKVf)^ʀ^骳"k0}Q<9ϟ*RЈ21eVEd6{}w"Hb$?2@i\; ["6׍8<ٍeYiAsLfo96[UƣlfVHl6e#=nfӍPț Tvk6] a+ jxD9ۥ8 X=F`p-Lh#Mz kbj5zο\z y`FroAwnYO_Ag:f> _3Blbw黾鵯O|WC10g x=Zeh Ŋ03ٺQqϼBiJL(_ 㹴{nKf-|f>n ;@kQY-O2 /ܻxo1Wi~~c]GGB!cgNY+?8L殮O?bd!^^|znbeӣ^%_9VNcqjܒAOd~^\y3Oʔs'xaL۶ii[&~AF7YDA9Q\z5Ft[%33*&7q"MXsY s J=^w/EvU’xhg{VREyN}>H߸#]H3;?^lJ:uj=_<%X WV6z~4fwcRf`]޲ fpߖkZ3`_t#P!e\'U:Cb|HzkLkǗyj{G7O}f&_°b=51 3P9rzfԧ@(uL7bElvx221?Q ѰR#چbM>yC^_x&>?D>~Oܑm瞔O\6*PvCm?~zBnt5 >.YdOa/SZݲ Չ(G 6hc-؜"gRU_ a-hY~X zn+Nve(9&msn1P10veN YEhd'=$]FFOk4Y/i;,|/SKFڏ}x3#9<-q<tڌmW;ǓE)+tJ}hp__o|{?@KT@)W뾊82BUE|>Jg9 yAf.^aC ,(xE3vqfeDT9x2 f.v}3 GYU^< 2]U* 0y-ONٳg 3MCY:|_GM鯦gޙܓ}DkT\{siw+Ĝr4<FdL)) YSǥ0 /_5=F߼z >'#G ~Y*VAnEc4^iRlĺcMkRA/{zTVՁs\ n/'15-:(ӆ;ȓ!cJg^Gw'6ᡑ㒍Rnh@9@Qxa3,t􊗝 a{듭=RqYΐL 'R|βa-{ 1pL)膺1`R \,>OJLNP|9Wɤ<ӫ~#\}CQ'ڍ]~:qO| ^2::f҆Vv(kͩی Oh{b)e5} do@S=#"[8'`N0h1.M;Hake 8ڎ=,C̝mD>7G31v AjJXh͇_ev)d15L!tP "_yNNP4#1sޕ㷭&76lq##(y_!0xGUAb̫[,"t.Ż>]k>؞D pdѫUk| 2^,= rhbzt'hPz(6T *Nj67~N%?8go} ( "5|.m~>2m~{vevx p|u H?"cE#gl ,iWJo}ӫ^C=?a_|J('ǔT3ExWgM&0R2B1xKU1oR6HMPY*[ɀBǗ%s(V%r2}:K(lNB\%H0R, mҘ ^hJӴ_* J5~?ee5[@,~g:`}8K+- z̽?vifOA̚sXi-RqrYgp[Jf\F&7QUy#hlM7e@IDAT>؋=䦦e8RY9Qѐ5WJr;2s{؋aBO1A>{@^\ kd-ڮ.(#8 (#O{( e֊uLVՎ77Gnc8n*5Ħqw_0#)Al)dA9<@VCEb]J=oQ)H#VH#eKV{@x6՝hgs3e6ܥsѱA/X ђ_4s/CO/7_MoO?W]ҧJ_:iJ//7Z\~Ĕu=6UZ| `*ּoҘ2DZ.4ŀ=FQ^TE Cz&} 48L7]C1P{ij (8*  HCuq1-Ճ;Td܃.ZoebRI^^ߠĜTOP]~(u}c#8@. ģm=ό]6W'@(ldTH({0# oL2GM|E+eR40u/iHeY@]d -\:UTቀAcb3!RZ>s]˼Ѓ"=}q+w<#k%yz1Ϣr[wjyh!hǺPOKx^~x;}rHb0rhNCȇ4ͣJM.lv Vm=eKL\ jB{ 0|=hWأ)siޘ0w9zoݓN=AN/uxb+ ֘# 6bN!2'z}i ī`(ˉً1Lt?ݽl:KCxQʕR#ߞtw~.M޸nэs@ooYFn(L2CW  YfQz>U^2,I]khz2P"PV\ 8 fٷ>J'B 0Nx'L@&APr8}5 DwZD >쵱r HU р4܈3H5Tg}x7=2w޺^x*Qg0NQK|`6%Mhxx1H77mYA 03p8GS%gu2ңq~4JŇ-=rf.ƀOye+zπtg9 Pf'}@p,wǁss>=,2޳AKEangh"}{J1Ga{do߄@XI8Tf[|HY^"K4̦g-^I[~i4`O5*@jӁx# IN <3氵 o@d槈G~hfgh2Y@jDU5Fi{t+i ce/ 'J\dQ!րucVϗI S O⦴Q L+gEL~N?e, а{:)ղ Fa|5+$ kymZE4I!bB`XB^85=>ǾK%<%qyav-vdyߴ-+4_(@k YA+Bf)3~6ַ0ko2J꼲%hrL^pB'md۴?r;F<kWçdۤhP(dpFP%?Ƹ鼰MwU(Nʮ; Jj _}Lgs5x@f 2<6@"(w,dss3{w34β;uc7 !$@P65#h ?{2O'wz׷|% F}UHbfeX.pH%Fuk)P p(чluC$EǙ,M@@oiG-t9n#0H@-3E@$<W~8s-`733##j$ oh5ܱ2L?oz*mng g UCY=UM|g #8S`q;T`A*&=+Fzj=̓Z0lsm@>%oFi0 G@l' jvWȘ zQm{o~}wq]&ϻܸn8)+Tvc':1}淿|ǧOo~L~Tx]D3<\\ےȆQ b=}\_KS Ք%Ws QJ/:2q EeG(+ϡ ]džQTp+jIt7+9l 8,ݛSNoE+}Bk\wCl-ĵ:B43Jvc,Qj.^bTu+(qpLH JdI` hGj3_ZS51_%c>{yL77Sz{x`̀޸n <.J,i2lBYyL`ϗP,))!W腖gByG @df%DB";Ȃ*K%@A.XAҳecSx0;칏Ǿ?{o]L:3h yݺ3I ȯӧ|Cs}#E$YNx!`&|Ge213B/Y_z{=PƼ_UQ<#4yxT] 1MjNJ=J\() >3)D[0TmO 5G_ق&Z]Xp\'N?58@ \8 c]nWah-pЋ.z5Dtf ÉJ };٦>hlm-Igђoi|q`ݳfJ&sδ|ᆊFhYr]AIaLtaE2&)Ӡ T!yl*u9/::!TZ) Mm'MOOåu0PxLN?+z? U :f|:lȞV3F'|v#6fc@ɔԐm.]%0MDp;..˘[GcXwzq0ckh,8wy1CYa͊-Bx6CR5V.>.te"pt43wz\cp=RV3էD.b|΃|=]-/p-ߠX 9QфN&OJJYX(̊B'_K|wF7!sE֘3gnvKx1XX _(OxYYS):>3{ Z}Q/Al??{vs<"_(! sJXeԸvEKQO@d3{CƑB+a>*6w'ߍstȍ [z]GE0`` %8z5BN}G AR:(ɐȵ_y+ 砱jAd'hq-"cfRY'/Zz<N9&֟UxP.| Jr4B;2  W΀.;.RHp<4G/-< zx=6=퐑FijtJw{mU2Η#ʓ9[a}}i_ V[(( T^4+~T `F#p1F,Ozc]>;> .x=]9VB4?B@^-U)wufC)D$'-Vnacgp4akT ku4d.]ƳIagKȕ:N[nI ]g+qe}ۅ)/~vUN :tOT"<)LeAyQE^W)^֑m2.;]47k'Gora@VOȸfd>6еu}J:txU3}S,ܗMS)7G!UkJ,7'fqntO}9IO4~S#<* 50)/t#qfm^Gr連P,ף`8nܣFO9tj{"GYmnm=mpG(SEEpn˸HVD-`g{^A8\ Ǡ:=$#x$dH` ߂LV-f J: 7PYzC^)>?R]kL6?22PzKQ2ã3mL3 [o)pZ7E"v{gttrIyTx 26p1cznAA氭O94r95fz/4=@n! {ȠD=P <=$kf}Agcjs18 ޓ[d xiaO**B%AU=Hi (d)Wsr,d0nT{Y 6lpZIR1akg;]ܹNנK {[U5'S,NYwWQ@Av49[UF kA#`2(BH5֦iJ@? al`=f?yI O<3/5JY5K1Q og~ 랴HY9^Io=f n&=R!:s甥n@D7@C~VDE ḄDFɚ'Ň⒫xWax[gD(@U>?hЫy9 dKx6NlսP4eɳY%G|ǐǴZrL4ArWpLCR(<'.ɑ ϓAK@[^K[ &M},;xA7 FXg#h[ž2H1@ kѴ23xsI, Ę$uh4"Cк E2%P@^^2+6Veӧ_P䦰G5;]ʪ35}I*>BblYC4⽫nmCzմYJ d0X+"xPs DNFلg\Tc3(jsz\3.Ua!IuwI| zV2T<*'LW06bX 17} '`5@3c27z:Aq+r]o c7ye^!V)D 4uovNXH[#܂> ǡI9iŏ 0|dYi/c,r|=L߾Њg4E ŐNu0|؇6}(Wn '+)`1O7>|'u1MAf6̿,)~d_Ňc罀C`$7<4[jsWt`vJmImb1]B듽X AE( eF_pĖW!@P\@߉d)tK_z#rzJz<)XK1{1Z}"ɽ+%A5cëUz$4$ [Z1I%({fL3u90` Wxf@뿴3s, +K4M'W!\\֐tg8@ ]4upP]>q~8c0SmTאB;[rՄ̂OjS O׬^ ѨԳSMjǒ5o&#`k{B3h# 2.+ $H si?Ehsb=yO|C{#u n)*Dj Tﴞ*7wyvhV/Y FпM!gŰVr1ʥ9K]>n36XyE _(H'6w%n!x/5+jR{1T;Pme솲ReqAvIpNa-tyYwiB +O܇JGaC(5A:D ~nzZEf@Kbr8T\;[W?7Y훿 qzǟr1D7ox 7z8ZYW瓊l**l+wCzOMQ՚pyYZa9nT.P\2d9N%?3%]9*Xɡ{uզB s?!9^V{%=bAa(_֎+`mD0 Dr|Y#PNmO^8``]pF2pL;Wi;naUFT5H ,ȃ\>n$ wU/>`/}5$b ]RwB{+Q:˞"{^Z|̺B1ۇQB 1_gx:\{NB "mE X PKg (CpZ ŌA]Y)@b9ƼbF18٠wNRo^N}ҭ5uE۠\'MtO*Y*+?1\!1_pmj8w:>˓ 9M'vÊ!^Z7Aͥ5mGk8``^< <|X M)WqWkmg>Si_vߓiryc9Ɔ' i{bcfk<%iqpQs`Q蓙3XL% kiv:&or\g-ssգ0j {;:Z4cbLiGUN֡:vQHEQX<4ԨSsk@gw>-D4ϳc% J =yavdbo}&q ΞJl$q}ӌs@Ay=# W9Bf>~˅i*' )b: *HgT7mjv&F6 iA)VG\Pi$:FDױ؛g=<<4`y--xe3! 9X,ot]'>dIX@FZ?yOZۏ^O_BgډRpL;BH#z1`]P<.Jxp^CZ^V@ !Ͽ"[(+Swl<ΓW(G[Öv.T9mCIРץؗR`7SPX )cKM-xՙ5IcJn.u <8H{7Q˄.|1LBPzhsU\e֓@MF-nx7=մ=xNRB1̣NEgmz=`:nPڛQ!PoWT]ቢzn}\d^b𠭨+zFć'%K =?i x~hb^o_]eW,ub̄qҫ%3&T8q3X{(TOrw̺1dܜkQ)4ޮw6ϲ救qSμ@CGO쇧ڶ}!3 3 2z21bmof ]H==40byGrrf2%4"7ofyzMO:2F9qDr_i1#N~+tb1lԈ[\м qڢ`\8~f޼'gYz.Bh]\VG! 3Y']pMkNg6"$J]&KlS^Ph]#z U>i QPuyG c>i G/VLVkOwjg⍕Fv@[ ljcꎏ7/Du & ~ow+4c@0ax'N*vU;axg]oP9 Fَ8P]&U@ƏU.[t3XVYچgbLjVd` GF5v{3u b kx;3TD6LZOHdg{[-Yhf0GӨIO03ۈģ^>XiX;#_7c4| d +n( 3)E hy 9*8=WY\ډ*`3@xQen#>;SX`1 [FO@)2:400 Ya@*\*4a~ O0t<XMn gmhVЀzNPqd?G˭5֦AoB l?ͦᱸYU\0m``5<ݬ-dnr??hI@d/*=v-2>g띶_+Y٥-(u5)|La |H 7 aB2r:ꁠ܈#s#h6ȐZC2E@:s&ȕ;_&%9wרT 5oYM0:`% OĚ$HoA'nz56GoiEW=L1ڂl&B|һͩ 3PجtV |k6rH>T)4Ӳ') ]+S=ef稰6U b,3x]*"20BSGPzgTiE@.[E`Kc@; /ߘg JJ1:|PM7l0 궅~8vJ p9n)Eܛ@딖E_ٿC% &k1'[V\4{B!7xGrc:U7S-TF.Θ*?U%nІo<[yP9W3gJR1';;m<-z{Z'5-%vt&G h¿ku)5GDTB1e6HΜȫ#=8%Y+]Wjx>ZxWFU2j}RAǽ&2e. γ:<9$Hpd=K v kKb=IJ' f]s6zp/2څP.9{ʕv8syP L 'X #$KDXɑì/z9?9 "x) KzT|@ՙˤ|)b2tHǼ %ӧ1{3, {[Ys7u*O\c]k׸Mx;aYC)r$ 0vKoB9tu3Ⅸx1Uxrc|ԷD:urwuԧTGH NgOK]ȲsX rU H :FP찘-0s}w3oxNuf^\*{V_T̞2#ٳ.nnnfr?_8#_G";pp4XPAY3"'-d^c(8}YE4&GK҈tB6ZF[tc~g ur٫[>7z3ȫiv^`R0_Edv/wɛ4♹N^zoPe%_}] ƍJJQ~ nhDNs9c*{mUȜ@,+fȋ_J1Bd=#,*.TN(_q]扄s;CcA3G"\~4 eb_@PexM4} zt^Gϛݵ*Wy70Jajuwu=u0k6RE,%C"0V[+".qc8s}3&W҃m]T5^ňTU8ť";Gǐ_d<$ EK% rxmz'; zfrq<:$MW1o˶v^>c H-̢`8{ es-#=y5 'Wkxxp2{{r_ fLj4I(]ҫN{55dE枞 ~ 6^t}blwib8>m;KjkFFϘ#>ցk_y)+Ze<\c'4 .1sj;pL /m R +sU}M*{2eEDoI''>Ds,mGߕ3=\s,(v 1x0ؕs2pZ0DN +x{VSwe}Ͼ{ӳ 3V!4Jbfe%\J 1%*Z$+Nb4FFl0fe߳K>tRɌaNϝoy~}P,._m Ye^$T(P!giLgkɖ94I< nk13˫zvr&ȣ˔U4z*K0· m4?igolwfm'I90b<^p}''¬^Ͼ䒛0HBqڠ:AY"[/N#0Y7H$,S̖V8pc]t02A1д>e 4eV 8Ʋ@ Al!dXGhք0ʏRAoLdb 0)k.Wܽ$z^2^BhsB2,]`9ue6@eVIݢb*ĝNػc*I[v˵Yu&^M7`baHܸVBѬVCi; [#l5Zt1' q捐]a::0S`k!LI3@8c4ʗ A,P莚L+>$vޣ&لV!옃qU:-=A,W0xaYC˹YkOCb`,&xۘqhQ"J)S:lm,C|5Ǻ+u6X"5oh.S<6V'1']=e:' <(jzNO˿0&aEdS#jm="%TZg)}* y~~< /B7ukG 4s;W&J'%NT#bf2T\o}.#,q0JZ*K]AwL[t'76x <JlLeQM%5/Ժ>3, ̟nqg;YCw$0( h33Nl$VI6Lx_-bb\ZiZnuab01Y4-Az-_Rq*пnt:(c4Y 0'ŒXmix:?$yX58XK0 [sgBEx1k`{55~]p(z3Ԙ2y*=߂8C|Cj% k__f,3 Iv[- yXّ *7B'eRp)^&BTJא 'KZ=y)s%r*-e g]0;X{Ǖdຳ T5f)soyB~usYb)4ؠ%L.C(6M?ݶҳ\D=#b}bȐ2<-.-хE딿G1D&!,V ZWFeǜH ÀgBIOp1__?S~F/'vDkߥ; "> cdT"1m' -7դE?x_ン9/`;ynVQ}lCۜ,TH+~Gv tq@HS@'5Z0m,jɌx & b2&EdJr:Yq:f1F d. N9ǺjLj8fdf BeXuv7w'3Tr%l>BjUFjңL}j`5M) p`Lۧ t7>A ރ3d= hRV OoY(x]qX޵$q 2䅸:,t;T谐ES]c!aL3C;Ix&v~Z bwv eVZ*C!,+:+tEui!`\@8gJZ~*8Pm=l`MIc,)gרcҘx$*k&jMWggTaec# LK{jyƈd¨"=7<nBC EpO qNB4( Uef;9E056eE'[ד5pqMO\N: S{]glg nxxEn-H AI!щ6$1n[ {f7H.RBK45Y2mil.@,[ϲ w2֌IP(ϳTC9XP1<БnKXzXC|6޶gIkL|ygL8P[6; 1kc@79 sCn\'6Z^Ͱ;jA0%Ҹ6ITgh6ܳ[0vVΐ/#/Kt;(~EJy*$hI7lo8%H,6v\x"gLAA-[}f)9v`=y{`w Ng8 ΖSxr1}z~[_jUu*/PVS#<^4{ZǏTgIQt#-[\2'J)ӡC" \JIq- `9_)h?z!uʖ<܄^:tU?ldp~~ݗS},]{;yJZkkdoT2dVggdaF:ۣ~e=Mvbx;}|^@? y( B`GCp},UXTaC2yn1.#krǖBAoIoVwYjwiqCg7l<6і)k gϾRlX0O9 -h&kUxJRmMi,9! XkA3WPzNUWu|-5koKRd{oD7;aVXN@*iԨ@0Y(CM(R]*r)f *V@]ƒs~`mf0#+pr6 ) kM'2IH8fyLʽ N;FBL8@qS_pV$MVR3~Ak)4X@Q&2b X_g|,3d|rzeNier_qZ{Z{E n(HHzٜDB ʼcQh)jJNNXI;.]-ZѢ:?"V!Y43)h= Эsk1kExXw{ƴFT -s!P^foSnĚMW2sNZOqߦQacBlݨa ~r%Xg-s׭@wvU P,vL) `cqmRNlmq#h5tMz +sh xuyv3ޙ$ḧ́1n h=v_Zj(`9=gr g+L} ;jMcY#xCy G= 䑂,CYuEHTș8@s!NTtqّn̯{b.I2o5YD:=g _ﺺN,,>5چ-{4 P' aI$ Ck[q4|B%k9jC,GbuC Q$p͹!{xFX_=]Țΐq=BA<;@2VqqP kPBYg~ZUNto*|򙢖ixץ[eZGXQ~L]b g&F ē&%E<[ЁYex^NV7]wݻ{$7X[8Yo2S,wt~`h(HL ƩcRR_֯)9ʛ#X9Lla9bhY7IW=\^-֨0aI KWƐ^1p4IMƙ9b>MOu].ldtSҪ?d|iw4&r1SN@.09TXB:.2WZI`> i`My {LLsd\X``˴B'`/`*/dM\ӺxO;LGIXo ƼɗV!b/ekl옿5Lڂ&L (9ڮC3 P7Aeb d\oMa᳠B˪Σ4e?Rݚḥ.尴!?,#\w$ cx-mșֿe @q2frSF/A8>βZ$mPXIGMAyjvDkN(&|6 yF<*$ LrB3g>K@L*.vsdc*֩#h͚@;4.c<+'kMc9`*98@ M̵GAc{~̰7VpLjS:4h &'.x!UԻ9q4+b?*gvl>zʽlB+kЭ, >zX2?cF"\C`.al^c^IqY2`IfkKuօ֝&\wᓨa=rZ Ge_\F}3Pr݃/ b z<N`[lj29ϻIyE,8iS %ml^`Ī*FLrZ 2r{g?L9xV4tl,P,a17 y:<.ӵ M ^Rn! nalG.1|fX:X?0b=c ?< "Aֆ Q.Arnn'.bk+%kC.ah R˭פo_9(A{Iۥ҆ `ķsv{X#qEx"ϨD7ɯx!Ma8=9al%3}gAh%9{b=9B:¯8qq}= S3Mu9C\ 8Lqmӟn$K~"k%*~6и8L@ThɎjJ$cь@Ţ;CGN[0"G(j,myV RsҽؘIl@ĺR\Gʞ6<SLpuX ZA8U =F=3\ǂX=C3vZج{)ͻ;^>kN?XPhE.R"KN#VD cd.r g\8;F:~F w B萐,qј%.xFkaJ(V?UpVpX]PZ `BzhΘ,`0XCTN@{xMj &٤{9Z-)(\=*. 0VCY0v]뱀?࡜SUw}3QL+A/,Vk;xNi%=PNL 5e<622Wӈ1/0TXE40ކ4ex>)_Yt\Z)jԬ˔2 R ik gs/􀸮QeԘ2#g^k_VkK;0P4$qsV}x0o-*n=C>2;{ HxJ ;LP$N-wV‹1a;fCV;ocw@/c}L"[8W}WAW L{DGŸy&=oj*v$ECM(aUeWS!&!?{zƥG!R_Ża 4Y"9|%p| J&`z4d4 jb ;dNB9뚀&!t/o:|^(V2}1ٻ| bo˹+EErea\͸40KdSEQ@I\^!uxC_ϗ/b{ <|דXg,:7J~ɗr_؎87~-[JbmArk*djXl&˨M|ʥljehأCg?w%bmѫRk&-ژhZ]8vS3Ԫ-'sg!QT'3;?!@v9w84R6:>owO g]@ O6#:<^^`'\6Ÿ-Ru5\;X6/F^ @&NucN]W-!fDXw_%E 2 +fQrsGbhȐ$ag=@BffX# ގIpuKxPRșX-7Y2ub`Cwksf+! z4)bqzjd5\q[bG@ٖHOձ<9_-.Zî穇L3t u*4Sw R,*tS* YBƈpG@F Up>ƘNCB`iehgcj2&\DX #ee1FgYgC"Ɋp,*1ƛIs̲':4U` @m=3g =—n &qgưHgxCeZ,;&5p=2z*\=%2X{ 4U.'C "u`?`6t\g f:4pcV^1Dx5gk峖wUX*kίZl-||_c#& v/oK@×x6r/~aɵ;oKFwsc6r\IƁ@ii ӋM KMV`-Hp {qr_Y2iuӭsLC8lڅ%#9k*]q:Wkv;;qRj4Zݽ lQɘx:FX)"z0:1~gL|y&̲Lrd/x wk0d rLe] O>o8Oz?]a6NЊ.f}Q_&ɘhD(g]#Ph-t'qug`Vi&Fw0Z3/64tk4($ uynqq*s\&fW#(/K)hDG#4r& fXa!3K)s \}k4U$ncsR}ՆCs.>cL W-bpÄIp]8=aEVrT d=iht => vi50(.yεBeP|E9 k^pڬQfɫa3tp!$AI-<-Q!OXd\(5*y9-vL0J PA<`[7~88\ƻ#f8mMӊmvDRg69 GIxqZ=G)sy./1Lc"1? V/ǵ(41.B + M*[Bި׳>P\ ShS Ě}xb&b`-Bq9?@&7fvܘJb]%C=RZ^imߜ9P4}pvC j3tc}:QPQ7f3ڊiNCM1fA(rܐV  sq8M) c*ܫ^Uopݽ x'b؆m7qXM# 9YCBӎW2ε^k5zE%=ׇX keɥ qJj i?;fbWG9{KzǬ|Yځ ׵W3:d3'H<3YІ(h(ՆZr-y75f&p:uv#e -?S$O֭_yXI<쒄;OQu.{xxgX0{wwwsJ?֫ ~3>oom2‡'Oտ^ʔ)iXT|%VS[7gIt\&Y a]58)c"k(M+X4Tau̍8&^yė ֒$ݲÐY< ,C8:g ZmCM]0o-cDkFIkp'hY@a$!1'(1.X9h:Sz:V0`cUu ڷ2dl H [*Meai}Uxq-k#1ra$v?/-M qAn ТZ!E?B~IeFZ tjZ<0.Xybv77nh-+5`  "Z[ZϹ}bAYg^ard!-Z?0Kk51s^mb PKkmQvZU-mulK`n _fqŲϑ>TҚ*h#c=$u:4 8Fs,&~)Aވa=%ƮB27%JxOqiE ;mWn6 BՄ^3tqSaal3%MҊ  ]uKG6+!]iAt7U+0^lcVhI̭ ZV;ZmfbBd^ ,I= Ьܲ{6̀w28&Aa)w9(mfc<ghP£B἖D zL z.5yf=\˲~<CW̺-ǍuP6ksr^n(*p؟=.ݿ?|''{b$ѭGq؆^D=mXds*kJ*VL*eD˻E^C VxfK'LhqIJ]TpUeCVFhXg}yiUv א_XkA0kTm΁J s쳖P*|NڀVOp>^Ιʘ*4)6*֡`(FZm͸v~tV(S\2|0 G eZ98E>_>Hѓk n &`6#dsNUU֕>\*S[kc3?/({ O}[+>og>,?^gk~b৊uS֓EnZjYi^Ic Fy0syw,3B7'_~~ Jor 5@IDAT*730 {>˽aN ActZ{SX;z.PpwʬuS؅i;kYfg'5-ͿV`=ʍeޔm=,bӦZ;`8Sib5eZq g=a(2  _-dV 0 ^RJnS5Ck2?A< 4zrR$EƬeږ,Θh VA/՝i*G+[$ \;c fT/a*5JG'LSm{z Sʹ h) TGi_@& t. EC*,2ZF9'cX=@cD3G(9waC*q~:>%goJkFr*- t8Fgqg*lº ֹ%㒇=c ϙg&!4j(kCdJP(Azyq?"q8sg Ғn?lT0C_٣t¸蔮|ڳhäd sCl}d]vb%Q#YR#R)l83B5 ' nЇۇ5&ʯ:P&, 0gjO?C7OaS6>A Q0N>J'$AFg-zHn8Ml-pbM,W8*n/w;;KϽ <+e붷_}|)F& w5.[L0Bbj/j(WFQL!+ $ٜ=V¥kS*set8̃ b3'izm^g kA[uB`G{ n X"ΙH`p."l̛ ۩1 \![!JJ8>n-$ iUCcmt"-xPMAn-u)@Zc;Q?N?JP sGrl:,㯵@e PγI(Xٲ 'yge`R^e~c# ' jtny>(iٜUc"V%ba`ukӒ5'rOu@NFK@Kkez~5|]jYk=C3ae5,Z *;]$4>@߰)o Lsc{<Pe3d.ZZ NUյu=K6QDzCJX@Fl6 t,<x 9qvI&.,z.RT sP(}",D[E׳ b1Ltڌ EKy&kmnp lO&X,UT3gFv5;gԾ`LcU6Wo q#ҽ zUrﭡ<@:VG+hu)iWdRZU5,} FϨx"knj,QGV9xLkue(s O3z~ h=##41}_Zd%6G%=qUHhhC2%1[`guQai`jCVLC̺-[MfmhyAYSpe3?Ҵ}bchJi ]5hS+"oǨ&VB_ZXP<#&#Vyjԁ^`G`):S|zR+ -dƇ,20iQds7\kڀ5ʐ$γbF+ 5ϻ(A20 a:IJ߅M] @~r+P#k' rE\ьswb9)(H  qcieeh7Yl\lX$WDh]dBNgb:Hr) p_U;yz7HJ p6^Y(e+9 c,L\šZ'&u [%C:t҅%zFAZm CKt)=hz7vpU\֘!K " S9ئ:s@r*|jo%@s$fy<DBƄ5" sBXVGBg4`M4D=yXqBYklXxE%'f1vykIUr/-*Hsz=<;6b540sș]PGR]복c]ݜC<%4fWMGqҢkr Cg 5/cDoԻf\<&P3+Zr-'B*( I3EbBzf2XOHˬg{{8+؀ɽɓVk]X5FZ0십#(q+&?7*V6˛+e6 `:1hY0[ָ}C!h&4nij 0Ɏ\;Vu&7lqԅߧ8#B3V(pȴ-^y>,=`4LOdB{لA[L /΀A4 M(:;8yy=m{<ؤ#R_ig~ZX,<:}-q03{p[S҉b9djkX&̖!$ǁ_#X&@C p6A) uu #4 !sit[U srF8Fo/֨/] ,IX?-Ѡ4RmΛE2n^{2)@="Yl`Jm-x3)ZY˚Iu}gf"WqeVpAW* 0 Ts,*D$h eʹZc5Y49`˪x"559^0pPX(QZM,˗`\*N2@pa*]S=d\d#ck wj+0lrc8$4P.+!BkTf1& :!-2nl½ EREA%/$:BÎYKOC01[fеkջ"y؃P%YJ\>tUV0޻QF)6巧Yʝd?^d_uaVr 7j)`B RZK%I@J/A eVb!Gb+`4ZAkGX$ӆ%1n 6J@63 Z` !GhMDi+RL %$!pB}̓qL? #fڵ>fڔB׮Carp9!AX 9Cry nmUwV)t9rASXFH:6]s1vZD}[&t`xj3z1Z*H. 4929.\ k"pCp:(AslނT *iu x}c!\v-8+cfz,.փ2+v93K: cf55t߸V\'.xִ اeM? _Qt!=b2%׺5 F%"{z ,ɷVx ^ɷkG9/ gҮxZm"(PQ_ckSyXv4@C! iC *x ҢoS){={7נJK*&,%ۣ=֤}N>|DJ$vIT,mTZd8DtLb9v)5W;wתONxw# #DϔfH:aHq?=K;#3k+@~6"7b T5eDTfdžǰ3>k<`,B2HwQL4 bBF5hJj@P\J}d_[Po}!Hŝb/W=V26XT&X,-ސ@tF4{4tɬD0b ~c# 305bCo=mYƽ! G0Za YwC=e&OjM=9â4of \@匎9#F..Pte,׭RCzh_҄nT7֑ 2( ǣ83xUɔˑŘ HSzV[ɯP}oxO|ͽ¨Pb"}0@clF:'SZ!N <}X sUE$l Gherq5R`5z%lC*8&<l(d+njEܲMr,vh6vA6 pCs_@NW~15Â84 ,j]y2@/\V,P0A+&a<e4v*x&VF| 89ϟ`$f WOҢnW LB ŰXlt*Z/aX9+= Z絥5Y0^^ص5ɞhͤw% a~V?(jx̓0m1fBbQVDq!Wl&ʖҵL0ȒZ9Q)68܂/[KgU3ϯ(#48&a2*ց%@pP ci# [6֒ek+cb.!~qf ?e.gcF2ۘÈ XzvҧB0r 8kkt v(ڗ INFlZ2)_ U\z+CZtM"ex5B(쵊!S=4p?Qp%<"uMa]AV`E/g#,u| K$<Ճg 5̠ e@U"VZ7[KM;8GP|v # 1m x~ VqIh#!r]ʑ JJ Fϑ\-Z ZnQo_ե#7lAʅ՛d8]nO|6,Xa(Xe:^m K8גʱqE&]jlFbs%{\Ot'˦6f} ]Es,@ňxU@,dWki[xd TKA?2|O:!m 8K0ok=Ʈ{YQ,tWbL`k4fHcI `,sL4>"{  8[ganL {5E\83>O/za)gA;eU=U*[yGO9C[g[k9Tq+tPf,dudhjQSyI6kci^" 0L1:oE~wei؋`_/Y7߭SWЀ9-Bvi=jn8I/`fRoXƀ_LMgIAm,`ȜϢnג?aDr9|tYO S>bvX%b,[L{W7֚¬/E2\Og\^'~K. 49̧`1-'`piz!,ZNt `2lt C1yV+ˢ4eOba*(iia`Bf1XXMN}OTI]ꐱa Һpt]<%"Y ÄCë$MZA5Z,E1$A*>Ĝ ]"3[= P!N:9'<>z,h?< zɽX>4\4 "q΀:it+gڌMAz G,ӝM\T, =NIK&U >M5L`i2f eTPYsJ 9mXH#Q*FZYrj(=')ok9hU,,瀜SY^YLj%aZ  1!O?E`y 0H89T#tXH)Rwi_Yy _?O;#/YV頒{huJaWv5S-H; W/@+quΐB% O r!Yh^&|c dږV`VSH0T*C ږƫ2D蕖h}~g?O?ݹXw0m\au?xo2`d;=|;n$W(DBV)hcrRaA0^yP5\zl#'a ݃n7 IGVAc$DmW|duce:pYX0` /'` s5c3.8,ZA60| `2KX|K4pÐT%3Ÿh Xx&w P^$2+(XMðY?A* A!mεCd\"\֪Ar\x9C_^Ǩu0>knf f)"1L] nh17kj^" X.R11.S r]0Zua\ V(=3a~el t'Vog <5BܾZ+6J1Ά#Cj0OgߍF*ٰ"-~'lT;7̥n>ײwѰ>2/@c4AKp2`5˙ȶOӷl$+$fKäN IRIѠet6Xs&JOV=iudX<.=K( ssˑG_g̱0:!v$@l-ZF -gͤE( 1 e3N{u]k6 ro +Q3QU|c4gypLUKQoETUHX8+Mbl X&TpT!<< VƥɎdKhzc>MI76 1os-(WuX5}-ё;׏IK3~C֋ϙ_T`/~i֑]dd*%*.|)u{  #$x ֐` 'Q>>'ǜchى15 Q~OWuiy+aV޸37ӞE1F"28%rD1i3ՍI͆9k>qy1JMr`ـ%GQs@He1.}Z"@LoZgوf'Eo !,0 ЫZNx&iofƭl07A XZqVz1|!vE,)%\Z\E?+/M˿}89Z}ML1+ۨHaoR~[m^{ޢ` j??G޴]EwȼluM,g'#9/29ٱ,Stk=7N( pWݒ,^H=r-sJd B3MXC Wh0>$ZzjA:kj,'[/=w(=o@ "m 8cN=_u+oeqLEjgm|gCIz(ű}P{>Iߨ Var!=IC~Ƶ5AB/k->ɪhwL R&J^rk,mu煕?v]JLVsi \JF^t!d+&U>1h\ QY5$uS׎9X#vir2o* fox]LOz7o&k$_Adic PyN^> yWB(@*OQ;8l{gzwrtHVQ81iycj33b )7Єf0kqdӗQR྄I ޸ y+&T#ΌxwZ1RxZGZNW bOwlFKq|_3uppQɛfM*Dh ڥ05Ã%>x'S >9DDqe2Z 0 P+`+ZP3%xwUʩiZi6ZI[Vt_ny_Ws=1.mL14j"DUH`  B:#*ˢXQC:UwbbGuE. P ̸H&Z | )onA,ZZ -4m7 91V NE"8Fq! 0+c$b?Z^[0fo%0ZSKŬ㺓&1nge񖊒 -f&`s>&\j2 CêU>֏U0kMr XoĹb:QFq*_9RxS*O͗I"~b9X2Hxvsd&ҡBJ*1 ~p- '0lf ༛/b̺ƺ3ތ*`StR%1a-zt̠֨;B3q΋A2cjRE/9XfO2=~g|Y2nZ8L*g# eLCqKVqR&GSQp<ɣ_4F4 Zi\#<") 7Fqv A'Q`k\`n-qKm]sT:aSeK-lm^G=ظVAG63769 ')B*H'=G% a5g>[-,*bY?Ÿ(h@]sϖ:. ϋjBhBb]Tp=;iAZ'z 7c MLb|=yMd/?t ~/; oQEB "P =`qAp $]3pneJt % ` [e.\yp5,$ %ӅhɭHߛoxT|hۣb0;–9Uxycy~\(Ȥ,)EڎXqNGLj'@C!6-3@BHl:"%QN-}}pf}rs#N0 1L@9`t.1applvua}񷖻 LВ\&5pi- +MW,nq"~Z`gi >0SK8c ,R&\bXRñLiYB*0&xi,В4#1F`XH6WVgx.u:1U(DŽ~LSAYH#np6cX׈ymi+F!uON(Y2( V5`kC\B0V!(TW1+꾺O*}=)v}pPb|H+5 g[@m{<Ԓ{\9t5h>c]EփS`߱"cNgc9^mGj ۟[eB:PUְZCA)c=f\8Kjށ;a5XYp2 3)uiF;ĔI:2X@[J {/AD CA@`OsuZGa%l (8F9a,&dz8 څ$#QK8U.(p @(ѥ:hy \7Mă&Ӽ;P eP\9'@AI|R֌iM˜!z[ ʫDAf\'Y B>Nl4YlvЄb l<; 13 JŸ @1gAZ; 0|dDC~I\&7ьYJyq CI ]s%MhPp2MiMX|V@[L oO01~һJ3 oȰf*Q #O3w/Fot 1<4 I6iYStڹo h0b0ɗ(1@IDATTңv?stiy3 YkHb ]گHXZ;VTX IJePD%ydF EW60%}i жذxc ]G!^&M?BP(ZϷ*ѝxtkL 8ZcrKCc b32,@@V?BT¶wvuemZWhm8N{p<St{<\W0׀G9Qv~F@!qoI#H>maS +(eEx҆ <kOH`3,J6VR(nkWI`tM<,aañ be;\LƟ ({/nYmw$ *cP݀{[sYw<Re]iZKy.XJgA@ !ͳ B EڀhM`WwDf 5.K P =Hc@GX {.r2Qchu '45*8g{ Q}! ƴ˙Y=7<#,DŽ2cOP$!c(Pd^5_YR XBmbhn@HO#t$P,1ĹPZx?*yi A:or+_4^[o Kl̡CyF-ż8ʍ4k.L-T߫Tb'ϠUh98vuwuN© 'OQ %b\Ň,޵>0Pd83DOi&4Tq.n#,E\14HK.ւ =9 ?gNp _=ˣ8<c9?~$ru3k1Mtnx$%k9S LB!%>lv qqxɴCGiU C#f0!GMm#a@&(DrO0(HdkBd!mg7ytcGD\+,01>}x2MgX3{.qy&A"D˴,0Ĩ~cWJ-Z詼F.VqÌ%=[6P~;L5ug^%[%i d!AwxQi< \K< ~l ) } @ !_tʷgg4UNеV7 {—g^>^zaJ5ve.aVԚ:%GϬ\J+#׮Z}-cUSJbQE% -yH*o3w}V kQ[fZ26<2IV Eq 58 fZMÝNckk8Pp=¤,3fzg&歒qr&0|羲:_- HXW B;㼝gbYI-+ޅ@ЊdqW"?(X4 Z 4gi`b y4GaȶVkŰH}ƚƜ[[E3ꁳ Ŕ3D@ 9%- Ze8gEcY)vD^Bbj31#֚0PBOF.䜱[e>jP5K._W[ph>ո˺hRYcu  $ T'ݽQ)ͬenFX\Jaƪ@CI=wl|p +4EDEW|E *<*ʌb+@ gI%LO~hZ,w!J WZ2Xճ$pԛ[Et[]x-6P*/tGR(kvsmFXqrՃn<=*?EaV[%Ǣ`_('f$08&qZS:?/#)0 cgQEB WPVT1Ҋ%`R9c 'InUbʖ4|k+y$2Wyӑ~F(z`焲"V~*SCwq3=kmiK.=4{WŮaeKA&ZYIGLfB뮠quAAfy.&@Uhz֞58‚D'CWxZ)F/K@F=Υ;49kz`l;Bɺyf^$U"v<ػj_y] RuKfQL[fIKب瞎,xRY^JX xޭ]OZ#\DX_XbĮ9kes_8G-$-*:qZ}fGnû @Id$;a]aN|,Md,ӃRa@KƝbj'o`ǵ}~LD%hXkte'm!+s$Bς๏TU!{؋U8-cuah[ׄ5ϡ1)q[~p;:xRߥw> `c=igj[q&BExTT]1ZUf6J!>@-CX՟KH$2WCh<<ʼn-`g]:א >ņl\F9~#[nxE)yGЕE nW(: zXHgZ#,Dkl(@ZO,*͠# E$~zo吏]Z*]ctkbr.Ssg4<=̣1pҢ:tdPi1ζjXɘY'`k>/(+l (3ւmT*Pc62ϸjD2@-~vL+.jC t%\( 2:R"(f# >>J|,7ƛkm⬀[?[pf_+َpme^Vۍ~L0|e { xkyHvZu@Њ҃XL"c17N$\'{}uۀz^7vyKu@3z -%l`f׊XyFfhps6p]n83k$o)Yǯ:1JW9T a0:U7Z HpmIT4Y/B>=[8φSi!t%*AJʇM4,Tn7 ˩ힵZQ#P\|5)YDYhBAe9,x@سjH?kȑ-UTج?̰oRcQpigLZ6**8}":17&$`-*ku(5eleh!k;s bMmcm3ItP #ZEa\Y.3W)pf +~ijyޓ@C4g08OLB>3Kh>>ذ] uz yCKw|*>Vr&P{PREK"Ƙ54?䷾p#i{4?C٨GrJg=dYJ]ucrIJf*9E*B%+Bi Վ(܆j3U*ۑbG~س(Qro ÛV`ɻW!:V([]g}MBصQ阆f.=}y6{K ဂnZ2$ !_kXdzmX{O?Toqxďuկ~omaWftO ⓪9ІNsW~l~{y]J|%l,Ӎr^0 檵/F% ٪ZRz`x1e Fd7][R9>#t!,ކcA" _K(0YA H *6$DBh c]+r,w((Uw]h`!ǰka nGZI-n2 F~Fע#X"M+8:fSGro^)Q-g?a!ʏѺ )u;lO-K Jw}A'-v4V8$Z r(s6NA!\ `_e%j< A!/>geZJH +@G&Zu)5%DaumY:IP)ܭZj-"7 GZ^ Re: XRQ$FxjqRv]4F|eֲvo)!>,]Q 7֤! DZ29H޶`Hf}&;ZԸ-ZhT(kvBidžaErb8AsNBjEsFMEHFQZLUz 䕞yaZISpCIZi< "aǮb8"BT$qA>dU1sJ 0&"pʞc^zzꔷ}ï#l6%ڨHԮ}3'(wk̉|PXbI*m^C\uuYiQn+}#0&DO>WsTfh3J4 Pg١r/!$hO%54jIUK=jxQ,<2*'a`oBXadq# )r1'2|𻛷:QFst<`5͂o~n)a~~^4N.?'L޼.)H́*8FD<+ gӽ+'.fCNdT fQoK_z~BYiq1~ `|3[qg^?ŋy884Jˏ7q쎕 [A6wrL2d0T< =<>Oƞò5o,$k4XcVUpg.@L@few-G82>2XVijj ~ ۨ3ƅJg+x(b͞hA2г "cRE$4ȊDxƌZcIǕnr݈ܸʉJ-(qP)\o+9//ckQPVɪ- R?2e͇0ܠGmse)YwQE9<((cYo212b)BȂgp4#³,?>P{@gG? /xLYk& qr`L/2Is \#->K"1ZaږD`-!hMzA2V0,_|Ë[gߐ{l_N9Y&Mv|xIHn@FEf MCPеX` ]09쬶X,]nY\pw6Ew+V7a~ĭ"]ɍ]ot2h)ESXJpr^ZȆ}9w\/~V2Sc,YZN#,64 Yrjap PD&ɨ6Fd.Wh LA(?l9\ߡ&, 򿰠S! A`\ bV; &ha|a-cn}F$K]:X¢5tI"@mݳvg@I3VL7`}z iw>ϙ$#kz ZޱN: ؆c-U\!pbaUv,g0?Ϡ ag͡7 )+Ŋi[[F_-s*@;'d} ^2@pYnø>5,~A7K,ptЀkuP(7a]XL7`{]JM@V*ODBAk*?J%{*.oڇT.HɰD z B=y2zĊj1=>&$\[k;zMd6W "kI|zM"2"¶s-VBee @KhOiZ<8_Zؗ0C] ePyͤ C1li&di(7fy[Ѕut(1fك[̝j:2T>Wy `[LNo%B'C;l+ qU"l"ac_Fn_+W`?g_zeBHu34?MJ4WҐq!cSx ?4To bjG7/˧'tr6Gh;m̞95"| 2n&)">2o~ӧ!\ و`5|"ۿ/[ 'Yy: nƑoj9!.|zk=|?3^L[^)멋ePB@풄Lco#ŠJ,g@`orZiv^X]dƻ]hoUU!֙:o^PF8 x*V&q vS,)CFmTXz _ *EpB-%mP d3ǎ>f̀/VzJژ!Dl `e3 P!kTnL}RTFػI=E3jMhJLEu>/l]]z^I],jB\P:MQh '[syZ%B4uJ$⹆e8'ȼ cOte԰9+޷D0P&ɭ|1XDU(pgHYknb酿4 ŀ$HV7dP0N|͵z3B,*TGK"_$e>W\y 9F՜W3grM?/p#:+Qktc'g9"H^|Z(6>3ձ A"~=ѡ=s]]ϫm-?ʖg |ovqͶ:R+yh$Y3!5yiqd<t5c%޾(۸ՀYy -r9a )Y x\ ҢnffC1 3+3%t/Ό"= NYγ3F)$s$,ET9G%F%[>p(΃CO|<%'fQ= W+La)Ϧ}p`> > sGe=/^L/oe^zu~!(D1 d@|Z,ֲn'>v|Ѝo_N+'}*eI=-VsLZӂ)@==FeceK-DȁVnBQ#@ֳ%xpbe F&㾚VYSKD qC鑬F½OUƦ"Zekf$,'0fQ(3R,'Ve$d`W¸fG9t3]\ `UB`@EJiY!A A v+P \<U%,ރwbOwV]7.ɝSXS]p C,z~Ҋ , Ѳu*ZLĠeǓHr"a~]5ZIPz& EKgo0۶!F+c(z%ո&V3kIC`=V7d,f niҰ[c$+w 2 kq+v`#*8r~&E%xK8l[YdF4Z:*SpmWMkdUy0y_313D1aMFi Qq/]1f|/z{dk^`ڀF S$N4dRS'ãl\$QܙO!+>]6,E9U:$@so ǹO37y)Ig(c5as1@{#!}ã١*D5Qm$gd=0@ԣL=\ty5#"Ф9Se+BF $ek PkZ@b><{Z0 E@a𗷮|+CT҆S*Ubqo;]@ӭ‚F,|rn2B8 t, KI) 5]PnG9.# @S{gK ޛ{!FȤЦX =oOߌ2:t_o4k[ 7aЋ7_+0L爡`d$Wl'jrQg($ζZ_SYg Ѝ3#ATn++k/_~5nKC2N34RWO!nP`oꛇ_8g~epl,Lړ[s+*!"!o|CQYY3aaWex)%lHE,?Q `Si]U&Z2@vDeR)|A 0\&ƻJ FgP.NTd &sff6!Zs,ZEDC|=1 fa FFƱji-0i6봎W8qiA2MI \4FQanl)Cv}*ZcJRMa l H#Z##Dp-ZBkloGus1٠%L>S93¥$ Nr&3zX p/ww qZXN yPZЭ5I&} +c69s/RzdG`lDPdU$tS0D(. =,_VOD DqeЌ1FL -<Mi+_S8u -3xNTy*Tc]j$/Fls2gnG699{ۻC F/́ g+xroAdYKF΀n +Ћ1eL*"|mh7KgOVm'ȸS[Qك)d%ò5rZcU]NnWlu1VƲznY'JҖŽ6˪Dlw g%XĪSKxIWCT89JTaܞh؍K\Ҡȍ5 #֍#s]\2(;VJo)ׯ֋WÚ$ZBQzztCH'oÒ%@/a ckd1 PCL(I A&vP1tEI5|DU;KX7 c"MD Pɚ'PV2&o8Z7φHe,ZoKf8Mcp0}T J*Uj\GB.1rQg?k|1aDqP=8gPkHݸhrxx&XOCl^1b0A&ɞ?ΖZa^'֑@)KO.@|'Ze|CVX|3=K~ zBۄ /ް]Ei~>bWz^,S.U#X>c&B(hBͯE^jvu{lVDpo‘tOdZ=נ YB񋴺Q[ED./~jt _(%|-<_0!`f+HCԏ&@l.aH {CP%R$`Pثgr[ z$^em>Oڽ9+;s< hOB;Z ӣu6Lrk%R l;g/f]ӨtMjQ)#([,;뼍38SA!\"m#Y,oE&n(Ȝ8 B>gmͲ+UJ[1A+А(Պnu-R"PkAL-'^{Zk="L KlaY4;}sq1,0$ڰzlR_H"´tю]k2Վ`b )F^JBavakvb %ৢFFsJ :Be= `HkQnMgյϴ҆;j/X9*T6V_/4B6 ) r-zN.Y"9/%TfD#}Z/`pYDhgH!"ƽ%Mz.3YKaẓaD$Ԙ` (Uu%A0*ZWW?kp1͸aZò H0u4tg.L>&iI+ h/⹞ᇷJ`Õm_c<CVICAI<+*xp6W|Ck(dx)^ii@KT+cr"cM Nf({<϶>dPy~?CXqD{ƫ` PV#\y1>cM>cizME CA*ף7r b_؃: H&O=}6b5xqvlOgϵ"tad>4uphDKGtlc&0)tLF cZe3]4eVW >8e4~\>dy:ޫټt8놘:Iko^ZlzHHVޚ"h52tBIarWF((MzBj5I}ysF?/pms[K7GN?6lQWŖ3P=p ]&Sf&7J8&?=l>u2^B QL PL$?ugHHnH:1b|'U,IryiQ, V>(un\?iᜰHX*/φV,C-DJG y[9z3~,&0*Foؠ ŭ̆ağaXmll:ְ(T sǕu\C2*o^Yˈ.(&VH˟n qwd(|ͧ:uA9ah|;q"^y {fkW)X6Ir@q/䁭 ٹH ?Yk\l0!l}/rO=p8{DZ:e̜cոWLPTyjp~& uns'H.*6k|dpTZY̗4/ћzu!Kb-h5W.rV(nbls@B/gZH3$y҈ sH"񥗮rWn`ߎ04-G؄UA5o{ y\ 7vH$&A pxg-Q|/W䧼j5."6J3 ZeN!U D|kpu]2`)`A]^ƝU9TґVp=BptEjqEPnBOkakys|yS` S@g[}DEC"`WpS_ ςg1fzkqfj PZr5(+f[zqktt[+YR`sdֈӔ 8>f:֞6DpH'Sڦih-6{T/ІPK7XJ 6c[` 5QRD$]];**?G-~blA#ίߣ. Y(TYd \Z3vcoyk%' 90Zâ+Pn]cb|v_cDC\랅+2a8|:E~ak*:M@:X~ iv9k *9bhM4 ~,iM\D ?3묷U&UX` ԭ -XnۼCfY@x̙;E.֯@raCƒTEkpք^?#ssyaoDH{c{::}ڀ^g}\˟`{@4>@w cM4%RL=}*itCy\-9+= y<># +6YlVk&HY!E0_Ry8hws6еrqOJӣH泆stQfZ*%bgYupZ+i? NmBY ۦe:*Hlk ^&|X /s[[1s)uIiqGP:b-W|[/wcm1N3]KY`f m K E=@<}y@؏[bgxOQX- vSe-;s Ie)A\uZZRF…G||rk|+7.xV 1,1h(PY3%{!sP'Cq*_fS a$Q5Kr_]y@@Z|Q< p (H/=i0[ ;ym/tu>5y1?9g3$/&˗},oAGF2eNֶL}`Ek| #86&&`2dXrtI=7aJBXNAl{Q p,  *;<3J]΂$^ RLhW{Bi ;E$MZAs4'm?L-sd CnZkgT%a6D¡I7ec"1IJ̙q6$Ngc%(¹ poD)r4= wja?>:G!#,Ah F%Ҍp1.NoA J7Zq RuG&VKKPP;/mЅDow2ZI*mkV$@lƃ "ʳK8懛ת1NМ 6c% ֬4cloxOCLrb%!k5̛$b\tQP9'h!2@*̜E.tU[TXV*bڀT ( mX4ɘژ҅O6pVUAԀ.Qm\h^#hJ20erkEMM 4C&.^Ix~mb_ZsW %`pr-h#Cb䨲w5 ZcS5:Z⡋U,cMXΆBa ĚJsH-By+$:(hԙtV\y"֘ ZAY[z#)H̵4 yG , *^_G"sҡu[ƼZ "C4FfC-o^!0kólyx,~G֏,5yhl-'Ȉ0ng4c&Sx^x+< a|/isؿ/ XrS: (~P}eƄj !/Q:RB*a.oB{[YTJH`^d3w>F@Һo%-%,'S*Y==oX{[>yT,P ;XEkUsb/al+B6"+BkE1+hcbK]H* 󗅆AX螐.<)eWaDk{@fO`݅h~2GI| =`n_s9*xtjɧV!f1g@eۂȇzwg <ó "^(r,|yQ*JjE4ڷ\b=)cփTLAӨahjB_bϪ`5t9{&{g 9 Bn \?19sqeUƞ {IrP9N,V>z>m !SľEkHXK鹰5ګ`A1sJ%LpXPD$;x_ʉroZp0;'PI<{)&!"L*Jk6 ĽTAm;ˋmS-G;p#_fgLrVa r-.سTgV0w4ƒ:&w".QwSdFxW[$C4WaJ‰DBbY,˳YGֺvUvg(|N.c0#CZ 7?\[kCfQ6W1|,``!-)qnF@.! (1Xe?d z y9 .C`k 4l5]3J`EW:jh#Jn4ц$|1~E]/}:˝6$ v[p+vڢRWJRVꐄ/z2WܡI!A-kG b}vntIVg͋*t dKʶIz! &VqbP^٤bľdCCT vIgQjL`}pTIX8M-E-Frw\NZКmsA(w1@g`r\+j'+:Ҩ]n[@?3칐so@yO0L ppg&e-0"g |l$tXRGܸ}RG$a^%XvأT$BJ31F+DN,,GG=m/I^bl7<1ٳ idкj3%\3&xiaoDsT,X/Yƨ}LЉ/>m ,&8CӬbm[=[O6Hl3b} )uoX#eص# XR<x1;t6fώ2k=^nnDNB U6y}̝!'s!fKI8Zxw+:!T^)4+zt Z͚C9 n4xww!9Q4*Kf y 5*9kW/=fE/;˗T.Xs.< 6JX'4ƪ;"~Q%Qjyh$!,QDQ: c_?Cb< U{ $6#`Z`;5h*֧&eqZ'"&i"uL"4=^Kt!#X!VVW*pq /j9EZX, ka|֊$Wϝ^Ao 7u28_= l屘UH5VBUyscM"xz# $ZaȬk3ϴ* "hpaS ^RW REqZ>JM%FOvijulÝ ]s @:{kx&*o󟿕ViA},;.-[Ԏ`Ї64HMx 쭝 Y_ؤo"VSW&\E8_~_2I Tby³M) Y]a)My*v`mP\7h]JjI'z W' k DdûAk ڵ&6 _jÓnKGzՏ JWYk;Ώ;C 'ksCET ixO8+,&49]&9A&9H%l` 0%>z߃={@/t:`(D8 D1I}epK o.KH#7w#_pC!{H8ɗ^/!84㙹.m> f۬ßƟ0Xx0։< tkJKB .z@+]*$,9 !1Vc yv k:v@Byp;]ăaŔA+ E\{u@/&\l@|fJf@P҂ۑ1 V- :1o JuHbvED3&2m[}eUEG!dXMLEQoTkE~JzŌv n:3V.!`"-wmx<4 l@;ֆ5Vxj,Vu)8RM?wpc|ۂ֤%~|^B`{!k3`` .ic dg˥cyTZ- ^s $wNV6߰Kj]Jݱ0!rQ(qRI&3 J%V!-.Rzwi&_,߯0 XgEQ)ɪ? ].{5QV϶PC{eA +XLˍn~e=Bm#H^smgAn.d_`٦A3DZ OֻJ&d QI[Njˌqjx z1ZE%@>VW͹j`Mܺμ_ΐ8OWR6# ފO!Wq>ZM\܌Va/znŸ!8kxz8l%[x&øhuϳDgJɋ_yV dsWPRc buG,4%*s;[(_oQ:uJe*j뛥|61O\ϳ<_B&_OA?v-|tNpO=l/bWx<%)V5b2vGd_(0s-3T݈MV(ņ@拕7_I+1l jX̏|odR  eH, , y޽gN^§`iEo^_[Y3^]#uTK3V`y2Ki&"0V2]u QWKq ]qbrg _ܻ 4 !gsΐܤ`r1SKt[Z^3KGH vp3I (Xš!AzEb/IIJ rT`C!b+Wh:r*l;%jcٸXH ?+ʥb+ j DV=]w;t-־ፄ; hc@:q>u߮"PH2 П6,WNhyٖ{ګ'ǗYIf0 ZB g!VN60 5J=0JG#Oډ'$RK}02y={$6V[;>| vw^)o?%(L*a1OM}6+ݶ=:y^ߝCw ^pxK-H7ɸbR<6[2.Z}yBYh@ DѴvgLpC^vǷd9n*-$f\şGnG)*b+%B#D~7<{f!M Xߒ;(aT {38ǐw ?8eYGiE:Hn(JFp2J=3PCh:, JS\ Wsňy)'m"&@4򙚗ึlx/>tӴITAۃDPW2k퍟Ԫ89Y¨b8/@(b$)L<̥`4I8)0b^,BTTHG]Uhe ԰! 401Op;PL JF>ߤF.{^XeM-p|'z]}Z !\4H+~~#'[*̔Y85䈌clBs+g#g(v5c8X}b+OC( >#c?7DI)vY>kԄʡ.V7"Kcv'Ν{5=5(' 0㿼!Z_>O]H܍wE\^=p>{k+,X^@!VXJ [Qm<|J93,!1N51˒8X0D rDtrTk3q4 8[@ (}{zHb\gI+rWpF6{Z'=@@O\#1#rp0Q.L΢v8@`}f9t |lNQb\D'XS\`uf ,pPؠc~irӻ7R`\?@t݇ WNN仺(+'Zi0wp cଉلεCtZhrN>ó@JܐkNf,N䎻 І!a!Q{ͺ 2 |D7ucB%"%E>w|I? me.p o9p C+F߼rzx/ ,Fn.dϚj;WZmԙ^Hi&6$7GsgZ yh#UU@wAab f8SdX6S~hh"ϭ\xFx<S=^[͝R&kbEpO 8yha\YZ2ʻ/]vg rVZ'z ܠ|BOіqt0s[Pcw>U 2]ݔB zhrL-bA%VaeSFδVqD}j!Iqadl*_IRY!#'@'VeK'}y0&KAU )M=l!"{)ISy 5w_ox|_ʿv/:~ :n%$Ĭˬf(t&l(V8h,U1b2J,ÄΓU/M'^m=gAP>a2:r5Z"]egi{/0ʠՂf(G^:AŮi0f5&> \Hztr~Z/zzDín~T>rB WEw?8v/ghKϘ$#W>U\m]q7ԩ`(]TWţ=mwURlBP!ⳅS!hx٨=!2:/m0-ttZ nQ:Izѻ|<7w'f>=U|23V2I"B-dۅDBV>rgDK2'}w?_"4P5ׂ鶳Xp!W{ 0@- r&ȇK$BB+|DebDe | 7AD4Ď}+2$BN(v}t%%y`MPGX1+dcFn" ׎0|‚e_S7 Rkѡ$(2 Y_?+vLb''f퍍>c7B| ԢFӎ.qԏwwv_wkj;v"f:vd3Ls0Y]ݘ\hW۽A^% ]kbDk,aK`B;ۿ j6G {"z;?fULRD[e&@h7TQ hD)cm9]p_g y\q#[̄l1ϝcp.xDsW=z~vfr){oX>h`(7^lŏŜ) DZ<oT;HOa;x-\8tosyY@ WI{dfpP1\[ 4m?_ 7"SUo .a2'1A .w\9.M>QZ+נu7>4Xlj_݅O>t{{i5DޞcnDBh?"#G4'VFRXc:aRA"*\`ĚgH '` m-mr ;qrSuaCI>ff5\ wg`cu0FFm&dNix)γD600όZ>B HVb4^b8#ƫϙ￳owb0C:RR Ozt\ f01.Mj٨Yg=`$1lqYFrH\BKDפ@0J7tgn0k0pU%eYgHN*SM oγQ£pI`\Mc1s&K )O Nf8Pb{q؇` t7b0q{4ZA&gH#a!z0ԹVN5~lQIe7iJYlÜLj a$0rgK.;pI`&}k26OjR} fLopX@wN@Juuh^'YOg@aR É88橍8 VB0Ũţ[5;5DD_yNf7^߸w˓m@h[kLz=7+GL`+J mw$ ~h,C96`kq8F)F7s T6 bf3(PfyE=Շڹ}uλK3%fzoz!}\H#ͅ}Ё_Rۡ޽ܫdt5SIfΠc70ZAu<3 [Q)tq_!N0Όɠd)IUC<1 o]67|2ǎ6*3S1/ntõ3ol2?H2 `y|O;H${{(q5ޅw nw+Rdjː"i. s;-Bg%O j~z 8afk>3a5KM@?hQ$^./F\ fȂf˦  "FHIUE'L!-c#M?=. k RAx)c@7]hd xSp._Ae"Gy5LJoj7PLӱZ"gIX*O2mV798q3?\i'F=j}~տb&s~d߻Kٻ'FFĴc_я\kgA7- < PEWJ(OSŎB9{Ws8 qba[CH;[T"x66AǬ#-$QQz0a qLĔ6 eb$8qZG~.5Z!'AG++>xz|U'`6ooy~xmssxNͿQu~$ s3o-TA1`8U[u#nsI)yF)6z&@2,ti.\/ 3S -j3N6scsCm{or:[?3r/\HGoz.wt/Szk46\Jv;vIdbL drd=sn2gVco<+y>\9g\q[|*vV_#С JN\}l} X蠍UBPK<_.qzp,$$u(+@LhO cPLFɶdȄ ??.7/m3j7?v6aʈ?v1;rpǼȯOv "ƍ'|l>߼_Lf,NHTx >ɴa%U"лv`@b1lBix rw6:d%f06QXV3pP#ؘ(j+PdA〛m!x0I1Ϸ< .".Qq4ROg/~͋ Q|/Kx@sM[t(o%#ё>G _+ wTwGxI\`.~6jLJGT:w) jօ]m24)ɤY"YCU;iuI[M/G6SLr.=5?v{".-NQ#/7\XX;F^@#v;DZr|DPߣGkN=B\[l]=c; h.oF<( vRs(\R.ăgYIp ۓ?@h4Rw^L79#> / RT'Li`o{_ٶmreڕZI`/At֊8)8w]C :Œ~dָJ: t*) B8oKȮgǑW{ DvF$,"iiBߧ*8@RY6$T%02(2xQ;yߏ&d h+]j4BSNo 4}6Dvkq$PX*)uQ("sR='xm Ëv8$wǭ|/g킛#,?+}gO}nnW{f8_ܜٻ!iwU%AHCk~lߛ tD&w]Se*PX4hĢR8<>C&0{Vn\p Yxdn8P]&1GWr,{*'ĈKi\="!` õH@`wD6R=jx!4Mq.>YZǙű)0Ucrs ɇ>HLK.!񗑝)iw;ybrt&(<>]yp|>F!Ƀ3`4e!NsnT~f5pB#c g).e[" VTQpK<*َI|XwLJFMJIHzgDet3^+d(nf=w]wE9̧g T*3YM~aEj͊$X`"JWFu&T `ɤQ] r2\緃'͔̳_9Vr쯚`6QSOIA-ǕcP.n prgw;`|x>푯;Nc.".T@w|e&*4P}x߁xֽ?EˈSnӳ'Ӱ^ }Nݳ7s>5DOY>Z3N1`n<^{nc%j&ݟuגn s: Ei&kLi?=(}8|+%#{S-VVBTD}t*q@;D 'F2ThiɼA l v!?}`^Cě3YVl|C/ C>ӕZMS<X@= whQ]ى\P,Pb)ڝXfdtGWz3v?OEwA4Ka 2;MrPg-)wEJm0o% *ێZ yg( 5:b4Hjd@"`E,~@`ݗsl ʃvswhqeW x$ک?N^ k+9Q!|' ^op}F#r\d h;g3!jmZaULZmMܭPq'ΞfLTXr"^ 6MHhCݡƝc0Q)p<%O'^-ZȴXYORgOyݶpI9]OfscbDruZr"DU11Ј2kњalt&Plt'0K>9PpM` I3¡g"ۀ1`@ ό(ɽ<4c˲ծI] @1KX\6c)'=R&#]fз?nRn31>gA/?ۅ]TCR}cMk;c6p'H޳>Cc_{!/ھ$x9_(T|ձ!q{bfӼMbA ,~NR9Ȯm7\R#1nngdI>n`k׎RvE8r.ZZ^C:[WoR*xƟesp#?< 7=:#tvh|3\|o >]&Vv>cT(O(.p5CG;UxQ** GLGd4ŲIҬUZOD7޺3zዮ{6(ڶFQte-3*p\)#@m&=T*˝4qB IAfXt5EI[d0) &h+ %*t.熕a&<=w֩+2xAo7@sLN/s^ҶAj YYA{H=5`Q}{??qۗ~)[`Mz`M_ۯ&Vۙ w[' 7L A8@-w"  od=3(]ơURG"[*F7ݼ#:(3o^/nAQs$MM٨Q$Ht\r.ģC O9,*VX?<\y2¡qnaIHh)2U00-~{0\q}is2(SָS DL}lA筙/P4ڄ4u F.Kq @FU pY 8 Yd: ԋY̬UF(v2缇/Yy;,ƕI[A҂4 @spC_Xf3fZޫr,ҨyIhZ`1Ha}Jx=mFFD;-p%,jxO˦ Hb!Itri'ϋ@*PEwդusO|lX;]R6y ="a%=a>D}qր<]i^Q)P'$IQCʛfcGݤWYc_:[!gg5 %l ݠ+fhw*D!$@w,(Hpި5?S]o9sY~Q,kNMRo(I:QH[;614귖tH#0ՌNp.IgCiOwJqυ(5|rvx0c6TW%zH(v cDѬ@EdW'+\S+>TM(T`A.Cj70O^c`)U&#m*Јԫ *soPL?DLD>2Ʌ.DkvL`J+a;kȜFo-eSWK/`<ݕMtYy Q}[.ϳL]9Zg5 4dI V MG˶ e?{`ȉG G HŷMs-.o`kŇ1p~*Kaaw9.o'* @FDGaAN؎VΫ\'aGޞ`LviFRF`?h ̀#tDGZH2E +61"Μ^z!5g,  !p~ѺzI`pl*E}.Q |ێ"<B!'ƽ4Ҡl 6PwvPv ?UbQw*z(4b.ǜC]4G,DQt߭և.QVit DC}z m* ?bI| AP!zˬPjq h1ќBC9 Qfˆc~9 h#ͽ(s(&'lXLe_#/ /|~+$h⃧j%[̤A;ÔP#]LFN3R<gX+&`ׂ)Us֠iDRV._pm: W MuV۴;9iؔ4෵YySuns96D Ʉ2"2~)o3ʤ|f.oΰu?{.Y5IP.Fg:ۗXEJ\`S`K1eYWOD4c:>3JU;zOtۣ}叄 9Ɠ]%E }*έDGE*Z]1~-t]~ٖ#AsWoB~F8: `DmD!)'hš,uŰv)# w=8 d̨c|_F& :,bCD <GdsZ#)"Pwzk A/ HjlcEw4hlM*/f 66gFXFK%2a࠾cPFB` Ҧ|\>nw $Jk|뺔߸|@G+r3GYH`IFkHď&$d-@>u=?&RG4z3CQX*\#b@0$ItꅪtpYDy\ /)xGJGS 9 |x$(:$&86h#KL,QY:32xDzm  "5hǐ<=V$X>]GmCA}PX2BzCȑɊM^ JV xV/Y@Oݲ`5S>loP7փ Co&Ɋ$8pu$=Z1#>C|X᥵6 <=BO OL$O=U?.r)U2͒-(W[5t62 SGߠ ۵ 5$SXN46#7oF:x`8O`[]f-ppldҊ&Ҋ&*IgS8X)CA,zڙ ڱA[P[.2.?KpIFai'3$r;;ᄂvWӺf]ɑ,脕8)#JE* 7vշyn9elma lVTה3TGݺRO>L6Kۍ:ATjio*>W;4# ;7?Ūq/ B>=ۜu{GD0yT8iF,G3u.thoٯ&^5JUa D PAt6:F!b@*G#qa@. $㽳ѹ9#DTh:ttjf~.Ot}e5GP#njzz",[ȶg7rk# 谔ړrn( S,('wգ:ќ~ia#D3|,j:R/T(K]+ qy"ܖDZ_$Ia|;ɸ>Q̟gç^ q% !Y|eҴVRsLAT .dA?(GD;pB&hDx= |kD&W([BSAww}ZNy!ϣxp %dȫ\:%eux9Ug|5:ǵ.jlm[-h B!N(褜3a5X $Ѩ\'ipjyc?>g'5*h#%:p8c:l1gHfVdz?p+!aϓj ߙdn $܃r\ ӕ=Z]bI_Fy'hD;v֢n_Ϝo_ >8ÄFc4,t]JVv /Tj @;_e]:B_돎( FK%@I 2+3~ڷ~#?g l_%M^n1Mљ&K\j׉d/*PMǹF8\3LLYI5dXڇ>#Ӝ'b>Z\NoE<QԴ r+Lri3:Ec HbXa4qV2DV5 OÅ.Pe}VKphep/gWmf3C31~gN_2-D(vSp8VP@M(I ]~򆎜'f<K#\Ɛ,t¯P\e#`~֑搗2B g*xhb1a5&<A}9'8j!5Od2Myi/8Y|*y7LyN"ҔgQݰ R\A SA)nNcX0,LMB F;hMG qfR=TP"E- 0 m6PRrRL1ljaEN /A6ﻏ\feSFNDz$Xx#ƣipLKZe;+l%hd;\[f$f0=|bB45=$+YTaathDY/;|5BUgV?@bS^nK1aԱc΍ۮH.ЖΝӸy4#P={oWnog{:L`XǪƢt?:P^ǹ)2kĎ  2dk;kz2:C1:\6'>*ౙQn|uctش@taek|8J$MP䧉&ar#$B p'$ "|.Ϲs ޺T%V_@X(U~ӧ8K$z{z}۶Ʌyk6 7mų͙>T zr!:tcV]3<7wVv!ψG*19"T;&>^(']V}4J.Q}zM~Աv/p{q'zѶvA.c YL]mE+^Tl PI2w@r!>8 ){`X8)Dx 87q Х|b2ĶhG҇F\iըBr͔.bÞ3 <$%v:\\pSQ@=^#_gQmh|ijH ɆFZs}7>@on.O?C-,3EK d(MF"3,i'?XΧ.5':Ƞ ,\"(VPpf뼝`dQ3` }&.ŨR  $buy2XKD{.@2x&ɛ OLfSk^ErhkjP%_AkqCo9E.ht9&Q]wjwWƕ)^:=hXtɟ7A\!yJչ9dzАE,ڄřX}`L"l_0JS%u,|1dfFQ.Nfi+ǎG&|i11VJyw t9,`^L{NY] Qt0r2d9Wg}ots{w-FsqPbqPu:ԇS,{8,4P]0 M#x5J(ZLޒ|tjC$y>s%[g $QHtq %zjx uApdM@]KNH[#8E2bDu,wd oPmiP8iu\};H[elw82J7U@H8 q3Zfey쉥ohxf)8b&k6wEf `DE3_ѐh ;xth~R7m>$DQK-& PC`9IHwX @&2D;m<4j^;DW,0cWԾg0ԼmSgr6`X6GbXGYtW 4oɡɥ\2huQ}lwe61KP-McϨc@Ƭ9z.:DsEVJ⩠CUB+bV0&УFɤ6N^uuo8l߹56Z`8ًGa7?;/.n'Vy3<kG%j.7(ߴYjah{|hCKvѱ8T35N cy`Y*T$ڧj/ФuF8RG r(p4Tr==!"[~'RwRDd%1 L7:-Rc& :(ܺ f=с^s]8+zRl(>4sT 1Z3J::LIg٦p\^ؙw{vƯlrďl*^:Z93pJ@Ք2LةLNzgDJ.L>y5Q5' >'v@s75cBFe 2 ^òOXN!^[@ }T%/Ϋ4,O;L5O2 /| _{2mgm_11Mݞh|'vJ ~i.i hv2WZ؂@۱ԃ>& ta 'k#FF!Aɚr UgHɗ Ə:,a^%Au4َa`/PqAѴc#yբd"z +/ɐ]JQ^&j%WT9-"Tmł,i Y␴ZABs\h-M[t[j̴^JAT aر2)VRZ0DBյ,)wgzW{(}lJ[=0yȥXrDx"^NVҟY8O2g~6h%%*dbuGk8dKN}o='}[-Р4Q1_&]Ё2ATI0IЅOYV 'z)pgp0j}/IQB-};>_~ru5qt樯G5L%{40[B=V?c*w艼#0)&F򀪄9(`ɩ+hT%Z;ӊ `4x?EY-5XEx2QPA˲Lds@amU 3& E}7 ,©2 yPk~bI>ꈪ>}UxkkBu:.a]TGg>SowWzIx3 r,y;<f"k$?qh\o&r*puǟ# .ς@IDAT4DL9QÐpXI;L(7+!8_x+~{)% 7\?;2j%̍\{dh#cőd K 1<`?Ve 4 /Eʑ偍:Kp2 3>LhKhA^FοGD LF=DOO"`oPyHP"%ʷljl5}&0lw90 m-o *a plv2y]_cC/_N?A'Z^f$)\a;K/7}1EBVgm͊~9mȥeNeB8 }D4FDh׽;+z 뼃e5 =_9&i1ۄxq4x ]fR!D%4Q4Qe! ׏L\S@LD8mC0Ќs}3#}E^3"-@fvUgj(9]Ju#چ^4"ޓS#!UİtӹϲZ#ĉFT[J5WS<}wN;]mglK8?pUca@c>8Oؗg%HyPh;WZ N¹<:elFĉyRW숦׃4^Dvu}:"ϡٷ}-NSmLї BiƬ` P`F $l3G>L-ts/s&Ki,@N/ʾ(ͩ9ML.mQ\yfiLgx] .o2qAR wtf&޷ﮌfwjjj鏯\c,ժϣ>r`m]UF*t>C֛Po~ i\L7nQF;#N5ao~P@dw<ͯȶ׮tGyj]YJ@ `) ɱ"A ,UtTt Ŗy(I3fq*&`ҢMM:v,s^"| 4A֨E)0 P1C2: 3 ֑]@Thh"B.(V9QHuh5A'q xP-V3L<7RQIHB?<+w4Zy3H"lQ1 ڂKQͯ}D;կ .U:IQ . :m`tivVj9E F'"BGJ<}4;h{cǣ$l!m7SvKSg2K'Gۿ$Kn._w܄vcr/T]P+h6S2b&2;qT6q '?6<(M4C+4 ^S.i-\n&Sph]އ."qB=Ԁ,RDH Y'ֆNT'R7߳5f.`-.<=2}}#q0Dsu=oLaV)bEM@;qK> C7&4Ci="|#.vG881W^{ӱ~QŽG~-/aFhozQb(hѨ{hǷPQ,WXV7h 'c"Kc0*L8F/`c50]Ta` BQAd>[cHlrDK->pg 1F OJbXltCD.QfA <?ͲSB@[d86U"T F{.:noA$# `.?I? Be!'.F=?NA45ͪLЗGEL㲺tjlDBA.}I6&DWnvOqw^-$36\:p1˛@_zͦBm2d`#C0 QK ~2ZgSeT䝘_")\&# Ct%Q9a "eΗ`KBe/{p4׆FJBaԈd*CNю4^jo# LBs@LeIo5߁49{h7#an{pKw}Ӟ3ה"# )y(/Ru/W@ QMC3hk;BȌT# 6tJD LHR!ԙϞ7]ډUI s5]?rR|m~ZUZ5ݤя:( ^)'RmuuSzFܸt{ y[zީ4]UtaPc0tvAZ _0G0&.݆k 93;1aSS&dsx%ѥI_Qdc._ph,;v9%tszMsAKK0]aSLQQ%励JvqvM{? 酂ˬ(ۇ&du@b@7Q%BL_J &IS&R{K@3+fj8ozIz?{p_"ԟeW"_<(3T©,k#hb<U4~.^'&ƫ~1NTLu`h"'0 UD톕-@فX{_`4TOW; &,/|\b]8 3}nzn _f [#䟔Mܧ7zǠ#oh69/ITB${K],+} @Yc2:4Y̹|y^GXmX@E%.QvA>q3Ra0DڀGG|kAq|=?8lΠ˰i̺X5V%Paȹ&e'7rb#m23>O~zUl(HI FLedAX2#pE H&l( hpn}`[E\ XLG)@r_'rȑnx` ojrGV`;h@HllL%۸X iZsDŽE$Y"J3R}V gf\8^}K}_`ՊoU% )F6`G~bA33dlȉ#n{iiΝ;E \ 1G323=gṛJ |'/MV/k^^ a^>B_D)ԲT< ,0- C7pJbB0 (O'0"(8 +d 2X% %aa$.-)\4S*ubQ u_Y 4*^GlHHl`-Mv4%@]IBل.q n|>ȤW؉oĽc:&Y88# `]fݑZS2NW(vg~^k`vϼFQۢ_l5ﻀPAS$Hc$Lucbl*JYj^Q% ~*@3 `/Rt 5 fuG].GjSoYV }܀zm46D\ߗH s '%d~ؾODz笸ؾNtw#oWɚIXrNFh8Q u%K%$r%-X0az c|`C#I!toHbA}2!oRZg&#vE:Y<(*Mj, `ZuE+,+K"PnAwj&9>YC%U{126 i 3f}>>_`t*d)]~̈/u~7~#={~Ww~,H6TҝG}ۿGNrnmƕj΋X%X 1Z.VmQ 0* 8Whgv.M UsJVH @(MgV &SK3a_웑ݐzZK& q*>jfw{w=o}v!ɏq bqǘ)MTL #m4&i7b"^ >`6J$i YuBk)@]=&0JU?WŶnh}sf5̀k)\I6!. "/Q+p#3'p9v$$^n$x_%@n}xG\?T}M^1Al6mڴebz=+&Z&C1#-2i1JN؇TKVz+j\; J|qmFޡsjuQ%-rGa/:=<8n!aM@_ 0PUUG/d$Pd!~2P Gs- 89тt0!%N)@"{(0`x&.L]$9iOfl 9Ncrp{Фυb ,9$¼Whf2X &)H-uDg`ˀlwX0ߔ bH*XP:Qn}G;煲̝Nׯ:q~8"'6ސ9XJ;$ 3` iYb=WkMd]gdвSfZBaK˲K@ +pt3} '3& uDv_9:MO>JsډR~S{hp+:3JW7agL4حm>c$$Fm^|U#ڋb8pny3~iz/_KSHA+{#q hQ=毃ɱ71CMMG-žby FSZJ1m7`ChƁLř8NLZ!F C:πN*["S卒W}ĺٽ\7`-_8vl•%[` rm$ mh2߲  YaiOE`v:Q-5q$23B'yOBBI Б aQgе& hߩȦ]%=HJss~喣_yD] ɋ`SZ Q`11wRމ1k҃w\m\gbjL:2u+ڮ6"@&1asSU %,pR@ -%Z ɤ&:Q?HԄΤׇ_I1sb8+Km*qsbVH@ BMz}$!bcڰ}eѯv۞=θ)$J Av**cu8nqD"F;.!,Wb?$v&?+.:&Jf۶V43H522 3SSsu^-F q;fAwvG'O {wOp+~ВlJ!Wk#Md샛/{kO)3|0@[,3 'A}k 7r%oJe++f# eI](k 9(~IJVd# aL>*nO}zJ;JN?h"${v̵j1|.pZffMO,:8E-_k`6(Um*r :Ϭ ~eujL/1%pKZEn{X`Jgz 9E=& ,.a6  jYշыBW&;g~3P^W=02+E/OrԀi^evzt}GAP1LH<'! MH4. ?2K0K.ꈉ Sᛜ~.bR<$ۃSOjX͵a@KЀٴi>"2Kfvv3y@2.< 8a89S'adT %u}̐OH1U{,\q3O[u<髵w:uO&axe=ڙ<Η뺣6.F QA ?(KEgX* GyjX.< E=%I}j͋+ߐ'n )xh[8#+9FvKs(K;>i{NWX,zh#2{06 u~KƄU:bsr@@/YZaY%V~!MH,cU =*m.;:1>r/Ǚтbi5[s|%.@mE $\3Xd[LJerhH Cj :!qmd&?4_t3[9 %, jdScKeXh ` ۣax\g@7t[ :똥4_bLI:Lk^zFs=q,d> 0d)C$;I?s(_= ig 0+Ò,=n?*:X)(c{ xJN!kIصj(pK+Pd̾tѺ SL\ t-+ֺ KA"6ij/T;z"X'3PUtJE"J=79LNlTmT\2{va'eɢjf hR+d6/ښlNg{Iq@]涴~!g_R mk]fUmWw;}̙& #O4鴿s;oPH/p%r+ `(#'~QtK,ZL'njݨzX)ɜWlc=éU&1)i]!AKԢ5c/mӭs<̇#F}v$?(Ѽe|&6Z)չR,-h!Yge¶L)8֐`?ո0et&Q"Sf6S}Q 3͚ee3܄6aA7YVN,g4! -V"]i0^)SqKa }e%> *4@r瑜P9:j]}ffU7rA.5R PN|#?:핫a 3 S|F[e&0 z#UDE٫;KB KI!> \C];Ya%Pfv:w$Lh XTˎNR vG&[Θk'!GKӔfD4h2-Ҧy`dёNR3-ǒ x{J`|EGQbwd`Þ9tqĎ +dI[ tXR>Hm.C:O(]tmpΣ2 5wtZӟBppUA^K \{?jy|K`}dtW]0yYZ0!60&P#)Vu7 @ixqqm6FQێ1q E1a6gYF%,ΕWctmPA_QI\ ) |UVx=3E @Cŕ(C_&/ zդ(=GLYWc˪< `iKsʑ:cR "h>bTUL; Grp.K%yr W݁Z$2@nk0b1bv0#hآk6l:`+YdS?z-3$йG)wǷBt͋m^F8aa\\ɔ!nM]x"!]0HVv`mISm`,OiAv rbk 1!֏869,YFg#&FBN?-yl}wZ) <mK!*)ݍI?'g՜|5y#hElt4+P6e''Mq-e[N;0m2$na IF.G-n~p-99hopwoij 0 n[qD$sof/gǎ]of9͒*Frf'm_ZuJDhd!?YMf!sDTK+Ȑ.So#}1>VV8ĴP 6#I,+[$Ɨ> Yؐ9꤀s! ,4Y+ ~դFZ԰ ZLpkGpEW!<tVH1Y!1.5; "p왴\>vH 0.Ys]$=AofC# L<\"6{msL.kK"XA R:J:G{~ c|6a3 ͯtokyO=^L0(PҔ;c-):CU̔<,PkE R=>1Ӳ%cKghS~M9V }@M[.n)~F>'OQ34IbxTbaц冄Z9ufW͈۸ػwRj8z+l͑S%Tu5eTiDCR`V(U|e)6u擋M|s#˼PlcX`\J`մl{rQ/H{*?p|Tߓ *St ;w1'i:o/\94M{u$F%bnƸbHSrxN,sY;kJF`[5xepQ_ Fiv }3PݒCSE<2IDr S)]阋!L@>aoLGn}SB+N<?Rlq83 fsO`\Q$k`%_ `#::@ e@tȆY?`z-8no]P=|Ag`aFU6_ ãUP~1Xuu(Fc^(\E)HC1*r$}cQ>y>3d&ʧ˻9KlpbkM*@O߅\jh¶ҺkpVp:,rkHPd4m1W2{tj" J98^9%P&0ZsGX5hą iDs~qwvXגּo} C[!]2vio HYϊ WAaB+7 uOmVs[[h%Фm!ݳbH"C"4v01V, ;(IamQm͙se3QR|1Taw~[ҍ?;ffXz8#*4 i*$uCXb00!b#jeu, Xf!l>!4dViȢC6PHCAlfx4ǣ] b#mR#R ,(18A뚥ٵ T,kqP\Z,`Y&-Ga}Ӂj,a ;6Y@/7 G9(`~3D#|<~ҽNbg/U๾߉~r}2mƪF!eThgZ~(_ eh}Vj;aszS,((|Mo&Z۞GQRD_7a5Y;f*; v`gard\ .oOqhQ) 1D'׋ O`Yg+" d>kM+iS:xx"@-C>C 9}FX؅H'D pULz+w6˃'_hUb(7pErx.gJ"1 \Fx 9U ~)>P*$Rbp Fe4&A6oKŠ $_XOM2C2H# 5\d`g%r+6ifu;p pmu`E<\ZlKnx\c4J2$8DĊguhg-G^J1l+X`)dEyG=9*L= JH5=3rs%%Qpiy5e;n$O7]cYes.|` x>x-gLyT!.NGbt@R SJhf՞& d.@ SA 6xβz`ڦQ HBã KtL'%ռܛi .=4[2[U FdṲbEQo {H[?bw7Y2#lqZֻM0Xr"vq;*1|I>L/[\.wxn~FAh&ߛI@IDATſ4B ׅP=ɱm+{xɩLx['LY BZA?  خ$h2T!C;$˨XC||B2\Fߊt݆ <9Qz(uDõd)2( ϠPaG@N$$!)8LÚfo !mY.˟~n[}\I҅3O^1(ylBZqwp GuN 6 8Y!o?W<2㸩u #b]N`*څi `$[zm@OY&Dy&Eޠ ()" KL3n> 7VE://9pw:l,O_ v|/>}-i<^Z6.s y s,)_UyB6vbH Tt@.RJ + R+j33t`G黸 vW&u~%B@ (>k&:z"܃^5]OW4S*D}7xW>4oCiaOdo4_ Fe@^[OhO4Mpᰙm^`MPckdjF\?\x;%@NmOu c](4~HD,}~Bެ, gNWGwߢg>p-m2bVئI٠A> Khf ȗ$H ͒ 8,\򀬢. i&¢f<$'5gx`(nOr=<hG,L6T`N[KAڡGrI ,Tc7eh nr~3=2V =){-YAqmzTu+,sres뮖Uy}+v%9YQS| (?͈ltnʉI E3~yLl9C7CvM8l@:-F}.2e?N#@`k$u>WҚ v; _f_;ĩJv%pVz:& 䘔B'KiKXg "O@w(}ۆ>jxZ~0YP̊kC䛴Js0@frZ`r\O ZH@saxĢוp}M7z=u R۸X jʷ'?kwɶ =|il&ԘGc/9U `NEjG}DA`4obfk07 <yM2܀w0ٕ I,XmEZITlUsiNNtwSzq}$-S^D^>\Wg3VuMe4taX@/g׈&h%> fXl@/:+Sn3 z b* ,Db8[\Ex?eҢ;RnOAvoXxRZ[֒KAh4Y}> 2s&ezLE3x 2! ^YTrM0hM,5&;@&<*jl,'J6櫓 ?bsm\2Ǐ.W&)0=d0*Zl4VXҟP2a%i|Z&-% u&4yUhWr#B݂fe`ihwY@݄GW]u|G%~|y~}v-9ϼG|Uh.{I(f.pW6~@_@[fxɲ+o poHLMF-"Uk$Z:=Η#p/bp譆t2Dy8'Hނ. A.4$bZ}An[qCI>fVRaoT9-@"#U/7B}?x}As01κ6~0z"#'wIϽ:dS%(]PФN$ӠK,ÈHhy"ڔȯV{I `FߟlTl'HNR+/:5 X->чYI Vuk2ΰ3ˇѓ螇{'OmV1fU(ߩ\+Uj(+kLƅw,K.ƙi'Cr`qrqM9U%>&Va«392ULRv[,X:I;JĄP#7%rN:o8&w=~^)i۸X mOݼY=/ 1i ,zrKܔ@"A !Jg.1@{H:$X\Umsf) Ր#zRH`=\oٹӸz Xu;t),Bg|"ӷVmؾUjfmuJ.b=D!%Uj.'yrT%fZb'qIBZ>)'.W &sD(4>^]gV., b')Lbjv1dF6CNt_vd+a˾i4tM$sPP'P.fPr8 3 2_N5}4.0zvGϿtqRs{v:wn̍|} jտx br +* f)|Mtm-?A6[wx_gg(6^=a,hv={H&q&%;w-Q}%K@c^˳F) CoA m^^HP6P9hU^s3UyKEo;9Y'igrQ[O?)(Ur'ekicyiz0jä"rߤR&+b˴i܍J9 |h^#ϕ`ףmޘ/R=cy{qY2Ms G&hS@Vj3`䑈gs3NkrM5ƕ&~0 i<Y5] ;h1>Ï:E<.E S$hXumO`Gr`bR PV[Zl5gg>\:\~.`-+sVVH#-0NXf&*]9Zck$ep$;!pD {ú$3,6 XMl(z(ϵҦI$Ijƥ+IK 07̼󟱌3eΛS&zHӃZG曘nwt蓢o%-J@A8aBA)!ӬIϛe0|E `|iJ;&a5%RIQUG1,@ǧeeSwZߚx~ -}@sK=}Vu+d&[q&ĕ{ }|t@CR㋎K~ MPqw" +[:wEY%hhSU#-Rsr߽4#< ikXBɱ3(ܲjc)^jSE39S %bd8WD # Φ\h/T@kIG~9ШP(+0߄9rOb=`ޓd"JRG8I@z5L_5u=XقLͶ?ʷ]O;q/ : [")^^1xT03uL&ˠ\ J'1dd(p̸l%HP7"9Yi1\\o 6fc{Y0+,/G-bBѳ3̒XBјR~꫺ s(z)U%pK`޽4^15[>nI#9M6In(Y-73Se#OnJDe*K?VFj5$D ,}daEz ƴyssZ/~ IDHu_1x{|}%`D?~vum6)sQ;e<ᑛ_ʛŻOhI$fe)[5fvV-2[Bb bҌ ZnTViSw1>K:R7ca|WSvbfyA R5yLpb+ߠ=Yаt]S>E>dNQD,Y`¨ Aja%T&Sė_z+s8#>1}\}UW`md;( ۬vNߟ`^K^Zm3fpmBtf\L>U͎w}n=p@4-M+Ү2 h0YZf.ADm#A?;fyxdV,&wub &wy]U5TG7yqҹ2Hȃ9@- %L G(T|XyZm C`C@K1-KlS@: Z &FH,j@- lr-畠rz G9ʦ6;nW'pf^[ulq<4xNX%Vֺ Eٴlee# ԤZ*;!y]MKT3%k< M6Ȑgjs{n]Ю)U2tR\9@`srP_d3g՟"6.vKگPB-ޔq<2' ցHrY}q蜺!k~i$*m@²c%ҲGA ~: jVH}j qi:O}G9.mWXi*VY6?Sh/4i׾t_"윺I덕^F- mN4t3qYDXT͌w/̭w4b|8e/xI{0]X_UA戤(>(VꉩlTZd1V#ZLRxK\H"2bĨyXW0ty]g09Z ',:V[pl#@7samN*q q͐ ڙOnVu\m;ޥ ]?щ[-SsfT #2 a tBSIJ^R*`#]ʦNQ6/L,)@;a͚I -,qb86Fa',ܩVZTzv`P V9s &M@[#y) !eB$(ܕrU[:t>-{Ԑnq#?䋠e68\eOV^_8ksGW}wC&xb7xXa$By#e25OJϘ4˾* 0`_@< )A9!Új 8aQ<uJDL8t<&o#ojtx`*Lr,m%X,GQKY*iW9~K<)M (aZ8_oD<5s 0=6]6+F"L(ZY )m[&+X~a?dGIk]jJV5SXm1dkש3?\u|߾}cyRΩDcy=w~{mi.; O%4q`? FmV ل*S!Ss?d*f"N@j"WC- ۶Vt9P@[NE#=e,lxvIТq 2VZ.Naam>x:w[xk]>"Οs.)Yk|Kt/6WϚm 7EhdZu]2SjdrѤt [거%d][_9tAbHLX@D#4OI5:jWB`CeUg̲zqiDi*C&m QΔ/cie6ڑi,cYf=_2k#ȁՠB hes(%h/0@@$Oc!9_E_*g}=u{8,ĽOy~|sĠ`5>t oD=KfJÞhi=0))Ll."`v>}$cafrX< Q:I f4g-c!$} o)P#!xÂh&33ó#=uաbu^he8(;ǩS4@)ڃ(LMgQR2}3upK<+^E-MvfVZ:wQO0-զO/amg$8M4{kJH7iU)K ,jg)&"Wq$a<9nysT^F3ٓRu]^7hӖ %?s5WC'ɎWd -QV xӞ]V®{s|wٕGe4Ar1hw2|̷=:~@A ONm @D=K;X?2@1BEX{G]L"8wbsTgm|}Ӧʣ+e5̪2BSnF}קY(DѬ Y# gr yX\6ăZfg7+P߼clWw3YqQ=' ۩qW-IEWC_WvK- FU'yv\f)21l/5݇w/K`kP+ > < I6gay1m ȳ${ P~X˭`!3`o*RB,vp@ V944Kr6h,1ʤi]Hl1s>l$=JÁ˳9Peu>e!X߈t3kn¢GtXyŠ葆xM֟oVѥ4l5жXysBsŭ>2/vh\3D@eϔSfj+)PY"ਂ2`VYf?+KɄYqQ:EKˡ67,EhT49X 0$56-ѩ9{~')?w3/}Cz@{3SGq %0?uc}=FH?Gn?qI+EӦ41)`2cՏ@g ujfȖfYEPUS6ZZ\$ HWa?I}-̳g9sZc0fX9dCWLS'M.YN@rԴBd#]DTQ/fu*MVO24ﺇd6Nf7We2f̝rdB;`0 %HHyJPXFŴz3ښSNj,ѹ4tRwNP~犩ab^18DAaPGW\%ɛBfޓsq:pM𤰓iYTj&2݄t(\d1'&hT@y(6(PМ8%%́fx冿Co>pOD c@V;{q\D<ZXo+$(Q *a2X])h0\BH7Gz6+LҗOK_TJҦ$6Bfie=Lg6OOb(,I)X 6צmy֣~-MoI<2%pnmE32fN" TqƩ\1۶d[Mhikf̈+ՈF ju aΘ2 ]ųF8#(Šd` ۫/E[{v5s wwN@gq8"<pmÇJ]e5{Ys3ȱY<~bD)F"E+Rp':.mXd~\4 $K.f>Ǧ҃i!w:>Вf4f;rJP#c} =8w|oǿV y8r0s Y"rW,?{,(ŬHˑ!bB;xjHv3H,dO)0"M0,jƚۀ&ƐE 0cFgZ$'`, @sˉ>gwf7Ν_xqA=_y?mDL(+<^)S,iyf*c6mc^3%T=,4{zNVb;-z-N懿΀g%QQ `[\a8U_gt ,oN*tVݵPy{8~gzM۸Hqq$b%f(Tur\sHz䱟܄vX gI:8鴑I YP4hWmڊ;Aa\%˸z r TQdYpЋJuﹽ7^xNtc9ZoΧۢ/ZK%'(Ma33%쯺fu1753<3Ona5ɣXeͤrkD-feI-\5u9{&$qt!4N[3:Z&qD @XAbݠ$6gaשwVVl@a:(.ɀ*K8m8eN_Pxl87EHX0JMPW/+FF!zA!3M>4|@<NoTdU{j[e3ҏFߋ^{iSԁV,pAg}gsf_ Bb1v\&Ղ:[vH * r HްB>e^nK7b`m"!ϐ@ 0I-li363]I7aϜߩ1v݁eN]"L/jQmlwrDfӍdeI b/g~ ?`9?oiκ:z&WYlF,e<r-݌}R0@ؓO3Vv`/F6 ,L!i8W TUPVe@ CZg `y#X$dn0>%}Z#xWޝchDn;mcCPyVPp0eo*.H+5GI0^d] WB˓%n.yO<#א.@k7gr?|GQst#2h?^ʃPfEҲajY=X,=J ,`OVgn5 T+[2 G,:,J9ድzN}e+gyR幊Z`[4Cq!Ah_}8^#LOGE49V6pTljҫIav'B߼L2 iC<}t7;{o'Uwխ4dfgIx!3c3I래go رlǙ16YmBBK/յߺN`5Hij֭S=9 )S7Gcy|msIgΣ1i>6jKZG3rg8Ϝ]:s2svweـC9o'tes88l2XiCGt2C1Dn BO!;]!#"d49I]dZq T{Iujߨ?zy^}tq[lh&ke3fZO:(@)uf.%:l} {oxv3 y~ERi}>`{j«!:_$)I.,1鄒jvYlwŠ+Un| 6/;,KxvЫѰEpN?|7vY`\s]|biBUML,z*-f\f/}^0 /r|W_$l(UP;.yilߖX*e̝]s< Lv4=QÓ{<pNp DOZ@(-aRuk ,q'\ }b?y oܥ%؊87#,-7~5L#^ФZuzr1^\YȁPNd4TobbHT^Dt۵ VMҧ" Ѽy}tei f:^y(ѬLrƙznu^u"L4E tM!h3sd6]T팊 頁C<-|KmjO.,:P-:$<׀[(hIM,*[sp-2b|ߓpnXn9ڐH_a3}KbOOG?gwRXH^J˩omFgô$D8s_Ł(B\TSC u0iǿr˯w û%PZ &-tk7wQ7~n&5"d5R5V!z2aαZ@$Go.ڙ6e&'m3SuBZ/gOkc?,MP=>FM $$;qe;:xW~멟}@wGqrw +s;CW]?9!+c! Ÿ<,MRj۽юqm9rs؊xF:sdn NOK9"D̑ݶ%!Jar[0pD[k͉Skfբ7v;%[h9/nhw Juo|>3OWy* 2D F„8u| ,gm@53$$' e; N^!x<+O+lZ>mqWZc#Mc7n|^O#}hnLTNq 1͑x䤂sTq(ZJݍDH滳v4 ;~^XV,sޞf8`Jrðے)zuҍۿvVlٯw`QIVl<#`pvey";׮'D8`hZ >yhX0`٣ ,kI7D'$QZi{j_%u>S݃P.FD6ȏ@,#bO?E0Hԥ񵀉6 \F`ukHH9\dCnpNV3?s-}4̳XKcƵZ-}b=޽3-o]hߋ}.^x[0~Lr߃իPJBH"U5/)Xpʬb?*0T<%P~OI]NձyO׉>4+'>ŞxYÛZyv y<ٻkk/m_ V=&եBa#Sc{{Kh6iLÂlU`E$"RnL}c [Bmb/1`: ,Z΂l966k8eɰ^%\eW&* B$I:h.8OdѨ4Osd=z׀4BNX4 4VFǴ_rMo 5q=xf;Yiz <*[9.vV>\ŝ-"{--\pP|#P 9tm a$'Bd6V%!r`0%fYSOAL|`F&sW'޽q_nm׿tH}VY8Ljb P3OpVJc0b3!]Kz>>M 9X;^?8l\+"E-d' nh!瑮kK/o|&'[1sm\ɟCN^Po^^+&ޥsfr5Jݔ뤭+[s& |:#*<3i0j>'f]8_ |'g,z˅ʋ~8mܰWթY<^O ]ĵ<9Ew 8C18u8bmS?\5Z{(uQOyWM} <{.H"e *x #pms[IE] zJQXVdRJœHSD(L'"̤Ff44q#``0S8'$"E?o}KF0Moqhs vIi[[,{ wx)T@ƌ#9\1%VVJz ωb'ZA)Y]k&ffӷK6q3y \<|o~l|շ\n9>^q5 (ŷ7=\_l5s{Q9Rn'f2LĩJѾKO1~c(e8v:>G4D~KahH1jVGPZ{@v8FWPo\< SE'hBcil'릂=O,UĄW_b+FL;ͅ签%z Ov20>39 2]l9F LL{S48Ot)VE<+YfQ:@)8|~,G hQIOO4$cB.FXXX @d/뉘"74}t,0ãCd=X.`z<8]oo$-8ne*J!ŇD!~Z,!1 `MλۃY_1`XЯ-Γo>WU9VobM@ZY|3X'@z DۘVӔzL;p ISYO>]TI O:07*)TK!'E>b`,u"۟!|ߗ.KRGs:|"K?8iJ.u*61,qDhv=bQءOZ! 'ZݖYl`a'+.$N,2V_{02}YD et΄SSfvv:qsɥsvs Tl<#9vK sqtEe.[;[dH,UiF05n!ZEfK̺sIs̤7MiVрdY"Sw_\_H8B6{`!=38 PF:n QH[^f3q$^ \`>EŚ췫Aie>XK:0:0(0[fMI&_LΎIIYZ a2@(*{r( `ä=cFJ}Z2ȑ-?x2~Tm<ՔPί)C~:JkpC$/8.!5.2 ½a㧐Pn f}^/,薁l8)E;Y ]XHMxh;I^vFVws0*- #I#G`IQ3렑qȔ{[~o2WaAsjZ Rֺ? I@!ő 9 & x5A}M}1vbO~x_ͩzLl, xĘ5o<묭Hsɯ=փoJƢFΞ=F7S6'OuLE!ƚ3L ɹrB_<~ŗ/vr,en̉`oYhrR|Lz1sֶ{a" s0X-|_dTDCR#\:;Ȫ㨳f=VPN Ě_sUv28Gg X2ァr!`WvLnmһ#ShV}*Xys17T}KTE1Q&64]<ʇ4=ţPdacOZ7&CX2i!d1<"V0ݗciq<(ТcR#1#ĥfdPэN677u7K$ ;sqI}]]o:A ǒc,5[_A Z! :h\7f*fbf+f ݐ'N̉L٭¾Af UZG AznS¶#ҳpx,F@&LZ)\ٗcCLg8W͟}C#Irf6V?|xt hfF! v53Lxz9GNBXMVLAt7+k}s]'#$=?a"jEH~ If z<. `/hDV+'~Σna(Bĵ쎅Rr㴗"v>3ؑ"*X.}owBy-On4D$KՈ%<,,mSi22g~#|Nŝs>{in0o`>Ue8vA06u( [|knr0֬,25:n3O'[ /f`_5Hj]]??י|; VZ79d !gE零d=|)D E+CUV1t8Fr.RGܨ굲Go~浃$YK<$}$Ա|X|9O{NKg` {\@ZuˑC(I 2͗W Cs^"]6GWmyBFOMaZ)m13pj.q`֏n1$VXlp$ %jkcN[fm_vj -#3%UlefKg>Pu9 4Vy+Lc."H 8%r)0@,>_kAjy9A/~Lo !F*R7sW}wV+jGQuЊ>S99`Q߬)n hfsn6&xG1K ,:L75GIn{-RAHO6PtrpS& ( 3+E!{ Zğ NyC  ƱWVLhH: 1Ct uW <ЈyNĉɬzQt:[˳/w;~<cuze뱖n̋0Uc:#"̉@3ڲ̼@g?V7&3Nf.R!+HdExIyz1T~/ӔeHP;9bi2~4|>BF ݍZ;;_}4q3&.=?ԕ{wԛSV#J4¢Or9tM M# Vr#vǨZcj?I>'tq$bi#ܑ0fG1r+dJϾb'%@ڃAȨJodVlصeUƎTqi˹XYS̥PdU2/+LUs?\uVA~qAL&cH,Kfg1 <ꢀ?l$ ǠD꽈34x].ŋir%Y/_;"g{/̮bNEp:u q _$@W;<5hln2̈́ףrS,u-T!!o\2±Jiusd+5OHXߗN~)3'zE5:RguT2E'#`Ց!2W.dgUo-E$}tqtcqnf!RJsnFmcW5z/¤=a_yMXL`WLɬi4SYja\q%y<ϼ1>\i<( j$[P ; Xe,Gz XBᔄ`68e6B[\5`eaWc> JIX"2F4=!nGȕq9)CnkQ>0iplb+F!. ;X@I\:fΔf.xFoNg^[k1o.Ĭ9}j>_#[B 5TdY 8VPt -f2jWb^f2/@ߨ37ԒyS* GB^;+8~=h7#O'w-+(#*%X>aPO~PHQbW7ӈ  *P*?}\LuDcN/2tw[y&e>^=GtA 1KXa*7,eN{U1(̆:vx X6U+0n=K1Ct'P,&0C83NZLvQS(ABB v?\>/ |8*яepS/t䦗b~Yſ,^a%>EGM>tJd,IKwvf.63SV4Ptzy[LrRbS+6%: ȊIx ١\HEvWtV|s],/^#pnF;уCi,iW_dVU$xIdh67dN"G"F}K6ViKz"ƪ6B#R($"rQu%@yY*o4F#MC-%3K$<$mGV+v|R[+:7AjJhDTMZI() A0)S 7s(^蓦#F??e.qa]mgUx'D9[;]&UMH' b H&$s5DDr]9MeGċUq|pj7.#ޛNhY0(WAS;+/}iKKK%#w:Ͷ)ı4ct?ʺfy sf4B5 )hLK-ن6"yp2,3XK(v*uZ*IaxـmZ;/6wG7N]V9+/E;{=s)s CБ9ykNs [3nXtSQ4H"}evȠxfg2<#cdd`’!t-!жl-( ?a%xES+;G:%V{DBغkh[hb ?^nj:AΠmˑe=&i d U:&E@,ÁT#?2>f*{Zby|oҗlk m.v0ԄٛtOLD4 FC"fewedp8Ȟ >~Tq )WLRb0\k'$PCd)j.n܊F)f9v͈j*yI;$vx BJ}kFsIͭ 2-U3#4.S5M`TgLg!@ı$ĔPmO}c?O^w3 |6i_9rmH_\ʖ^\l1A2qZjBaQVm33DF eL&ЫSe?昷5V|}j ɒs[KM V+VX$y&xŊ"q0ؿ*HH&Єj!mi\64a,4(zYlB~{Wв+;͝v˜ n݈BBY5'N|C4=V! 9ja)]LZ2u)bt,oѲj`1]ĎN .ZجoCXllNlܫbD8d%2O|΅}vENpq{]CV31\,~՝%>,R`&#7mA% FԾ״]," YD%Ƅؾ0vV# $L7qINvD,B Hb&"鞹Vxo&#M菨 OX; -/91!ҭ&@Jyj~W\yhQ'_H.̟L\sŢ%?b\:\[{X8%A( $ aRb2ƃi##bY4bd d \gWMИ>J[6xʨ\ vq!bJbN#`z9H#RhL J/lx/z/= 9 <_]\t n)/h .I|XeM~b}qFlJ!jP$$*ӧ(6:D|,b߮7ݿ6߇6˯fԡcs 5 *NH̃$34̺̏0İlEٓQ_"߂Tp#$ᶑА"$E lY"+97puwy7ŹEdQO#Pto f'EMjh_+,=LdR4:F%ufr `ZoŦyi= a"͛1]N<9y?Vw/?<팆x ўfy4Ђ GcLL!",X-N!!R[L6Ib"dE4N"p+;%ɑ^?^uTAҸ:|s=FG.x0_ocYJ~4\:"6b>3fL3/NV&xfeHFƊ  !<v8H\թ'^lԼ? PK+ܛ3u׌@f@_np_wRU–),`/ i= RŗcP2 d%tH'̀4dY&#:V!K+B~Z=urx l5>i3^>t_vYS,i Zl||Y}d'ϙ}2cq Y bh>cYC^˒MLQN2a=Ì(j%+M4EhAm܅ei8!1I/:b!bR'mapn|Ǐ%nۀxQfDvtMM^NSlnr]a}m4LeSPTIy,$+CȐHmHen#e:[`M c^/ݴf)<ˏ7t`GJ;5@Lв؊xH#{w=w{e.kBd se`x=$ڠe ԂZdQlhFgӮ6~o-B =>{$he类h:x=SiPL4pKUs״[CVđiFtTQӮ7^9w,%kG[=Sb;)U҉sG?q$[J~']pLߏ|?b@;%%r">*B,MNÃJj[ n Ox|8_)i˶UH7``B `e+ȯk , yM=p*YtkgW?@oUw|hX˿bu(~Aź^ D̐*PcCza0PrJe%Z/9*lGA但zsFP8Ԉ]teA]t&z}]ۿţ/|fHßkg˕bqV * 3?Q7 ,*e,kF HZc$Ap`FV̳S@R քTbVLI@Htnb K|n˒#W|xni'L7 !CDN "ÅxSOЦ?b]n hCl&OdH92]w\7fNWSh74F?8_ׅ9J7]wUH(-+. Z7D(XP蚔!VZ:qlHDF C94 \HcO*2˶^o[a%KEZZZlg=_uȭqnߓϕu ے`T[[ĶF1w̃J詸KVv6uMG/G0C@O:ʿ< aǚ)apbDddR޿Gj 4cTKrbNcNxyԻ5hmYh?Hy5Zl.px-럜K5K o{#Gs# 2v9Z!.-T[#{2|I)A*VEk6WBD%iJ~35Qo$KՀ1;g h uexHV 7NZr־sǥӦ>DD&7݊KȄ %/$DHwwIF)>Zx%!bmw':qTk(;b7a4!@,RhCj Fu#'.#{0roI]ӄhԐuodR|liMu,jpPp[1nOΝ7-K_ϼlq'}C>$L/% Nʺ/Y3lLdbyOTI͵xlCz+n4vA|Í;\u7sbn'CLmY5o fT0#CFS`7ZEPb 0k:V{]V~(iƪF$. HI@k asxjM29ew'o}8F1d /x!e6nu'_yiN$Ќ1`ӂPwbD)zi&a#uF1sde4Emg3U/mwZ0k48 HB K503 ^yl۴ Ϧs2wF2'Ww#1\{1BG~ְꏋֹ6"5/k]B m x.!bdh>4d_V~#d_d|6/9sV`ϻHy,˶Ol쳠8[nY9 9z J;2uB8jð3fmf<ŶS%399| }hN%c.Tgu`Vozێ޽DS`o]f5]J 7ؚQ3=CdFph/jsY3M"S!+-&ex,[ t$NĪ>'' ;;,ã!6v"K0%T2馉3qcWaW5Rgե%ye~6'gX`AKR0{]Y֩w4R gS"ɓ.\HYE1} )pcIZP5%rIS0n&HݘCRJt4}yMen\2=g. 6Mt0qׅtۅ@IDAT#7˄9H2]2uL\VK"Yk!D*ŝOhXhKXEg|m6Ĩ^Kgo,^OUǍhM4MF0(#Ǿ6OLj͖xL p-2#'Wͷ!:e888 5f`^.1mzٗ#_4`FA\َ@- )(<8j0)`-ZAxu +STp.:P0*S&J9:)Wɩ@[)r%3%SL|spMO[1X6nvuɫMggpT jS\Y0*TGΣH2ՖyZxD J1Nt LEbM^#mzJ{ XozcO:E\n *z̗#Wie?bw-,@/zS|ԃpb&i| 09gL7x Qlnn0,4S !H@|'XM;f2 c{WoGS[1pv- !FZ{ +sWn ->>\X:^Aqa ?0fޓ;Gu#&޸czq7CB,X_}ʶkYll)A(!-651b1=4=g +q".J<]R(t~FijΑIpګ+v# 9v?i8%'WW9#)o>>_&**r޲i:^0p#!]H4ap< x\'1U-ZvT <9 jtz>`K1>[?E -,D*a6?$Ao'ݳ|i<Ψ=Bdll;{4 =XN蛵 a'u3IA@ "_Fm>k\i|,7 {=_-sޣ׼.^RR1>s?w<Ǒ8mw a">=oG7k;;|E(,HWD Pjm'R)Ay_,֊vX^"hY +."ݠE!lsbj֘BBvUgNi=:NE#4JHa. `BHpaGRٯ3f7๐n<ﯸ`<БbZJreAiSAOwߺerfB֧cF;aT6;Cd+WBS+<^ O5I7x[+ʟ#[[18] yvlE6Lha|)'593Qi7tcB 0F{h0aBQ]Nr0b )RZND40V@=Ł2xJg >0/ȨudKs\W̝ݖsGX$QǕQ\ .ẊR<}]\g֣8+fԲ=&vd]m]@t.L^ALݣu"LXG#T2U!&*u¯J^]l<<#0&w*$'H!]ΒI$gɭCrV c9U0gBl`_xX)2׫UWS{9+-**6YuhvRU:<<S#pUHX@'ĶEjzsB)bc:?sGT<ձ[+zF@~dF1Gp瑅웏|eS[B|FftQ}u:DZ21k H @+6,2bH mSYkH/Kx[{;@&Q0jBv̀şn}P&a%og{@Ɔ$%! TA1 "r2sWGN3F$HГ=XxP^TA,Zq~hyi@3hi,l-kz$8k7HQJsR܄ֵhPk5BVUbeV6aްu.5rN1"n ؑ圇j͡8id#IX%lݧye\s ~-N4/}vyʖnzs72f,mtFyD3GhCd>RCVSYu&%o߿{j1E2@h!rɐ=UFnGa-=U4HI?7w`\Rudd%*(D*Ԯ]K>gb;(3MTpf'O%Z$`f Z4z!,VV/v:qƫOs֘ ]ɼ -rɋ݊xxG-*6ZW~opK듍w}&-,.$R .jh58H\$ Ar\<tbS046-wR?g_ xjC9(m^';9u;> 8|4*0!_b >bHm*n6LUkp q`Xyjf!7+w N}#A9Yu29̝ В~@jLr%b|H9^U`y+7}3MTW:j; L7yt. Q]5_%VqC] ;?ʀ[k_L,k[ÿwbGgϱ@tŗ pZ%y@B:~b[ yb89Zp!fV.tsq33KEFϴ7Hy҄*.P#p.Ibqͬ5eS4!i7_frk4(5%vlXD@=d9ݡ=J!|:YͭwC'z,|j˄Oߟ٦ٷa ~s1v1m[;9&n9}kq̱ϑu+g`'oy*~I3]}OSk _:i:Q x( ?ǫϽᆟ-9 ,KPk^ ]7;$D1!UY+# O>b\]q!9cF_ah'"&eD\>~s^Yl#`At8vW4ꠝ"):,{98s{15XIaXiD㩁OVz4>6iלhpr,@+2xOޤ3ȃ&++Mㇲy¡!I=77~0WݗS8f8΃[dF\sm{m:ebcZh>,uVr԰KM baªj; V~70!n8 9R>1x?Lu߃~ 9A,Pp-/š !umP @A5YquޛIvj_{F댄@ A-Đ$$K25bm {5 `1;@I#Y{:K% ߙZNUw|C!>jaH} fK0.g3{h.lfji&MXQ2!mn﬩,6:=bp5dIumrGҳ֑s-m/sP]YZ+('3s389;HHoxNv-]19iA>f?6[[_?41mRo[%,sK0] 0[j9Ki&C1mO#g 4m/p]d;ƣ۷Xsheա!oB !:umC|h[8hw=_+l1e䷘A{+t`PZ LK=UfV<V̞),lTa1s/>=5M{/̝Ljw}vW}B;?ie.v#Q%u2~ 2K5|4|,Lj78T V6@B#(,} i4~ r6 zRLSum٣azW@#n/4]{GFN 1_.$B1T{&:1ЪRQK *78|v NPSn :NR]9s,#'҅zѩN`Cmᒡ1t{@:X`}Fo'1s&Q:4vl1Q }1CMLk/7kE3i\r>f* 5H|kX4@66fߝ״Aezu, ]Qqā(r`Xrml νEvA-wp'͙n\bgdyT/G胅_ޙRZ5+ ]Z ^nsQ/F;z G5F?ӒjŔFBۃ;DJ~ud}^=6̓L^XXbX?v每Nb*pV9!xұ?_ؗI]^G5ϥy0C\<:L6M}T[]96Y*c{xN صz+,}Ծ]?w,zsh|˿MzoɃLk;'?t1fr)CuDG]g{hPSY#}]u՗n,V9[kxauT๩󿚮ii#eVDA"[XV0y#:k zef:fmq,QqIբ).CIx\LNݪL&Cpb尿7OpfV _+:EͭѼ2s| t?"g`􄋓*T=<1, H:ONgL֖椼k06VnC&T*v[Ko TWnm1{ёdc$%$I! ٿJ4`@n !W!9Ƨ]d)n ŐC3~ >.0 /!Er gDg ׬ N*߰9/d8 ufm}= =\6t9 dW_o'S6skQU&2:HM:b?}?j}|̙!P>yhn8??on`IId\7B&~4)BEarAZֲ +y8nxH:ԾAqJLL7ŮrOGQӷnfoߦ2}UCM[_{}l {cTʼnd'56jwU@ ])h iA0LgblD$` />str1lM?;­=|xIǘY bWxMK\yxy3(i'ktH @PH5\?VRqU-KgԗSѧ"3Ţ3Ux<+\V@NDϜ/Tx>UݰWk>gS^AM]e9+%y|ESq0-h-~c6_* !N}hZ*p@eѪ4[QvϾ! 3r<\bRя}ɩ&wᄀߩB/@P(~#,Bd\Oi:'zMuPTh;ܢ u7:,%DO6wswdGOʣ-J{^e a  ^JM <uYIQaw#{;aKZ>. W: k@:@#> p&X9c/1lܱJ/b"&ҁuZ9;^5u4jћACNQZ5NK& .8cu?A=!yA>w9G}BL7Z[i0RsIP`]^893;^4ʿBR,{νG| O RH奌~4 og*FR8)RpK28:wO07_fʵ wt Oy޷6A@pEOc x8Y! s4sooy ~MP^o:"ߪ8'i 66/|>ZN>|fCJU NK,$:i\I1png,FNom*-8\"cdUӧ7H~o(JMB/h4EQddg KdO~ (Xl@T@@ #0BGm-|%I#jknlI's??xha%.y>-Y ,~ߕpX֬#TQ4NLg N @@)H^^,+Y {gy<ȵ0ѵǟg^ToW GT_FēY^PPyYh6} ݰ>#4:dB۬r ~RsCq˖?CCKADw[=YÀs;5M=d)l:$oe*m[e,ݻ{{3ͳK ۋ,m'i1VT {lԉh._ՂM4дzǎ9y?ABSIjwJ)W_~%9d`nFdd'溜1ZWqy~U>Tk ov E s mq/T(.ߑl&)Uy+*B.y's4梼qz'_֏Cal^T۷~mwxwIC^Q-HRġ;m~IѰ%1 f*N9ˑE8L3vq9JޢG$4l^VN>,b,]uು_gwI6ڸt$B>oTϚNː*ҷz\W>38.Mz.Lu&™I,[]K4S& :/: =# ΰS}o#>p=SHfy~*Paܭ@"߾xnOfhnhGe9! 8nLDàD78gi i&BF+}ݐ|??μPj+9 hah b9 =kԷߧIjcSLZ`Y&{UXck!ѫf1*pI3iZ'&S! H蜏GXAhJPO镳gs$U4E_L# K;HJJg" zT{^<Ԁks a/"XMw-+ֺl՛H= ;KDc$_T!.'jXBqtjyShZd6/e*_tF(YqɯOw}рfAVh#WJgq5ɐc2b`> \$8TфȱkQ3,'RS1goi*m\˺<;h2W^=kzk;z+i[W9w#Dpdep|ШP&J;%7]9--M@ zI=@z `6b=#tcmh܎?_z*ȭT86sZ־-Sj.`ep̗r10dHLIm3}%XF _p\:HGhN[2* 9ĥWb*`*`*p~PYG3U.0n|,IRȬg?:dM!ؓ$G_En80UWM9_=eb*paWͅ]]3>;;]^\`N?U̗6vpʅMwuNj06bmMeen=Lg$Xt&-{vCTϮ_}\~inH;bgWo6wfEߕ~5u7 /tKKBuڵd>#յ=i|Ƿw`*`*mSs xM1%fuD87 ƽeث?-+xWx\5!v%?ANg^&b"p h- $ p^:oQef1+pNixHe}zf3|d]T&'&d! nHK׍_TQ;Wxݫ\);we;dycHhXl1!Iu6h䃌#fnqFh7-X5Ls=TM_*oY+w4ӽM]@WxdKYA]ےrIifbeǑ͡),zu\\npch~JEl  ĵH Vpts36eV:ThK$?OFs]& Yunrs+Vet*~r]v3Sl (VY_}[XikG`m[7o GH{(X.J ZOK]U#E#ĎlnU{&ws] WЂH)Q XYY>v&[?A[QWJaX0-:;=H&;r0Q9YbB.,ە0˗zF6f956m2׈, MH5϶t`<Oz޺Y6@,F|?j,x-ov"º+RVxrćP`ՍCu{c"J#&*Q6[.ncpBqA+QIH!rX,:f(d03ZLufޡ4\$y,=57EsW.fb7'>!+˫#/ӈ dDw0e]Egnod73Gn]* jw?OuGy^to/Xٶ*HYrdrzNupPVTؗgr l$LgdJΈfv)+DfgT5O200 ߳n)hdnstMHI<[in)~hts(Vn $gnL" ʮCGf;= #i;py&Sm\[o5;N^+^uI)|u رcV.?px)OB$ęɲ7\Y_[%ND4D B^ ؤ9Iflmn:յSA8yG,`y3>X:'s-/ #e%#T0!~ U\mo-J4+hWp46>:T WR#kyJ1lSSmS3=zaӱ.&$H,08=Z)嬣=ktRi23@ aH4$ -H}W&(;d׾pK79Vm^-tlaw(^a*`*p"@K/3n-kgG$*d vC6>$#r̦Tk]j3ad'̓ȩ0р VݒGO8bNw3DGf= ,ls;$=(v0EI(E3h/ц8R=u2tΧs#ALZe'HLtӕ: vXFV[[8J6m5dSYaWb w68xyNE8f]C!R ,pYjѸl00%L $oPLI/I l.-IpH^f10x\ԍ<䗿ӳ7OLN| ;ή< vj~7}'SÏ:w~^mɩ4[sh]uUiT7hJe*'sU{+.{#CYǽMsiT`iaN}(0UdNnT,{/4TN]dpL !;H+!l:) l 8L0~f3ً{Lb觱SL}Gi :"!qL[Io.P2菘9g \98B3(9V4KV+x>s,u꾡r3tBi79XݴzgȎ͟۱g=Tyt vLM Uّf+ZِfS'O@zfjF4 j_&zi75p&,yF){ :c &Δ ?4בSSr큽a"??Zo }fo /*:嗩 ~)xyK# 'hG\:SrIJ$y"qxloRE4d=G.27WW}FO{TC\UʀuBPv8 8>R9ɸ>1 }IP@@2CH7|,n.Y4{H>K7ُshb*`*>)p!بP^]cs\fr6'NXI$N7e4'&!t-|B9Tp i ƥ\V^rœTKe%w+8͍5m_qrrru+2j /A4SÞdIQ\9 8f.kդg̝.\ Wʋ~lS5mKZmvfCd!͆itюj=r=sLLm ~ܦ9ѯISҎ:$tfy ` '16ӲRZ5C;Rܗ4*!e@2EZo4ؖA/Q lB+g~l lH|+7#ODԌ6Sa({8hfz 3Op[N$kL9xigA$ 'OƖl$[ٻs%!"yp-/0}/}KKao $. /SGnNgsthDuш,34ᙑqaI t`؍c; h67X~Mo-SS}*}>rKV4ꝡtd 1Ag 0#Xd{lYfXvzi+#3= ht!Jh\:l l lQ޷}%? ubьFYS>`ovISD*%.,:nV+:*0SIdxF!8~,>)+1;UhlќxiYI:[i7-g?߷4}ԀU]ZPK^3;+ȱyi03^m޽ q&t#UC$Al(@<4wtGCiVD!r#G6didu4"6:r'fMdp1E80ѫ#+1!3㞗gsMLi fß Bv{^Clvϳ瀁[u͚=LtTdvqмF:>]VZBCY׈iZzd 6¬)m0JyT`U nS \km߹o+( ng&z[rGKoL]p]H[Ζ2:e}&vYO2%;h;{/] Be}rΓ‰3n:Z[,%NZg,_Ħ:l=폻!|cd://8dD a@z" ;X%IDϤlC)u@2xsIo'q荛[dV^LJ!ITH< uB=Ee/fdugY(d'$M|!%9u+7DYmodqʆ3+1za01GŃk'.m̫,g𪖙I[xd3M oUzU* 00aI0T8N`l+D1^G| :BToC-̲M*nF/v~h666L;kTRE!\ ݱg|>c[}Wrj铛zW$ ܶ Z.Itq3iZ72QFUǤ {Gc0kBYj>#hjd pRr׉ګd/mꀁ#;n 47-嘬a2`QhTЫ''WętU9b/̡ xc$ ,gMs/S,6 b*N s]6d:.qϽ |YF+ 6a)j’I`2[1W_eD"C)m*840%<7ؖ=*N˺ba!^΍_懩yW`[h ڷo$+ۃY 7f(HAfoxDOq#`$0@ơ3Ol-TYk8gIGER)` LCls_N #8ﯲysDW}e'GFjs%kfjV4. il5v(pW赥!G&,<9=-)@ء/A'b .ip8s,i%ٵk왝b e Vk.AJ4Mð^K*twoo-|X,!y[OwDYt!-Rr<-tM.+5*nF6zgη/1ۊJu2t3hՊYLLL~~LJ:W} @ClT]eUC>:9FrɡS ƏQ<4Y u&>S8+s{CCX<6]z^aSmR wye魟_'&gk :v.2i ~lcId [ٯT}oHr:3 JbufE#;$ )4 ^|p1)leK$6,f2~;wB`{q=1v3$U,OY \]>ڜ$5 !iE#iM:]r0v>wCƓ{JwCɈiBUm8Bs"x~j`*`*`*+,:Gh|K~3~yw#CQeEs׭Q aN">Vn. V)g1P"Y,!0hb>bVeqPI+Qz"TYL. G@WN/_y}~%`msd%P.@^&6 ';0mV-b tk wȗ>@,pY`;>::Tfb-0)x0#nS`N%dz j;ͳky:֫fy pA U^B7w6}/Vآxc[`̴ R Hn5IyeA2"Pcpm, 㯎HBef:3%Rhiᓼ[s߮"m1+7}fMWL2 `Clu}&Z\H_,a!4 \x^xm#ҏ2har0yn#> siq&2T <DN._kOAplYv!%Gy lb +['hR٥apG<;pټKgrt3bd")mq&!Inp2$54u]UudL/BRku6yV`NBVh׹#[/?"wuŽvsV@urGfW١9Q,l|/#~n2z,%L;a<ĻSOTu4 NJFh+@#QԵwvؠ翻-OQm@ v7|5NgH(θ+y Ut.!2crU#Y Ǯ Ֆ΅QUV`>zC3k)m*aS.57\áG~S/h,۴Bp뭑h=bimj ^G$ScI 9\\FیP |miȭ$7ՓYxauUv1HJ0 U|]Ŝz!c]$$hޟR:Y1b!pt!b*pW)";9u!S[5jk81 q_1H^r`\:cewR_uL`'}A2]ˀAFtt?r|9kZH2ZR*h< L:g{Vw_PK ~<srKx|p0Ԇɑty0N200Gj{m^RN'1xs^@@Ho0:3?fn*`*}*_>>_wYe7(vO$@CwK ezIH|s],Pf.z茦ZZ\P8HjV=nxsS?r͆XHP:@8H`S[szbQwo7:pIӍ MC2{dI OG,KtS'%iuHl3v6 `Jd4B7v;r3A"IM,AGP=+lb 빐D'Dìs"&D]v2yXnad:uԘ:CDtD|f=<62-N0X07Y ^X8WSz>r tpkd7S`= v] -"]l:23RcӉ`Dt$*g&iGec +THtXyhR,۽ q?ifb'K,(=ǵӚFؗރ\Pܓ$d Nyqȡd!)C;xK7G;8YՙvC] {븐bFIDNtJ%;/wo>RWg.V"1;oǤmDW"D^&'3RB|%ϧdy~Tv`c2iȈ>nC*S139=\v/ή(jc/fl@h*4RNl4LLʕ$G?,U/Қr @ɅYq[!`l@r23Io"BA kRtxx&g@w Sҁ-{/pYbSmZt RK̦?㻩+{ ؈aK ˓+EZ#ך7f, skH7\&ed+cHZ$ZC\$z@lu//ݢJjD&Sm"ÿQX4ڌ.^Ўsn c77?LLLηQ|m8g]rQ|=h#2f+q~IG΀Ⱦsre1&\!`*PWڂg0s+2 ɩ\wʞd_t ]5@C hkg= UHJùši*+ ZeofvY&~V}̒R 3Y;شC;rڋW)BFv윐Lo;ݚlll, d2zx%FC[a8s Б#Wv[6x@03ƆC?!*sYr}rĦIp%i4I΀K&w+_K;ޒKWTr Y[ʩG7ބI*MNhh~ĩC/-1e'V 7ɸfvH̦B.|>ߙ4W4nI I&$s<;.ɱx (Ԙ)N'<Z5֪Ձ4oa:4q2^ii M"4 h!)ĨpYzFzRQiu4xA3qpHM3]tv PU.J˫+]R|0PPQyoT:RwvwxUWۧ>!}L 㛱%erH(JXSWߺK\nn13M 6]gBg<-x= b ǃ}s*pASٝN_*X f)4}ܑ|>(E`ZST1d0p!r?FI"9ӮRv(/hmgi1aq\508WJ_DG(8ӥdfbK2fH&-IYaEdd\r\oFr%Dy"+/H fCimlʃGOʱHC44R wL_{E*aꛉ>sN*x>g`S0j%RQ* <礜fyX$yd1'4Ч3C8h<ą#T[P@7zu\UG kȘx?Jz~SSS8=F%.{e-\D0*PP'SLT9$Ǩdvb^7`/]$Ր&E` V> !ʿ9)uYR;?r`wI<(H]}Ifzg fi@gGymNfq@ؓ&4+솃hbayBe47^]\7+ZPʤ_|N:$,f&ՇWF ̴,!=ʔ$S$h <p'jZa 6>?~Rŀ':;<aU,;2 НNcrVTtLphvcqBY>';:qq33Eٻ'Ot#2:ǯ{ )y=btm @wlᷦ8M#SlJs*pHXȑ g,pAJH,n-{p ؾn~|5c&vSUXtI5FL`0H A+nkO㫸Kf$TF[i/ GO.lg- g*`*`*pcu@@9wkiBz]?*2Y#zgL( `7geyQxAkbNq@иWk;囕vvLp!BmQ9'hi2aB<n[)pRa9ȰOYLLg6w旾}s?u;rhe遨4g(hNb&XDk0UpֆaguVK= 7Pmq"}{}mZQ#%".\wE2;^+ф|{cfHd|fT xIjFgB s$KJYz|rY">ąF4oѾQR?*֣PϜ3A,.l&I(dFTa?^-;fhLdFwފ%-}~-N4*V"<ЄH9 OqJIzL@^%O@hdd4' 3lTFv66f`slVT1h.f_!@7ZM= d<4iG_2}}?)H<$,O\mR[V7gףXx02_rZ )tkzoBsh!sp C:3+)QLƠt3 F(@V]0xk%4+8NΛߪYLLΫq¡[ouvҷo:- 3L.I0&*]LYU 4oWO7FM ӓoh ٜО8zHTۃn6yd)[ ,h]ۿ*'Wzb3{fB Y76 "mf$٩Úp|YScG{wWiqr F4wf;.Z+!,ϴ<ZXxL4%}&iR*$J0E 6~'v b 5sdc㜐0S6Il_V{<x4 >a g:Ns{#lr S#+|SpBmSSRdZ*}˟'W( 四`$gMݥ-}cp8M/! W!w i^vC8xJi(`(#&81z†p0y@ZՕ*}(5S2M2$;ɨx͸~YLLLΣ@/-ݤ.-s4va8^Z Ãu(؉RY]cZZ9Biw a]e/ɝy3gJޛͅǩ2a.UwQъz^"nFe07UѕJźC]|<~Kp4[͍qIln#O‚Fih~ͷW|}f,S%ڟ 2ڞQ ,#@JLl~At?qSz;Vcvtfð3Ȳ|GNav6O1n.\TDo7<@W+1@+n4Lrrّ< LA4.ہ hٗ^Vg"k}}~ QiBu:G/տ^!BI?95|ri:Pg K1'hq O,Md9pSi i<0u .TZx zo)^k ;v;S=uߩ-4'W6͸^[NZƟo_{vu;q DөJqnnS#{IVrL L\.$,"a(Kn#_qdh El< +|j:O RڽДK=Pˍ0^/= 5cxф%lOuQ=w/2:tfqn˧~$jWkV/^+)yrp>}HNwNIˏzH:hҌ]_¢rSkF.tt ځ,Z((9ˉ 9|ױב"fT xHdx+:x]گj."{X Sɍz6 P^O6 e}t(\;(jcELmKC%.pQ͇vZk"PI9f1vҊ8{vδ5p;!42ͬ o^jeRStK K(~yo*O{.ض6^ >QNsdwTU^aJsdx+]#e50LzU/I$IiDr;lhVAöB?whe`}K<Js!qlCyFH","V<{wC01쯟17_j%vZId;H;Js)96r(ZIfO86pAsXo+N = *_<5|Y5qra#{6o(|k>;r>GޞTBvĿՆA2/émN\wbONT.(d4HGrwecr=߽O4WG:.,WΓ<>h44%b_ScTfF*rvwHUj1Ǐ4s_gK~cCD Svwҙm5nI.p1gz>hJQ]дn)DCy DZ 9H5L>5p ghv!\DlV¶/m!p8KGo_ucsZ<FVcΛ6ݯm\9?{݃^'ɐvz|;(4m%Sl U*6| E RGe>Erм϶'Awl6I}K< Ff^gt#k4oOJ~tT<$?q-7@ywa`!`όeJ{O7\+tqWbz%s@S|U>/cR6T.Z1$Vfkt=c'V/+dqYj.p4(EC[zơ[6w$Q($z)Ŗ}\[Ϯ5'?'7C0S;Qxxߣ`3:cH,PF2K~Ds Im:~Y8ҋB7HtH۪ɟ-rm8m~#{|pwnSG z$עKҔ`<w޵siD߉Ɂ.cG@!te2"t$]9`@[#j4i|x:Ft|љ7N8C!C)OyNԹ[ ` ~⓯x*XiOW:=;1Y*Ey."v<9>UG%SC!S}5W)d 1L}Hh psE7zPD.@S*JB e؋;'c v\o 2Ryn$F2Xd-q*?ȰƅGDyO<"#,BMPO֬ zf~( WϳQv*|KUn ;E,{ #f 3JyוN-qm]7-|ѓ.V U 'Ì҉ܺu׊<~5AM!ܡApnfvh"X225.<k1ڻ8IX޺KNazٯ`3&]x7x(qi,ㄺd,Apb9H:aP[\ڐ˄uS%sL"d6#OJ ֍Nͤ*~TOȢ㢳܇S[tP@xZ"b7, C%O~wv;O^wzw|9Zj mvfh*mɲ\vš,H~|DdlR #eAZ+Hq M , jU.GFHHh! &X/ԯK6;`/?; s6Ɉtun^'4А"yԭBr)DwFAKxD.r[E{@hW1X:u%k+1}7+jو44[<~V,В+F>T;suJ1a9Hڶ!p)#mFd'%IwHH9Nl܄ 7b`IL]^&NjWd^ˣR,f0 v FchRՂ@y4o0`5{HO?=)tƴN4 ~ka_ЗvC#` Ӑh.ɋoݫQǝSr`{@f'#mԐ^+ SJZi{ңX\ɕ%zz)O6$<ѕʵHG/]VjWJT4ewɉ^}gh&+숆!"Y:g6^[?.ԩ;.9lsf"CQӕSRE]><ڕ% {rT3%ڟҏqL7!j zQv:x%7BEsh"=2ӈSgŖ;!`\%̻2 w|S fAkY\z!QtQ,u =w2.ž6Ԧ}TfUk_BoͲ%R( SrѧxĢ: dA$o&hYZ#%&jgaAjx:DdG/Y1Lsܘ&mZ_lC"`3p8J+ő$iF1J{iBUJcZ!UEOZ(!SQ>gGS8T]F^iJ^ K$ $= 5j3=',0IQP4F*R^2srҁ'*s,\VN::MZ; ~iMB 5_{޶ቶ匀odɗ=&:k3IH!k҆jh{O,ʲU5YsY2LIh]#pqM}jvۑx(bMwx~V"D=Yʞ Gb kNt~t{&Νr'$zov0.5fgJV\_/(u1sșmͩP*K@Lpu 6 1jADԅՒvܰq=' #tobPsL|mWÕ^aDƗL`+ŅغlW/|rǏK^!p0 DDx,K ^8(2d4c4iN4:ˉ]F_haO_=,yBN=|J|bv,TeNP>JsEYh᭠0XVLF'#ڨC;GsxIR~kc!;brio4 K nu`M i|)#{jyW@,] 9hIW`"t :$t,_3&/_ [WVɲ23=*ڷaJoe+ Ќ+>J<W?^wキ ^!`C@? ̙Yvſv4":I0ByАB9bja`PjRYehK|䁿xL9"a]Nn&_aQQC4NaHTѣȫjN4BV:~#v2Qjv0 Kه31cP~5jpt`,X$>DAt(WYy%F#cUeQYǪӖ%^_n.+K\D.U/Y/Y&.BFҭdvn:ʗ~Eɩʧ~읟ކ\:NJ Y8i=~/U_C[ݘ%=" 㮌2v6OOQ(&ܼzWFAX<꡴RM&FGa=CAKYuĤ)xf$3j;2U PsVk:6戈f5IKptfo(e~];͐ǒ9.ڿ&{aA ؔ7^/Z5Hn٤P %m,vehSJn̶%:$'=RuM4XY3b󀋟3 LSi!;6Hy}$Ek6$"%Aql5vT΍ONJhM!`\}U^_:(yS.mVꧪz(K<+Ű+BCNcyD&4VB#feS'%9Y]9TE9eJ1E 8wfT*k_73sm /?"{YyBgv;Ipn-JiiĴ~@qΦm"=*n&AW֒qSXv3v!`\lzwx[|7N ł,j!VZ 4@zsAyH'Lm{+zNߌ_lOz g@uɒMoGL8~]LJ̊SaGT*A{IDSJ@__O6e2B6UХp**-ɾߌR!Jtf1t­g/U28~  ~|hCC V͇@O=hsļޠm~ԫ_ԩhE6Cxz;!;+U_,gĄ܇Fm~\Ґʜ8 ;.xQ#Y`qD;\CˣhjeEbg!JZɏFqv v<]UTFB}/L`J V1?xF=_`la7߶ibߒC<"`M!_=v٭<2ѤЕ>|zWTYC@񥨢 ʜHЗ i>tuEUIQEZrQڗJ #qI|=*˷`(}[Kš~8 z@,. Fg~^C'*5ꈔKA>}{KN6.8e``=2Յkt2[*Ǭi!wzX>\UUfdj=Ƕ!N ekw>)[ kaVJ#gO9wFYՠ&#:bJ!,S8j1tC:}JUǴ:u6Aqqfq ʘ d?Wp~ ~ϖff~>9٭O0^`hiڙqj#jxb?.BZMP]H~)eqe:9Z1G&IctP;Z~uK}''4gD8˅:|S¤rPqNB*Y Fh8I-u46g.C0.w++i ;]7ה ̓ƶA-WRˬQ@ՂM-j={:[I xp_HZ)ʴ&lu`yk,p̠01{ MCQ^|h'6 #Pݻ2v&1TtQ20kt(>v42^JBZ$RPbsX\SP>.,<⧉ Sw[҆G|0<@vs̾># 7pPy Ž$ #6gFfgynCdtzx^P/cu 8^(br Fdu#SȑΈa{,wG^ $ֺ}_5hmN ӕǬBzRj5DxO\p[wIg+ l3 F UlvOaf]i_/Zad\JAM)$=,TW5BeTHwj2w( :@Uϴ*ϚIgKo;Hkm?TCRB9qt5coxb(8_NCl3 CB# ݤQ騹Z3>nRdӕKe'ϊ{bAt|+D:KpXo3#})f~X8Z% X9ϐjʑyyMnQ>_YT?-2mćH+Ӷ0^8ID IꉃgEˌ&k_^bs`Vlhb>jP59ǥZ!q dl65ft!؍'eNsNU`PmkPFrF; _grhG}4s.J"ϡDKv|}v&Gf# 8lC0 CC W;D:i-$( k5/Z/7""T€ImuScgYUeJ6g4׺6PkY"sAm"5k|.j >ܔ-,?Jg6o[0.Ye=g얗հN@iAªAq[g$%9#IZʡq|_^*Ks-{4J;Э< 3d!`tr͟1doU:ACE<ͼpf?j>rKEPU'eŊQx3GYuzw*1Q7տ]#J9AfнT,wDga&kWpTQL>L42 sI|0.nst۶E w!pI#`9޾Qv,kQD!䪱B]ƪggnr։Z #k$h[2J1NT҂hx>h;Hx&DCQ}L4z#|=3hTܼ"K PǾQ8>1夛vZ-9m!`\ 2 WZ). $[+.Y(X&qX℄ȁ~Z7C0. f`IJd p6QOiJR=f*tTK>qB:fk mv4]'ޮPDY탈'jݠ$ӳ Vl=ɔi+V^ڑbGt WlMPY1ZFh6CQ|:zy.lMGrG4 F4܀`N";GU>ڧ<Nɇ-|]0M]])[%=LA8Pe:!G@Qi3:&-eWC'Ն2%|dO9 g>Uҡ {9Ү2 wm ;`s0 ǷApfAv%^Nlq8G_sCZO.iοRhvm-JGD5- ˮCr?*{PuaBи9D($JMI3YnG1مPc:KK- ]T)1 A1$|`G>C36 C| /3؁@g.AHJpdmEe?ҹCJ]|(329FکAyTÂYII]aPn2 ҜErxڛV}Cߴ==L|g$J$֭ *Wt7Kҗe!`\Z6&ծVȑv=tNj-Aysg EsVtƖ:[uC8&V;yѤ#qe~Xx KhnFǩͨh&@<4'Vq+Gԁc8Jc459YlUu f=SE]Vt7SYK6C0mެu%u_\\rFF`!߇[/O.Z{x+Z-bY>q;P6-u5s4 98MF;3ו=WFw%"W=0.14.0 websockets==8.* wsproto==0.13.* # Testing autoflake black codecov isort pytest pytest-cov requests # Documentation mkdocs mkdocs-material uvicorn-0.11.3/scripts/000077500000000000000000000000001362253677400147675ustar00rootroot00000000000000uvicorn-0.11.3/scripts/install000077500000000000000000000001521362253677400163610ustar00rootroot00000000000000#!/bin/sh -ex python3 -m venv venv venv/bin/pip install -U -r requirements.txt venv/bin/pip install -e . uvicorn-0.11.3/scripts/lint000077500000000000000000000004701362253677400156640ustar00rootroot00000000000000#!/bin/sh -e export PREFIX="" if [ -d 'venv' ] ; then export PREFIX="venv/bin/" fi set -x ${PREFIX}autoflake --in-place --recursive uvicorn tests ${PREFIX}black uvicorn tests ${PREFIX}isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply uvicorn tests uvicorn-0.11.3/scripts/publish000077500000000000000000000015341362253677400163660ustar00rootroot00000000000000#!/bin/sh -e export VERSION=`cat uvicorn/__init__.py | grep __version__ | sed "s/__version__ = //" | sed "s/'//g"` export PREFIX="" if [ -d 'venv' ] ; then export PREFIX="venv/bin/" fi if ! command -v "${PREFIX}twine" &>/dev/null ; then echo "Unable to find the 'twine' command." echo "Install from PyPI, using '${PREFIX}pip install twine'." exit 1 fi if ! command -v "${PREFIX}wheel" &>/dev/null ; then echo "Unable to find the 'wheel' command." echo "Install from PyPI, using '${PREFIX}pip install wheel'." exit 1 fi find uvicorn -type f -name "*.py[co]" -delete find uvicorn -type d -name __pycache__ -delete ${PREFIX}python setup.py sdist bdist_wheel ${PREFIX}twine upload dist/* echo "You probably want to also tag the version now:" echo "git tag -a ${VERSION} -m 'version ${VERSION}'" echo "git push --tags" rm -r dist uvicorn-0.11.3/scripts/test000077500000000000000000000011311362253677400156700ustar00rootroot00000000000000#!/bin/sh -e export PREFIX="" if [ -d 'venv' ] ; then export PREFIX="venv/bin/" fi export VERSION_SCRIPT="import sys; print('%s.%s' % sys.version_info[0:2])" export PYTHON_VERSION=`${PREFIX}python -c "$VERSION_SCRIPT"` set -x PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov=uvicorn --cov=tests --cov-report=term-missing ${@} ${PREFIX}coverage html ${PREFIX}autoflake --recursive uvicorn tests if [ "${TRAVIS_PYTHON_VERSION}" = "pypy3" ]; then echo "Skipping 'black' on pypy3.6-7.1.1. See issue https://bitbucket.org/pypy/pypy/issues/2985" else ${PREFIX}black uvicorn tests --check fi uvicorn-0.11.3/setup.py000077500000000000000000000043041362253677400150160ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import re import sys import platform from setuptools import setup def get_version(package): """ Return package version as listed in `__version__` in `init.py`. """ path = os.path.join(package, '__init__.py') init_py = open(path, 'r', encoding='utf8').read() return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) def get_long_description(): """ Return the README. """ return open('README.md', 'r', encoding='utf8').read() def get_packages(package): """ Return root package and all sub-packages. """ return [dirpath for dirpath, dirnames, filenames in os.walk(package) if os.path.exists(os.path.join(dirpath, '__init__.py'))] env_marker = ( "sys_platform != 'win32'" " and sys_platform != 'cygwin'" " and platform_python_implementation != 'PyPy'" ) requirements = [ "click==7.*", "h11>=0.8,<0.10", "websockets==8.*", "httptools==0.1.* ;" + env_marker, "uvloop>=0.14.0 ;" + env_marker, ] setup( name='uvicorn', version=get_version('uvicorn'), url='https://github.com/encode/uvicorn', license='BSD', description='The lightning-fast ASGI server.', long_description=get_long_description(), long_description_content_type='text/markdown', author='Tom Christie', author_email='tom@tomchristie.com', packages=get_packages('uvicorn'), install_requires=requirements, data_files = [("", ["LICENSE.md"])], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Topic :: Internet :: WWW/HTTP', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], entry_points=""" [console_scripts] uvicorn=uvicorn.main:main """ ) uvicorn-0.11.3/tests/000077500000000000000000000000001362253677400144425ustar00rootroot00000000000000uvicorn-0.11.3/tests/__init__.py000066400000000000000000000000001362253677400165410ustar00rootroot00000000000000uvicorn-0.11.3/tests/client.py000066400000000000000000000107401362253677400162740ustar00rootroot00000000000000import asyncio import io import typing from urllib.parse import unquote, urljoin, urlparse import requests class _HeaderDict(requests.packages.urllib3._collections.HTTPHeaderDict): def get_all(self, key, default): return self.getheaders(key) class _MockOriginalResponse(object): """ We have to jump through some hoops to present the response as if it was made using urllib3. """ def __init__(self, headers): self.msg = _HeaderDict(headers) self.closed = False def isclosed(self): return self.closed class _ASGIAdapter(requests.adapters.HTTPAdapter): def __init__(self, app: typing.Callable, raise_server_exceptions=True) -> None: self.app = app self.raise_server_exceptions = raise_server_exceptions def send(self, request, *args, **kwargs): scheme, netloc, path, params, query, fragement = urlparse(request.url) if ":" in netloc: host, port = netloc.split(":", 1) port = int(port) else: host = netloc port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme] # Include the 'host' header. if "host" in request.headers: headers = [] elif port == 80: headers = [[b"host", host.encode()]] else: headers = [[b"host", ("%s:%d" % (host, port)).encode()]] # Include other request headers. headers += [ [key.lower().encode(), value.encode()] for key, value in request.headers.items() ] scope = { "type": "http", "http_version": "1.1", "method": request.method, "path": unquote(path), "root_path": "", "scheme": scheme, "query_string": query.encode(), "headers": headers, "client": ["testclient", 50000], "server": [host, port], } async def receive(): body = request.body if body is None: body_bytes = b"" else: assert isinstance(body, bytes) body_bytes = body return {"type": "http.request", "body": body_bytes} async def send(message): nonlocal raw_kwargs, response_started if message["type"] == "http.response.start": raw_kwargs["version"] = 11 raw_kwargs["status"] = message["status"] raw_kwargs["headers"] = [ (key.decode(), value.decode()) for key, value in message["headers"] ] raw_kwargs["preload_content"] = False raw_kwargs["original_response"] = _MockOriginalResponse( raw_kwargs["headers"] ) response_started = True elif message["type"] == "http.response.body": body = message.get("body", b"") more_body = message.get("more_body", False) raw_kwargs["body"].write(body) if not more_body: raw_kwargs["body"].seek(0) response_started = False raw_kwargs = {"body": io.BytesIO()} loop = asyncio.get_event_loop() try: loop.run_until_complete(self.app(scope, receive, send)) except BaseException as exc: if self.raise_server_exceptions: raise exc from None raw = requests.packages.urllib3.HTTPResponse(**raw_kwargs) return self.build_response(request, raw) class _TestClient(requests.Session): def __init__( self, app: typing.Callable, base_url: str, raise_server_exceptions=True ) -> None: super(_TestClient, self).__init__() adapter = _ASGIAdapter(app, raise_server_exceptions=raise_server_exceptions) self.mount("http://", adapter) self.mount("https://", adapter) self.headers.update({"user-agent": "testclient"}) self.base_url = base_url def request(self, method: str, url: str, **kwargs) -> requests.Response: url = urljoin(self.base_url, url) return super().request(method, url, **kwargs) def TestClient( app: typing.Callable, base_url: str = "http://testserver", raise_server_exceptions=True, ) -> _TestClient: """ We have to work around py.test discovery attempting to pick up the `TestClient` class, by declaring this as a function. """ return _TestClient(app, base_url, raise_server_exceptions=raise_server_exceptions) uvicorn-0.11.3/tests/conftest.py000066400000000000000000000071401362253677400166430ustar00rootroot00000000000000import pytest CERTIFICATE = b"""-----BEGIN CERTIFICATE----- MIIEaDCCAtCgAwIBAgIRAPeU748qfVOTZJ7rj5DupbowDQYJKoZIhvcNAQELBQAw fTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSkwJwYDVQQLDCBmcmFp cjUwMEBmcmFpcjUwMC1QcmVjaXNpb24tNTUyMDEwMC4GA1UEAwwnbWtjZXJ0IGZy YWlyNTAwQGZyYWlyNTAwLVByZWNpc2lvbi01NTIwMB4XDTE5MDEwOTIwMzQ1N1oX DTI5MDEwOTIwMzQ1N1owVDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNl cnRpZmljYXRlMSkwJwYDVQQLDCBmcmFpcjUwMEBmcmFpcjUwMC1QcmVjaXNpb24t NTUyMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALahGo80UFExe7Iv jPDulPP9Vu3mPVW/4XhrvmbwjHPSXk6nvK34kdDmGsS/UVgtSMH+sdMNFavkhyK/ b6PW5dPy+febfxlnaOkrZ5ptYx5IG1l/CNY/QDpQKGljW9YGQDV2t9apgKgT1/Ob JIKf/rfd2o94iyxlrRnbXXidyMa1E6loo1AzzaN/g17dnblIL7ZCZtflgbsgnytw UtwS92kTsvMHvuzM7Paz2M0xx+RNtQ2rq51fwph55gn7HLlBFEbkrMsfFj7hEquC vJYvyrIEvaQLMyIOf+6/OgmrG9Z5ioMV4WAW9FLSuzXuuJruQc7FwQl4XIuE8d0M jPjRfIcCAwEAAaOBizCBiDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB BQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBTfMtd0Al3Ly09elEje6jyl b3EQmjAyBgNVHREEKzApgglsb2NhbGhvc3SHBAAAAACHBH8AAAGHEAAAAAAAAAAA AAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBADLu7RSMVnUiRNyTqIM3aMmkUXmL xSPB/SZRifqVwmp9R6ygAZWzC7Lw5BpX2WCde1jqWJZw1AjYbe4w5i8e9jaiUyYZ eaLuQN7/+dyWeMIfFKx7thDxmati+OkSJSoojROA1v4NY7QAIM6ycfFkwTBRokPz 42srfR+XXrvdNmBRqjpvpr48SAn44uvqAkVr3kNgqs1xycPgjsFvMO7qZlU6w/ev /7QFUgtyZS/Saa4s3yRXHZ++g3SpPinrzf8VqmovL/MoaqB/tYVjOA/1B3QAkli6 DIl+99eKANlqARXzMeXvgLpcg+1oAw0hYjFpCtqKhovhQzqN6KlAbmJ9JWTk35x8 81nOERZH5dh6JZoHzaaB/ZMEjWkmHnyi4bf5dXiPLzfXJslbQKHhnSt4nfZiSodS brUVv/sux119zyUPe9iA6NNPFS/No1XOKcHrG19jiXTq/HIdJRoIrN6eRJDTRVK1 HyJ6uTvTJDu4ceBp2J1gz7R5opWbGyytDGg3Tw== -----END CERTIFICATE----- """ PRIVATE_KEY = b"""-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2oRqPNFBRMXuy L4zw7pTz/Vbt5j1Vv+F4a75m8Ixz0l5Op7yt+JHQ5hrEv1FYLUjB/rHTDRWr5Ici v2+j1uXT8vn3m38ZZ2jpK2eabWMeSBtZfwjWP0A6UChpY1vWBkA1drfWqYCoE9fz mySCn/633dqPeIssZa0Z2114ncjGtROpaKNQM82jf4Ne3Z25SC+2QmbX5YG7IJ8r cFLcEvdpE7LzB77szOz2s9jNMcfkTbUNq6udX8KYeeYJ+xy5QRRG5KzLHxY+4RKr gryWL8qyBL2kCzMiDn/uvzoJqxvWeYqDFeFgFvRS0rs17ria7kHOxcEJeFyLhPHd DIz40XyHAgMBAAECggEAZ1q7Liob/icz6r5wU/WhhIduB8qSEZI65qyLH7Sot+9p Abh51jbjRsbChXAEeBOAppEeT+OKzTHSrH6MjrtSa+WJQ3DTuCvGupae1k1rl7qV B8wV0zIOhjHQ/PuHAJOfCOK73ZclwXkhcLLvMaGcRLAgPaupj6GnGggEWPtqodDo qBOcixT3/lMW5M1GklkqJqbD8g8qcx7SFBwORJjpwVX84Ynnursu0ZvTfK/CzZTk D5t/UXyRV5Y5QBkzKIKzC0qUHv4eMIqkzlPBYx2PnAgrHokOm9/RS28yKT2DVPhw t311ZM6+Z5AxfKamARWZbZdC8RG5Qo0ujLmgogNn2QKBgQDsqpwO+/yJlvF81nf9 0Ye5o0OdOdD5q1ra46PyhQ56hIC5cRZx3s3E9hUFDcot81qj9nMTpSGJL5J6GqAY W7p3PbpYxT27MDjthgHHcZy7hu1M9no65ZAK1ElxVhKMgl89RQu/HQoa6Uh3qjbF X0edTBTBJoGOYQ1lVaoL8s307QKBgQDFjGtEKubolZ0OqFb361fDcYs0RDKNlNxy RIMM6Dhl0tgGHxNFuFNlLdjKyPEltfNaK0L0W3i3Ndf5sUlr2MuXYgO6RRqWo/D2 Tr2/jd6gsVKLK871WD7IS5SbCirCwuEsZQsZ2J2TWECoPqc8L3iZwyW6VGRkIW+K o2Sl7P4cwwKBgQCnhAt6P7p82S6NInFEY28iYwGU5DuavUNN9BszqiKZbfh/SiCM 8RvM8jHmpeAZrkrWC7dgjF20cMvJSddP5n2RsUuZUeNj/7oLxfK0bSJ3SgXlmADk d2EBiUmCw13VvuISyDCMUc25Rq5YpU6nXc2e9R8rqEnDscZ9l6kJVA+b8QKBgBAZ coB6spjP4J3aMERCJMPj1AFtcWVCdXjGhpudrUL3HO3ayHpNHFbJlrpoB+cX3f5C OlGpxru/optRzHcCkw0CSuV6TkFqmO+p2SLsT/Fuohh/eH1cNLmkFzdPa861jR5O GcqAcc8ZSSOs/3oTMFPvqHp3+DqE0w9MY552Ivt7AoGATtJkMAg9M4U/5qIsCbRz LplSCRvcarrg+czXW1re6y117rVjRHPCHgT//azsBDER0WpWSGv7XEnZwnz8U6Cn FCXoiqqEJuD2wLwQlhb7QVXYTMdCwfPj5WV7ARJO1N4ty3g8x+jnTQCVoMpdhgxC Sflxx+6bI4XMh0AsZhgtdW4= -----END PRIVATE KEY----- """ @pytest.fixture(scope="function") def certfile_and_keyfile(tmp_path): certfile = str(tmp_path / "cert.pem") with open(certfile, "bw") as fout: fout.write(CERTIFICATE) keyfile = str(tmp_path / "key.pem") with open(keyfile, "bw") as fout: fout.write(PRIVATE_KEY) return certfile, keyfile uvicorn-0.11.3/tests/importer/000077500000000000000000000000001362253677400163035ustar00rootroot00000000000000uvicorn-0.11.3/tests/importer/__init__.py000066400000000000000000000000001362253677400204020ustar00rootroot00000000000000uvicorn-0.11.3/tests/importer/raise_import_error.py000066400000000000000000000001001362253677400225520ustar00rootroot00000000000000# Used by test_importer.py myattr = 123 import does_not_exist uvicorn-0.11.3/tests/importer/test_importer.py000066400000000000000000000024621362253677400215610ustar00rootroot00000000000000import pytest from uvicorn.importer import ImportFromStringError, import_from_string def test_invalid_format(): with pytest.raises(ImportFromStringError) as exc_info: import_from_string("example:") expected = 'Import string "example:" must be in format ":".' assert expected in str(exc_info.value) def test_invalid_module(): with pytest.raises(ImportFromStringError) as exc_info: import_from_string("module_does_not_exist:myattr") expected = 'Could not import module "module_does_not_exist".' assert expected in str(exc_info.value) def test_invalid_attr(): with pytest.raises(ImportFromStringError) as exc_info: import_from_string("tempfile:attr_does_not_exist") expected = 'Attribute "attr_does_not_exist" not found in module "tempfile".' assert expected in str(exc_info.value) def test_internal_import_error(): with pytest.raises(ImportError): import_from_string("tests.importer.raise_import_error:myattr") def test_valid_import(): instance = import_from_string("tempfile:TemporaryFile") from tempfile import TemporaryFile assert instance == TemporaryFile def test_no_import_needed(): from tempfile import TemporaryFile instance = import_from_string(TemporaryFile) assert instance == TemporaryFile uvicorn-0.11.3/tests/middleware/000077500000000000000000000000001362253677400165575ustar00rootroot00000000000000uvicorn-0.11.3/tests/middleware/test_debug.py000066400000000000000000000033121362253677400212550ustar00rootroot00000000000000import asyncio import pytest from tests.client import TestClient from uvicorn.middleware.debug import DebugMiddleware def test_debug_text(): async def app(scope, receive, send): raise RuntimeError("Something went wrong") app = DebugMiddleware(app) client = TestClient(app, raise_server_exceptions=False) response = client.get("/") assert response.status_code == 500 assert response.headers["content-type"].startswith("text/plain") assert "RuntimeError" in response.text def test_debug_html(): async def app(scope, receive, send): raise RuntimeError("Something went wrong") app = DebugMiddleware(app) client = TestClient(app, raise_server_exceptions=False) response = client.get("/", headers={"Accept": "text/html, */*"}) assert response.status_code == 500 assert response.headers["content-type"].startswith("text/html") assert "RuntimeError" in response.text def test_debug_after_response_sent(): async def app(scope, receive, send): await send({"type": "http.response.start", "status": 204, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) raise RuntimeError("Something went wrong") app = DebugMiddleware(app) client = TestClient(app, raise_server_exceptions=False) response = client.get("/") assert response.status_code == 204 assert response.content == b"" def test_debug_not_http(): async def app(scope, send, receive): raise RuntimeError("Something went wrong") app = DebugMiddleware(app) with pytest.raises(RuntimeError): loop = asyncio.get_event_loop() loop.run_until_complete(app({"type": "websocket"}, None, None)) uvicorn-0.11.3/tests/middleware/test_message_logger.py000066400000000000000000000036041362253677400231560ustar00rootroot00000000000000import pytest from tests.client import TestClient from uvicorn.middleware.message_logger import MessageLoggerMiddleware TRACE_LOG_LEVEL = 5 def test_message_logger(caplog): async def app(scope, receive, send): message = await receive() await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi") caplog.set_level(TRACE_LOG_LEVEL) app = MessageLoggerMiddleware(app) client = TestClient(app) response = client.get("/") assert response.status_code == 200 messages = [record.msg % record.args for record in caplog.records] assert sum(["ASGI [1] Started" in message for message in messages]) == 1 assert sum(["ASGI [1] Send" in message for message in messages]) == 2 assert sum(["ASGI [1] Receive" in message for message in messages]) == 1 assert sum(["ASGI [1] Completed" in message for message in messages]) == 1 assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 0 def test_message_logger_exc(caplog): async def app(scope, receive, send): raise RuntimeError() caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi") caplog.set_level(TRACE_LOG_LEVEL) app = MessageLoggerMiddleware(app) client = TestClient(app) with pytest.raises(RuntimeError): client.get("/") messages = [record.msg % record.args for record in caplog.records] assert sum(["ASGI [1] Started" in message for message in messages]) == 1 assert sum(["ASGI [1] Send" in message for message in messages]) == 0 assert sum(["ASGI [1] Receive" in message for message in messages]) == 0 assert sum(["ASGI [1] Completed" in message for message in messages]) == 0 assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 1 uvicorn-0.11.3/tests/middleware/test_proxy_headers.py000066400000000000000000000017621362253677400230520ustar00rootroot00000000000000from tests.client import TestClient from tests.response import Response from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware async def app(scope, receive, send): scheme = scope["scheme"] host, port = scope["client"] addr = "%s://%s:%d" % (scheme, host, port) response = Response("Remote: " + addr, media_type="text/plain") await response(scope, receive, send) app = ProxyHeadersMiddleware(app, trusted_hosts="*") def test_proxy_headers(): client = TestClient(app) headers = {"X-Forwarded-Proto": "https", "X-Forwarded-For": "1.2.3.4"} response = client.get("/", headers=headers) assert response.status_code == 200 assert response.text == "Remote: https://1.2.3.4:0" def test_proxy_headers_no_port(): client = TestClient(app) headers = {"X-Forwarded-Proto": "https", "X-Forwarded-For": "1.2.3.4"} response = client.get("/", headers=headers) assert response.status_code == 200 assert response.text == "Remote: https://1.2.3.4:0" uvicorn-0.11.3/tests/middleware/test_wsgi.py000066400000000000000000000046641362253677400211530ustar00rootroot00000000000000import sys import pytest from tests.client import TestClient from uvicorn.middleware.wsgi import WSGIMiddleware def hello_world(environ, start_response): status = "200 OK" output = b"Hello World!\n" headers = [ ("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(output))), ] start_response(status, headers) return [output] def echo_body(environ, start_response): status = "200 OK" output = environ["wsgi.input"].read() headers = [ ("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(output))), ] start_response(status, headers) return [output] def raise_exception(environ, start_response): raise RuntimeError("Something went wrong") def return_exc_info(environ, start_response): try: raise RuntimeError("Something went wrong") except: status = "500 Internal Server Error" output = b"Internal Server Error" headers = [ ("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(output))), ] start_response(status, headers, exc_info=sys.exc_info()) return [output] def test_wsgi_get(): app = WSGIMiddleware(hello_world) client = TestClient(app) response = client.get("/") assert response.status_code == 200 assert response.text == "Hello World!\n" def test_wsgi_post(): app = WSGIMiddleware(echo_body) client = TestClient(app) response = client.post("/", json={"example": 123}) assert response.status_code == 200 assert response.text == '{"example": 123}' def test_wsgi_exception(): # Note that we're testing the WSGI app directly here. # The HTTP protocol implementations would catch this error and return 500. app = WSGIMiddleware(raise_exception) client = TestClient(app) with pytest.raises(RuntimeError): response = client.get("/") def test_wsgi_exc_info(): # Note that we're testing the WSGI app directly here. # The HTTP protocol implementations would catch this error and return 500. app = WSGIMiddleware(return_exc_info) client = TestClient(app) with pytest.raises(RuntimeError): response = client.get("/") app = WSGIMiddleware(return_exc_info) client = TestClient(app, raise_server_exceptions=False) response = client.get("/") assert response.status_code == 500 assert response.text == "Internal Server Error" uvicorn-0.11.3/tests/protocols/000077500000000000000000000000001362253677400164665ustar00rootroot00000000000000uvicorn-0.11.3/tests/protocols/test_http.py000066400000000000000000000537261362253677400210730ustar00rootroot00000000000000import asyncio import logging import h11 import pytest from tests.response import Response from uvicorn.config import Config from uvicorn.main import ServerState from uvicorn.protocols.http.h11_impl import H11Protocol from uvicorn.protocols.websockets.wsproto_impl import WSProtocol try: from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol except ImportError: # pragma: nocover HttpToolsProtocol = None HTTP_PROTOCOLS = [p for p in [H11Protocol, HttpToolsProtocol] if p is not None] SIMPLE_GET_REQUEST = b"\r\n".join([b"GET / HTTP/1.1", b"Host: example.org", b"", b""]) SIMPLE_HEAD_REQUEST = b"\r\n".join([b"HEAD / HTTP/1.1", b"Host: example.org", b"", b""]) SIMPLE_POST_REQUEST = b"\r\n".join( [ b"POST / HTTP/1.1", b"Host: example.org", b"Content-Type: application/json", b"Content-Length: 18", b"", b'{"hello": "world"}', ] ) LARGE_POST_REQUEST = b"\r\n".join( [ b"POST / HTTP/1.1", b"Host: example.org", b"Content-Type: text/plain", b"Content-Length: 100000", b"", b"x" * 100000, ] ) START_POST_REQUEST = b"\r\n".join( [ b"POST / HTTP/1.1", b"Host: example.org", b"Content-Type: application/json", b"Content-Length: 18", b"", b"", ] ) FINISH_POST_REQUEST = b'{"hello": "world"}' HTTP10_GET_REQUEST = b"\r\n".join([b"GET / HTTP/1.0", b"Host: example.org", b"", b""]) GET_REQUEST_WITH_RAW_PATH = b"\r\n".join( [b"GET /one%2Ftwo HTTP/1.1", b"Host: example.org", b"", b""] ) UPGRADE_REQUEST = b"\r\n".join( [ b"GET / HTTP/1.1", b"Host: example.org", b"Connection: upgrade", b"Upgrade: websocket", b"", b"", ] ) class MockTransport: def __init__(self, sockname=None, peername=None, sslcontext=False): self.sockname = ("127.0.0.1", 8000) if sockname is None else sockname self.peername = ("127.0.0.1", 8001) if peername is None else peername self.sslcontext = sslcontext self.closed = False self.buffer = b"" self.read_paused = False def get_extra_info(self, key): return { "sockname": self.sockname, "peername": self.peername, "sslcontext": self.sslcontext, }.get(key) def write(self, data): assert not self.closed self.buffer += data def close(self): assert not self.closed self.closed = True def pause_reading(self): self.read_paused = True def resume_reading(self): self.read_paused = False def is_closing(self): return self.closed def clear_buffer(self): self.buffer = b"" def set_protocol(self, protocol): pass class MockLoop: def __init__(self): self.tasks = [] self.later = [] def create_task(self, coroutine): self.tasks.insert(0, coroutine) return MockTask() def call_later(self, delay, callback, *args): self.later.insert(0, (delay, callback, args)) def run_one(self): coroutine = self.tasks.pop() asyncio.get_event_loop().run_until_complete(coroutine) def run_later(self, with_delay): later = [] for delay, callback, args in self.later: if with_delay >= delay: callback(*args) else: later.append((delay, callback, args)) self.later = later class MockTask: def add_done_callback(self, callback): pass def get_connected_protocol(app, protocol_cls, **kwargs): loop = MockLoop() transport = MockTransport() config = Config(app=app, **kwargs) server_state = ServerState() protocol = protocol_cls(config=config, server_state=server_state, _loop=loop) protocol.connection_made(transport) return protocol @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_get_request(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"Hello, world" in protocol.transport.buffer @pytest.mark.parametrize("path", ["/", "/?foo", "/?foo=bar", "/?foo=bar&baz=1"]) @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_request_logging(path, protocol_cls, caplog): get_request_with_query_string = b"\r\n".join( ["GET {} HTTP/1.1".format(path).encode("ascii"), b"Host: example.org", b"", b""] ) caplog.set_level(logging.INFO, logger="uvicorn.access") logging.getLogger("uvicorn.access").propagate = True app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls, log_config=None) protocol.data_received(get_request_with_query_string) protocol.loop.run_one() assert '"GET {} HTTP/1.1" 200'.format(path) in caplog.records[0].message @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_head_request(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_HEAD_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"Hello, world" not in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_post_request(protocol_cls): async def app(scope, receive, send): body = b"" more_body = True while more_body: message = await receive() body += message.get("body", b"") more_body = message.get("more_body", False) response = Response(b"Body: " + body, media_type="text/plain") await response(scope, receive, send) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_POST_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b'Body: {"hello": "world"}' in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_keepalive(protocol_cls): app = Response(b"", status_code=204) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 204 No Content" in protocol.transport.buffer assert not protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_keepalive_timeout(protocol_cls): app = Response(b"", status_code=204) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 204 No Content" in protocol.transport.buffer assert not protocol.transport.is_closing() protocol.loop.run_later(with_delay=1) assert not protocol.transport.is_closing() protocol.loop.run_later(with_delay=10) assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_close(protocol_cls): app = Response(b"", status_code=204, headers={"connection": "close"}) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 204 No Content" in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_chunked_encoding(protocol_cls): app = Response( b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"} ) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"0\r\n\r\n" in protocol.transport.buffer assert not protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_chunked_encoding_empty_body(protocol_cls): app = Response( b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"} ) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert protocol.transport.buffer.count(b"0\r\n\r\n") == 1 assert not protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_chunked_encoding_head_request(protocol_cls): app = Response( b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"} ) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_HEAD_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert not protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_pipelined_requests(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.data_received(SIMPLE_GET_REQUEST) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"Hello, world" in protocol.transport.buffer protocol.transport.clear_buffer() protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"Hello, world" in protocol.transport.buffer protocol.transport.clear_buffer() protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"Hello, world" in protocol.transport.buffer protocol.transport.clear_buffer() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_undersized_request(protocol_cls): app = Response(b"xxx", headers={"content-length": "10"}) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_oversized_request(protocol_cls): app = Response(b"xxx" * 20, headers={"content-length": "10"}) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_large_post_request(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(LARGE_POST_REQUEST) assert protocol.transport.read_paused protocol.loop.run_one() assert not protocol.transport.read_paused @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_invalid_http(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(b"x" * 100000) assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_app_exception(protocol_cls): async def app(scope, receive, send): raise Exception() protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 500 Internal Server Error" in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_exception_during_response(protocol_cls): async def app(scope, receive, send): await send({"type": "http.response.start", "status": 200}) await send({"type": "http.response.body", "body": b"1", "more_body": True}) raise Exception() protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 500 Internal Server Error" not in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_no_response_returned(protocol_cls): async def app(scope, receive, send): pass protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 500 Internal Server Error" in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_partial_response_returned(protocol_cls): async def app(scope, receive, send): await send({"type": "http.response.start", "status": 200}) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 500 Internal Server Error" not in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_duplicate_start_message(protocol_cls): async def app(scope, receive, send): await send({"type": "http.response.start", "status": 200}) await send({"type": "http.response.start", "status": 200}) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 500 Internal Server Error" not in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_missing_start_message(protocol_cls): async def app(scope, receive, send): await send({"type": "http.response.body", "body": b""}) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 500 Internal Server Error" in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_message_after_body_complete(protocol_cls): async def app(scope, receive, send): await send({"type": "http.response.start", "status": 200}) await send({"type": "http.response.body", "body": b""}) await send({"type": "http.response.body", "body": b""}) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_value_returned(protocol_cls): async def app(scope, receive, send): await send({"type": "http.response.start", "status": 200}) await send({"type": "http.response.body", "body": b""}) return 123 protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_early_disconnect(protocol_cls): got_disconnect_event = False async def app(scope, receive, send): nonlocal got_disconnect_event while True: message = await receive() if message["type"] == "http.disconnect": break got_disconnect_event = True protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_POST_REQUEST) protocol.eof_received() protocol.connection_lost(None) protocol.loop.run_one() assert got_disconnect_event @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_early_response(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(START_POST_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer protocol.data_received(FINISH_POST_REQUEST) assert not protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_read_after_response(protocol_cls): message_after_response = None async def app(scope, receive, send): nonlocal message_after_response response = Response("Hello, world", media_type="text/plain") await response(scope, receive, send) message_after_response = await receive() protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_POST_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert message_after_response == {"type": "http.disconnect"} @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_http10_request(protocol_cls): async def app(scope, receive, send): content = "Version: %s" % scope["http_version"] response = Response(content, media_type="text/plain") await response(scope, receive, send) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(HTTP10_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"Version: 1.0" in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_root_path(protocol_cls): async def app(scope, receive, send): path = scope.get("root_path", "") + scope["path"] response = Response("Path: " + path, media_type="text/plain") await response(scope, receive, send) protocol = get_connected_protocol(app, protocol_cls, root_path="/app") protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b"Path: /app/" in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_raw_path(protocol_cls): async def app(scope, receive, send): path = scope["path"] raw_path = scope.get("raw_path", None) assert "/one/two" == path assert b"/one%2Ftwo" == raw_path response = Response("Done", media_type="text/plain") await response(scope, receive, send) protocol = get_connected_protocol(app, protocol_cls, root_path="/app") protocol.data_received(GET_REQUEST_WITH_RAW_PATH) protocol.loop.run_one() assert b"Done" in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_max_concurrency(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls, limit_concurrency=1) protocol.data_received(SIMPLE_GET_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 503 Service Unavailable" in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_shutdown_during_request(protocol_cls): app = Response(b"", status_code=204) protocol = get_connected_protocol(app, protocol_cls) protocol.data_received(SIMPLE_GET_REQUEST) protocol.shutdown() protocol.loop.run_one() assert b"HTTP/1.1 204 No Content" in protocol.transport.buffer assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_shutdown_during_idle(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls) protocol.shutdown() assert protocol.transport.buffer == b"" assert protocol.transport.is_closing() @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_100_continue_sent_when_body_consumed(protocol_cls): async def app(scope, receive, send): body = b"" more_body = True while more_body: message = await receive() body += message.get("body", b"") more_body = message.get("more_body", False) response = Response(b"Body: " + body, media_type="text/plain") await response(scope, receive, send) protocol = get_connected_protocol(app, protocol_cls) EXPECT_100_REQUEST = b"\r\n".join( [ b"POST / HTTP/1.1", b"Host: example.org", b"Expect: 100-continue", b"Content-Type: application/json", b"Content-Length: 18", b"", b'{"hello": "world"}', ] ) protocol.data_received(EXPECT_100_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 100 Continue" in protocol.transport.buffer assert b"HTTP/1.1 200 OK" in protocol.transport.buffer assert b'Body: {"hello": "world"}' in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_100_continue_not_sent_when_body_not_consumed(protocol_cls): app = Response(b"", status_code=204) protocol = get_connected_protocol(app, protocol_cls) EXPECT_100_REQUEST = b"\r\n".join( [ b"POST / HTTP/1.1", b"Host: example.org", b"Expect: 100-continue", b"Content-Type: application/json", b"Content-Length: 18", b"", b'{"hello": "world"}', ] ) protocol.data_received(EXPECT_100_REQUEST) protocol.loop.run_one() assert b"HTTP/1.1 100 Continue" not in protocol.transport.buffer assert b"HTTP/1.1 204 No Content" in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_unsupported_upgrade_request(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls, ws="none") protocol.data_received(UPGRADE_REQUEST) assert b"HTTP/1.1 400 Bad Request" in protocol.transport.buffer assert b"Unsupported upgrade request." in protocol.transport.buffer @pytest.mark.parametrize("protocol_cls", HTTP_PROTOCOLS) def test_supported_upgrade_request(protocol_cls): app = Response("Hello, world", media_type="text/plain") protocol = get_connected_protocol(app, protocol_cls, ws="wsproto") protocol.data_received(UPGRADE_REQUEST) assert b"HTTP/1.1 426 " in protocol.transport.buffer uvicorn-0.11.3/tests/protocols/test_utils.py000066400000000000000000000040531362253677400212410ustar00rootroot00000000000000import socket from uvicorn.protocols.utils import get_local_addr, get_remote_addr class MockSocket: def __init__(self, family, peername=None, sockname=None): self.peername = peername self.sockname = sockname self.family = family def getpeername(self): return self.peername def getsockname(self): return self.sockname class MockTransport: def __init__(self, info): self.info = info def get_extra_info(self, info_type): return self.info.get(info_type) def test_get_local_addr_with_socket(): transport = MockTransport({"socket": MockSocket(family=socket.AF_IPX)}) assert get_local_addr(transport) == None transport = MockTransport( {"socket": MockSocket(family=socket.AF_INET6, sockname=["::1", 123])} ) assert get_local_addr(transport) == ("::1", 123) transport = MockTransport( {"socket": MockSocket(family=socket.AF_INET, sockname=["123.45.6.7", 123])} ) assert get_local_addr(transport) == ("123.45.6.7", 123) def test_get_remote_addr_with_socket(): transport = MockTransport({"socket": MockSocket(family=socket.AF_IPX)}) assert get_remote_addr(transport) == None transport = MockTransport( {"socket": MockSocket(family=socket.AF_INET6, peername=["::1", 123])} ) assert get_remote_addr(transport) == ("::1", 123) transport = MockTransport( {"socket": MockSocket(family=socket.AF_INET, peername=["123.45.6.7", 123])} ) assert get_remote_addr(transport) == ("123.45.6.7", 123) def test_get_local_addr(): transport = MockTransport({"sockname": "path/to/unix-domain-socket"}) assert get_local_addr(transport) == None transport = MockTransport({"sockname": ["123.45.6.7", 123]}) assert get_local_addr(transport) == ("123.45.6.7", 123) def test_get_remote_addr(): transport = MockTransport({"peername": None}) assert get_remote_addr(transport) == None transport = MockTransport({"peername": ["123.45.6.7", 123]}) assert get_remote_addr(transport) == ("123.45.6.7", 123) uvicorn-0.11.3/tests/protocols/test_websocket.py000066400000000000000000000375471362253677400221050ustar00rootroot00000000000000import asyncio import functools import threading import time from contextlib import contextmanager import pytest import requests from uvicorn.config import Config from uvicorn.main import ServerState from uvicorn.protocols.http.h11_impl import H11Protocol from uvicorn.protocols.websockets.wsproto_impl import WSProtocol try: import websockets from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol except ImportError: # pragma: nocover websockets = None WebSocketProtocol = None WS_PROTOCOLS = [p for p in [WSProtocol, WebSocketProtocol] if p is not None] pytestmark = pytest.mark.skipif( websockets is None, reason="This test needs the websockets module" ) class WebSocketResponse: def __init__(self, scope, receive, send): self.scope = scope self.receive = receive self.send = send def __await__(self): return self.asgi().__await__() async def asgi(self): while True: message = await self.receive() message_type = message["type"].replace(".", "_") handler = getattr(self, message_type, None) if handler is not None: await handler(message) if message_type == "websocket_disconnect": break def run_loop(loop): loop.run_forever() loop.close() @contextmanager def run_server(app, protocol_cls, path="/"): asyncio.set_event_loop(None) loop = asyncio.new_event_loop() config = Config(app=app, ws=protocol_cls) server_state = ServerState() protocol = functools.partial(H11Protocol, config=config, server_state=server_state) create_server_task = loop.create_server(protocol, host="127.0.0.1") server = loop.run_until_complete(create_server_task) port = server.sockets[0].getsockname()[1] url = "ws://127.0.0.1:{port}{path}".format(port=port, path=path) try: # Run the event loop in a new thread. thread = threading.Thread(target=run_loop, args=[loop]) thread.start() # Return the contextmanager state. yield url finally: # Close the loop from our main thread. while server_state.tasks: time.sleep(0.01) loop.call_soon_threadsafe(loop.stop) thread.join() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_invalid_upgrade(protocol_cls): app = lambda scope: None with run_server(app, protocol_cls=protocol_cls) as url: url = url.replace("ws://", "http://") response = requests.get( url, headers={"upgrade": "websocket", "connection": "upgrade"}, timeout=5 ) if response.status_code == 426: # response.text == "" pass # ok, wsproto 0.13 else: assert response.status_code == 400 assert response.text.lower().strip().rstrip(".") in [ "missing sec-websocket-key header", "missing sec-websocket-version header", # websockets "missing or empty sec-websocket-key header", # wsproto "failed to open a websocket connection: missing sec-websocket-key header", "failed to open a websocket connection: missing or empty sec-websocket-key header", ] @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_accept_connection(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept"}) async def open_connection(url): async with websockets.connect(url) as websocket: return websocket.open with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() is_open = loop.run_until_complete(open_connection(url)) assert is_open loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_close_connection(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.close"}) async def open_connection(url): try: await websockets.connect(url) except websockets.exceptions.InvalidHandshake: return False return True # pragma: no cover with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() is_open = loop.run_until_complete(open_connection(url)) assert not is_open loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_headers(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): headers = self.scope.get("headers") headers = dict(headers) assert headers[b"host"].startswith(b"127.0.0.1") await self.send({"type": "websocket.accept"}) async def open_connection(url): async with websockets.connect(url) as websocket: return websocket.open with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() is_open = loop.run_until_complete(open_connection(url)) assert is_open loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_path_and_raw_path(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): path = self.scope.get("path") raw_path = self.scope.get("raw_path") assert path == "/one/two" assert raw_path == "/one%2Ftwo" await self.send({"type": "websocket.accept"}) async def open_connection(url): async with websockets.connect(url) as websocket: return websocket.open with run_server(App, protocol_cls=protocol_cls, path="/one%2Ftwo") as url: loop = asyncio.new_event_loop() loop.run_until_complete(open_connection(url)) loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_send_text_data_to_client(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept"}) await self.send({"type": "websocket.send", "text": "123"}) async def get_data(url): async with websockets.connect(url) as websocket: return await websocket.recv() with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() data = loop.run_until_complete(get_data(url)) assert data == "123" loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_send_binary_data_to_client(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept"}) await self.send({"type": "websocket.send", "bytes": b"123"}) async def get_data(url): async with websockets.connect(url) as websocket: return await websocket.recv() with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() data = loop.run_until_complete(get_data(url)) assert data == b"123" loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_send_and_close_connection(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept"}) await self.send({"type": "websocket.send", "text": "123"}) await self.send({"type": "websocket.close"}) async def get_data(url): async with websockets.connect(url) as websocket: data = await websocket.recv() is_open = True try: await websocket.recv() except: is_open = False return (data, is_open) with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() (data, is_open) = loop.run_until_complete(get_data(url)) assert data == "123" assert not is_open loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_send_text_data_to_server(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept"}) async def websocket_receive(self, message): _text = message.get("text") await self.send({"type": "websocket.send", "text": _text}) async def send_text(url): async with websockets.connect(url) as websocket: await websocket.send("abc") return await websocket.recv() with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() data = loop.run_until_complete(send_text(url)) assert data == "abc" loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_send_binary_data_to_server(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept"}) async def websocket_receive(self, message): _bytes = message.get("bytes") await self.send({"type": "websocket.send", "bytes": _bytes}) async def send_text(url): async with websockets.connect(url) as websocket: await websocket.send(b"abc") return await websocket.recv() with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() data = loop.run_until_complete(send_text(url)) assert data == b"abc" loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_send_after_protocol_close(protocol_cls): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept"}) await self.send({"type": "websocket.send", "text": "123"}) await self.send({"type": "websocket.close"}) with pytest.raises(Exception): await self.send({"type": "websocket.send", "text": "123"}) async def get_data(url): async with websockets.connect(url) as websocket: data = await websocket.recv() is_open = True try: await websocket.recv() except: is_open = False return (data, is_open) with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() (data, is_open) = loop.run_until_complete(get_data(url)) assert data == "123" assert not is_open loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_missing_handshake(protocol_cls): class App: def __init__(self, scope): pass async def __call__(self, receive, send): pass async def connect(url): await websockets.connect(url) with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: loop.run_until_complete(connect(url)) assert exc_info.value.status_code == 500 loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_send_before_handshake(protocol_cls): class App: def __init__(self, scope): pass async def __call__(self, receive, send): await send({"type": "websocket.send", "text": "123"}) async def connect(url): await websockets.connect(url) with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: loop.run_until_complete(connect(url)) assert exc_info.value.status_code == 500 loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_duplicate_handshake(protocol_cls): class App: def __init__(self, scope): pass async def __call__(self, receive, send): await send({"type": "websocket.accept"}) await send({"type": "websocket.accept"}) async def connect(url): async with websockets.connect(url) as websocket: data = await websocket.recv() with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info: loop.run_until_complete(connect(url)) assert exc_info.value.code == 1006 loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_asgi_return_value(protocol_cls): """ The ASGI callable should return 'None'. If it doesn't make sure that the connection is closed with an error condition. """ async def app(scope, receive, send): await send({"type": "websocket.accept"}) return 123 async def connect(url): async with websockets.connect(url) as websocket: data = await websocket.recv() with run_server(app, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info: loop.run_until_complete(connect(url)) assert exc_info.value.code == 1006 loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_app_close(protocol_cls): async def app(scope, receive, send): while True: message = await receive() if message["type"] == "websocket.connect": await send({"type": "websocket.accept"}) elif message["type"] == "websocket.receive": await send({"type": "websocket.close"}) elif message["type"] == "websocket.disconnect": break async def websocket_session(url): async with websockets.connect(url) as websocket: await websocket.ping() await websocket.send("abc") await websocket.recv() with run_server(app, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info: loop.run_until_complete(websocket_session(url)) assert exc_info.value.code == 1000 loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) def test_client_close(protocol_cls): async def app(scope, receive, send): while True: message = await receive() if message["type"] == "websocket.connect": await send({"type": "websocket.accept"}) elif message["type"] == "websocket.receive": pass elif message["type"] == "websocket.disconnect": break async def websocket_session(url): async with websockets.connect(url) as websocket: await websocket.ping() await websocket.send("abc") with run_server(app, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() loop.run_until_complete(websocket_session(url)) loop.close() @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS) @pytest.mark.parametrize("subprotocol", ["proto1", "proto2"]) def test_subprotocols(protocol_cls, subprotocol): class App(WebSocketResponse): async def websocket_connect(self, message): await self.send({"type": "websocket.accept", "subprotocol": subprotocol}) async def get_subprotocol(url): async with websockets.connect( url, subprotocols=["proto1", "proto2"] ) as websocket: return websocket.subprotocol with run_server(App, protocol_cls=protocol_cls) as url: loop = asyncio.new_event_loop() accepted_subprotocol = loop.run_until_complete(get_subprotocol(url)) assert accepted_subprotocol == subprotocol loop.close() uvicorn-0.11.3/tests/response.py000066400000000000000000000026251362253677400166570ustar00rootroot00000000000000class Response: charset = "utf-8" def __init__(self, content, status_code=200, headers=None, media_type=None): self.body = self.render(content) self.status_code = status_code self.headers = headers or {} self.media_type = media_type self.set_content_type() self.set_content_length() async def __call__(self, scope, receive, send) -> None: await send( { "type": "http.response.start", "status": self.status_code, "headers": [ [key.encode(), value.encode()] for key, value in self.headers.items() ], } ) await send({"type": "http.response.body", "body": self.body}) def render(self, content) -> bytes: if isinstance(content, bytes): return content return content.encode(self.charset) def set_content_length(self): if "content-length" not in self.headers: self.headers["content-length"] = str(len(self.body)) def set_content_type(self): if self.media_type is not None and "content-type" not in self.headers: content_type = self.media_type if content_type.startswith("text/") and self.charset is not None: content_type += "; charset=%s" % self.charset self.headers["content-type"] = content_type uvicorn-0.11.3/tests/supervisors/000077500000000000000000000000001362253677400170465ustar00rootroot00000000000000uvicorn-0.11.3/tests/supervisors/test_multiprocess.py000066400000000000000000000007301362253677400232100ustar00rootroot00000000000000import signal from uvicorn import Config from uvicorn.supervisors import Multiprocess def run(sockets): pass def test_multiprocess_run(): """ A basic sanity check. Simply run the supervisor against a no-op server, and signal for it to quit immediately. """ config = Config(app=None, workers=2) supervisor = Multiprocess(config, target=run, sockets=[]) supervisor.signal_handler(sig=signal.SIGINT, frame=None) supervisor.run() uvicorn-0.11.3/tests/supervisors/test_statreload.py000066400000000000000000000021761362253677400226270ustar00rootroot00000000000000import os import signal import time from pathlib import Path import pytest from uvicorn.config import Config from uvicorn.supervisors import StatReload def run(sockets): pass def test_statreload(): """ A basic sanity check. Simply run the reloader against a no-op server, and signal for it to quit immediately. """ config = Config(app=None, reload=True) reloader = StatReload(config, target=run, sockets=[]) reloader.signal_handler(sig=signal.SIGINT, frame=None) reloader.run() def test_should_reload(tmpdir): update_file = Path(os.path.join(str(tmpdir), "example.py")) update_file.touch() working_dir = os.getcwd() os.chdir(str(tmpdir)) try: config = Config(app=None, reload=True) reloader = StatReload(config, target=run, sockets=[]) reloader.signal_handler(sig=signal.SIGINT, frame=None) reloader.startup() assert not reloader.should_restart() time.sleep(0.1) update_file.touch() assert reloader.should_restart() reloader.restart() reloader.shutdown() finally: os.chdir(working_dir) uvicorn-0.11.3/tests/test_auto_detection.py000066400000000000000000000031451362253677400210640ustar00rootroot00000000000000import asyncio from uvicorn.config import Config from uvicorn.loops.auto import auto_loop_setup from uvicorn.main import ServerState from uvicorn.protocols.http.auto import AutoHTTPProtocol from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol try: import uvloop except ImportError: # pragma: no cover uvloop = None try: import httptools except ImportError: # pragma: no cover httptools = None try: import websockets except ImportError: # pragma: no cover # Note that we skip the websocket tests completely in this case. websockets = None # TODO: Add pypy to our testing matrix, and assert we get the correct classes # dependent on the platform we're running the tests under. def test_loop_auto(): loop = auto_loop_setup() policy = asyncio.get_event_loop_policy() assert isinstance(policy, asyncio.events.BaseDefaultEventLoopPolicy) expected_loop = "asyncio" if uvloop is None else "uvloop" assert type(policy).__module__.startswith(expected_loop) def test_http_auto(): config = Config(app=None) server_state = ServerState() protocol = AutoHTTPProtocol(config=config, server_state=server_state) expected_http = "H11Protocol" if httptools is None else "HttpToolsProtocol" assert type(protocol).__name__ == expected_http def test_websocket_auto(): config = Config(app=None) server_state = ServerState() protocol = AutoWebSocketsProtocol(config=config, server_state=server_state) expected_websockets = "WSProtocol" if websockets is None else "WebSocketProtocol" assert type(protocol).__name__ == expected_websockets uvicorn-0.11.3/tests/test_client.py000066400000000000000000000014421362253677400173320ustar00rootroot00000000000000import pytest from tests.client import TestClient async def hello_world(scope, receive, send): await send( { "type": "http.response.start", "status": 200, "headers": [(b"content-type", b"text/plain")], } ) await send( {"type": "http.response.body", "body": b"hello, world", "more_body": False} ) def test_explicit_base_url(): client = TestClient(hello_world, base_url="http://testserver:321") response = client.get("/") assert response.status_code == 200 assert response.text == "hello, world" def test_explicit_host(): client = TestClient(hello_world) response = client.get("/", headers={"host": "example.org"}) assert response.status_code == 200 assert response.text == "hello, world" uvicorn-0.11.3/tests/test_config.py000066400000000000000000000031641362253677400173240ustar00rootroot00000000000000import socket import pytest from uvicorn import protocols from uvicorn.config import Config from uvicorn.middleware.debug import DebugMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.wsgi import WSGIMiddleware async def asgi_app(): pass # pragma: nocover def wsgi_app(): pass # pragma: nocover def test_debug_app(): config = Config(app=asgi_app, debug=True, proxy_headers=False) config.load() assert config.debug is True assert isinstance(config.loaded_app, DebugMiddleware) def test_wsgi_app(): config = Config(app=wsgi_app, interface="wsgi", proxy_headers=False) config.load() assert isinstance(config.loaded_app, WSGIMiddleware) assert config.interface == "wsgi" def test_proxy_headers(): config = Config(app=asgi_app) config.load() assert config.proxy_headers is True assert isinstance(config.loaded_app, ProxyHeadersMiddleware) def test_app_unimportable(): config = Config(app="no.such:app") with pytest.raises(ImportError): config.load() def test_concrete_http_class(): config = Config(app=asgi_app, http=protocols.http.h11_impl.H11Protocol) config.load() assert config.http_protocol_class is protocols.http.h11_impl.H11Protocol def test_socket_bind(): config = Config(app=asgi_app) config.load() assert isinstance(config.bind_socket(), socket.socket) def test_ssl_config(certfile_and_keyfile): certfile, keyfile = certfile_and_keyfile config = Config(app=asgi_app, ssl_certfile=certfile, ssl_keyfile=keyfile) config.load() assert config.is_ssl is True uvicorn-0.11.3/tests/test_default_headers.py000066400000000000000000000050211362253677400211700ustar00rootroot00000000000000import threading import time import requests from uvicorn import Config, Server class App: def __init__(self, scope): if scope["type"] != "http": raise Exception() async def __call__(self, receive, send): await send({"type": "http.response.start", "status": 200, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) class CustomServer(Server): def install_signal_handlers(self): pass def test_default_default_headers(): config = Config(app=App, loop="asyncio", limit_max_requests=1) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) response = requests.get("http://127.0.0.1:8000") assert response.headers["server"] == "uvicorn" and response.headers["date"] thread.join() def test_override_server_header(): config = Config( app=App, loop="asyncio", limit_max_requests=1, headers=[("Server", "over-ridden")], ) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) response = requests.get("http://127.0.0.1:8000") assert response.headers["server"] == "over-ridden" and response.headers["date"] thread.join() def test_override_server_header_multiple_times(): config = Config( app=App, loop="asyncio", limit_max_requests=1, headers=[("Server", "over-ridden"), ("Server", "another-value")], ) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) response = requests.get("http://127.0.0.1:8000") assert ( response.headers["server"] == "over-ridden, another-value" and response.headers["date"] ) thread.join() def test_add_additional_header(): config = Config( app=App, loop="asyncio", limit_max_requests=1, headers=[("X-Additional", "new-value")], ) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) response = requests.get("http://127.0.0.1:8000") assert ( response.headers["x-additional"] == "new-value" and response.headers["server"] == "uvicorn" and response.headers["date"] ) thread.join() uvicorn-0.11.3/tests/test_lifespan.py000066400000000000000000000103061362253677400176540ustar00rootroot00000000000000import asyncio import pytest from uvicorn.config import Config from uvicorn.lifespan.off import LifespanOff from uvicorn.lifespan.on import LifespanOn def test_lifespan_on(): startup_complete = False shutdown_complete = False async def app(scope, receive, send): nonlocal startup_complete, shutdown_complete message = await receive() assert message["type"] == "lifespan.startup" startup_complete = True await send({"type": "lifespan.startup.complete"}) message = await receive() assert message["type"] == "lifespan.shutdown" shutdown_complete = True await send({"type": "lifespan.shutdown.complete"}) async def test(): config = Config(app=app, lifespan="on") lifespan = LifespanOn(config) assert not startup_complete assert not shutdown_complete await lifespan.startup() assert startup_complete assert not shutdown_complete await lifespan.shutdown() assert startup_complete assert shutdown_complete loop = asyncio.new_event_loop() loop.run_until_complete(test()) def test_lifespan_off(): async def app(scope, receive, send): pass # pragma: no cover async def test(): config = Config(app=app, lifespan="off") lifespan = LifespanOff(config) await lifespan.startup() await lifespan.shutdown() loop = asyncio.new_event_loop() loop.run_until_complete(test()) def test_lifespan_auto(): startup_complete = False shutdown_complete = False async def app(scope, receive, send): nonlocal startup_complete, shutdown_complete message = await receive() assert message["type"] == "lifespan.startup" startup_complete = True await send({"type": "lifespan.startup.complete"}) message = await receive() assert message["type"] == "lifespan.shutdown" shutdown_complete = True await send({"type": "lifespan.shutdown.complete"}) async def test(): config = Config(app=app, lifespan="auto") lifespan = LifespanOn(config) assert not startup_complete assert not shutdown_complete await lifespan.startup() assert startup_complete assert not shutdown_complete await lifespan.shutdown() assert startup_complete assert shutdown_complete loop = asyncio.new_event_loop() loop.run_until_complete(test()) def test_lifespan_auto_with_error(): async def app(scope, receive, send): assert scope["type"] == "http" async def test(): config = Config(app=app, lifespan="auto") lifespan = LifespanOn(config) await lifespan.startup() assert lifespan.error_occured assert not lifespan.should_exit await lifespan.shutdown() loop = asyncio.new_event_loop() loop.run_until_complete(test()) def test_lifespan_on_with_error(): async def app(scope, receive, send): if scope["type"] != "http": raise RuntimeError() async def test(): config = Config(app=app, lifespan="on") lifespan = LifespanOn(config) await lifespan.startup() assert lifespan.error_occured assert lifespan.should_exit await lifespan.shutdown() loop = asyncio.new_event_loop() loop.run_until_complete(test()) @pytest.mark.parametrize("mode", ("auto", "on")) @pytest.mark.parametrize("raise_exception", (True, False)) def test_lifespan_with_failed_startup(mode, raise_exception): async def app(scope, receive, send): message = await receive() assert message["type"] == "lifespan.startup" await send({"type": "lifespan.startup.failed"}) if raise_exception: # App should be able to re-raise an exception if startup failed. raise RuntimeError() async def test(): config = Config(app=app, lifespan=mode) lifespan = LifespanOn(config) await lifespan.startup() assert lifespan.startup_failed assert lifespan.error_occured is raise_exception assert lifespan.should_exit await lifespan.shutdown() loop = asyncio.new_event_loop() loop.run_until_complete(test()) uvicorn-0.11.3/tests/test_main.py000066400000000000000000000050001362253677400167720ustar00rootroot00000000000000import threading import time import requests from uvicorn.config import Config from uvicorn.main import Server def test_run(): class App: def __init__(self, scope): if scope["type"] != "http": raise Exception() async def __call__(self, receive, send): await send({"type": "http.response.start", "status": 204, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) class CustomServer(Server): def install_signal_handlers(self): pass config = Config(app=App, loop="asyncio", limit_max_requests=1) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) response = requests.get("http://127.0.0.1:8000") assert response.status_code == 204 thread.join() def test_run_multiprocess(): class App: def __init__(self, scope): if scope["type"] != "http": raise Exception() async def __call__(self, receive, send): await send({"type": "http.response.start", "status": 204, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) class CustomServer(Server): def install_signal_handlers(self): pass config = Config(app=App, loop="asyncio", workers=2, limit_max_requests=1) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) response = requests.get("http://127.0.0.1:8000") assert response.status_code == 204 thread.join() def test_run_reload(): class App: def __init__(self, scope): if scope["type"] != "http": raise Exception() async def __call__(self, receive, send): await send({"type": "http.response.start", "status": 204, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) class CustomServer(Server): def install_signal_handlers(self): pass config = Config(app=App, loop="asyncio", reload=True, limit_max_requests=1) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) response = requests.get("http://127.0.0.1:8000") assert response.status_code == 204 thread.join() uvicorn-0.11.3/tests/test_ssl.py000066400000000000000000000032211362253677400166520ustar00rootroot00000000000000import contextlib import sys import threading import time import warnings from functools import partialmethod import pytest import requests from urllib3.exceptions import InsecureRequestWarning from uvicorn.config import Config from uvicorn.main import Server @contextlib.contextmanager def no_ssl_verification(session=requests.Session): old_request = session.request session.request = partialmethod(old_request, verify=False) with warnings.catch_warnings(): warnings.simplefilter("ignore", InsecureRequestWarning) yield session.request = old_request @pytest.mark.skipif( sys.platform.startswith("win"), reason="Skipping SSL test on Windows" ) def test_run(certfile_and_keyfile): certfile, keyfile = certfile_and_keyfile class App: def __init__(self, scope): if scope["type"] != "http": raise Exception() async def __call__(self, receive, send): await send({"type": "http.response.start", "status": 204, "headers": []}) await send({"type": "http.response.body", "body": b"", "more_body": False}) class CustomServer(Server): def install_signal_handlers(self): pass config = Config( app=App, loop="asyncio", limit_max_requests=1, ssl_keyfile=keyfile, ssl_certfile=certfile, ) server = CustomServer(config=config) thread = threading.Thread(target=server.run) thread.start() while not server.started: time.sleep(0.01) with no_ssl_verification(): response = requests.get("https://127.0.0.1:8000") assert response.status_code == 204 thread.join() uvicorn-0.11.3/uvicorn/000077500000000000000000000000001362253677400147655ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/__init__.py000066400000000000000000000002231362253677400170730ustar00rootroot00000000000000from uvicorn.config import Config from uvicorn.main import Server, main, run __version__ = "0.11.3" __all__ = ["main", "run", "Config", "Server"] uvicorn-0.11.3/uvicorn/__main__.py000066400000000000000000000000761362253677400170620ustar00rootroot00000000000000import uvicorn if __name__ == "__main__": uvicorn.main() uvicorn-0.11.3/uvicorn/config.py000066400000000000000000000265101362253677400166100ustar00rootroot00000000000000import asyncio import inspect import logging import logging.config import os import socket import ssl import sys from typing import List, Tuple import click from uvicorn.importer import ImportFromStringError, import_from_string from uvicorn.middleware.asgi2 import ASGI2Middleware from uvicorn.middleware.debug import DebugMiddleware from uvicorn.middleware.message_logger import MessageLoggerMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.wsgi import WSGIMiddleware TRACE_LOG_LEVEL = 5 LOG_LEVELS = { "critical": logging.CRITICAL, "error": logging.ERROR, "warning": logging.WARNING, "info": logging.INFO, "debug": logging.DEBUG, "trace": TRACE_LOG_LEVEL, } HTTP_PROTOCOLS = { "auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol", "h11": "uvicorn.protocols.http.h11_impl:H11Protocol", "httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol", } WS_PROTOCOLS = { "auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol", "none": None, "websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", "wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol", } LIFESPAN = { "auto": "uvicorn.lifespan.on:LifespanOn", "on": "uvicorn.lifespan.on:LifespanOn", "off": "uvicorn.lifespan.off:LifespanOff", } LOOP_SETUPS = { "none": None, "auto": "uvicorn.loops.auto:auto_loop_setup", "asyncio": "uvicorn.loops.asyncio:asyncio_setup", "uvloop": "uvicorn.loops.uvloop:uvloop_setup", } INTERFACES = ["auto", "asgi3", "asgi2", "wsgi"] # Fallback to 'ssl.PROTOCOL_SSLv23' in order to support Python < 3.5.3. SSL_PROTOCOL_VERSION = getattr(ssl, "PROTOCOL_TLS", ssl.PROTOCOL_SSLv23) LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, "formatters": { "default": { "()": "uvicorn.logging.DefaultFormatter", "fmt": "%(levelprefix)s %(message)s", "use_colors": None, }, "access": { "()": "uvicorn.logging.AccessFormatter", "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', }, }, "handlers": { "default": { "formatter": "default", "class": "logging.StreamHandler", "stream": "ext://sys.stderr", }, "access": { "formatter": "access", "class": "logging.StreamHandler", "stream": "ext://sys.stdout", }, }, "loggers": { "": {"handlers": ["default"], "level": "INFO"}, "uvicorn.error": {"level": "INFO"}, "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False}, }, } logger = logging.getLogger("uvicorn.error") def create_ssl_context(certfile, keyfile, ssl_version, cert_reqs, ca_certs, ciphers): ctx = ssl.SSLContext(ssl_version) ctx.load_cert_chain(certfile, keyfile) ctx.verify_mode = cert_reqs if ca_certs: ctx.load_verify_locations(ca_certs) if ciphers: ctx.set_ciphers(ciphers) return ctx class Config: def __init__( self, app, host="127.0.0.1", port=8000, uds=None, fd=None, loop="auto", http="auto", ws="auto", lifespan="auto", env_file=None, log_config=LOGGING_CONFIG, log_level=None, access_log=True, use_colors=None, interface="auto", debug=False, reload=False, reload_dirs=None, workers=None, proxy_headers=True, forwarded_allow_ips=None, root_path="", limit_concurrency=None, limit_max_requests=None, backlog=2048, timeout_keep_alive=5, timeout_notify=30, callback_notify=None, ssl_keyfile=None, ssl_certfile=None, ssl_version=SSL_PROTOCOL_VERSION, ssl_cert_reqs=ssl.CERT_NONE, ssl_ca_certs=None, ssl_ciphers="TLSv1", headers=None, ): self.app = app self.host = host self.port = port self.uds = uds self.fd = fd self.loop = loop self.http = http self.ws = ws self.lifespan = lifespan self.log_config = log_config self.log_level = log_level self.access_log = access_log self.use_colors = use_colors self.interface = interface self.debug = debug self.reload = reload self.workers = workers or 1 self.proxy_headers = proxy_headers self.root_path = root_path self.limit_concurrency = limit_concurrency self.limit_max_requests = limit_max_requests self.backlog = backlog self.timeout_keep_alive = timeout_keep_alive self.timeout_notify = timeout_notify self.callback_notify = callback_notify self.ssl_keyfile = ssl_keyfile self.ssl_certfile = ssl_certfile self.ssl_version = ssl_version self.ssl_cert_reqs = ssl_cert_reqs self.ssl_ca_certs = ssl_ca_certs self.ssl_ciphers = ssl_ciphers self.headers = headers if headers else [] # type: List[str] self.encoded_headers = None # type: List[Tuple[bytes, bytes]] self.loaded = False self.configure_logging() if reload_dirs is None: self.reload_dirs = [os.getcwd()] else: self.reload_dirs = reload_dirs if env_file is not None: from dotenv import load_dotenv logger.info("Loading environment from '%s'", env_file) load_dotenv(dotenv_path=env_file) if workers is None and "WEB_CONCURRENCY" in os.environ: self.workers = int(os.environ["WEB_CONCURRENCY"]) if forwarded_allow_ips is None: self.forwarded_allow_ips = os.environ.get( "FORWARDED_ALLOW_IPS", "127.0.0.1" ) else: self.forwarded_allow_ips = forwarded_allow_ips @property def is_ssl(self) -> bool: return bool(self.ssl_keyfile or self.ssl_certfile) def configure_logging(self): logging.addLevelName(TRACE_LOG_LEVEL, "TRACE") if sys.version_info < (3, 7): # https://bugs.python.org/issue30520 import pickle def __reduce__(self): if isinstance(self, logging.RootLogger): return logging.getLogger, () if logging.getLogger(self.name) is not self: raise pickle.PicklingError("logger cannot be pickled") return logging.getLogger, (self.name,) logging.Logger.__reduce__ = __reduce__ if self.log_config is not None: if isinstance(self.log_config, dict): if self.use_colors in (True, False): self.log_config["formatters"]["default"][ "use_colors" ] = self.use_colors self.log_config["formatters"]["access"][ "use_colors" ] = self.use_colors logging.config.dictConfig(self.log_config) else: logging.config.fileConfig(self.log_config) if self.log_level is not None: if isinstance(self.log_level, str): log_level = LOG_LEVELS[self.log_level] else: log_level = self.log_level logging.getLogger("").setLevel(log_level) logging.getLogger("uvicorn.error").setLevel(log_level) logging.getLogger("uvicorn.access").setLevel(log_level) logging.getLogger("uvicorn.asgi").setLevel(log_level) if self.access_log is False: logging.getLogger("uvicorn.access").handlers = [] logging.getLogger("uvicorn.access").propagate = False def load(self): assert not self.loaded if self.is_ssl: self.ssl = create_ssl_context( keyfile=self.ssl_keyfile, certfile=self.ssl_certfile, ssl_version=self.ssl_version, cert_reqs=self.ssl_cert_reqs, ca_certs=self.ssl_ca_certs, ciphers=self.ssl_ciphers, ) else: self.ssl = None encoded_headers = [ (key.lower().encode("latin1"), value.encode("latin1")) for key, value in self.headers ] self.encoded_headers = ( encoded_headers if b"server" in dict(encoded_headers) else [(b"server", b"uvicorn")] + encoded_headers ) # type: List[Tuple[bytes, bytes]] if isinstance(self.http, str): self.http_protocol_class = import_from_string(HTTP_PROTOCOLS[self.http]) else: self.http_protocol_class = self.http if isinstance(self.ws, str): self.ws_protocol_class = import_from_string(WS_PROTOCOLS[self.ws]) else: self.ws_protocol_class = self.ws self.lifespan_class = import_from_string(LIFESPAN[self.lifespan]) try: self.loaded_app = import_from_string(self.app) except ImportFromStringError as exc: logger.error("Error loading ASGI app. %s" % exc) sys.exit(1) if self.interface == "auto": if inspect.isclass(self.loaded_app): use_asgi_3 = hasattr(self.loaded_app, "__await__") elif inspect.isfunction(self.loaded_app): use_asgi_3 = asyncio.iscoroutinefunction(self.loaded_app) else: call = getattr(self.loaded_app, "__call__", None) use_asgi_3 = asyncio.iscoroutinefunction(call) self.interface = "asgi3" if use_asgi_3 else "asgi2" if self.interface == "wsgi": self.loaded_app = WSGIMiddleware(self.loaded_app) self.ws_protocol_class = None elif self.interface == "asgi2": self.loaded_app = ASGI2Middleware(self.loaded_app) if self.debug: self.loaded_app = DebugMiddleware(self.loaded_app) if logger.level <= TRACE_LOG_LEVEL: self.loaded_app = MessageLoggerMiddleware(self.loaded_app) if self.proxy_headers: self.loaded_app = ProxyHeadersMiddleware( self.loaded_app, trusted_hosts=self.forwarded_allow_ips ) self.loaded = True def setup_event_loop(self): loop_setup = import_from_string(LOOP_SETUPS[self.loop]) if loop_setup is not None: loop_setup() def bind_socket(self): sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((self.host, self.port)) except OSError as exc: logger.error(exc) sys.exit(1) sock.set_inheritable(True) message = "Uvicorn running on %s://%s:%d (Press CTRL+C to quit)" color_message = ( "Uvicorn running on " + click.style("%s://%s:%d", bold=True) + " (Press CTRL+C to quit)" ) protocol_name = "https" if self.is_ssl else "http" logger.info( message, protocol_name, self.host, self.port, extra={"color_message": color_message}, ) return sock @property def should_reload(self): return isinstance(self.app, str) and (self.debug or self.reload) uvicorn-0.11.3/uvicorn/importer.py000066400000000000000000000021621362253677400172010ustar00rootroot00000000000000import importlib class ImportFromStringError(Exception): pass def import_from_string(import_str): if not isinstance(import_str, str): return import_str module_str, _, attrs_str = import_str.partition(":") if not module_str or not attrs_str: message = ( 'Import string "{import_str}" must be in format ":".' ) raise ImportFromStringError(message.format(import_str=import_str)) try: module = importlib.import_module(module_str) except ImportError as exc: if exc.name != module_str: raise exc from None message = 'Could not import module "{module_str}".' raise ImportFromStringError(message.format(module_str=module_str)) instance = module try: for attr_str in attrs_str.split("."): instance = getattr(instance, attr_str) except AttributeError as exc: message = 'Attribute "{attrs_str}" not found in module "{module_str}".' raise ImportFromStringError( message.format(attrs_str=attrs_str, module_str=module_str) ) return instance uvicorn-0.11.3/uvicorn/lifespan/000077500000000000000000000000001362253677400165665ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/lifespan/__init__.py000066400000000000000000000000001362253677400206650ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/lifespan/off.py000066400000000000000000000002531362253677400177120ustar00rootroot00000000000000class LifespanOff: def __init__(self, config): self.should_exit = False async def startup(self): pass async def shutdown(self): pass uvicorn-0.11.3/uvicorn/lifespan/on.py000066400000000000000000000063151362253677400175610ustar00rootroot00000000000000import asyncio import logging STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol." class LifespanOn: def __init__(self, config): if not config.loaded: config.load() self.config = config self.logger = logging.getLogger("uvicorn.error") self.startup_event = asyncio.Event() self.shutdown_event = asyncio.Event() self.receive_queue = asyncio.Queue() self.error_occured = False self.startup_failed = False self.should_exit = False async def startup(self): self.logger.info("Waiting for application startup.") loop = asyncio.get_event_loop() loop.create_task(self.main()) await self.receive_queue.put({"type": "lifespan.startup"}) await self.startup_event.wait() if self.startup_failed or (self.error_occured and self.config.lifespan == "on"): self.logger.error("Application startup failed. Exiting.") self.should_exit = True else: self.logger.info("Application startup complete.") async def shutdown(self): if self.error_occured: return self.logger.info("Waiting for application shutdown.") await self.receive_queue.put({"type": "lifespan.shutdown"}) await self.shutdown_event.wait() self.logger.info("Application shutdown complete.") async def main(self): try: app = self.config.loaded_app scope = {"type": "lifespan"} await app(scope, self.receive, self.send) except BaseException as exc: self.asgi = None self.error_occured = True if self.startup_failed: return if self.config.lifespan == "auto": msg = "ASGI 'lifespan' protocol appears unsupported." self.logger.info(msg) else: msg = "Exception in 'lifespan' protocol\n" self.logger.error(msg, exc_info=exc) finally: self.startup_event.set() self.shutdown_event.set() async def send(self, message): assert message["type"] in ( "lifespan.startup.complete", "lifespan.startup.failed", "lifespan.shutdown.complete", ) if message["type"] == "lifespan.startup.complete": assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR self.startup_event.set() elif message["type"] == "lifespan.startup.failed": assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR self.startup_event.set() self.startup_failed = True if message.get("message"): self.logger.error(message["message"]) elif message["type"] == "lifespan.shutdown.complete": assert self.startup_event.is_set(), STATE_TRANSITION_ERROR assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR self.shutdown_event.set() async def receive(self): return await self.receive_queue.get() uvicorn-0.11.3/uvicorn/logging.py000066400000000000000000000105621362253677400167710ustar00rootroot00000000000000import http import logging import sys import click TRACE_LOG_LEVEL = 5 class ColourizedFormatter(logging.Formatter): """ A custom log formatter class that: * Outputs the LOG_LEVEL with an appropriate color. * If a log call includes an `extras={"color_message": ...}` it will be used for formatting the output, instead of the plain text message. """ level_name_colors = { TRACE_LOG_LEVEL: lambda level_name: click.style(str(level_name), fg="blue"), logging.DEBUG: lambda level_name: click.style(str(level_name), fg="cyan"), logging.INFO: lambda level_name: click.style(str(level_name), fg="green"), logging.WARNING: lambda level_name: click.style(str(level_name), fg="yellow"), logging.ERROR: lambda level_name: click.style(str(level_name), fg="red"), logging.CRITICAL: lambda level_name: click.style( str(level_name), fg="bright_red" ), } def __init__(self, fmt=None, datefmt=None, style="%", use_colors=None): if use_colors in (True, False): self.use_colors = use_colors else: self.use_colors = sys.stdout.isatty() super().__init__(fmt=fmt, datefmt=datefmt, style=style) def color_level_name(self, level_name, level_no): default = lambda level_name: str(level_name) func = self.level_name_colors.get(level_no, default) return func(level_name) def should_use_colors(self): return True def formatMessage(self, record): levelname = record.levelname seperator = " " * (8 - len(record.levelname)) if self.use_colors: levelname = self.color_level_name(levelname, record.levelno) if "color_message" in record.__dict__: record.msg = record.__dict__["color_message"] record.__dict__["message"] = record.getMessage() record.__dict__["levelprefix"] = levelname + ":" + seperator return super().formatMessage(record) class DefaultFormatter(ColourizedFormatter): def should_use_colors(self): return sys.stderr.isatty() class AccessFormatter(ColourizedFormatter): status_code_colours = { 1: lambda code: click.style(str(code), fg="bright_white"), 2: lambda code: click.style(str(code), fg="green"), 3: lambda code: click.style(str(code), fg="yellow"), 4: lambda code: click.style(str(code), fg="red"), 5: lambda code: click.style(str(code), fg="bright_red"), } def get_client_addr(self, scope): client = scope.get("client") if not client: return "" return "%s:%d" % (client[0], client[1]) def get_path(self, scope): return scope.get("root_path", "") + scope["path"] def get_full_path(self, scope): path = scope.get("root_path", "") + scope["path"] query_string = scope.get("query_string", b"").decode("ascii") if query_string: return path + "?" + query_string return path def get_status_code(self, record): status_code = record.__dict__["status_code"] try: status_phrase = http.HTTPStatus(status_code).phrase except ValueError: status_phrase = "" status_and_phrase = "%s %s" % (status_code, status_phrase) if self.use_colors: default = lambda code: status_and_phrase func = self.status_code_colours.get(status_code // 100, default) return func(status_and_phrase) return status_and_phrase def formatMessage(self, record): scope = record.__dict__["scope"] method = scope["method"] path = self.get_path(scope) full_path = self.get_full_path(scope) client_addr = self.get_client_addr(scope) status_code = self.get_status_code(record) http_version = scope["http_version"] request_line = "%s %s HTTP/%s" % (method, full_path, http_version) if self.use_colors: request_line = click.style(request_line, bold=True) record.__dict__.update( { "method": method, "path": path, "full_path": full_path, "client_addr": client_addr, "request_line": request_line, "status_code": status_code, "http_version": http_version, } ) return super().formatMessage(record) uvicorn-0.11.3/uvicorn/loops/000077500000000000000000000000001362253677400161215ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/loops/__init__.py000066400000000000000000000000001362253677400202200ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/loops/asyncio.py000066400000000000000000000006661362253677400201500ustar00rootroot00000000000000import asyncio import platform import selectors import sys def asyncio_setup(): if ( sys.version_info.major >= 3 and sys.version_info.minor >= 8 and platform.system() == "Windows" ): selector = selectors.SelectSelector() loop = asyncio.SelectorEventLoop(selector) asyncio.set_event_loop(loop) else: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) uvicorn-0.11.3/uvicorn/loops/auto.py000066400000000000000000000004351362253677400174450ustar00rootroot00000000000000def auto_loop_setup(): try: import uvloop except ImportError as exc: # pragma: no cover from uvicorn.loops.asyncio import asyncio_setup as loop_setup loop_setup() else: from uvicorn.loops.uvloop import uvloop_setup uvloop_setup() uvicorn-0.11.3/uvicorn/loops/uvloop.py000066400000000000000000000001601362253677400200140ustar00rootroot00000000000000import asyncio import uvloop def uvloop_setup(): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) uvicorn-0.11.3/uvicorn/main.py000066400000000000000000000406011362253677400162640ustar00rootroot00000000000000import asyncio import functools import logging import os import platform import signal import socket import ssl import sys import time import typing from email.utils import formatdate import click import uvicorn from uvicorn.config import ( HTTP_PROTOCOLS, INTERFACES, LIFESPAN, LOG_LEVELS, LOGGING_CONFIG, LOOP_SETUPS, SSL_PROTOCOL_VERSION, WS_PROTOCOLS, Config, ) from uvicorn.supervisors import Multiprocess, StatReload LEVEL_CHOICES = click.Choice(LOG_LEVELS.keys()) HTTP_CHOICES = click.Choice(HTTP_PROTOCOLS.keys()) WS_CHOICES = click.Choice(WS_PROTOCOLS.keys()) LIFESPAN_CHOICES = click.Choice(LIFESPAN.keys()) LOOP_CHOICES = click.Choice([key for key in LOOP_SETUPS.keys() if key != "none"]) INTERFACE_CHOICES = click.Choice(INTERFACES) HANDLED_SIGNALS = ( signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. signal.SIGTERM, # Unix signal 15. Sent by `kill `. ) logger = logging.getLogger("uvicorn.error") def print_version(ctx, param, value): if not value or ctx.resilient_parsing: return click.echo( "Running uvicorn %s with %s %s on %s" % ( uvicorn.__version__, platform.python_implementation(), platform.python_version(), platform.system(), ) ) ctx.exit() @click.command() @click.argument("app") @click.option( "--host", type=str, default="127.0.0.1", help="Bind socket to this host.", show_default=True, ) @click.option( "--port", type=int, default=8000, help="Bind socket to this port.", show_default=True, ) @click.option("--uds", type=str, default=None, help="Bind to a UNIX domain socket.") @click.option( "--fd", type=int, default=None, help="Bind to socket from this file descriptor." ) @click.option( "--debug", is_flag=True, default=False, help="Enable debug mode.", hidden=True ) @click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.") @click.option( "--reload-dir", "reload_dirs", multiple=True, help="Set reload directories explicitly, instead of using the current working directory.", ) @click.option( "--workers", default=None, type=int, help="Number of worker processes. Defaults to the $WEB_CONCURRENCY environment variable if available. Not valid with --reload.", ) @click.option( "--loop", type=LOOP_CHOICES, default="auto", help="Event loop implementation.", show_default=True, ) @click.option( "--http", type=HTTP_CHOICES, default="auto", help="HTTP protocol implementation.", show_default=True, ) @click.option( "--ws", type=WS_CHOICES, default="auto", help="WebSocket protocol implementation.", show_default=True, ) @click.option( "--lifespan", type=LIFESPAN_CHOICES, default="auto", help="Lifespan implementation.", show_default=True, ) @click.option( "--interface", type=INTERFACE_CHOICES, default="auto", help="Select ASGI3, ASGI2, or WSGI as the application interface.", show_default=True, ) @click.option( "--env-file", type=click.Path(exists=True), default=None, help="Environment configuration file.", show_default=True, ) @click.option( "--log-config", type=click.Path(exists=True), default=None, help="Logging configuration file.", show_default=True, ) @click.option( "--log-level", type=LEVEL_CHOICES, default=None, help="Log level. [default: info]", show_default=True, ) @click.option( "--access-log/--no-access-log", is_flag=True, default=True, help="Enable/Disable access log.", ) @click.option( "--use-colors/--no-use-colors", is_flag=True, default=None, help="Enable/Disable colorized logging.", ) @click.option( "--proxy-headers/--no-proxy-headers", is_flag=True, default=True, help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info.", ) @click.option( "--forwarded-allow-ips", type=str, default=None, help="Comma seperated list of IPs to trust with proxy headers. Defaults to the $FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'.", ) @click.option( "--root-path", type=str, default="", help="Set the ASGI 'root_path' for applications submounted below a given URL path.", ) @click.option( "--limit-concurrency", type=int, default=None, help="Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses.", ) @click.option( "--backlog", type=int, default=2048, help="Maximum number of connections to hold in backlog", ) @click.option( "--limit-max-requests", type=int, default=None, help="Maximum number of requests to service before terminating the process.", ) @click.option( "--timeout-keep-alive", type=int, default=5, help="Close Keep-Alive connections if no new data is received within this timeout.", show_default=True, ) @click.option( "--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True ) @click.option( "--ssl-certfile", type=str, default=None, help="SSL certificate file", show_default=True, ) @click.option( "--ssl-version", type=int, default=SSL_PROTOCOL_VERSION, help="SSL version to use (see stdlib ssl module's)", show_default=True, ) @click.option( "--ssl-cert-reqs", type=int, default=ssl.CERT_NONE, help="Whether client certificate is required (see stdlib ssl module's)", show_default=True, ) @click.option( "--ssl-ca-certs", type=str, default=None, help="CA certificates file", show_default=True, ) @click.option( "--ssl-ciphers", type=str, default="TLSv1", help="Ciphers to use (see stdlib ssl module's)", show_default=True, ) @click.option( "--header", "headers", multiple=True, help="Specify custom default HTTP response headers as a Name:Value pair", ) @click.option( "--version", is_flag=True, callback=print_version, expose_value=False, is_eager=True, help="Display the uvicorn version and exit.", ) def main( app, host: str, port: int, uds: str, fd: int, loop: str, http: str, ws: str, lifespan: str, interface: str, debug: bool, reload: bool, reload_dirs: typing.List[str], workers: int, env_file: str, log_config: str, log_level: str, access_log: bool, proxy_headers: bool, forwarded_allow_ips: str, root_path: str, limit_concurrency: int, backlog: int, limit_max_requests: int, timeout_keep_alive: int, ssl_keyfile: str, ssl_certfile: str, ssl_version: int, ssl_cert_reqs: int, ssl_ca_certs: str, ssl_ciphers: str, headers: typing.List[str], use_colors: bool, ): sys.path.insert(0, ".") kwargs = { "app": app, "host": host, "port": port, "uds": uds, "fd": fd, "loop": loop, "http": http, "ws": ws, "lifespan": lifespan, "env_file": env_file, "log_config": LOGGING_CONFIG if log_config is None else log_config, "log_level": log_level, "access_log": access_log, "interface": interface, "debug": debug, "reload": reload, "reload_dirs": reload_dirs if reload_dirs else None, "workers": workers, "proxy_headers": proxy_headers, "forwarded_allow_ips": forwarded_allow_ips, "root_path": root_path, "limit_concurrency": limit_concurrency, "backlog": backlog, "limit_max_requests": limit_max_requests, "timeout_keep_alive": timeout_keep_alive, "ssl_keyfile": ssl_keyfile, "ssl_certfile": ssl_certfile, "ssl_version": ssl_version, "ssl_cert_reqs": ssl_cert_reqs, "ssl_ca_certs": ssl_ca_certs, "ssl_ciphers": ssl_ciphers, "headers": list([header.split(":") for header in headers]), "use_colors": use_colors, } run(**kwargs) def run(app, **kwargs): config = Config(app, **kwargs) server = Server(config=config) if (config.reload or config.workers > 1) and not isinstance(app, str): logger = logging.getLogger("uvicorn.error") logger.warn( "You must pass the application as an import string to enable 'reload' or 'workers'." ) sys.exit(1) if config.should_reload: sock = config.bind_socket() supervisor = StatReload(config, target=server.run, sockets=[sock]) supervisor.run() elif config.workers > 1: sock = config.bind_socket() supervisor = Multiprocess(config, target=server.run, sockets=[sock]) supervisor.run() else: server.run() class ServerState: """ Shared servers state that is available between all protocol instances. """ def __init__(self): self.total_requests = 0 self.connections = set() self.tasks = set() self.default_headers = [] class Server: def __init__(self, config): self.config = config self.server_state = ServerState() self.started = False self.should_exit = False self.force_exit = False self.last_notified = 0 def run(self, sockets=None): self.config.setup_event_loop() loop = asyncio.get_event_loop() loop.run_until_complete(self.serve(sockets=sockets)) async def serve(self, sockets=None): process_id = os.getpid() config = self.config if not config.loaded: config.load() self.lifespan = config.lifespan_class(config) self.install_signal_handlers() message = "Started server process [%d]" color_message = "Started server process [" + click.style("%d", fg="cyan") + "]" logger.info(message, process_id, extra={"color_message": color_message}) await self.startup(sockets=sockets) if self.should_exit: return await self.main_loop() await self.shutdown(sockets=sockets) message = "Finished server process [%d]" color_message = "Finished server process [" + click.style("%d", fg="cyan") + "]" logger.info( "Finished server process [%d]", process_id, extra={"color_message": color_message}, ) async def startup(self, sockets=None): await self.lifespan.startup() if self.lifespan.should_exit: self.should_exit = True return config = self.config create_protocol = functools.partial( config.http_protocol_class, config=config, server_state=self.server_state ) loop = asyncio.get_event_loop() if sockets is not None: # Explicitly passed a list of open sockets. # We use this when the server is run from a Gunicorn worker. self.servers = [] for sock in sockets: server = await loop.create_server( create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog ) self.servers.append(server) elif config.fd is not None: # Use an existing socket, from a file descriptor. sock = socket.fromfd(config.fd, socket.AF_UNIX, socket.SOCK_STREAM) server = await loop.create_server( create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog ) message = "Uvicorn running on socket %s (Press CTRL+C to quit)" logger.info(message % str(sock.getsockname())) self.servers = [server] elif config.uds is not None: # Create a socket using UNIX domain socket. uds_perms = 0o666 if os.path.exists(config.uds): uds_perms = os.stat(config.uds).st_mode server = await loop.create_unix_server( create_protocol, path=config.uds, ssl=config.ssl, backlog=config.backlog ) os.chmod(config.uds, uds_perms) message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)" logger.info(message % config.uds) self.servers = [server] else: # Standard case. Create a socket from a host/port pair. try: server = await loop.create_server( create_protocol, host=config.host, port=config.port, ssl=config.ssl, backlog=config.backlog, ) except OSError as exc: logger.error(exc) await self.lifespan.shutdown() sys.exit(1) port = config.port if port == 0: port = server.sockets[0].getsockname()[1] protocol_name = "https" if config.ssl else "http" message = "Uvicorn running on %s://%s:%d (Press CTRL+C to quit)" color_message = ( "Uvicorn running on " + click.style("%s://%s:%d", bold=True) + " (Press CTRL+C to quit)" ) logger.info( message, protocol_name, config.host, port, extra={"color_message": color_message}, ) self.servers = [server] self.started = True async def main_loop(self): counter = 0 should_exit = await self.on_tick(counter) while not should_exit: counter += 1 counter = counter % 864000 await asyncio.sleep(0.1) should_exit = await self.on_tick(counter) async def on_tick(self, counter) -> bool: # Update the default headers, once per second. if counter % 10 == 0: current_time = time.time() current_date = formatdate(current_time, usegmt=True).encode() self.server_state.default_headers = [ (b"date", current_date) ] + self.config.encoded_headers # Callback to `callback_notify` once every `timeout_notify` seconds. if self.config.callback_notify is not None: if current_time - self.last_notified > self.config.timeout_notify: self.last_notified = current_time await self.config.callback_notify() # Determine if we should exit. if self.should_exit: return True if self.config.limit_max_requests is not None: return self.server_state.total_requests >= self.config.limit_max_requests return False async def shutdown(self, sockets=None): logger.info("Shutting down") # Stop accepting new connections. for socket in sockets or []: socket.close() for server in self.servers: server.close() for server in self.servers: await server.wait_closed() # Request shutdown on all existing connections. for connection in list(self.server_state.connections): connection.shutdown() await asyncio.sleep(0.1) # Wait for existing connections to finish sending responses. if self.server_state.connections and not self.force_exit: msg = "Waiting for connections to close. (CTRL+C to force quit)" logger.info(msg) while self.server_state.connections and not self.force_exit: await asyncio.sleep(0.1) # Wait for existing tasks to complete. if self.server_state.tasks and not self.force_exit: msg = "Waiting for background tasks to complete. (CTRL+C to force quit)" logger.info(msg) while self.server_state.tasks and not self.force_exit: await asyncio.sleep(0.1) # Send the lifespan shutdown event, and wait for application shutdown. if not self.force_exit: await self.lifespan.shutdown() def install_signal_handlers(self): loop = asyncio.get_event_loop() try: for sig in HANDLED_SIGNALS: loop.add_signal_handler(sig, self.handle_exit, sig, None) except NotImplementedError as exc: # Windows for sig in HANDLED_SIGNALS: signal.signal(sig, self.handle_exit) def handle_exit(self, sig, frame): if self.should_exit: self.force_exit = True else: self.should_exit = True if __name__ == "__main__": main() uvicorn-0.11.3/uvicorn/middleware/000077500000000000000000000000001362253677400171025ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/middleware/__init__.py000066400000000000000000000000001362253677400212010ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/middleware/asgi2.py000066400000000000000000000003111362253677400204540ustar00rootroot00000000000000class ASGI2Middleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): instance = self.app(scope) await instance(receive, send) uvicorn-0.11.3/uvicorn/middleware/debug.py000066400000000000000000000052251362253677400205460ustar00rootroot00000000000000import html import traceback class HTMLResponse: def __init__(self, content, status_code): self.content = content self.status_code = status_code async def __call__(self, scope, recieve, send): await send( { "type": "http.response.start", "status": self.status_code, "headers": [[b"content-type", b"text/html; charset=utf-8"]], } ) await send( { "type": "http.response.body", "body": self.content.encode("utf-8"), "more_body": False, } ) class PlainTextResponse: def __init__(self, content, status_code): self.content = content self.status_code = status_code async def __call__(self, scope, recieve, send): await send( { "type": "http.response.start", "status": self.status_code, "headers": [[b"content-type", b"text/plain; charset=utf-8"]], } ) await send( { "type": "http.response.body", "body": self.content.encode("utf-8"), "more_body": False, } ) def get_accept_header(scope): accept = "*/*" for key, value in scope.get("headers", []): if key == b"accept": accept = value.decode("ascii") break return accept class DebugMiddleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): if scope["type"] != "http": return await self.app(scope, receive, send) response_started = False async def inner_send(message): nonlocal response_started, send if message["type"] == "http.response.start": response_started = True await send(message) try: await self.app(scope, receive, inner_send) except BaseException as exc: if response_started: raise exc from None accept = get_accept_header(scope) if "text/html" in accept: exc_html = html.escape(traceback.format_exc()) content = ( "

500 Server Error

%s
" % exc_html ) response = HTMLResponse(content, status_code=500) else: content = traceback.format_exc() response = PlainTextResponse(content, status_code=500) await response(scope, receive, send) raise exc from None uvicorn-0.11.3/uvicorn/middleware/message_logger.py000066400000000000000000000043641362253677400224460ustar00rootroot00000000000000import logging PLACEHOLDER_FORMAT = { "body": "<{length} bytes>", "bytes": "<{length} bytes>", "text": "<{length} chars>", "headers": "<...>", } TRACE_LOG_LEVEL = 5 def message_with_placeholders(message): """ Return an ASGI message, with any body-type content omitted and replaced with a placeholder. """ new_message = message.copy() for attr in PLACEHOLDER_FORMAT.keys(): if message.get(attr) is not None: content = message[attr] placeholder = PLACEHOLDER_FORMAT[attr].format(length=len(content)) new_message[attr] = placeholder return new_message class MessageLoggerMiddleware: def __init__(self, app): self.task_counter = 0 self.app = app self.logger = logging.getLogger("uvicorn.asgi") def trace(message, *args, **kwargs): logging.log(TRACE_LOG_LEVEL, message, *args, **kwargs) self.logger.trace = trace async def __call__(self, scope, receive, send): self.task_counter += 1 task_counter = self.task_counter client = scope.get("client") prefix = "%s:%d - ASGI" % (client[0], client[1]) if client else "ASGI" async def inner_receive(): message = await receive() logged_message = message_with_placeholders(message) log_text = "%s [%d] Receive %s" self.logger.trace(log_text, prefix, task_counter, logged_message) return message async def inner_send(message): logged_message = message_with_placeholders(message) log_text = "%s [%d] Send %s" self.logger.trace(log_text, prefix, task_counter, logged_message) await send(message) logged_scope = message_with_placeholders(scope) log_text = "%s [%d] Started scope=%s" self.logger.trace(log_text, prefix, task_counter, logged_scope) try: await self.app(scope, inner_receive, inner_send) except BaseException as exc: log_text = "%s [%d] Raised exception" self.logger.trace(log_text, prefix, task_counter) raise exc from None else: log_text = "%s [%d] Completed" self.logger.trace(log_text, prefix, task_counter) uvicorn-0.11.3/uvicorn/middleware/proxy_headers.py000066400000000000000000000037311362253677400223340ustar00rootroot00000000000000""" This middleware can be used when a known proxy is fronting the application, and is trusted to be properly setting the `X-Forwarded-Proto` and `X-Forwarded-For` headers with the connecting client information. Modifies the `client` and `scheme` information so that they reference the connecting client, rather that the connecting proxy. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies """ class ProxyHeadersMiddleware: def __init__(self, app, trusted_hosts="127.0.0.1"): self.app = app if isinstance(trusted_hosts, str): self.trusted_hosts = [item.strip() for item in trusted_hosts.split(",")] else: self.trusted_hosts = trusted_hosts self.always_trust = "*" in self.trusted_hosts async def __call__(self, scope, receive, send): if scope["type"] in ("http", "websocket"): client_addr = scope.get("client") client_host = client_addr[0] if client_addr else None if self.always_trust or client_host in self.trusted_hosts: headers = dict(scope["headers"]) if b"x-forwarded-proto" in headers: # Determine if the incoming request was http or https based on # the X-Forwarded-Proto header. x_forwarded_proto = headers[b"x-forwarded-proto"].decode("ascii") scope["scheme"] = x_forwarded_proto.strip() if b"x-forwarded-for" in headers: # Determine the client address from the last trusted IP in the # X-Forwarded-For header. We've lost the connecting client's port # information by now, so only include the host. x_forwarded_for = headers[b"x-forwarded-for"].decode("ascii") host = x_forwarded_for.split(",")[-1].strip() port = 0 scope["client"] = (host, port) return await self.app(scope, receive, send) uvicorn-0.11.3/uvicorn/middleware/wsgi.py000066400000000000000000000115121362253677400204250ustar00rootroot00000000000000import asyncio import concurrent.futures import io import sys def build_environ(scope, message, body): """ Builds a scope and request message into a WSGI environ object. """ environ = { "REQUEST_METHOD": scope["method"], "SCRIPT_NAME": "", "PATH_INFO": scope["path"], "QUERY_STRING": scope["query_string"].decode("ascii"), "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], "wsgi.version": (1, 0), "wsgi.url_scheme": scope.get("scheme", "http"), "wsgi.input": io.BytesIO(body), "wsgi.errors": sys.stdout, "wsgi.multithread": True, "wsgi.multiprocess": True, "wsgi.run_once": False, } # Get server name and port - required in WSGI, not in ASGI server = scope.get("server") if server is None: server = ("localhost", 80) environ["SERVER_NAME"] = server[0] environ["SERVER_PORT"] = server[1] # Get client IP address client = scope.get("client") if client is not None: environ["REMOTE_ADDR"] = client[0] # Go through headers and make them into environ entries for name, value in scope.get("headers", []): name = name.decode("latin1") if name == "content-length": corrected_name = "CONTENT_LENGTH" elif name == "content-type": corrected_name = "CONTENT_TYPE" else: corrected_name = "HTTP_%s" % name.upper().replace("-", "_") # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case value = value.decode("latin1") if corrected_name in environ: value = environ[corrected_name] + "," + value environ[corrected_name] = value return environ class WSGIMiddleware: def __init__(self, app, workers=10): self.app = app self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=workers) async def __call__(self, scope, receive, send): assert scope["type"] == "http" instance = WSGIResponder(self.app, self.executor, scope) await instance(receive, send) class WSGIResponder: def __init__(self, app, executor, scope): self.app = app self.executor = executor self.scope = scope self.status = None self.response_headers = None self.send_event = asyncio.Event() self.send_queue = [] self.loop = None self.response_started = False self.exc_info = None async def __call__(self, receive, send): message = await receive() body = message.get("body", b"") more_body = message.get("more_body", False) while more_body: body_message = await receive() body += body_message.get("body", b"") more_body = body_message.get("more_body", False) environ = build_environ(self.scope, message, body) self.loop = asyncio.get_event_loop() wsgi = self.loop.run_in_executor( self.executor, self.wsgi, environ, self.start_response ) sender = self.loop.create_task(self.sender(send)) try: await asyncio.wait_for(wsgi, None) finally: self.send_queue.append(None) self.send_event.set() await asyncio.wait_for(sender, None) if self.exc_info is not None: raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) async def sender(self, send): while True: if self.send_queue: message = self.send_queue.pop(0) if message is None: return await send(message) else: await self.send_event.wait() self.send_event.clear() def start_response(self, status, response_headers, exc_info=None): self.exc_info = exc_info if not self.response_started: self.response_started = True status_code, _ = status.split(" ", 1) status_code = int(status_code) headers = [ (name.encode("ascii"), value.encode("ascii")) for name, value in response_headers ] self.send_queue.append( { "type": "http.response.start", "status": status_code, "headers": headers, } ) self.loop.call_soon_threadsafe(self.send_event.set) def wsgi(self, environ, start_response): for chunk in self.app(environ, start_response): self.send_queue.append( {"type": "http.response.body", "body": chunk, "more_body": True} ) self.loop.call_soon_threadsafe(self.send_event.set) self.send_queue.append({"type": "http.response.body", "body": b""}) self.loop.call_soon_threadsafe(self.send_event.set) uvicorn-0.11.3/uvicorn/protocols/000077500000000000000000000000001362253677400170115ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/protocols/__init__.py000066400000000000000000000000001362253677400211100ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/protocols/http/000077500000000000000000000000001362253677400177705ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/protocols/http/__init__.py000066400000000000000000000000001362253677400220670ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/protocols/http/auto.py000066400000000000000000000004411362253677400213110ustar00rootroot00000000000000try: import httptools except ImportError as exc: # pragma: no cover from uvicorn.protocols.http.h11_impl import H11Protocol AutoHTTPProtocol = H11Protocol else: from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol AutoHTTPProtocol = HttpToolsProtocol uvicorn-0.11.3/uvicorn/protocols/http/h11_impl.py000066400000000000000000000430631362253677400217620ustar00rootroot00000000000000import asyncio import http import logging from urllib.parse import unquote import h11 from uvicorn.protocols.utils import ( get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl, ) def _get_status_phrase(status_code): try: return http.HTTPStatus(status_code).phrase.encode() except ValueError as exc: return b"" STATUS_PHRASES = { status_code: _get_status_phrase(status_code) for status_code in range(100, 600) } HIGH_WATER_LIMIT = 65536 TRACE_LOG_LEVEL = 5 class FlowControl: def __init__(self, transport): self._transport = transport self.read_paused = False self.write_paused = False self._is_writable_event = asyncio.Event() self._is_writable_event.set() async def drain(self): await self._is_writable_event.wait() def pause_reading(self): if not self.read_paused: self.read_paused = True self._transport.pause_reading() def resume_reading(self): if self.read_paused: self.read_paused = False self._transport.resume_reading() def pause_writing(self): if not self.write_paused: self.write_paused = True self._is_writable_event.clear() def resume_writing(self): if self.write_paused: self.write_paused = False self._is_writable_event.set() async def service_unavailable(scope, receive, send): await send( { "type": "http.response.start", "status": 503, "headers": [ (b"content-type", b"text/plain; charset=utf-8"), (b"connection", b"close"), ], } ) await send({"type": "http.response.body", "body": b"Service Unavailable"}) class H11Protocol(asyncio.Protocol): def __init__(self, config, server_state, _loop=None): if not config.loaded: config.load() self.config = config self.app = config.loaded_app self.loop = _loop or asyncio.get_event_loop() self.logger = logging.getLogger("uvicorn.error") self.access_logger = logging.getLogger("uvicorn.access") self.access_log = self.access_logger.hasHandlers() self.conn = h11.Connection(h11.SERVER) self.ws_protocol_class = config.ws_protocol_class self.root_path = config.root_path self.limit_concurrency = config.limit_concurrency # Timeouts self.timeout_keep_alive_task = None self.timeout_keep_alive = config.timeout_keep_alive # Shared server state self.server_state = server_state self.connections = server_state.connections self.tasks = server_state.tasks self.default_headers = server_state.default_headers # Per-connection state self.transport = None self.flow = None self.server = None self.client = None self.scheme = None # Per-request state self.scope = None self.headers = None self.cycle = None self.message_event = asyncio.Event() # Protocol interface def connection_made(self, transport): self.connections.add(self) self.transport = transport self.flow = FlowControl(transport) self.server = get_local_addr(transport) self.client = get_remote_addr(transport) self.scheme = "https" if is_ssl(transport) else "http" if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sConnection made", prefix) def connection_lost(self, exc): self.connections.discard(self) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%Connection lost", prefix) if self.cycle and not self.cycle.response_complete: self.cycle.disconnected = True if self.conn.our_state != h11.ERROR: event = h11.ConnectionClosed() try: self.conn.send(event) except h11.LocalProtocolError as exc: # Premature client disconnect pass self.message_event.set() if self.flow is not None: self.flow.resume_writing() def eof_received(self): pass def data_received(self, data): if self.timeout_keep_alive_task is not None: self.timeout_keep_alive_task.cancel() self.timeout_keep_alive_task = None self.conn.receive_data(data) self.handle_events() def handle_events(self): while True: try: event = self.conn.next_event() except h11.RemoteProtocolError as exc: msg = "Invalid HTTP request received." self.logger.warning(msg) self.transport.close() return event_type = type(event) if event_type is h11.NEED_DATA: break elif event_type is h11.PAUSED: # This case can occur in HTTP pipelining, so we need to # stop reading any more data, and ensure that at the end # of the active request/response cycle we handle any # events that have been buffered up. self.flow.pause_reading() break elif event_type is h11.Request: self.headers = [(key.lower(), value) for key, value in event.headers] raw_path, _, query_string = event.target.partition(b"?") self.scope = { "type": "http", "http_version": event.http_version.decode("ascii"), "server": self.server, "client": self.client, "scheme": self.scheme, "method": event.method.decode("ascii"), "root_path": self.root_path, "path": unquote(raw_path.decode("ascii")), "raw_path": raw_path, "query_string": query_string, "headers": self.headers, } should_upgrade = False for name, value in self.headers: if name == b"connection": tokens = [token.lower().strip() for token in value.split(b",")] if b"upgrade" in tokens: self.handle_upgrade(event) return # Handle 503 responses when 'limit_concurrency' is exceeded. if self.limit_concurrency is not None and ( len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency ): app = service_unavailable message = "Exceeded concurrency limit." self.logger.warning(message) else: app = self.app self.cycle = RequestResponseCycle( scope=self.scope, conn=self.conn, transport=self.transport, flow=self.flow, logger=self.logger, access_logger=self.access_logger, access_log=self.access_log, default_headers=self.default_headers, message_event=self.message_event, on_response=self.on_response_complete, ) task = self.loop.create_task(self.cycle.run_asgi(app)) task.add_done_callback(self.tasks.discard) self.tasks.add(task) elif event_type is h11.Data: if self.conn.our_state is h11.DONE: continue self.cycle.body += event.data if len(self.cycle.body) > HIGH_WATER_LIMIT: self.flow.pause_reading() self.message_event.set() elif event_type is h11.EndOfMessage: if self.conn.our_state is h11.DONE: self.transport.resume_reading() self.conn.start_next_cycle() continue self.cycle.more_body = False self.message_event.set() def handle_upgrade(self, event): upgrade_value = None for name, value in self.headers: if name == b"upgrade": upgrade_value = value.lower() if upgrade_value != b"websocket" or self.ws_protocol_class is None: msg = "Unsupported upgrade request." self.logger.warning(msg) reason = STATUS_PHRASES[400] headers = [ (b"content-type", b"text/plain; charset=utf-8"), (b"connection", b"close"), ] event = h11.Response(status_code=400, headers=headers, reason=reason) output = self.conn.send(event) self.transport.write(output) event = h11.Data(data=b"Unsupported upgrade request.") output = self.conn.send(event) self.transport.write(output) event = h11.EndOfMessage() output = self.conn.send(event) self.transport.write(output) self.transport.close() return self.connections.discard(self) output = [event.method, b" ", event.target, b" HTTP/1.1\r\n"] for name, value in self.headers: output += [name, b": ", value, b"\r\n"] output.append(b"\r\n") protocol = self.ws_protocol_class( config=self.config, server_state=self.server_state ) protocol.connection_made(self.transport) protocol.data_received(b"".join(output)) self.transport.set_protocol(protocol) def on_response_complete(self): self.server_state.total_requests += 1 if self.transport.is_closing(): return # Set a short Keep-Alive timeout. self.timeout_keep_alive_task = self.loop.call_later( self.timeout_keep_alive, self.timeout_keep_alive_handler ) # Unpause data reads if needed. self.flow.resume_reading() # Unblock any pipelined events. if self.conn.our_state is h11.DONE and self.conn.their_state is h11.DONE: self.conn.start_next_cycle() self.handle_events() def shutdown(self): """ Called by the server to commence a graceful shutdown. """ if self.cycle is None or self.cycle.response_complete: event = h11.ConnectionClosed() self.conn.send(event) self.transport.close() else: self.cycle.keep_alive = False def pause_writing(self): """ Called by the transport when the write buffer exceeds the high water mark. """ self.flow.pause_writing() def resume_writing(self): """ Called by the transport when the write buffer drops below the low water mark. """ self.flow.resume_writing() def timeout_keep_alive_handler(self): """ Called on a keep-alive connection if no new data is received after a short delay. """ if not self.transport.is_closing(): event = h11.ConnectionClosed() self.conn.send(event) self.transport.close() class RequestResponseCycle: def __init__( self, scope, conn, transport, flow, logger, access_logger, access_log, default_headers, message_event, on_response, ): self.scope = scope self.conn = conn self.transport = transport self.flow = flow self.logger = logger self.access_logger = logger self.access_log = access_log self.default_headers = default_headers self.message_event = message_event self.on_response = on_response # Connection state self.disconnected = False self.keep_alive = True self.waiting_for_100_continue = conn.they_are_waiting_for_100_continue # Request state self.body = b"" self.more_body = True # Response state self.response_started = False self.response_complete = False # ASGI exception wrapper async def run_asgi(self, app): try: result = await app(self.scope, self.receive, self.send) except BaseException as exc: msg = "Exception in ASGI application\n" self.logger.error(msg, exc_info=exc) if not self.response_started: await self.send_500_response() else: self.transport.close() else: if result is not None: msg = "ASGI callable should return None, but returned '%s'." self.logger.error(msg, result) self.transport.close() elif not self.response_started and not self.disconnected: msg = "ASGI callable returned without starting response." self.logger.error(msg) await self.send_500_response() elif not self.response_complete and not self.disconnected: msg = "ASGI callable returned without completing response." self.logger.error(msg) self.transport.close() finally: self.on_response = None async def send_500_response(self): await self.send( { "type": "http.response.start", "status": 500, "headers": [ (b"content-type", b"text/plain; charset=utf-8"), (b"connection", b"close"), ], } ) await self.send( {"type": "http.response.body", "body": b"Internal Server Error"} ) # ASGI interface async def send(self, message): message_type = message["type"] if self.flow.write_paused and not self.disconnected: await self.flow.drain() if self.disconnected: return if not self.response_started: # Sending response status line and headers if message_type != "http.response.start": msg = "Expected ASGI message 'http.response.start', but got '%s'." raise RuntimeError(msg % message_type) self.response_started = True self.waiting_for_100_continue = False status_code = message["status"] headers = self.default_headers + message.get("headers", []) if self.access_log: self.access_logger.info( '%s - "%s %s HTTP/%s" %d', get_client_addr(self.scope), self.scope["method"], get_path_with_query_string(self.scope), self.scope["http_version"], status_code, extra={"status_code": status_code, "scope": self.scope}, ) # Write response status line and headers reason = STATUS_PHRASES[status_code] event = h11.Response( status_code=status_code, headers=headers, reason=reason ) output = self.conn.send(event) self.transport.write(output) elif not self.response_complete: # Sending response body if message_type != "http.response.body": msg = "Expected ASGI message 'http.response.body', but got '%s'." raise RuntimeError(msg % message_type) body = message.get("body", b"") more_body = message.get("more_body", False) # Write response body if self.scope["method"] == "HEAD": event = h11.Data(data=b"") else: event = h11.Data(data=body) output = self.conn.send(event) self.transport.write(output) # Handle response completion if not more_body: self.response_complete = True event = h11.EndOfMessage() output = self.conn.send(event) self.transport.write(output) else: # Response already sent msg = "Unexpected ASGI message '%s' sent, after response already completed." raise RuntimeError(msg % message_type) if self.response_complete: if self.conn.our_state is h11.MUST_CLOSE or not self.keep_alive: event = h11.ConnectionClosed() self.conn.send(event) self.transport.close() self.on_response() async def receive(self): if self.waiting_for_100_continue and not self.transport.is_closing(): event = h11.InformationalResponse( status_code=100, headers=[], reason="Continue" ) output = self.conn.send(event) self.transport.write(output) self.waiting_for_100_continue = False if not self.disconnected and not self.response_complete: self.flow.resume_reading() await self.message_event.wait() self.message_event.clear() if self.disconnected or self.response_complete: message = {"type": "http.disconnect"} else: message = { "type": "http.request", "body": self.body, "more_body": self.more_body, } self.body = b"" return message uvicorn-0.11.3/uvicorn/protocols/http/httptools_impl.py000066400000000000000000000444021362253677400234270ustar00rootroot00000000000000import asyncio import http import logging import urllib import httptools from uvicorn.protocols.utils import ( get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl, ) def _get_status_line(status_code): try: phrase = http.HTTPStatus(status_code).phrase.encode() except ValueError as exc: phrase = b"" return b"".join([b"HTTP/1.1 ", str(status_code).encode(), b" ", phrase, b"\r\n"]) STATUS_LINE = { status_code: _get_status_line(status_code) for status_code in range(100, 600) } HIGH_WATER_LIMIT = 65536 TRACE_LOG_LEVEL = 5 class FlowControl: def __init__(self, transport): self._transport = transport self.read_paused = False self.write_paused = False self._is_writable_event = asyncio.Event() self._is_writable_event.set() async def drain(self): await self._is_writable_event.wait() def pause_reading(self): if not self.read_paused: self.read_paused = True self._transport.pause_reading() def resume_reading(self): if self.read_paused: self.read_paused = False self._transport.resume_reading() def pause_writing(self): if not self.write_paused: self.write_paused = True self._is_writable_event.clear() def resume_writing(self): if self.write_paused: self.write_paused = False self._is_writable_event.set() async def service_unavailable(scope, receive, send): await send( { "type": "http.response.start", "status": 503, "headers": [ (b"content-type", b"text/plain; charset=utf-8"), (b"connection", b"close"), ], } ) await send({"type": "http.response.body", "body": b"Service Unavailable"}) class HttpToolsProtocol(asyncio.Protocol): def __init__(self, config, server_state, _loop=None): if not config.loaded: config.load() self.config = config self.app = config.loaded_app self.loop = _loop or asyncio.get_event_loop() self.logger = logging.getLogger("uvicorn.error") self.access_logger = logging.getLogger("uvicorn.access") self.access_log = self.access_logger.hasHandlers() self.parser = httptools.HttpRequestParser(self) self.ws_protocol_class = config.ws_protocol_class self.root_path = config.root_path self.limit_concurrency = config.limit_concurrency # Timeouts self.timeout_keep_alive_task = None self.timeout_keep_alive = config.timeout_keep_alive # Global state self.server_state = server_state self.connections = server_state.connections self.tasks = server_state.tasks self.default_headers = server_state.default_headers # Per-connection state self.transport = None self.flow = None self.server = None self.client = None self.scheme = None self.pipeline = [] # Per-request state self.url = None self.scope = None self.headers = None self.expect_100_continue = False self.cycle = None self.message_event = asyncio.Event() # Protocol interface def connection_made(self, transport): self.connections.add(self) self.transport = transport self.flow = FlowControl(transport) self.server = get_local_addr(transport) self.client = get_remote_addr(transport) self.scheme = "https" if is_ssl(transport) else "http" if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sConnection made", prefix) def connection_lost(self, exc): self.connections.discard(self) if self.logger.level <= TRACE_LOG_LEVEL: prefix = "%s:%d - " % tuple(self.client) if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sConnection lost", prefix) if self.cycle and not self.cycle.response_complete: self.cycle.disconnected = True self.message_event.set() if self.flow is not None: self.flow.resume_writing() def eof_received(self): pass def data_received(self, data): if self.timeout_keep_alive_task is not None: self.timeout_keep_alive_task.cancel() self.timeout_keep_alive_task = None try: self.parser.feed_data(data) except httptools.HttpParserError as exc: msg = "Invalid HTTP request received." self.logger.warning(msg) self.transport.close() except httptools.HttpParserUpgrade as exc: self.handle_upgrade() def handle_upgrade(self): upgrade_value = None for name, value in self.headers: if name == b"upgrade": upgrade_value = value.lower() if upgrade_value != b"websocket" or self.ws_protocol_class is None: msg = "Unsupported upgrade request." self.logger.warning(msg) content = [STATUS_LINE[400]] for name, value in self.default_headers: content.extend([name, b": ", value, b"\r\n"]) content.extend( [ b"content-type: text/plain; charset=utf-8\r\n", b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n", b"connection: close\r\n", b"\r\n", msg.encode("ascii"), ] ) self.transport.write(b"".join(content)) self.transport.close() return self.connections.discard(self) method = self.scope["method"].encode() output = [method, b" ", self.url, b" HTTP/1.1\r\n"] for name, value in self.scope["headers"]: output += [name, b": ", value, b"\r\n"] output.append(b"\r\n") protocol = self.ws_protocol_class( config=self.config, server_state=self.server_state ) protocol.connection_made(self.transport) protocol.data_received(b"".join(output)) self.transport.set_protocol(protocol) # Parser callbacks def on_url(self, url): method = self.parser.get_method() parsed_url = httptools.parse_url(url) raw_path = parsed_url.path path = raw_path.decode("ascii") if "%" in path: path = urllib.parse.unquote(path) self.url = url self.expect_100_continue = False self.headers = [] self.scope = { "type": "http", "http_version": "1.1", "server": self.server, "client": self.client, "scheme": self.scheme, "method": method.decode("ascii"), "root_path": self.root_path, "path": path, "raw_path": raw_path, "query_string": parsed_url.query if parsed_url.query else b"", "headers": self.headers, } def on_header(self, name: bytes, value: bytes): name = name.lower() if name == b"expect" and value.lower() == b"100-continue": self.expect_100_continue = True self.headers.append((name, value)) def on_headers_complete(self): http_version = self.parser.get_http_version() if http_version != "1.1": self.scope["http_version"] = http_version if self.parser.should_upgrade(): return # Handle 503 responses when 'limit_concurrency' is exceeded. if self.limit_concurrency is not None and ( len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency ): app = service_unavailable message = "Exceeded concurrency limit." self.logger.warning(message) else: app = self.app existing_cycle = self.cycle self.cycle = RequestResponseCycle( scope=self.scope, transport=self.transport, flow=self.flow, logger=self.logger, access_logger=self.access_logger, access_log=self.access_log, default_headers=self.default_headers, message_event=self.message_event, expect_100_continue=self.expect_100_continue, keep_alive=http_version != "1.0", on_response=self.on_response_complete, ) if existing_cycle is None or existing_cycle.response_complete: # Standard case - start processing the request. task = self.loop.create_task(self.cycle.run_asgi(app)) task.add_done_callback(self.tasks.discard) self.tasks.add(task) else: # Pipelined HTTP requests need to be queued up. self.flow.pause_reading() self.pipeline.insert(0, (self.cycle, app)) def on_body(self, body: bytes): if self.parser.should_upgrade() or self.cycle.response_complete: return self.cycle.body += body if len(self.cycle.body) > HIGH_WATER_LIMIT: self.flow.pause_reading() self.message_event.set() def on_message_complete(self): if self.parser.should_upgrade() or self.cycle.response_complete: return self.cycle.more_body = False self.message_event.set() def on_response_complete(self): # Callback for pipelined HTTP requests to be started. self.server_state.total_requests += 1 if self.transport.is_closing(): return # Set a short Keep-Alive timeout. self.timeout_keep_alive_task = self.loop.call_later( self.timeout_keep_alive, self.timeout_keep_alive_handler ) # Unpause data reads if needed. self.flow.resume_reading() # Unblock any pipelined events. if self.pipeline: cycle, app = self.pipeline.pop() task = self.loop.create_task(cycle.run_asgi(app)) task.add_done_callback(self.tasks.discard) self.tasks.add(task) def shutdown(self): """ Called by the server to commence a graceful shutdown. """ if self.cycle is None or self.cycle.response_complete: self.transport.close() else: self.cycle.keep_alive = False def pause_writing(self): """ Called by the transport when the write buffer exceeds the high water mark. """ self.flow.pause_writing() def resume_writing(self): """ Called by the transport when the write buffer drops below the low water mark. """ self.flow.resume_writing() def timeout_keep_alive_handler(self): """ Called on a keep-alive connection if no new data is received after a short delay. """ if not self.transport.is_closing(): self.transport.close() class RequestResponseCycle: def __init__( self, scope, transport, flow, logger, access_logger, access_log, default_headers, message_event, expect_100_continue, keep_alive, on_response, ): self.scope = scope self.transport = transport self.flow = flow self.logger = logger self.access_logger = access_logger self.access_log = access_log self.default_headers = default_headers self.message_event = message_event self.on_response = on_response # Connection state self.disconnected = False self.keep_alive = keep_alive self.waiting_for_100_continue = expect_100_continue # Request state self.body = b"" self.more_body = True # Response state self.response_started = False self.response_complete = False self.chunked_encoding = None self.expected_content_length = 0 # ASGI exception wrapper async def run_asgi(self, app): try: result = await app(self.scope, self.receive, self.send) except BaseException as exc: msg = "Exception in ASGI application\n" self.logger.error(msg, exc_info=exc) if not self.response_started: await self.send_500_response() else: self.transport.close() else: if result is not None: msg = "ASGI callable should return None, but returned '%s'." self.logger.error(msg, result) self.transport.close() elif not self.response_started and not self.disconnected: msg = "ASGI callable returned without starting response." self.logger.error(msg) await self.send_500_response() elif not self.response_complete and not self.disconnected: msg = "ASGI callable returned without completing response." self.logger.error(msg) self.transport.close() finally: self.on_response = None async def send_500_response(self): await self.send( { "type": "http.response.start", "status": 500, "headers": [ (b"content-type", b"text/plain; charset=utf-8"), (b"connection", b"close"), ], } ) await self.send( {"type": "http.response.body", "body": b"Internal Server Error"} ) # ASGI interface async def send(self, message): message_type = message["type"] if self.flow.write_paused and not self.disconnected: await self.flow.drain() if self.disconnected: return if not self.response_started: # Sending response status line and headers if message_type != "http.response.start": msg = "Expected ASGI message 'http.response.start', but got '%s'." raise RuntimeError(msg % message_type) self.response_started = True self.waiting_for_100_continue = False status_code = message["status"] headers = self.default_headers + list(message.get("headers", [])) if self.access_log: self.access_logger.info( '%s - "%s %s HTTP/%s" %d', get_client_addr(self.scope), self.scope["method"], get_path_with_query_string(self.scope), self.scope["http_version"], status_code, extra={"status_code": status_code, "scope": self.scope}, ) # Write response status line and headers content = [STATUS_LINE[status_code]] for name, value in headers: name = name.lower() if name == b"content-length" and self.chunked_encoding is None: self.expected_content_length = int(value.decode()) self.chunked_encoding = False elif name == b"transfer-encoding" and value.lower() == b"chunked": self.expected_content_length = 0 self.chunked_encoding = True elif name == b"connection" and value.lower() == b"close": self.keep_alive = False content.extend([name, b": ", value, b"\r\n"]) if ( self.chunked_encoding is None and self.scope["method"] != "HEAD" and status_code not in (204, 304) ): # Neither content-length nor transfer-encoding specified self.chunked_encoding = True content.append(b"transfer-encoding: chunked\r\n") content.append(b"\r\n") self.transport.write(b"".join(content)) elif not self.response_complete: # Sending response body if message_type != "http.response.body": msg = "Expected ASGI message 'http.response.body', but got '%s'." raise RuntimeError(msg % message_type) body = message.get("body", b"") more_body = message.get("more_body", False) # Write response body if self.scope["method"] == "HEAD": self.expected_content_length = 0 elif self.chunked_encoding: if body: content = [b"%x\r\n" % len(body), body, b"\r\n"] else: content = [] if not more_body: content.append(b"0\r\n\r\n") self.transport.write(b"".join(content)) else: num_bytes = len(body) if num_bytes > self.expected_content_length: raise RuntimeError("Response content longer than Content-Length") else: self.expected_content_length -= num_bytes self.transport.write(body) # Handle response completion if not more_body: if self.expected_content_length != 0: raise RuntimeError("Response content shorter than Content-Length") self.response_complete = True if not self.keep_alive: self.transport.close() self.on_response() else: # Response already sent msg = "Unexpected ASGI message '%s' sent, after response already completed." raise RuntimeError(msg % message_type) async def receive(self): if self.waiting_for_100_continue and not self.transport.is_closing(): self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n") self.waiting_for_100_continue = False if not self.disconnected and not self.response_complete: self.flow.resume_reading() await self.message_event.wait() self.message_event.clear() if self.disconnected or self.response_complete: message = {"type": "http.disconnect"} else: message = { "type": "http.request", "body": self.body, "more_body": self.more_body, } self.body = b"" return message uvicorn-0.11.3/uvicorn/protocols/utils.py000066400000000000000000000033531362253677400205270ustar00rootroot00000000000000import socket def get_remote_addr(transport): socket_info = transport.get_extra_info("socket") if socket_info is not None: try: info = socket_info.getpeername() except OSError: # This case appears to inconsistently occur with uvloop # bound to a unix domain socket. family = None info = None else: family = socket_info.family if family in (socket.AF_INET, socket.AF_INET6): return (str(info[0]), int(info[1])) return None info = transport.get_extra_info("peername") if info is not None and isinstance(info, (list, tuple)) and len(info) == 2: return (str(info[0]), int(info[1])) return None def get_local_addr(transport): socket_info = transport.get_extra_info("socket") if socket_info is not None: info = socket_info.getsockname() family = socket_info.family if family in (socket.AF_INET, socket.AF_INET6): return (str(info[0]), int(info[1])) return None info = transport.get_extra_info("sockname") if info is not None and isinstance(info, (list, tuple)) and len(info) == 2: return (str(info[0]), int(info[1])) return None def is_ssl(transport): return bool(transport.get_extra_info("sslcontext")) def get_client_addr(scope): client = scope.get("client") if not client: return "" return "%s:%d" % client def get_path_with_query_string(scope): path_with_query_string = scope.get("root_path", "") + scope["path"] if scope["query_string"]: path_with_query_string = "{}?{}".format( path_with_query_string, scope["query_string"].decode("ascii") ) return path_with_query_string uvicorn-0.11.3/uvicorn/protocols/websockets/000077500000000000000000000000001362253677400211625ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/protocols/websockets/__init__.py000066400000000000000000000000001362253677400232610ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/protocols/websockets/auto.py000066400000000000000000000006641362253677400225120ustar00rootroot00000000000000try: import websockets except ImportError as exc: # pragma: no cover try: import wsproto except ImportError as exc: AutoWebSocketsProtocol = None else: from uvicorn.protocols.websockets.wsproto_impl import WSProtocol AutoWebSocketsProtocol = WSProtocol else: from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol AutoWebSocketsProtocol = WebSocketProtocol uvicorn-0.11.3/uvicorn/protocols/websockets/websockets_impl.py000066400000000000000000000203541362253677400247320ustar00rootroot00000000000000import asyncio import http import logging from urllib.parse import unquote import websockets from uvicorn.protocols.utils import get_local_addr, get_remote_addr, is_ssl class Server: closing = False def register(self, ws): pass def unregister(self, ws): pass def is_serving(self): return not self.closing class WebSocketProtocol(websockets.WebSocketServerProtocol): def __init__(self, config, server_state, _loop=None): if not config.loaded: config.load() self.config = config self.app = config.loaded_app self.loop = _loop or asyncio.get_event_loop() self.logger = logging.getLogger("uvicorn.error") self.root_path = config.root_path # Shared server state self.connections = server_state.connections self.tasks = server_state.tasks # Connection state self.transport = None self.server = None self.client = None self.scheme = None # Connection events self.scope = None self.handshake_started_event = asyncio.Event() self.handshake_completed_event = asyncio.Event() self.closed_event = asyncio.Event() self.initial_response = None self.connect_sent = False self.accepted_subprotocol = None self.transfer_data_task = None self.ws_server = Server() super().__init__(ws_handler=self.ws_handler, ws_server=self.ws_server) def connection_made(self, transport): self.connections.add(self) self.transport = transport self.server = get_local_addr(transport) self.client = get_remote_addr(transport) self.scheme = "wss" if is_ssl(transport) else "ws" super().connection_made(transport) def connection_lost(self, exc): self.connections.remove(self) self.handshake_completed_event.set() super().connection_lost(exc) def shutdown(self): self.ws_server.closing = True self.transport.close() def on_task_complete(self, task): self.tasks.discard(task) async def process_request(self, path, headers): """ This hook is called to determine if the websocket should return an HTTP response and close. Our behavior here is to start the ASGI application, and then wait for either `accept` or `close` in order to determine if we should close the connection. """ path_portion, _, query_string = path.partition("?") websockets.handshake.check_request(headers) subprotocols = [] for header in headers.get_all("Sec-WebSocket-Protocol"): subprotocols.extend([token.strip() for token in header.split(",")]) asgi_headers = [ (name.encode("ascii"), value.encode("ascii")) for name, value in headers.raw_items() ] self.scope = { "type": "websocket", "scheme": self.scheme, "server": self.server, "client": self.client, "root_path": self.root_path, "path": unquote(path_portion), "raw_path": path_portion, "query_string": query_string.encode("ascii"), "headers": asgi_headers, "subprotocols": subprotocols, } task = self.loop.create_task(self.run_asgi()) task.add_done_callback(self.on_task_complete) self.tasks.add(task) await self.handshake_started_event.wait() return self.initial_response def process_subprotocol(self, headers, available_subprotocols): """ We override the standard 'process_subprotocol' behavior here so that we return whatever subprotocol is sent in the 'accept' message. """ return self.accepted_subprotocol def send_500_response(self): msg = b"Internal Server Error" content = [ b"HTTP/1.1 500 Internal Server Error\r\n" b"content-type: text/plain; charset=utf-8\r\n", b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n", b"connection: close\r\n", b"\r\n", msg, ] self.transport.write(b"".join(content)) async def ws_handler(self, protocol, path): """ This is the main handler function for the 'websockets' implementation to call into. We just wait for close then return, and instead allow 'send' and 'receive' events to drive the flow. """ self.handshake_completed_event.set() await self.closed_event.wait() async def run_asgi(self): """ Wrapper around the ASGI callable, handling exceptions and unexpected termination states. """ try: result = await self.app(self.scope, self.asgi_receive, self.asgi_send) except BaseException as exc: self.closed_event.set() msg = "Exception in ASGI application\n" self.logger.error(msg, exc_info=exc) if not self.handshake_started_event.is_set(): self.send_500_response() else: await self.handshake_completed_event.wait() self.transport.close() else: self.closed_event.set() if not self.handshake_started_event.is_set(): msg = "ASGI callable returned without sending handshake." self.logger.error(msg) self.send_500_response() self.transport.close() elif result is not None: msg = "ASGI callable should return None, but returned '%s'." self.logger.error(msg, result) await self.handshake_completed_event.wait() self.transport.close() async def asgi_send(self, message): message_type = message["type"] if not self.handshake_started_event.is_set(): if message_type == "websocket.accept": self.logger.info( '%s - "WebSocket %s" [accepted]', self.scope["client"], self.scope["root_path"] + self.scope["path"], ) self.initial_response = None self.accepted_subprotocol = message.get("subprotocol") self.handshake_started_event.set() elif message_type == "websocket.close": self.logger.info( '%s - "WebSocket %s" 403', self.scope["client"], self.scope["root_path"] + self.scope["path"], ) self.initial_response = (http.HTTPStatus.FORBIDDEN, [], b"") self.handshake_started_event.set() self.closed_event.set() else: msg = "Expected ASGI message 'websocket.accept' or 'websocket.close', but got '%s'." raise RuntimeError(msg % message_type) elif not self.closed_event.is_set(): await self.handshake_completed_event.wait() if message_type == "websocket.send": bytes_data = message.get("bytes") text_data = message.get("text") data = text_data if bytes_data is None else bytes_data await self.send(data) elif message_type == "websocket.close": code = message.get("code", 1000) await self.close(code) self.closed_event.set() else: msg = "Expected ASGI message 'websocket.send' or 'websocket.close', but got '%s'." raise RuntimeError(msg % message_type) else: msg = "Unexpected ASGI message '%s', after sending 'websocket.close'." raise RuntimeError(msg % message_type) async def asgi_receive(self): if not self.connect_sent: self.connect_sent = True return {"type": "websocket.connect"} await self.handshake_completed_event.wait() try: data = await self.recv() except websockets.ConnectionClosed as exc: return {"type": "websocket.disconnect", "code": exc.code} msg = {"type": "websocket.receive"} if isinstance(data, str): msg["text"] = data else: msg["bytes"] = data return msg uvicorn-0.11.3/uvicorn/protocols/websockets/wsproto_impl.py000066400000000000000000000255501362253677400243010ustar00rootroot00000000000000import asyncio import logging from urllib.parse import unquote import h11 import wsproto from wsproto import ConnectionType, events from wsproto.connection import ConnectionState from wsproto.extensions import PerMessageDeflate from wsproto.utilities import RemoteProtocolError from uvicorn.protocols.utils import get_local_addr, get_remote_addr, is_ssl # Check wsproto version. We've build against 0.13. We don't know about 0.14 yet. assert wsproto.__version__ > "0.13", "Need wsproto version 0.13" class WSProtocol(asyncio.Protocol): def __init__(self, config, server_state, _loop=None): if not config.loaded: config.load() self.config = config self.app = config.loaded_app self.loop = _loop or asyncio.get_event_loop() self.logger = logging.getLogger("uvicorn.error") self.root_path = config.root_path # Shared server state self.connections = server_state.connections self.tasks = server_state.tasks # Connection state self.transport = None self.server = None self.client = None self.scheme = None # WebSocket state self.connect_event = None self.queue = asyncio.Queue() self.handshake_complete = False self.close_sent = False self.conn = wsproto.WSConnection(connection_type=ConnectionType.SERVER) self.read_paused = False self.writable = asyncio.Event() self.writable.set() # Buffers self.bytes = b"" self.text = "" # Protocol interface def connection_made(self, transport): self.connections.add(self) self.transport = transport self.server = get_local_addr(transport) self.client = get_remote_addr(transport) self.scheme = "wss" if is_ssl(transport) else "ws" def connection_lost(self, exc): if exc is not None: self.queue.put_nowait({"type": "websocket.disconnect"}) self.connections.remove(self) def eof_received(self): pass def data_received(self, data): try: self.conn.receive_data(data) except RemoteProtocolError as err: if err.event_hint is not None: self.transport.write(self.conn.send(err.event_hint)) self.transport.close() else: self.handle_no_connect(events.CloseConnection()) else: self.handle_events() def handle_events(self): for event in self.conn.events(): if isinstance(event, events.Request): self.handle_connect(event) elif isinstance(event, events.TextMessage): self.handle_text(event) elif isinstance(event, events.BytesMessage): self.handle_bytes(event) elif isinstance(event, events.RejectConnection): self.handle_no_connect(event) elif isinstance(event, events.RejectData): self.handle_no_connect(event) elif isinstance(event, events.CloseConnection): self.handle_close(event) elif isinstance(event, events.Ping): self.handle_ping(event) def pause_writing(self): """ Called by the transport when the write buffer exceeds the high water mark. """ self.writable.clear() def resume_writing(self): """ Called by the transport when the write buffer drops below the low water mark. """ self.writable.set() def shutdown(self): self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) output = self.conn.send(wsproto.events.CloseConnection(code=1012)) self.transport.write(output) self.transport.close() def on_task_complete(self, task): self.tasks.discard(task) # Event handlers def handle_connect(self, event): self.connect_event = event headers = [(b"host", event.host.encode())] headers += [(key.lower(), value) for key, value in event.extra_headers] raw_path, _, query_string = event.target.partition("?") self.scope = { "type": "websocket", "http_version": "1.1", "scheme": self.scheme, "server": self.server, "client": self.client, "root_path": self.root_path, "path": unquote(raw_path), "raw_path": raw_path, "query_string": query_string.encode("ascii"), "headers": headers, "subprotocols": event.subprotocols, } self.queue.put_nowait({"type": "websocket.connect"}) task = self.loop.create_task(self.run_asgi()) task.add_done_callback(self.on_task_complete) self.tasks.add(task) def handle_no_connect(self, event): headers = [ (b"content-type", b"text/plain; charset=utf-8"), (b"connection", b"close"), ] msg = h11.Response(status_code=400, headers=headers, reason="Bad Request") output = self.conn.send(msg) msg = h11.Data(data=event.reason.encode("utf-8")) output += self.conn.send(msg) msg = h11.EndOfMessage() output += self.conn.send(msg) self.transport.write(output) self.transport.close() def handle_text(self, event): self.text += event.data if event.message_finished: self.queue.put_nowait({"type": "websocket.receive", "text": self.text}) self.text = "" if not self.read_paused: self.read_paused = True self.transport.pause_reading() def handle_bytes(self, event): self.bytes += event.data # todo: we may want to guard the size of self.bytes and self.text if event.message_finished: self.queue.put_nowait({"type": "websocket.receive", "bytes": self.bytes}) self.bytes = b"" if not self.read_paused: self.read_paused = True self.transport.pause_reading() def handle_close(self, event): if self.conn.state == ConnectionState.REMOTE_CLOSING: self.transport.write(self.conn.send(event.response())) self.queue.put_nowait({"type": "websocket.disconnect", "code": event.code}) self.transport.close() def handle_ping(self, event): self.transport.write(self.conn.send(event.response())) def send_500_response(self): headers = [ (b"content-type", b"text/plain; charset=utf-8"), (b"connection", b"close"), ] if self.conn.connection is None: output = self.conn.send(wsproto.events.RejectConnection(status_code=500)) else: msg = h11.Response( status_code=500, headers=headers, reason="Internal Server Error" ) output = self.conn.send(msg) msg = h11.Data(data=b"Internal Server Error") output += self.conn.send(msg) msg = h11.EndOfMessage() output += self.conn.send(msg) self.transport.write(output) async def run_asgi(self): try: result = await self.app(self.scope, self.receive, self.send) except BaseException as exc: msg = "Exception in ASGI application\n" self.logger.error(msg, exc_info=exc) if not self.handshake_complete: self.send_500_response() self.transport.close() else: if not self.handshake_complete: msg = "ASGI callable returned without completing handshake." self.logger.error(msg) self.send_500_response() self.transport.close() elif result is not None: msg = "ASGI callable should return None, but returned '%s'." self.logger.error(msg, result) self.transport.close() async def send(self, message): await self.writable.wait() message_type = message["type"] if not self.handshake_complete: if message_type == "websocket.accept": self.logger.info( '%s - "WebSocket %s" [accepted]', self.scope["client"], self.scope["root_path"] + self.scope["path"], ) self.handshake_complete = True subprotocol = message.get("subprotocol") output = self.conn.send( wsproto.events.AcceptConnection( subprotocol=subprotocol, extensions=[PerMessageDeflate()] ) ) self.transport.write(output) elif message_type == "websocket.close": self.queue.put_nowait({"type": "websocket.disconnect", "code": None}) self.logger.info( '%s - "WebSocket %s" 403', self.scope["client"], self.scope["root_path"] + self.scope["path"], ) self.handshake_complete = True self.close_sent = True msg = h11.Response(status_code=403, headers=[]) output = self.conn.send(msg) msg = h11.EndOfMessage() output += self.conn.send(msg) self.transport.write(output) self.transport.close() else: msg = "Expected ASGI message 'websocket.accept' or 'websocket.close', but got '%s'." raise RuntimeError(msg % message_type) elif not self.close_sent: if message_type == "websocket.send": bytes_data = message.get("bytes") text_data = message.get("text") data = text_data if bytes_data is None else bytes_data output = self.conn.send(wsproto.events.Message(data=data)) if not self.transport.is_closing(): self.transport.write(output) elif message_type == "websocket.close": self.close_sent = True code = message.get("code", 1000) self.queue.put_nowait({"type": "websocket.disconnect", "code": code}) output = self.conn.send(wsproto.events.CloseConnection(code=code)) if not self.transport.is_closing(): self.transport.write(output) self.transport.close() else: msg = "Expected ASGI message 'websocket.send' or 'websocket.close', but got '%s'." raise RuntimeError(msg % message_type) else: msg = "Unexpected ASGI message '%s', after sending 'websocket.close'." raise RuntimeError(msg % message_type) async def receive(self): message = await self.queue.get() if self.read_paused and self.queue.empty(): self.read_paused = False self.transport.resume_reading() return message uvicorn-0.11.3/uvicorn/subprocess.py000066400000000000000000000040121362253677400175240ustar00rootroot00000000000000""" Some light wrappers around Python's multiprocessing, to deal with cleanly starting child processes. """ import multiprocessing import os import sys multiprocessing.allow_connection_pickling() spawn = multiprocessing.get_context("spawn") def get_subprocess(config, target, sockets): """ Called in the parent process, to instantiate a new child process instance. The child is not yet started at this point. * config - The Uvicorn configuration instance. * target - A callable that accepts a list of sockets. In practice this will be the `Server.run()` method. * sockets - A list of sockets to pass to the server. Sockets are bound once by the parent process, and then passed to the child processes. """ # We pass across the stdin fileno, and reopen it in the child process. # This is required for some debugging environments. try: stdin_fileno = sys.stdin.fileno() except OSError: stdin_fileno = None kwargs = { "config": config, "target": target, "sockets": sockets, "stdin_fileno": stdin_fileno, } return spawn.Process(target=subprocess_started, kwargs=kwargs) def subprocess_started(config, target, sockets, stdin_fileno): """ Called when the child process starts. * config - The Uvicorn configuration instance. * target - A callable that accepts a list of sockets. In practice this will be the `Server.run()` method. * sockets - A list of sockets to pass to the server. Sockets are bound once by the parent process, and then passed to the child processes. * stdin_fileno - The file number of sys.stdin, so that it can be reattached to the child process. """ # Re-open stdin. if stdin_fileno is not None: sys.stdin = os.fdopen(stdin_fileno) # Logging needs to be setup again for each child. config.configure_logging() # Now we can call into `Server.run(sockets=sockets)` target(sockets=sockets) uvicorn-0.11.3/uvicorn/supervisors/000077500000000000000000000000001362253677400173715ustar00rootroot00000000000000uvicorn-0.11.3/uvicorn/supervisors/__init__.py000066400000000000000000000002321362253677400214770ustar00rootroot00000000000000from uvicorn.supervisors.multiprocess import Multiprocess from uvicorn.supervisors.statreload import StatReload __all__ = ["Multiprocess", "StatReload"] uvicorn-0.11.3/uvicorn/supervisors/multiprocess.py000066400000000000000000000034441362253677400225010ustar00rootroot00000000000000import logging import os import signal import threading import click from uvicorn.subprocess import get_subprocess HANDLED_SIGNALS = ( signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. signal.SIGTERM, # Unix signal 15. Sent by `kill `. ) logger = logging.getLogger("uvicorn.error") class Multiprocess: def __init__(self, config, target, sockets): self.config = config self.target = target self.sockets = sockets self.processes = [] self.should_exit = threading.Event() self.pid = os.getpid() def signal_handler(self, sig, frame): """ A signal handler that is registered with the parent process. """ self.should_exit.set() def run(self): self.startup() self.should_exit.wait() self.shutdown() def startup(self): message = "Started parent process [{}]".format(str(self.pid)) color_message = "Started parent process [{}]".format( click.style(str(self.pid), fg="cyan", bold=True) ) logger.info(message, extra={"color_message": color_message}) for sig in HANDLED_SIGNALS: signal.signal(sig, self.signal_handler) for idx in range(self.config.workers): process = get_subprocess( config=self.config, target=self.target, sockets=self.sockets ) process.start() self.processes.append(process) def shutdown(self): for process in self.processes: process.join() message = "Stopping parent process [{}]".format(str(self.pid)) color_message = "Stopping parent process [{}]".format( click.style(str(self.pid), fg="cyan", bold=True) ) logger.info(message, extra={"color_message": color_message}) uvicorn-0.11.3/uvicorn/supervisors/statreload.py000066400000000000000000000061031362253677400221050ustar00rootroot00000000000000import logging import os import signal import threading from pathlib import Path import click from uvicorn.subprocess import get_subprocess HANDLED_SIGNALS = ( signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. signal.SIGTERM, # Unix signal 15. Sent by `kill `. ) logger = logging.getLogger("uvicorn.error") class StatReload: def __init__(self, config, target, sockets): self.config = config self.target = target self.sockets = sockets self.should_exit = threading.Event() self.pid = os.getpid() self.mtimes = {} def signal_handler(self, sig, frame): """ A signal handler that is registered with the parent process. """ self.should_exit.set() def run(self): self.startup() while not self.should_exit.wait(0.25): if self.should_restart(): self.restart() self.shutdown() def startup(self): message = "Started reloader process [{}]".format(str(self.pid)) color_message = "Started reloader process [{}]".format( click.style(str(self.pid), fg="cyan", bold=True) ) logger.info(message, extra={"color_message": color_message}) for sig in HANDLED_SIGNALS: signal.signal(sig, self.signal_handler) self.process = get_subprocess( config=self.config, target=self.target, sockets=self.sockets ) self.process.start() def restart(self): self.mtimes = {} os.kill(self.process.pid, signal.SIGTERM) self.process.join() self.process = get_subprocess( config=self.config, target=self.target, sockets=self.sockets ) self.process.start() def shutdown(self): self.process.join() message = "Stopping reloader process [{}]".format(str(self.pid)) color_message = "Stopping reloader process [{}]".format( click.style(str(self.pid), fg="cyan", bold=True) ) logger.info(message, extra={"color_message": color_message}) def should_restart(self): for filename in self.iter_py_files(): try: mtime = os.path.getmtime(filename) except OSError as exc: # pragma: nocover continue old_time = self.mtimes.get(filename) if old_time is None: self.mtimes[filename] = mtime continue elif mtime > old_time: display_path = os.path.normpath(filename) if Path.cwd() in Path(filename).parents: display_path = os.path.normpath(os.path.relpath(filename)) message = "Detected file change in '%s'. Reloading..." logger.warning(message, display_path) return True return False def iter_py_files(self): for reload_dir in self.config.reload_dirs: for subdir, dirs, files in os.walk(reload_dir): for file in files: if file.endswith(".py"): yield subdir + os.sep + file uvicorn-0.11.3/uvicorn/workers.py000066400000000000000000000044631362253677400170420ustar00rootroot00000000000000import asyncio import logging from gunicorn.workers.base import Worker from uvicorn.config import Config from uvicorn.main import Server class UvicornWorker(Worker): """ A worker class for Gunicorn that interfaces with an ASGI consumer callable, rather than a WSGI callable. """ CONFIG_KWARGS = {"loop": "uvloop", "http": "httptools"} def __init__(self, *args, **kwargs): super(UvicornWorker, self).__init__(*args, **kwargs) logger = logging.getLogger("uvicorn.error") logger.handlers = self.log.error_log.handlers logger.setLevel(self.log.error_log.level) logger = logging.getLogger("uvicorn.access") logger.handlers = self.log.access_log.handlers logger.setLevel(self.log.access_log.level) config_kwargs = { "app": None, "log_config": None, "timeout_keep_alive": self.cfg.keepalive, "timeout_notify": self.timeout, "callback_notify": self.callback_notify, "limit_max_requests": self.max_requests, } if self.cfg.is_ssl: ssl_kwargs = { "ssl_keyfile": self.cfg.ssl_options.get("keyfile"), "ssl_certfile": self.cfg.ssl_options.get("certfile"), "ssl_version": self.cfg.ssl_options.get("ssl_version"), "ssl_cert_reqs": self.cfg.ssl_options.get("cert_reqs"), "ssl_ca_certs": self.cfg.ssl_options.get("ca_certs"), "ssl_ciphers": self.cfg.ssl_options.get("ciphers"), } config_kwargs.update(ssl_kwargs) if self.cfg.settings["backlog"].value: config_kwargs["backlog"] = self.cfg.settings["backlog"].value config_kwargs.update(self.CONFIG_KWARGS) self.config = Config(**config_kwargs) def init_process(self): self.config.setup_event_loop() super(UvicornWorker, self).init_process() def init_signals(self): pass def run(self): self.config.app = self.wsgi server = Server(config=self.config) loop = asyncio.get_event_loop() loop.run_until_complete(server.serve(sockets=self.sockets)) async def callback_notify(self): self.notify() class UvicornH11Worker(UvicornWorker): CONFIG_KWARGS = {"loop": "asyncio", "http": "h11"}