pax_global_header00006660000000000000000000000064146436261260014524gustar00rootroot0000000000000052 comment=13ca7dfcf2605600ab51fe32d46be8bc096d542f prometheus_flask_exporter-0.23.1/000077500000000000000000000000001464362612600171125ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/.github/000077500000000000000000000000001464362612600204525ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/.github/workflows/000077500000000000000000000000001464362612600225075ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/.github/workflows/test-and-publish.yml000066400000000000000000000046661464362612600264310ustar00rootroot00000000000000name: Test & publish package on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install coverage coveralls pip install -r requirements.txt - name: Run tests run: | python -m coverage run --branch --source=prometheus_flask_exporter -m unittest discover -s tests -v - name: Upload coverage report if: always() && matrix.python-version == '3.10' run: | coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} integration: runs-on: ubuntu-latest needs: test strategy: fail-fast: false matrix: include: - testname: gunicorn - testname: gunicorn-internal - testname: gunicorn-app-factory - testname: uwsgi - testname: uwsgi-lazy-apps - testname: wsgi # - testname: connexion-pydantic - testname: restful-with-blueprints - testname: restful-return-none - testname: restplus-default-metrics - testname: flask-httpauth - testname: flask-multi-processes - testname: reload steps: - uses: actions/checkout@v3 - name: Run integration test for ${{ matrix.testname }} run: | sh examples/${{ matrix.testname }}/run_tests.sh publish: runs-on: ubuntu-latest needs: integration steps: - uses: actions/checkout@v3 - name: Install dependencies run: | pip install build - name: Build package run: | python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} verify: runs-on: ubuntu-latest needs: publish steps: - name: Verify installation run: | pip install prometheus-flask-exporter prometheus_flask_exporter-0.23.1/.gitignore000066400000000000000000000001001464362612600210710ustar00rootroot00000000000000.idea/ .venv/ .venv27/ *.pyc .coverage coverage.xml *.egg-info/ prometheus_flask_exporter-0.23.1/LICENSE000066400000000000000000000020541464362612600201200ustar00rootroot00000000000000MIT License Copyright (c) 2017 Viktor Adam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. prometheus_flask_exporter-0.23.1/MANIFEST.in000066400000000000000000000000421464362612600206440ustar00rootroot00000000000000include README.md include LICENSE prometheus_flask_exporter-0.23.1/README.md000066400000000000000000000444711464362612600204030ustar00rootroot00000000000000# Prometheus Flask exporter [![PyPI](https://img.shields.io/pypi/v/prometheus-flask-exporter.svg)](https://pypi.python.org/pypi/prometheus-flask-exporter) [![PyPI](https://img.shields.io/pypi/pyversions/prometheus-flask-exporter.svg)](https://pypi.python.org/pypi/prometheus-flask-exporter) [![PyPI - Downloads](https://img.shields.io/pypi/dm/prometheus-flask-exporter.svg)](https://pypi.python.org/pypi/prometheus-flask-exporter) [![Coverage Status](https://coveralls.io/repos/github/rycus86/prometheus_flask_exporter/badge.svg?branch=master)](https://coveralls.io/github/rycus86/prometheus_flask_exporter?branch=master) [![Code Climate](https://codeclimate.com/github/rycus86/prometheus_flask_exporter/badges/gpa.svg)](https://codeclimate.com/github/rycus86/prometheus_flask_exporter) [![Test & publish package](https://github.com/rycus86/prometheus_flask_exporter/actions/workflows/test-and-publish.yml/badge.svg)](https://github.com/rycus86/prometheus_flask_exporter/actions/workflows/test-and-publish.yml) This library provides HTTP request metrics to export into [Prometheus](https://prometheus.io/). It can also track method invocations using convenient functions. ## Installing Install using [PIP](https://pip.pypa.io/en/stable/quickstart/): ```bash pip install prometheus-flask-exporter ``` or paste it into requirements.txt: ``` # newest version prometheus-flask-exporter # or with specific version number prometheus-flask-exporter==0.23.1 ``` and then install dependencies from requirements.txt file as usual: ``` pip install -r requirements.txt ``` ## Usage ```python from flask import Flask, request from prometheus_flask_exporter import PrometheusMetrics app = Flask(__name__) metrics = PrometheusMetrics(app) # static information as metric metrics.info('app_info', 'Application info', version='1.0.3') @app.route('/') def main(): pass # requests tracked by default @app.route('/skip') @metrics.do_not_track() def skip(): pass # default metrics are not collected @app.route('/') @metrics.do_not_track() @metrics.counter('invocation_by_type', 'Number of invocations by type', labels={'item_type': lambda: request.view_args['type']}) def by_type(item_type): pass # only the counter is collected, not the default metrics @app.route('/long-running') @metrics.gauge('in_progress', 'Long running requests in progress') def long_running(): pass @app.route('/status/') @metrics.do_not_track() @metrics.summary('requests_by_status', 'Request latencies by status', labels={'status': lambda r: r.status_code}) @metrics.histogram('requests_by_status_and_path', 'Request latencies by status and path', labels={'status': lambda r: r.status_code, 'path': lambda: request.path}) def echo_status(status): return 'Status: %s' % status, status ``` ## Default metrics The following metrics are exported by default (unless the `export_defaults` is set to `False`). - `flask_http_request_duration_seconds` (Histogram) Labels: `method`, `path` and `status`. Flask HTTP request duration in seconds for all Flask requests. - `flask_http_request_total` (Counter) Labels: `method` and `status`. Total number of HTTP requests for all Flask requests. - `flask_http_request_exceptions_total` (Counter) Labels: `method` and `status`. Total number of uncaught exceptions when serving Flask requests. - `flask_exporter_info` (Gauge) Information about the Prometheus Flask exporter itself (e.g. `version`). The prefix for the default metrics can be controlled by the `defaults_prefix` parameter. If you don't want to use any prefix, pass the `prometheus_flask_exporter.NO_PREFIX` value in. The buckets on the default request latency histogram can be changed by the `buckets` parameter, and if using a summary for them is more appropriate for your use case, then use the `default_latency_as_histogram=False` parameter. To register your own *default* metrics that will track all registered Flask view functions, use the `register_default` function. ```python app = Flask(__name__) metrics = PrometheusMetrics(app) @app.route('/simple') def simple_get(): pass metrics.register_default( metrics.counter( 'by_path_counter', 'Request count by request paths', labels={'path': lambda: request.path} ) ) ``` *Note:* register your default metrics after all routes have been set up. Also note, that Gauge metrics registered as default will track the `/metrics` endpoint, and this can't be disabled at the moment. If you want to apply the same metric to multiple (but not all) endpoints, create its wrapper first, then add to each function. ```python app = Flask(__name__) metrics = PrometheusMetrics(app) by_path_counter = metrics.counter( 'by_path_counter', 'Request count by request paths', labels={'path': lambda: request.path} ) @app.route('/simple') @by_path_counter def simple_get(): pass @app.route('/plain') @by_path_counter def plain(): pass @app.route('/not/tracked/by/path') def not_tracked_by_path(): pass ``` You can avoid recording metrics on individual endpoints by decorating them with `@metrics.do_not_track()`, or use the `excluded_paths` argument when creating the `PrometheusMetrics` instance that takes a regular expression (either a single string, or a list) and matching paths will be excluded. These apply to both built-in and user-defined default metrics, unless you disable it by setting the `exclude_user_defaults` argument to `False`. If you have functions that are inherited or otherwise get metrics collected that you don't want, you can use `@metrics.exclude_all_metrics()` to exclude both default and non-default metrics being collected from it. ## Configuration By default, the metrics are exposed on the same Flask application on the `/metrics` endpoint and using the core Prometheus registry. If this doesn't suit your needs, set the `path` argument to `None` and/or the `export_defaults` argument to `False` plus change the `registry` argument if needed. The `group_by` constructor argument controls what the default request duration metric is tracked by: endpoint (function) instead of URI path (the default). This parameter also accepts a function to extract the value from the request, or a name of a property of the request object. Examples: ```python PrometheusMetrics(app, group_by='path') # the default PrometheusMetrics(app, group_by='endpoint') # by endpoint PrometheusMetrics(app, group_by='url_rule') # by URL rule def custom_rule(req): # the Flask request object """ The name of the function becomes the label name. """ return '%s::%s' % (req.method, req.path) PrometheusMetrics(app, group_by=custom_rule) # by a function # Error: this is not supported: PrometheusMetrics(app, group_by=lambda r: r.path) ``` > The `group_by_endpoint` argument is deprecated since 0.4.0, > please use the new `group_by` argument. The `register_endpoint` allows exposing the metrics endpoint on a specific path. It also allows passing in a Flask application to register it on but defaults to the main one if not defined. Similarly, the `start_http_server` allows exposing the endpoint on an independent Flask application on a selected HTTP port. It also supports overriding the endpoint's path and the HTTP listen address. You can also set default labels to add to every request managed by a `PrometheusMetrics` instance, using the `default_labels` argument. This needs to be a dictionary, where each key will become a metric label name, and the values the label values. These can be constant values, or dynamic functions, see below in the [Labels](#Labels) section. > The `static_labels` argument is deprecated since 0.15.0, > please use the new `default_labels` argument. If you use another framework over Flask (perhaps [Connexion](https://connexion.readthedocs.io/)) then you might return responses from your endpoints that Flask can't deal with by default. If that is the case, you might need to pass in a `response_converter` that takes the returned object and should convert that to a Flask friendly response. See `ConnexionPrometheusMetrics` for an example. ## Labels When defining labels for metrics on functions, the following values are supported in the dictionary: - A simple static value - A no-argument callable - A single argument callable that will receive the Flask response as the argument Label values are evaluated within the request context. ## Initial metric values _For more info see: https://github.com/prometheus/client_python#labels_ Metrics without any labels will get an initial value. Metrics that only have static-value labels will also have an initial value. (except when they are created with the option `initial_value_when_only_static_labels=False`) Metrics that have one or more callable-value labels will not have an initial value. ## Application information The `PrometheusMetrics.info(..)` method provides a way to expose information as a `Gauge` metric, the application version for example. The metric is returned from the method to allow changing its value from the default `1`: ```python metrics = PrometheusMetrics(app) info = metrics.info('dynamic_info', 'Something dynamic') ... info.set(42.1) ``` ## Examples See some simple examples visualized on a Grafana dashboard by running the demo in the [examples/sample-signals](https://github.com/rycus86/prometheus_flask_exporter/tree/master/examples/sample-signals) folder. ![Example dashboard](https://github.com/rycus86/prometheus_flask_exporter/raw/master/examples/sample-signals/dashboard.png) ## App Factory Pattern This library also supports the Flask [app factory pattern](http://flask.pocoo.org/docs/1.0/patterns/appfactories/). Use the `init_app` method to attach the library to one or more application objects. Note, that to use this mode, you'll need to use the `for_app_factory()` class method to create the `metrics` instance, or pass in `None` for the `app` in the constructor. ```python metrics = PrometheusMetrics.for_app_factory() # then later: metrics.init_app(app) ``` ## Securing the metrics endpoint If you wish to have authentication (or any other special handling) on the metrics endpoint, you can use the `metrics_decorator` argument when creating the `PrometheusMetrics` instance. For example to integrate with [Flask-HTTPAuth](https://github.com/miguelgrinberg/Flask-HTTPAuth) use it like it's shown in the example below. ```python app = Flask(__name__) auth = HTTPBasicAuth() metrics = PrometheusMetrics(app, metrics_decorator=auth.login_required) # ... other authentication setup like @auth.verify_password below ``` See a full example in the [examples/flask-httpauth](https://github.com/rycus86/prometheus_flask_exporter/tree/master/examples/flask-httpauth) folder. ## Custom metrics endpoint You can also take full control of the metrics endpoint by generating its contents, and managing how it is exposed by yourself. ```python app = Flask(__name__) # path=None to avoid registering a /metrics endpoint on the same Flask app metrics = PrometheusMetrics(app, path=None) # later ... generate the response (and its content type) to expose to Prometheus response_data, content_type = metrics.generate_metrics() ``` See the related conversation in [issue #135](https://github.com/rycus86/prometheus_flask_exporter/issues/135). ## Debug mode Please note, that changes being live-reloaded, when running the Flask app with `debug=True`, are not going to be reflected in the metrics. See [https://github.com/rycus86/prometheus_flask_exporter/issues/4](https://github.com/rycus86/prometheus_flask_exporter/issues/4) for more details. Alternatively - since version `0.5.1` - if you set the `DEBUG_METRICS` environment variable, you will get metrics for the latest reloaded code. These will be exported on the main Flask app. Serving the metrics on a different port is not going to work most probably - e.g. `PrometheusMetrics.start_http_server(..)` is not expected to work. ## WSGI Getting accurate metrics for WSGI apps might require a bit more setup. See a working sample app in the `examples` folder, and also the [prometheus_flask_exporter#5](https://github.com/rycus86/prometheus_flask_exporter/issues/5) issue. ### Multiprocess applications For multiprocess applications (WSGI or otherwise), you can find some helper classes in the `prometheus_flask_exporter.multiprocess` module. These provide convenience wrappers for exposing metrics in an environment where multiple copies of the application will run on a single host. ```python # an extension targeted at Gunicorn deployments from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics app = Flask(__name__) metrics = GunicornPrometheusMetrics(app) # then in the Gunicorn config file: from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics def when_ready(server): GunicornPrometheusMetrics.start_http_server_when_ready(8080) def child_exit(server, worker): GunicornPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) ``` Also see the `GunicornInternalPrometheusMetrics` class if you want to have the metrics HTTP endpoint exposed internally, on the same Flask application. ```python # an extension targeted at Gunicorn deployments with an internal metrics endpoint from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics app = Flask(__name__) metrics = GunicornInternalPrometheusMetrics(app) # then in the Gunicorn config file: from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics def child_exit(server, worker): GunicornInternalPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) ``` There's a small wrapper available for [Gunicorn](https://gunicorn.org/) and [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/index.html), for everything else you can extend the `prometheus_flask_exporter.multiprocess.MultiprocessPrometheusMetrics` class and implement the `should_start_http_server` method at least. ```python from prometheus_flask_exporter.multiprocess import MultiprocessPrometheusMetrics class MyMultiprocessMetrics(MultiprocessPrometheusMetrics): def should_start_http_server(self): return this_worker() == primary_worker() ``` This should return `True` on one process only, and the underlying [Prometheus client library](https://github.com/prometheus/client_python) will collect the metrics for all the forked children or siblings. An additional Flask extension for apps with `processes=N` and `threaded=False` exists with the `MultiprocessInternalPrometheusMetrics` class. ```python from flask import Flask from prometheus_flask_exporter.multiprocess import MultiprocessInternalPrometheusMetrics app = Flask(__name__) metrics = MultiprocessInternalPrometheusMetrics(app) ... if __name__ == '__main__': app.run('0.0.0.0', 4000, processes=5, threaded=False) ``` __Note:__ this needs the `PROMETHEUS_MULTIPROC_DIR` environment variable to point to a valid, writable directory. You'll also have to call the `metrics.start_http_server()` function explicitly somewhere, and the `should_start_http_server` takes care of only starting it once. The [examples](https://github.com/rycus86/prometheus_flask_exporter/tree/master/examples) folder has some working examples on this. Please also note, that the Prometheus client library does not collect process level metrics, like memory, CPU and Python GC stats when multiprocessing is enabled. See the [prometheus_flask_exporter#18](https://github.com/rycus86/prometheus_flask_exporter/issues/18) issue for some more context and details. A final caveat is that the metrics HTTP server will listen on __any__ paths on the given HTTP port, not only on `/metrics`, and it is not implemented at the moment to be able to change this. ### uWSGI lazy-apps When [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) is configured to run with [lazy-apps]([lazy-apps](https://uwsgi-docs.readthedocs.io/en/latest/articles/TheArtOfGracefulReloading.html#preforking-vs-lazy-apps-vs-lazy)), exposing the metrics endpoint on a separate HTTP server (and port) is not functioning yet. A workaround is to register the endpoint on the main Flask application. ```python app = Flask(__name__) metrics = UWsgiPrometheusMetrics(app) metrics.register_endpoint('/metrics') # instead of metrics.start_http_server(port) ``` See [#31](https://github.com/rycus86/prometheus_flask_exporter/issues/31) for context, and please let me know if you know a better way! ## Connexion integration The [Connexion](https://connexion.readthedocs.io/) library has some support to automatically deal with certain response types, for example dataclasses, which a plain Flask application would not accept. To ease the integration, you can use `ConnexionPrometheusMetrics` in place of `PrometheusMetrics` that has the `response_converter` set appropriately to be able to deal with whatever Connexion supports for Flask integrations. ```python import connexion from prometheus_flask_exporter import ConnexionPrometheusMetrics app = connexion.App(__name__) metrics = ConnexionPrometheusMetrics(app) ``` See a working sample app in the `examples` folder, and also the [prometheus_flask_exporter#61](https://github.com/rycus86/prometheus_flask_exporter/issues/61) issue. There's a caveat about this integration, where any endpoints that do not return JSON responses need to be decorated with `@metrics.content_type('...')` as this integration would force them to be `application/json` otherwise. ```python metrics = ConnexionPrometheusMetrics(app) @metrics.content_type('text/plain') def plain_response(): return 'plain text' ``` See the [prometheus_flask_exporter#64](https://github.com/rycus86/prometheus_flask_exporter/issues/64) issue for more details. ## Flask-RESTful integration The [Flask-RESTful library](https://flask-restful.readthedocs.io/) has some custom response handling logic, which can be helpful in some cases. For example, returning `None` would fail on plain Flask, but it works on Flask-RESTful. To ease the integration, you can use `RESTfulPrometheusMetrics` in place of `PrometheusMetrics` that sets the `response_converter` to use the Flask-RESTful `API` response utilities. ```python from flask import Flask from flask_restful import Api from prometheus_flask_exporter import RESTfulPrometheusMetrics app = Flask(__name__) restful_api = Api(app) metrics = RESTfulPrometheusMetrics(app, restful_api) ``` See a working sample app in the `examples` folder, and also the [prometheus_flask_exporter#62](https://github.com/rycus86/prometheus_flask_exporter/issues/62) issue. ## License MIT prometheus_flask_exporter-0.23.1/dashboards/000077500000000000000000000000001464362612600212245ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/dashboards/flask_webapp.json000066400000000000000000000516031464362612600245620ustar00rootroot00000000000000{ "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "6.0.2" }, { "type": "panel", "id": "graph", "name": "Graph", "version": "5.0.0" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "5.0.0" }, { "type": "panel", "id": "singlestat", "name": "Singlestat", "version": "5.0.0" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "Example dashboard for monitoring Flask webapps using prometheus_flask_exporter", "editable": true, "gnetId": null, "graphTooltip": 0, "id": 6, "links": [], "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 4, "w": 10, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 2, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Requests per second", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 4, "w": 6, "x": 10, "y": 0 }, "hiddenSeries": false, "id": 4, "legend": { "avg": true, "current": true, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "errors", "color": "#c15c17" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(flask_http_request_duration_seconds_count{status!=\"200\"}[30s]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "errors", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Errors per second", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 4, "w": 8, "x": 16, "y": 0 }, "hiddenSeries": false, "id": 13, "legend": { "avg": true, "current": false, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": false, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "HTTP 500", "color": "#bf1b00" } ], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "increase(flask_http_request_total[1m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "HTTP {{ status }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Total requests per minute", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 10, "x": 0, "y": 4 }, "hiddenSeries": false, "id": 6, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(flask_http_request_duration_seconds_sum{status=\"200\"}[30s])\n/\nrate(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Average response time [30s]", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "s", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "description": "", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 9, "x": 10, "y": 4 }, "hiddenSeries": false, "id": 15, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "histogram_quantile(0.5, rate(flask_http_request_duration_seconds_bucket{status=\"200\"}[30s]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Request duration [s] - p50", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "none", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 5, "x": 19, "y": 4 }, "hiddenSeries": false, "id": 8, "legend": { "avg": false, "current": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "process_resident_memory_bytes{job=\"flaskapp\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "mem", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Memory usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "decbytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 10, "x": 0, "y": 9 }, "hiddenSeries": false, "id": 11, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "increase(flask_http_request_duration_seconds_bucket{status=\"200\",le=\"0.25\"}[30s]) \n/ ignoring (le) increase(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Requests under 250ms", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "percentunit", "label": null, "logBase": 1, "max": "1", "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 9, "x": 10, "y": 9 }, "hiddenSeries": false, "id": 16, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "histogram_quantile(0.9, rate(flask_http_request_duration_seconds_bucket{status=\"200\"}[30s]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Request duration [s] - p90", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS}", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 5, "x": 19, "y": 9 }, "hiddenSeries": false, "id": 9, "legend": { "avg": false, "current": true, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "7.1.0", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(process_cpu_seconds_total{job=\"flaskapp\"}[30s])", "format": "time_series", "intervalFactor": 1, "legendFormat": "cpu", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CPU usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "percentunit", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "refresh": "5s", "schemaVersion": 26, "style": "dark", "tags": [], "templating": { "list": [] }, "time": { "from": "now-5m", "to": "now" }, "timepicker": { "refresh_intervals": [], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "", "title": "Prometheus Flask exporter example dashboard", "uid": "_eX4mpl3", "version": 2 } prometheus_flask_exporter-0.23.1/examples/000077500000000000000000000000001464362612600207305ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/connexion-pydantic/000077500000000000000000000000001464362612600245415ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/connexion-pydantic/Dockerfile000066400000000000000000000004201464362612600265270ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache curl && pip install flask==2.2.4 connexion pydantic prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/connexion-pydantic /var/flask WORKDIR /var/flask CMD python /var/flask/main.py prometheus_flask_exporter-0.23.1/examples/connexion-pydantic/README.md000066400000000000000000000003461464362612600260230ustar00rootroot00000000000000# Connexion with pydantic This is a test case for [issue #61](https://github.com/rycus86/prometheus_flask_exporter/issues/61) **This test is currently not running on CI as newer Connexion versions need different setup somehow** prometheus_flask_exporter-0.23.1/examples/connexion-pydantic/endpoint.py000066400000000000000000000006601464362612600267350ustar00rootroot00000000000000from pydantic.dataclasses import dataclass from main import metrics @dataclass class Info: foo: str @metrics.summary('test_by_status', 'Test Request latencies by status', labels={ 'code': lambda r: r.status_code }) def test() -> Info: return Info('Test version') @metrics.content_type('text/plain') @metrics.counter('test_plain', 'Counter for plain responses') def plain() -> str: return 'Test plain response' prometheus_flask_exporter-0.23.1/examples/connexion-pydantic/main.py000066400000000000000000000004261464362612600260410ustar00rootroot00000000000000import connexion from prometheus_flask_exporter import ConnexionPrometheusMetrics app = connexion.App(__name__) metrics = ConnexionPrometheusMetrics(app) if __name__ == '__main__': app.add_api('my_api.yaml') app.app.run(host='0.0.0.0', port=4000, use_reloader=False) prometheus_flask_exporter-0.23.1/examples/connexion-pydantic/my_api.yaml000066400000000000000000000005731464362612600267100ustar00rootroot00000000000000openapi: 3.0.0 info: version: 1.0.0 title: Test paths: /test: get: operationId: endpoint.test responses: '200': description: Test /plain: get: operationId: endpoint.plain responses: '200': description: Plain content: text/plain: schema: type: string prometheus_flask_exporter-0.23.1/examples/connexion-pydantic/run_tests.sh000077500000000000000000000025611464362612600271320ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs connexion-pydantic docker rm -f connexion-pydantic > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t connexion-pydantic ../../. > /dev/null || _fail docker run -d --name connexion-pydantic -p 4000:4000 connexion-pydantic > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s -i http://localhost:4000/test | grep 'Content-Type: application/json' -q if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done for _ in $(seq 1 7); do curl -s -i http://localhost:4000/plain | grep 'Content-Type: text/plain' -q if [ "$?" != "0" ]; then echo 'Failed to request the plain endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'test_by_status_count{code="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi curl -s http://localhost:4000/metrics \ | grep 'test_plain_total 7.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f connexion-pydantic > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/flask-httpauth/000077500000000000000000000000001464362612600236675ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/flask-httpauth/Dockerfile000066400000000000000000000004211464362612600256560ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache curl \ && pip install flask Flask-HTTPAuth prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/flask-httpauth /var/flask WORKDIR /var/flask CMD python /var/flask/httpauth_example.py prometheus_flask_exporter-0.23.1/examples/flask-httpauth/README.md000066400000000000000000000002021464362612600251400ustar00rootroot00000000000000# Flask-HTTPAuth integration This is a test case for [issue #81](https://github.com/rycus86/prometheus_flask_exporter/issues/81) prometheus_flask_exporter-0.23.1/examples/flask-httpauth/httpauth_example.py000066400000000000000000000010111464362612600276060ustar00rootroot00000000000000from flask import Flask from flask_httpauth import HTTPBasicAuth from prometheus_flask_exporter import PrometheusMetrics app = Flask(__name__) auth = HTTPBasicAuth() metrics = PrometheusMetrics(app, metrics_decorator=auth.login_required) @auth.verify_password def verify_credentials(username, password): return (username, password) in {('metrics', 'test'), ('user', 'pass')} @app.route('/test') @auth.login_required def index(): return 'Hello world' if __name__ == '__main__': app.run('0.0.0.0', 4000) prometheus_flask_exporter-0.23.1/examples/flask-httpauth/run_tests.sh000077500000000000000000000027001464362612600262530ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs flask-httpauth docker rm -f flask-httpauth > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t flask-httpauth ../../. > /dev/null || _fail docker run -d --name flask-httpauth -p 4000:4000 flask-httpauth > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs -u 'metrics:test' http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s -u 'user:pass' http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s -u 'metrics:test' http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi # ensure we can't access the endpoints without authentication if ! curl -sv http://localhost:4000/metrics 2>&1 | grep -qiE 'HTTP/[0-9.]+ 401 Unauthorized'; then echo 'Unexpected unauthenticated response on the metrics endpoint' _fail fi if ! curl -sv http://localhost:4000/test 2>&1 | grep -qiE 'HTTP/[0-9.]+ 401 Unauthorized'; then echo 'Unexpected unauthenticated response on the test endpoint' _fail fi docker rm -f flask-httpauth > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/flask-multi-processes/000077500000000000000000000000001464362612600251645ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/flask-multi-processes/Dockerfile000066400000000000000000000004541464362612600271610ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache curl \ && pip install flask prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/flask-multi-processes /var/flask WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp CMD python /var/flask/processes_example.py prometheus_flask_exporter-0.23.1/examples/flask-multi-processes/README.md000066400000000000000000000002121464362612600264360ustar00rootroot00000000000000# Flask multiple processes example This is a test case for [issue #121](https://github.com/rycus86/prometheus_flask_exporter/issues/121) prometheus_flask_exporter-0.23.1/examples/flask-multi-processes/processes_example.py000066400000000000000000000005201464362612600312540ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter.multiprocess import MultiprocessInternalPrometheusMetrics app = Flask(__name__) metrics = MultiprocessInternalPrometheusMetrics(app) @app.route('/test') def index(): return 'Hello world' if __name__ == '__main__': app.run('0.0.0.0', 4000, processes=5, threaded=False) prometheus_flask_exporter-0.23.1/examples/flask-multi-processes/run_tests.sh000077500000000000000000000022151464362612600275510ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs flask-multi-processes docker rm -f flask-multi-processes > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t flask-multi-processes ../../. > /dev/null || _fail docker run -d --name flask-multi-processes -p 4000:4000 flask-multi-processes > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_total{method="GET",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f flask-multi-processes > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/flask-run-135/000077500000000000000000000000001464362612600231405ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/flask-run-135/Dockerfile000066400000000000000000000004511464362612600251320ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache curl \ && pip install flask prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/flask-run-135 /var/flask WORKDIR /var/flask ENV DEBUG_METRICS true CMD flask run --host 0.0.0.0 --port 4000 --debug --reload prometheus_flask_exporter-0.23.1/examples/flask-run-135/README.md000066400000000000000000000002041464362612600244130ustar00rootroot00000000000000# Flask run example for #135 This is a test case for [issue #135](https://github.com/rycus86/prometheus_flask_exporter/issues/135) prometheus_flask_exporter-0.23.1/examples/flask-run-135/app.py000066400000000000000000000005061464362612600242730ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter import PrometheusMetrics app = Flask(__name__) metrics = PrometheusMetrics(app) @app.get('/info') def info(): import os return {'response': 'ok', 'env': dict(os.environ)} if __name__ == '__main__': app.run('0.0.0.0', 5000, debug=True, use_reloader=True)prometheus_flask_exporter-0.23.1/examples/flask-run-135/run_tests.sh000077500000000000000000000021351464362612600255260ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs flask-run-135 docker rm -f flask-run-135 > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t flask-run-135 ../../. > /dev/null || _fail docker run -d --name flask-run-135 -p 4000:4000 flask-run-135 > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/info > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/info",status="200"} 10.0' \ > /dev/null curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_total{method="GET",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f flask-run-135 > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/000077500000000000000000000000001464362612600247775ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/Dockerfile000066400000000000000000000007701464362612600267750ustar00rootroot00000000000000FROM python:3.11-alpine ADD examples/gunicorn-app-factory/requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/gunicorn-app-factory/server.py \ examples/gunicorn-app-factory/app_setup.py \ examples/gunicorn-app-factory/config.py \ /var/flask/ WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp CMD gunicorn -c config.py -w 4 -b 0.0.0.0:4000 server:app prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/README.md000066400000000000000000000014521464362612600262600ustar00rootroot00000000000000# Gunicorn example (app factory pattern) This [Gunicorn](https://gunicorn.org/) example has a sample app in [server.py](server.py) with multiprocessing enabled for metrics collection. In practise, this means the individual forks metrics will be combined, and the metrics endpoint from any of them should include the global stats. This example exposes the metrics on an individual internal HTTP endpoint, but still within the same Gunicorn server, also configured by the [config.py](config.py) file. The app is configured using the [app factory pattern](http://flask.pocoo.org/docs/1.0/patterns/appfactories/) ## Thanks Huge thanks for [@mamor1](https://github.com/mamor1) for bringing this to my attention in [prometheus_flask_exporter#33](https://github.com/rycus86/prometheus_flask_exporter/issues/33) ! prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/app_setup.py000066400000000000000000000004031464362612600273460ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics metrics = GunicornInternalPrometheusMetrics.for_app_factory() def create_app(): app = Flask(__name__) metrics.init_app(app) return app prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/config.py000066400000000000000000000003111464362612600266110ustar00rootroot00000000000000from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics def child_exit(server, worker): GunicornInternalPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/requirements.txt000066400000000000000000000000731464362612600302630ustar00rootroot00000000000000flask gunicorn prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/run_tests.sh000077500000000000000000000024111464362612600273620ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs gunicorn-app-factory-sample docker rm -f gunicorn-app-factory-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t gunicorn-app-factory-sample ../../. > /dev/null || _fail docker run -d --name gunicorn-app-factory-sample -p 4000:4000 gunicorn-app-factory-sample > /dev/null || _fail echo 'Waiting for Gunicorn to start...' for _ in $(seq 1 10); do PROCESS_COUNT=$(docker exec gunicorn-app-factory-sample sh -c 'pgrep -a gunicorn | wc -l') if [ $PROCESS_COUNT -ge 5 ]; then break fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi curl -s http://localhost:4000/metrics \ | grep 'cnt_index_total 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f gunicorn-app-factory-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/gunicorn-app-factory/server.py000066400000000000000000000003571464362612600266640ustar00rootroot00000000000000from app_setup import create_app, metrics app = create_app() @app.route('/test') @metrics.counter('cnt_index', 'Counts invocations') def index(): return 'Hello world' if __name__ == '__main__': app.run(debug=False, port=5000) prometheus_flask_exporter-0.23.1/examples/gunicorn-exceptions-113/000077500000000000000000000000001464362612600252355ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/gunicorn-exceptions-113/Dockerfile000066400000000000000000000006361464362612600272340ustar00rootroot00000000000000FROM python:3.11-alpine ADD examples/gunicorn-exceptions-113/requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/gunicorn-exceptions-113/server.py examples/gunicorn-exceptions-113/config.py /var/flask/ WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp CMD gunicorn -c config.py -w 4 -b 0.0.0.0:4000 server prometheus_flask_exporter-0.23.1/examples/gunicorn-exceptions-113/README.md000066400000000000000000000010201464362612600265050ustar00rootroot00000000000000# Gunicorn example This [Gunicorn](https://gunicorn.org/) example has a sample app in [server.py](server.py) with multiprocessing enabled for metrics collection. This example checks that exception metrics are not counted twice due to both the Flask `after_request` and `teardown_request` callbacks seeing that request. ## Thanks Huge thanks for [@idlefella](https://github.com/idlefella) for bringing this to my attention in [prometheus_flask_exporter#113](https://github.com/rycus86/prometheus_flask_exporter/issues/113) ! prometheus_flask_exporter-0.23.1/examples/gunicorn-exceptions-113/config.py000066400000000000000000000003111464362612600270470ustar00rootroot00000000000000from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics def child_exit(server, worker): GunicornInternalPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) prometheus_flask_exporter-0.23.1/examples/gunicorn-exceptions-113/requirements.txt000066400000000000000000000000731464362612600305210ustar00rootroot00000000000000flask gunicorn prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/gunicorn-exceptions-113/run_tests.sh000077500000000000000000000022031464362612600276170ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs gunicorn-sample docker rm -f gunicorn-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t gunicorn-sample ../../. > /dev/null || _fail docker run -d --name gunicorn-sample -p 4000:4000 -p 9200:9200 gunicorn-sample > /dev/null || _fail echo 'Waiting for Gunicorn to start...' for _ in $(seq 1 10); do PROCESS_COUNT=$(docker exec gunicorn-sample sh -c 'pgrep -a gunicorn | wc -l') if [ "$PROCESS_COUNT" -ge 5 ]; then break fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_total{method="GET",status="500"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="500"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f gunicorn-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/gunicorn-exceptions-113/server.py000066400000000000000000000007361464362612600271230ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics application = Flask(__name__) metrics = GunicornInternalPrometheusMetrics(application) # static information as metric metrics.info('app_info', 'Application info', version='1.0.3') @application.route('/test') def main(): raise Exception("Crashing") pass # requests tracked by default if __name__ == '__main__': application.run(debug=False, port=5000) prometheus_flask_exporter-0.23.1/examples/gunicorn-internal/000077500000000000000000000000001464362612600243665ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/gunicorn-internal/Dockerfile000066400000000000000000000006621464362612600263640ustar00rootroot00000000000000FROM python:3.11-alpine ADD examples/gunicorn-internal/requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/gunicorn-internal/server.py examples/gunicorn-internal/config.py /var/flask/ WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp CMD gunicorn -c config.py -w 4 -b 0.0.0.0:4000 server:app prometheus_flask_exporter-0.23.1/examples/gunicorn-internal/README.md000066400000000000000000000013041464362612600256430ustar00rootroot00000000000000# Gunicorn example (internal metrics endpoint) This [Gunicorn](https://gunicorn.org/) example has a sample app in [server.py](server.py) with multiprocessing enabled for metrics collection. In practise, this means the individual forks metrics will be combined, and the metrics endpoint from any of them should include the global stats. This example exposes the metrics on an individual internal HTTP endpoint, but still within the same Gunicorn server, also configured by the [config.py](config.py) file. ## Thanks Huge thanks for [@Miouge1](https://github.com/Miouge1) for bringing this to my attention in [prometheus_flask_exporter#15](https://github.com/rycus86/prometheus_flask_exporter/issues/15) ! prometheus_flask_exporter-0.23.1/examples/gunicorn-internal/config.py000066400000000000000000000003111464362612600262000ustar00rootroot00000000000000from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics def child_exit(server, worker): GunicornInternalPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) prometheus_flask_exporter-0.23.1/examples/gunicorn-internal/requirements.txt000066400000000000000000000000731464362612600276520ustar00rootroot00000000000000flask gunicorn prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/gunicorn-internal/run_tests.sh000077500000000000000000000025761464362612600267650ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs gunicorn-internal-sample docker rm -f gunicorn-internal-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t gunicorn-internal-sample ../../. > /dev/null || _fail docker run -d --name gunicorn-internal-sample -p 4000:4000 -p 9200:9200 gunicorn-internal-sample > /dev/null || _fail echo 'Waiting for Gunicorn to start...' for _ in $(seq 1 10); do PROCESS_COUNT=$(docker exec gunicorn-internal-sample sh -c 'pgrep -a gunicorn | wc -l') if [ $PROCESS_COUNT -ge 5 ]; then break fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi for _ in $(seq 1 10); do curl -s http://localhost:4000/error > /dev/null done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_total{method="GET",status="500"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected error metrics are not found' _fail fi docker rm -f gunicorn-internal-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/gunicorn-internal/server.py000066400000000000000000000005631464362612600262520ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics app = Flask(__name__) metrics = GunicornInternalPrometheusMetrics(app) @app.route('/test') def index(): return 'Hello world' @app.route('/error') def error(): raise Exception('Fail') if __name__ == '__main__': app.run(debug=False, port=5000) prometheus_flask_exporter-0.23.1/examples/gunicorn-multiprocess-109/000077500000000000000000000000001464362612600256125ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/gunicorn-multiprocess-109/Dockerfile000066400000000000000000000006031464362612600276030ustar00rootroot00000000000000FROM python:3.11-alpine ADD examples/gunicorn-multiprocess-109/requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/gunicorn-multiprocess-109/server.py /var/flask/ WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp CMD python /var/flask/server.py prometheus_flask_exporter-0.23.1/examples/gunicorn-multiprocess-109/README.md000066400000000000000000000013661464362612600270770ustar00rootroot00000000000000# Gunicorn example This [Gunicorn](https://gunicorn.org/) example has a sample app in [server.py](server.py) with multiprocessing enabled for metrics collection. In practise, this means the individual forks metrics will be combined, and the metrics endpoint from any of them should include the global stats. This example exposes the metrics on an individual endpoint, managed by [prometheus_client](https://github.com/prometheus/client_python#multiprocess-mode-gunicorn), started only on the master process, configured in the [config.py](config.py) file. ## Thanks Huge thanks for [@focabr](https://github.com/focabr) for bringing this to my attention in [prometheus_flask_exporter#109](https://github.com/rycus86/prometheus_flask_exporter/issues/109) ! prometheus_flask_exporter-0.23.1/examples/gunicorn-multiprocess-109/requirements.txt000066400000000000000000000001021464362612600310670ustar00rootroot00000000000000flask gunicorn pebble prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/gunicorn-multiprocess-109/run_tests.sh000077500000000000000000000024711464362612600302030ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs gunicorn-sample docker rm -f gunicorn-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t gunicorn-sample ../../. > /dev/null || _fail docker run -d --name gunicorn-sample -p 9200:9200 gunicorn-sample > /dev/null || _fail echo 'Waiting for Gunicorn to start...' for _ in $(seq 1 10); do PROCESS_COUNT=$(docker exec gunicorn-sample sh -c 'pgrep -a gunicorn | wc -l') if [ $PROCESS_COUNT -ge 5 ]; then break fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:9200/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:9200/metrics \ | grep -E 'flask_http_request_total\{.*status="200".*} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi NUMBER_OF_FLASK_EXPORTER_INFO_METRICS=$(curl -s http://localhost:9200/metrics \ | grep 'flask_exporter_info{' \ | wc -l) if [ "$NUMBER_OF_FLASK_EXPORTER_INFO_METRICS" -lt "1" ] || [ "$NUMBER_OF_FLASK_EXPORTER_INFO_METRICS" -gt 2 ]; then echo "Unexpected number of info metrics: $NUMBER_OF_FLASK_EXPORTER_INFO_METRICS" _fail fi docker rm -f gunicorn-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/gunicorn-multiprocess-109/server.py000066400000000000000000000075211464362612600274770ustar00rootroot00000000000000import logging import os import threading import time import traceback import gunicorn import gunicorn.app.base from flask import Flask from prometheus_client import make_wsgi_app from werkzeug.middleware.dispatcher import DispatcherMiddleware from pebble import concurrent from prometheus_client.core import REGISTRY, InfoMetricFamily from concurrent.futures import TimeoutError from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics from prometheus_client import Counter class StandaloneApplication(gunicorn.app.base.BaseApplication): def __init__(self, app, options=None): self.options = options or {} self.application = app super().__init__() def load_config(self): config = {key: value for key, value in self.options.items() if key in self.cfg.settings and value is not None} for key, value in config.items(): self.cfg.set(key.lower(), value) def load(self): return self.application class CustomCollector: def collect(self): info = InfoMetricFamily('xxxx', 'xxxxxx') info.add_metric(labels='version', value={ 'version': 'xxxxx', 'loglevel': 'xxx', 'root': 'xxxx', 'workers': 'xxxx', 'ip': 'xxxxx', 'port': 'xxx', 'config_name': 'xxxx', 'mode': 'xx', 'debug': 'xxx', 'node': 'xxx', 'pod': 'xxx', 'pid': str(os.getpid()) } ) yield info thread_sum = Counter('thread_count', 'Total count of the thread application.', ['pod', 'node', 'mode']) def add_metric_thread(count=False): if count: thread_sum.labels(mode='mode', node='NODE', pod='POD').inc(count) else: thread_sum.labels(mode='mode', node='NODE', pod='POD') def when_ready(server): GunicornInternalPrometheusMetrics.start_http_server_when_ready(8080) def child_exit(server, worker): GunicornInternalPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) def thread_function(): @concurrent.process(timeout=300) def job(): open_threads = threading.active_count() add_metric_thread(open_threads) print(f'How thread Open .: {open_threads}') print(f'run_threaded - {threading.current_thread()}') time.sleep(20) while True: time.sleep(10) future = job() try: future.result() # blocks until results are ready except TimeoutError as error: logging.error(f'Job timeout of 5 minute {error.args[1]}') except Exception: logging.error(f' job - {traceback.format_exc()}') def init(): print('Thread - starting') thread = threading.Thread(target=thread_function, daemon=True) thread.start() add_metric_thread() def create_app(): app = Flask(__name__) metrics.init_app(app) # Add prometheus wsgi middleware to route /metrics requests app.wsgi_app = DispatcherMiddleware(app.wsgi_app, { '/metrics': make_wsgi_app(registry=REGISTRY) }) init() @app.route('/test') def main(): return 'Ok' return app REGISTRY.register(CustomCollector()) metrics = GunicornInternalPrometheusMetrics.for_app_factory( path='/metrics', static_labels={'node': 'xxx', 'pod': 'xx', 'version': 'xx'}, registry=REGISTRY ) if __name__ == '__main__': options = { 'bind': ['0.0.0.0:9200'], 'workers': 4, 'loglevel': 'debug' } std_app = StandaloneApplication(create_app(), options) std_app.run() prometheus_flask_exporter-0.23.1/examples/gunicorn/000077500000000000000000000000001464362612600225545ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/gunicorn/Dockerfile000066400000000000000000000006551464362612600245540ustar00rootroot00000000000000FROM python:3.11-alpine ADD examples/gunicorn/requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/gunicorn/server.py examples/gunicorn/config.py /var/flask/ WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp ENV METRICS_PORT 9200 CMD gunicorn -c config.py -w 4 -b 0.0.0.0:4000 server:app prometheus_flask_exporter-0.23.1/examples/gunicorn/README.md000066400000000000000000000013661464362612600240410ustar00rootroot00000000000000# Gunicorn example This [Gunicorn](https://gunicorn.org/) example has a sample app in [server.py](server.py) with multiprocessing enabled for metrics collection. In practise, this means the individual forks metrics will be combined, and the metrics endpoint from any of them should include the global stats. This example exposes the metrics on an individual endpoint, managed by [prometheus_client](https://github.com/prometheus/client_python#multiprocess-mode-gunicorn), started only on the master process, configured in the [config.py](config.py) file. ## Thanks Huge thanks for [@Miouge1](https://github.com/Miouge1) for bringing this to my attention in [prometheus_flask_exporter#15](https://github.com/rycus86/prometheus_flask_exporter/issues/15) ! prometheus_flask_exporter-0.23.1/examples/gunicorn/config.py000066400000000000000000000004711464362612600243750ustar00rootroot00000000000000import os from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics def when_ready(server): GunicornPrometheusMetrics.start_http_server_when_ready(int(os.getenv('METRICS_PORT'))) def child_exit(server, worker): GunicornPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) prometheus_flask_exporter-0.23.1/examples/gunicorn/requirements.txt000066400000000000000000000000731464362612600260400ustar00rootroot00000000000000flask gunicorn prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/gunicorn/run_tests.sh000077500000000000000000000024771464362612600251530ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs gunicorn-sample docker rm -f gunicorn-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t gunicorn-sample ../../. > /dev/null || _fail docker run -d --name gunicorn-sample -p 4000:4000 -p 9200:9200 gunicorn-sample > /dev/null || _fail echo 'Waiting for Gunicorn to start...' for _ in $(seq 1 10); do PROCESS_COUNT=$(docker exec gunicorn-sample sh -c 'pgrep -a gunicorn | wc -l') if [ $PROCESS_COUNT -ge 5 ]; then break fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:9200/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi for _ in $(seq 1 10); do curl -s http://localhost:4000/error > /dev/null done curl -s http://localhost:9200/metrics \ | grep 'flask_http_request_total{method="GET",status="500"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected error metrics are not found' _fail fi docker rm -f gunicorn-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/gunicorn/server.py000066400000000000000000000005431464362612600244360ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics app = Flask(__name__) metrics = GunicornPrometheusMetrics(app) @app.route('/test') def index(): return 'Hello world' @app.route('/error') def error(): raise Exception('Fail') if __name__ == '__main__': app.run(debug=False, port=5000) prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/000077500000000000000000000000001464362612600245035ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/Dockerfile000066400000000000000000000004031464362612600264720ustar00rootroot00000000000000FROM python:3.11-alpine RUN pip install flask pytest ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp ENV PYTHONPATH=/data ADD ./examples/pytest-app-factory /data WORKDIR /data prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/myapp/000077500000000000000000000000001464362612600256315ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/myapp/config.py000066400000000000000000000005031464362612600274460ustar00rootroot00000000000000from flask import Flask from .extensions import setup_extensions, metrics metrics.info('app_info', 'Sample app', version='0.1.2') class TestConfig: DEBUG = False TESTING = True def create_app(config): app = Flask('Example app') app.config.from_object(config) setup_extensions(app) return app prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/myapp/extensions.py000066400000000000000000000003561464362612600304060ustar00rootroot00000000000000from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics metrics = GunicornInternalPrometheusMetrics.for_app_factory(group_by="endpoint") def setup_extensions(app): metrics.init_app(app) return app prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/run_tests.sh000077500000000000000000000006341464362612600270730ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs pytest-sample docker rm -f pytest-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t pytest-sample ../../. > /dev/null || _fail docker run --rm --name pytest-sample pytest-sample py.test test if [ "$?" != "0" ]; then echo 'Failed to execute the tests' _fail fi docker rm -f pytest-sample > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/test/000077500000000000000000000000001464362612600254625ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/pytest-app-factory/test/test_example.py000066400000000000000000000027021464362612600305270ustar00rootroot00000000000000import pytest import prometheus_client from flask import Flask from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics from myapp.config import create_app from myapp import extensions as myapp_extensions @pytest.fixture() def app() -> Flask: app = create_app('myapp.config.TestConfig') prometheus_client.REGISTRY = prometheus_client.CollectorRegistry(auto_describe=True) myapp_extensions.metrics = GunicornInternalPrometheusMetrics.for_app_factory(group_by="endpoint") ctx = app.app_context() ctx.push() yield app ctx.pop() def test_http_200(app): @app.route('/test') def test(): return 'OK' client = app.test_client() client.get('/test') response = client.get('/metrics') assert response.status_code == 200 assert 'flask_http_request_total' in str(response.data) assert 'endpoint="test"' in str(response.data) def test_http_404(app): @app.route('/test') def test(): return 'OK' client = app.test_client() client.get('/not-found') response = client.get('/metrics') assert response.status_code == 200 assert 'flask_http_request_total' in str(response.data) assert 'status="404"' in str(response.data) def test_info(app): client = app.test_client() response = client.get('/metrics') assert response.status_code == 200 assert 'app_info' in str(response.data) assert 'version="0.1.2"' in str(response.data) prometheus_flask_exporter-0.23.1/examples/reload/000077500000000000000000000000001464362612600221765ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/reload/Dockerfile000066400000000000000000000004371464362612600241740ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache curl && pip install flask prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/reload/reload_example.py /var/flask/example.py WORKDIR /var/flask ENV DEBUG_METRICS 1 CMD python /var/flask/example.py prometheus_flask_exporter-0.23.1/examples/reload/reload_example.py000066400000000000000000000005261464362612600255340ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter import PrometheusMetrics app = Flask(__name__) metrics = PrometheusMetrics(app) @app.route('/test') def index(): return 'Hello world' @app.route('/ping') @metrics.do_not_track() def ping(): return 'pong' if __name__ == '__main__': app.run('0.0.0.0', 4000, debug=True) prometheus_flask_exporter-0.23.1/examples/reload/run_tests.sh000077500000000000000000000035471464362612600245740ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs reload-sample docker rm -f reload-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t reload-sample ../../. > /dev/null || _fail docker run -d --name reload-sample -p 4000:4000 reload-sample > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/ping > /dev/null; then break else sleep 0.2 fi done echo 'Starting the initial tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi echo 'Changing the server...' docker exec reload-sample sed -i "s#@app.route('/test')#@app.route('/changed')#" /var/flask/example.py docker exec reload-sample sed -i "s#@app.route('/ping')#@app.route('/ping2')#" /var/flask/example.py echo 'Waiting for the server to apply the changes...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/ping2 > /dev/null; then break else sleep 0.2 fi done echo 'Starting the changed tests...' for _ in $(seq 1 12); do curl -s http://localhost:4000/changed > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/changed",status="200"} 12.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f reload-sample > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/restful-return-none/000077500000000000000000000000001464362612600246665ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/restful-return-none/Dockerfile000066400000000000000000000004351464362612600266620ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache curl && pip install flask flask_restful prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/restful-return-none/server.py /var/flask/example.py WORKDIR /var/flask CMD python /var/flask/example.py prometheus_flask_exporter-0.23.1/examples/restful-return-none/README.md000066400000000000000000000002041464362612600261410ustar00rootroot00000000000000# Flask RESTful returning None This is a test case for [issue #62](https://github.com/rycus86/prometheus_flask_exporter/issues/62) prometheus_flask_exporter-0.23.1/examples/restful-return-none/run_tests.sh000077500000000000000000000023711464362612600272560ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs restful-return-none docker rm -f restful-return-none > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t restful-return-none ../../. > /dev/null || _fail docker run -d --name restful-return-none -p 4000:4000 restful-return-none > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/api/v1/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done for _ in $(seq 1 7); do curl -s 'http://localhost:4000/api/v1/test?fail=1' > /dev/null done curl -s http://localhost:4000/metrics \ | grep 'test_by_status_count{code="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi curl -s http://localhost:4000/metrics \ | grep 'test_by_status_count{code="400"} 7.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f restful-return-none > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/restful-return-none/server.py000066400000000000000000000013101464362612600265410ustar00rootroot00000000000000from flask import Flask, request from flask_restful import Resource, Api from prometheus_flask_exporter import RESTfulPrometheusMetrics app = Flask(__name__) restful_api = Api(app) metrics = RESTfulPrometheusMetrics.for_app_factory() class Test(Resource): status = 200 @staticmethod @metrics.summary('test_by_status', 'Test Request latencies by status', labels={ 'code': lambda r: r.status_code }) def get(): if 'fail' in request.args: return None, 400 else: return None, 200 restful_api.add_resource(Test, '/api/v1/test', endpoint='test') if __name__ == '__main__': metrics.init_app(app, restful_api) app.run('0.0.0.0', 4000) prometheus_flask_exporter-0.23.1/examples/restful-with-blueprints/000077500000000000000000000000001464362612600255525ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/restful-with-blueprints/Dockerfile000066400000000000000000000004411464362612600275430ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache curl && pip install flask flask_restful prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/restful-with-blueprints/server.py /var/flask/example.py WORKDIR /var/flask CMD python /var/flask/example.py prometheus_flask_exporter-0.23.1/examples/restful-with-blueprints/README.md000066400000000000000000000002051464362612600270260ustar00rootroot00000000000000# Blueprints with Flask RESTful This is a test case for [issue #29](https://github.com/rycus86/prometheus_flask_exporter/issues/29) prometheus_flask_exporter-0.23.1/examples/restful-with-blueprints/run_tests.sh000077500000000000000000000024211464362612600301360ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs restful-with-blueprints docker rm -f restful-with-blueprints > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t restful-with-blueprints ../../. > /dev/null || _fail docker run -d --name restful-with-blueprints -p 4000:4000 restful-with-blueprints > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/api/v1/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done for _ in $(seq 1 7); do curl -s 'http://localhost:4000/api/v1/test?fail=1' > /dev/null done curl -s http://localhost:4000/metrics \ | grep 'test_by_status_count{code="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi curl -s http://localhost:4000/metrics \ | grep 'test_by_status_count{code="400"} 7.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f restful-with-blueprints > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/restful-with-blueprints/server.py000066400000000000000000000013611464362612600274330ustar00rootroot00000000000000from flask import Flask, Blueprint, request from flask_restful import Resource, Api from prometheus_flask_exporter import PrometheusMetrics app = Flask(__name__) blueprint = Blueprint('api_v1', __name__, url_prefix='/api/v1') restful_api = Api(blueprint) metrics = PrometheusMetrics(app) class Test(Resource): status = 200 @staticmethod @metrics.summary('test_by_status', 'Test Request latencies by status', labels={ 'code': lambda r: r.status_code }) def get(): if 'fail' in request.args: return 'Not OK', 400 else: return 'OK' restful_api.add_resource(Test, '/test', endpoint='test') app.register_blueprint(blueprint) if __name__ == '__main__': app.run('0.0.0.0', 4000) prometheus_flask_exporter-0.23.1/examples/restplus-default-metrics/000077500000000000000000000000001464362612600256775ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/restplus-default-metrics/Dockerfile000066400000000000000000000006141464362612600276720ustar00rootroot00000000000000FROM python:3.11-alpine # werkzeug needs to be pinned due to https://github.com/python-restx/flask-restx/issues/460 RUN apk add --no-cache curl && pip install flask flask-restx prometheus_client werkzeug==2.0.* ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/restplus-default-metrics/server.py /var/flask/example.py WORKDIR /var/flask CMD python /var/flask/example.py prometheus_flask_exporter-0.23.1/examples/restplus-default-metrics/README.md000066400000000000000000000002131464362612600271520ustar00rootroot00000000000000# Flask-RESTPlus with default metrics This is a test case for [issue #34](https://github.com/rycus86/prometheus_flask_exporter/issues/34) prometheus_flask_exporter-0.23.1/examples/restplus-default-metrics/run_tests.sh000077500000000000000000000023051464362612600302640ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs restplus-default-metrics docker rm -f restplus-default-metrics > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t restplus-default-metrics ../../. > /dev/null || _fail docker run -d --name restplus-default-metrics -p 4000:4000 restplus-default-metrics > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'by_path_counter_total{path="/test"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi curl -s http://localhost:4000/metrics \ | grep 'outside_context_total{endpoint="example_endpoint"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f restplus-default-metrics > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/restplus-default-metrics/server.py000066400000000000000000000020141464362612600275540ustar00rootroot00000000000000from flask import Flask, request from flask_restx import Resource, Api from prometheus_flask_exporter import PrometheusMetrics metrics = PrometheusMetrics.for_app_factory() def create_app(): app = Flask(__name__) metrics.init_app(app) with app.app_context(): setup_api(app) metrics.register_default( metrics.counter( 'by_path_counter', 'Request count by request paths', labels={'path': lambda: request.path} ) ) metrics.register_default( metrics.counter( 'outside_context', 'Example default registration outside the app context', labels={'endpoint': lambda: request.endpoint} ), app=app ) return app def setup_api(app): api = Api() api.init_app(app) @api.route('/test') class ExampleEndpoint(Resource): def get(self): return {'hello': 'world'} if __name__ == '__main__': app = create_app() app.run('0.0.0.0', 4000) prometheus_flask_exporter-0.23.1/examples/sample-signals/000077500000000000000000000000001464362612600236475ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/sample-signals/README.md000066400000000000000000000057661464362612600251440ustar00rootroot00000000000000# Example dashboard ![Example dashboard](dashboard.png) With this example, you can bring up a simple Flask application that has 4 endpoints, plus one that always returns an error response. The 4 main endpoints simply return `ok` after some random delay. The example Compose project also includes a random load generator, a Prometheus server, and a Grafana instance with preconfigured dashboards, feeding data from Prometheus, which is also configured to scrape metrics from the example application automatically. All you need to do is: ```shell docker-compose up -d ``` Then open in your browser to see the dashboard. You can edit each panel to check what metric it uses, but here is a quick rundown of what's going on in there. ## Requests per second Number of successful Flask requests per second. Shown per path. ```json rate( flask_http_request_duration_seconds_count{status="200"}[30s] ) ``` ## Errors per second Number of failed (non HTTP 200) responses per second. ```json sum( rate( flask_http_request_duration_seconds_count{status!="200"}[30s] ) ) ``` ## Total requests per minute The total number of requests measured over one minute intervals. Shown per HTTP response status code. ```json increase( flask_http_request_total[1m] ) ``` ## Average response time [30s] The average response time measured over 30 seconds intervals for successful requests. Shown per path. ```json rate( flask_http_request_duration_seconds_sum{status="200"}[30s] ) / rate( flask_http_request_duration_seconds_count{status="200"}[30s] ) ``` ## Requests under 250ms The percentage of successful requests finished within 1/4 second. Shown per path. ```json increase( flask_http_request_duration_seconds_bucket{status="200",le="0.25"}[30s] ) / ignoring (le) increase( flask_http_request_duration_seconds_count{status="200"}[30s] ) ``` ## Request duration [s] - p50 The 50th percentile of request durations over the last 30 seconds. In other words, half of the requests finish in (min/max/avg) these times. Shown per path. ```json histogram_quantile( 0.5, rate( flask_http_request_duration_seconds_bucket{status="200"}[30s] ) ) ``` ## Request duration [s] - p90 The 90th percentile of request durations over the last 30 seconds. In other words, 90 percent of the requests finish in (min/max/avg) these times. Shown per path. ```json histogram_quantile( 0.9, rate( flask_http_request_duration_seconds_bucket{status="200"}[30s] ) ) ``` ## Memory usage The memory usage of the Flask app. Based on data from the underlying Prometheus client library, not Flask specific. ```json process_resident_memory_bytes{job="example"} ``` ## CPU usage The CPU usage of the Flask app as measured over 30 seconds intervals. Based on data from the underlying Prometheus client library, not Flask specific. ```json rate( process_cpu_seconds_total{job="example"}[30s] ) ``` ## Cleaning up Don't forget to shut the demo down, once finished: ```shell docker-compose down -v --rmi all ``` prometheus_flask_exporter-0.23.1/examples/sample-signals/app/000077500000000000000000000000001464362612600244275ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/sample-signals/app/Dockerfile000066400000000000000000000002541464362612600264220ustar00rootroot00000000000000FROM python:3.11-alpine ADD requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD app.py /var/server/app.py CMD python /var/server/app.py prometheus_flask_exporter-0.23.1/examples/sample-signals/app/app.py000066400000000000000000000013201464362612600255550ustar00rootroot00000000000000import time import random from flask import Flask from prometheus_flask_exporter import PrometheusMetrics app = Flask(__name__) PrometheusMetrics(app) endpoints = ("one", "two", "three", "four", "five", "error") @app.route("/one") def first_route(): time.sleep(random.random() * 0.2) return "ok" @app.route("/two") def the_second(): time.sleep(random.random() * 0.4) return "ok" @app.route("/three") def test_3rd(): time.sleep(random.random() * 0.6) return "ok" @app.route("/four") def fourth_one(): time.sleep(random.random() * 0.8) return "ok" @app.route("/error") def oops(): return ":(", 500 if __name__ == "__main__": app.run("0.0.0.0", 5000, threaded=True) prometheus_flask_exporter-0.23.1/examples/sample-signals/app/requirements.txt000066400000000000000000000000401464362612600277050ustar00rootroot00000000000000flask prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/sample-signals/dashboard.png000066400000000000000000004231221464362612600263100ustar00rootroot00000000000000‰PNG  IHDRŠL-×UbKGDÿÿÿ ½§“ pHYs  šœtIMEâ %¿üfê IDATxÚìÝwxTUþÇñ÷’NB BïR¤ " ÄÖu¬XÕ]{ï]ÑuÝŸ ×QWÅ‚¨té½…ÞPÒéÉ”ûû#dÈ6$ø¼žgždfî=sιwι÷;çÜkLÿmº‰ˆˆˆˆˆˆˆˆˆˆŒ&ÍZ˜†a`FÁ‹ODDDDDDDDDD¼X<ÿiœ¢ˆˆˆˆˆˆˆˆˆ”Ã&˜æIñDEEDDDDDDDD¤8 fÑð¡‰"""""""""R:‹WñøSqE)AÁ”gLXNÄM…EDDDDDDDD¤LwyÖ Eñ犊'ŠˆˆˆˆˆˆˆˆHy,PZ0Q!Fñf3M…EDDDDDDDD¤|%ŒPThQDDDDDDDDDJfSˆˆˆˆˆHu Æb±Ô˜ü¸Ýn2³³jt>KËã™ÜgC½Uu>kâvõ ÕɈiÜÄ4 †aœø+"""""rºB‚‚q8òq»Ý5&O‹»ÝÏ뤬¦å³¤<žémp6Ô[Uç³&nQßP­yЮ""""""ÕqòSÓ&n·»Øh“š–Ï’òx¦·ÁÙPoUÏš¸]DD}CµöëÚUDDDDDDDDDÄWº†¢ˆˆˆˆˆT Ã0”Ïs¸lÚ¾ÚçDÔ‡?l7u.zggãøsÝñYDDDDDÎy¦©<ž«õVÕù4uÎ,¢¾áüÌ£­*?ºaÆ\3jd™ËüôóT*´¬ˆˆˆˆˆH¥)çl½Uu>NQßp¾æ±J§<½óêÔ©Sæ2?Ž'ž|šÆßO```™ËÞuç¼øÒËÚ«EDDDDÎB•=]Ìf³qã7°lÙ2ö쉫±ù<—·ÁÙXošò,"úžVB\•‰—L (7˜áCz盇þþ ƒbÄWUIúo¿ùáááÜu÷=g,íªÌÃÙRW""""ç»ÝÎEu':ºV«…#Gް`ÁÂbË1pàüýýÙºu[È{ýúõhÞ¼9¦i²bÅJÝÁ×GX,²³³15•Øç:kÛ¶ M›6%4´¦i’‘‘A\Ü^¶mÛNnn®*I¤¨U+„† c¼¾§û÷ ++ë¬-ÓyqS–1£oá/7ÞPêûS¦üÀ'}©=\DDDD¤†èÝûbBCCY±b99¹$''Ó¬Y3êÕ‹"88›Í†ÍfÀb±Ð·oüýýY¿~ÃÉopp0‹…ŒŒ Z·nMÛ¶mؾ}©©©Ô¯_ÇhÞ$ ÀŸÎ;Ó²e ‚‚ ›äçç³gOk×®#33S•TŠ&MšÐ¿?üýýOªÓ"##éÔ©# ,$>>^•%r†øûûÓ»÷Å´hÑ‹ÅâõžÛífçÎ],_¾‚ ¢¹è¢|÷Ý”³¦lçÕ]ž¿úúRRRн·W{¹ˆˆˆˆH«Èt±fÍš2mÚo¤¦¦Ò A47ß|ÁÁÁe®sÑE=°Ùl¬Y³¶Úò ÆW\ŽÍfeýú 4lØÀó^—.hÑ¢9›7oaéÒegÅ6°ÙltèО͛7ãp8«¤ÞÂÃk3lØ0jÕ ñzÝÏ϶mÛЬYSæÌùƒC‡UyœJyÏÔw qãF :Ã0Ø»w7n$))€¨¨H:uêD“&6l³gÏUPQä |OCB‚1baa¡@Á%G¦ár¹ˆˆ¨ƒ¿¿?mÚ´&&¦!X­Ö³jZõyP\²t)ûö©!©éüüüp:„„„0lØPìv{Ík·n] *¸„ÓEõðz¯E‹æžÿÛ·¿€íÛw”8È¡F$Úl >”èèhbbb˜9sNgåÙìvÆ ¥V­23³X¾|ûöíÃívÓ A4½zõ$""‚!CñÓO?“™YuÓ­V«WygÍšuF‚о  ÿK1 ƒ+V²nÝz, v»-Z°mÛ6Ò­[WzôèÎ¥—öeÊ”ϹéÏ‹A÷îÝÙ¹sG=çÛÄ`»›çÑ«±›Ÿ6Ûxw±¥¥Ý9ÚÉç׿ñðtæï9¯ÂDUÆ0 DXX(.—‹5kÖ²qc¬W[Ú®][z÷¾¸ÜËjl_Q#nƒ]‘<œJ~ ×1ÍR×oßþÞzó þøcï¾÷OÏëo¾ù:mZ·æ¾qã=¿ŒuëÚ•n¸Ž–-[’——ÏÖ­[øü¿_pгÞ{テÛífÚ´_¹ñÆiذ©©©üøÓÏLŸ>ƒk¯½†ËG\Fhh( |1i2kÖ¬ñ¬ÿö[oâr9™;w×_-õêÕ#))™)?üÀìÙsʬ—¨¨Hn¿í6ºví‚Õjeóæ-|öŸÏÙ¿™ÕÔ¥Kgî¸ý67nLjj*Ó~ý­0q¯ô})?À€ý¹fÔH6lȱcé¬^½š¯¾þ†cÇŽÍ8:väöÛn¥Y³¦$%%óË´iüöÛï'~}úpÓM7MRR2?üø#³fÍöZª~ýúÜ:f4;_ˆ¿¿?»wïá›ÿýuëÖ{ôùP†·ßz“   ~ÿýwþú×›HJJâ=R¡º‘Ó9!+8á*L,ᑟŸ‡aXˆ‰ièyoùòlذ±Ø´²*9Ç(bùòÄÄ4,6õôd›7o!99ùÔF TÓ1fÑ`"@DD‚‚IOϨÔ<µoßžZµj‘““ÃÏ?O%''ÇS/ ™:õF޼šˆˆºté¢E‹«ì|3((ˆˆˆºDG×gذa¾ÏÀ±Û¶mð÷÷gïÞ½¬_¿«Õz¼ôëׇíÛ·cµZY»vQQQ4iÒ˜¶mÛx]ÀÏÏ[oÍÆ±¬X±²Òò6jÔHrrr˜9sV±÷:vì@¯^=½^‹ÝÄòå+Né³üüüiÙ²%©©)§P,+Ï•©–ŸÉŸwg{žç8LâÓ¬L^ocúöò0¹¥‹“þÍ\¼:ßÎúCÖ* ÉœM×.½´©ƒñ;hfr8Óà‹5v¦n­äžN£>Z¶lIdd$óçÿÉîÝ»±X,žïkA–Wñþª´Ažþ Îì­¦Í*Z¶¤uK[Óæ-Ìœ5‹áÆñ˯¿±k×.úöéCÇøï“8x<˜Øûâ^<óôS¬[¿žøµBB¸á†ëyõ•W7~½Ä×24lØ€;«VsàÀÌ Ô•ˆˆˆˆx«h Í0 """ŽŸxå³xñvîÜåy?001cnÁívóçŸ Ù¹s—×ÉÚ©žcT$ŸAAA„‡×öZgÅŠ•lÞ¼…àà`ìï9©  N:¤§Ãí6O+U± l¶‚QƒÑÑõ=u>cÆ,222K\ïtê­Y³f¬]»Ž¼<ï“jÃ00MX±b#F §yóf,Y²ô”ƒåå333“éÓg0bÄpüüüˆŽ®ÏðáØ5kv¹AEó |7š6mz<·Ùk/L£ð¯Õj%6vMš4¦iÓ&l(r~Uôó*wŠ¥‰iš%¦DVVK–,õ¼–žžqÊŸŸ——ÇäÉ_UÂ4ÑÒó\¹í_Áß?vLÝl#Ànrm7¯ ÉçH¦•µËn»šÕv“˜eðóV? èÔü&UÙéV•&µÝ¼}Y ÷Zy{¡A-Ý<7(Ÿ½i6¶UâÞ|êßÓ–-[°ÿ~âââŠõMv»aµžhûG…ëÿLƪt,kvNA>ܽ¹"éU¥Ï?ÿ‚ž={rÏØ»xúÙç¸óÎÛÙ»w?ý<ճ̥—öcûŽ<÷ü‹¸\.’SRxæ©'éÒ¥ ‹/ñ,ët:yä±ÇHK+‘·rå*&~ú1-š·àαw{Fêíܵ‹×_}…‹z^įž‘nàp8yè¡G8š–ÀÌY³™øÉGŒ¹åfO@ñdwÜv+~~~ŒàAŽ-XoÅÊ•|òчŒyŸýç¿%®wÛ­£1‡}ÌsíÙ³çðÉÇz-çkù‡ÂÁƒyý·¼:ê[n¹™Ö­[±eËVO'÷ìs/°'.€ÅK–òõäIôêÕË+ h=ñ¤gáô3ù䣹ù¯7yŠcﺓ   g¹Y³çðþ{¸Ü8V­ZM¾ÃQ¡mh±Xxô±'<ù«H]‰ˆˆˆÈé+<ÙZ»v»wïñ:)3M“¬¬l.\È §?Òãüå/7x}î žQ’éÌŸ¿€n¸(˜þÜ¢Esbc7UꨰJ91,%˜˜˜˜X%†ÂkŠ9r„!CѤI¯÷N'“' Œ¦+ FU•¤¤$¦OŸé *Ö¯_ŸaÆúT¬n¡¡µHMM-wÙÂ%µjÕªÐg4jCÝ #==ƒ•+W²ÿZµjIçÎ "5õ(Ë—¯ 11‘1cFãçW0Blôè›=Û¯PHH0G¦y]†¬`×:±EFFrõÕW»‰Fb !.n/ûöí£gÏ‹ >~? üIPP7Ýt#Ë–-'99™+¯¼‚µk×ѬYS‚ƒƒÙ»w ."::šË/¿ŒY³f³ÿ¸ù濲lÙrºuëV,ϵk‡Ñ»wo"#ë’™™ÉªU«‰ß_fÙ+"!ÝÂ’ý~lItòûmytŽv²ö •fá.žè—OûzNeXø`™ ÷ÚùàŠz7)8w];>›û~ Ûa0éº~Úl£_Sï/õcú{©i\ÑÆÁ]Ýó‰ v³+ÕÊ„Åþl<|¢M½(ÆÅø^"ƒÝÌÙeã•þ^Û(5ý —åÐ6ÒÍå_Lß½½k>ãzæ1bR0!þf‰ët¨ç*V†Q8hjzÒùÇ%¹ÜÔÉÁÐÿ“–[ÐÖØLþ»ÆÆwí¤æZÙ™ââúŽ.:Öw±á°­ÜrV‡ùóçcµÚÈÍÍ-± u8œ|ýõרlÞ#ðÏ&UÚãþôóÏå.óãñe~ôaY_Ò+ËÇþ3~ÿÕëñòK/œxeeñé§éС=¯¼ü"Q‘‘üëƒ{Íqý·øÇCxQû7.Ñõë{7Þ©©ž`"À„rrrؾc»×´ß}ûöPïø/—…ŽKóÒÓÓùcÞ|êׯOýzõŠ•ÏÏn§wï‹Y¶|…'˜pðà!öîÛG‡öJ¬—€€:´oÏŠå+<²Âú8tè°×²¾–?;'›ÚµkÓºu+ÏkS~ø‘«G^ã &vÜEƒu©©©dffz~Å-”œ”ä5977—yóçÓ¤IcêÔ©C@@=ztgÙòåÅ–›ú˯„‡×¦C‡Þ†‰‰‰^ù«H]‰ˆˆˆÈé+œj¿¿ØI™Ãá`òä¯8|øÈUsr3;;«ÈkF‰×­«i#€ª;˜'¦VZ­V,XÈ7ß|ëõøöÛïOi´é©2 ÃT,<©/ *Úí5ûšrmÚ´æ®»îð®KÚ×*²ƒƒƒ—xSŽï@Áe¼V®\Å®]»iÕª%={^IJeËÙ´i3-Z4÷ŒÒ,Ü— ÷§Æ±zõZvìØAëÖ­ˆ‰ièyï䮦iËsÁ”ÿaØl6æÌ™Ë‘#‰ 4ààà2Ë^Ñ}Í0 ípe;7q©6“_™C ÝÍ#ÓýØxÈà­áyÔqóü\‹÷Z8’ W|áÇúƒO]õoædúv£Ü4šÖvó \æí¶pç~ä9à½9XŒsa´›WX™¾ÝʨöNz5ò¦—•þÌê×2iY§à<»OkZÉÌ7J]§¤2Ì:žNëˆÂt\¬:`ñ¶'[øp…?GóløÙLF´uãt™¬?hñ©œÕ!//ŸììlÜnw©Ëäç;ÈÎÎö<*ûZµUÞwTeâ+V¬dçÎ]tìPr +vÓ&ϯ* üÉÆ±>-{ª&}9¹Ø”ß䓞ÿ¹pƒ ¤G÷îü>}[·m+–΀ý1|8 6¤ví° 5Ò.·çI¿p¹\;˜ÕVþæHHH ¢n‡ñz/¢n]üýý2xC*¶î‘R~9‰ˆ¨ƒÅbñLë./åŸøÙç¼ðÜ3¼ÿÞ»ÄÇdzvÝz.ZÌÖ­[ËMßívcóáà¡°üQ‘‘dggãçççùÕ¬¨½Ç¶ D³vݺÓÚ†­«Òê/0 à¤2›Ìœ5 ‘sYŃSûöÅÓ®][òòJåa·Û¹þúk™1céééÕžÏÕ«×P§Nš7ov< Ñ„õžÑtíÚµó,›••EBÂÁƒ£gr\rIoO0`îÜy$%%UxÄgEÊ”’’Btt45"99¥Äé- ¦ æææ’]iuVZ:†aœœÌܹó1b¸'¨Ø§Ï%,X°°Æ|7ÒÓ3ˆŒô'2².‡f×®Ý8à9—*šVݺu=ë”6͹¤@ý´i¿qìØ1œN';vì Q£BCÃ]ic~œÁ-]Ô¯OͶ³á°kº¸²]6š;ù6ÖŸ\§—Ëàp¶¿WðõÝÅ6¦ï(ÈÇà–ÎRÓX{°`jxŸA¶ÓÆÃ3lÔöw{ÕßÄ•6ìõcÓ7tÊ¡Y¸ÉŠ'ÊwIÓÒÓŸºÅG>}›¹Hʶұ¾›7ÿôã’¦¥—+öˆµXŽdZxüR}›:ÉvZhnòÕ:ûIõl`µÂßzåqkׂþµÔΦD;mêºJ/'Fµ}O;thOPPç¹Óédݺõ%^º!&¦! 4àØ±clß¾CE€þý/åÚQ£J}ÿºk¯áÇŸfÁ‚?}Zö«o¾9­©Ë–//÷.φax†ƒ‡–0,üöÛnå†ë¯ã—i¿òõ7ßz4¨ÈH¯‘ŽUÉr<Ðæ,aØ}áŪ.ZÄÒã× ,*/¯äá³…;tY‘óŠ–ÿþýÜ}ï8ztïN×®]èÕ«'#¯¾Šùóðö„w+åb¯–ã_f‹ÕâiEF*œ&Sø™§³ +RW¥éÖµ ƒôzmË–­ (Šˆˆˆ”àÀ$''c·û‘Ÿï(q™ÎÔ ¿ÂÄ¿üåFBCkàÏÈ‘W¿Ÿ   ¯›ÆÄÆnbãÆX,K¥¸eËVš4iâ™úÙ¥Kg’’’ªt´ÌλˆŽŽ¦cÇìß_°‹ ¦GnìÚµ»ÚêÂn·Ó¥KgÏóüü|¯V5ÁÞ½{‰Œ¬KûöpèÐaÜn7ÙÙÙ¥5 ×ñ•Ãá :º>ö'$$Ä3RÔ0 RRRøóÏ…\xa'.¸ ééé¬Y³–={âÊM·0ph$Ó˜˜†'ŸëY ¯à_a ³üÓHãørf…@û]A誫®ôz=((ˆÔÔÔS.{QsvZ˜ºÅÆe­]\ÖÆÉô6 ÃB½à‚öíókó|Oy¢‚Ë>÷t¸­žà½ãË–”Æöd?^úÃÆ˜®.®ïè$á|¸ÂŸ»ŠÔ‘QÐ6¹Í‚ô¬ï /+ý\—…EqVú6qp(£`ý?vÛ¸¼­«ŒrY‹•áhž•5-ômê"ËáÀå6™Wʧ¿ÝheÁ?ú5uó·‹ìM³ñgœ­ürVƒž=/òzžššÊúõŠÅBl6}ú\Bpp°×uNÏUP¼løðr—¹vÔ(,ø³Ì`b¡kFެòk\yÅå´mӆŋ—зozΟïõ™W^q9K—-ããO>=d:àRENÉML*>Ú0%%·ÛÛmòçÂE>§™’’ŠËå¢þIÓ}K«ŸòÊoµZ '++‹å+V°|Å Œ î{#¯¾Š9üQì®Ë§TÇïrÚÛ°"uUš ïþƒwû{öùt¶ """R„ÓéÄf³aóç/(qêpAàÁr<rf¦ŠY,:thï¹®\&§è¥ uéÒ™½{÷UéµOERR3fÌä²Ë†ynJ2tèfÏžSeAÅ;vÒ¶m¢¢¢1b8›6mfÿþ¸ÝnêׯDžv"ðøõøœ43«ÊNŽm6†â­ép8˜9s‰‰I5*¼}û:uêH“&MèÔ©#7Æ–¸Ü…v¢qãFäååUhÔSƒ èÕ«' .bûö4mÚ„!C×bLOÏàûï§F¯^ѯ_?âã÷—¹¯øùùѹó…ìÙWäNçÕS§nwA@Ë0Êq›™Y0rpæÌY^3$Nç)—ýd‡3-,?àÇÎ'CZ¹¸¯§ƒ§çØ9\ðÑüýW;;RNä5Çi©@Ú¥§Ñ Ô$þ˜Q_Ùiææ}¼0(Eûl•’>À¬6Þ¾,Ì|+÷[9–oãp¦«Ôu×.ùsfï´ñTÿ|l“U¬¤ç{ç±GC'ƒ[:ù`™?XÙpØäÆN.iâdgеÔrf;,gä;›ŸŸÏôéÓ‹ L²X,ôë×—àà`\.[¶l9«úi ¦IU=‚8qoå2¦éÓrAA§–OÔˆ2—«Á­cF³jõj^{ý víÞÍý÷Ý[0=Õ4±‹?¯õŠþ‚Uü3KËKI¯}nF­"õHÿK/eÏž8ަ-Roéeef±16–Kz_LãF¼Òpé¥×],¡Üy¹¹lÜËŽzR'<ÜózHp0 4ð¤ïkù˜ôßÿðÀßî÷,cºÝlÜXm­U«XÞˬ#LêFF ¿àïÏÀýÙ·o)ÉÉäæä°fÍZ.és‰W9í6W^q9ÇŽcÆ ܆Åóçk]•õ0ÝnÞ™ð.sçþÁ¦M›yæÙçÈÉΦ*¿‹z衇z衇güÁ‰©‰¾< ¦6Ö®]Ó,±êõ¨];Œ=º“žžNNNN…Ò÷º›éiäÓ0 ¯`ÂÉ#Åyyyǃ.Ï´ËÓÊc%o‹ÅBrr23gÎò\?0:ºðúö*©7‹ÅÂìÙsHLLòŒ ¼êª+9ò*zõêé &BÁ!6lP¥Û×n·3l˜w0qÖ¬Ù$%%{F”V÷v)푟ŸÏ‚ 1M“=º3xð 4ˆÆÏÏ4hÀ!ƒèÞ½¦i²`ÁBòóóKÌHH111žGdd]ÏHÕÐÐZ4mÚ„Ž;x‚‘‘u¹òÊ˹ð ñ÷÷Çétaš'¦Í:ÂÃÃiÚ´‰×g9bbbèÛ÷5jD‡í©_¿žgú±zÅûùÉïü_ô›Vø:ÅÒIOÏÀårѶm7nLïÞŸXë¤<'$$33“=ºELL Ç'44¬Ü²ûº?FA]ÍóãÇM6†¶tÒ*ÂÅòývgÀ}»¸ ž… Þ¿ÂAÃ0‹'{rûqr]••Fûz.>»&—Ñ]Ý„øÛÈuÜlÞRfýý¼òò¸t¿YƒKš¸˜½ËæÓ:%}Îü=vÜ&´‹2™»ËZ¬³0ª]>OôË¥Wc÷÷räg°áPÙå¬Î¾áä€úСÃhÖ¬`J}hh(-[¶àꫯ¤Y³¦,_¾‚œœÜ3Ó7œâÃÆyäâ^=Kü¥ðÀ¶nÝʸûîÅn·óé§1M“>ú„ ï¼Åm·Žá£?Áív³dÉRÀã?ÊλhÛ¦g(k`%ÞÑ 8(ˆ Þæ×i¿0bÄe„‡×æƒÿ»Ôu>ùd"ï¼ý&Þy‹Ÿ§þÂáÇéÔ©ÆaâÄϼîX]Ô“&ñö[oòî»ï0cú #FŒ 88ˆœœìã¿ìøVþÌÌLfÌœÉå#FŸŸÏƱ„֪ŨQ#9vìX‰CÚËýUÉåâ7^ç·ß'7'‡Aƒá5ÊðÓ‰yïÝ Lxç-¦þ2ì¬lD“&yý7=z§» }©«ò˜¦É„wßóŽ/""""',^¼„K/íËÈ‘W•zs·ÛMJJ sæüqFóºuë6ϱäš5k8p-Z4/i3{ééôìÙƒU«Ö••]#ë»à¦$AÅáÇ¿Óq½*©˜Ÿï`Ú´_iݺ-[¶$"¢6›ôôtöîÝÇöí;8£M8á IDATpQQ‘ <ˆ¹sÿààÁC•ž«ÕÊСƒ=³ F&ήқҜ®0{ö\ú÷ïG“&iÒ¤q±eòòò˜?ÿOJ-GóæÍ<×ÿ„‚köÏš5‡Í›·Ð®];Z¶tOýúõ©U«{öìaùò´oßžîÝ»‘––Æœ9s=ûÇÆ±ôë×—‹/îUìrc³gÏæâ‹{Ñ¿?±±›Ø°aU=R1//ïø»R·n]víÚåõ~Ñ<ûí÷üþû z÷îE¿~}q¹\ìÚµ‹£G’’’RfÙOŤu~\ÛÁÉÝ=òylV÷M àÑ~ù¼00‡|ÌØacwŠïõ“ã´–šÆ¶$;ï9ùë…ùŒï™ÇÞ4ƒG¦û‘㬜ôœ¦…{¬ oíäϽ~å®Ó&²äsácù6V'XéÞÐÅü8{±÷7'Úxj¶?c{8x÷2'‰™†ç:ŒÀi—³²$''säH"íÛ_@TT¤g–bQ.—‹+V²uë¶ _·öŒ÷ 62=ÑÍ“"þ§ëƒýÓ§åþöÀß+´lEÝ:f47ýåÆRߟ9k6«W¯æ™§Ÿâ‡â³ÿ|îyï‰Ç¥_ß¾<ôÈ£lÛ¶€€n»u ýúö!88˜Í›·ðÅ—_2áí·X·~=Ï=ÿ"ÿ|oÁÁÁŒ½û^¯ÏúaÊwlÞ¼…ç_xÑóZhh(ßû Sù…?™À„·ß$<<œ)?þÄu×\CTT$‰‰‰|ûÝæÌëY÷á‡þÎÁƒ>â Ïk5b̘Ñ\xa'üýüØÏÏ?OeÞüeÖSç /äÎ;o§IãÆMKã—_¦Ñ®];Z4oÆwÝ àsù­V+#¯¾êø”h222Ø´i_NþÊsæÂ2¦]èûo¿aë¶íž:šðö›8].f̘ÅM7ÝHƒèh’’’øîû)Ìš=Çk݆ pûm·Ñ¹ó…X ƒ={âøúÿóšbíkJËŸ¯u%""""'„‡ÕÆåªØ‰·Ëå*÷Ç×ÂÑn§zþbµÚ8z,í´òYÜ„‚‘G;v¤Gn¸\.¾ûnŠçƧšÏ“óX•ÛÀ4M"#ëz‚Їƒ©S§‘‘‘Q%õVXwEïÔ[pjjÁb±`µZ1b8‘‘‘8N~ûmz…nÖéK>CBB5êjìv{…‚‰Õ¹]JÛV~~~´mÛ†¦M›z¦Ý§§gǶmÛq8¥–£¤@˜aX­VÜnî‚a]X,n·‰ÕZ0R¬à2[n϶²X¬žÏ0M×ñëÚÛJ¸éhÑï´Åb)@)\¿ð³ óYtÙ¢Ï ÿ7 £ÌõŠæ¹ðyá{'çÙ4MÏ>yr>Ë*»/N.Kák…õ^ÖgÖ]a½–TW…¯W4ÿ¾Ô»/闔ϲÖ)­ ¯ Ê",Àäo¿‡”ºÿ»Ý.Ï$ÇÊÜN•ÑÆµlÙ‚¸¸8u騱= 6ô bÊÊÊæÀlذ‘ôôôS &VV¤€â9¦¬`–ˆˆˆˆÈÙ&<¬¶çZf5‰Åb-vÒxºù,z2[RPåtóXÕÛ  ¨É€—²`ÁBÏÝt«ºÞJ?i¶0bÄêÖà?æ_éÛ·nÝ èÏ‚ }™XÝÛ¥¬ýÍ4ÝžÀJÑ€¬ÈÙ&*ØMß&º$—çù3{wÀYÙ7­ ÛÔ€jaŸPH>Ä*« :U6í®""""""•y"zvs ¦?'ñ¿ÿ}WâÈ¡êær¹ùõ×߯MjêÑ*ÉORRr)ï©ìo×Á9û]XßÉçðÛ6³wÙ«ëÞ=Uô½<Ѧž|éŽÊø±éLS@QDDDDDD¼†QãNx«*˜XSË+r>š½ËÎŒí!X,‹URƒUi‹™ššJ:uÊ\&''Çó·¼bTäZ"""""R³œ-#¿Î¶j5¥lU]o••~Mܾçò>'RÑïBMá­ï©7¦Y0„´ n4;ñÓϸöÚkÊ\æÇÓä_ïàó²çƒ‡yL{§ˆˆˆˆHu8Î1jbÏ–s³ªÌçyr~*¢¾AyS@QDDDDDDDDD|¦€¢ˆˆˆˆˆˆˆˆˆøÌVÒ-Y @·j‘“i„¢ˆˆˆˆˆˆˆˆˆøÌ†Yd,¢†&ŠˆˆˆˆˆˆˆˆH4BQDDDDDDDDD|¦€¢ˆˆˆˆˆˆˆˆˆøÌv:+»\NL· ÓÔ©9}”¯Nyʳéváï¨PDDD°Z­øûbº]:žµ}jûDDä,ì£|eƒS»±³išêEDD¤ØA‹iš>[èxBDÔö‰ˆˆÔœ>ÊW6Jºf‡èR"""rªtM0QÛ'""rÎöQºË³ˆˆˆˆˆˆˆˆˆøLEñ™Š"""""""""â3EDDDDDDDDÄg (ŠˆˆˆˆˆˆˆˆˆÏ¬AÁ!/††QpwgŠü-ƒéváï ‘ó§Á´Z™>ãw¬+6lP…ˆ”"??ÃjóiYOˆ¨ÿQÛ'R}êÖ­Ë?ý@VfÛ¶mS…T›ÍÆí·ßÆÑ£GIKKS…Hï£|Þ·ÁÏ£ Š<‘SÌ?Nñ<ÏÍÍåàÁ~üágæÍ›Wãòû~ÀÑÔ4žyæYm<©¦Ž'ε¾¬PVV×]{½*HDmŸT²W^y™nÝ»•øÞœ9syw»çõñüÙZÆ` @\Ü^öîÝ«s69gú(›jT¤ê,Y²”Ù³gãïçÏðˆóèc””Dll¬*GDDΪ¾¬ÓéR¥ˆˆTÉ“'3í×_xà¿qìX“&M )1It–JK;Æ-7VEÈ9Çü‚AÁ”gÃ(˜ælhʳÈiñóóã†o`åÊUüöë¯ÄÇdzeóF޼šƒ‡²iÓf5jÄÓO?ÅýãÇ1`À’’“H8@÷îÝyáÅçsëêׯÏÃ?DP`nÓdÒ¤ÿ²sçNçç©?y¦,”•æàÁƒxú™g¸ãŽÛéuq/öÅÇ“œ”ÌO?ÿHTT 4`Ô¨‘|ÿý”R—-ªmÛ¶LþêKBCC?þ~nüË_¨Ɔ 0M³Ô¼®Q‡ÿþ iiiÄÅÅy¥]Öç—UÆ–­ZòÔSOrϽ÷0xð@RSSÙ¿?Íš5ã©ãë <‡ÃÁ®]»hßþ¾œ< ‹Å`ܸqÜ~û­Ä4jIJeËèÚ­/½ôcnCT½z´jÕŠM±›4åL¤ ùù¹X,VŸ–ÕñÄÙÓ—%$$À¡C‡JlËýýý‹½”Øö–Ö¨ÿQÿ£¶OÎg)))ž¶öò+® %%…/'M&!!´´´RÛ“’Žç»wïÎ3Ï<Í=÷ÞCÿýILL$!!   ®¹fkV¯ñšò\Z©s–’ûŒÂ÷ƒƒƒ¸÷¾{¹óÎ;ˆiƒa±ðòK/2zÌhš5kÆòeˉˆˆðL3·Z-¥¶ý;u*µÞ^{ýÕbå/«EN§ò•nÊ"R üýý8p ûã÷À«¯½J@` ¯¾òÛ·mã©§ž¤^½(ÂÃkóô3O‘‘žÉÛo½#?ŸÐÐÐr?£¬4cbbxèá‡X¶t)=úùùù<ÿüsX,ÆÝw?ñññÄÆnâþqãË\¶$mÚ´æ?Ÿ}ÎâE‹¸îúë8h`™y)tñŽ™?ññû½Ò+ëóËJ·V­Z¼úê+8œ^í ¶nÙÆãOž¯[·.Ï>÷ qqq<úÈ£$%%ñä“OX¡v]ç,¥÷'ÒmË_Lb០4hcﺓ‰Ÿý‡ßûþý/å’Kz—¸^ImYN.¿/y©jšò,R…®»î®»îß|ó K–,¥oß¾DFÖå­7ßdÓ¦ÍÄÆÆ2hð zöêENN|üñ'ìܹ“uëÖsõÈ«Ëý¬=z”šææM›1 ƒ€€²²³yù¥W àðáÃ8òóò8rä-Z´(uÙ’üðÃ,^´˜E‹qQÏž\Ò»7y¹y¥æeÇö|6q"üQüz’þþþ¥~~YeÌÊÌ$44”?þ„}{÷KzF&AA´k׆ڵÃxáùçÙ¾}+W¬¤k×® ¿lß|ý _õ K—.cëÖ­\uÕ•4jÔˆð:áòч±sçN–/[ÎÐaCµc‹ÈyÛ—¼ñú9’X¬-oÛ¶­×kƒ*µíüåWÅÖWÿ£þGDJ׫WÏRÛ“E‹yÏñøcO°ÿ~²²²˜3{ݺu£aƒ;v¬ÌÏ)ÚFꜥô>£ÐÏ?ÿÌ’ÅKØÇ¡C˜6mK/aÍšµ\Ãõ4jÔ˜-[¶[¯¤¶÷ž=¥~ÎÉå/kÛLûeš¾0¢€¢ÈÙnÑ¢ÅÌš9‹ûÓ¿æïŒ"##xû·½–ŠŒ$==ÃÓiTDYiNÛ½›÷ÿù>×^{ —_q9‡bÒ¤É8p X:»+°ìÉ^'¼Ì¼vÎyùù%¦QÖç—Yoþ$'\_&//ÿ|ö]ºtààÁuzèð!š7oæyn¿@­ËUpm0«ÍBxíðSÚ""çT_¶p3fÌð<ß»w/õêÕ/µ-/|­NˆrÛÞ¢ë«ÿQÿ#"¥ó¥M-”Í´åáG&**???€RGï•Ԇ뜥ì>ãdGAîp: Úôã­¥Œ0/©í¯¬z©. (ŠT¡#GY³f {ââèÛ·/7ßr o½ùII)¼ðüK^×âÈÉÉæ¢‹ztQQdddx¥çé˜J8(+ͨ¨z$$dìØ{hذwÞu'=ôV,_ANNŽw'TeOUÝ»w•™—† –›FiŸ_Vº=zÔ[ݺuÉÊŠÇf³Ñ¯_?¶o߯‘Ä#Ô¯_ŒŒt¢£œœRf^RSS*ï8BÇgzõ¢èÖ­›çÑ¥kWŸÖ«hÛ«þGýÚ>‘SoS‹Ïo¿ëѵk7®¾úª‚“KÅFÁ霥z•Uo'—¿¦å]έ>ÊׇnÊ"RM¾û~ ‡ƒÑcn!''‡§ž|Š´£i<òè#Ü5v,ë7¬'>>žcÇŽñê+¯^;ŒÇŸx‚ÀÀwLK;ƧŸ|BëÖmxðÁI<>½ (3ÍE‹ñÙgÿa䨑¼ÿ¯Ò´Y^yùrssøéÇŸñó³1îþqå.{²ää$î»ï>À”ï§0oÞü2óRž²>¿¬t322xîÙçäÙgŸ¦k×®¼ñú›ÄÅÅ‘““ÃÓO=Mnn.?ñ8ýû`âĉÌ)ç„.##W^~…ðð0~ääæåi¤ˆˆœwúöíÃ+¯¾ìy¼üò‹>­WѶWýú9õ6µèñü²¥Ëøí×߸òÊ+wÿ}ÄÅí ºAt…?Sç,Õ§¬z;¹ü5-ïr~2"êFš†añü-‹Ûå V­ÚªA‘*f·Û™öë/üï›oùòË/kL¾Ú¶mË{ÿ|—W_}Å‹kC‰H‘`H«Ý§euÊW¡(""""""""">S@QDDDDDDDDD|¦)Ï"""R©4íODÔö©í‘³³ò•F(ŠˆˆˆˆˆˆˆˆˆÏPŸ) (""""""""">S@QDDDDDDDDD|¦€¢ˆˆˆˆˆˆˆˆˆøLEñYµï¼ë.fÌœîyŒ3¦Ø2C†áËÉ_òíwßpëm·j ‰ˆˆH‰î{'~rÊÇ""g“V­Z1öî±|øÑ¿Où|+<<œ~œâyÉ_|NÇŽTÁ""R!¶êü°:áµyîÙçXµju‰ï‡……qÇwðÔ“O‘‘‘Áëo¼ÆêU«Ø¼y‹¶”ˆˆˆx´m×–>}.Á‘ŸJÇ""g“ððÚŒ»ÿ>–.YF:§|¾UTHH/¾ô_Lú’ØØMªd©j¡^'œÔÔ£¥¾ßºu+¶lÙL\\ÉÉÉ,Z¸ˆ¶íÚi+‰ˆˆˆ‡Ýnçþûïç£?9åc‘³ÉÑ£iüãï1kÖ¬Ó:ß*ÚŽ>÷ü³ÌŸ?ó¨‚ED¤Âªu„bxxâQ"#£X¿~Þ™@ff¦çýÐÐPÒÓ3<ÏÓÓ3ˆˆ¨£­$"""cÆŒföìÙ$$8åc‘s‘omŸÁÃ<„Ýnçû令ÒDDä”TëÅ>üˆgŸyŽ[n#?Ÿë®¿®ÜuLÓÔVÚ´iMóÍùuÚ¯•~Ì!"r¶ó¥í Ân·^»6={õT¥‰ˆÈ)©¶Š6›ÄÄDŽI`îܹ\vÙe^ˤ§§êyʱôc%¦×²eË[©5dÿþ„ójG:ßʬòª¼µk×.õ8"•à/ù ]»veÆÌéž×~øq ×]{}…Ž9jÒñÄùxÜ ~OÎÄ69×ûb_Û¾œœ^í Z¶lÉsÏ?˃»$))ù¬;çÒwXu¢zQœKõr6öQÕP4 ƒ ï¾ÃóÏ¿@Âú÷ïϾøx à"ÃG¦±cÇN~ä!Z´hAZZ}ûõe„ geeŸÁƒó­Ì*¯Ê+"ÕïÅ_òüÃóÏ?ËØ±÷xO”uÌQS¿ëjsÔˆ¶É©ªhÛçv»q:lÛ¶~ø'žx‚Ç{—ËuNÕ¹öÕ‰êEu¢z©ZÕ6åÙápðþûÿâÉ'Ÿäëo¾" 0€)ßO!*ªo¿óÇŽã³Ï>çùžçÿ>ü€Å‹³më6m%‘Säßÿzìö;çËYôx¢´cµ}Þ~úñgÒ32sëhU¦ˆˆTˆQ7Ò4 0(ø‹çoYÜ.µjÕV – eË–ç]äú|+³Ê«òJÍe©S[‹N`óùs=îäü{_ŽiXÉ_2 ¬vüûŽÄ}ôŽ­+ñkß ü!/#,’¼•x2 öv=°„FàܽwR–¨ÆØ/è‰cÛ*܇÷â×s8X¬8w­ÇÞ¾7fæQ ?{·boÝÍó-«ã µ9Ú&r®lµ}Ú_T'ªÕ‰êå\è£|eÓn!""ç k“ þë#X#bø`:düߣØZwÅ¿û`Žn[…µn‚¯OöôI^6[L+¯4Š/¿‚¼?Ä4É[ðîÔ#åæÇ´X }à]ì-;ðzž¿{!#ïůcï‚ôÜn²¾~Ó‘ïµ@Μoðï7ª ^¢áßÿºj (ŠˆˆˆˆÈ¹Í¢*‘s…kßÒ'Œ#óË×È™÷=†Íޝáä­™€_ûžØ/耙—ƒ-¦9ó§pôù¿àJÜ_bš®÷zØš\€;+ݧüøw„½egrf~IÚ ÅÌË&`ðMå®—»”ôÿ{3;Ýë¹-¦~{ãØ½‰ì_>ÅÌË)F—»h*GŸ»wÖ1ìz“öðpò·­!ã½ñÚADDDDD¤Rh„¢ˆˆœ3Œ°º„>ú1ÖÚ‘'^ Á¹uîœ,ìôÂR7gÂŒÀ`òWÍÁL9„+5kT#¯ôr~ÿ€ÀËnÀ±;–Œƒ¼ŸòcmØ€¼õ q''þÎ}`±akܦÌõ›—áܺ{—þ^Ï®¸{‹Ø[tÀôô¬çÚ¿3õ0fæ1Œ“GVÊåï"ÇeáÔï¸""""rnP@QDDÎ~]`­Iæ·p¬ùƒð·§¼áÌDZqö®±ØýÈþísL§#¨V™iæüþ9¦Û½Ug2>yÒç`"€™“ €%07`mÖÃbÅtä,`­`7|<½ÌI¯¿jÖÂtæá×±6~ huór—,–%Ùùbw *DDDDDÎ ú©\DDÎfn~ú|ûó¯]]ÞÚùXì~ä¯së LÓ$øú ºîoØ[v,5ÝÜ_ñ¯¿W(˜àˆ]Šértíý^ÿwBn~û=q''pé5ÿõQ,uø”^^ìRÜŽ|¯OàÈq„>òµn¡ÌuÜ9YXë7ÆàõÚA΀{Zå2i—?—D9±¦*DDDDDÎ (ŠˆÈ9#ÕlrWÌÆÖ¢–º qçfc‹ŠÀ¹m5î¬tœ »q'ÆãJØMÎÔ1‚°wŒ™Ÿ‡™—[©ùqÜC椗Á@ÀÅ—‘»˜ìÿ½kß6rü€¥N}lûxF2–ÇLÜOægÏb¦§péHÈÏ%gÎ×e®“ûÇÿ0C°·é®¤šõˆpà¶³óçaƒ£óU)""""rNДg9w¸œdO~•ìÉ%¿—öø•ž§F­Úø_z=Ž­«1¹ø÷¾ÇÖU•ž%ÇÚù8ÖÎ/özöýÃÅ^Oé‰u×-ðzàܼœôÍ˽‹·™Ôµó<Ͻ<ÚóîÌÉäΜ¬}ã ¸»u.O®)¸–åŸGüx¸}63ú«bDDDD䬧ŠÔ9ÊFTªMDälçÎÊ í«Rìª$9«i¨]<ß7”÷V¤3~ÖQ~º6B""""ÅÜÝ*‡oöŸÚ¼7ÓŠ¿Å¤I°S•TŠöaNÞï‘AÏ]oRDDD¤&ÓEõjàG¨¿…Åû p'®ËäßÃÂ?ë¨*GDDD¸*&5)vRò¬%¾ÿça;C8˜¸S‡`%Ö0Ÿoãü¹ºq>û³­̱ªRD€ÓEC·o?6Äd¥rÈt‘eoC¹ó°aú”N-~ª|9e~¦›Z¦ËçåÓ +Cã¾äì £Y=ß7Œ‡çž®>ä *8§z×âµ¥ª ‘ó\]ýê9xecP©Ë¬IµsmÓ,‚¬n²]:a(**ÀE½7[ÙØzÌÆ{=2¹}i¨*FDhåÎáo®Ã¾-¼/Dk4±Öàbo=ä8H˜á[€gÌ–èb¯_èÊâB3Ëç¼o­K®‚D>©eº*€;dØ1 ÃëµP·“' >­oß™Àtw0󬵫¤<íÝÙ¾ï·À{¥ì·¹2ðó1žlØØf ª’ò„›NÞÎßëóòSmuøÍZG;ö9JEŒíÌì=9$e»½^Ÿ¾+—Û/ æÖŽALŠÍVE‰ˆˆT£»ÆÞEÏ‹z0vì=ÅÞ2t£GÆÏÏÆŒ³˜ôŤª?^h•Ë×{>‚þ, IDATÊÉòça;ƒ£L; ;>54ÚÁÂ#'MßÞÈË3yv}ˆ*GhÕªýô§K— wßø—)¯í gâgŸrݵ×É[o½É„ ï»I•ì£æf.ýÝé>/?ÕZ‡Ü|µ±Ún'-ÍŸ—ßa’n)Jx=¡¦o—õØ` æS{ýb¯v¥q¥Û÷Y€·7%ý¤°†Í0©‡7As8¶–øÖ(g ÍÍ\Ÿ’IÇÊÄÊSYnr&W(^R@±±;Æf^¹ëG=@}W:ËœïýÖÀÄbøžo£”hÓ¿é[€ÔU:Røw¶O£–d&âpç²Ï F°ð»¦*([Ý@ ×· âæ©)%¾ÿß YEFF¯¿ñ«W­bóæ-U–Ÿ~Qù¤äZˆË,ÿÐjá;Ï\˜­€âI'(êçóðêÁÃÙV¶qw«l>ݤJ’óZxxmÆÝK—,£N’¯å^Ѷ/$$„_z‰/&}©`â™Ø¦5h¤W33—q®#>/ÿž5šØB ¦›@÷ ‘¿£íΤ&fí} ´¦ž¡•nîLß‚µ‡ n@»ÑDzÂÇ8“èdfû\·ø5-¾}ܹ u¥ùü™3¬á°?öë8â[°6þP©£–Ûº³iëö=(?ÝNþ90jYÅr¼Ð7Œ®({JóëKÒùçÐpeºØœ¬ ­ÿ?{çU±þñÏöÝôÞH!…jèXPTŠŠ‚ *ô§¢^ËE¯`QÁ‚zUT»€^.^¥“„ôÞ{6Ûûîï(ÈM›@€óyžt–¥×L[ ýÜf$9¥Ô;D,”G /pAà²s‰Kïvþý.oÊéš¹D¢ÓÔ.ËÚm_¬@Œä‹Ò‘ƒHâÞÈ}UŒ‡ËÅ¡ªÓ;ÿ}r“šOÀ_)øÆ8{ôï)åƒY>,zÈ¥°&¸H¸çžlÚ´‰ŠŠòV¿÷ññA«=ñ2P«ÕáçëÛeõy¸·™•EíÛþ²«ZÆ„!’ñ_LŒ°¶*(üZ®`€¿¾öKÛþ~vÞ¡ÃCâ:„@‡pì1{ÎSÈd2þóýBÃçxáÀÏ?o‡_Bƒ t¥…âÕ %¼O0º:#ºz#Ú:º:Ú??ëë›ßðÌãË}?7¸]î½?׳þö fü!t¬¿Ëä8m‚©Ó=¥üc²^bVo1‘œgcçûÁŒz¤NhN#6¶' . ©IÃãÏD [ЧOoââãøì³Ï‰ŒŒtû8W~zΨ> r ÁµhéI\;ý¹Kå…ŒM CÖ㢾¦½M¨ÿÈüÛȳÕ'fó¹:£ë#®FEíkâbNÈQ›‚˜Ö_Æ^c˜p³wÁ5ÉÏÏ¿èÚ­µ±ÏÓÓ™L†¿Ÿ£FâÀþmqg8Fv&áÚ(w?¸EDD&ïàé’ìpº'¬yzzݲ ükœÐà¾5Rll,éÉ–Q>63䕸]F`` Á-ëâQÒ÷¶ŽJ$’V¯©Ð¶BÛ m{îÛö||F]T‚¢‡Ÿ’›æ^ÉÆ÷÷³wå1¼Ux¨ð TÐǨ¡xªðòdTA1Œv&øch0¢m0ch0`ÔZpÚ[_”XðØïjÞ½JÂß|¥ƒyïž TìÚJñok…éZÿâ¶ùl}'kžjJ Ã,[ö1³g?^¯ç®»îdãÆM¬ZõÝy^wÝ}7Þ8 ±XÂÖ-[Y¶ly‹:}út†ʆß×Oûñ§ŽÐjµøúžˆìããƒF«éáÉ‘:^O÷Àh/j÷±›üì ð/dS™×E)tüÅ­Œü^$£Pwê6\$‘óâ  ;è}æÂ„ÊA•IÒmħ' ¬È•Se2ðH_3_çë8·×ä|ÄݱÏd2±èõÅ$$$ðò¼¹BÛ m+´í¹oÛó‘‹FP M಻³îÕ8Í A}ƒ }ƒ òNÎå%æ¶+½™_$ÆÓO…O¨a}ƒñôW"Ê3ëHßXÐêï”iüT"géDÛ¨æbÄ+2šQ//¦xýZJ6­'fÂ$nX³™â ë(üu ƪJaÆ×Nô”òpBâñÁÍìbÖûÖ¾êÏÍ/u}ßSxÊsï`¼ƒ½°™lXŒ6Ìz+V£ ‹Á†Å`Áb´c1ذ­XŒ¶ãß¹œ‚¿ÇîH||<555èõÍç~þùŽó{{Ì!I\zéh}äqœN‹/fà A¤§¥xF„†rë­Syê_³1Œ,|u!#FŽ ¤¸¤Õôƒv‹zŸ*ÏÌ™лwoæÏ›OIIÉÓO,xåøÿ‘‘‘Ì›7÷x”g?Ôê&rsó˜=ç)âããijjbÌcX²dI§×厞&6UÊ0ÚE:>­IÆÔžVöj.^KþHžRù:Éió">ÏWòü‹Žyvè÷úÙ˜eÅé±±RFrã¹÷rY°‹CD†¦¹.}}ldk_&îÑÞ±Ïétb·ÛÉÎÎæÇä¹çžã™gž=ïŸ÷g—‹BPL¸$’¸lxg¯[ù_áÁ‡)êÔÑRÁŽΤg.ãàÔµŒ*”¥•âÔYxñ2o^ûCwÆõ—ÈÄ ¿¹/=CŽ+ç­LjD[Ðü×Þ‚ýå¤nèú7Œq“§3áz¿1sc³¥\Îw_“óÝ×D]=Qsc¬© ð—5Ô%º ú—ÌË›!#6¹g³C~CUúÊ U•«+0ÖTu¨ì=¥<4Ùo•˜Õ[[ÿNÚÉâ•z¾~ÁŸ{^ï:Q1¨§/W=8ŒßßÝ‹\%Gî!C®’!SI‘«šÿ÷ö$ðÏ4…‡¹Júg>)N»‹ê¼ꊛÈÞYŒÃ&ø‹êLœ87ðÊ+óIJ̇¾Ï²eŸ²rå*n¸a÷Ýw/r¹œ7ññÇË2$‰éÓogΜgxíµ…lØð;z½Y³Ãáp’’’ÂG}rÎÎ+77—^œ‹ZÝDHHó6½îä±ÙjµbµÚq:]þ9Ðëôm¦w—z·•G©R1nü8zðaÔê‹ãåVHH(¯/z™ÌD£Ñ°bÅçÌ›?¹\ʆõÉÎÊîÔß‹òpÐÏ×Á’Ì3‹>¼³FÆÈ€:Ž^¤ã΄p+»kÜŸŽè$SK˜gæ›B÷ýV&ùÛ¹)Ú‚ÞëËåé%¼4ÈH‰^LƒUrÎÎßCâä®8 /$ŸH÷×IfE³2ö­ùi-âž{gðÅç_ * à6çDPœùàLFqÜ¢à0ž3f4?7lä«/¿:£ß:¥/J/»¿LuoA-£Þä$«±m£ðP•Yõ žÔ}½‘Ãÿmù ÞP`æþÁžüß@¾L7v¸þýÇÅ1pB<)?g“¿ÿœÏ»NññÇè¤p®yd[?îO$‘0zÁ[è+ÊØ;÷éVó”mÛDÙ¶MöDüäi |ðŸþ²†âõÿ=ïnUt,½‡_JÈÐQx†…S—–‚¥ ëãJÑ(#¨ FÙ‹ÇÄþa(ƒÃ0TUb¨ªÀPUŽ¡²}UU³ØXÝÒbóïB⪭FRòÜwD_Péà«F–>éËcïi:ýÜcGDÐwL ¿¾Ñì,Ônµ`ÔXÚ7ðÈÅHåRâGõàÖ×®¡$¥Š¬%4Uê87ˆÅbFŒÎòåŸðòËóùðÃ÷Y¾üSÒÒÒéÕ«ó/Ôj5k~ZÃ_6o ^¿~™™™.¶ZOïõn+Ob¿DŒF/Í}‰ž=cHNNaÉÛK0›ÍT¿-//?>—¨­­aæ3·eó¶lÞÒe¿ýpo+ ϵZ}’‹€óœçnòñ$UêV^• “V­,Ã’ûÆ¡r÷9ãC©`¹)Ðqš¢‚ÙÞ;Öíüšl=T›Z¤×˘]îõE­L]d¯a“ËHqß§m½] j¡\¨œuA±ob_.¿ü2lÖ–Û{|}}¹ÿþûyáùÐét,Zü:‡"#£c‹·«J}©†¬íî/fŸîÉôß5œn½YoãÀ÷D áæ—¯äàTdžì{äóTÏ]êÍøž 6·Op‰ÊÈiý)N®dÝ«»Î¸Ýsv•ïÏíoŒcýÛ{ÑÕ;횆Œ¸”ÞÏÌ%ùÝÅ4f;mþ†Œ42ÒP‡=îºób;´ÜÇ—¡#:’С#±7ÖS~h?9«¿D[\È€XóîðâÕ:â"j 3ÐOL¨HB˜RŒµ ôA”¹© F×£/VÏ1ˆ|CQ…F`¨ª¤úð>ÄÛ>í°øwRómx)aуÞ<ÿiç‰tƒ'õÆ'HÅŽÉgTŽÝêÄnµ’±µˆŒ­EÄ ã²»a3ÛÉÞUBéÑjNÂS†‡ßé­s\.ÐÖè»zh‹¡C‡’––ŽÝÞzŸ1b8Û¶m§  ÙÝÃÊ•«¹é¦)§‹ŠŠ)((ì6mvçwÎ+¯ÌçÐáÃdefÇBB™<åFœùF£‘yóç1tèÊË+[MONNéõn+—§' …ŒåË–Q^^Á3Ï>ͤ&ñÓ? 7O'p]„…ô´>øÜ]"²,þ\b<­8v¡1>ÂÊÎêŽME(QðD¢‰r£˜B}Ë2. ¶19ÒB¥IÌ·…rª[¹Vj«˜õ2ìeâÓ<ÕY?ÿqaªL­×_”+Cml©Vtúïþ³¯ £CÄgyJAX8¯ÐÇ…Ò8óz·ò6Æe‡ ³¥oHÓýW{÷–1½Z™÷NÁ¶qñn×ݺ`èOÞid‹)—»?öe °uMÛjÂØÝ#Ôíüºr+Ô·¬Ì±¨T.÷”¬RäÐJ<ÉÒ¤¶E¹ÿ¢É¶­ ,' mfo¶½øˆ[Ç‹D"Š7æÁæ–šAöÀx4 ÷Üš˜œ"H7´¬_ 7Ü<ÌýyÁ²C­ Š9¯ÝÊÍ~[ÖF¿u·mJ³¥Õ¶ux*мtû÷ío¹°©¥»¸Âè,"÷Öº‘*ºnlÙýä-¨T§ß ‘ˆ)ÉWÃÚœ–÷§\FÍýg¶C$âBଠŠ2™ŒÇ{Œ?ZÆÌ™÷µø¾wï^dff·Àؽk7};$(N~a é› ¨Îq?@ÅÓCU¬8fÂátÿâ–§×R‘YGÒ¤^Ä çÀ÷'×uñ^ïŽ÷£ÊààXÝéo¿oFNë‡ÕhcËLJ°è;ϯRmšõoïåꇇ“¶1¢CUg\æ GžDBáâ¹4¶Ó ÉTWKÎê¯ÈYýQWoÞ]]Aá/?Q—røœß~} 6Š¡£PSŸžBý±4r¿û†èðPŠþ<ßkG(¸f˜‚'?l¶¬jhyͼ=D„øé (#,Pò§Ø(&T!ÆQ ùú@òCB¨z÷wÖ¯{“Ã_¹UÍÇlx*E¼x·¯}{æ[4Çü_º:#‡×fwz[—¤TS’RM`´ ££5­Y;ŠÈÞ]ŠÝ"¼nkŒSz¹÷Ð*8TÁÞoÓN™gâÄ ¬]Û¶µ°ÝnovXü'b±™Lögp’îíÓ×בšš4TUU‘œœB¯^½NæúõO$33“òòfKð=»÷””„¯o«égCPt§ÞmåÉÉΡ¤¤Œœœ\víÜÅÀ…§PIœŒ²ñ~–G§•™jäÚM 81ÂΛÊÿï,o 3ðøÏã–|cB¬L‰²R¤—ðY¾’:ó©-üŽ4ÈèícæÒ`+{ëÎ^û*\ÛÃÆ«i­÷£ýuRvº x]„…„\­„šø£NÊšR¥pc ´‰‘¼KéVÄ»¯y fº¿–kC˜Éí‹^qzO¥ò ^o‚Ü–kk˜öûܽ¬ËA}K±VóÏë°º)ziÛ½ìÑAˆ&º]ܵ§¶…è®v^ŸÖŸÖ+ûaïâ^›¨Í¾½[w[wÛÖJ³yUëmÛ9è'CïfÛÔf˜ß²mëûõdëí×»ý› «A^c‹tq„?b7ú­ ©[×gª¯Ff;ú­ã¥­ ;ÿ}hŸUAñž{f°iÓ&**Ê[ýÞÇÇ­ö„5•V«#00 ]¿á ä¦—®dó‡ÐÕ›Ü>®·Ÿ„2¾Ìl¿ðâr¸Hù9—&¿påÕþÍ(ç_››XuS ·¯­§ÑÜúÂ["—0rj"Q¾¤®Ï¥¡TÛ5ƒˆÅÁ¦÷0â–D‚cü8øcV‡Êù{à•œï¾!..öŒêU¶m3eÛ6ØC']GÒ-—ÿÃJ~JklwY~á^„÷ "{W .Gû…Ž>w?@ìuSЗ—PŸ~”¬o?GWÒº•Õý׫ð³xÕ©ûÎèBgtPPÙr@öRAˆ¿Oe)é«ï æ®GIœ}5yŸ,Ân83ëÂM‡­ÜlØð{·j«¤!IL¾ñF.\ˆL&cÈÐ$öìiÞ²ÿ—#ûò²2þïÿî%<<“ÉÈèѣذaC›éÝ¥Þmå),,$44„¤¤Áäååqù˜1¤¤¤7N'ðÖ0¯§{tj™j‡­ML?_;™š‹#^ÞÈ@+yZ1zÛ™mé}ó˜’׆øµ\ÁÔ M–æ(Q[Ü/wu‘’¹ƒŒdk$gÍbï‰D_ä·=ŽW›$ØœçeoÕ‚±£L‰²0﨧˜…iRÆGXø÷Ÿå)Ik|6 tCzBÜ4¯3ºÄì’ú æ&æñ°¸!ÌXKÂŒ€ÀùˆK.FØ·)2±ÐhÌY›µöéÓ›¸ø8>ûìs"##Ýï$®Ö!UH^‘Q'öëÈ%w dí+;pµÓgÀ Ã=XšzfÛ€kóÕüþîFß2ˆ± åàÇ}ËÝûs¿Má’/kZ÷—ŸÄ£¿årô·¼³r=­É"nd×Ͼ”õKö¶ëØÖ¯œ)C‚D\*âÒ° üŠ2IÑ*ˆºá*n뵇E{9Zsú ˆX*fä´D‚{úS’ZÍoާà`Ù;‹ÑTܪLj¢-.d÷Ó`7ÚרË÷xSXeç‹ßMgtîzèM'„Æ’•áÝwCÞü’²ÿ~Cõæ3ó1¹v·…{&¨¸÷Z%_ýÞ>ÿiJ/Sæ^Iþï5].&ž41ÒÛŽo‡ŽN ã²»b³:ÈÚQ,l‡þK€¨ÔâáãVÞÆÓˆ±W^9†]»öœ2Oaa+V|΢E¯áííŦM›ùé§µØív8Äwß­¢¢¢‚ÆÆîç$e×Î]ôíÓ‡eË—átºøyÝ:ÒÓÓOrdŸŸ_ÀÏëÖñö’·ËålÛ¶­[·ár¹ZMï.õn+Ào¼ÉO»k¤L·^4‚âÄV~.;s뻋„²=¸)ÊÂÛÇTèì[,,ÍVòü@OŸŠ“#-d¨¥TO-^¨—rehç ŠãÂ,¨—aqžh£Í• ö×ʸ=ÖÂUa6>ËW¢·_˜ ®™ &Vä«87\áÔ2ÈåÞZ«)»EîÎY›µNŸ>¡C‡²á÷ šúá$§ÀZ­_ß d4ÚÖƒJÄDE~Û=x¾ô:†¼,Dš|<%åä®Ë$¶gû¬å® 2Sewàð ÚïÌÏÕQ&Cç42î#©ËÖR“Þ¼˜ÿ×A8|?Ì:èI­YL`‚'=¯¡!OGúª2$¨ÎØÒ¯]ÔCíQ=w¿w-Ç~,G_}j±I$ùÀ,¬u5Tñ~>à×|½ÂÃÃÚõÓ~R;I>f’|Ì ö±Pl”‘©WðŸ:ææ7äâýu$ÝðÏŠ>Amñy¾‚&kë“܈¡~DŽ ¤|…¿×RR¿)%°·ã…Íä¤ê¨uaÛÂbàU‘ZLˆsÒ‰?eýçÝafÃorªdDGwÁµ1ªQ/[HèØÉô{=š5+pª;.èíɇ[/3ñðT [SÝ[Ày‡+és}8?–Úé}ÓsàH\#Ö†:luˆÚz  …¢M x+xe—NHÙ†ã÷UWÕ£ÓËÌÏïÜ(ëÅé ÆÇ;ïü»EúãÏ:éó† ¿·j}øæ›oñæ›oµHŸ5ëÉnñ s¹\,[¶œeË–Ÿ”þ¿Žì׬ù/kÖ´ðÛJïõn+@VfÿxøÂL§“x~€ŠåÔ˜ºFlÉh’rS´• …“zË…ý=ÖËŽ%úαÔÙDíŠøÜª€a³©RÆý &>ïBÑ)ÒÃÁÈ ;ogœþ7ö×Éxo¤/ :gKò”h+‹Ó[þ®Î.fEžŠþ¾6 5°¡BÎú ÅÓßä^hbc…`) Йœ5AqÁ‚WNL¦"#™7oîñÈŒmÝÊÍÍcöœ§ˆ§©©‰1WŒaÉ’%­–—“šBÑŸûЇÿßuxDöÆè1ŒðG§Ò~˜úÔC4¤Áå<µ©¢Tìâ®$n]ß¹âDiiÙG ÏÑÄO æÐ™4Uë¹¾¾¼ÉÉoþádX%lüp§úIl7…q(‡±3‡RxXGή’V³…ޏ„aϼLÊ»‹hÌÊh½¨ÓøPW†ZISK1œÂú0C#cnŠŒI‘Þ¦ã³<ÙÚóÛbö’ +Ób¬,ÍQÒh¶º t&ç|–ð÷­[†+>gÞüyÈåR6¬ßHvÖ©@\v÷ ÔÅYdüÔ¼M„ßÁDŸÌ YsQg§Ñz˜†ô#*[F#}~˜'KÓŒ]v~9»J©Ì¬cÄ´~T«Å#@Å·á^Œ/.FSh䀾{8âܱ"™Á×%pù½ƒÙóUêIß xðq<Âz°õ¡»Û]î 1b. qI˜ˆlµ‹”:'凉(qS“©OMÁ;:–ÒÄñܹö7nKTñÇ=!|˜e¥at¬fÛ–Á|‡¦uEMÔ5¡ô–Ókt$·/º†âä*²v– ­³0úåElýÇŒS–‘” ã_ÓÏWzs>q‚ ¥ØÅÂ4:Ÿs"(–——·Nüß-g[6oaËf÷"ÜN|b4Y;Š©+<á³ËÜPOÕ[©úc+~}úãŸ8ˆW]X&£!íÕûwДsŒAA"½$|’nîÒóÕÕ›ØóU*q#ÃiªÒ“±¹=Àý•ü+IÌ»GÍÝ¢3¤nÈ'r@7ͽ‚õKeOÀ IDATöátÉóÖRŠ7þJÞ«ÛUÖÄ(³KØZî`_‹ÓÁêìX½Š~YCÒ?ç ÍÏå§¼ÊúD1u„ïúbÞ8 Ã¬s?ò”Yg%}s!é› é94œËg Æ5àaо_rÊã®­äÊÁ žúèÜ©Þô_ÔÉûˆ½çŸèò²(^¹´CåÌûBÇ’G}ѵd•œÜvWÜ—„¦ÆÀ‘u9^¿>úôBR–ÌÇXÕ20“¥±Kcä´´€U¡ E†gDž=¢üä<4ùYÚw¿ðHî~/á´Â⌠–î-Á[.ÆG!ÂG!Æhs¡µ8 ŒN™‰Ú(dá>xÆRZª%SUU&¼GÇ’86†¬%ÂD@àgr¤»þ8‹Ñ—wVKféôè¾Ý…‰VvUwo«·¥ÙJíŸK 'zöò¶“àíàÃìöm§.1HPJ\ôPÙ©0u¬Ý. ¶’§• i‡ÙhóqŽŠ¡6>eàÅdêÏRÀš3Å_îä…6WÊ9X/lsè*ÎÛ} Áq~ùo&†FË)ó5ådД“A«QøâŸ8ˆ¸›î¤rçF^ðKáõC†³VçƒU'}þ,ÃÌõ=e,ãÅìÝúnÑ®åÇjQWh¹ñõ{1…NfÿÂ1T¹¿µ·€ˆ9ƒÅTè]<¼ÓÞÖ9õ:úÁÛ\ùÎÇx–|DÆ–Bæn­¥§·˜ÇzPovòNŠ‘sû":'W¡8ÑÛ °“øìeì/'{w).牲¾Ñ¥ˆ7WŸû­®–úrÞ›Gðåã±t yŸ,¢)ýP»Ë™ý‘†å³}ùçûZ*ê›EÅž½Œì%TdvþF¯èë§ÍíXJn”‘Ü(cA’ù©ž¨­ÝÛRñ’`·Å˜ù [%lqèbÎ[A±¾¸ Ÿ°öY XÔ TïÝNõÞíÌš~i¹>Tέ·¾ØF•ÁÁ®óá¶ ÚnѶÁc¦Pmì‡ÇÁ÷ˆITYuúc<%Ž”ÐÃ>Ít’§é¼ú„õ`ÀÕqÔîùM ”¥5ûã,Ö9yiŸKÂ%|1· %V>Nw?ârØ¥W#õô"oU³õ¥ÂSJÏaŒ}`ÛW$ƒ æÿŸ79e6~ÜiíVý¿nÏfÔÉ{‰¹ëQ‚/GÞ'‹Ú]ÆCK4¬šëÇoèÿÌX¶zÄíhØíaЬ—0«ë9¶tq§—­ÉËB“—?ÜÏŸ€ÄAyÿ{ÄΨ¦>åÞ[~eÞ /nW“Vkoµ£ÝEİpâ/‰Æ¤1³¯ÜNæþÖ}3~rµoÌ!þæ˜tVªsê…'‰€ÀÆÐIv¾Ì?ûÛr­bªM"ûÛÎÊ6ë³ÉåÁVRÕR,Q·¯k¦%K%µŒ ´r áÌ,Tï7ñK™cÏ{ŒW‡;´~D • £˜zË™Y~S à¡^fÞÈè¾Û‡ï‹7ã!u± › Ö+±Q%v·òš°D@@@ ÛsÞ Š"QÇ'£Þ2×Tmçã[ÞAtè¸lösz.)uN0ðÇ­þܾAC¹ÞyÎê’ôÔ| Ud|ò6ñ£#™4çRêK4TæÖS“×€Õxr{=˜(æÖ„j>9æäý´Î«‹‡¿‚A×öÂn¶shm½ÐK<ˆ½ùNŠÖ®:žo_•ƒ}U:nŽ—³õf?ÞI1ò[ñ©@Ðpân¾ƒ#¯=w<Íb°“³«±LÄÔùWq>…U[L¤غå=`7(øô-ü‡_FâœÅd½ýœÛÇDúÖ;ÏêøðéJf.ÞƒÃÖ¹ýNáÀÈyïQ°æ[ÒŽty{X›ÔTïÛIõ¾ø&ôå¹AÆ÷â„;±)’~”ºôd\öf«LÏ% ££I݃âä*¯ÉĨ¶Õæï¼´ÏÀÒ±^ÜòmWÿc8{WZQWh¸0Hð¶3)ÒÂYçN8ÙQ#gJ”¥ËÅk¬hm"r´´¶®·æšØÃÊê¢óÇòòÛB%¯$ÈÖJÑt°}ûÛP8YSÚñó¶9E¤«%7§DÙø¶ðÌ·ìçë¤ ²sY°•?êäÝê:ùÊœ¼0Ðȶ*)ê/LËÞ ä$LýBÜË«6ÃüíB£ ts¤Û ÷ðpñl'óÒD4½KÒ¿òæ‹ç¼^ÕF7ý¢aéU^¼›bb_õ™‹XJcäLŒ–£ÀÁ;{«lì¯n) *ƒÃ9ï]rW-GyB,Ø_NIJ!qþôèDÒu½0iÌTåÖÓ·¾Ž‡£,¬+rò|N¥Ueg6! õÄ/Üÿ^ø†ycÖ[)>RISÅ +Òš}»H¸í^“FÐpôä­¾k ¬üVláÁþ*®ì!åÃ43¥ºÖE²óÞåð«Ï´ú]€ÒÅ}òTÖ)âÈkÌlݺO«ÿ>7ƒ¡ï}Gò“Ó[Í#÷Ö+€ð^A„öÄÐh ¶ ‰ŒmÅ$7èøå%O®Ÿ×yÖº!Ã.¥Ï=ÿàè;¯àoidˆó½é ‘ªæÿÕV¥Fø±DD¹±s­Ub½\,ËäÇð½ZLü«H9Œ „ˆžxr…¹­ ¥«’’}ÇXÿÖ^·Ë®7¹X•cfÞH|r˜ëf_†wöaÒXÎêu—É.ºá[à,b;Ç/ÚNÇ]wßÅ7NB,–°uËV–-[Þ"Ï3g2mÚ-Ç?¯^õ_ýõ©Ç.¥ƒ‡z™y=ýÜZaåk%lD΃½L|š§ê’߸;ÖŒÎýýœÜgÁd‡­„,”­¤Ó·¶öñ±c¶‹¨4JΫ{ai¶Šçy>Ù«CÇÏêkâéÃ^g\õ2®³µKPLò·ÓhQmêœ6ÿ®HÉ;#ôüQ+Q÷°2håÎ8 f)ÏØ ó|§­1oü„ñ̘1£9àå†|õåWm¦ ¸Ëy»"u¹\í>æÿâ\ Åþ Fy‹øiO%µ‡÷pû}äÿE·8¯Ç¶ëyq¤ŠH/1?ä·_ ‹aBŒŒ‰Ñ ¢½%쪰òáQ&‡‹¡ÁR¦÷V°dŒ7û«m쫲²¯Ú†cÀÄM»—#‹žÅnh¹åÕnqP™UOeVóÖÎá±J^êiCâÁꈾTªtøiÄT×K°Ý ¢òUàî…o„7þáͺ:šZ=š*=¥©ÕÕ­[æÿç+Ïž®(«F}ÒwV‡ˆ¥ifb}D¼y™'ÛÊm,?vrЛÁOÍ'çÛeØ-Ïõš$ —õ—ñÐûF “KîHÊo9hªºvk|HB±CÃË$Ôf & j#µƒÚ„Ó~jëA›¶‰¼_gðk+H}±ÙOá_Vˆá½ðô÷ ¦°‘ú"5™;а[þ~D<õ©‘ÿ<ïÉm‹:¶å9ÜSLO1‘^H„ÂŒßÆY„µªŒ"ªM.jÌ"öÔŠ¨6C¥Qĸp¯'¹¨1;ù©TÌþú3_ Lq2!ÂÅË©b,ÍåìÊ¡`W±#"ð ÷¯p Ö`l¡W|ã HRQg¥!+ì­÷áqIRFõ‘RÝääçýVý¥LŽ•óó’}ܯè!cb´œKÃeì(·²&ß±†“…‘ %66”ØCC$ –r×`ódz+E2´-èøÈ\<Ñ×E„ÊȲ4(ÐéÈ3Œñ'ºo(WÞ? ³ÖLm‘šš|5MŠpR¥ÿ0o|Ã|ððÂ/‡͎¦Æ€¦Æ@þþr´5h‡“ùé;$=5Ÿƒóžhõû"­‹î005AÎ×û²à€ô;=o¼ SuÅIV˜1gš›ÍÅëN¹ûV§3|j"9»K©/RwÉõ0>•¯‚Œ­Ex¨Pù*ñôSÐà ¥¯ O?6‹c“C£±YpT1ª-èÿ&6:µµÿÄèw–Yó5ÎF¦ò&l‹1é,ô‰è+áÝSŠ—L†LŒ·\DƒÉI´é1Û¦ûR­ny!d2 ¶^>-Ò 4v®è!§Þä¤ÊàÄ7„*… G ©6‰©6ÃÕZŸjNÛT)bS% òwqK”‹Y}¬)±¶LÔÆqm#»xcˆ‹"=Ýß׎Éelz¸AÆð@ý}ídhÎÝõ¼%ÚŒBìbu‘¶Ç¼Þ½{‘™™AQQ»wí¦ob"­¦ ‚¢€€€€@{¸à}(ÎCƒDù(ø%ÍNqM³µZe£ƒ?2mL-çj5ü6i6ºâ<Ìu5Ýâü¾È431ZÆ{Wxòä®Ö­Æ’‚¥LŒ–11FÁ‘»+m¼ç¦™\ëÀq÷\~OOFœ¶‹$Ó{:Y<5ˆØ_/â@=T™šÛùþ'“#]|‘'æãºmï°º¨ÍkDfRRQ^‰wˆŠÀ(_ú‹ÃÓ_‰¾Á„W  Mm­Êì:²wc39Ψ}lZ-%¿¯¡ï}ÿ$û‹ÚÌ÷S¾•Ý6žâA¾¬0Œc¿yRžÄh /MWòù&3Éù-íÿ”EÒ¤^(<¤Tdt^d¯@#oëOqry{›·‹7Uêiªli ©ð”áá«@é«ÄË_…/T~*<ý”ø6iPVFO©¹-ÕG°ˆeèŒf :_O)F;èm.ô6'5&'›ýÏÏ.$âæk"bì ï¯;Ù:6""œÊÊ–Ñy.KSM(£Hú×|r¿ýmQ †î’¦‘¦¢t11ÂɆ«]¬¯€5ebJ §/kLˆ“Àâc"2šNßi;Y0µj5ÔÞGÝá}xÇ&Ðÿòá„Ïš…H¦¤º<çw¤ ÍNÃiw±-ÕÆ¶TIñüŠøú:^-—°wM%SßÉþ ‘zy#÷òFæíƒÜǹ—7ÉK^ž8€Á`À`€U«WâïïÏêU«/ŠOZ\ûðìsOÂÑ£©,y{ z}ëÖæï ×ñjºG§Š?E±^Âëé¼5ÌÀ’L õˆ"T^I2ðZš µõôe4ZÅ46ˆ9ÔÐ,0úÊœÄû8‰ñrpc”‰‹\­„,„\­ô”ÂÕ„p+»ªÏïwØ_(¹$ØŠÝ%â¨ZL£EN“EÌÙôz} ^Ê3Ìn ŠS¢,l¨è‹Ð•…ræ 0ñÄ!ïsr-nëiÆér±¡Bð—xº1ÏÇÇ­öÄ ­VG``Mm¤ ´‡ ÖBñ¶þ"föpPá)åýl(®i¹}Öæ€ÿ° áúK?BñÌ|Ž.x„&}÷8‡¥6ªŒN~ø3´ ˆó3!ZÁµ12ªôNvWÚ˜ñ»k;f´Þ1±$=µ€cË–`(/Dün‚ß+›Cœ pq[Œ »S„\âb[•ˆ™{O¿ÑÕšÐÕš(>RD.Bá)ksëò™¢ÎHÃ+²'W]KåößÛÌWkrñÒ!+?~oæ,âµ`1ü)ŠÎ¸ZFß( ³?5b9…«Ä£¿åÑïš8äJ)EGªÎ¸î±#z=(”#k³1ëNß>ƒ ‹Áÿ#6Þãdˆ?|רD«¶b°ƒÑ!Æ«ÿT‘ñ”ÿ¸ÌÍ5‹l•E`Á]×Éyã‡[ÅÅfq›Á‚Â.KÄ9²ø9\¶3ŠkÍ"¾)”ðM!Lˆp²p°“: üT"f_Û¡çôsâ/s1cÏ™[a\1@¸!åXµl[g Wí…g\?|‡%bÚ£hÒ ÷B¬òĨòâg•о L>Š_ÒuFF<:†ÊŒ ìFV½S]º’"ái# ÐÉÜyÇ]„‡‡óÊ+ó9tø0Y™Y'}ÿñGSSS^oà‰'f1íÖi|ùÅ—-ʹ6¿³Th¬ân{®F»ˆS<™•hbK•Œ½í Š14ÀÆ´ sÎÀ—ŸÆ&&¹A HY_^Òf Æ8/'cCM*]ä%êOL1'DXyúÈù¿-uß9F¢·‹)Ö‹Iò·sTÝö¾P¤ïši¾Î.fGµŒÛbÌü§äìZÞkÆh‡íÕ‚˜ø¿´6æ••–¶œñµá6ª­t—%¶b·ë±VÀ/Aœ¸¸à|(ˆóò ‡‹÷‹ÅdWŸ^i«jtðÙ¯Mô½’‰/—Ga*íX}E#§õGßhbÿwÇ:|Þ|<‘àdK˜…™"ÀÂß­µ‡+T„ßp7U¿~Û®²S x*áñå|øË©¯_̤©Èý9öñ[~m7UŠÙT ý\L9¾ZÌÚ2°»DÄz9ycüP_Vw\Lô÷†k’¤\3XÆÁ<+·[)ý©®Ö4©ûФ6[/ª¢v“ §Ù€Ãl$ßfåö(AGÒ ZÆø¡rÒ=YöKuÎónl]ºôCž}ö¹–\ƒæÞ{ïá©§fŸ—ÏŒ;I“®G$³}ÛvV¬XqÒ÷>ú(×^wíÉâÁ¾},z}Ñi=—õH$ÜqÇ\uõUìÞµ›/¿üÒícÏG|}}‰ ©ICUUÉÉ)ôêÕë$AQ*•R[[KMM-[¶láºë®kµ¼b¾z; <‡çæV¾_-pMT9ý{(Ø®pë˜KÒeÑÅÿmÖÑÃSD‰®ãAúÜû(³™Ìåï¹}Ì_[ž»3Y+þÍÈWÞcÏ“÷¶ú}Âí÷S{h7æšjŠäL Æ¿HÏÞR1àþ’»§ŒØá„õ¡´´}Q­C6¹GË¥±\×áó}4ÞI¸ÂÅ¢l šSèÞMÉ» ¸t"!ãn¡vËšvýÆÞL*¹ˆÿ/ãËÍ-$pðpúÌx”¢5_Sµ{[—^ßô&éM"‚•.&F¸øíj_ˆ¸:^L¡¶v¬ö‰3~¨œè`{2¼ôµ ÇŸú_XëHSiëƒü÷e"æ&:y÷¨ÿîµsã8‹ ¤¸ÌÌÚ]&RòíçÅ8Gmmíq1ñûïWóÈ#ÑØØx^?ÌèÑ£˜õÏY8N^}íU ȱôôãy>úè#>úè£ãŸyäŽKwëØsYo€ûî»^½zñêÂ…”””¶ëØó‘¤!IL¾ñF.\ˆL&cÈÐ$öìùãÏÉœju"‘ˆ%ï¼Í¼yó©(¯`ìØ±””¶þ&¨  Ïn0*,tÏ’¹¸:ÜÀDU=KsN=¿?Á„UKË@×[JgÿOŒDäA¼·vVËqrþYj»{MÎj€+“ŒÔ–䢵µœ¿ÄyÙÁÃÂÎPߥuùÂCÌm=s™—ÝõÖ§3L”i‚ù%KG³”ÝQÄ\ˆ´5æåææ1{ÎSÄÇÇÓÔÔĘ+ưdɪ*«ZMoââöÝ äkZΗ¢¾øãž•´Ãáhua© ¡/n×¥²²’Êü–÷A’#Úí2 c«uñìÝ‹¨v¼Š***j±+ÉÃOÁ0bÛ×¶ùBÛ m+´íÚ¶ç#çýSµŒ˜Åw)x¥¿s…•‡ˆ;$&ïð»×SíÙŸ—¶Ä0 FÂ’™*úEŸûf²;é°˜èÝ3ža/¾‰&?‡Ò k/ȉԱåï0ø©ù-E¼‘—#Q*©;r€Ü  o¤„—~µñXŠ„>.ös¤p¿m‹Wb79rc·0>ž˜¤0¶-;Òa1qt€“Õ#í”á½|ñ)ÅÄ¿hÜ»©—/—Žo÷ïm=jÇé1íò>˜Är9ýf>IèÈ1yýiŽ¥žµk\gñm¡˜»vK°»\<{DÜ!11ØÜ­dòhÉù6^ÿÞÄ®cÖãbbGy;WÌÂþÍo™~Ù¢áã#žäY}¸í*>þ—ïyq?žÍ››ËüúëÏñÃ߀——¯¿þ¿üò3 ÌC$1xð`–/ÿ„?^ÊÃ?ÀĉùþûÕ¬Yó#3fÜ}¼ü¶Ò»šüü|æ½<ŸÆF5ryó–ÅÖ‚xü…dïÞ}í>öl×[©Rqõ5W³xñbŠŠŠq::çó‰];w‘››Ë²åËxÿƒؾmééé„„„òÖÛo`³Ùø÷¿ßçùçŸgåªoQª”üðŸ.˜gÞ¶*9ÇÔRnÛGË  ”ÄüZ~n¶†:\"rµR¶WËq"Й¨“pEh듀ɑV¶ThÚF1Ez1ãÃ,]ú;ÿèm¤D/&Ã(\ü6hkÌÓh4¬Xñ9óæÏcéG°g÷²³²ÛLhç­…¢§RÄ«÷( ÓÛ­2²<_L–¾s„¿òÿ|D¯§ÞbåëácåΫä4éaÅF³ÛþE"^Jž*ðV‰ñTˆðRºh2ŠÈ¯p`> ;ª½{Æsý4$JòV~‚¹Q}Ávdcy)ÉÄÝ|…kW6/´ƒÃˆš0™ô÷Ñ+RÌS”|·Ã±’¦Åß–ŠIðtñzªE¬­toëlS±»ÊÄÈ[ûsð‡Œ6óyªykŠSN^i/ ‰‹'œHDðdª¸ÝAê¶­%ìú;qšM¨“ÛÉü—ý6n¿RÎå}-lë1–¸[î"ÿ‡¯iÊ>vN¯÷ÖªŽmqò…§nQòåf+5M»Ä59à‹b1Ï÷q°(GBÉ‘jä—F‘Y¶’Ún‰Åb† ÆŠŸpà “O²P|£ÑØjzVVV—Ÿ›ÁÐØê«¯¿Âßߟï¿ûžâââ6óßxãlÜ´ ‡ÃÑîcÏv½{öŒÁd2ñÜóÏÃÑ£)¼÷î{ç´Þ]ËåbÙ²å,[¶ü¤ôÚÚf>0óøçƒrðÀÁ ö¹—ª–Rk†O/iŽ­þÓ¤‡ÄÉâ¡F>ÏSPl pᱯ^Æc}Ì-Äâ(J'MgozÿßR¯ 5°§N†ÉÑù/àïc$£IBJ£Œ8_áÚŸŠ¶Æ¼-›·°eó·Óÿ³§Šmƹ]²¤7D@@@à"à¼cBÄÄ›ÈP»X\Ñùæ’oÞ%öá¹|ðK~²04AÌ›x°áF /•oÞJðV‰ðT‰ñQ5 Þj2/ fЛ]Í. :'¡¾bÃÄ4häU9É)sWé ¸ÆÕiuÿ»X±cÚüœ‹¢3×ìÛMü­÷8d )Hzj>©ï.`êå2"$,øÖHk1Cò "ž?&aJ„‹÷;XQ,¢Ä Bg?µpW׈Ýb㲃ùã›––z=‡÷ :•;1 IDATfp(GÖecÖvÌœy\ˆ‹ûc¬(„TMÇ'êÕëW~óØtÙ)í:vmª7Ãþñ½ªj8°ð™óº>(üƒNŸQ†ò2œöS¿Å2déééØímoÏÎÏ/8.áççV«¥¸¸øøÖÀ#†Ó·o_þóŸï—B¡h5ýlŠqï=÷ÆË/¿Ì‘ä#­Zc( Æ^uOÌšÕîcÏE½½<=Q(䬸ôS*++™=ç)®»þ:Ö®Y{Îë-ÐõT™¤<DÂÜAF>ÍSb²Ã3L¼ž¦Bo t¢¶ˆ©·ˆéçk'Ssb*?9ÊÂæJÙY¯ÏÊ9÷6ó^–G§–ûd¢‘ä)ij©pÑÏ%r ¢«ÝÎî0æ ‚¢€€€ÀEÂyû„–šE|Q¥Æ®™0[ë*ÑÝKÄM÷Qùß/HÎw’œobüP)C$L.ŒVõZ(©ua4;Ð[\-.|üÂ)(®l£äæE}D ˆØP }¢$\;LF 1Ur+äV8É«tP¯i_/V!ñïüð5I³çqÙ5Tÿú/Nu‘ZËÖ›O{ìºJ›E¼¤^. y:9zy:È7´ìkõ¥:,æRÆÎÆŽGšõ›?¯Î ðJ ÜÅ“ Nj-ðÏ”ÎéãUk?#êŽâ4ë1ç¹uLð•7à?rÕüʸczŠ9V|~n^»r”*µ“².”òS¥˜çú8ÉÔ9É׋ɨRHÂ¥QDzŸ‘ïÌÿåòw>Eîãž¹FÅ®­¤xêÀ9ãÇcݺŸÛUQšûºu?óþûœ”6mÚÔVÓÏ>>>ˆD"4 ÕÕÕ=z”„„„VŵñÆs`ÿ~ŒFc»=õÖju”””‘—×|OïÙ½‡þýœÓz œ]ì.¯¤zðH3‘‹’=…F¹Ø_'el˜í¸ ¦råáde¡ò¬×%S#cDƒ65tŽ ùt?ÔÊNLºçíS:WmCôÿìÝw|Í×ÿÀñ×½7÷f‘=¨Q+¶ {¥¥(ªè¯´ßVŠÖªUZFP³ÚRªhljtÙ­Q«"‚˜5‚È+SöN?­T7DDò~>ypÏ=÷~Î9ŸsÏù|ÞŸõd’ø?ÎÝûaÛ´½áRѽ§þpÓ /×í;znßÉáH`îk•ROÍJ*jVRñBc5ïv6ÅÄ$÷òè+·t\¹‘í8]¾—\çû 2·äÖÁâ $V´Ð£×+HIvúÆ¥Õ?ÐÄ­ CZ‡³rOqZ£?‘¡à§ë ~º=5-õÔ´„ŽŽzjWД¬çjŠ’XM*Ù¦z¢2$G§rvg]>hEÀoiÛ¯Éc=xåµçt¼RIÇŠ0%Á©ÅÛÇol\Hõ÷>çæÏ?u«À|æÏU§Ê#I >G˜ÏT*UªÌê2ÑÔô¬lBn?[AE3ž¾í5x¯M+‘åÍ R𻞷soÝI°ÿb &FÉkffFÍš58[0'G‹ F/ëìÙðòêÇÞ½ûÇÓ³=ǘ^”ï~Tnîî¼Ú£³fÍB­Vãîᆿ¿?666†2(•JzöìÉdoo£>[ʆ““nn®\½LÛví9{öÌS-·x:–\1C©ÐKC”gãLx«F¦J32u zUÍdoÄÓÛ¬_jÊìfiœ8úøÅñSÙwÛ„«ÉLB!J3™©"j×j Oêõ«dÅF=±åhu ‚oë¾/Pcm¡àyg%µ*+éÓÞ”êN ÌÔ ¢tDÄé Ó× ºÎkd+͉8´‹ˆ‹H´²Ôãd­ÂÑFA%%ζJ­8Ù(Éʬ,=z„Eê‰È!,JϵHúR¶ßò¼³‚6 LhY7ÓÁ—˜¹ññnT›¥ 6KÁ‰x€ÜÀ^M =5,ÁÝ:î u˜©àj \IN'tû7¯Îé•'swø‹¸Mm¡‚Ïêé8Ÿ˜{ ö“r}Õj˜JèSÉN|0˜Sé•~XÖn@ÄöUdÝÉÛç—ü™ÉÇÿgÆ¢?3ˆ|Ìÿšj ³„l5¤«9ÿÎ,±¾˜­W°(¼h™~éɬˤk¡XÕ¨eTÞäðÐBß÷ôloxJîý:Ä‚ßñá‡ãŒ.Wpp0Ë–-ÇÛ{"ÖÖÖøùáàÁ¿ L/ ~‡S¯n]-^„^¯gÇö\¸p''g¦Mÿ’áï ]ûö„††ýÐÏ––rggg3wî·Œ=Ž=Æî]»ÑjµO­ÜâéÑéÒåÈÑ5/:gs*NE=+¿^3{jeÉÑ+Ø®æ½Úé¬ 1äï™ìšÊ7Ô„¦È.ŠBQÚ=³³µBQrÍáëPóýÉ\™9ªD똘¦ç\˜–saZî]*mª†çêס‚gLÌ-¨ñ5tÁØ·UbÞÁ‚è-щ¯#*AGL¢ž¨-z=8Ûªp´Ràl‚“­ŠÌ,=w’tÄ&Âdá9Ä&Cl¢–¬»'dÚUTRÍQÉóN*Ú6PPÍQÅ¡ZB£´¤éµÜz ýÀÁZ×WÓº¾ŠÌl8¢åÛMé¤e>™hgXš‚°4¸¤²#22‚ *=5+@ =ÿg£EF5·G[vPŠ‚a ng<ù¾ºd*.ã¾áò¬Ñè²rm–µQíÍáÄßÏõ5s üìÜ-L|ÛœÓHL)¼¬-ÁÉZ‰“'k%Î6wÿo£äf¬ŽÅd<ñ³_=ê(P( ðº¶Dûæ¥d.õ¼^EËæ'pŸWÿÏFÛwuêÔ‰ ¾ }éÒe,]ºÌðzܸ ÿÿòËiù¦ìܹ‹;w=ð}¥?iz½žåË—³|ùò<éÑÑQ†`"Àa__ûúõÙÒTîË—.3zÔè>ÿ´Ê-„(ÇbÔ ¬“A öF¨ŸzyŽÆhø Au*æläÙ…& õ¬tÔ·ÖboªeÛu áò0!!„â™ ‡ÿŒÙ©ËL#òÏõÃÐØÚsmÕ×hÓ~iîÌéÌhÁGKÓ1Sëq¶Qáh«ÀÙJ‰³mnÀÐÉZI¶bµÄ&Á$Wnæpä’žØ$=Z­ž¯™3zÑ“(îjÆÔuOçšýí·•|RWK`’ž°Rܾøb¼ ôBñŒ‰ÊP’­UÐÜ>›‰gJǽ3ׇš1¢^:Ÿžª˜ïû*u­uÔ·Ê¡¾•–ç,t'+ IVq$Jíty˜Bñ¬€¢‘ÒB1®Žzó÷4Ö¶hìQÛ;ç íœ1u¨„ÆÎ }f:&±dÅÅ’KÒ¥ÓdÇǃ^¯Cif‰‰™9 3KL,,QšY`bnʲ"¦öÎ(L-01ÏMW™[ ²°D¯×“u'Š;w‰ùÉÖBDœ–ˆ¸’i“›±:nÆê8z *U²'!.’êJª;)é䦡º“’„T-陨'"NOLbÑ ¹¯³‹xEr‹ºJZ×Ï}bóélvžÈ&æ}ål U=ûÿÉ–FB!„B!ž€;C155•ÔTذq=¶¶¶lܰ‘°°¼Ï=µ²²")éß cRR2ööv²–„('æmNcª—/}ôǃéfÊÆCYe¾­²³åli!„B!„OG‰ßؤßÛ^ zo0žžíiÐðágêåÆyB”©°þ`:÷1}¤Ïwp3áæ=7cäRg!Ê2¯w¼øù— üúÛ/¼ÿþ°|óty¹ kZËÏ¿l`໥фBÆH!„ŨÄÎP´¶¶F¡€„„D"""8}ú ...\ ¼dÈ“””„µµ•ᵕ•‰I‰ù~_ÏS©’m©lT{{ÛrבÊ[¥¾ONšn'f2¸‡%»ÿ13ús¦&zú´ObÑ+*U*õ½-3ŽÅ ¸ïÉ,„匑B!ŠC‰Ý=ÜéÕ³'Ó§OG­VãÑÔ?¿#ØÚÚŸ@PÐU>þdµk×&!!Ï<™;wn¾ßwíZ8–šŒRÛ°‘‘å®3•·:K}Ÿœ]‘з†ç­ñ4î!-ô2eýÁl"#Seý Q†Ý»'s||NNŽ@á÷d ÷d–e!„1R!Dñ(±€¢ï!_ê׫‡ÏRt:=Û·mãüùó8993köL† Bbb"Ë—¯dÊÔ)h4&ìÚ¹‡Ë—.ËZ¢úýH£{šq뎎ð¨Âo}ÐÔE‰N—¯Ë¥ÎB”urOf!„x<2F !„(%PÔëõøø,ÅÇgižôèè(† bx½oï>öíÝ'kFÁ;2˜ù®9c}ÒÉ)ä¡ÍCºš2ù§ti°bà㳄?þ””””2U/¯w¼èÙ³J¥Šýûö?0¨T*¼¼¼èÔ¹¾‡±bÅJúx‡^½z‘––ÎêU«9xð`©*·‹‹ :vÀÃÑ#FðÁ£y¥Ç+yò9âÏŒé3ÊÌ:í÷¶•+WfÚ´©œ8y2Ï-T ÚB!c¤Bˆâ¡°wpÔ+ @Aî¿þ-ŒN›MÅŠ6O­àæ*Õu.•Z©Rårw¹dy«³Ô·äØUT0¨«)“V狃5DÆéÊ)Õõ=y¶ôßC±víÚ ØŸÉ“§°iÓ¯ :œ¸¸¸gºÿzx¸3dè`¼'M1Üsoñâ%yî¹ðþûè[·.?ü°ˆððpt:nnnŒ=‚I½177gÚôéŒûhwîÜ)å¶µµaò”Éø9JŸ¾¯óÖÿÞÎ÷»FŹsçðõ=\"mžœœ€R¥6*oQ·'î¿'3Àˆùuû6Û·m7äiÑ¢9Ý_éδ/§çŽš–¦ß7?ð}uêÔ‘­A!ʉàààgvì+N2F !„ÌQÅÁDV›¢4‹KÖ³3 ›‘=MY¼#3Ï{.U•T¶S²ýX–4T1èÚõeöìù €={vbnnÎÖ­›4h(o¾Ù—cÇŽsàÀAúöíCãÆ˜:u,Zô=ÿû_?^}µï½7FÞ=±d‰Z­ö©×˘{î™™›Ó¹Kg† }ŸøøxCz­Zµ8qâÑÑ1;v WW×9KјrÇÇ'ðч㰲²¢Oß×óýkkkÜÜ\Y²äÇ2ÑO‹ûžÌOzãM!J#…B¥4¢´ ¼®%*^G¯6y¨ ~Ù”_eJÇd TÒ¢EsŽ k×Wˆ‰‰áµ×úÌñã4mê@›6­¨[·.&&&¸»»qìØq\\\<ø=>ûl<¾G½zuéÝ»g©¨[jj*ññ lظž5k×pÔßÿ{îÕ¬Yƒ´´t&yObÓæß™8i"fffܼyƒæÍ›aooOõêÕquuÅÆÆ¦Ô”Û½{÷b×îÝ¥"¸[|ù„ÏR¾_¸ƒþ6Ü“ù›o¿ÈsOæE‹âwØOîÉ,„wÉ)„¢8ÈŠBˆgÂþ³Ù¼ÓÑ”¦.JN_ÕñZ5Ç/çZ>ïùccªÀÉRõÐ|z=\KÌ!û!Ï«iÚ´)çÎ'''ÿKÇOœ8É»ï BK4 'NœÄÍÍww7Ž=F‹Í9pà !!!¬_¿‘×^ëÍæÍ[KM›vϽ ––˜šªYêãÃÍ›·øìóOéñj6ý¾ WWW–-_Jddiii¤§§•šr?Œ©©)/uz‰Q#G•™¾/÷dBˆÇ'c¤BˆÇ%E!Ä3cÝÁL>écFFvÍëªøvSF¹m‹M}°5W•÷Ï«éxû&š§k×—Ù²¥àà_RR©©©ôèñ ÇpåJmÚ´¦Q£†üðÃbªT©‚VûoÔR©T¢V«KE[ÝϽˆˆNŸ>ƒ‹‹KžÀ\RR2áá7¸r%È= ®I“&¬X±Òð€–E‹>ÒY‚OªÜÓµ[WŽú%55M!„B!D±yf/y6â¹1Bˆ2hîæ þç©aå_åû¾‰Æsó>Ô›™™Q«V ó¤kµZìííQ*s?p‚úsäˆ?gΜ¥U«–ÄÅÅ“‘‘ÁÉ“'éÔ©#µk×ÆÆÆ†ÿýïMJE[¹{¸ãíí5ŽŽx4u',47(hk›{ùrhh(ÎÎN¸»»aiiA{OOBï ÚÛÛ3ìý¡ääè AÇÒPîB'x¥’Þ½{³uë68„B!„Åê™ (êõ²ò„(ôzønKqɺrÝWã²Îô¼/¾è‰¯¯ßéþÍ¢EßS§NmŽ 99™°°käääpåJ'Nœ 44ŒåËW2{öL6n\ǵkר´iK©h+cî¹—Íœ9_3|ÄÖ¬]Krr2;ÿÜ @ÇŽY¼d66¶L2µT•»0ž/xBTT” B!„Bˆb¥°wpÔ+ @âîi #NÿÓi³©XÑæ©ÜÜBE£ºÎ¥²Q+UªLddD¹êHå­ÎR_©oQ<{»ÔÖ÷Ûoç0oÞnß¾Å!99¥Ê¸KÞŸöö„BÈØ'„B樢“{( !D9÷É'ŸK#QŒ\\\èбnŒ1ÚîõŽ={ö@©T±ßþ,s/O¯ V«9pà K/@¥RáååE§Îð=tÈp_OñôÖIŸ¾}èÓ§Í¿éz¹„¦ÄÖI~yº¼Ü…þýû£Ñ˜°k×Ö¬^# -ý¥ÌŽ«ÅÝ&|0šWz¼’'ß‘#þ̘>CÆûg|¼mÒÀ;ôêÕ‹´´tV¯ZÍÁƒe*‡ó!„BQ,lmm9j ñ ØÙÙÒ=<ÜiÛ¶5#GŒfØÐa¸{xÐÄÕ5Ïg[¶lAÛ¶­;f,#†¤iSZ¶j À!ƒqsseꔩ¬ZµZú)¯gggÞx£Ÿ|ü1ƒ ¡~ýú´hÙB»ÖIAy¬­­4hS&Oaô¨1´oߎFJcËo¸LŽ«O¢M.üîÝ^1üýùÇŸø:$ãý3>Þ?‰6qssÃÓ³=£FŽb²·7ï¾÷.ööö2•ÃyHŠB!„Å$>>>Çž={ò¤]eÒDoâââÐh4¤$'ãää̲å¹Gµccïðý‚ˆ‰‰%66–L5¦˜™›Ó¹Kgf̘IXX:Nú)¯“¬¬,²²rÐéôwÿ´¤$§Hc—À:)(Oݺ.^$,,ŒØØXû¦~ƒÒØò.“ãê“h“ûY[[ãææÊ‘#þ2Þ?ããý“h“ZµjqâÄ)¢£c¿Î±cÇpýOÐMæ¡ò1É%ÏB!„OXjj*©©°aãzlmmÙ¸a#awŸ$þÙ§¹· 5ä¯Rµ Õ«UgÞñï¨]§6iiéLòžDÏsúôæ~;—ŒŒ iا´N²²²Ø¼i3«Vç^"¹sç.¥QK`”§V­š$%%¾+)){{;iTù —«qõqÚä~½{÷b×îÝhµZïËèxÿ8mâææJ·î]Ù¼i3–––¸ºº)óP9œ‡ä E!„BˆÒïm/½7OÏö4h˜{Ô:>>>O[;;&NœÈüùóÉÊÊ¢‚¥%¦¦j–úø0 ÿ@45=^í!ù׉““3½z÷dèaxõ{‡:uê䩇4f ®“üòü—ÜÓRúKyW¥Mî155å¥N/±g÷é+å`¼”69qâ$ÇX¶|)_ŒÿŒ´´4ÒÓÓd*‡ó…B!ž0kkkll¬ˆˆˆàôé3¸¸¸<ÏÒÒ’©S&³lÙr‚‚®¹G¸ÃÃopåJ©©©øò¥ZÕªÒ¨Oq4lÔ€ÀÀ@nÞ¼I\\~‡ýpww—F-uRPž¤¤$¬­­ ù¬¬¬HLJ”F•ßp¹W§MîéÚ­+Gý’šš&}¥ ÷ÛWV¬XÉëÿׇ‘#Fcf¦1œÅ'óPùš‡$ („Ï µÚDþäï‰ý‰âçîᎷ·766Ö8::àÑÔ°ÐÜo[[4 “§Læ×ß~ãÌéӆφ††âìì„»»––´÷ô$´Œl¸?«ëäæ4nܘʕ+cccMëÖ­¸~ýº4j ¬“‚ò]¥aÃÔ®]{{{<_ðäâE¹ ]~Ãåk\}œ6P*•ôîÝ›­[·I_)ããýãö{{{†½?”œW®Éwùòe>úpsæ|ÍØ±cqppÀßߟî”F}Êëdû¶m|;÷4 dÿþÒ¨%°N ʰ|ùJ¦L‚Fc®{¸|é²4ªü†ËÕ¸ú¸mâù‚'!!!DEEI_)ããýã¶IÇŽ>â}N:ÍÔ)Se*§óÂÞÁQ¯P(¹ÿbø·0:m6+Ú<µ‚›[¨hT×¹T6j¥J•‰ŒŒ(Wzy«³ÔWê[T'ÏÞ.ÖÈÄ“”óXŸONN@©R•÷ioO!Dq‘±O!DY˜£Œ%—< !„B!„B!Œ&E!„B!„B!„Ñäš9!„‹ýÀçŸAJJJ™ª×ÛýÞ¦GWP(”Ë–.Í÷{¬¬¬hÒ¤ þþG¥“ !„B!ŠE‰¡{‡ïü@LL,!!!˜jLØq ¼HXX‡}S¿A.^ ”5%„OR©¤Y³f,_¾€:uê0pàÆŸ@\\<“'OâÕW_åÚµk<ÿüó|þùxnݺÅÂ… hܸIIÉôêõ*#GŽB¥2ÁÛKÿ]‹ IDAT{"'OžâÒ¥KO½n©©©¬Y»[[[~ùù®]»ö@>[>ùôc9wî<ó¿û+++’’þ â%''cggWªÊíååž}û¸}ûV¾ßÓ³gOöüõZ­V:ºB!„¢X”X@144Ôðÿ*U«P½Zuæÿ.Ožÿî¸%%%coo'kI!þæ8Xw’yx”ŽìœÂóxxxpþüyrrr36oÞŒ¿ÿ>DHHîØýóϿЫWO®]»Fppˆ!P†-õêÕ£~ýúüúë/†ï¬S§v©(Þ3pÀ@*UªÄäÉ“9uú—/]ÎóþRŸ¥DEE‘ššÊ¨Q£ø¿×ÿ[7o>ð=z½¾Ô”ÛÅÅ…šµj²zõjªV­òÀgMMMéб#cÇŒ‘B!„¢Ø”øCYlíì˜8q"ó¿ûެ»÷z*LIï¸ !ij`Ýkl*PÜÉŒui…æéÒ¥3Û¶m7¼ÎÉÉA§û÷Œ6…B‰Z­Î÷³w¯xfÛ¶í|ÿýÂR×VVVV( ‰ŒŒäìÙ³Ô©S'O`ÎÄÄ„èèh¢££8pà]»våR` VVVy¾+1)±Ô”ûÍ7ßÄÃÃí;þ]wÞÈÛo½»^_îÂñcÇHKK“B!„¢Ø”h@ÑÒÒ’©S&³lÙr‚‚®>ð~RRÖÖÆí¸Uy®& ¥ô¹ Á‰€E¹êHå­ÎR_©ïÓfl0Àö!yÍĮ̀Y³Fž³ O:ÅW_ÍfïÞ}ÄÆÞá7úrô豿ãìÙðòêÇÞ½ûÇÓ³=ÇðÔÛÊÍÝW{ô`Ö¬Y¨ÕjÜ=Üð÷÷ÏmGP(ÌùzÓ§Ïàö­›¼øâ‹\¿q  «|øÑ‡ÔªU‹„„ÚµoÇüù JM¹gΜiÈ_µj&L˜ÈÈ‘#ÜËØ{öìÉdooÙÚB!„B« (j4&O™Ì¯¿ýƙӧóîìÚÚŸ@PÐU>þdµk×&!!Ï<™;wn¾ß,kOQn]½™ƒKUã†ðàÛ…_ïìéÙ?¿#yÒ®±råj¦OŸFÅŠÙ»w[·n¥Q£FŽÉË–-ÇÛ{"ÖÖÖøùáàÁ¿KE[ù>L½ºuY´xz½žÛwp᜜œ™6ýK†¿?œììl~øaŸ}ö)666üóÏ?lÞ´™ÔÔTV®\Í$ïIh4jöìþ‹+—/—šr¦]ûö„††-?!„B!D±RØ;8êsŸÐ©0<©óÞ¿…Ñi³©XÑÆè½ð‚'ã'ŒÏ“vùòefϚìÙ32x»tfÀ€h4&ìÚ¹‡5kÖÈZBˆÿP«‹ïxÐW_ÍfÁ‚ˆ†d?즛‘œœ€R¥6*oQ·'„¢´’±O!DY˜£ŒUbE!„ŧ8ŠBü—…BÆ>!„åsŽ2–RšU!„B!„Ba, ( !„B!„B!Œ&×Ì !„B”r¶¶¶lظžÅ‹—°cûŽ<ïù,ý‘[·n1íËéžÜ!C‡Ðªe †}ÿ÷¼Þñ¢gÏ(•*öïÛÏÒBÓ S±bEÖ­ÿ‰ñ_L 00°Xë`LyT*^^^têÜ ßC‡X±b%}úö¡OŸ>h4j8È’ÅKÐëõ.ËÉÉ‘>ø€ÆM“””̶­[ؼyk©ëOÆ´IAu÷zÇ‹=^A­þ7]”vvv¬[·†W^éiH«T©Ë–ýHÏž¯1oÞ·4oÞìÏM˜0‰×_ÿ¿ß ¼ÄÖ­› i111lÞ¼•õë7<ÿ½÷Þ¥wÕjöîÝÇüùßн{7„F£fÇŽ?Y¶ly¡é¥a,í?àzõêEZZ:«W­æàÁƒ¸¸¸Ð¡c<<Ü9btžßfQ_Orü,hŒûècRSÓ˜>c:-Z¶ àx@Ë›úåŽ=ÎW_ÍÁÆÖ†)S¼¹u;‚ãÇŽ—HùiÝZÆ|0–øøøÇj“‚êŽ^OÛ¶­;f,z=Ìþj-[µ,´MDÙ2nÜ'¸º6aôèQ 6ÜðžŸß‘ß³³³#%%Åì©V­_}5“àà`Žß×Ú´i§g;† Ž^óçÏ¥mÛ6^bøð÷7î’’’˜?.Ççúõù¦Ÿ;wþ©¥nnnxz¶gÔÈQ˜››3mútÎ;‡N§e䍸9н!Ë–-é÷õ¤ÆÏÂÆ@;[&{OæÄ‰“y>SPziî·{ߨ¾››×–Ï>û‚c÷ù666Ï\ß-¨/fgeå;(Š|ûú;wJ¼ïTö«AAÅú»¹ä’g#¸¸¸0tØP/ù!Oº×;^üüË~ýíÞX¾ŸõzÇ‹ ×óÛï¿2bäCºJ¥bÀ€¬Y»†Áƒ•ù:ðÁhvíÞ™ço’÷¤2½ŽûôíÆø}ÓoŒ5Ò¨‡=Ëõí?à~ûýWÖ¬]CÇŽËTÎ/O——»°ö§µüü˾;PJ!Ê îÝ»±iÓ¯lß¾…àîîÎÊ•ËX¶Ì‡‘#‡?ðàÕW{°iÓ¯ìØ±•Ñ£G¢R©òÍ7fÌhvîüƒM›~¥k×—Zž„„xÒÓ3©W¯®!­[·®øû5¼611aøð÷Ù°q=¿üú3}ûöÍÍ×½Ó¦ @ëÖ­Y´x!•+?DzåùŸ=¨V«5jKûäû~PÐU&Mô&..F@JrréÓ¹SgæÍ›G³fÍ Ÿûbüç¼øâ XZZ²aãLLLxëíÿñó/Y¶Ì‡éӧѳ׫899ç[cÊcfnNç.™1c&aaaèt:²²²ÈÊÊA§ÓßýÓ’’œR`êÕ«‹©©9k×þDjj*·nÞbù²XZXâááÎŒÿž92yŠ7mÚ´¦‰«+‹/äû…ß3dÈà^wîÒ™uërçš~ýúÐÄÕ•yßÍcúôilÚü;“¼'¡P(ؼelظžZµj=V›T÷ØØ;|¿àbbb‰%$$S© ¢ÈnܸÁ‘#þyÆ3€˜˜X¾ùfÑÑ1ÄÄÄtSSSêׯDž  !&&†ƒÿ¦aƦ—†±´V­Zœ8qŠèèÂïsìØ1\]]‰Oà£DZgÏž<ùõ÷õ¤ÆÏÂÆ@[;[ââùäsâããøòË)ôîÝ“ÐÐkyò5n܈f͚ѯß;Tªä̘1°wï>C0+ß Ÿ‰šÝ»wÓ­{7®\ ÂÂÂw–-]FÇ—:`mmMhh?ý´FÃÂ… Ø¿»wí¦cÇŽ´kߎwßÈ×s¾!"â6Ÿ}úy¾Ë0 ?ýõ·nÝÌ÷ýÔÔTRSaÃÆõØÚÚ²qÃFÂÂÂî¾—zAªW¯ ÁÁ!œ=s†6mÛpèïCÃÜoçÏæM›Yµ:÷òç;wz…³³37®çݦ 8a˜O boïÀø/&píÚ57ibx­ÓiùhÜGŒó!&&*¾?žÓ§OÚlÒÄIܺu›yßÍ¥aüþ}8CñQÛĘºW©Z…êÕª3ïøw2à”"¦–j,lÌšO¯‡¤¨tÚÇÉ *àë›÷rÅÄÄÄb-çsÏ=GË–-Y¾|Ežôàà`Ãÿ«U«J5˜={;v 11é¾ò$áà`O|¼u¾é…)©±ôæÍtëÞ•Í›6cii‰««+‘‘–+44´È¿¯'9~6ØÚÚñùŸâèèÄÙ³ÿ0÷Û¹¤¤¤˜nÔ¼­×b«øö­ˆThÈÉçd§Ùwï'Ožˆ³³3§NfæÌÙX[[?s}· ¾˜•••ïüáà`_¤¾^Pß-Ž~[ÔßÑ£üî„v/’meeEŸûîq/:Ÿ€““£!:ïääÌÌYÓ:dXžh7`ˆvß;>lèû…^’R–ê|?kkkÜÜ\Y²äÇ2[ߢžÕð¬×·ÊsU G¥ÃQ)cîQšë[Pžºu] ¼hØ;ì{˜ú ”h@QQ¸Y©XiŒË{$Ê„eÁæyÒZ¶lNƒ زåwCš‹KÂïv7@ïÝ¢Es8HHHë×oäµ×zz-O¾k×ÂQ©”|õÕLŽ=Îĉ“ &˜¨ÕøòeÀ€þ˜™›Ó¡cüÉ“çÎ;T~®2K—ù`gg€•µ5ññ |ÿý÷|¿`>»vïáêÕ«†ÆÿªW¯.µj×bÅŠ•T­ZµÐ2õ{Û‹Ê•+3mÚTNœ<É¥ÀK…¦ç§K—Îü}ðo8ÈoöåÐ߇8uê$#G@£ÑкMkþØñ 6ÄÏïׯ_È3î¶MUXy*XZbjªf©7oÞâ³Ï?¥Ç«=8ìëG¯Þ=:dii¹;1M›zpúô™|—¡ÓëÑë‹ÞWsûTد_ý5êÕ«Çú ë ïÕª]‹7nÂåË—ïö¿0llm ˜­Mœœœ ­»­'Ndþw¹;˜¢ôè5Á³ Æ]râþë<Àÿåðï}è×ýÁž;wîðÇ; —I?h·gÚ´/™3çëûXA÷3Õ?ä‡XRcé‰'quueÙò¥DFF––FzzÚCÛ©(¿¯'9~6,Y¼„¨¨HRRR;v }ßèËêU« L7ÆÌ¬p**tFåõWTd¹Ú¹ÔõÝ ¾'""‚””T>ýôcúõ{;ß“‡J{ß}X_üïüñ(}=¿¾;uÊ—Å6ïuž’yMŠ%ª°£»÷"åE»kשï‘ðŒŒŒ2[çûõîÝ‹]»w£ÕjËl}³²²ŠtVó^_77×"•zê[PžZµj’”ôïåaIIÉØÛÛÉÀ(D)bl017oþð[¶lã»ïæçIsww/ô»rrrÐjÿÝ!R*•¨Õê|wzÞ{onnn´iÓŠÅ‹ _¿þ…îL˜˜˜™™É±£Çèðâ tíÚ•yóæQãùç yZ´hN³fÍ÷Ñ8¢¢¢Y´x¡á=Zƒ03-<ØðÖ[oÑ´iSvíÞiHû}Óoôíó†áµµµ5 $$$ÁéÓgpqqáö­Ûù¦PT*•t|©#ööö {hn`N§ÃÞÞž;wîpêÔIZ·nEÍš59þ<Ï?_ ½Ngôº-¨œ÷—'))™ðð\¹€ï!_š4iBÃF  äæÍܳ4ýûáîî^`@12"‚jÕ«çIóhÚ”Š,IJJ‚G¸óÉŽ°xÑâ+õÍ/Ïéå´!Äs#UQ„¼n9ó:u¤aÆXZZÒ½{7lmmú]'Ož¤S§ŽÔ®]þ÷¿7 xðv/½Ô‘ ¾ 44”­[·aee…………Qåݵk7ý¼¼Ðjµ„_Ë{惹¹9dffÒ´Y3*UªŒJ¥’±Žá«Ùshа>Mš4ÎÏó©Ó—_N£{·WèÞí†ÆÍ7 ÁÄ{ùÝ=ÜñööÆÆÆGG<šºV`zAš6mJdD¤ayÝ»½ÂîÝ{èÔ©bÐàÁœŒŒÌLâããyþùç±±±¦fÍšÔuqyèzþçŸó¼øâ Ô¯_ º¼ÜëB?£Õj±³³C©T>V›TwFÃä)“ùõ·ß8s÷òkQºÄßN2:oÜ­¤RW~FìY3ذa#'Oþû`Ë—¯Ð¤Ic\\\pppॗ:rþü…ÓKÃXzϽ INŽÎpࢠºå÷õ¤ÇςƅBÁÜyßRÇ¥ææætèÐðBÒž‹Æ¸¯+4¥ò÷·hÑ÷Ô«W :wîĵkáÏdß-¨/>lŽ7¶¯Öw·ßõw$óÚã‘3‘1Gw!ÿhwAGÂËrïéÚ­+Gý’ššV¦ë[Ô³ÊÂú-êØg¡¾剸}û#ЉI‰20 QŠxŸ­ðXŸ¿zõ*?þèÃÔ©“±±±Æ×÷0û÷xèçBCÃX¾|%³gϤbÅ üõ×^6mÚBãÆóäóó;‚››+«W¯@§Ó³bÅ*RSS*[hh(ñqqìÞµû÷üýòb‡Y¹j%—.]"**[;;š5oFtt 'Ož$##±cÇ0uê4¾œö%C1j¹NNÎÌš=“!ƒ‡à{È—úõêá³ÔNÏömÛ8þ< …"ßô‚tîÒ‰}û÷çIÛû×^Æ}ü!¿þú+çÏC©Trè/{yïöm;˜;ï[’’’HIIA«Õå)Ûý *çýù³³³™3çkÆŽ‹ƒƒþþþìüs'999lß¶oç~ƒF£áÀƒíÓ§Mcô£Y×·É))lÙ¼Õpÿä“'O±rÕJ""nŸðÐö aÕÊU|1þ ¬­­ð?âÏ¡¿ú™Ã¾‡ùvî7|úɧ$%¥!„Ïæe,y(‹B!„B!„ÂhPB!„B!„BMŠB!„x,M›zЩÓKÒBû„Bˆr2GI@Q!„B!„Ba4 ( !„B!„B!Œ&E!„B!„B!„ÑL¤ „BQM›z ×ë ¯+T¨ "„±O!„(GsÔchµZT*•¬9!„¢±µµ-ð=­V (d{B!cŸŒ}B!žÉ9ÊÈ™ÌÞÁQ¯P(¹ÿbø·0z½¥B‰©©¹L„B!„@«Õ’™™ž{tTaÜ]Ud{B!cŸŒ}B!JÏe¬G(èu:ôèà¾Ó*…BQN)(JEÜX‘í !„Œ}B!D陣ŒñX÷PT(•(ä¹.B!„í !„±O!D¹!3˜B!„B!„ÂhPB!„B!„BMŠB!„B!„B£I@Q!„B!„Ba4 ( !„B!„B!Œ&E!„B!„B!„Ñ$ („B!„B!„0š…B!„B!„F“€¢B!„B!„ÂhPB!„B!„BÍäq>¬×iÑ£½^ZR!„(ï (P(U²=!„B”Ò¹Wæ]!dŒ(PÔë´(• LM-Q©T²’„BˆrN«Õ’™™†N§5z£E¶'„Bˆ’›{eÞBƈâòÈ—<ëõzLM-dB!*• SS ôE8ãA¶'„Bˆ’›{eÞBƈâò÷PÔË $„Bˆ6Z (,²=!„B”ÜÜ+ó®2Fy(‹B!„B!„ÂhPB!„B!„BMŠB!„B!„B£I@Q!„B!„Ba4 ( !„B!„B!Œ&E!„(@ÕªUÙµ{'»vï¤g¯WŸÈ2~þe#»vïdÂÄ ÒàB!dî-ÂÜ+s¨BñôH@QñÌ211aðàAÔ¨Qã‰.gݺõø:üD¾{üˆ‰‰••)„¢X©T*víÞÉ€žÉ¹Ø˜¹WæP!„â)îK‘kÔ¨‘¼ÚóU>ýäS.\¸( RJ-Z¼ø¸&Mò¦BK:¾Ô‘°°k\»ví‰-3**ŠÄÄDZµjÍ»ï ¤J•爊ŠfÓ￳{÷ªU«ÆèFS§v-âÙ¼y3;ÿÜYèw‡……‘-+V!žKKK~ßô›áuFF·oßbÓï[8pà@©žËò\|ÿÜ[Òsèë}þ¡C‡æIÛºe+>>Ki®B”Ÿyd옱]Íóþ²åK©Zµ*‹/fÇö?¤ÁD™!E!FCÇ—:е[W (>#yÇ«‰-¯J•ªLòž@@@K—.£}»¶Œýp,×¯ß 00ïÉÉÊÊfΜ¯iÚ´)|0šðk׸x1PV–B”rGŽøó×_aª1¥[÷n|úÙ'ÄÄÄpþüyiœR4—îÄÆò£ÏRCÚ­›·d®Bªk×®yŠ5¤jÕªÒ0¢L’€¢€§g{,--Ù³{/vx‘%‹$-- €)S&S³V-Þø.ÿ{ëM @ÿw`iiÉèÑ£¨[¯.QQѬZµŠãÇŽS¿~}¾›?]»vѺukV,_ÁþýhÞ¼9ƒ½GÕjU‰ˆˆ`ÅŠ•ÀÍÍá#†ãèè€ÿÜÜÝ9vìK/É=žÏrîWÐ2 ûlçÎx»_?ìíí¸ví>>K¹xÉð]Û·ï U«–˜šš±oï^V­ZN§£fÍšŒ9‚ºu]ˆ‰‰eÓ¦MìÞµ›FòíÜoÙ°aíÚµÇÑÁž#þG™7w^¡ËŒªãæ-›077‡Zðû¦ßþþ~Z·Ÿ—rùòe¾›?-[¶Ð¢E ñóóãèÑc 2˜ŠVVœàÛo¾E«Õµ¼ÿ25ÕðÛ¯¿³cÇâãã ᕯРa}ÒÓÓ©V­:3fÌ$ à§N¦s—.¼ð \¼XhÝ…B<}‘†9ùêÕ«¬Z½’FrþüùBçŒæÍ›3ìý!ØÚÚáë{˜öíÛñç;9sö,_ýS¦L%àx¶¶¶lظŸ—²uëÖGšŸÿ;öíóFž:4mÖŒÇack‹¯ï¿— 7qu-°,÷æOc·Y ›‹·nÝúÈÛ…)î9ôaÛ9ŽÜ¼y ¿Ã~y>W³fÍBçz!DùvóæM^ìÐ¥K—‘™™ äoÞ¸AÕjÕ ù ÿïMÆîÏ4Þæ·_ص[Wœœœ û´C‡ ¥wï^ô{Û‹¤¤¤‡Î[·n-t,~’û¹¢ô’{( AîY‰ÁÁ!üþû&ÌÌÌèбƒá½¿ÂÙÙÉpo Í[páÂEÒÒÒ˜9k&fæJ¢è IDATæÌœ1‹+—/3aÂxœ ŸmÓ¦-þÍõë7pppÀ{ò$ÂÂÂøô“O‰‰‰aüø/037§bE+&OžDNNóç/ =#''GÌÌ̺œûÝ¿ÌÂ>[µjUÆ}<Ž£þþ|öégdee1eÊd”ʇ…zõê²bùJü¦ï}y©ÓKT¨PY³g¡Ñ¨™5k6ÇcìØ1xzz>׺u+Ö¯[ϾýèÒ¥3Íš5+tyÆÖqäˆQ\¿~óç/0jäè|ë_¯^}V¯^ƒï!_:uêÄÐ!ƒY¶|þñ':¼H»vm‹Ü¦÷„††²víZâãã111¡cÇ—ÈÉÉáâÅ@ˆ‰@«Õ‹£“£Qm-„¢t055套^àÆCæR[[&Nš@rR ß|ý ÙYYXYY=t:?6Z[[3iÒ’SR™?&*U‘êmì6KaexÔm„Â<É94¿íGGªV«Ê†øù—Œû¦¦¦…ÎõBáwøOÏö˜››ÓÞ³=ümÔø_”ýcÆÛûÇõþ¾8;;Q³fMZ¶hÎ?ÿœ3¦°±¸¤ösEé#g(Šrï¹ç*Ó¤IV¯^ÍÍ›7 ¿N×—_6Üçø±ãdddвU bcc©ß >Kÿx÷¨‘_ϙÅ 9þ<:w¢UëÖ] `ù²eìߟ{ÿ% >ÿì nܸAjj*{ÿÚK³fͨòÜsT¯^ KK–,^B`` ~‡ýèÞ½[n³ålß¶ýúÜ¿LOOÏ?{ñÂE fff¤¦¥1}Ú ¬­­ó|×ï¿oÂï°‡¦e«V´kÛôzll¬™:e W®p<€¦M›Ò­{W6¬ßÀúuð÷?Ê¥K—èÕ«'ÕªU#!!¡Àå[ÇÈÈHrr²ÉÊÌ$** ‡ê¿eËŽøáZX]^îÂöíÛ9âw„S§NóÆ›oP­ZuôzŠÔ¦ÿõÞ wyóÍ7Xµj—/]¦ýÝ NoȧÕêѨ5˜šš>´­…B<]}û¾Nß¾¯͆ 8rĿй4==333~üч«W¯ræÌYz¿Öû¡Ë*lÞ+l~þï<˜ç;[¶ÀÜÜœ%‹—põêUŽ=ÆË]_6ºþÆn³„„„8·nÝê‘¶N:U`¹u÷@žü¶söíÝÇÉ“§pppਿ?uê¸ÐÀ;$Ä'Zà\/„™éwÇÞ®ìÛ·Ÿ_|¥R‰ÿQ ìÿÐñÿÞ>¤1û3¦Àñö§µë×ccc>b­[·"==ªÕª±ió£ëVØXœ––V"û¹BŠB”:]»uÀÏïGŽøÑ¯_?jÖ¬IXXЪe+¢¢¢ïæõ£S§Ü{.~óí7y¾ÏÉÑÑ0dfeÒÓÒÒhذ>ò1NNŽh4¹ŸJ¥[;nß¾õ@ùí \N~î_faŸÝ‚ù èÓçuz¼ÚƒÈÈÖ¬ù‰›7oæû½ÑÑQØÚÙbgg·¬‘†÷""#¨U«¦áµ^¯¿»‘­@e¢$¤åµŽÆÈÎÎ]vvNNnYîþ«R©{yìøƒ'hѲï¾û.áá×ɺÛîªûÎQ©ddfZw!„¥ÃáÃ~ìÙ½‡Ž/u C‡¸»VØœ‘”” äúŠ¢8çç{lml©,ùm?¶ÍR˜GÝF(̣Ρ;ÿü“€€ÜËíÒÓRZÿ{Û9ë×­7¤œÀ£©;Íš7ãò•ËÎõBafjήÝ{˜5kUªVáå®]ñõ=LÎÝý’‡ÿ÷ö!ÙŸ1f¼½\Oàü¹ó´jÝŠ”ÔT´Z-þGü®[acqIíçŠÒGŠ¢\Sý?{wU¹?pü3ð﫸 Š ‚Š¢¹–¦f{ö«›K]—Òìf·å¶˜[¶Þ²²EsÉÌR[nu³nZ¹/¸á¾²/²Ã00 0ÃÌï”"Q‘õû~½8sæœç<Ï™ç{ÎwžsŽ…£F`õêU5^;v Ë—À®»˜ûò\JKK9qâEEEäææ°pÁ+$%%U¿O¯/Å××÷²u…‡‡1ãÑGYºô¶mÝÆÀÈÌûùùxyy¡ÑÔ|¢a}ë¹’úÞëééEFÆfÌx _ßL›>§Ÿþg÷¬ðôô"!!žìœªÞÞ^W ‘÷ñé@^^~½e©o}׳×âZ×Ö‡¡C‡²fÍNŸ>ÃéÓg¸ãŽ;ÁO?ý|± =‰‰‰¹èÝ9}´Þm×ëõòABˆ ;;‡#GŽ˜”ÄСC™8iÿ~ëßõÆŒ".Æ8OŠ‹‹k,¯úį–$ܵÆçúbFAAÕ2ÝÝݯª,µ©ï˜¥Þ:¼Æc„k=~¨¯>233ÉÌ̼ªõ$$Äcggǃû{÷ì&öâɽÙ\õ_ÎÅ/–k‹õB¡RYrìèQ²³²˜>mÁÁ=X½j5–*‹õÿµC6f»{Ïžxb6J¥E­—;×+êë‹{ôèÞäç¹¢ex‰v-""WWW~øá,XXý/11‘7D¥Rpøp4z½žþýÙµs7ÑÑÑäææ1yÊDüý»Ò‹… çãéYû=líìª;Ó¾}ûq×]wV}•JŽDA§Ó1ëñYDFF2sæcÕë¾ÚõüY}ïíÞ=ˆ¿ý÷ÜsVVjÊôe˜Íæê‘÷Üs#òèc3ðôô`ßÞ}:xˆüü|Ÿ=‹"øûÔGèÚÕß~û½Þ²Ô·¾«ÙÆ’’R:wéÂàÁƒ®¹Ý¯µNõz=cÇeöìÙ„‡‡1eÊdlll8{ö,ÉÉÉ$'§ðÀƒ2`àf̘Ž-»vïnP] !„h ØüËfFŒŽŸŸ_½1#:úeee<6ó1""ú3ãÑÕ˹p!£ÑÈØqc9Y³f6J|®+:Mii)3gÍdàÀHž|ò *ËÕ³Ô[†kcÇO'55µÖuíÚÏÏ?ýÌwÜÎã³g‘”” €OJJJX´h1ÖÖ6<ûܳØÚÙa0®i=M€ÕõÞ={ö°zõ§Ü}ÏÝ,ýà}üºváÕůRVVVýþ¼¼\fÍšÅÍ7äÛo¾eûöèõzæ¾4—²²2žáyFŒɪU«øý ' õ­ïj¶ñûï~ÀÊÊ’Çg?~Íí~­uÇ¿ßú7]ýýY¸h!ÃGŒ`õêÕÕ÷&yuñkh‹Šxþ_Ï1`K—~@̹˜ÕµBˆ–ãëo¾Å`00yʤzcFQQ¯½ú:.ÎN<ÿÂ ØØXW/C£)båŠugΜ9ä\|ÇõÆçºâ`q±–W¿Š‹‹Ï<ûOÊÊË«G*ÖW–«=f©¯ ×zŒp­Ç׫¶ã€… ‘››ÃóÿzŽ{ï½—ï¾û¯¿þºÞX/„—üúëoTVVòë–-vRÛr®¶¿-**âäÉ“u^î\_¬¨¯/nŽó\Ñ2(ÜÜ=Ì …PPõ“êŸõ1Uppp–¢‘8::V;wrr⫯7²îóulÜøU“—¥G¼÷þ»¼öÚëìݳ·Ý¶IÇŽYµz%_~¹žŸú™¢¢¢F_G×®]YôÊ"bbbxýµ×åƒ Ú„âb J Uƒæ•ã Ñ–¨T*6ýô#7|źuë¤Bnpì½Ö*Ç9¢=Ç^‰»íÛó/<££#s_šÛ.Ïs¥P5ê2e„¢-€R©déïñÐCÖ‡þs•••eÒäÉ<ðàÿ‘‘žÁk¯¾Fbb¢TN3JOOgÜØÛnè:|àoRÑB!Ä5Ä^‰¡BÑ0½{÷búŒilß¾;wÊy®hrɳB!•\ò,„B´ÌØ+qWé#‹\ò,„B!„B!„h0I( !„B!„B!LŠB!„B!„Bˆ“„¢B!„B!„¢Á$¡(„B!„B!„h0I( !„B!„B!̲)W6mút&L¸·úï¾bݺuŒ¾u4“'OÆÊÊ’Í›åóµŸÔ9]!„íWCŽ,,,˜8q"·Œº…Ý»vñé§k¤â„Bˆk0qÒDÆ¿ •JÅöí;X¾l9ÿøÇÜ6þ¶óíÛÅ«‹_½ª˜íââ†ëY¶l9?mú©Æk+V~BFF¯,Z\=€Ñh$++‹ÏÖ|FTÔ~i !šI“&]]œ™?o>‡GWOsrrbêÔ©¼ôâK󯛝}ø0ééµN?s欴šBÑNÕuÜð×ãƒéÓ§Ä III‘ŠB!®Á€ Éœ'ç`6Ão¾Î€øðÃøðêç{â‰Ùœ!¡¡Ì˜1½^O@@Qûöqþ|“§LÁ`0ðÁÒ8xð3g>ƨѣÑëKYûÙZ¶mÛ.­/„B´2u7üùøÀÚÆ†Q£GñèŒÇ(,,”JB!®Q^^>,ýˆÜÜ<P[©kÌãääDŸ>¡,_þÉUÇl¦½¾œî݃ˆ`ìØ1DEíÇÖÖ¦Ör©Õj´Z­4ͨiŠ.®<ÿÂsxxxrüø –¼³GGG´Úâêy´ÚbÜÜ\ÑÔ1ý¯ÜÝÝxñ…Ñj‹yïýw±T©˜2åÄÄIqðà‚{δ©Óðòòä±™3Ù±c'&“Iö!„¢qlÀñA×®~”–êyyÞËøùuáèÑc,yg eeeRB!ÄUHLL¬þÝ·£/;uæÝƒï՘箻îdó–-TVV^uÌPYªØ²e cÇ%66[[[ÂÂÃYµr#oQ=Ÿ›·ü@ee%óæ-¢5éCY–/[μ—ç3iâd L¸B­ó™ÍæOONN&%%•ÂÂB’“Ø»gez=ÇŽÃÙÙ€Ô”T,” .Z@ÿþ,~åI& !„mÄ_ìíìP«U¬\±‚)“ÆÊJÅøÛÇKE !„×ÈÅÕ•¹sçòþûïSQQQ=]­Vsó-7óë–_¯)fXªTìÞµ›þýûcmcÈ‘#ˆÚ·ï²ùt:ãÆÞÆmãÆóÂó/ðÜsÏ`cc##D3i²Š–––äääÀÖ­[7ngNŸÆÉɱz>GGGŠ´EhµÚZ§×Û9a®ÑA)t<³fÍ&$4”ýy÷½w™6uz­Y```‹n°N|IKËhw;ªl·l·l÷µ‰—H'Ú”†hµÅ¤¤¤U_6µ{×nBBBj]^cÇýöÚIßݰí–>YÑÙÙÙ±pÁ|V­ZM\Üù¯;†ýQûÑéJ¯)f_Ê”——s`ÿF Ƙ1cx÷ÝwñëÒ¥öó~³™Ó§Ï Ó•âëÛøø„&;¯oÏq^êåÆÖIk_ê¥}ÖÉÕŸ7”¥4³Bqc˜•8>ù.ªÀ°ª¿JV½ŒÂÖ» OPòÅTd%c3v Xª(ýþcì&ýïP» ÏN¨GLh²“!„h©,:v»¬¿4•¡´wªú]›fÑ$”𥿬¥2+¥Ö>VÿãŠQV o?ìz _VÖ˜Š?~,,±›ðJGPÛb3üJÖ.nürvéYëú-ƒú¢î?ŠÂ˜ÃX¸w¸XŸŸc3n –»ÕXÆŸŠ6ã§‚µí_ª™òÿÁT-±W!Z¥TBqc¨ûÝ‚*0 ý–uh>„¹¼ëQ»âûÊOE¡ýø9Ì¥Ú[v À*d0†„Ó”þ¸s¹¾ê„袲=ÿ¥pþ˜tE¨zFóÌØª“¡˜#¿÷„4ˆB\ê/w~‡æÕ‡1W”Ê ÍëSÑoÿ¥£–þ½kÏ_úØ–RÖÊ”³h—öJeU8¹ãøÜ'X8{T¿Gac(ýþcœžû€Òï>º!å«kýÆs‡1éu¨zF¢t÷Á˜‘ˆÂÆ€ŠÃ¿cÎϤ²  ÏN5–§ÿßlÆ=\NQ¼ì_P®—Ø+„-ˆ$…BˆĬ/@ic °èÚ …Ò³¡ââYÏU†á‹Ë+ùüU*ÿ6˜åX… ‘ÊBˆvÊ*|$Δ|µÑm¸¼ýÇC¨TÁþ8ñ @eFBÓ­ßXáäT}oF©²¢ôç5˜¶õ‡»ÿ­Ál2¡êFñŠœL”Ø+„MGŠB!Ä b8…ù¶©ØÞ7CÂi¬‡ÜIʼnݔmû ëá÷¢ò FéÞ“&÷ŠË+?…õíÓ±¹÷ ,|»¡ Œ);•òz.a3éuXxwF}óý”oÿVE!Ús™«Ð!X…V%¹Ì % 7lÆLăÂÒ›qSqd+æÂÜ&Y?@ùѨ^¼øè*æ»Åîþ9ÎBRçrË6¯¥l³Ä^!„h©äŠB!Ä Ry!‘’σÊëAã¨8µ—ÒoS™CÙÎÿ tõÆ2dHõhŠ+ž´å¤Q²zfm>ÖÃ2ô¿¯¯÷=eÛ6¢°±GÕ½¿4ˆB´A‡£ìàoX„¢t÷ÅTVŠ¥gGl'<‰B¥FÿýG”þç#”jlï{²ÉÖ`Œ‰Æ¤ÓbÌHÀ”“JeFúÿ.Cam‹ªß(Ìå˜ËË$ö !D+¤ps÷0+ @AÕOªÖG7ß<ÚëãØe»e»e»…h=Š‹5(-T š·­O´çϱôÝBˆ+ž|:8ãøÜ*ŒqG1ÊP¾ùÔ|ò‚TNÅ^9—>_ê¥}ÖÉÕŸ7”\ò,„B!„â†3銩8º«cQXª0œ9€îë÷¤b„¢’„¢B!„BˆNaªDÿßåèÿ»\*C!Z9¹‡¢B!„B!„¢Á$¡(„B!„B!„h0¹äYÑ´€¹uÕÂʂΡ^t óFemIy©ôÓ9dœÉ¡\g¶B!„BÑ.IB± ñí剡̈±Âˆ±¼c…Cy%••˜*[nÇÚAEY±$gÚƒÀÈŽxº’p ìø‚–¹?Ú«èÔÇ›Î}¼ñðsæÂÙ\ÒNe“›G‡îtèáN¿»ºSRPV•\<›CAšVW!„B!D»! Å6bÌS‘”ê±q²F¥¶ÄÂJ‰¥ÊKµ–V–(`¬¨ÄXQy1ÉX•l4VTb,7¢Í)¥´¨Œ¼äB Ò‹ohYmÕxusÅ»›ÞÝÜ(Ê*&ítñûÓ¥!Û°›&…b6›ÉŒÍ£ÏmX;X·'…¸}©Ížð¶w³¡s¨Ã|°wµ!ã\ñûÓˆúòdù2Îæ‘q6_¼ƒÜô`olœ¬«G/6÷öXª-èæƒ__¶-?,;ŸB!„BˆÆ=ï”*hÝ”–Jî]0œý_®w””B¡¨J2ZYTÿ³P)±T[b©RfÜý\è6¨#Î>ä&kÈKÖ“TH^²†²âŠk.£µƒ^®ø¹á膥ڂœÄªåÆïOCWXFß;»Ó±·';W•Fmc¬¬ûT$çv&“z"€¬Ø|ìÝlððåÁßJRtq{SÉo‘~Î>ötîãM§P/TÖdœÍçôoñ .CaF1…Ŝۑ„ÚN…w;}úp9‰…Õ£‹sK›¦3ÿSѽ‹3ég²‰?&; B!„BˆÆ?•*h½½ì¸ý¹›øeIÔïçf6›«.ƒ.¯¬sžŒ³ù(”àÚÉ ·ŽŽFv"òÿzQi4“—¢!7±¼ y)Eu.ÇÚ^…×ÅчÞÝ\QY[’“XHn²†„C”äë/{ÏÑM±øtweòÒql[q˜ G‰Ö­s/LèÉŽUG)Õ”Õx­$_ÏÉ-ñœÜO—po>ЀØ=)cÊãîçDç>Þtîã…±ÜÄ…s¹ùáEÙºëZn¹Î@ʱLRŽeàéï‚w݇ö¤ËD“YLQv EÙ%˜Œ3‚±®$bÔú“²ó !„B!„¸aš%¡8}Ætˆ`ÆŒÇ}ëh&OžŒ••%›7ÿÊçk?¯wº€=Ýéw0?¼²«Ñ—m6A~Jù)E@Õ'['5®œpíäH×_Ü;;‘›¬!÷âF“ÉŒOw7¼ƒÜ°²V‘›T•@LŠÎ 8Oß õfÆðŸyÛ<1„N½½8øÍiè:(” ̦–ýd“þ÷cïjÃÿÞŽºâ¼)DzH9–…³þ:àæƒó>Kâö¦¢Í¹ú~Žž¶8zÙãâc“·Î>ö8zØQxAKÆÙ<ö~~]¡þ†m{Nb!9‰…œÜ«5^ntóÂÑÃ;ÊŠ+Ðæ”P”U‚&³m®Ž¢¬’†uÚÕIDoÜ»¸´É$b×®~,^¼¦ˆ'žxR>ðB!}²B!Z˜&O(öîÁ!7a¨¨º„ÖÉɉ©S§òÒ‹/Q\\Ìo¾Nôáä§gÔ:ýÌ™³í¾Ñ‚†v¦SoO~ÿèP“­³´¨œÒ¢ÒOçTOsëäˆKG'ºöÒD~ª–_¾îK<£ÖŸ¢kî[4‚í+P˜QÜ"ÛÁÖYÚΊ²’ ÊK*ý¾y–j ì<¬èd뉃›-öî¶8¸ÛbïfK…Þ&3©§rH?•CQvI‹©¥…‚±O "õTg¶&^Õ{5™Åý1Íé FÎèOiQ±{SI=žuÙüv8ûØá|1ièä퀓·%yz´9:´y:r ‰?†6»³¹é“°º‚2 2H<œQcßqp·ÃÁï@W";âèa‹¥¥%™îeU%‹²K(ÊÒ¡/.£k?_ºöóÆ­ó¥$b:QëOµ™~mÅŠå<óÌs”””0qâCüúëolØðU›Ù¾¿~‘6qÒDî¸c÷M¸ûî»+«?Þûçcr6l\ϲeËùiÓO5û¢•Ÿ‘‘Á+‹WÏ`4ÉÊÊâ³5Ÿµ_:m!šI“&U*³gÏfù²LŸþ÷ªäXP7Ξ=CRR{vï¡Gp0¶¶¶µNÿsB1$4”3¦£×ë  jß>Οcò”) >Xú`æÌÇ5z4z})k?[˶mÛ[eƒù uG§/i‰„ü4-ùiZâo@žIfl>7M %åXúÄ–Óöî¶„Ü€“·=–*%j[j{5•ÆJÊK*(×U /®J2^úYvñ_yI9e%ÊJ*0MعØ`ïnS(¬þéfƒB©Ä «Ä%ËšRMº‚2r Ñê))ÐãÚÑ‘ÝÝ>5 ¥¥’´S9¤ŸÊ&;¡°ÙêÆ+Е[fE°cU4š בä4CÂt¤ãÑÕÿ_"ÿ¯'¶Äãåï‚“·=NÞöç–R”SBqn)YñÄE¥QœÓ<‰Ã«Qª)§TS~Ù“®»õ @—W5ÂÒÓ–Áî8zÚQR §TSÆùýéìû²í$/  ;;›’’ª}ÆÓÓ“M›~¢²²²Mlß_¿H cðàHŸõ&S%o¼ù&!¡¡ØX«<8’9OÎÁl†7Þ|pèà!Dë4ô‘02cs‰ßŸ!•!¤Onfu bøó¹E]ýó©“'™>}AAA,\°”””UîºâMŸ>}:t³Ÿ ¯,^ÌÉ“'ÉÏÏoÑå0 ¢Ùâáõ”»¾ý§5«¯=®ô¹ðòòâþûïãé>ƒNWÊâW1 â²¶,++cܸ15Š={öÄÃÃŒŒ?b¨N§cÂ}÷£T* ëÃó/¤BY‰¡ÙÊãâë@È­¸vt$fw*ÇŽ«ùaR+QÛZ¡¶·ÂÊV…µVv*=íðèꌕ µ½ µk;…™%XÛ«.&ËÐêÉŠËGW˜Ž® Œ ½ÿ®$&&ÕZž‚4-iZNoMÄÎÅŸ„ÝÞgÒNg“~*›´Ó9˜›è‰Ã!cðéîÆ‹v6êrs“4ä&i°²QÑ©'bóˆÝ›rM—B·t•&òÓŠÈO+jWh̘[ùõ×ßxå•…„…õá£>`ÅŠU¬_¿ÛoÏßÿþ0VVVüúëo,_¾‚ðð0|ðž}ö_¼öÚb6oÞBI‰Ž'ŸœMe¥‰cÇŽ±lÙ'ͺmµ}‘wž—çΣ°Pƒ§§%ÅÅèJJø`éGäæVÝC6!!µ•ºæç¬~‘ÖZ©íTxwsÅ 7ba©$v<$IHŸÜœ}r]ƒþœ(ª«¶¶±aÔèQ<:ã1 [\¹ëŠ7þþþ>|„œœ\8@hhh“ŒR¼žrçåå_1¶Är×µÿ´vuµGC>TT1™ÌÿURR|ù ¦½¾œî݃ˆ­:Ç;v QQû±µµ©=ΪÕhµZé´…hFM–PìÞ=ÿ>ýt ;v¬wÞºFÕ6=99™””T’“Ø»gez=ÇŽã‘¿?@jJ*J -àð¡h¿òJ­ÉD€‚t-=oîJàÀŽì[ò²I4—ÛŸÂÙí‰ØÛÝNzng2JšÛŸÊñÿÅ ½I×ïàBÈ­Ø8ª‰Ù•ÂáïÎÕ:Ÿ±Ü„±¼ ]aÃö…BÑh#ét…eÄïO#~*µ>=ÜñÐ!Sú}¾€ÔS٤ʡL[~CêhÔãý)È(fÏÚ7¬*ôÈ(Ÿ¶F©TÑŸ•+W0þB>úèV®\ÅÉ“§èÖ­Ó¦ýgŸ}žÂÂ-ZÀ]wÝAjjÝÉžzêé:“ñM©¶/Òt::lظ6nØX}âr‰oG_:wê̻߻l™ýEš¸1º„ù~:‡›Ï6¾Ýn‚óû$©(¤On.Ž ¬PWÿÜ3˜ÒR=/Ï{?¿.=zŒ%ï,¡¬¬¬E”»®x“žžÆØqcøþ»ï±³³#44”¬Ì¬Sßu•;11±Añ°¥•»!ñ½5ª«=®ø¹(,,äûï¾ç³µU—BÿòËfΞ½|´§ÊRÅ–-[;n,±±qØÚÚΪ•«yóˆêùìììØ¼å*++™7otÜB4£&K(>øàƒôíÛ·ºøÏwßòÖ›oáääX£/Ò¡Õjk^3æ …âÎ}Ö¬Ù„„†2 ¢?ï¾÷.Ó¦N¯5™@Áñ2ì½ÍÜþìrΑ~¨ù.!µ´VÒ÷‘®ÄþœÁïv¹£:[¹rî» êB÷]‰ý_æ_g[|#\Á Ù§ŠÈº{üýí›l»¯¹½‹!÷°žÜé8t°Á¿w'úŽïŽAWIAb šÔRʵ úëK.ØyXÑë¾N$nϦü‚ÿ®Í»Ý­ÜØî3Çcuyj;¶ÎÖWœÏlmvÉï-Ú·o_Nž<…Ñh¬õõˆˆþlß¾ƒ„„Ö¯ßÈÝwßUïÉkRR2 ÍŸ„+}‘öÐß&âããÃ+¯,äpt4çÎV}YáâêÊܹsyÿ½÷¨¸xÕŸ5öiâÆðëëCÌîdŽÿï<áwa6™‰ßŸ.•#¤On!êúb÷¯ý³½jµŠ•+VžžÁ¿žŽñ·ç»ÿ|×"Ê]W¼9|8šÐÐPV­^IVV&¥¥¥èõ¥-¦¾¯'¯[j¹ëŠï­Ý_Û£!Ÿ OO/î¼ëfL”ÒÒR,\@ß¾á=z¬æ9¯JÅî]»™2e2Ö66Œ9‚¨}û.+Ã¥Kž ½zõ䥹/1mjÕ•—Ÿ×ÞzèÔÉW‚ÔË ©“øøøV·M–P\´è•êß;vìÈ‚ó˜1ã1œœœxæÙ§ @£Ñ0tØP–,YBæ…ÌZ§_‹áÇ1hð V|²’œœlF… ¥¥¥µ4bÖöV'£bÙ•‡|Ù·þ$9M|o:÷.NŒü{?6½±‡JÃ÷­i £nšCbb‰‰Iøôpgðì¶­ˆ&3&¯Ñ×Ó9Ì›Ð1èµåùél³_~zÝíý§cxg:»ãÒÓ·.ÞXX*ª^rñ_QŽŽâܪß+ õ'ºíDÇA^lzc÷çm–ínÅûyKvçKC±¶oØeG ‡3ˆú²þûs+?üðß:_7TVþ±)•JT*ÕŃû–}¯Ìº¾H›6u: h4Edffrôè1ºuëÆ¹³ç°³³cá‚ù¬Zµš¸¸+ßÞ¢1¾HÏÊV…“·¹IšêiÇ~Š£ïA˜/Þ#Vé“›VC+899ÕÚ?ÇÆÄ’’’V})æî]» i1å®+ÞL¸ï~>ýtMõƒ2>^öa“˜»Þr_mƒNWНoâãš49Ó?MAê¥ýÕ‰es ¨¨ˆÕ«×°`Ⴊ'iýò+1çªFÔÔ5ýjEEí§wïÞ,ÿäcL&X·î‹Z“‰µ9·#‰”c™ô¿7mŽŽýšæ]úzÓs¤??¿µO>…‘“Çw w0ø¡P:ôp#õD6úâ ÊŠË1–_û Ã#}é}k …ÅD–¢l]›«;Mf1šÌ?.ãPY[bïöÇCaºö뀃» ön¶ÊŒ疢ͩzÚ°6§m®ŽâÜR†>ÜCy%Û?‰–²iè‰+€½Uý˲¶Æß߯ÖK_.‰ŽŽæí·ßâ×_#??Ÿø?¢¢¢((( k×®¸¸8ãêêFÝÙ¼yK‹ª«º¾H>b8wÞq‹/F¥RÞ7Œ½{÷aeeÅüóùæÛo9vôèu­ûj¾H7 †‡{“~&ç²éG7ÅÑïî˜M&]ŠÒ'7¡¸¸óuVpqq¦°PCXxX­ýsbb"^^ž„…õáüùó :”cÇŽµ˜r×o.qssã¾ ÷b4šª“?-¹Ü›²ÜuÅ÷Ö®®ö¨ïsq©®ÒÓÒx䑇ñññA¯/%2r ›7o®s]›7oaᢅäåå‘’œRgBQ¡P†‹‹3™™™!šG³$ÓÓÓk¹­¿oeëï[/›¯®é—œ:y²ÆS³^Y´¸ú÷ÂÂB&Oš€Á`àã—ññÇË®©¼¥š2v¯9F—p&/ÇÞ/OtøÆôík''v®:"{h]̵þ$~ý|èwÔöUCQZ()+.§¬¸ý¥ŸÚrÊŠË/&+.þ^ŽA_u9Oá]¹5€ çòØûù t…úvS†2#…Åf\~Ãhk«ªD£«-vtöÀÞÍ–Ò¢2’f’~*GöÃv¨ð‚— »—kA†ö I¯¡ìÞ½·Þy“X½z o¼ñöüöÛï|÷ÝF<ÌW_m ##ƒ‚‚ÂVS‡»wí¦G÷î¬X¹“É̦äÔ©S 6”ÐÐBCÿøv?&&†>õôU¯ãz¾H×Ï/܇Ø=µ?öÈcèo0f³™¤Ãr$¤On*u bðôôâõ7^cú´éuöÏo½õoæÌ™ƒ»»;QQQüò¿_ZL¹ë3räHfÎzŒ#G޲pÁÂUßu‰ŒØhñ°)Ë]ßþÓšÕ×µ}.þ\Wññ lúñGÞYò6VVVlß¾£Þ‡Ä%&&RXPÀ–:¾¸tE“ÉÄ… ™,yg :ßÑ\nîf…B(P\¼VêÒÏú˜* 888·½ Q*ª.y®Gÿ{z`ã¨fßú“è ÷f̃þÖC™‘3Ûj¿¡¾§þ¶e Ýn¥¥‡ª§,«í­°±·BmW•lTÛYamo…µ}Õk*µ%9‰…çêˆÙB¹ÎÐj·»)Y¨”7ägÙÏoÌvëoЃxÃ;ï¼Å»ï.å©ÕÖkPZ¨4o[;žèÑ+ˆÐ‰øéÍú3÷“z2›ä#×–T4c®:^kAW™¶‰K{®6ε•í–>Yˆö{Ûêy¼Ä:©©“Æ;>o(Kiæ«ýC ]¹õ‘$JçäækÛI½ìpöqÀÅ××€™¬óù$Í’J¾F&£ ]aÞ´¬PV4ËåAWãF'Eûñì³ÏK%ˆ6Ç­›=駯<‚ûðwçè3.c…‘ôS¹W½žÛÿuN^|õüÒ/7ÿˆDLè‰B¡ íT6ç£ÒšüþÙÒ' !„¢µ„â5ÊMÒ°å½ýðãž#ªÚ_Pë¼ÖŽêªÄaûêä¡“}Õýé²ue—|<]¾žâ<²ÝTÌ&$™(„¢Q¹:pbklƒæ=±9žôDi¡$õxvƒ×1fN$'~9•ŠáSÃÙ±Rn‘r=\|‰| Åy¥üüÖ>LFC= ¿£;ÖvVÄîMå|Tª|¡&„Bñ'’P¼Nçv&“r<“~wS’¯çôÖ<ý]péàˆK{œ;8`2š(ÊÒ¡É.!?ECÒá Š²tò´M!„¢ ±²Uaë®&'±á£Ú~}–ôÂl2“vòÊ#o™ÕŸ˜]Éä§VÝÏ·—']ûw )Z.S½j ˆ| 7n8öSlû §žÌ!õdî6tíïËý¯ÝÒ.G- !„BÔEŠ TSΞµÇéÜÇ‹~wõ ¢Ô€6§„˜Ýùh²Jªþ!„Bˆ¶«K˜7…É%Wý¾ƒ_ŸaÐßzc2™É8]÷åÏ熓|$³FÂ2úûsÜ=o)dzÚÍ¥Ïf……Yq]ËÚ™ˆ{‚9º)†¿œ¯s¾â<='·ÄsrK¬ ¾½<8¶)Nvx!„B´k’PlD©'²I=‘-!„B´C~á>ht×ôÞýO3ø¡ª'hÖ–T¼ir(™qy\ˆÉ»ìµ_ŸaÄ´¾l_Ýæë¸s˜}Æu#õd'þwmI½QG +ÐóýÂWwœwqÔbÐÎŒœÑ—«ŽÊN/„BˆvK)U „Bq}T6–¸tt $ëÚŸ¬µá=†v¡CO÷Ó#èEa†¶ÎK¢³âò©ÐðСM×±ƒ‡áãƒØúña=lñëçsÕ˘ðê-œÙ–ÄñzF%^IÜÞTRŽgqçÜa˜ø˜mKµ7M•ŠB!Ú ¡(„¢ÁT* íÁ ·ê¸Z]¼ôtç+‰ZŠ›&…b6™ÉŒÉ§ß==ÐiÊIŠÎ¬÷}GþÃ]/#åXV›}xÈ/á‡WvU—zß<³?šÌ4Šôþ;^ÊîÏŽQœ«»î²dÆæSœWÊä÷ÆqbCJ½óô#tl ÇŠmô:‘>Y‰½BÑ\d„¢B!Z•Ñ·ŽfÝëøêë <üÈÃõÎ;}ÆtV­ZqÃËä×ׇŒ3¹²¬}_ž¤÷¨nšŠÉh"~ZƒÞwðëÓ ŸÖ·M¶ùøç³}E4æ?åJ·ÍmÏ B¡¼òýoýÇ@Nü×(ÉÄKJòõ|·`ÝÇûЩ×e¯{wwçžùñwµeÓk{H=™#^!„B´’PB!D«áääÄÔ©SY0OÌ~’!Cn¢W¯žµÎÛ#¸C†ÜtÃˤ²¶Ä­£ÓU=ÝùJ~}š¸½)ÄíKkð{²ÎP®« ` o›jó›&… Mæå¼ùíÃÜùÒ°zß?ô‘0§“›¤¹!å;÷ß DøÒûV¬¬1½/½Gue÷Úãœþ=A>¸B!„hs$¡(„BˆV#(¨gÏž!))‰¼¼<öìÞCààËæS©TÌž=›åËnüèÄ.aÞ¤ŸiÜÑg†òJJòË®úÉÍGŒ¥ÿ½ÁXª-ÚD{÷¼ÙŠ2c½Ó”qjËyFL¯}dæ€û{RVDÆÙ¼Z΃ߜÁÚΊ>·rû¿n"õd6û¾8I©¦L>´B!„h“$¡(„BˆVÃÑÑ­ö{æiµÅ8;9]6ß”)“ùí·ßÈÈH¿áeòëëÓ(÷Ol,¿>Ãð©á×µ 'õ5½Ï­³áw1¡'.¾×UïîîtöàôoõðËŒËG“YBŸñA5¦‡Ž ÄXf$ñð…&©÷3Û’ÐW𿷣Ȍɓ«Bq{:¥eëHi žzÙC÷ÚI( !„hvüööö—MïÓ§ï¾»¤Õn×ßúŸ¯ûœ 7ðècUOhâClظO×|ÊðáÃkÌÿåú/X¿a=Ó§O—£ÌæšOÚíÞ=ÿ~ÚôÓ _·ÊÆ·Î{¹óõÊŽ/ ã\.îïyMï÷éîFä½xàÍQŒ|´=oîŠ{çzçpOúMíÊ€ =1Uš)L×rÓ¤PÆ>I—pï«.ƒµ£š!“BØ÷ÅÉÍ»''Ï?žü4¤¶NjÎíJiÒºO«c$¥ôÉ-Ã-£naÍgkørýLž2ùªúí¿OʦŸ6Uÿ›4iR‹+wmñ£9ãÊõÔw]qRÊ-„èê‰ÑPÉmOjñeu÷sÂÉÓŽ‘Óû¢mµƒ<N!D³ ð'''‡’’ªû£}ýõFfÍšMAAA«Þ®þýû9gž~³ÙÌâW_¥Dåå rsžœƒ .àÔ©StêÔ‰ÈÈ<ù'©¬4ñêk¯Ò;$„Ó§NÉNò'Z­''Çê¿)ÒÕ˜çÁ¤oß¾lÞòKõ´ÿ|÷-î»ÿ²å}ü&¶ec®¼¶òxör¤(Y¿W||¼[FEiÀÑß‘ñO %öçÌ¿­C?œ;Û’ð{6yÇôØ{YÓÁÏ‹ H?ì=Ôg–Q”QJq¦Ï`G\ºÚS’]†&µ”ƒ2R  ÔØ“ôk>vžjz íÆ BÉ:¥!ëDƲú+[i}§våÌÒ«ëµ!ò•Ñ÷Ž`:øbïmMÊzÿµºš6?s}jJe³Å•ë©ïºâdS샭µÜB´7~};|$ µýïíAô÷-7¾†ŽíFÌ®d ùh?v¬8ÒfÚAŠB!šÕèÑ£ùý÷ßøùçMØØØðí·_óè£3°··çõ×_#$$„£G°pá+„††2{ö,*++9~ü+V¬d̘1Lú*•Š~ø/_|ñ%@Óo´üü|>þhyyU—>&%%`­VãÛ¡ÑÑGÈÍ­z"ð¡C !::šó¢Ñhðð𠤸¸Æ2{‡„0uêßÑëËðgÔ~âãÏóÐĉ –}¼ŒC‡0cÆ nu z½ž/Ö­cûömb‰‹;Ï3Ï>M@@†¡Ã†²dIÕˆ)g 5,ZôJõü;vdÁ‚y̘ñX­ËÛ½1šñÏ áÛ—¶]Ûí­œ‹J¨1B111©EÔUb"xww#bZO¶¼¿mvýO8ö÷pôE%^ñ§Q±üªP(p÷s½‹V*N¤qá«\* U÷yô÷ïzù¶'@ecIàÀŽ„üÍ—ÌØ|вJ°v°Bmo…ÚÎ k;j;5j{¥…z~ýàeÅW½Í«ÓñðåðÚSM\×IÒ'·ð>9(¨çÎ%))€}{÷Ò½G‰¢ºúmWg š~$rCÊ_küÈÍͽb\i‰õ]WœÜµk—”[@—>ÞDÿpÌ0xb¾==È8›ÛâÊéâ뀭“š¬óU_,xw÷ ²# ÒÛD;È%ÏB!š/)•ôë×C‡pûíw’——Çý÷?@BBÕ}Ó¼½½øâ‹/™8q~~~ôîÝ wwÞ~ûV®\E—.]¸óÎÛyüñÙÌœù8D\çô¦””D\\|}éÔ±3‡"##~ýúáêêJ§NèÝ;gggt:†Ï×}Χk>åÀþ$''_¶\777V|ò ³f΢wH=‚{2mÚtÖ~¶–ÿö Põtã>a}xtÆ£¼þÚkŒ;¥²m„ü¢¢"V¯^Â… øxÙ‡ìݳ—˜s1xzzñö;ï\õò*Jìýü㟻ú§A·ÄËÿ*+6Ÿ-ïígÄ´¾êXë<–j îzy8ÎårzkbË2›Íä&i8·3…›ãI;™]L¼ƒÞȹÉüüÖ>²Îçce«¢¬¸œœøBâ÷§qtS,;?=¦×÷ðÛ‡‡®)™P®3png²t®Ò'_æ¯÷_-..ÆÉÑ©Aý6€³³ Ï>÷ ß|û /Ï›Wë%áÍUîºâGCãJK«ïºâ¤”[U—;§ŸÍ‹w¼‰ZŠak‘÷S ¹5€˜]ô»Ç~Š%üöî¨í­ÚD[ÈE!DÓœ¤XZàÔ…Ò‚‚ÓG¥BZ)+GGÔ. ¸¡°téi˜Œ†zg çÔ©SÆ:ç‰Oàܹª‘‰‰I8;» ÕjINN®ÑŸ=zðÍ7_W¿/00µZ]ëôKËk .®®¼ð |øÁR***ˆŽ>BïÞ!,ÿd9YYY”–êÑëõÕó?<åa¼½½™?>GŽ!æ\ÍK8RSSHMM 9)‰}{÷R¦×sâÄ ¦<<€´Ô4” %óæÏãHô^í5L&S›Ù·þ¾•­¿o­1-''›éÓ.¿?Xzzz£/Ñd³+™áÓú²ëÓ†÷O½nñ'þ@Z‹¯/C™‘ß?:DøAx¸õå‰ê×¼\ñh?¶-?Œ® ižHœq&—Œ3¹é“›£Oþ³¿Þµ®~`劕dgg£Óé˜={6÷Ü{_¬û¢E•»®øq¥¸ÒÒêûJqRÊ-Dûæ×·ÃeÇ»×gÌ?°ù½-¦œÖΖ¸tpäðw5cÜþ'¹yFßUÖkÕ¤ ʼn“&rÇãQ*-ضu+V¬`ô­£™øàÃÓ&L¸¯ÖéMÅÎÎŽ—çÎeÍš5œ?_=}íÚµ¬]»€¥¼ORRŽŽŽ( ŠŠŠÈÊÊâøñãÖ{âgÆLõ׳T]’ U£Už|òIz‡„п_?Þ~çm{ô±:OŠ\ˆÉÃÆÉšþ÷ýý•“ƒ'†`e«âà×gZÍ6û)ŽÎ¡žÜ38ÿ{' ÿ¾téãͦ×öÈ }r›ï“µZ-ŽŽõßµ®~ÛÒÒ’œœrrªžæ¾}ûvÆŒÓbÊ]Wü¸qáªãJK¨ïºâdKßOš³ÜMq?~üm¨T*¶oßÁòe˰°°`âĉÜ2êvïÚŧŸ®¹ì½Ó¦Og„{«ÿÞ¸á+Ö­[Wý·‹‹ 6Y³` IDAT®gÙ²å—=LmÅÊOÈÈÈà•E‹«ç0deeñٚψŠÚ/Á ©¾ÜùO Ò´dÆÐg|'þ×"ÊÙ1˜ÝÉ—M/HÓ’—ZD¯[ü9³-±U·E“ cðàHŸõÎx”°ðpBBCqrrbêÔ©,˜¿€'f?É!7Ñ«WÏ:§·E6îžt?ÐÌeÐëËè?o áϾBÈìç þû?èö·ét½ëA:Ýzöa‘xôŒk¯0ýƒ°ëÐ µ‹+ïï"Ú……E‹+“•“3ž7Ñ}Ê,"_û˜!ï­£Óè;©¬('ñ¿‰zv§—ÿ›˜Ï>âÀÜÇÑÄ¡ÃQŒ\ý}ž^H‡a£±´³—ÆmázâÚy­­­éÚÕﲑ)Fc%®®®Wu‰îñã'9rÁÁÁØÚÚ2fÌ­8;;×9½IêÊÊŠ—æÎå?ß}ÇñcÇ.{ÝÕÕ•iÓ§a4š8þ<}ÂÂx饗prrÂÝݰð>$'%_Óº‡Ê?Ÿ~Š”äd~Ù¼{{{¬­­e¾‚„ƒé(-ôáWç<^®<ðæhòS‹ZU2ñ’Ô“9ì^{œÓÃqò´e÷gǤá¥On}r\Üyzö ÆßßWWWnrç.&Ö.•¡®~[¡PðÖ¿ß" 0k†NjZj‹)w]ñ£1ãJSÖw]qRÊÝ< ˆ`ðàHæ<9‡Y3§oßp ÀôéÓèÓ'”… òÙgkk}¿«‹3óçÍgÜØÛ7ö¶ÉÄKÊÊÊ7®f’¾gÏžÕ÷þ¼D§Ó1nìmÜuçÝ,_¶œ9OÍ‘@ÐÎt õ"ýLΟ¿O¯»'/g¼º¹6{9mÕ8øÚz"»Ö×OÿžH`¤/Žžv­º=šl„b\Üy^ž;ÂB žžÜ8(¨gÏž©þöfÏî=ô¸xQÛô3gÎV/3$4”3¦£×ë  jß>Οcò”) >XúV #9ó1F^_ÊÚÏÖ²mÛöæ=tvÅkÀ0¼ ÃÊÑ…Ü£ûIýí¿(XX[cic‹…Ú kk,¬m±´±Gíì­‡;:VM³¶ÁÂÚK,¬m1êJ°P«1”ê0êJ0––`ÐcÔé0芩(ÑVM×é0”–`Ô_üY‚Q†ã· ŽþAø ½Ç.þ8øP®) BSHya>å…ù”æS®) ¼0¯êµÂŒzÝ )‹µ»ÎA½q Å%¨¶vhb(Jˆ#öË•”fÖ}£Y“Á@î‘ä©ú|ºôìƒ[h?º=8 ]F*•I1XkK)ËË–Foa´É‰8úù7hÞâ”Ä+$½†°wï¾Ë¦ïÚµ‹¥Kßã©§žnp¹âããYµj5óæÍÅÉɉ½{÷±cÇÎ:§7…Ò›ÞèÄÆòܳÏ1|øpf<:ƒcÇŽóêâÅìݳ‡îAA|¼ìcÌf3?mú‰Ó§O_Óº÷ïßO¯^½øð£0™Ì¬_¿^.»j S¿&0ð^è JI;™S㵈ûzâÜÁžÍKöa(¯lµÛXª)ãÀ×g0èÒàÒ'·›>Y«Õ²fÍZ^ž÷2VV*~Ýò±1U÷_}eñ"f>6³Þ~û£>æ_ÿzgggNœ8Á÷ß}ßbÊ]WüP(Wšº¾k‹“Rîæ‘——ÏK?"7·ê!4 ¨­ÔXÛØ0jô(ñ……ußKØÅÕåŠ4Òh ÑëËéÞ=ˆØØªÑecÇŽ!*j?¶¶6µ¾G­V£Õj%´0 ¥¢ê‡§s1ÿ*4¿¾>õ>|eÏç'˜°øf¾˜³¹Yë!tL 9'‹ê'jÃ)FÎèǯín½ííæîa®ºDJQ}©”¢®kþœ¨4ààpõß(nظ6nØÈºu_pË-7Ó;$„¥ï/àî»ïÆÍÍ•äääZ§ÿyuHh(/¾ødð AÜ}ÏÝ<ù' îÌœ9sxþ_ÏãååÉc3gòܳÏÕz?)…Rõ ºA¦ÊÞ¯Ãðކgr ïØ¡+àýYçÎHM­û~M–¶ö¨ìl±´±ÃÂÆKÛªßU¶vXÚÙW% míPÙÚ^|ÝK[{JR©Ð‘}`'9ÑQMººö Ci¥F©´¥…… ¥…Ò…R %^žUHi‰ÂBBaÂB‰Ù f£ ­† m†bMõïæÊÊVß!ûûw%-'Ÿ¡£è0l ¦ò2rí!ë@ÕÓ⬜\°rt®úéä‚•£VN.¨\°r¬š¦TYR¡)¨J4jò)/,@Ÿ—ƒ¥­=J¥¢j´ãÅúT^¬ïªiU¯U·‹R‰ÂBIyAžƒ1›¹˜@Œ¥(!–²ÜÆIþ9úÑõ¦¨ü‚0êuäDï'÷H%i­÷’¥¥êŠ÷­ºÔÞý¤P½¶¼qû1Uã}õæ›o°tédffÊÑW c0\_¢©¸XƒÒBÕ y¯õx¢±Xª-P©/߯o~¬û7ž¢ ½·NŽŒ˜Þ—Ø=)$ºÐäŸãÖ³Úã¶_ÍvKŸ,„hîØ{=q×·£/s_z‰§žú'¼ð äååáç×…£G±ä%”•Õ¼ïòO–cañÿìwxTUúÇ?w’™ôÞ !!„$$AªÒDŲkÙUTÀîO]uׂÁºJ± *¬¢®îª‹Š…"HI¨!$ÔÒ{¯“L&™ÉÜûûcB’@€ p>Ï3ÏÌÜ{î½ç¼÷̹÷~ç}ß#áååÍYüÎbÚÖ»¹¹ñþûïòù_λËÞÃÞÞž>þˆ•Ÿ¬dµãÛ…<˜L&æÎOJrÏæf0`™™™¢ÃvbÏ@WÆ<Kvb!×u¿‡î½K¯gÍ‚­z(žÀ«¿+ƒÆ²éƒÄ^±…Æ^Í­sÆú]ñYïBG÷ÅÚÆšäµ?Źܟwù>º§{ÏÝÓðóócáÂìKJê°Lg9ž:Zž››K^ž9ä ';‡ 4éõ¤¤¤pÿ÷Ÿ—•JbÁ+óÙ—˜Ä¢… ;MN?`@ûî3‹¤±ÁqpŽQCQûô¡1íÉ»¨+Ê3 €[¿¾]ÞŸwK¶€¾Îü:1è¶¾:›7Ñ~à`B¦ÜBÔã/Òp4Ý‘$Ó»ÿ_L+'ìC£°…}hÚä]X;»‚¬€"£(rëg“Y0”eìll°ñ×ɼ^‘$LVvv¨ZÅQ•½#V­ïŠÑ€I×€IW¬3{dštõ˜´˜t ȺzŒÚZLµ•9(ÛŠÆsÔúyù¡;šŒvãŒÕ¨¾}þ0S§b„Úr¨-Gš[_m}ÐZ•£VNX98ãèè„s¿@PY¡( ’¢´ÚTEE¹Å¼Ì Ÿº^V°5©üßg˜êÍ}Ëð¶ÓÀ9ôã3w]=š´ý”m_ÆË÷pú>ù’ÚmòêvüvI]\]®¹§¸«Q»¸Ñ˜yŒÆôÃ4fÅT×þŸZ??ßn?þÑik›^xQÜ} ,–-ïç¦ç¯æè–‚býØòÉþóžyX ¸c²@ 8nîîÌ™3‡eK—b0ptpÀÆFÍ'LaaÿxþïL½i*kþ·æ”íV|¸‚²²Rt<õÔ“Üqç¬>-<ÚZ­&~{<3fLÇÖÎŽñƳkg{iNǷ߉$I ÁKs^bÖÌÙ" ÂÈÑÍYæ‰ëv «é¾ ßÎîüG*rjñv#òº`ŽüÖó9 £¦„Ÿœ]¼ËØUÀ¸Yqxõw¥"§ö’;ß=&(º¸¸ IP[[GII ÉÉ)„††RR\Œ‹KûÄ·Z­¶ÃågBA9Et<áh©ÓéxôÑÇ‰ŠŽfø°«X²t ³fÎîP Ì)(Ä5¨/&ƒ¹¹ ÙØŒ©¹ùœ&PY[á=|¾#Çá:0‚òä=düúuÝ3ƒÝ™</lǰy’JÂ3v^±# ¼íÊ(ݽê£çŸgÉ%dž1ÃñŒÚÑ™ê£ÈÙºžšåouiû³yfvعíì°vtFãäŒÚ±õåäŒÆÙµ ¶Ž.uuxFGWRDciº¢|ó«¤]QA—¼ÊºǾAø½¿k&Q›~”êä=߶銼åç˜ûäþ}ظ{à:˜Þ!uÕ2Ê-{~7ÞNÈm÷R´uÇ>ƒ¢(¸…Á}p ÞÓŸÄÔÔDÕ¡}TÜGͱCmÛ]©žM%òË[; ÄÖOö càŠÅÁÁóç±rå*ÒÓÍ^gZm=yym!ÊñÛ㉊Š:õy¬uB£²2s ‘Í›7sà 7´n³¶¦¹¹™=»÷0~ÜX¦L™Â’%K ìø¹_Q8rä(:]#þdffµ+3`À€‹b‹¾}.Í“()øF¹Py¼–fù¢ØÅÚFEŸÁÞÔj¦$±–kgàØEÝvŒÐ1>h‹ô÷?kY}„Þˆ³ åÇê{ÌÌ*k‰A×rð˼.;‹ï¬câÃÃùï ¿]rݪÇŘØn¹ùf-Z„Z­&6.†;v’ŸŸÏ³Ï=CHHµµµŒ;†Å‹SR\Òáòóaܸ±Œ=Š?ú„òò2&Mž„íÊÚùøõø‹¨46XilPi4Xi4 €ÉÐŒÉÐŒÜÜŒlhÂd0`jnÆdhB64·~nÆìuT$ï¦lÏ6Ž}úÞ%×)YiËo§R«ñŒIÐÍ!êñ)ÛOÙÞíÔ¤>ËIGÌ0¼bGâ3 }E)Õ©IÿzÅ=ÒŽ½ž½þ졸҇Øûøcïã‡wÞîÁÎÇßšëji,)@WlŠói,)ÄØÐ}ƒ’•-~c&â?f *µ5å{HZø -z=ýºËëï2 ¹ºŠò½ñT¦ì!ô®YŒ›Bê¿–Ñ\mY¦ý®ÿ!·Í 8a{^x¹ådøJMêAjR`ïë[øBn»çþ¡TÚT”…¦ªC]͹ß#¨TØyûaïë·?ö¾þ8øú³óåˆÎ#\Ƕå #àŠE£Ñ0oþ<¾ýî»S‹³³³ñññ&&f\3f )­“Ô¸¹¹RSS‹$I,^òóç/ ¨°ˆñãÇ“—ßù„Fë×o`Á+ ¨¬¬$/7¯SAQ’$bccpssí4MÃÅ K¾Cž=ú¹6À÷p~zíâ8fØ+dì*0;HdÃpÿ Ž ä(í–ý||ÛþtVŶ>úAWÝNä=dîÊ'sO!M ]wò ó$(Î{ŽþžMizõY·‰¹i GÏ&;;¿õwÒ5g‘ pïãDuaý%Õ¯zLPŒßOØ A|üÉÇȲÂOk×rø°Y”ZµêSæ/˜FcÍúuIkI«³åçÊ®]»‰ŒŒdÅGË‘eøâ‹/;tE¤üóåöƒ–•Ê,0ª5XÙØ Y›…FÕ‰e­âcK£ŽÜŸ¾¹l. ²ÑHybå‰ XÛÙá;’àÛg`ïãGYbe{¶S—i>/v^¾­^ˆ#qNÕ‘ª ï×5´46Xn#h,-¦±´8Õ ÅÆÝ{ì|p ‹Âoì|ýQ™šãGTVÈF²¡ÙhD6N ÏFCëºÖ÷Öõ²Á€©Åˆ¤²¢ïä›ñŒ¾ŠÒ½ñd~·]až¸k9[Ÿ48þÅ \ fØÜÅÅÿFÎ_õz½ú^w+!·ÝKéîmì™ó²ñÌ«}®hëz$kkÜ#†ÐwøÕŒX8ƒ¶–ʃ‰TH¢.óäDT§‹†~æþéí»'M¥è+ËÐW”ÑTQNíñTÑa@ ç/âŒAttÑÑ'½ÓÒÒøÛÓÏðÖ[ÿä©§žÂÓÓ“]»v±î×ux{ûðú¯1{ÖlŒF#ï¾û/¾ø"nn®8p€ï¾ý®s(;›šêj6¬ßÐázÖoX‡,Ë—°øÅètâ$uÁ“‚I‹Ïà72ù‰á%¿`ØØ@6/?¹ßäµiLýÇ5Ý"(v5Üùt’ÖÃÁÍŽ 8_nz~ Uudì. ðpy‡å}zçG`¬/Uyu­àðÆLFÝM`L{¿=zÆãEN fͼ­çÜ>½ÖÐíy–{‚Ÿ”ÅÒQ©­qöó¶ØúOèïÅ@íäŒWì<ãF`ãâŽQW•Õ©¨9z€ÚôÔ˲ÝmíwtÄÎÛµ£#*k *µ•µI­ÆJ­F¥1?±ìÄg•Úºm]cY 5ÇSupß%ÓnKìç}¯»ßQãI]µì‚ÂòÏûâ6é&‚o›NÙÞò7|©¹ù‚ÛíÐ÷ˆÜ£±÷ïCÙžxÜ#ã°u÷¤©²}EúŠrš*ÊÐW–ÑTYJSUÇžšµùÅÝÛ÷ÕÖ.Ĥ,瘔ELÊræË”E \×ÞËõ9¾§¸'eqô°cÒãÃØ¸l¯ùù%ʯ`Wv~y¨ÛŽ1âú!Øõ±j'¸…Œèƒ­“š¤ï/,¯û˜ûc(M¯¢ðHùíÇo ;qþxõw#sOÇãóqô°ký¨)ÒRt´œ‚#eíBÃûõgðÄ`¶–LyVûˆ²¨ë‚ÑØkHÝ’s^÷E[P¼,&e±tEØ K¼z-Åñ›(Žß„ÆÕ +[ôeWά€Æ†Œ é¢#X¿ýDÙÞsôxs´©©é¢·ÏµS ¾mû÷ôês´4vß¿£º"sÏ‚M?cek‹S¿`J6u* @ AGDN &=ᤳFÁá2ìœ5ÄÝÖm³ ûq%y]û9#²ö2ù‰a¸ø8RWvþQ‹1¾$}ásR”¤WS’^ÚÖšþCý=- I…G*ذt7Ʀ΅ýœýŧU0⯑TÔ±oÍ©õ‰ºnk_O¸¢ú–Œ¡¶FAл}°®†£¿ƒgÌUŒYö%Yßÿ›‚ßÖ^”cŒ¿žàÛî¥ê`ûßxÍÔÔÔí¿Â…þ{.1&  gPÛZçÇÚÓò&¦ï, úúÂÆ’¶ýÂÒn¹ú9bmcEe^Ç“è&¯=Ψ{¢Ø°t÷yí¿o´Ï{&¶»~6µ¾³€Œ]…NÖÛÍ:#ñŸ¦2"€Û_ÏöOS¨Ì«#|B ™{ ‘[ä+ª©ÄOL \.THb÷ àà×—‘¯ˆspèïÓÚÁ—Ax˘w¿Ä%4‚”·ç’µæË‹.& ËÆy@Ã_Y†WÜHaŒÓ°²µEem% !ÁY0ªFõÁ;Ä;g›nÝwÄÄ`Òâ;žçІ,|xÐ/Æç‚Ž66Ê´Î'©*ÐR_©#dÄùÍçGQjÅE±½rž!ªY{‹ØòÉ~†Ý1˜¸[u]è ³—"ÂCñ4º>R X89kÿƒ½_a÷=Ž6'“´Õœ±¼Jcƒƒ¯?v>þØûàà×{ß>Øûø$ÑX^B]úQ,Y€¡®VX à8€°“ûó7ø¿ž°‘¿ég¤¬ÃW¼m\Bɘõ4¶ž>H*rëde¦æfdc3-hs3(IØD}^¶èL@ ¸" Šó#î–0² qt·#xXNžöhìÕhËuÔW6šß+t.ÇÐh<çcDNìÏ ·uº~ïwG7+}]39çþœ#©$‚‡pðËü3–K^{œÛN koÑ9£»Â»›¦zÛVî'òº’¾O=c¸ôåŠOCäP.KŠ8¸t¾#Çrí§?‘ºjÚìãØûœòrð ÀÚÞ‘Æòbó$'¥hs2(Û›€¾¢Ü²g(½FÜ?‘ôÚ ´46P—q ‹~×L¤Ï³¯c“˜@Á¦Ÿ©Ïͼâìâ=b,¾#Æ’üæKm*µ+TjT Vœúñà߉âí¿Q¼c3&½˜©S —?þžÄ݆¶¼mÿÚ¾îÔÉ8TÖ*œ<ìpô°ÃÁÃß„ dÝâÝ(¦® aã‚ÈÜSˆr–(ÜíÿJfÊÓ#ؼ| UúsjË 1] “Jcä]‘ìùï‘.ïÿb„;w7G~ËBºBc… xÂCQ ¸¼(ÝOYb!wÞOÿ[ïB_QJSy)Må%TIA_Q†¡Nä]'òÑù¿/OùÃÁPWCÞ¯ÿC9¼—&Ÿ~„Ï|’¦ÊrjÓRu( ]qA·ÖÁÎÓ›¨ÿ{‰ÚŒcTØGõ‘ä^·‹]è`\†Œ õ“emËYÁÔlöNü#õyÙ'lÆÞ¿/>ÃÇpÍ’ÕÔ¤¢xûoTHL µ“ŽÉ(SšYEYf5åY5(rÇ¢ŸWWânD‹ÁľïSÑ–uœ>In‘©+ÓQ÷‡õ®~NÜð·Q¬{gW×ïW&³yž.•ݸl/š7Žo^Ø„Ür.¢e ;>?ˆ›ïYËæ$•0á¡¡xôs¦*_Û¥ý_ÌpçîD‘¯Ð߀NëÂCQ ¸ü~ײBæ7Ÿ CX0¼‚gŸý; ——Gè´{§1uê¨Õj¶lÙÊŠWÊø ã‰Âc>ÀÿýßÜ8õÆS¶ß¹s¯.zUt ÂwôµH*‰Ê”½–)ß·“ò};qìŒWìpž|•µšÊCû¨:°ÊCITÉÊŠQo­dßÂgñŒJÐÔ;ˆ}ö*%Qy ‘ªCûz|Vx×A‘¸^3…¤Å Îi»Æâr~üšœ¿Æ#:Ž>“n¦ÿ­wS¶oùëÖXÞ³½ãá¹~¹ŽÉ“¯›ÌôéÓÑh¬Y¿~#Ÿ¯þ¼]™ŽÆg€Ûï¸Ûo¿æäx®ôЃCWêÝÙõ泜͈áÃxðÁ‡-ÊÞçr¼êÝ›öôH¤¥ÙtÖò~ažÔW4¢«Ñƒ[׎±m£îŽæ—·vt©¼¥†; Zï‹„ @Л„„„PVVÖöàºfÍ·<øà#TWW_Òí>|£Gä©'ŸBQà7_gøˆád¤§óØã²kçnÜÝ=ÚÊ¿ÿþ¼ÿþÉ|ŸO<ñ8‡Ä‚Ð8»0ðîYìûd—Ê7ägÓŸMÎÚÿbëá‰[øúLù1­â_î/ßR{üè9×#î…79¼ü u5'l¦8a3Hà>÷Á1ô¿å¯u4U”vºc½$‰æêJšjªi®­¢¹¦ŠæÚj µçöÛs ÀÀ{¢ü»]}«%Su(k{{úL¼‰ +¿'뇯,JXìë]¨íIý×21&_b¸¸¸0sæL^zñ%êëëyãÍ×IÚ·£GSÛʸ¹¹v8>ûøøpç·óÌßžE§kdÑ«‹6|‰{-¢Þ]oNÔ/,<Œk®¹£Á`Qö>×뤥׻7í-è"¼)KCe#©[s(9^uAÇËÚ[Ĉ¿ ¦_Œ/ùJÏX6rR0‰ÿ;·{ ÙñŸ&sÇ üòÏg Dö¾sˉ¨-ÓQ™[ˈ¿F°ç›£Ht"z)„;_éAQ ½Ê”)×±qãolܸ;;;~üq 3g>È_þr{öìeË–­ÜqÇíDFfÁ‚…xzz²|ù{üõ¯÷pÓMSyàûÐh4lÜø+V|ŒÉdêõvUVVñÞ»PQaöËÊÊÂFcCMM-{úœ¹ýŽÛ:} 2$š+>:eyTt4>8½^OHH»vî$##é3f`4yïÝØ»w<ò0“&OF¯odõg«ùý÷-¢³] ±Ï½Ê‘Þ9¯m›ª*)Ùñ;%;~7?lDDÓgò-8öíOáæ_º¼Ÿà?ßK]F*ÚìŒSW(P“zšÔƒdö~Øyv>k£,· qtEãìŠË€Ahœ\Ѹº¡qvÃÖÍÊCûÑåR¿éŒáÚ~}ˆ|ôï$¿ñýúõí;·46’ûó·äoø~×ÿ™ +¿'û‡¯É[÷¿^=ÿa÷=†¾²‚]Ãæ/aß+ψ1ù“ %5õ(999$Ä'~ŠPÔÙøl00Ze¥õe¢¡¾ÁbêÝÙõ@­Vóøã³âÙ=û‹²÷…\'…½–@@¸'I?´÷ž«.ÐR] …Ý̽ßeêßGSžUMS}Ç¢µ_˜'zm3ÚòsÏM,›`ík Lyz «R•_×ñõßÍ_JÓÏ]$=¸>ƒÐÑ}™¾ô²“ŠÈÜ]@yvû aúÇùQxTŠ–Œ@Ðk¨T*† »ŠO>YÙú {ã)Þ0{÷&Ë–-[5j~~þX[[3„={öʬYðÜsÏSSSÍ+¯ÌçÖ[oæûïìõ¶egŸœ½6 OýúöcÉÞ¥]ÚöÖ[oaý† >„{zzðâ /¢ÕÖ³tÙ¬Õjf̸ŸÑ£F1íÞ{Ø»wááÄÄÆ2kæ,||¼yø‘Gغu²,‹Nwž„Üy?•ÑåwËþªSQzˆÀ›ï$ðÆÛÈ[÷ýY·q‰[ÄŽ,ó¬eKŠh,):ïúY;8à3|,ÑO¾LK£Žâí)NØ„ò‡>dãæAìó¯³oÁßynä IDAT.ŠÍe£ñ¤°8åO\»ê²¾ÿªMX´¶s@íè„ÚÉ µ£³ù³ƒ35éGiÈËêÖºø^3k'J×~@}^“>ÿ•} ŸåòñBºœÇdggg´Úú¶ïZm=î]Ú¶¦¦†ï×|Ïg«?`ݺõ¤¦¦ZL½Ït½™1c:¿ýöEE…gï ¹N { zŸ7ª ´È-=oµue27üm?,ÜÞáúÈIÁOÈ» cl\¶—ñ³ã8ô[&EGÚç04.ð¼fl>AÆ®2vëKÜ-aØ8jÈØU@æžÂ¶Ù¬ûÅø²O„;[4BPA—qµ‘ðv°:k9EܺŒg¹ÇŠ‹‹ãСô´´t¸~ß¾$Žh4öíKbÈhbb†°{÷† »Š-[¶’•e¾úê?üéO·ZÄÃë ÜÜÝ™3gË–.ÅÐ…ð'®x-?öx‡ësssÉË3‹Z9Ù9ìHH I¯'%%…û¸€ü¼|¬T ^™Ï¾Ä$-\(ÄÄ 9‡aQ¸ŒàÈò·º}ßy?Gÿ?ÝEß)¢`ã™ûíÐÞ`Ç3÷÷H›[t:ж®§hëzœƒñ1–A÷=NéîmÇÿFC~#_[Áž—½èu‘Frù޼ ?xýŸ‰}þuÜâhÑë1êêiÑ5`ÔÕcllÀÔØˆÏÈqäþô*&uËñüûÒÿ¦¿´Í\ æIežšÁ§_Ƙ~²sĘ|‰ŒÉ§Ú¥k9½½}¸åÖ›ypöC4662Á|ââbINN±¨zŸ~½4h Á!Áüë_ŸÒ§O‹µ÷¹^'…½–€„¥U½rìÆÚ&R·d3úÞ!ìú÷ÁSÖ¹÷uFcg}Jöù²mU2£ï‰ÂÖAÓN< Óµ¯Æ_ø}PJ)y)¥8zØÑ?Î[猡2·–ª­w¾‚¢@ ºÌšÛ=q³³êRÙ_3ô̯;c™)S®ã‡:ÐÔjµèt:¦N½‘½{9~1q3fLgçÎ]¤¤`ĈáTW×ÐÔÔDRR'N $$WWWþú׿˜˜h¶Òh4Ì›?o¿ûŽ”ää®]”U*n½õV~üqí{ܸ±<÷÷gÉÍÉá—_ÅÉÉ ;;;Ñ;A²²B¥Ö`mg‡µƒ#WlÜ<°óôfÈßæ“¶z9r'[ÝEö÷_áÜ?”€ 7¶[xÓ_h(̧6=µ×mU¾o'G–¿ÉñÏ—÷Zdƒñ¬Âêwæ2rÑû|¬¸ß$õ“%ÈgðšªÝ¾žª#)\³t5VvöxDÅ1pÚCŒzóc®zù\ERy ±SQRŒÉ=Czzá„„„àááÁ˜±cÚòâ¹¹¹žqÛ‚"##ñóóÃÕÕ…‘#GŸŸo1õîìzóÊ+ ¹áú¹áúypöCô˜¸u!õ¶ô~b‰öô,¶Nl4Ô•ézµ)?§1>GÜmñ t¥èhE·'yíqì]lúç0ÂÆ’xñÂúKŽW±ë«#èëšEg³p„‡¢@p>7ð(j½I\1dT uïš·IzµñŒëÇC||ûlÕ[·ncùò÷xâ‰'IOÏ`ÏžD®»n299¹?žNff¦Y„ÉÎaÕªOyã×prrä·ß6±fÍa«‘#GEttTÛ²´´4þötç9Œ;†¬¬,ÊÊÊ.èØ»ví&22’-G–á‹/¾¤±±Qtàˆzäü¯™ˆ"›M- ËÈ--(² ÈþáßT陰Ƭï¾`À_@‘o߀sð@|®ÍÁe‹,Ên¦f˾É76Ô“þÕ'ÄþýURÞ~ù¼ö1à/3©M;L]fÚYËVL¢>/‹èÿ{ $‰º´#ûô=ôå¥bL¶1¹®®ŽU«>eþ‚ùh4Ö¬_·‘´cix{ûðú¯1{ÖìN·ÍÌÌâ§µkygñÛh4¶lÙÚc]u¥Þçs½õ¾¼ê-èY"¼(I¯²ˆºl]™ÌÏŽæ›63xb0é;.ΟG6g3`T_ÆÏŽ£Å`êu1U`Hž^Š$I€„Ô+uâýLÈ&#NN®—ŸA¬­qñ÷¶Øúõë×—üü‚+®£öD»Õ’‚‡-¸kÀÃFÁ]#áacÝ4ài«à¦1¯¯5Bv=D¸@A#¨–H©”j ƒ,‰óݾ¶ a.ÐhRH¬T‰~ÞCí®Í/¶Øö¾óÎ[,Yò.ÅÅÅ./êëkQYuMäèíû GìþÞf ¿ãÐiRy ‘Ò[˜ðÉ÷ì™ó˜9ÄW\«Ï™>oDRY“ùÝgç´÷U£ñ3‰´Ï–wk»Å˜,zûÚ{¹>Ç÷ ÀŒ#ÅÇ*)J­°ˆ:ù t'誼ƒÝº%¯áïQF÷%ÿ`)ͺSïK‚ƒû“ÝKù„-•sµ‰^ÛlcĹ <OÃJ áuv93ÈYa˜Ø+D¸˜…B•Du³BM³D­Q¡ÎµÍÛ qÐ5Í5¨5œÚ7Bœd"\àŽ~ ¢!»Aá`5$×H¬£|åõ%J!ÜEa°‹Yt wQ0ÈY/¡’hâÛ<‰uE"ë•ÌsÏ=/Œ t@ÆW+xïÃô›ògŽ­~ÿ’-•Âß×1è¾Çð>†òÄ„.mcëáÉÀi±ï•gŘ,‚vø‡y‘òóq‹©OIz5Žž”gU_ôceì*@ÐF ŠÓîÆÔ©7¢V«Ù²e++>\Àäë&3}út³[ùú|¾úó3.¿˜ r·âóI2-Po­Qj}‡ºÓÞëÚ¶ÏÐÐ"DHKÅMcGx*\å¥M k$Ê$6™…ÂÆó<Yõ*²êáçÖ4œ»Â]2¯ ŒzH©V‘Rj@Vz¯ŸH(؃³ZAÛÖ/¼>AfïÃÁ. .àoéZsÛw”K|–)Q÷‡ãØ+Lícÿ—'ñm¾tÞö´ÇY ÁŽ2Aް:_ØC ¸ÉøÏJ$+5J‹Aã9þù‡ ó y™4–•œ±¬ÚÉ™aó—‘òö\a8@ ´ÃÉÏ–ÚÒZ ²eÝ7¡OÐ ô˜ 8|ø0FÉSO>…¢Ào¾ÎðÃ9žvœ™3gòÒ‹/Q__Ïo¾NÒ¾}u¸üDRÜ‹öC¬‘¹+Þ kkpR+8Zƒ£ZÂÉÕ ^¶àh Nj¹õÝüÝÞVKT4+¤T«8X ¥z!’ôCÜdFxÂ0p·1{ ¬VñEèZ.žg\f½Df=¬-0'JuVì"so‰·bŽÕAJÄjÐ˵¨3t_.Æ¢a€Bèco~ùÛ)øØAI£D~£BßVaÑI F m‹Y×Ìbcêþ ¨ŸxU7+ t–ˆpW™pg(ojm·VbK)èÎÜ–¢F‰OÒ%¾È2 ‹ßŒ‘‰/“ø6O"Og™¿™Î2éZËò¨´–ú;BGqRvTrµJ"¿A!¿QŒ?Á¥Šb’QL"yw‘òö\F¼úÛº½Ý:‡€~xÅŽÄkèHl=}H~óEZt"7”@ œ 3Ç +++¦M›ÆÄI‰ß¾ýëÓ·½ù橨TVü¾ùw>þø“SÖ»¹¹ñõ¾âÃWðóO?Ÿ²îãO>¢¨¨ˆ…¯,j+ÐÒÒBii)Ÿ}ú»víîQ[¸ÚS–Q%:…@@ Š••U¼÷îTTT••…ƆCIM=JNŽ9¶°ðpìíí;\þGA1*:šœ^¯'$$„];w’‘‘Îô30¼÷îìÝ»€Gy˜I“'£×7²ú³ÕgM¤¬k‘е@y“ÔN®é\È1{ÂE¹ÁpO…ûB¬$sØë³ØX$Æ‹†¿ÂU0ÂSæ*wHÓJª‘øè8äézOÊÐJdh­ø±õO£0s˜õr”qVƒ‹¬Tu¥U`”¨5‚ɦš•L­Ñj}B|¬2€§Y( °WÚDÃ{o[(n”(m‚R=”é%טÅíöý¹õaÊZiÆÔ'Erk™;©UT7{¾U6$)dÔï*–ÕK4™ÎÏ6M&‰5ykò`‚¯ÂÂ!2ez‰mMz,Á±ÎÃFáz?…àXŒñ‘9R£p FÅ‘Z…CµRyœØ+ôwTèï!NìdÎK™§“(Ð)äë$Ö©È׵˂+Ù`àèGo3tÎÛìí︇Gã7 ïØ˜ ÍT9@Îÿ¡>/[K þ@gŽA‰{™={dÁüäååµÛ666†Ñ£GòØ£O Ë&ÞxóM¢¢£9|èЩÏMMÜpÔSňˆ¼¼¼(**:ùŒ®ÓqÇíw¢R©ˆ‰Âó/<ßã‚¢[i߉k…@=((fgŸüÑô  _ß~,Ù»”1c®A«­o[§ÕÖãááN­³s‡ËOÇÓÓƒ_x­¶ž¥Ë–`­V3cÆýŒ5Ši÷ÞÃÞ½{'&6–Y3gáããÍÃ<ÂÖ­ÛåîuSV€jƒÄö2‰íe'‰…7¸;PÁÆJáPíI±à=ˆ¬dú;*xÚ€—­ùÝÛ°™ÇÚ1)lq°Vð×ñÑ´Ð$K”Ô”¬)7Z³¯YÍ/ j¬]½Oý}Û˜_>®çoËÌÌân=7jµ5‚Ë£±Eá2'ÊU&ÌEÂÞJÁÎ lOüùh¶Ö ÕMT-ñ[Éåç Ýbâ›±2¥z¥Û÷+Æd@\{{ŠÓƒ°±QóÉÇSXXÄ?žÿ;SošÊšÿ­i·í=wOÃÏÏ… °/)‰c©ÇN}&U«‰ßόӱµ³cü„ñìÚ¹³Ý~N„Š¢päÈQtºFüÉÌÌêà¹>³Ûí0tv©kŠÈÎ΢„]®<›ô˜ 8r䢣£ˆŽ>9Ȥ¥¥ñ·§ŸaÕªO™¿`>5ë×m$í˜Y™ílù¹²k×n"##YñÑrd¾øâËÅDI¡„D×"‘Qõííâk§àß:{p{°±RШ@-ZeΧ±µ$™—«l¬ÌïhTæÀ̓5àn#‘®U8®•ø¹P:-|Y"ÀQœì¬P±³âÔe^¶ ¾¶àg¯Ð×^a˜øÚ™'1)Õ›g”.l4‡ñûÛ+\ï¯p´ÖœëtYêÅïWåMåMâÜ –Ê­ÁÖÕ+]³¢G»)Ì‹’ù±@â¿9gä>L³ÂY£pS…×ÊüP ñMžDUsïØÊÑZáÍX…íeðËóªØ_m¹çmfˆL¬»ÂŒª ò¦‚Þ¤3Ç ììl||¼‰‰BFF׌CJJ nn®ÔÔÔÃ-7ßÌ¢E‹P«ÕįŰcÇÎNµ~ý¼²€ÊÊJòró:%I"6677WJJJzÄý\ЖëŠèA+=&(ÆÇ'ŸÐáºÍ›6³yÓæ./?ÁáC‡N™!já+‹Ú>×ÔÔ0ýÞF–/ÿåË?g¼)ÕK”ê!ùBŸU’‚µdö^·Š&‰Š&sÐÓñ¶Uð³Sð³“q”©l–x2Q…®Eô7@`&¯Næ­ ‡kVeöžXv±y:\&ÄQá…dÕ9åGÖ$¾Î–ø:n Y1Ül«¯r »¡ç¼¯õUx:LæÃ*2ê%vW(<¡0ÎGaÉ1ËòV´Q),*s¨F╃Â+Q \ÚœÉ1è­·þÉSO=…§§'»víbݯëðööáõ7^cö¬ÙÄo'lÐ >þäcdYá§µk9|øp§ÇÊÎΦ¦ºš ë7t¸þDEY–)..añ;‹Ñéz&§aÀ`/Ê2«¹Ì‚“Hž^ŠÙ+OjóÎ늗žl2âääzÙD­¶&0ÐÛbëÐÇÿŠ ‰ííí>?DEÁùp¡yœêëkQY©»Tö|î'&_7™éÓ§›#ÖoäóÕŸ·+3íÞiLz#jµš-[¶²âÃîËÍÃ7gÆû(ÜÕ_&¡\be¦DãyþñЯ__òó ÎXÆE­ë!Ž2¿™g‘¿XĹ+Ì‹Rø&6wð6ÚK!ÖCÆI _ç¨HnõìJÛ†¸)H(¨éZ}^,ck¥ð~Z{qn¢ŸÌ]A óª:üc©'øc»GxÊÌVXtHE†Vc²@ °¸kïåú±¹áÙÑ\—Ž›§íí€ààþÂ.h½¶ù¢Öç\îÏ»Š¸ A¯³|ù<ÿü 444\Ví0`cÇeÈ!<õäSmË'NšÈ´iÓÐhÔlÜø_~ñ%VVVÜ}÷ÝL¸v ñ ¬^½ZtŽÓpqqaæÌ™¼ôâKÔ××󯛝“´oG¦¶•>|£Gä©'ŸBQà7_gøˆá$îMìt¿ÛÊ$¶•Y1Å_á›12?J¬Êìž¼vV 1n0ÔÃ,$ºk×Jì«”xw˜Ì®r‰åéR·‡Å>!ãoO'I4»oß»*$vUXåªpo…‡Ce¾ÎQq¦[æþŽ “|&ùBIkˆN&vUHì(‡ím=ÐIá­8™Ï³Uì(ëX|ü½Dž*…gÂŽÕ),OïoÅX7…‡Ê¯ƒ;.}¯ÄËuLîlìíJ™{¦ÝÃM7Ý„^¯ç‹Ï¿`ûöíUïή7wßs7S§Þˆ$©Øºe+«V­º$êÝÙrQoAo ¶³ÆÉÓŽš¢zÜ‚=…A‚V„ (‚^%$$˜òòòËîÁÕÕÕ•‡y˜=»÷àî~rÆvgggî¿ÿ~æÍ‹V[Ï¢W_eR©©Çxà åÕE‹ÈËË£ %5õ(99fù*!>°ððSÅÊÊ*Þ{÷***ÈÊÊÂFcÓ¥ýo,–ØXlÅÍ}e6^+³:Kb[¹tÎ9=l®õU¸Öú9˜Ä#5ƒ‚Æ“¢×î ¸>@fýµ2ŸfJü7ï±{û+Lï/³*SbuæÅØ×J®•èï¨pK_…A®…|a2{]‚Y8è×ù)XK ;ÊUÌ;H[X¹­•Â0…)þ s£öUAB¹Šørøs_ë£ðl’ íYÄP­AbÁA‰d¾#3ç€Df}Ïx+tRx²OÆf…Ï2%²ê/ýY±/×1ùLcïÙÊX[«¹æš«yêɧ°³³cþ‚ù>|˜êêj‹¨wg×›!C†0räžü¿'1™d^}íU"£¢8r†SK¨wgË-¥ŸXb½玃»-áãƒHúþìó4DxQš^%Œ&œ†@ЫLž<™M›6µ}Ÿ:õFf̘ŽZ­aÓ¦M|òÉJ"##™={& :"##INÞÏ‚ Q…)S¦0sæý¨Õj~øáG¾üòßÑ®ÚÚZþþÜßqvvâOþóIb`(ÇŽ¥’““ ÀÎ;FvN.×N¼–Ç{œššš÷ÅÌ™ ×7Ìî]»ÉÌÌàžiÓ0|¸üCÍ^x>ø 'MD¯×óå_°eËÖËFÐjëÛ¾kµõxx¸ŸR&;;ûäC@ŸúõíÇ’½KÏé8?¨ø¹ny+V¡¢Iáç"ØRzfÁh˜£Žÿ‹“ t€uE «³$²êÏì±¶¡Hņ"˜,óý8ËKü^zîÂÔíýdf€Ÿ ¦õ —\NƒÄ»Ç$¢‚¼îZÌá&vUÀUP!ñÑq‰<]ûö4™$Ê%ÊÍ߇z( õP¸)ŽÖ)¼”|nmX_¤"±Ráî ƒ¬°ä˜„ñ„7³|ˆmÛ¶#ËòeÙ¥ãÝÜÝ™3gË–.Å`èXð ÂÙÙ¶Ó}•À»f®jàïz´Žl«s¢°Y@ã\êã¢#«ÅHB¥'ÿ®mݧ ¸t­Û Tlâ–ÀZ¦‡¶ðu¹;5-V”­9Sø1. ÜéYCrƒ#sóœ1**úôÂïÙÕƒ •Vl-éoÛ̺B»Öngß¾øµ4M2ùüÛð“†;5ðË„Z¾©pcs­ÓY·±UÉLu×2ÙM‹Î$á¡–©2ZQÝbE…ÁšÊ5•Fk ›ÕŒwi ̾‰_«ù¡ÜOOúØv©nÝCQŒÉ>öv¥Ì‘#‡¹nÊuüøÃ888EiI™ÅÔ»3t:Ÿñ9nnn|óßoÈÍ͵øz[z?\úŒøë`üzœœ¤ÂÇwë ’×ï´|@¸G~ˆNCŠ@ è2®ŽàéÒ5Ï©¼2™³ååðáô´˜ ^uÕP¶mÛNV–ù¦í¿ÿý†[n¹™ÜÜ\23³ÚJ³³spuucРA„……ñí·ß´ísÀ€‹;BQ°±Ñ°jåJŠ‹‹yö¹g¸áÆøáûN)›ŸŸG~¾9:7'‡;vФ×sðàAfÜ7€‚üT’йóæ²?i?¯¿öÚe#&jµZ\\œOyø«ÓÖµ+çààÀ‚ùóX¹rééî/7/7ç³·Øh¬$®õÑ1Û¯] XK6V [K%:ªÂÃÏé‚'Wz;‚&ú–éþö Åz‰¢F(ÐAa£Da#X©žÇêàÙ}*ê @c¯žŸm7ÿ‚kz­?? â¾j^ ¨bɱŽ'm °S¸+Ha¼¯ÂO³R%Œ2€ O[OÞ¶÷ !!!ÔÖÖ2fì/^ €››+55µh4æÍŸÇ·ß}GJrr·ß`’ØP Š­ठ œ5¤ù|ÈmøWæIÌ×NÁÏNÁ×¹(ŒóÊ&xã°tQg‰¾ÔùË?|Ÿ ;H;–†··o¿ó#GŽ ::Š—_žÃú ëX¿aK—-éöºdÖ÷Ü$¥z‰”jë‹T|š¡âõÃ*>ÉP 1± êT,8höPüßX™ùÑ& Uˆ/Sñ·}*~/½tm(Æä {¶:ž© €»»;³fÏ¢¥EnûóÇêÝCbbx饗pqqÁÓÓ“˜Ø!ä¶æ´äz[z?\º žÔŸ’c4Tµ÷0ÎJ,¢¶´«§GŸ²<`°ÈŸ(t†ðPA—É(l!´O×.™ÅgŽ­3ævìØyʲœœ\>ýt5‹-ÄÉɉM›6óã?2xðàŽ‘™ÉÊ•«˜;w...ìØ±“­[·Y´ µZ-Ÿ~ºš—羌F£fã†ß8žf~XY¼øžxâ <==Ù½{Öo8¯cìÞ½›Áƒóþï!Ë _}õU¯†çu7›7mfó¦Í§,+//cö¬ÙÄÇ'Ÿ ~°‚SØY.±³ÜŠÁ®2Gk/ÿÔŘ|ác¯·· ½Â#?rÆñyܸq<øÐƒ¤¤àÕE‹,ªÞ±#!A²üÃå(ŠÂÏ?ýÌ‘#G,¾Þ–ÞO—&¶NÂÇñëÛ»:-“›\‚©ÅÄØ™±Äš‚ÚÖW_Gª ´Â€AHž^Š9DJj •:ñ~&d“'§Ëï_µÚšÀ@o‹­_@ÿ^ËÓ#Ú-Ú-Ú}éµ»»'P«»ï¨7ß|ƒwß}’’q5¶0ŒgK´vêëkQY©»T¶·ï'Ü<»”CQŒ_¢íÝÑn1& ‚Þ¾ö^®ÏñgcâcÃÈÜ]@yÖÙóûú‡{ãKVb!A±~$þïdnÅààþdgçˆ{Â.n½¶ù¢Öç\îÏ»ŠðPA¯ð / #“à¢ç‡Qoì’˜P|¬“ÑDÔuÈØ•/ (t‚@ @ \–ŒžÅÚW·ŸÓ6e™Õ4ë Ôþ?{÷_E•ÿü5s[z/ EJEQAAײ"¿ïî"èRTÀ{4(EÝ”-"EVpíŠ. kY)¡E ÔÐC i7=¹w~ÜaIB¡$y?HîäÌÜ9gî=Ÿ™Ïœ™IÉVŠ”Ce‘Z§ëï.gý—[9áõ•¦d¢HÅ”P‘Z%ºiáõƒØ½îCäPBQDDDDDDDj•nýÛ³ú³-j‘sD E©5®¸ù2öÿr˜œ´|5†È9¢‡²ˆˆH¥«DDÔ'‹ˆ\´l“þaÇ•N·Ùl 8Ùsf3xð 2ç=Ý1}xx8 -àö;n?åoÓ¦¿Å ÏŸTná¢|ùÕ|fÌœN·nW—ùžž"/»×¥hÉœcçm„bxxÃG #qÙr"""K§‡††2hÐ F=7 ·ÛÍ˯LdÍêÕìß Ìé›6mÖV“Ç0,B!4Ð$$À 4ÀÀfƒ‚" w¸s-Üy¾ß‹/âÁõ" ^¸‰;gxÉÎÓ¶-Ívìv°6»Í°0 Ó40°0M› †6Ó &²˜@L Ã7Ý4,Lнàñ@~‘EAäzÉ/„‚"ËR[‹H-Û9u@q‘ÚADäbйs'ºuëÊÈGFbYðò+éÜ¥3«V®bÈÁ´lÙ’± cÙ³gÏ)ó–w¬ÿ¿Çôùùùôés _Îÿ²tZÛ¶m‰ŽŽæÀ_GæääpOß~˜¦I||{žyö—k#‰\¨}¶óõFéé<öèã„„„Ð÷ž»K§·lÙ‚Í›7‘œœ À’ÅKhݦ eN?±ó¹".Ž¡C‡——G³fÍH\¶ŒíÛ·1`à@ŠŠŠøë”¿³rå zèAzöêE^^.³ÞÅwß}¯­_Þ‡ÂvÓÂa7°™v›ÃvÓ—³ÛÀn3±›VÉïàñZä@^—‚"È+°È/4ju[ù»,_’Ðßð% ƒL B BMB $À$вr­’ç%ÍmfäoèÁ~&Aþàñ¸ó½dçZZ9¤¦;qçZdæY¸s½¸ó 2s¼dç{ÉÌ>7í—Dš4ˆ´Ñ Òä’(ƒ&)i^¶ì/¦iŒ¨P¦aq8ÃË¡ ‹Ci^gx9œnq8Óª‘ƒÁìo```àKY\ab3 ì¦/h·—L3 ì6ßw¦ØcQìõýLs{ ò3ñxÁÂÂò‚ÇËÂ7ͲðZ`w䑟ï,}íµ ¼HÏõjÃå?;¸œ.»Ë E‹‚"ƒüB/E¾ï\A!äy),¶³C}šˆÔá!ÐÿzéÙæþ ËyED.´ÔÔcüuÊß9z4€;wârºðó÷§g¯ž<0ôAÒÓÓËœ·¼cýÿM(fd¤“—W@«V-Ùºu½{ßBbârüË\¶Ëå"++KHäBæŽ.ô „„„•å.}•å&22‚Œr¦ÿ¯¨¨Hž{ö9²²Ü¼þÆdìÞO·«¯¦ÿ½`åÊ´iÛ†øí„Õ²¼`”\ÂèûgarÂk‹’ä£õkÒòxâÒ8~‰ã¯—;&Ø 0, › ‚ƒÉÍ·c3(ß4ÀaÃnáÁ—,.ötóxÀ‹Ç ¯ïoÇ““×¢$i ‹6x½Õ¿=v —Ãòý´û’›.›ïµÓõ¢,"‚¼ø;,ü]^œèòârXääx ÉÊ#§ÐĹ&9Ùy&^|75L0M_»š%m|¼l¦¦‰¯]m¦¯¼·Ø"*Ä"ØßK°¿— ?ßïÙù&9ùÙ&9&Ùù&‡² v¤šdç¸óÍÓ¶—á‚„T¡Ý O8DzeØ|ü`×K Ó"ÀÏ"Ðå%(¢Â½”L÷wy öómƒì“ì<wžIÓØ"r LRÝ&Dzms›lO²‘šm;¥^†Ô«WþzÌöý;Q°Ÿ—ð`/áþ^Z5öÒµ­‡ˆ@/.»E‘²óLÜù&î<w¾o2ó ²óMþa•n'—Ý"4ÐK¨¿—Ð/aþaA^Â=„øYùyÉÊ3ÉÈ1É)4È)0É/4É,€ƒY&¹¹¾é•búþv°áûW]"#ÃÏ®ïò|à)yÁ^E:‘*¨ }»ùbqI„ ÓðÆ/öúF{¡°Øâ—ÝEun$ÝåMLšÄš8æagŠ—¬œ³K¶F„À°[]$òðò‡¾³_‡3¼¬ØZÌ]W;¸1ÞÁ?¿/`Ç‹ûMê´ml#5ÓbÛb2²•„‘Ú#<"‚Ñ£GóÆë¯SXXHP` .—ƒéÓ¦±ÿž~æ)nûÍm|òñ'g|¬ow8Xüãb€Ÿ¿?=nèAâ²e§”;~ɳa´k×–Q£G1xïŠE9ÿ.xB1++‹ÐÐ_Ó-!!!dfe–;½âƒië¤Ê0~íx† Áqqtît“_ŸÌàACÊìÌ¶íØ…ÃsÑn°zõ:t¬N|8 ƒ’‘z&úS“M°¿A?ÄFš8®_G~z<¾ËD-Ë*M¢z½–eà±,,¯å»ŒÔkáÁ72=ö¤Xdi˜kQPxñ´”?bíÌÛ3Øß7:Ï4àPš×7ºô\­w ö7 „ƒ“@‹˜@“æ1¾ËÄòq9,2s,2s¼¤ç@fއ© ¢ "ƒmD†øFbAZŽ—t7däxÈȂݽ¤g[däøFÖ$Õµ½E¤j¢Cá·×8iiòõÚ"6ïýµ£´™%ÿl¾ÛØLhÛÈÆŒ‘Ìû±6œ}bÑßÏ×7û8p$ÃCjæ…OLµ¿ÌäÊv:6·³+ÅÃÏ»=\ÛÖEÿ ŠŠaÇA©y…op µò1ô–+íÜç`î ØwÔû?ðYb±a^î¹ÆIZ¶ÅÜ Ëq¡\kpÃåù´îåOv¾—•[‹éØÜN¿îNŠ<ÛxHÚçaë/éº2ODj¨ÀÀ@Æ&¼ÀŒ3Ù¶m{Éq¼›={ö•^¢¼øÇÅ\qÅ•:Ö?%)a·SPPÀŠå+èqýuÜrË-Lž<™&—^Zöq¿eñË/›ÈÉÉ¥AƒKرcç)eš4jzNÚ¢ »˜1ô¡P»T{›ìȪy÷ªºà ÅmÛ¶óēӬY3222è~]w&MšDÊÁ”2§WÅõ×_ÇÕÝ®fÚ[Ó9rä0={õÄßߟÜÜÜ2;'¹8X%—A{ÀeçÐ!ÝKélÛ3«äÒè Íç»4Üw‹åS³šõêÕçÈ‘Bü _Ò1 äa6Ain‹]‡ŠIφŒïE•ü‘š+,Èâ·Ýœ´h`ãë5E|¸¸ð”2¾U”ÜõÁ×÷,O*fyR1wtu2þ>?¾ÙXÌý¿—Ë —_jÒ4ÖFã“` ðÝ7¿Üù¾[/ì?æ¥mC!Ap(Íâà1/ŽyIIóý;’qîÚÃ0 ¾™A§N:6·±e¯‡Ÿ÷xørEnéɨ5Û|q9<ؤIŒIÛ&Åtèã"2Ää—äb =G3 5ËCj&Íò”Þû7"†ßæbWЇW>ª8Cx8ÃÃ[ <´ojcü@æ¯(bÙ–b /ÌgåÒXƒN-ì\ÙÂNNÅžco-Ì'#Û÷™X½Í×@‘Á&—Õ7‰kbç·WÛðZÐo´¾k"R³8N^Hx?úˆõëÖ•Nßµk±±1ÄÇ·gûöí\Û½;ëׯ÷Å…ð0ÒÓ3Ê=Ö/ÏÂ…‹ûâXRSSÙ³{O¹ EÃ0èÐ!žðð0RRÊ>¿c‡n$.r®]ð„bff&3g¾CÂØœN; |MÒ–$€r§Ÿ©ÄÄå\~ùåL}ëx½0gÎ?ËL&ŠÈÅÃë…Œ‹Œ‹’ˆˆTÿ’Ý ÿ â›ÙùzmŸ%Ví¾óWjãÿ]ŸOû†.æþ·€´F¤Õ‹€öMÄ55¹4ÖÆæ½6&±û°‡ì|ƒœ|‹ì|ï)Onÿ’"6ˆ7‰ 5©aвƒØßÈÀÜØsÄÃîCö±8”V¹“,Çï/b#*"CLbB "CM¢B ŽfxÉ+4øyw1/+¨ðén/én/)9þ:”Óî{°WdˆID0Ä5±ûF•‡¸rÁ‘ —Ãའ؟Zùþý§d?%çѽƒÉCX™TÄ·ŠH9à 7üœþ.—A ŸA€“Òû@¸Lü,ü&.ß=¡ýœþ?—ï¡h†aðs²‡i ÈÈöR¯^xi2ñDÇÜ^޹½¬ö Þ!,ÈÔNDjœ®]»wqq¿Ž>LJJâ±GçÕWÿÌÈ‘#‰ŠŠ"11‘ÿ^@LL,_žÀÁC*<Ö/Ë®]»HOKcÑÂEeþýø=½^/¦0éµIääè¸^äB1"£¢-Ã0ð=p¤äÑ#Æé/­ñzŠ«u âç²qy›Ø‹výêÕ«_'/‰T½UoÕ»jÖl8¨H'çÛisTªì…ÞŸxv@ ÙEþ¬H*ª¶ïq¸ówvuòÃOED…ÀåM[lÙWÌ–ý^’UÏI› ¢MFÙhiÐ(Ê$4Ødïa»y((òÝ&$ÈÏ$Èß ÈÏ"Ðåûý˜ÛƒËar,ËKF¶EzŽÅ±,‹ô’[HTåv•éÃl&D›Í<û6èÒÊÁµíl¤¹á›õ…lÚ]ö2ý¡uC“6l´nd£ È"Øß$¯Ð"·€’ÆAA‘En¾EA±ïarù%•Ë/ò=˜§  нV½ïVŸ,":öÖÖãx©¾ýóʲ«YEDD¤®šóm>—ÄVïîЖ½¶ìÍ£ûåÒ²)ÉVÝ<^Ø}ØËîÿ.Ûi‡Q¾$£¿Ë";ÏàPš‡Ü‹Ü‹ì|_­ èÂÜ.Âã¥Z’‰+·±rk-/±qKG¿¿Îà?ë‹XºÙC»Æ&­›´ih#,ÈdûA/;zY‘THj–F½‹ˆˆˆœ-%EDDDÎ%¿÷÷,,†äCÕ7 ²&ØvÐöƒ¢CM®mg£s+;…Å»yø`q1‡Ò=ú0ŠˆˆˆT3%EDDD¤Æ;šéå³D/†zÆžˆˆˆÈ¹¥»C‹ˆˆˆH­¡d¢ˆˆˆÈ¹§Š"""""""Rç´hÑ‚7ô C‡ö öpéôþ÷öçöÛoÃ4m|÷íwL›6ý”yûßÛŸÛn»‡ÃÁ÷ßÿÀÔ7§`³Ùèß¿?7õ¼‰Å?þÈÛo¿£vúÞÓ—¾}ûâtþ:ݪAg϶MÊ*Óëæ^ 0À÷ô…_3{Öìõ9ÑE©SÂÃÃ>béDDD–NïÐ!žnݺ2|ØÃ<0ôâ;tàŠ¸¸“æíܹݺueä##öÐp:vì@ç.2d0íÛÇ16a,ï¾;KíÒ¥3±±±ôë×—'Ÿx‚Áƒ†Ðºuk:uîT'Ú¤¼2¡¡¡ 4ˆ„xxÄ#\{í5´k×¶F}V”P‘:%==ƒÇ}œ¯¿þú¤éÛ¶mgÌèçIKKÃétívËŒ™¾‘e©©Çøë”¿sôh*©©©ìܹ—Ó…Ÿ¿?={õdüø $''ãõzÕ.N………ãõZ%ÿƒˆˆˆˆˆTM¹ä9++‹ÐÐÒ×!!!dfežv>Ã0ÈÉÉaذ\GçNW1ùõÉ 4¤Ìƒ‹†š’‘}ñ¶CÆŽL  Î}PUoÕ[õ‘s±ÏS­q¿.Õw‹ˆˆˆÔ5&¡¸mÛvžxòqš5kFFFݯëΤI“*5ïõ×_ÇÕÝ®fÚ[Ó9rä0={õÄßߟÜÜÜSÊîØ±CŸ ‘ìLö÷EDDDDÎ\I(fff2sæ;$ŒMÀé´³pÁ×$mIªÔ¼‰‰Ë¹üòË™úÖ?ðzaΜ–™L‘šïlöDDDDDäôŒÈ¨hË0 ÀÀ÷“ÒŸñzŠS ŠˆˆÈIÜî L›£Reµ?!""rþb¯â®ˆúˆê¢G‹ˆˆˆˆˆˆˆˆH¥)¡("""""""""•¦„¢ˆˆˆˆˆˆˆÔi11ÑŒ÷Ÿ}þ)³çÌæî»ïºh×µÿ½ý™÷þ\>úøC† V:Ýf³1pà@fÏ™ÍàÁƒ*\Æ¡C˜1cZéë¾÷ôeÞûóøø“>bxé­ðZ´hÁІòæÔ¿—¹œððp.ZÀíwÜ~ÊߦM‹ž?©ÜÂE øò«ù̘9nݮ֯SBQDDDDDDDê´±/&°}ûvîí?€Q£FÑ»Ooºtír^×áŸïÍ!<<¼Â2;w¢[·®Œ|d$ÃNÇŽèÜ¥3C† ¦}û8Æ&ŒåÝwg•»ŒÖmZsíµ×”¾Ž¥_¿¾<ùÄ 4„Ö­[Ó©s'ÂÃÃ>béDDD–»¼üü|úô¹å¤imÛ¶%::ú¤i999ôé}+wÞqSßœÊÈGGêƒWƒÙÕV‹-èqC:thÏða—Nïon¿ý6LÓÆwß~Ç´iÓO™·ÿ½ý¹í¶[q8|ÿýL}s*à;+Ñ¿nêy‹ü‘·ß~§Ö×ûOz˜[o»õ¤rË–%2~ÜøZ¿½ûÞÓ—¾}ûâtþ:ݲ¬Z_ïïåŽ;î 77YïÎâ‡~¨uŸó²Êôº¹ ð=¹vá×Ìž5[©ˆâ´b–ún‘->>ž#†‘——GóæÍY¼x1[·neðàAñÚk¯³lÙ2úôéÍ!ƒp8|òÉgÌž=çŒæ/K«V-q¹ü™3知‰¯™3Þ&((˜âéÛ·/cÆøFÚ½ð<ÿùæ?dçäòÐCCñx,~þé'V®Z}Òë™3ߦg¯žÜß@ì;ó¿øŠyóæqE\üãýädgÓ¶][Ö¯ßÀ„ñøäÓñ÷÷gÞûs1üa²³s˜0qC‡åÝY¾“œ ,dóæÊÇ)‡ÝÁ¢E‹èݧ7[·n# €ø˜1}7ÜØ£´\`` -Àãñðüó úòÔ`ºäù:žíÿúë¯Oš¾mÛvÆŒ~ž´´4œN'Ùn711±Ì˜é;Ó}âY‰ÔÔÔSÎJŒ?ääd¼^o¨÷‰BCCiß>ŽeËk}½Ë;“TÛë}Ùe—±zõZŽ9Êž={Y±bqqqµ¦Þå•iÙ²›7o"99™ÔÔT–,^Bë6mÔ™Š(N+f©ï¹ îÕß<}íiÿÝþ̵týýe.c×®d’“w“––ÆŽ;ùá‡ÿ’——Çš5kK/îÜù*Ú´iÃgŸ}ÌG}@»vmiÑ¢y¥ç/ײ¨Ê€ù={ö’œœ\:ÚþÄ×W]Õ‘V­Z1wÞ{Ìž3›6mZsY³Ëߨ¤¤$Üî,’““ +gý*iÁèÑ£yã7(,,$(0—ËÁôiÓ8à>œN·ýæ¶“æiÕª%—5»Œ/çyÒô˜˜Xî¸óv†y€þ¸—æÍ›Ó±c‡J·ƒÝá`ñ‹¹êª«ðó÷§Ç =H,#{ü’ç[ûÜÆ³Ï<ËSO=¿¿¿¾@5”F(^„rrrÈÉyïÏ%<<œ÷ç½Orr2ðëг=+QÛê}¢;#…‹áñxjýö.,,<«3I5µÞíÛÇÑ»Ï-|úɧÇ¡”Cµ¦Þ啹첦de¹K—••å&22B¦ˆâ´b–ún‘ Ê/ÈUé²þAÎÓ–±,ë¤[b”<€Ï>û‚×_ã¤òñññ•ž¿,‡RRhÔ¸ñIÓ:tìHpP ï²\£jíòå—_ñæ?ÞûôóÒûü®Y³–wÞ}‡””ƒ¤¥gTjy;wîäÝwÞåÙçž%44„Äe‰üøß+œgÉâ%¼6é/<õäSdee3ñå <ä¤2]»v!.î ââ~ÝHJJâ±GçÕWÿÌÈ‘#‰ŠŠ"11‘ÿ^@LLl™Ë9ÑŽ;™ÿż6é/8N¾ÿþ‡*µë®]»HOKcÑÂEeþýø=½^/¦0éµI5êØ]N¦„âE(¾CtÒ´#G—¹œýû÷3t胥¯?ýôs>ýôó2—›••Åïþï÷eþ-=={úö+}=r䣥¿ÿøãb~üqqi¹>½oÕ‡½QBñ"´øÇÅ´nÕŠiÓ§áõZÌÿâ 6nÜxÒ™…3=+QêÝýºîìܹ“Çשí]g’jb½o¸áö k×®clÂØZµ½Ë+0sæ;$ŒMÀé´³pÁ×$mIR§)¢8­˜¥¾[DDDä¼2"£¢-ß @ÒV憠^OÁÁajA9‰ÛisTª¬ö'DDDÎ_ìUÜQQ]ôP©4%EDDDDDDDD¤Ò”P‘³Ò±cnºéF5„ˆˆˆb¯ˆÔ‘>B E©4%EDDDDDDDD¤Ò”P‘J³« DDDäLtìØ˲J_©QDDD{E¤õg‘P4ðx<Øl6m9‘:$<<¼Ü¿y<ÀÐþ„ˆˆÈE{wEÔGT#2*Ú2 0ðý¤ôgE,˃i˜¸\þêŒDDDÇCAAžïì¨Q¹»ªhBDDäüÅ^Å]õÕ¥Ê EËëÅ ' «‘:Ê00 ã wV´?!""rþb¯â®ˆúˆêpV÷P4LCÏuíOˆˆˆ(îŠH¡^DDDDDDDDDD*M E©4%EDDDDDDDD¤Ò”P‘JSBQDDDDDDDDD*M E©4%EDDDDDDDD¤Ò”P‘JSBQDDDDDDDDD*M E©4ûÙÌly=XX`YjI‘ºÎ0000L›ö'DDD.ÒØ«¸+¢>¢:T9¡hy=˜¦ËˆÍfÓF©ã<¹x½žJï´hBDDäüÅ^Å]õեʗ<[–…Ë NHDDD°Ùl¸\Xg0âAû"""ç/ö*¨.gqEKˆˆˆœ²Óg²Ã¢ý ‘ó{wEÔGT=”EDDDDDDDDD*M E©4%EDDDDDDDD¤Ò”P‘JSBQDDDDDDDDD*M E¹(4lØ…‹°pÑn¿ã7–ý×ï³pÑF¥†9Ï”P‘óÆf³±pѨƩ1×n·3xð š4iR­Ë}ï½¹,þqI…ež{vG¦jc‹ˆˆ\v5ÈÙ äãO>*}ŸŸÏÁƒøäãÏøþûï/ºõýÇ›#=-ƒ1cž¯ÑíÞ±cîÿã¹ôÒÆ¤§§óÕ—ÿæã?àeèС'•ÿü³Ï™6m:5âá?=Lóf—‘ž‘ɧŸ~Ê‚/ÐYDDq\q¼ ëÈ 7Þ@rònvïÞ]mïqøða233+,“œœLQQQµ×¯s—ÎÜß}\ÒàöíÛÏôiÓÙ¸q#€ö#D¤Z†A¿~ýèÝç"""8tè0Ÿþ9‹.:%.säÈQ¾ýÏ·|ðÁ4kÖŒ¿þm Sޘ¢E_—–›÷þ\¶mÛÎØ„±j`9/”P©&Ë–%òÍ7ßàrºèݧ7O=ý$G-Ý•êS¿~}ƾ8–õë×3kÖl®¹¦ƒ‡ âÀý,_¾‚¨¨(Ž¥¦òÖ´é¥óØ€ç_Maa¯¾úg:vìÈŸþô0{vïfÓ¦ÍjXÅqÅñ3”‘‘ɽýÔª}Œ1cF³níZfÍšÅu×_ÇKã^dР!¤§¥i?BDªÅƒ>ÀíwÜÎçŸAÒ–$ââ®`äÈGÀ²X²déIqÉfšÄwèÀÀû`š&«V­RÊEA E‘j’’rˆU+}ûöíÛywÖ;´»¼-7nôÍ~x-[µäðá#¼û\±€«®ºŠBxx‹/áÚk¯áß_-`ý† üùϯ0–U+Wμ÷ç2í­é|þùç.³gÏ›øýþ@dd»wïfÚ´élÙ¼…O?û¸ >þä#îéۯܲ'º".®ÜuÙ¾}¯MzyóæqÍ5×ɲÄåLž4€ŽW^ɰ‡ ,<œÅ‹O¾t©¼:´nÝš×ߘÌÂ… éÚµ+oÏ|›ï¾ûu”Hûøöòæ?¦røða6oÚÄ­·ö¡Yóf% Åhöï?ÀÒ’`|\Ó¦MiÔ¨1ãÇO`ÕªÕ¬]»Žž½zqÝu×áñxyýÉ|öÙgtêÔ‰èèh–.]Êòå+:d0Á!!¬Zµš×þò§Rí&""Šãç:ŽŸ¨¼˜[QOJJ*3æ^uÕU ôG6jHJJ o¿ý«V®:ezpÿ|oNi½š6mʰáÃhÙ²G¦òÉ'Ÿ°há"Úµk[áþBEª;æßϘ?ÿKºtéŒËåÇ·ÿùï¾;‹¶mÛàp8˜9óöïßϺuë¹îºë¸¡Çu¬_ÿS¹ûJ(ŠHe………ò›Ûâ…‹˜1}K–,¡EËÜ~ÇoJŠ'Æ¥åËWÇU®<£„bEýÿçŸ^aÿZ^hß¾= {ˆèè(—%Ò>>ž+V0õÍ©Æ7©]tE‘jær¹¸ñÆØ·w~~~L˜8?&ŒŸÈÖ¤$FzŽØØÂÃÃ=fî¬lþòç¿PTXHHHÈiߣ¢e6lØÇŸxœå‰‰<ýÔÓ’ð¦i2|ØöîÝËÆ¿0bøÃ–=S]»vaî{sùö»ïéÕ«'W^y%¡¡¡Œ3 wvo¼1»ÍV©:wõÕÝøá‡ÿ²wモÞkÑÂEÜÓ·‡ mÛ6lMÚ @tt 5dÞûóø×ïóÈÈ?ár¹ˆŽŽ õèQ<ii©DÇD—.»U«ÖÌš5›Å?.榛nbèÁ̘ù6ÿþêßôèq=×\Ó­ZÛMDDÇ«ÇOTQÌ­ŒcnTTÏ¿0†äädžzò)Ž=ÊsÏ=‹Ÿ¿…ëÄÄ—'ât:˜8ñeV®XÁȑн{÷ ÷*r.cn«V-y{æ;,]²„{úÝÃ7ÝÈ¡C‡èÞ½;~~~tèeYÄÖ«_©ý‘ÓiÙª6› ~:iú¸—Æóê+95icš´i󠯯˜ÒãŸêPQÿZQá…ÆP\\ÌoL!/?Ÿ˜’~°2ÇxR{h„¢H5¹çž»¹çž»(**bÞ¼y,[–H÷îÝ‰ŽŽâϯ¾Ê/¿lbãÆÜÔó&ºtíJ^^~~~¼õÖ4¶oßÎúõ¸ó®;Oû^¾te/sÓ/›0 ???rrs÷ÒxBCC8tèÅÅEpøðaš5kVnÙ35÷½y$&.gË–-ÜqÇí4jÔˆðˆpüýý™úæT¶oßΊå+¸ù–›O[‡m[·0sÆŒ“F&–%&&šÇŒ•+V°zõÖ¬YKTTËiÞ¼ÞKFz&»’wàõZ¥ó{<N‡³ôõgŸ}Ʋ¥ËØœL¯›{1þ|–-]ÆÚµëè÷ÿúѨQc8Xmí&""ŠãUã'-³s§rcneœsxæégÙ·o999üç›ÿpå•WÒà’KعsçIëUºŒ®]»ÊØ„¶nÝÆª•«èر#½ûܼ¹óÊÝ_X»vm¹ëår¹ªsRº=“’’xìÑÇO)óñÇŸ°tÉR–,YBç.]¸¦[7^|ñ%æÏÿ’÷ `à}Ø¿ov»Ã0pº\§Ý9À€@²³Ý'M?vìÇŽ#00𔸰cÇNÞžù6aaáÕ²õ¯¹¹¹åÆÆÈÔ7§²yóf–.YJŸ>½Oßæ1_¿–QBQ¤š,Y²”¯}Í 7ö G|_²S À_^;ùlSLt4YYîÒ„3QÑ2çïÜÉ”7¦Ð·ïÝÜö›Û8t(…Ù³ÿÉþýûOYÎÎ3({:–e•ìX{°ÙMÂK‚]Yõ«¨ÇŠ……¾gxx8&Nähj*¯¼úëræ¾7·ô÷U«VÓ¡c@ll IIIØl6""¢X»fÝÕëLÚXDDÇÏU¯lÌ=Ó8Þ¡C;t 00€/>£mܪUËJµ›ˆˆ(ŽŸË8ž˜¸¼R1÷Lãx@Éåv±±±tìx%wÞyðëÅ×aÛ¶í¿®ÃÊU;vŒá#†ñþ¼÷iwy;š6mÂäɯWy{œË˜ûÛßþ–¢¢"Ú·#&&š9³gû¼m6®¹¦:vä–[næÛo¿eçÎÕ²!"u<Τ§³há"z÷éMnn.Û·ï >¾=­Zµâ¯ýÛiçOKKcãÆ_øíoï"ÛMê±Tzöì‰išü÷¿ÿ­ôq\EýkEq`íšµäää0lø0þõþÄÇ·Çápœ6¾ýïÉ.©ùô‘sàƒ?¢¨¨ˆï%//QÏ"#=ƒ'Ÿz’!C‡²á§ ìÝ»—ÌÌL&ŒŸHxX(Ï<û,þþ~¥ËÈÈÈdú´i´lÙŠ‘#Gr¤äà@…Ë\²d 3g¾Í]¿½‹)}ƒ&M/eü¸ñäççðé'ŸátÚ>bøiËVf]*âvg1~ÜxÂÃCyâÉÇÈ/(( $ÕátÚ·£e«–„††2vl¯Mz×&½FÂØÆ&¼ÈÑ£Gxæé§¸ûî»ùä“Ïøàƒ?nY™™<óôStêÜ™)SþJÒ–¤3Ú¾•m7Q?—q¼²1÷LãøòÄå|õåWÜ~ûo>bÉÉ»¨Iý ×!//Ñ£F“ŸŸÏ3Ï>C70cÆ þóͪ¼-ÎeÌMM=ʰaøñÆøèÃøþûßCž|êIZµjÅÛo¿Ãë“ß(§:ö#DD¦N}‹÷çý‹ë®¿ž'ž|œ¶íÚ1å),\°°RóOœ0¥K–Ò¯__žxâqÂÃÃxé¥qlÜøK¥ã*ê_+ŠÙÙÙ¼øâ8üüüyò©'  ¤¨¨è¬ñ¤æ1"£¢-Ã0ßOJVÄë)"88L-(Ró¿ü‚÷çý‹9sæ¨AD¤Fr»30mŽJ•Õþ„(ŽË‰6lÈŒ™Óyï½¹|õåWdff–[¶iÓ¦¼øÒ‹$%%1qÂÄJ¿GëÖ­yýÉL˜0‘¥K–ªÑ¥NÅ^Å]©!!!deùnmÊ¿>x¸øq» IDATŸ9³çðþûÿRãÔ‚ýóÊÒ%Ï"""""rQ¹÷Þþdeeòåü¯Ê-óò+ %IEDÎÓ4™ò××ùÏ7ß±yó&îºëN<+W®RãÔ1J(ŠˆˆˆˆÈEaÿþýôé}k¥Êþîÿ~¯9ϼ^/3g¼Í½ð¿ûØ€ ã'°k×.5N£KžEDD¤Zé’g‘‹3ö*¨.z(‹ˆˆˆˆˆˆˆˆˆTšŠ"""""""""RiJ(ŠˆˆˆˆˆˆˆˆH¥)¡("""""""""•¦„¢ˆˆˆˆˆˆˆˆˆTšŠ"""""""""Riöóùf-Z´ Ç =èС=Ç=\:½×ͽ0`N§… ¿fö¬ÙN?Ñ™ÌÄ„ ㉉åå‰/óóÏ?ë ""RÃTfÿ ï=}éÛ·/N§ƒï¿ÿ©oNŲ,5žˆˆÈøÓŸæÖÛn=iÚ²e‰Œ7þ¤iƒ‡ áž{î.}ýþ¼1gί‹Ôbç-¡ÆðÃH\¶œˆˆÈÒé¡¡¡ 4ˆQÏÂívóò+Y³z5û÷(sú¦M›«Ž©Sß:åoáᤥ¥W*fëx]¤v¸à÷P !+Ë]ú:+ËMXhh¹ÓÏfÞåË—Ó©Sg^}õÿ¸˜k®½†ù_Ì×§@DD¤†¨ÌþAzz:Ÿ~ò)ïÎz‡?ú;wîbóæÍj<‘³pçw°pÑ"<Ï) à™gŸâ³Ï?%alAAA:^©åìãJ•w£ÊÜû¨¢yÝn7# ÀÓÏ<Íwß~ˬٳ(**"á…8x0å”ùbà nïæ_¥zäXø»Œ*Í[PdárTmÞ¢b/{ÕrÅ^¯…ig±íÀ¨â쯅ͬjÁQÅOs~¡…ŸÓ¨â¼^üœUkëÜ|~¶*Í›“ç%пjï›gä_µúºs½˜5l^‹àã¼Ï›ç%¨ŠÛ('ß"Яjï;óß¹ŠlRçüo쉉åŽ;ogèÈÍÍ%al;v`ݺõj,‘*p¹\ÜxÓŒ>¢Ì¿O}s*‡";;‡‘#áž~÷°oïÞs~¼."ÎO(feeRú:$$„̬Ìr§WǼ={ÞÄæM›‰ïз¦¾E``½n¾¹Ì›º7kv) êU©nEpT-_„Ç ¶Óä"‚ƒƒp—q W±e`7.Ìç ×T¬ ¯Ì*ÖÙk˜U¬s±×ÀnZç}¸ìV•ê[Pdârx«ö¾g3o±‰Ë^µyó‹MüN3o¹õ­D[•»Î….gç-2p9¬ Pߪ·3(¡(µ[eöÚ¶kÃæÍ›Ù¿?K—,%>>¾Ì„bóæÍ«uý5jÀ¾}êä¶©«u?“zïØ±C_b©‘né} Ë—““s꾦ÝnçÈ‘#>|€o¿ý–>}ú°é—_ÎùñzuÇqÅ5µÏ…j›š¸pÁŠÛ¶mç‰'§Y³fdddÐýºîLš4‰”ƒ)eNß^ÒÓ3ª4¯Ýn'®}{&OšÌ°aÇGÕ•=hÕúÝlÛn^”ï²Ëš²kWÝ:KS×ê¬úª¾"R¹ý†÷öïÛÇý÷ßGýúõÉËË¥k×.,\¸ð¼í¼Õå¤Q]­»…µEР±ä}û>Å—©A¤N3M“;3QÏ:iúñ¸k“&¿FBÂXì?@=سwïy9^?×ý¯úwµÚ¦|<¡˜™™ÉÌ™ï06Á÷Èø_“´%  Ìé11±L|yC9ãyŠ‹‹™>w¿®;;wîäðáÃ¥ÓNŒ»EEEL™òWž{î9ÂÃÃØ°a}ø999:^©Í±22*ÚòeûÒ¬¿Q‰›ày=E‡Õú²Û ,èb¡˜\§>°u­Îª¯ê{¦R3½Èùc&Ž60C")ÞùÞ£0cãhÛ…¢¤ÕxíÆÙ¥7˜6ŠwlÀÑ®Vv:†ÓEÑî-8Z^Yúº`ÍwXE8Zw ‹¦x÷f¼)ɘ1 q´½OÊ.l‘õðºÓ)ܘˆ_÷»ü¿ÇðÙGÞ—3)\ÿß‹¦]Üî L›£RekÛþDóæÍëìÙúºZ÷ëmÚMB¢ð vâìÂ?Ø…_°“€P?ü‚|7u:Î ÌŒn€£]7¯¶|Õ÷ïwlH=ŽÓ´G° Zs>‹Í—¯c5ÎË Ýñö#o@æÓûë§Ÿ7aQÝ4”TØŠ~÷XJ ÄÍ®ûÈ”Nöè4t•z ÕFôUŠ3ËÑkŒBF6€¬sÈE:ÕRUÔ©&Z¼E]BùK÷ õêŒãôyTÿ¾s~fzX»yMÔUÖ¡+Žw/DæÝ ‰«ÉHåOƒLŽãôyH]ÜÁÞÕ¨[¨Ú°¬ùëЫÎëËCûa?`e'cyuü[žQMš‰¼sÈyç8× x¥}˜-÷Ï@p-!" ñØ÷‹"8íŸQ¾ôn¬új”ãîºl9}âÔ<µZ}Þ±¼svaC1¦§zóÇXõÚšÉÎßèöþLÙ‹wbÑT è3”ò§n¨™èœŒ£òíy]G5úöóþä½°hÔâÆ Wc@-“0ã­‰ìûì;?Œf߯xbJ&áNíË"ëX)¥BP6„n÷”/Ÿ…Õ …å¯ÌF»ë;¤.žÈûÔ]æ=l+u5g&¡^ùUŸ½‚v×wHä ì‡Ü€þðSQŽžŽrøÍOÁ»³ÙëWïõãjVBÚõŒ¢×¬z-òÎ!hÿúž²%ÿ‡¹ðlç¼’>ÌÖûg@ ¸V+@ h²N5Žø=XŠsP¿ù(HåÈý»_²œñÄALÉÑ(ú^Þ±rò(‚ú ª™ÌZíUÿNØÎžÆZšµªÉ…+7.@ûÛúš Ù¤Y5×HM¤òÃg@¯7N hf¤r ÿ÷Æ~Z²[£ aÊÏÀ’Ÿ¥ªKnj­qK¢rÂj,»¨LcôpKÖUâê…ËÓ!só®-#Q9!ªú×§? úÇ÷¯Jý껾)9‹Vƒ¢×¤^~˜rÒ¨0ÄìÀZ’‡¹´™O—fíÃl¹‚k aP  XµU5Æ•@Ö­7© «Ñð÷Œ¦‘]ìßç«Ú¸CÌP9c5é± Þ¤úi[ÕbAIåšç„1±*](H-Â4m m/cÀœ@6½´[CÐjØõÌÍ›ªoVbŒû÷ÿýþ¯žë9èßçµç Ì9©-w}“cÂ^ýÆ UØQýëz¬¦š­ÿç«Ö‡Ùzÿ,×Ì8Hˆ@ ‚ÆcL<€õÆÙ8Ü6cêq”ç`8¶ÝŸß u+Š®=‘zuÄR^tÙóé œ<Õ­óu A6KAúKlO³h5È:øc?ævô».vh¯ÛºÝVq¯ê½‡Z3½ÆtE®Ts¢PDÐ(*9·.ͱ/2…0­ŠU§À.|8vá5F.«DŠÄÓÕÄ3O"‘ËQMš…!n'Ö²¢¹>€þÈ_Øþ{ ð‘¿(X§>„ãí 0&G£«÷¼MíÃÚBÿ,ׇ¢@ MÀœ›FÕÆe P¢¼n†Ä}Tý?Ì™'Ñíþ©GäaÃkWJ\vBVx–ªu‹±ªKPŽšÚ_^²Œîϯ‘¨œPt nHIØžJ¿›C… ÂÞQÁ-/ŽbË+{„0­Ž!f;ºÃÛ‘…#õê„EWܧ3Óç#QØ£ýé}ªx©½ ‡Ûæ·ØõL'c±hÔ˜rR±faÎIEûó‡H”(úÃjÐcÕëDÿ,m‰§—·U"‘jþ¥ößKÑ^ÂÍËeàæd›v×ÀÀn¤¥¥·«¶½µY´W´·±WXDÏ&hu*+Ë‘Ê ÊÛÚã ¹½Œa3ÂÉ8šGæÑü+>_pp0)))íò¾·—¶+í¸ù¿ÃùõõýÖåZµ^(Aûž|:»áòôZL§`5ê°:cr UýW§…úÞö2ýZ3¾·R V‹Uȧ?;Ÿ7±BQ A»æÄŸiôܲ«*>AbB×äö²V¹®B)§C¨'“ŸVkLâ©ÄpdòÞC°0ã‰Ch¾~SF °aLëÁÿ½>Žî#ü¯úµ&ÌLç>ÞBèmeL&D  =£)ÓQœYAà N¤Eç4ë¹¥r)îpíàŒ›Ÿ3œpõuBn/'+¡€®ý:ý}Ò5/c…J†§¿;Þ®x¸¢)ÕqâÏ4ªË¾ÕQ*“vC^®dÆç‘r ûêVZB½èØÃ¿P/œnx¸ãà†ÊÙŽ’³”åT’q$'7üç:ŠÒJ9¾3²u½ç’)¤„O ¦ç¨n$íJçÀ ô½9”N=¼‰Z´Ùë6!¿î^xws£ µ”¢ô2Žl9Ey^¥xQ@Ðnûè@Níͤ8£€øßÏ•AﱄO &/¶‚æÚÕ;å…‘ìÿ"ª-¡#ð p¥8³âª¶¯¾­Ü‚†# Š@ Ú=ºJ¹'‹Ö…3ûÏ^ñù&<>˜Óû³HØzæ’ùÒcs)L+eÂü!$n?CÊÁ†4=ý]pós&ÿL šÒæ7D* Ìóeó:z¨ð p¥k_/BnöÂ3ÀÜäbª+t”d–sfU%ÚóÊ©‡sèæÃÐaè5NìL#ïTÉ¿ƒT{á“BÚ…ä¿ÒÙôÒîÚߎþršN½¼ù¿7ƳóÃhŠ3®|ÒáéïÊÄC8µ7“äÝéìÝX!^ @ ´Y‚‡v¡÷˜nì^GE¾¦QeÇ<<€ÔÃÙ¥—Ÿ—®×9²å*;O gÚ‹£ˆÿõ4Gòš\Ï)Ïàð7‰µc…´èlº 83áªÉÆ­£3Ãf„³gC<•Eñ°4aP@ NìLç†ÿ\wÅÅ©‹FrdóÉY×”êøãíƒô½¹;{ø°çÓºWÝÙ;Ù4¨ÁC:cÒ›)L/%ü†¬V+ù§KÈ;ULþéôãÕ¿ï”PÜýœñ ò@"• «4 ¯2 ÓÐUêÑU¨*ÑÒ©—7^®˜ôfJÎV`ÒYH>œNéYuƒ¯•XHvb!>îôDÿi=ˆÛ|’N½|Ô™¤¿ÒÙ¼¼îHÊ9IEäŸ)eĬr’‹HØÚôe=GеoG6½¼Äb@ ¨åñÇçqãM7ž—¶ÿ–/[~^Úø ã¹÷Þ{±³“³uë66nØØèt'''V¬Xޝ/¯¾ò* â4‘Áwô¦²¨šý_cäý}É8’Oⶆõ“£ìGz\.)¥õæÑª dí/¡ ,Ÿ^cºÒ}dÉ»3ÈŠo\€»›ÿ;œÃßG]T]›–u¬€¾“CQ(åu¦f—M@ßôÄ/˜8?,þK<0MDæàิ)Qž­V ööÊk^@R)(í$6Y7wwwÊÊÊÛÕÛÞÚ,Ú+ÚÛXªõb&,h} iÃg´öxB*—"“×Ĩ³˜,(¸ø84iÅ›Ü^Æ„gõÉÊóª]>ÿt )Œ›;ˆÜ“Åè*k"Dv ÿ´DÞJu¹Ž¤?Ó8µ7‹¢´rRe“ª¹”N=½é7µ;Aƒ:áäå€Õb½heà¥è9º+“þsù§K8ö{ §ödrzÿYrNR”QFUQ5­ ™L‚ÜNFQj)ÇwÖÔ%7¹{“¹™Mºš2Y ”fW8 3šr‡¾N¤4ûÒÆI«ÅJÆ‘<|ƒ<ˆ¼1„Œ£yXÌÓƒ×Ïé‡T*m²/ËÆèr“Þ,„@ hÕ¾·±ýntt4_~ñeퟻ›ÑÑÑdffÕæquueñ‹‹Y¼h1?oÚÌìÙ÷“–šŠÁ`hTzŸ>}(+/gç¸ëî»Ø½{7 ž˜Ï±c ˜L¦—©‡‡¥¥¥mîY4½šRé±¹µ&Òbrñ ö ÿ´žä.ÆP]ÿ‡ÇQ³ûr6±ð¼—êÿ óŠÈM.¦$«‚îÃý ›Du…uáåWýM~v±›’QT_ô›L!ý£…©eÍ*›°‰Áø…z²ï³c˜ f,f+þ‘¾ä&_ÕgG¡”c1YÚÌø¼¡ƒâå& (Ú,Ú+ÚÛ†Ú+ Š‚¶4©±…ñĹE€¢Ì F=ÐãÛSu¦¾0’ãße“ŸÝtGâ•EÕ¤Çæ2|fö Æ>:¹ŒŒ#¹ýå…ieè/˜u&ÊóªÈI*âôþ³g–£r²#hPgÝÖ _G¬VêÜêĸÇR]®g÷º#”œ³ÂÐj±bÔ›ÑU¨*ÕR‘_EÉY5¥gÕT•h1-ͪÓôUFòO—P–­nT¹âŒrÔÅÕÜðÄÔ…U˜ô&ŒºK˜òüHNíÍ$õpÓ}g ƒ¢@ hK}ï•ô»®®®Ìœy«W„Õúï˜3<< W7W6oÞBuu5înn¸º¹!—Ë•®×ëqrr$33“þýûªµZN?Ñ*2m‹Å·õDW©'õŸÐÅ唜­`ĬHävRŠÒ.î·FÜIîÉâ×Îíÿ Z9'Š(Î,§ÇÈ®ôHu…¾Þ±Ç ‡qdË)*ò«êi4½7I»Ò›M6#fEb6š9öû¿îhÊrÔôDy®šê }³?;*W{ÞÖ‹£º’“ƒÕjû:¢1ƒâå& (Ú,Ú+ÚÛ†Ú+ Š‚¶4©±…ñÄ…E«ÅŠL.ÅÃߥÁ_Å}‚=13’ßß<€›«Û¿×“…ôØ\Üüœ‰þ!‰ìã…T•6|¥¡^c¤4»’¬c¤>‹Ü^N·þzwî~ÎTTÑ©·7£êÜNÆÁ¯´Á–u¸V­çÔÞ,v¢ÿÔ„À­ƒ2… ­Zžñ3pp'†Üч¿ÖÄRž[ÕbíE@ÐÚ}ï•ô»wÞyIÉI$8EwÏž=pusãð¡Ãt눇»;Z­¶QéÛ¶mãÞ{ïeÚ´©lúiC‡ eíÇk[M¦mÍ Øÿ–è5¦z#0ë5FRçЩ§7‘7…’›\T»¥xؽᦖ‘}¼ðŠú?½ÆHöñBJ³+è1²+=ÇtCS®¥òœ-Í“ž¼Žc¿Ÿ¦<·þ`g&ƒÎ.XÌ* ®ÜÇáMO%óh>i1@,J/cÄ}‘œŒÊl¶{Ñ¡‹7A£ýè?¥;Y …`±àìåXàÆÖÇç EøP@ 8‡¦3}ÙŽoO»lÞ®ýýêÏö÷7{=Òcs¯ø&ƒ…¬ø|²âó‘H¡cOoº èHŸ AhÕz}sü¼AþµÀñiß‘†£‡Ÿ@tdȽ©®Ð‘{ª‹É‚“»’íï»@ 4{{{ÆŒÃÜÇæ6(¿µž¥X—J¯¬¬dÁü<óì3ü¹s'6nÀh4²äÅÉÍÍ7¢úMíŽIg"åàåý@ß™†›Ÿ3æádT:î\(ΨàlbA³Õ§"_ÃÁ¯qós¦×˜®DÜBì¦dúOëAâ¶Êr*/{Ž”CÙô¼¾YÇš^/7%S_Áîuõ»£Ñ”éH‹É¥ïÍ¡ýåô•½'NvDÞB×?w¤Ô®†tòTqÝÝá$ïθ¦ž;aP@ ¸€Äí©DÜrÞ¶˜ðèâBÇîžøõðF¯1°wc|›h“Õ9'ŠÈ9Q„ÜNŠÉ`¹¦ï¡¦TGzin­aÖµƒ#>˜ &âödЇ¼tëÖ•eË^¢¼¼‚yóæ 픉7Läàƒh4„R«Õ¸ººÔ»¸¸P¡®htú?Œ7–¤IDöíËG«?ÂÑÑñ&Ôt9—ààà«Öæ.]:µ‰{ã?Ô«ÅJáq5Ý\îôæ|ºô'ïH)ò*U£ÊøùuhP¾üƒU8xÚ3ä¶²öãªðÄ5гAe= Ü‹êC£åâÚEEÐX_¿ÉÆCåG w½yM¹ÐýæŽXŠåhŠ-¹½”.C<ñ q"÷H9%{MHÊ”çÉTj–ÒodÔ¹º:Ï‘’’Òæô‚0(  ÅY³f5O=õ4UUU×T»BBB¸~ôõôíÁcΫMoläÃsÑ[‡S{3¹åÅQ$nOÅN%ǯ‡zzÓ±‡š2)%$GeP’YÑ&Ûw­ë¢"_CE¾F<ÜÔÑ3fÜͶmÛùê«oÚ|»¢se23fÌ`츱쉊â“OÖpïÌ{˜2e ÕÕZ6|º¿þúËfê}©(¼·M¿Ûn» ;;»výÅêW×»BÌ–ê=ãžÜ|óMH¥2þÜù'kÖ|,äÝŠH¥R¦NÊóÏ=^º»{›Ó§ÏðÔÂ' ¢¼¼œ#G°råJòró• —Ë ˆà­•oñè£`µÖï–íjalÝÈqc0e¥eœl⇲´´+óQØàòip<¦ñç·îÑãÒՄûNÞÞÆ/oìmp™Â/ò¹îîp¶¬ØÓ¨k…M¤çõœØ•ξïØí"Ù÷UãèΑ=)\+ƒ¢@ Z”   ®9c¢»»Í}”ûâáñïWWWWWfÏžÍóÏ=Oee%¯¾ö ±11dgçÔ™~â¿D-Û¹s¢cb8°ÿ³î›EBB ž˜Ïš5kÑiµâák$1?&3é?CP¹*)8SBAJ ÛR0jMB8‚v££}||زåÌæ¶íÿ±>}zâ_psæ<@hh(K—,%3³f‚ÁˆÙûØ\T*//[FBB%%%6Qï÷Þ{Ÿ÷Þ{¿öxÞ¼¹$$$àëëËí·ßÆ“ÿy ¦šeË—1pÐ@¢GÛt½ûödèÐ!<öè<,3¯¾öaáá$¶ÀG²¶*ï«Íˆ‘#HMM¥ àß­§>>¾¼òê æ<0‡ŠŠ Ö­[Ï’¥Kj ±¿oãdòI€F§›L&ÞZù[·þÁÒ—–`6[X¼h‘PÐqc2¹„äÝ×îªûô¸\nY2ŠÃ?$a57Ì8ß©—7½Ç±÷Ó£ºVU‰–ì„|Â'“°õòF?OFÍîOÒ®4~ymßeóŸM( ß”îÈÒóü:·e„AQ -Êĉضm{íñäÉ7qÿý³°³³cÛ¶í¬^½†°°0~øAªªª ëCll‹/Ájµ2iÒ Ì™3…BÁ?nbãÆÏl¢]eeåüç‰'qqqá¶é·Ö¦‡††”t‚ôôš¯”{÷ì¥GÏž888Ô™~±e+Õ•H$`±ZJ%Œ=šôô aLl"Ù' )ËS£)Õ aÚ¥Ž~ùå¥DFFðþûï²fÍZ¾üò«:uvß¾‘üßÿÝÉ…ϰbÅ2¶nýƒª* óçÏÅl¶pôèQ>üð£VkW}úô\«T©7~=ø0eeÿe $&&ŽÂšî‡"<<¼EV)6¤ÞçâêêJDD8«W„‹‹ ƒ ‹Åú÷Ÿ™ªÊ*›‘w}õV*•,za1eeåøøÔlQ¬ª¬´ùz·¦¼¯6Q»£ˆÚu^Zaas˜S{¼sÇNvîØyQÙÆ¦ŸKFF÷ͺ_(æ:q_$i±9”f©¯ù¶¦ʦLj€ùtöR1àÖ^l÷P“®••ÉøyƒÈ8’‡úÁ`úN Å/ċݟġmDtèôØ\B†ús2*㚸7Rñ*  Å:©”pøïÒ¥ IDAT¯õ!!!<ðÀý<óÌsÌšu?Ý»‡2uêÍð駹㎻èÖ­aa}èÚµ+Ó¦MᡇåÁfÈÁôêÕ˦Ûìââ‚ZýïDH­®ÄÍÕµÞô+){ðàAÄ믿ƞ¨= >Œ-›·ˆï ÆDA{ÖÑ/¾¸”„„DæÍ›Ï—_~uI]ÞÞÞ¼öÚë¬^½Æ&uñ¹tëÖ•êj-‹/âÇŸ~à…E/ T*ÉÎ>Ë€ýñôôÄßߟððpÜÜÜl¦Þç2uê¶þñf³™²²2~úñ'>ݰžï¾ÿ†ÔÔ4’’’l¾Þ†²²r¾úúK6~¶‘ƒÔø„¼÷Ø@ʲ+Ú…1 åp=F4(ïÔF±ý½CWt½Ãߟ`ä}}ëüͽ£3ÓÄb´ð×ÚÆ¡fÅeèð.×̽+@P/nN¼Ý.ÿíÉj…Ì3ÆËìíׯ ‰˜L5À®]‘šš À—_~Í´iSIKË %%¥v žššŠ»»;=zt§gÏžlÚôCí9CB‚ÛÜ€½±‘ZVDGÚöŽ Ü” ÒÑê‚*,—Ù.v¡Ž¾útvVVýQEÓÓ3HMM³Iù]¨O±·Wðñš5dgçð̳OsÓ䛸ñ‡ gíºÉÏÏ£ºº­¶Úfê]û<\…×ÇÇ—)SoæÁ9Q]]Í’¥Kèׯ/G޵ézÿÃÝwÍÀÏÏ—_^JLl,ÉIÉBÞ‚vL!eꢑÄm:IaZY»iwu¹ŽÊ" {y‘›T\o¾)Ï`Çû‡á Ý–ª 4äŸ.¦ÏønßñïÇŒˆƒéÖ}ŸCSÖ´ΕEÕè5F|‚Ü)Lmû÷P@P/߼莻³¬Ay?¤ã¥—Þ’4qâ6mú¹öØd2a6ÿëCD*•¢P(ê,û3îM›6óöÛï´65Âá•”mLtD@Ðv™òü”Nö Ê›“Ã/¥£/¤>]cp±íÀ Ó¹•dfžåÔ©Óì‰ÚCXXŸ|²¾6@˾×b+æRïÚûwAÞ^½{’””Dvv6ûöî#22²E \WRoWWW$(/¯ //#GŽÒ"Ŷ*oÁµ{'&.Ìöw¡UÚ]ûS¢sè9ªk½ÅëçôãøŽTÔEÍó±çÄŸéL|bGò‘H¥\?§/ÙÇ ùsuÌŸ;#6‡¡]® ƒb«oy~üñylýã÷óþ-®q¸úÀœ9ç¥Ïœ9ó¢òã'Œç³Ï?ã›o¿bÖ}³.™îääĪUïðõ7_.´’@ \nðÒ@c"€‡ó¥»¥RI``×óVÆÆÆ2vìh‚‚‚pssãÎ;ï :º~çåGcìØÑôêÕ GGG&Mºww7›–áéÓgèÕ«'AAAxzz2bäNœHª7¨mSSÊþñ×_­Y–Ä¥£# ‚¶KC‰*'»Fëè ©Og—––Ò­[7ÜÝÝ ¢GîmFŸ«sÓÒÒðõõ!22GG†AÚ9†COOOzøAL&K­ÑÑê ÿFáýùç͵iÙgÏÒ§OüüüpsseÈÁdeeÙ|½#ûF²xñbÜÜ\ñöö¢o¿HÒÓÒ…¼m7%~=¼è5¦+½ÆtÅÉSÕà²]ûû1tF›—ïi—ÆD€‚3¥8{;Ô)·¾SB)Ë­$ïTóÊ:üíqFÎûsø»$NF5O𛬄BüÃ}‘ÊÛ¾ÂV_¡X_”,w7^\ü"11±u–Ñ/àêr&ÛHHgEÃòæ/ùû¨Q#سçühiié¬[·žW_]³³Û·ïàÇ7ѧOŸº¯qæ }´†¥K_ÄÍÍ•={öò矻lZ†|x¥QEtD ýP–«Æ½£Kƒò–樭£/¤>m2™8|8†o¾ùŠœœJKmoÕE}úô\k4yýõ7X°`^^^8p€ßû€Ñ£GóÈ£w„¥K–ÚT½¡î(¼))©lÙ¼™7Wþ;;;víú«ÅúÌ+©÷ž¨=ôèÞ5¯Áb±²eóf…¼m…J†G'WÜüœpï䌛Ÿ ®¾Nõ&*ò«¨,Ò «20nî@ GrÉ8šOuyÝ[h#n ÆÕ׉]Ŷ{Ù¦ʡǨb:Y›8¨#Žn*â~>Ùì×+Ï«"þ×Ó”å4P¨ôØ\B‡váäž¶¡[âéåm­Yµ ©]½ÐU ³gçæ]âêêÊ›o¾Á#<†Ùlæ•WWðɺõµ~Z.dàÀÜ0é–½¼€™3ïES]MVffé•êJ||½Ù»w<0›¿víÆÙÅù’ëå2ps²MËq``7ÒZèkh³h¯hoÛhoq…ÅfÛû曯óÖ[«ÈÍÍ£ÍkœÊÊr¤²†¢¯Æx¢1Èíe(ìå6ý^ ~í´[«ÖÛl;„ŽÚGßÛÚýn["88˜”””çwövdäý´&ªŠª©(¬B] A]¨Á¨7_”ß­£]úøÒ9Ì—êr-éqydÍCWY³ qÄ}‘T•T7Ûʸ¶ÞïK$nY:Š/ÿ³ ¯7ÜÖ“¨uGÚœl\¼|g¶¼²×&Çç GÛ’ÐÏ’àîîÁ³ÿ}ooâã±òÍ•TUUý{êˆÂåééAy=éÛ·mgùŠåLž<™uk×1lø0–/[.4¥@ ´ >+„ BG Á5‡£‡Šqàwa¸<·ŠòÜ*·§âÙÅ…Î}| ›DE³ÑLV|9—BÒÞ°Z­¤Åä2¬ GòóH~yu_›l‹º¨CµŸ@7 ÓÊÛì=±ƒb]ѽV¸š‚‚|ªª4,X0Ÿé·Ogç.ûÕ—Þ”è—AA¸:Ú¦ß)?¿íN‰´·6‹öŠö6–⣩b´!@ ´ ÓdÓË»›\¾ä¬š’³jŽm=ƒW€+‹•Ò³j!Ø H=œËà;z1)”íïnÓmIË%d¨¿0(6FÉ’ËåRPPÀÎ;™4iÒyeZ"úejjšÍnyÚåÖ¢öÖfÑ^Ñ^@`› yå# £÷öóWB@ ´Sn~n;>ˆÁÚLž‡Š3+„P롲HCIf9™ñùè5Æ6Ý–¬cô›Úƒƒß$b1YÛdlÂRVW”,‰DÂÊ·Þ$8$•JÅõ×_OæßQ²DôK@ ­É°ÿ­ãÌ×kqîŒÏ€¡B -HûæÑí–B@ huFÎîKÒ®t*‹4B-ÄÑ_ÏPš]yM´%=¦f•b[Å& ŠuEÉ2¬Zõ.Ï=÷_~õJ•’ï¿û_þ÷æ›ÀùQ¸>øð=öíÝÇÉä“õ¦ÃÅÑ/yôafÜ3ƒíÛ¶‰7S ÁeõÁ×_³’ª³œüôÂA¥…ð¿á°ZQ882nãoMŸ‰Ì^)#‚'â¦PÔr“‹„0M"-6—Ða]ÚlýmbËsÔî(¢vG]”}8šèÃÑç¥i4æ<0§öxçŽìܱó¢²õ¥ŸKFF÷ͺ_<Å@ .‹L©äúÕßsè…y˜ªÿ —¼~á¿@Â{+„®²ü§ÝÍÁÿ>@æo?ÒyÜMŒ|ïK²£¶“ùË7Ôb›˜@ ®>ÝtÄÅKE즓B‚&SY¤Á¤3áÝÍ¢ô¶çKQ.n¡@ êC¡ÝD{Äh4 !\€ÒÓ‹!+V³ïÉûà77ÅÇâð€ïQP˜%„u•è5{g¾ýô¼´ì¿‘½ó7:ŽÏRÍé/×`Ö鄎¢ïm&d23fÌ`츱쉊â“OÖ_”ç9s˜>ýÖÚ㯿ú†Ï>ûŒñÆsï½÷bg'gëÖmµq êJwrrbÅŠåøøúòê+¯’`“òðôw¡ç¨®üµ6N<¸‚+&-6—aþ (@pµ©orÒØÉOCqêÒ•È'—Ö®Œ«‹ÓŸ¯aØÊõd¼òd»¿?R¹‚³çS–tŒ’Ä8 eW|N÷žáØ»{P|´îˆŽ¹Q;ÈÚA‡¡£è÷ô b–=ÕòÌðþts*?Œ5ÆÊJ ê ŒUÔjŒUjÒÿØ*^`@Ðæ˜3çBCCYºd)™™™uæñpwãÅÅ/[›æêêÊìÙ³yþ¹ç©¬¬äÕ×^!6&†ììœ:Ó;wîBtL ö`Ö}³HHH`ÁóY³f-:­Ö&d¡PÊ?w›W솠YÈ:V@ÿ©=8ôÍq,&K›ª»0( @ h3Ô79ù'øZc&?®!¨<=0i«±èu˜´ZÌz-&­‹Ñ€S@}~Š˜—.o J^ÿ.]ï|´WþÛ®ïÑà—ß%gÏv<Ãú|û}«*(Nˆ£4!–Ò䦭6é9{>I¯¼l¾üQ¸÷ÆgÐ £[f²'÷ð¦ï­³A"!å» ÈU*ŽÎÈP89£ptƱcgäŽN (ÚJ•ŠqãÇñЃSVVÿ"wwJKÏÿ=44„¤¤¤§§°wÏ^zô쉃ƒCé•êJ$°X-H¥FMzz†Í¦-ɶU‡Äƒ!hVŽl9…o°y'‹ÛT½…AQ A›¡¾ÉɹņN~\ƒ{ÐaÐ0dJr¥©¹R‰L©B"“S’ËÑ77¨n¥'âñ:ŠCÇ`×U•ƒK`(R…å§ŽÛÔý š>“Â#)8´‡‚C{pìØ·at»e‘ —Q’Ëé/× -.lÐ9n¼•’„8´E Ë_px¯ŸpÕ Š©”î÷>‚Oä@N·‘²äD€fY‘)¶B·n]©®Ö²hñ"ºv àÈ‘£¬|s%º \K¸»{ðìŸÆÛÛ‡øøc¬|s%...¨ÕÿFãU«+ñôô ¼žôíÛ¶³|År&OžÌºµë6|Ë—-·YL˜?ˆCß@We† YÉŒÏG«Ö·¹z ƒ¢@ ‚6C}““¦L~röì¤ôXt×’H%X­ÖFÕ¯tÛOôœ·˜¢¸˜õWÇŸÏ€¡„Üõ £‘„w—¡É=k÷ƾcÎaý9ööËç¥kr³Ñäf“³k+™·î}è÷Üë$¯—ÒG/=PU©è:å.]b»ù…”Ÿ:NèÝ`ïôª´Õÿ†[¾ã>Røœ¼OWQ–uV¼œàšÄÉÑ{{¯YCvvÏ<û47M¾‰øñ¼|«?\MAA>UU,˜ÏôÛ§s6ëb¿Âõõ«V«•ÊÊJÌ_À3Ï>ß;w²aãŒF#K^|‘Üܼ‹Ê_µ¶wéÒ©öÿnÝP`‹Ô—@wñ`~~„šQ6'âÛ^€aPA‹óÁïóì³ÿ¥ªªª]´÷þÙ³¹å–iµÇß}û_|ñÁÁÁŒ5’ˆˆˆÚô…Üu÷]Üpà ( vïŽâã5kêMwrr⥗^ÂÇׇ7^ƒÄÄÄv!ß '' ü\ö¼k“ê“üé{„?þGß\Üìm š>§.]‰]¶¥·/ sàÙ‡lâ>øÞõ0q¯?i™š-”%%›´Þ,ıcgÎîø¥Þü=gÏ'åÛÆû¿,8¼¿ãÉØòmóœUŽ yåŠãcØÿälüý»´Á¬Y3yòɧÚd»ÆŽËŒ3°³S°mÛv>ÿìóë놔mÍzËd2îºë.FÍÞ={Ù°aÃ%Ûcëò¾{ÆÝLž<­VËg?#**ªMÈ»¾>»-£VW’™y–S§N°'jaaaçëF¹œÂÂB jV’ïܹ“I“&qâøq\]]j󹸸P¡®@­V×™þãÆ%éD‘}ûòÑêptt`ü„ uúLNII¹ªíÿçüÂ{y2—´´l1?‡´´t!„v,aP×,œ¤LqÀÙ^J¹ÎB©ÎB©ÞB™ÖB¹¾æ¯TgÁh²jI‚‚),,¬¨~ûí×<úè\JKK¯Ù6»»¹ñòK/ûo4@777~äa<„‡‡gå À!ƒyêɧ°Z­,[¾œ‚ÕZgº»›±q±| æ/@¥R±dé[ä¼y××gÇÆÄ´q£H¾¾>DFFpæÌ†ÁÑ£5«ËÝÝÝ(++G"‘°ò­7Y²d)9Ù9\ýõdfeqúôžZø$AAA”——3bäV®\I^n^éPcœ ˆà­•oñè£5«Ó­VH$­*‡!žÄnJF ü‹0(Ú(#:Ù‘Zn"W#,AS˜éÄÀv¬M¨ÂÅNŠ›RŠ«½g®ö2\ì$¸*%¸ÙË0Z¬Tè-”éþþûûÿ¹3V+èLVtf+:3èL–šcè-t&hâ"¦vËøñãÙ±c¿þº•JÅ÷ßËC=Âôé·røp »wïæÖ[o¡W¯^,_¾OOOV­z‡{î¹—›nº‘™3ïE¡°cÇŽ|üñZÌf³M·ÙÝÃí"Gåååå<½ði\\œ™vË-u–+))áƒ÷?¤¸¸ÆAszz*J{{rrrêLÿg¥žÅjE"‘0jÔ(233¯c"Pïää܉ͥ&?M”ÒÓQc l²²Î’õÉ{ô}úeŽÿú#fÝ•m}¶sqcв÷Hýv=¥I5Éúšð‹(‘ÚS‘Ò:[e<ÃûãjïHÙî­d5rëoÖÆ5t6és8úÆ çý6ìÑ8¾f%ºúZ<Ÿ³8Œ, TáØl~&]CzâYZÄ™Ãë¼çíAG899ñÊ++ ãÈ‘8–.}™ððpæÎ}³ÙL|ü1Ö¬ù˜‰'2{ö}( 6mú™Ï?¯YW_úÕ&44„ää$ÒÓ3Ø¿oÝ{ô¸ØÀU‡¾nhÙÖª·R¥bÌØ1Ì}lîE~cëj­Ë»[·nÄÆÆQTT@tôaÂÂÂZd•â•È»¾>»­c4yýõ7X°`^^^8p€ßû_^yus˜ƒÑhdÕªwyî¹çpww#>>žï¿ûFúuëY²t vvr¶þ¾“É5}U}é&“‰·V¾ÀÖ­°ô¥%˜Í/ZÔj2P(å8y¨¨È׈¼@p hC8)$L R1%ȸsÂ8^ddà EZaXÂ-Á*‰pbã Oí.oP•\ìd¸ÙKpSJk ¡v|U2ìdìå”2°—K±“JPÊ%ØËÀ^&Álµ¢7ÿmxüÛø˜¥6ñ{ºŽÄbá´ù\¤R)ýû÷gݺO˜1óŠŒu½æÃßÞÀgÂb4àãm'Ä5јXCaô^:ŽœÐlE¿¡£)Š»¶#{^NGGDDСƒ/o¿ý999¼÷Þ*úôé €——7 .$==ƒ€€¦L™ÌcÍE&“³xñ ÄÆÆQ]]]gzrrˬô;×ÿjee% Ò× -ÛZõîÚ5­VËŸ{Ž€€âãòÎÛï ÓéšÜÿ´¦¼sr²™0q?oúGGGúô #?¯Àæå]_Ÿ}-œ”Ì#Ÿï˶°°€9Ì©=Ž>Môá‹W…ïܱ“;v68ý\222¸oÖý­Þ~ß` ÓËç# Š6@oO9Ó‚èïkÇŽL-‹ö•Q®·ÕŒèdϪÑîÄäëÙ¤¡Lwí-…êé!纎öÌV~KÓQ¦ÆSAãìgÇÜgâ ôÌø­¤Qeµ&КÌT7íÚ )5FG(ÿ6>b…Ù})ÒZx爚ê6jWt³oYƒòfT˜/»}¼oß¾$&&b2™êü=..Ž™3ïÁÑÑ;;;bcã#""‚C‡1`@vïŽ"55 €o¾ù–)Sn¶yƒâÇk>¦  FÃܹs¹åÖ[åËÝÃÿþ÷¿¼÷îªóŒ‰¦ žzòIžZ¸¿víbÝ'ë0¼üÒËäåå]ï{]“ '6uM~ZCE'ֽà—ß%úÅù.ßyìMx÷zٲƪJ2ûž÷?Nò§ïµhÃæ=Ï©Ï?Äj¾²¾»ütÇV-gԇ߻âºM¾“CÏ?zEç,ŒÙOèÝ’´îíšýrW:¡|=±Ëž´©wÁÎÅ{w¯Ëg”€&û,“ñŠt4@JJj­0--77wÔj5µþ¢@=øî»}Xaoo_gzK뢮àuéëœìì•m­z×øµcÝÚµäææòÔÂ'™tã$6ý´éŠûŸÖ÷çŸ}NŸ>a¬þh5ùùùTWkѶâJûÆÈûR}¶ íâìAq†0( " Š­ÈÝ”L V¡5ÁÎL-kŽ]üµpoŽž½9zFw±ã£qìËÑóéq UƶkX”KaˆŸ×ùÙ3ØÏžüj3G ôÈ%ðÑxN–Øœ¢åH¡XÙ%¸<þ.2æF8aA«ÑU·¼AÚh£ÅŠÆðﻹâšëüìøòF/¾JÖðýé¶·íôÛ©¸)¥ Ê»5UÇÒý—^õ0~ü86oÞRïïjµFäI“ˆ‰‰åÔ©Ó <˜ž={²zõGtêÔ ‹åßí͉…BaÛ:ïoGå……5+­víÚÅĉ\ÞÑÑ‘E/¼Àúõë9s&å²écÆŒ!9)‰ˆÈÖ~¼GGÆ×j“ÈöŒ&;“¬ß$ò©—‰_ùbƒË…Í{žÊ³$}üVƒòÚC¯‡þƒWøŠb[¤m†^Õj¡8¾y®g(/ãÀÓ¾` ï¿Ò,çÌ?EÇ‘ãÉÚ~Eçñ @EúiL6æ>`ø[k±si˜ÏÊœ=’øþÿ®HG×E}nÍ6oÞ»ïžoàž>ý¶:Ó[µZ‹Ë¥ý¯Ö§¯““’.[¶5ëýߨ3gΰoï>z÷îsÅýOkÉ`Æ µNV½{þê?[•÷åúfAÛ¥C°G9-!\€Tˆ eéì,cn¤[oõ¦·§œÕñU,;XÁÁÜK½ú묹;Ë(ª6óÙ$O‰pÂNÚvŒŠeL RñúW~žêÅ„%)e&î.céþ ¶¤èø5MÇÜ¥Ì1pWG6Nò`zˆ {™D<8¾¸B$(¤0¿¯3K®sekºŽ•1êV1&^ŽƒyÜVНƒŒ7xÐ×GѦäÜPcbCò*•JºuëzÑJ“ÉŒ‡‡RiMù˜˜Xî¹çn8ȱcÇ4h ååeèt:âââ=z4AA¸ººrûíÓ‰ŽŽµiJ$^ãu‚‚ƒQ©”Œ5Ь³Y—–¥›vvv<ÿ üðãÄŸã°¾ô&haááüþûïXÿ~%,ÂÑg«Rvò8% qô~ð?—Í+³·gØÿÖQ’K^Ô¶F]'ù“U„/XÜ"m’)•ô˜9—3_®mös'¬ZNunóø$,8¼‡N#o¸âóø½žâ#mîÙj¨1±!yª£B|ü1F¾žž={âààÀĉpss«7½%8}ú ½zõ$00† Fòß>Ûþ©C}úúRem¡ÞéééøøøŽƒƒC‡ '=#½Iý-Èû<<Sj"Ä£a]Å™2Ó%1b8ûöí¿(=**ŠU«Þæ‰'žäÌ™3DGÇ0~ü8222j÷)))â3X¿~Ë–½Œ³³3;vìä矶iFÞÿžyæiÜÜÜ8vì?ýøS½ù}||yyÙK<òð# <ˆ°°>„…õ©ýýÔ©SlÙ¼¹Îô§>ÉdbÕ;ï°mÛ6-^ŒÅbfé’¥¢3nE cöaçâBðí÷“òý§uæq íEäS/sô‹›ä;Ðj¶pò³Õ„Í{ŽÄ÷¯®ïÌþϽFÒ'«l^î•©ÈœpèЉºúj IDAT‰êüœ&ŸÇïºÑ¤|ó©ÍµO‘†K×À†É"3íŠutCIIIaíÚu,^ü®®®ìÛ·Ÿ¿þÚ]oz‹ÈJ­fýú ,Z¼;;ÛþØÎ©“'ÏÓ¹õékFSgY[ª÷Ê•o2oÞ<¼¼¼8xðlý“ÉÔ¨þÇVä 0jÔ(|èAŽgù²e-÷N]¼¯»nH½}³ íâìNqz™„@PO/okMvIm(ö†„d·˜8;»]ó’ËÀÍ©i 9íe0=Ô›8Sf`G¦Ž¤S³ÕÍß¿ áò"îèîÈ·§4lLªn]YIa¨Ÿ=Ã;Ù3ÐÏŽLµ™£zŽÈ­º2ƒ×ˆÎvŒ P¡´·ã‡ä ¶¦ëÚÅ Ø ÏêÆúÛs}%sõÈÑ“Tb¤¿¯áÞvôõµ#Oc&&OOt‘ä’æ1øy(¥„y)è㥠ÌKAGsõȤ>ˆ¯¤Bo½*íýÇÿR] ëhÏc‘NÎÓóUru›½¯ƒýì˜æÄŸEö|x0§YÏ]\ѼÆe…¢ù¾;½öÚ«¬Zõî5ãÇïZÆh¼²¾ª²²©¬a«q[{N:V©"&¿yÖßÚ²AQ &5¶8žhiƒ"@÷™Ppx/‡÷Ðû¡'1 ¤oúºÙê1ôÙóø Ìúæû×mÚ]t4’¤õïÖ¹‚ÒV Š ''ú=û*{¿»Iå#Ÿ\Jþ¡(Ê’š|ÏÿÁ– Š }ô½Â Øp‚ƒƒ ™âͱ­g(Ë[ž:w´ƒ¢…43÷õvàöP~8]Í {+Z캛S´lNÑr{w_ÞäÁ‘#ñ…â ”\ãK?cT\ßÙŽ#æéùðèÕß™«•r蔆 'ª dÉu®”ë,|wJÃáü¶ÄÅßYƸ%£»()×›9càÍSJN¥ÿ£~CZ¹‰´r?§hqC„×ùÙñH„EÕbò Î7tÎêÅPw9}¼ì÷RÐÛKN¥ÁÊéR#§ÊŒü’¦­×áÁ\=sõL V²eš«â«´btx';n q TgfsJu³®ðµ¶ç+ØTXð÷6h%ïÄU‰(çA;àÔg6÷ êrBï~ˆÜ¨íÆîoÖkza.C^YÍþ§î¿âs©¼|_ð"¥'â9òÆ¢¶7I¯ª¢2+¯ÈÇÇ4ª¬Lå€kH/’׿'\@ hgHåÜ;: c¢@P ØLÜÓÓ{{9òÝÉjfm-mµz|J˦ÓÕ ë¬ä:?{æ„9¡3Y‰/4_dàX‘¡É«%{z*Û¥fëmz…‰ƒ¹z>?QÕ*þÍV Û3ôlÏÐÓÓSÁø÷÷qâóäjöçèÛÌsã©”0Æ_ÉX% ©„¹z^9TA‘¶F¨þþ÷³Ym‚ƒ¹†¿ýhèê"#ҷƸèï,ã¯,º*ÉP›9]jâPžŽÇT6Ò»%EÇiZîííÄm!|_ÉÑfˆÌ­’ÃÔ ¦;pºÌÀO§«9YjºfuG‰ÎÊ›1•ô÷­‰äþKZ5_´áíÜ a$~ðAwÌ"íç/Q§6äH‹ÑÈñ_gĪ/Ø»àž&Ÿ§ËÄiøO˜JòúUhrÚîv¯‚Ã{è8jb£ І\OQÜAñÀ A;Ä¥£ŠÂ4á?Q ¨aP¼Bþ¯»³z;òsJ53~+±‰:™¬¢Îê‰:[cXëà(¥·—‚Ñ]”Ìt¢BoåXqÍêÅc…F*õ»ºÈ dtg{ÊôæX¸»Œ*£íl9M.©ñèï,eZˆ÷õvà³öæØf•FwQá­ ÜÛŽƒ¹z>>VY»¥¸¹ÉP›ÉPkùùŒ¥]ä|{ªºY Á‹„O5tv–qO/Gn ±òa|%yšÆŸ<ÄMƽ:Âü؞®cѾ²«â*ÀV‰+0WPÊ­!*^Q¹:*[@ ¸vIýnãU=¿®¤ˆø•‹µú;¢½£qDGg"ŸXLUN±Ë¶yY—$!䮑©0kþÑÆoØ2ûA<¬@Ðq鬢(³TB ¨o¼(DÐ4¦‡ª˜ÕÛ‰­iÕMŽØÜRäk,äkôü™Ycœèä$£—§‚ JþÓß…¢j3ÇŠ-¨YÁèb/etg%c”H€9:–ª XkÛÆ¬J ï©¢‹³Œi!*îëíÄçÉvŸµ £Ìè.5šûx)ØŸ£cw–žµ -¡Xg‚¤«°Ú/»ÒÌk‡ÕDzËym„;Ñùz>Œ¯ÂÚ@¹Üì€\ q•RÞ=ؾ;íŸÎhqµ—pOOGîu`Mb EF )Ôjb^úcÖoa÷C·`1]Þïpð³qò$së&Ôi§¯YÚCÇ8»½a‘ᕞ^(=½P§Ÿ’@ ´C\:ª8W.!Ôƒ0(6’iA*fõqdW¦Ž‡¶•`´À¥|ÛÙ"9UfrªÌìȬ9öw–ÒÛËŽ)A*žèç‚Ébá@®Vr¶²í<9[iæ½#Utt¬Y±8«·#_$iø3«å ‹Cüì`ÏÈNJöçèÙ¥ã¸k×G|‘‰øÝeL°gëmÞ|œ á§3¯q¶“2-HÅ´Ç |•\EJ¹¡d€ ½•â«èæ*ã¾^ŽT›¬¬I¨j“ï£@ h}Ì:ÎfÔG?²÷ñ˜´uÌê2q¡ÿ÷é[¾%yíÛל cö:ã¡}‡Œ¦0ö€x€ "‘Jpô±§ä¬ZC ¨aPl 7ª˜ÕÛ‘}Ù:æÿYzU"·Y•²*ulM×!ÁZo$á¶F®ÆÂ‡ñUø:ȸ%DŬ^N|–¤ag–îª^7ÒGÁ˜.5ÁU‹ ÌÕ³&^Ó®Þ—í™z¶gê™ÑÓ/nô`u¼†ý¹5þ.o VÑß׎?Ò«yò¯24F«P0õ^aæ•Ãj"}ä¼4Ô•%FÖ«jQ—WqP ØV‹• `ØÊO9øü#Êÿ] î3hÝï~¢øhö=y\£jY“›Ålƹk0•)—ÍßqØN~¶Ú¦Û$t´@ h d23fÌ`츱쉊â“OÖ_”gü„ñÜ{ï½ØÙÉÙºu7lltº““+V,ÇÇ×—ÿgï¾Ã£ªÒŽg’™I2É”” !B ½+ˆ" *º ºë‚mY¢®eÁUW×BSÝwQTô·k/‹º"ETPºt¤÷^§¤L»¿?"H 2IÞÏóäIrrgrÏ{ïœ;÷S<»€={ö´[£“ÌØŠeú!!NDŠ'¤æ½‰‘l+nð»¹ÏÈ G'I&­¤ÖÃk»íD…¨¹69„Û…ðïßd·Ý¢9K{1®‡Ž»‡M ܽº§WÕ¥_?ÿ9PË7Yuüq`(cºkéa`uv¯ï¶KãÒ »JÝì*­æâ:Þ½2‚¯2ëxkŸC#„hµÍß蹋Ù6ï‚-Ýèó‡;©¯¨`׋Oá¬éüÏç¯ù’^WMf߫ϟp»Ðñ((ÔÈI#„èò¦O¿ƒ>}ú0wÎ\rrrŽû»ÑhdÚ´i<þØãØl6<÷,Û·m#?¿ Uåqq=غmÚÈ­·ÝÊž={˜ùÀ –.}ƒúºº³Zçè¤Åurð…8I(žD˜–.·8DgUVëåõÝv"‚T‡‡Bûz,nÈo@jÕá/Ðþ M€Š@•êðÏ :¼M€ŠPŠI ÁÔº66ðø†jjÏ•ß*Æ‹¶‘` ³F†ìžŽ#‹-]“Äÿ®äõ½vV¤×K`„­²mî ¼û!Ô-Ÿüö¼ì®sMÚ»ÁÄàûg￞mv»nç_BÙvYÝY!‚‚ƒ¹lÂeÜuçÝTU5ýÁSŸ>ɤ¤ì'++ € ë7ЯBBBZUn³ÚP©À«xQ«UŒ?ž¬¬ì³žLè–NÕyŸ-ĉøEBñŽéÓ™2å÷¿¿ÿÞ¼ûî»Ív>Ú™îB]`÷¢ÑªåLéL7õ oîu`ªåwI!\“‚)H…Ë ^EÁå¢àQÀí·¢àö(xpy}åEÁã…B»‡lµRRë•Àž„$ÛÎéõ|YÏMý|CÊ—î¶ûíªæBÿtèÝWq×uÍžÅ?­Ãe«á‚,cË“÷ãi8þ†±Ûù³û¥§åDBty½{ÇS[[Ç“³ž$>¾;vìdá ©¯ÿ¥í4 X­¿ÌoµÚˆˆ§º•å«W­fþ3ó™4iËÞXƘ Ç0ÿéùg½Î QñfòÈ–@ˆð‹„b¸ÙÄìY³Ù¶m{cYsݦ÷ïO9é6mÙ…ZQ¤gbgUU¯ðö>j•â׫t Ñ·þ/¥–¯3ëøÃ=×÷ aén;ß×HbQÑ‚6¤®kãªØó3µEy\ôÏÿ°ãﳨIÿåý¥©ï@ê*J»Äp!„8™P½NÃëK—’Ÿ_À#þ•‰“&òé'ŸžðqÍÝGŸ¨Üf³1sÆLyô¾]»–åï,Çår1göl ‹ÎJ£“"(Í’k€'ã Es¸™ÊÊc_°Íu›>:¡ØÚ®Õ§Ò…Z¥’DSg'ÉDÑ¡oŠëþ¹ÃN¢)éCÂø>³¢Cì÷Ò¥Kxè¡¿b·;Ÿæ°aØ6í6fÌx S¯éwNç¼Q#¹óλ[TÍO„>yÊd&OžŒV«aݺïXòêôz½ßLf.„¿«++eã#w1dÆm\GþÚ¯èvÁxÊwlîÒ±i®îèZ2ú©¹vyê§rõÕQ«øví·,]úz‡ØïæF‚ùû~·äú(ñ>;¬V99y:” ÀúÖ3xðà_mcÅh44þn0¨±Ö´ºüˆË.»””ý) >œ×–¼†^„Ë/oòX$%%µyãF…㩘˜nr±<‰OÛÆfÿ®ƒ®žþ‘P4‡óèßþJT”…]»v³ð……Ív›>šá,t¡–ŠBtlÑ—ÿoC= ¥ùÔâ¬*ï”ǫ̃v3SM‡Ø×ÄÄDJJJoT?ýô#î¼óOTVVvês±_ÿ~\xá\Ng‹ÊoVš˜=::šë®›Ìƒy‡£–§ç?ÍÈQ#1›Ì~3™¹Åž—Ÿ!á÷S0ýR–½D·ÑãØ:ûÏ]6µnÉè§æÚåáÇqÁ£¹÷žûñz=,xî9ÂÞ³ð¡Íéì74=ÌßãÝÒë£ÄûìÈÌÌ$:Ú°aCIKKã±cÙ¹sçáûxUUÕ¤¦¦ñÐÃ’˜˜Huu5c/ËÂ… )*,jU9@`` C†eÑÂEÜsÏŸß“7ßÑ'==½ÍëœpÕ(|ŸAm&33K.”']96~‘P\òêJJбÛÌœ9ƒ)×M!/7÷¸íZ’Ükë.Ô ô0úçÁ³D[ºÜ‹²«ÕYê{ꂇA?îjlß…&¶'êç`Ž@¤Ç[]Ž»¢wE1žª2<•¥x*J:Åñ=Xâÿ®+®¸œU«V°jÕ×óßÿ~Ê´iwÊßÿþC† fûöŸ™5kC‡eÆŒûðx¼ìܹ“W_}+¯ü Ó§OC£Ñðé§ŸóÎ;¾Í•·'FÃ}÷ÝÇ’W—2}úí'-?¢¹‰ÐN'N§¯W9üåÁn³c2šüf2s!:Ô›þÏþCô¨±œÿüRªSSð4tÝé#NÔF_ý6oÞºuß1eÊd ÈܹOÉ+¯¼Ì 7üI“&rûí·¢ÕjYµj5K–,Åãiÿy”[2ú©¹v955'Ÿ˜EUU5Kv›Íï÷š Ööûd×G‰÷Ùãr¹xþù¿3sæL"##Ù¸q#_ÿïk,–hž]ð Óï˜NMM Ë–½Åœ¹s|=;¿^ÅÁ¾ÞV­-w»Ý,Z¸€•+¿aî¼9x<^f=ùäY«stb–ïÆ`– ¤'Ðî ÅÀÀ@JKK)))`íÚµ\yå•ìß·ï„Ý ³Ò…:33%Bã·077¯Ë´]­ÎRßÖ ë?Œž“oÖ–Bæ¼?û>Ò<ŠJ£!(ªºÈht‘1è|Þ¥EÅP_VBCiuÅùÔçS_RHm^¦ß6¤V«9ò\^ýÃ7®WÓûeذaDGwãÿXD~~>K–¼ÂàÁƒˆŠŠâ$33‹øøx~÷»k¸ë®{ `îÜ9lÛ¶ÚÚÚ&ËSRRÚµÞ·Ür3«W¯¦  ¿EåGôîÝôDèUUU|öég¼½Ü7üùë¯W’’’B^^ž_Lf.DGT²uu¥Ør2ºl NÖFoÙ²•#†³nÝwœþyÄÄt'00aƲyó’““¹ãŽÛyøáG©ªªdÞ¼9üö·WóÙgÿm÷ºµdôSsí²ÃáÀá€÷Þÿf³™÷ß{¿1áäÏû M;CÙOw¿Ov}”xŸ]Rð§»ÿtLYii Óï˜ÞøûÚ5kY»fíqmmùѲ³³¹íÖÛÏj]-‰f™?Qˆ–¾ohïP©T,\ôIÉI3nÜ8rrsIMMcÀ€þ$&&ÁØ‹Æ6~2d6›šÝæD=Ò…ú«¯¾jL4œ¨ µh핈ö¡³t'éžÇ‰wÙÿ~…¢•—LP\.ê ó¨Þ³’u_’ûá2Òþ5Ÿ½sî#÷ƒ×¨Þ»ãÀs±ŒŸHë¦ué¸j F ñ‰'ý ‹ODxò_FŒÁž={q»ÝÍn“žžNJJ V«•ŒŒ Ìfß§ÃYYÙddd¢( £FKÿþýùüóOøøã8pÉÉIÍ–·§¾}û˜À—_|Ù¢ò£=ú-7ߊV«a⤉X,Ñ\óÛ«¹sú]LýÃIJJbĈá=ño¼á&FœsNcOü7–½A÷î1ÒPqÖŒC('hŸüI§¢OxàI¿’ÍhZð®ÿdmô¶mÛ:t¡¡z´ZmãïÆ eëÖmŒy.ëÖ}GFF••Uüç?ï3jÔ(¿ß¯G6¬]þÃMS™vûŒ{!ýôïû½äÕ%Ìzr6œz3.§“)×MñûýnÉõQâ-ΔèÄpʳ«%B´@»÷Pt¹\,^ü2=öf³‰]»vññGãp8šì}º]«ý¡ ugdz½o¾³ô%Ïžñ^]B4  îÚ[0ôLñÊO°e8å§j(/¥¡¼ô˜²ˆ‘c:ÿu2Þ^„=ã`— ︗ßDg4µhÛ¼ïײkñs'ÜæŠ+.çóÏ[×S¥¹|>ÿ|/¾øÒ1e×_?¥Éòötã72bÄV~óucÙ'Ÿ~ÌÞ={›,Ÿ2ùº_’ÍL„>``RRRÈÏ÷õŒøqà 6Œ;|sµ¦'¾¢ãútr$æà€mû¿´:f­¯9­6Újµâp8˜8ñ*¶lÙÊ¡C©œþhÀ¿þõ*±±±x<ÞÆíÕj5Œô9Ù¦µ×wL›ŽJÕÕ5±cÇN’““9rÀ¯÷ûÆnjr$˜¿Ç»%×G‰·8S¢“ÃIû)O!D øÅŠ[·leë–­Ç•7Õ út»V­%]¨õ¡ú÷CñzQÅãE…×÷»× Š‚âñ (‡öz—{A€³¬¨SŸ@*­–„[f€ûŸyU@ ·?@åö)ùîò g”eÜDb'ÝHáבö¯§ÏÈÿ¨Ø¶š”]ô¼þjó²Èûlù¯—ièH·—úò"ÊŠÁëm··4™èÛöÄΑÜðcÇCDDÕÕ-ÿ4xçÎÝÜrËͬZµšœœ.ºh,›7on¶¼ªªý>iž7ï©ÆŸãââ˜3gÖq«UþºüÈ$çÍM„žŸ—Çm·ÝJLL uuµŒ}+W®ô]·Z9™¹¢ãji2Ñ·­ú´Ûh¯×Ë֭۸喛¹ÿþäåå3cÆý”––Q__ÏöíÛùÇ?žgÕªÕTTTpà ׳qãF¿ˆUs‹FÝæ6×^_<îb®¹újž~úi4 ÃG ãÇòûýÖh4,\ôsæÌ¥ ¿ q$˜¿ï÷‰®oq¦E'…³ñß{%B´@ „àÄ‚"¢ˆ?E¥FuäK ¨P©Õ¨Ô*P©AåûY¥@Q©P©T¸ª«p;jÈ|çåvMœ)‘ç_Bܵ·ûÉÛØúV¹S@:š>=#Nùñ†þCéyýtrÞ[BåÏmûIfÏž=ÚeAÁLï[gÐPQJáW4»]XÒzÞt7i¯<ÝfÉ—öªs{éLõ8<ÞúzœÖjܶ\¶¼uŽVÕ78¶'=~? wƒâo>Áe=û½Îà ôºá.l)Xñmr|ÃϽ˜+&SW”Gé+}½¢5G  BaA….< m¸… H •e4”—ÐPVL}Y!>ößžÁ/¼ð<‹-¦°°P®¾œÍV: eIŽö~?¡3†|Ôð6i·¥îg²ÞÕ¹þÛþI-D׸öv•ûøS1è²Þ…éØÿ­oÑ¥„„ÞdffI`š!ñiÛØÔYüæýy‹ïåPŸØé³ØÍ¾y&îÚ[1CÖ;/¡86Ñ—LÂrñUä}ú6ŽìôŸ°é)¤,xˆÞ·Î¤fßvŠ×üWN¨ÃBz%¡5š¨Þ³½s¿~5t›ð{b.»†¢oWÓƒ@}¡FCÃÐêpÙ¬¸íÕ¸¬5á¢{A.[¥¯Üæ+wZ+é9yÁ±ñ¯úGvû­Àé¶YÉXöçgÐì’ùö‹§“š»)^õY§:®ú0Œý‡b8œ°¾Ãp–cKßOáʨËo:ÉdK?€íåy„Ïð…ÿ&÷“·©Øôm‡«{pl<1~Gp\F>wêŠ\.÷i=Þf«F iѶíý~ÂJ„Ù@H B_ƒ/”l€~…ll)W‘fƒÝU*¬-è¡×7>–Øú|FG*ă%X¡¢Ê ²Auø»¯`YÄ+Ä…@ìQ_ú@…\‡Š• 5.5¹…l»Š’úæÿ¿Iëë 7Ȥ0ðpo¸ÜZV…L»š äžf/Ìà…˜_BÔ Qèo„z_²PâÕ³§¤Ž,;T4@¼ÞWŸ! ±!§•¢_«B­‚H‚I ¥õPZ¯¢¤Þ÷s–]EƒÂðp_löVù¼ûªUäýª=õ ñ¡ ñ¡ÐSïûŸûkrì*¾+Q‘v†“x½C®N604¨­¾-R(ªóÅ»ÚyüÿNO/”6ZÑ®×ÞÎv¨ @£ 䪇ÎçëE›¨«iýˆ ¨}ÆôdÇŠcçùOHèMff–œœÍø´mlê¬gv4[kÞŸ·”$Oâl&‚c{Ñóº;(^÷%õE¹¨4¨P  Ô¢R«!PƒJ@„ÅBuµ&U@ *µµ&¯×‹.< ]D4º ¨Õ¾dau9ÎÊ œUe8«*|«ÃV–ûEœ£Æ^qÐ9àqárØñ8ì¸kmxj¸k¸V<† 22ð6Ô·ü$×hÐúŒ*Ö—hÔ¡ÖêPkµxݼµ6\¶œ55(.ÿ–‹+yç]BÞ'oQµsãÙ9»÷$¨{/ôqñ„ôH$8¶'n»ëÝTîÚŒ³¬¸Ã$Øü™$EW¿©ñ‡÷GŠM¾ SHUlRh‚jì®T±»öT©{&†*œ¥p~¤BB˜šíå »*a{…Š:OëZA¾\”N!Ép8)§W0hŽôbTÈ´«¨rÂÈ`„@…C5fS“jU8T£ælM+ R衇¡qfœ•Ä…øÔy ¤Šë ¨VEq=Ø~•”U«"uäë}©ó%Tk\*öWC†MÝêý‰Ô)Œ±Àè(…àø®¾+L»ºMêÛϨpQ”ÂØhpy8¬Í°µ¨§¦$…í}í=•ëîÓ§3eÊïÿ½x÷Ýw[´Í„Ë'póÍ7£Õ²rå*ÞYî›"©©òÐÐPžyf>–èh<»€={öœtߎ$GǪaûg­Ÿ6êw³/æÇwv㨪“„ÙNšIlš×Šò.ÄÏÔäpè¥ÙXÆO$✠ðz<(^·•ÇãûÝëÆëvŒNŒâv£xÜ(gŠÓ‰­¼”ŠšpVVà©uø}½Ë6¬¢ìÇÕp’UµÍ±±­J&(.n— öyN”~¿’Š-?Ðýªë±\t¹Ÿ¼I]An›<·*0Ø^uGߣ7!=éÞ‹º²Bê‹ò¨+Χô‡¯¨-Ì;«½V…ÂdÚTdÚT¬-òý¢Ðߨp‘îNVhð(*Ô8U쪂²ÔØ ±äŸ^Ò¨Þã&›aS±ù¨Ïýt¾¹{èÕÄ…øz¨†¹*JêN–Ý¡µEE¶\ö ò[7´WQî¡Øvû]Þ bE¬ÈS¤p~”ƒ@£öò} ì¬„È ÐBX „j|=.ÃUúš*µ‚FÍá/•BQ­ 7 ÛÊÕ<¿ŠëÔÄÆ(¨µË FÑi…›MÌž5›mÛ¶·j£ÑÈ´iÓxü±Ç±Ùl,xîY¶oÛF~~A“åqq=غmÚÈ­·ÝÊž={˜ùÀ –.}ƒúºÏOŸ±9Ÿkç\ÌÏÿ=„ÒŠ)¯ú§`ÙqÉD!ÄÉIBÑO•~÷?JO²Mll,Å©7—¢Èoîf­®–¼O—Ò3‘ø©÷cË<ˆÖ7I4{Õ{·RW˜/ÇB!šPP«¢ ö—£%HÁéU3´5ÖpæþƒÇ7_†íH‰ÌÉw2%õ*þ›§â¿y¾ìùQ “â|=#kÝ*.—BY½‡ ìnß1uzUxn/8½¾ù]^5^EÁéUK`…]Š9ÜLeeU«·éÓ'™””ýdeùzjmX¿~ýûÒd¹ÍjC¥¯âE­V1~üx²²²OšL<"í§<\Ïþµ-ë  `è•ɬ˜¿^²§@Š'¡œbbEHxOº`-^Åߪ‰ŠÇ×@ú~÷þ]Añ(x=^Å÷l¥ò)‰8Vmné¯- ,iµjT¨T¨{W¡ „5ju *ujMju RளcÏËm꘩šù™“l+„]ƒÙΣû+QQvíÚÍÂb·ÛOºÁ`Àjmü «ÕFDD8ÕÍ”¯^µšùÏÌgÒ¤I,{cc.Ãü§ç·x?ý˜Ëoþr~‹Š£¯ÈÎ/ÉâIB± …E…ÙÛDdOº0-eUGÍK ¨U¨Õ*Ôj5¨Aí[VµZJ ê5j5(¨ð8=hõ:Ò6åR‘]-Áǰ¥§HD‡öÊ+ÿâÑGÿ†Ýn禛ndÍš5|øáG¶¾7ýá&~ó›ß Ñhøþûx}éR¸é¦›Éx6¬ßÀòåË›}ŽiwÜιçŒäÞ{ïm¶<44”yóæa‰¶ð÷çÿÎÞ½{;],›>µÿñíb¿þý¸ðÂ1¸dÊ!N¹îL.½ìR¦NŠV«aÕªÕüß»ÿ×âöº¹òްß'^…J¥æ»uß±lÙ2‰w'Ýï3iÉ«K())Ænw0sæ ¦\7…åo/?é6y¹ÇOÓÔ\‡EQ°ÙlÌœ1€G}„o×®eù;Ëq¹\Ì™=›Â¢ã—˜˜@ 6 ñwk^ç_=‚²ÖÖ)´›Ž¨Tïs’лÉmbbºÉá$>m›ý»v¸zJBñ4¨UDö4o$²—‰z›“Êü2¶ਨ=íç1éˆK¯aÝHÛ˜‹­´V‚ÞÆ"z™Ô¨)I¯”`q–$&&PZZÚx£j±Dñå—_áñx:e}Ï=÷\F>‡|EQxzþ|Î9’íÛ¶qûí·“œœÌü§Ÿ&'§ù¹QûöëÇùçŸûuùùçŸÏöŸ·³iã&n¾åföîÝË}÷ßÇ›o¾ÕâáBþ®¹áS¿N(j4î»ï>–¼º”éÓo—ž§ØFwƒÛn»Ù³faµÚxzþ|~Þ¾””'m¯Q”fÛqÞo—ÓÉèÑç1ãÏ3ðx¼Ìf>ƒfßYø°©+Æ»=÷ûŒ& )--¥¤Ä7!×Úµk¹òÊ+[´Íþ}û0 ÇÄ·ÆZƒÕjm²üˆË.»””ý) >œ×–¼†^„Ë/orDBFF&Ý/i2[ £oĦ/wœ°^“&_ȦOvc-9ñz²è‰M³d˜VÒtôlaø5ýǺˆÄ^YÇÎ/RÙ³2ü½¥m’L¨­n e]&y{Šéqo]žDP˜VÂi 3Ò|oÆßu=‡DÞÃȨëÞË$ÁñCa–†_Ý—¡W%;ÈBQ'Aéà&L˜Àš5k˜3g6C† añâ¹é¦˜8ñ*>üð}>ûìSî¹çO0bÄž{nAãsÌ›7—1c.`èС¼þúk,Yò wß}—_Ö·¢¢‚Wþõ*åååTTT••ANGPp0—\z Ï=÷YYÙx½Þ&¯ÑhøÓŸîæ×_?iù‘Oý½Š‚J¥ââ‹/&''§Ó$Ütüz˜”ÉhÉ8BVV6üôãôíׯEíusåþ¾ßéééÌ™=—ÊÊ*´Zß=„Ýf“xwÂý>“T* ½@RrÁÁÁŒ7ŽœÃ=ÍfÓ ·IMMcÀ€þ$&&Á؋ƲJ³åàKN:”¯¾úªq^wE¡±9[Y-ŽÊzbD5»Mò˜TäUŸ4™(„81顨æØPÂ{šˆŠ'0Ê󭦔’òíÙ¹ [Kìþ:•Èxç^ÛŸ²œRÊ‘ÓÊchIŠ$:ÉŒ½¼–²œj6Xˆ×í»×‡‡ÐkX ½Gľ9š¢3û©|Q‡¹»ºšZU ¸êÜršÐkx ÑIáìY޹»SL(ñÃ|ÝÇ+ò¬TäÕP‘gÅëòH°:µZÍ9çœÃ²eo0oÞS¼ôÒ‹¼ùæ›ìÝ»¤¤$n½õ{ìq*+«˜=ûI&MšD^^^³ÏÅÃ?LVV¶_ÖùHO:€î±±ôˆëÉâ­/“˜@]]{ì1zõêÅ®];yéÅ—¨¯?v%û©S§²víZ NZ¾eËæÎ›ÇUWMäí·Þâ‚1°àÙþ¼úõð©¾}û˜À›o¾E\\Ü ß+ƒ!¨Íö%22¢Ë¾¾»jÝ[SïôôB¿®Ë¯ÛèæÚäììlzõêÅ£>FAAÿüçb ˆÕjãšk&qï½÷ȬYO°}ûÏ8p Ýëöë#l6ááá-j¯Gõ?º¼£ì÷;テÙlæÃ>$;;[âÝ ÷ûLr¹\,^ü2=öf³‰]»vññGc±Dóì‚g˜~Çôf·q8,[ösæÎñÍ{üõ*ð ël®Üív³há"V®ü†¹óæàñx™õä“-ÞçÔŸrxYo RÊšüûèëñɬuòÆ\ˆÓ$ Å“½Iìi"ñ¼Tæ×pp}&uÕ í¶/åÙÕ”gWÓ½_㦠"ÍNgZä¹Íßt %¦O–D3u5 ”eÕ°ý³xœÇ'Ÿ•µ¤¬ËÀ`ÑÓç‚8ë=dlÉÇ^Þ¶ÃÌ»õ$v@Aú@J2«ˆëA°)EÚêz•u8ªjqT6à¨îÚCÜGü¶/Žªzv @en5•¹Õdº0¦˜0ºˆdÐe ØËTäÙ(Ï«ÁZl—“¿ éô[–l±–8ðzN¼ÕðáÃÙ»w/nwÓIôsÏ=‡ï¿ÿŒŒL>øàC®¹æê&³³³;Äsx8ûÛßøçË‹q:„êõètZ–½ñ………<ôðƒ\yÕ•|þÙçINN¦wBo–/_N\\ìIËm6=ø =ü0ß­[Dz7—ár¹xjÞSuøsòdän¼ñFFŒÁÊo¾n,ûäÓ™2ùºãÏŸœl"Ìm»4sA~a—m3ºjÝÛ«Þ¦Pˆ4¶lÀQN‰×I>¿üuÝ\›œMzzFc¢033 “ÉLß¾}éׯ}ôaãs&%%úEB±)ÍÍåöëöúdåþ¾ß·Þr+ݺucöìÙü¼ãçÆÄÄ»sïw[Úºe+[·l=öþÉá`úÓO¸ ÀÚ5kY»fm‹Ëýï¶[[?mIyv5Ú$"z¨È;v.ÅónÈÏ+ „8}’P<‰ÊÂjö®J÷«}*<„¨ÞfBLAhtÄV±WÕa¯¨§¶ºGeu5 öšc Œø]_ö|“†µ¸éa ¶Jl ”¤–ޱSL(ýÆöDoöõ\̵RžWCƒµãÅJ¥ @ñ“^—W=2¾eS-dý\È–÷÷Ÿp› .cÅŠ/šý»ÛíÆëý¥î*•F(ú¼Öëõ<ùļõÖ[¤¥ù®)V«œœ<ÒÒÒøqà 8è˜Ç]ýõ >œ/¾ü%fïð>ûöîk²ü¦oà’K.á@J C‡ å×ß@¯á² —59±|G“ššÆC?Hbb"ÕÕÕŒ½h, .ôµfUUÕÌ›÷TãöqqqÌ™3댬ò,D{û÷ãFL¡-K(~³µùÿ®mUÝ|›ÜĵëðhÄ+¾àå—ÿéw±²Z­ 'þ0¢¹öúDåþ¼ßƒ•JEMM ÅÅÅìÚµ‹¤¤¤³’PìŠñnÏýM¼_ø)‡—&°aù®_î3º‡ofÏJ96B´I(žŒ¢òÛ]«Ì°shk&ñçÆ?¼©ó(Ïêš+B÷ÕØ>‘8<”çT±û›4œ×)?_U¡ªB‘ñ&ιº•…626åãláÐdµFMlÿHbZ𸼔fT±ñ?{P¼M'E\õ.\õ®ãæñˆ‰ëŽ­¾†ƒ}x0æØ0ôF®z7*µ[e-öŠZåõØ+8k;öÐé¨Ô!aüxÔ…¿%l¥l¥òv—  TcînÄÔ]Oïáݰ–×R–c¥è@iû7¸AhƒÑ…hÑé %¸W‚ô´!´!:´!ÔVÕâu+äï/¥4£ª]÷¹¥ÉD€ “lDïÞñ'ì©òóÏ?óÜs X³f-åå\wÝ6mÚLee½{Çc2™ˆˆ§_¿¾¬^½ºCœ×Z­–ÇŸx‚O>ý”];w6–geea±X:tiié\0æBvíòýÝd2Q]]Í3Ï<Ó¸}\\,?þÄq«<ÿº<00ÁC†°ø¥—¸ë.ß¼’^oÇNÈ­¦¦¦ÉaRG½¢«hi2À|’m›j£›k“›³k×n¦NýkÖ¬%''‡±c/dË–­TW·ÿûÓÔÔ4øË$$$P]]͘ ÇðÒK‹is›k¯›+÷÷ý:l“&NäÙgŸE£Ñ0løP6nÜ(ñî„û-ŽW°¿Œ!W$bÒQ{x”áy7 b×W‡$8B´Õý­„ có¸àÝwß=æ±.ŸÀÍ7ßìö´rï,§ÙòÐÐPžyf>–èh<»€={ötº×Zê`ÿÚL‚ :Lݰ$…“<¶'žU6* ¬TX[¼hE°Q‹!Ú@·äúŽí‰«Áë뽘_ãnÙΉ°ÖH87–0K»¾h¿ù/ò÷–’¿·sl™›ó;ÄùPom€ÜcËUj‚C5èôZ´zÁaZŒÑ!…iÑê5hu¾D£ÍI͉­ÌA½Ã‰­¢î´; Ó2ü𾤔“½ýì.UînðýsÙ»Šè5$šKîIöÏdý\Ôª¡áQ &bF£7Q”VÎöÏSp7œÙ…T¬%ß<››ÔÄ$G0ø7Éxœ.òö—S|PfB!„âhé›ò Õ’¶)O‚!Dk÷„byy//þee¾ÄWFF:­s¸™ÊÊæ$èÓ'™””ýdeùº’nX¿~ýûÒd¹ÍjC¥¯âE­V1~üx²²²;]2±)u5NêjÊWÅ 6é0Z Äô¤ïEñ8ëÜTÚ¨,¨¦Á~êÉÅ_/*ÒÖºŸc¦º¼†ƒ?dûE\« lú¼PÜ^j«OT||]TjZ½–àP-ÚPß\€½Œ„™ƒÑ„hpTÕa+÷- c¯¨ÇQUwÒät·~Q$êΞÕm>çe«x¼äì,"ggqƒ£Çrv—õs!Þf>½ ÓÒ}`±ý-XKY*Ua IDATm(£ºÈÖ.û^t°Œ¢ƒe„EééÖ'‚ãzQx ¼Í‡< !„BÑQ9ëÜìøBbâLh÷„bfæ/ 0ÄÆÅÒ³GOmy³9œGÿöW¢¢,ìÚµ›…/,Änÿe(¡Á`ÀjýåfÞjµNu3å«W­fþ3ó™4iËÞXƘ Ç0ÿéù]òÀ×U7PW]Fqª/ùl ÂJ·¤H‚´§ü¼¡áÁdn+$ggÛÏ¿3rÊ@Å äî.–WîY¢xl Ç$þ /þ¨ T£7bÖ¡7‡Õ;}x0 à¨ô- c«¨ÇQéÀ^Y×åaЄD¶}šâWõÌß[BþÞbFqÑ­Ã)\9nco›?ÜÛaêÜÕŽqg¯oq±_—ËBi£…B!ŽÖî ÅÑ£ÏcÈÁ 2¸±ìàÁƒüåY¼øe{ì1Ìf»víâã>Æb‰æÙÏ0ýŽéÔÔÔ°lÙ[Ì™;­6•_¯âàƒÍ–»Ýn-\ÀÊ•ß0wÞ</³ž|RΆ3Áã%k{!Ù»Šé5´ãï:‡ômùäí,9áÃú^Ô µF;5éC!„B!„h'wLŸÎ”)¿oüýý÷>àÝwß=f› —Oàæ›oöݯ\Õ8ú¯5å¡¡¡<óÌ|,ÑÑ,xv{öì‘à áÇÚ=¡¸~ýÖ¯ßÐäß¶nÙÊÖ-[)s8L¿czãïk׬eíšµÇ=¶¹ò£eggsÛ­·ËYp(n/Ù?’³»¸qŽÅô­ùäí:>±8üê¾TÚ(<(óÁ !„B!D{ 7›˜=k6Û¶moòïF£‘iÓ¦ñøcc³ÙXðܳlß¶üü‚V•ÇÅõ`ë¶mlüi#·Þv+{öìaæ3Xºô êëêä@ág%âlRÜ^²·»«ˆ^Ãb7ý2¶û‹ªÜ4„ôÍyTÙ$XB!„BÑÎÌáf*+›_X±OŸdRRö“••À†õè׿?!!!­*·Ym¨TàU¼¨Õ*ÆOVV¶$…ðSj h^·—¬ílýd?zc0OÁØ[†±wMº$…B!„ÂO˜Íá<ú·¿òù?cÎÜ9„†»P¡Á`ÀjýåÎjµa2[]¾iÓ&FŽÅóÏ?ÇúÖ3æÂ1|±â 9Bø)é¡(Ú•Çå!k{¹{Šñxðx%(B!„Bá'–¼º„’’bìv3gÎ`ÊuSXþöò>FQ”V—Ûl6fΘ À#>·kײüå¸\.æÌžMaa‘ !üˆ$…_ð8=!„B!„ð#”––RRR ÀÚµk¹òÊ+ÙÆjµb47 ÔXkZ]~Äe—]JÊþ† ÎkK^C¯aÂå—7.èr´ø½ÏXÝìnb-=ä$øœ•ؤ[;Þ‚´’PB!„B!ÄqT* ½Àœ9s)È/`ܸqääæ`6›¨ªª&55‡~ÄÄDª««{ÑX.\HQaQ«ÊÁ—À2t(‹.âž{þ€¢øö£)ééér„h'’PB!„B!Äq\.‹¿Ìc=†Ùlb×®]|üÑÇX,Ñ<»à¦ß1šš–-{‹9sç Õ²òëUGÅgò”ÉLž<­ö—rEQº\lšÚfÂå¸ùæ›}+¯\Å;Ëßépq‘ŠB!„B!„è´Ìf÷ÞwÕUÕ„‡G4–>Œ .ͽ÷ÜÏ]wÞŰáÃ[^ÄétòÙ§Ÿñörß0ð¯¿^IJJJ—ŠMsÛ$$ôÆjµ5>—Õj#""¼ÃÅ@z( !„B!„¢ËúÃMS™vûŒ{!ýøzŠUUU³9<œ'žx‚—^z §ÓI¨^N§áõ¥K¹åæ[Ñj5Lœ4Qâs8>K4×üöjîœ~SÿðG’’’1bx—ŒMSÛüZGœ[RŠB!„B!„èrŒF#&“€¢¢"vìØIrròqÛéõzæÎ™Ío,#55 ðõ*ËÉÉãСTëXO¸8‰Ïáø ØŸ””òó󩬬äÇ ?2lذ.›æ¶±Z­†Æí 5ÖšI( !„B!„¢Ë6|³fÍÂd2ÉðÃÈÊô [5›MhµZfÏ™ÍGÌÎ;›™™It´…aƢׇpáØ±dò*ñü¼< DLL &“‘Ñ£Ï#77·KŦ¹mRSÓ0 ?‰‰‰DDD0ö¢±ìßßñ†ƒËŠB!„B!„èrÖÿ°ž~}û²ôõ¥x½ _¬XÁÞ½{±X¢yvÁ3L¿c:£GŸÇ!ƒ2dpãã<È_xçŸÿ;3gÎ$22’7òõÿ¾–øŸ/V¬à……ÿ@«Õ²nÝw|ûíº.›æ¶X¶ìÿÛ»óð¦Êôÿãï“4éÞ4-;Ê"²:”ù (Œà¾ÌWœkpE, *q™Q–¢È¢# ¨"¨àw„¹QG`FñK‘"Ë€ÖQ ‘Ò’tM›œß)±µ DdÒÏëºz5yòä$Ï}Òç>½Ï’WÉœ’‰ÝÁªÖ°ëË]\ Œä MÃ0ÿo¿OÇç­ >>1ì'˜è+—µk|^¾·&Mšrøð¡z5á×·1k¼ïµõ³ƒÚ2”_œÛ]ˆÅj ©ï/½=áLŽ#Ù™pΖ×ü¢fø®~þÖ×±ÿ˜qïÙ£9ZD~ÙÜ[_þ‘³ß>•Ny‘© ("""""""""!Ó5EDDDDk∵∅ÄXgœÿ01‘¦éïgšTÝ6ñaPZn'ŠLò]>ìæ_Ë0Lb¢ ¢m&Ö L¬V«,«LL|>ƒ ¯‰§Ò¤ÒKÕmû⿓˜(ˆÄÿ^#-ÄDšÄØ b£,øLÃ0ðTš”Wš”{Lâ*ˆÅBy…I™ÇÄçƒJþ÷ZiRiTV½g› ÄC‡…  9,$Çx}&Î8+E>N¸MòÝ> Ü&Ç]>NA¾Ë‹»äûuòs‹´CL$DGÄF\Ü´‚+±‘&ÃâI…I™ÇGy¥Ay9”U˜ìÙ£¿5 “íçpØ€k0dÈÿ.W­aÉâ%ÄÅÅ1mÚÓ4jܘÓg““£O€ˆˆHäø|×`nºéFl6k×~Ìüyóë\Ö}#¹±g4vœ,W©W ¸KM ‹MÜ%&E¥5ŸSýRÓ±‘pis+ŽX ÎXHJpáóÆp¢ÈǾc^¢¬ÄDšDEÄDDEØ­PRn’{ÈGc§Ï>¼>0M¯¼>ƒc'½4M²a›Å "  v«5¤Ðm’o¡¼<þ¢cy…Iy”züïÏj‹aa5±þûV8YÉñ`³ø_# V«‰Íj°ç —æÉJ<&¥åPVõ»ÔcRZnr²ÄÀkb·جVìWޝÈ¿¤x›ÅÀb5°YÀab³TøLLN¸LòÝPPäå„¶­¬jóa0HŒ7HŒ1HŠ·Ð Á M3Iqà*¶qY++E¥à*ñâ*1(,ñá.6),1ÉwyqÄZ‰±DGšDÛ b"-DWÝ/-g¼¿pkTq-U±2«ÕÀb˜X Ë©>†‰Õbp¤À‡3ÁBY™I‰Ç¤¤LÃC+‚2|7$Æ‚Ýf`·Z±Û "m&‘6ˆ´¬ûT¿"""² èp81bO<þn·›3§³uË.ºèb6oÙBö†l† FNNãÆ²`ÁBÊJKõi¹@süŽ;}zôèΕWö$cl¦ 3fN§Ç=ؼis­å­Ë©$g)ežsóþš4iJá‰Ã8b-$Ɔ—2”yüE¾2O幋‡Íê/TÙlv«Ýfb›Í_0ó™àóB¥Ïôßöù —•^Óôõèõ™TVµWTšU…ͳ{‡»ÎØÏ0μüJ¯II¹‰ÿ+L¼uö‰2ˆ¶ q1⣡©Ó ÃJ”Í Ô¥åE'¡¬ÂK™ÇX>À¬*æúL‘ÓW5voÕûóùÀç31!h\š4qrøðIýqŠˆˆH½–ÅvíÚ²sçòòòXŸµž;âv¹1 ð™>,ƒ~ýú‘—÷Љ"""xޝ^P<~<Ÿçç¾È±cÇØ»w/‘öÈ:—WPd{nßc™Ç¤ÌãåHÁ?U§ASf^0ëð\ª\\fR\æåp+:ŠˆˆˆÈGX~)KBB.—;pßår“èp°qãFºwïÁ3ÏÌ$k]½z÷⽕ïéS ""rçøêrssùꫯh~QsZ\Ü‚M›6)x"""""çH½ù–gÓ4q»ÝdŒÍ`Ðïî¤k·n|ôá‡,^²˜…‹Ò¬YS}DDD.Ð_gR&L`Μ9x<JDDDDä ËSž].GBà~BB']ß_Û¦ÿkعc'©ii¼4ÿ%bccpíµu^Ô½y³ÖŸã,Üsˆ©WØú6fWã‘—ãO‰eJæd.\Ä×_ﺼ˜ØFç4Ï×ç¿ëú:vÍå"""R…eAñë¯wóð#ãiÓ¦ ………ô¹ª³fÍò8"‚”.]˜=k6£Gü×ò1ªec5{öìѧDDDäÈñNg"…Øív&gNæÍ·Þbû¶m§]žò¼ˆˆˆˆÈg$7hhú‹iF ¨¬¸VÏ[A||ây;°þú3tèPìöV}°†%Kj}تU+¦<™‰×ëcÒĉüˆ ^ÚÇ´iOÓ¨qcfLŸANNNÈï¿Q£†Œ3†_uþ.—›•ï¾ÃŠïž—ëÕjµ2xð`®é YëÖñÊ+¯ÖêsOz:wÜq{àþ²¥ãõ×_çSÒG¦sEîŒymÛ¶¥o¿¾¤¥uáþÑü¤8‡EDDDDDD$¬•••qà ×Õ((vêÔ‰† ràÀsö:/¼ð"/¼ðbàþƒ>P«ÐÔ£Gw®¼²'c30M˜1s:=®è¦Yg{…ÇÕWöäþÑâóy™1s&SRˆ°ZêloÖ´)›·l!{C6Æ#''‡ŒqcY°`!e¥¥§}ÿSžÌäÓ›˜9ó‰dfNâÀÁClútÓ϶®þ÷¯¯3vL§í—ž~íÚµcJæöíÛWgŸ$g"“'MfË–­gŒÿæM›èб½{÷¢ÂãÀéLäþF“½a#IIÉåôêÕë¬ãTP<ÔUñ†à{"ª V]¥bNcuoP8­ãw dàÀØíß·›¦¶ã2ô.n½õVJJJYüÚb>þøã°ù<×ÕgÀµ2dˆÿÛlW­aÉâ%š,E”›•³4—‹ˆ„Ûoÿ C‡!""‚åËW°xñRSS=ú>*++iݺ5YYY<÷ÜlRS»0hÐïxä‘ǘ6m*«V­æ“O6]~aa¥¥å´oߎ¯¾ú€ë¯¿ŽììÄÄDû $¤§ßÃUW_…Õjå­7—³|ùr®¿áz®¼òÿ1yR&={ödÈÐÁ<=uOMÂÈô{ƒ¾¦Ãá K—æÏ©Fûñãù,ƒ~ýú‘—÷Í‹\íÛ·#22š×_ÿ_Š‹‹Y´ðâââIKKeàÀLœ8 €É™“ø×?ÿEQq £FÄë5Éùüs6mÞRãþ¢E¯Ð@†J„-‚÷V¾ÏÒ¥K霒ÂÝw§¸¨ˆN—ubûöϘöô4Þ^±œèèh–.{ƒî¢¢b¦MŸZ+ÎQÑÑôП{GÞwÚ£3Éɉ!ÅÀf³ñÀ0ÞÒÓï   ‡Æ'!!ÕŽv4Mó¬â.t ÅóÄ©ŠwaAaŠwZZj`ý#ï%5-Î))5ž[½º>zÔýtíšæß»¿bߥK S2§ðÚk‹Ã~Ì/¼ð"7\càçïÿƒ¬uëÂv¼7æ·¿È#?Ì=#ÒéСÝ{tÛñvéÒ…>}zóÀý0yÒ$†ß=œäää ~¼Áú8FŒAæäL|`,½{÷â²Ë:iÂQnVÎÒ\."r^‰ŒµálÆŸÄfñX¬F…¬Aƒ1fÌ8† ÁÕW÷¡sç_þSpgÍšÍ!ÃhÓ¦ ô?«÷h‹°±zõj®¿ázbbbHMK«qäÃá 77‘é÷2ê¾Ñüæ7·ât&²zÕj"#£èÕ»÷¤`ΟŸçСƒ<öèNûš·Ýv+«V¯ÆëõÖhÏÍÍ嫯¾ ùEÍiqq 6mÚ´½¸¸˜‚‚B–.{ƒ%¯/acv6yyyAÛ7nÜH÷î=xæ™™d­Ë¢Wï^¼·ò½3ƨqãÆìÿ¶æ‘~›7oaíÚµ§}^rrþ<ûϤÕï·hÑ‚›o¾‰±cÇ1vL—w¿œ:ТE Þxc)Ç eË–têÔ‘Ûÿg Çç÷w&77—£GÔçÖ­[QRRÊÄIy{År&Lœ@TTT¹=‰?üñQÞyw™S2‰‹‹ g€¡C‡ðÏþ“¾;c¼Î6Î*(Ê9uªâ½fÍší_½›‰&qâÄ ìv;àßãШQc.òïõ®^]?~üx º~ªbÿôÓÓÈËËÃçó…ý˜«;µ7hÆì°¯ÇãÁã©Äç3«~¼¹‹Âv¼—\r [¶ü›£G±oß·|ú駤üຠq¼Áú´k×–;w——ÇñãÇYŸµž;jÂQnVÎÒ\."r^¹õ‰>ÜüXï3þÜò‡Þô¼³s­ç§¥¥ññdzÿ~òóó3&ƒ¯¿Þ Àþýß‘››G~~>ï¾»’NÎn°ÙÈZ—Åå—_NTt4}ûõ%{CÍ#óóóiÚ¬)//\ÀÒeoÜ  Ï?ÿ<ãǦM›Ù½{wUÞ~d\dd$¿¾æ×¬Y½&hgR&L`Μ9xªN¯=]ûïï̈»ï¡OŸÞt¬‡¶»Ýn2Æf0èwwÒµ[7>úðC/YÌÂE iÖ¬iÐ÷ã3MÎæÄ}û¾%///pÖAõû—_Þ•öíÛóÆÒ¿²äõ%tìØKÚ\ø ܵkn·‹¼¼<Î ù¹vœãbc‰Œ´ñò‚ 2 »ÝÆM7ßT«ßüyó™4q2w B…Çÿ½#hœÛ·oÇ%m.©uÍ`Î6ÎáB§<ŸçŠ‹‹).†¥ËÞÀét²lé2òòòUúÜÜÜ@ÿSÕõÙ›þL›KÛ*ö­ZµdÛ¶íÌzneeea;æê‚í §ñz<V¼½‚×û÷}ðÁ*vîܶãíÒ%…ëo¸Žo¯ 66–””:|Á7XŸK.iËå,Ëår“œœ¤‰QD¹Y9Ks¹ˆÈy%*.2ä¾ÑqöZm•••5.QTT\çs- †aTõýq•¯ˆˆÊËËùtã§ô½ú*®»î:fÏžM«–-}ºw¿œnݺ1þ¡ñ9r”¿Ì{!ð˜ÝfÇ¢"Cëu×_ÇÆì—Ôùxll,S2'³pá¢@ñ4X»ÃáÀ0 °ð$‡bÛ¶í´mÛ–ƒÖÙþåÎ/èßÿvîØIjZ/Í‰ØØ\{mÐKo>tˆ‹[´¨Ñ–Öµ+ñq±¸\.0Îîóñ÷¿¿Ï¼¿Ì«ÑÖ¹ŽC #ôp¹ÜìÛ·?púzÖº,:wî\k=z”#GŽðá‡rà 7ó AƒèÚµ+«VXÆò·ß⎿=í{ù±q:BñQמˆVéX]µbNc>%”½Aá0ÞFsëm·02ý^ÿþ..½ôRºvM ÛñnÙ²•Í›6³pÑËüññÇ())¡´´$lÆlcuçÛµÆD”›ëWn®¯9Ks¹ˆÈét…Ü÷ÄÚ}?ÿ<‡¾}¯¦yóf8‰Ìë?˜àâ‹/¢uëV$''sË-7³cÇNNœ8AëÖ­q:iÓ¦ :´ùõW­ZÍïÆëõ²ï›š§÷FGGSVVFyy9]»u£I“¦X,2ÆeæŒgèØ©Càtl§3±îB‹ÅÂm·Ýƻﮬ™«úÛív&gNæÍ·Þbû¶mǃµ§¦¥2iÒ$4lØ€´®©äåæm1-¥KÞÿ}Nvhš§/ÚíÙ³—’’î¼sÑÑÑ4oÞŒQ£î¥¬¼œ‚‚Z¶lIb¢ƒÖ­[Ó®mÛâýùç_põÕWÑ¡Cbbbpí§}Ž×ë%)) ‹Å4ι¹¹4n܈ÔÔ.ÄÆÆÐ»Or«vöêo³f?Ç¥m/%::š¾}û²ïÛoƒÆùÉ'Ÿ \>mdú½|·ÿ‹‰gçp¡#ÏsÁöDœÚãpJ]ÕõP*öá6æSδ7(\ÆÛ鲎ìܹ“ï¾ó_ßá“õŸššÊ¶mÛÃvý¾òÊ«ksüeÞ #D.äñësèàAŽ„@¿„„NºNjbQnVÎÒ\."r^yÿ™ ?éù»wïæÍ7—óüósˆŠŠbÙ²¿ñùç9¤¦¦RRRÊc=J«V-Y¿þþõ¯©¬¬dÓ¦-üíoK9pà@­/Ý8ÜÜ\ Nœ`õªÕµËÎÞÈÕ}¯æÕ×^åË/¿äÈ‘£8“’èvy7Ž=ÆÖ­[)++%#c,S¦<Å“O=Iú=éµ–Óçª>ìÝ»—#GŽÚ5jÌôÓH¿'ž=¯ %¥3))ßoìÚµ‹wV¼Sgûø‡¦Cûö,xy>c’IIDATŸÉ{+WòÅ_`Fíà?êsö¬Ù€¿ˆ:åÉL¼^“&NõŽy¿Þ1wQï¬x7ðíÇ[·þ›W_{•C‡r¢ 0¤xïÝ»—×^}?>þG޲7d³îÿNÿ=ë³Öóܬ?ñè#ârâV]EEÏ<ó,4hЀììl>øÇ5â\QQÁܹÏóøããt&òÙgŸñÖ›oÿCãÆÿèÏîÙÆ9¨ xžKMKåÖ[naêÔ©Øl6Òº¦¾¹ÊéL¤  0hu½zÅ~÷îÝôîÓ‡íÛ·‡õ˜áû½AO<þDدãïöïgøða4mÚ”ÒÒzö¼‚U«V…õúHNNfà·SYé üS~!7XŸo¿ý–‡O›6m(,,¤ÏU}˜5k–&Fåfå,Íå""agÅŠwX±âZíùùùŒ;®Vû³Ïþ‰gŸýSHË.((¨q¤YFÆ÷Ë[·.‹uë²Å¡©O=]ëùÕóÙþ³ƒ‘#勉˜°îÿÖÕ*š=z$Ð?+k=YYëë|n°ö ^fÁ‚—k´™¦Ygû}óÍ7 vwH±:vì8™“§ÔùØÜ9s™;gn­ö?<–¸ýENNû«W¯aõÎüa¿éÓ¦nW?ˆätqþrç—ŒºoTÐ8lÞ´9P­ã`ql·|÷]`=Ÿâr¹ô»;ÏIœÃ Šç¹¬uYuîqeïÆCãÆ×Y±÷1×µ7(œÇûÞÊ•<7ëOØívÖ®ý˜>ZÖãíׯ£FßÇ¿ÿ½)™SÂbýë°hÑ«dNÉÄn`ÕkØõå.MŒ"ÊÍÊYšËEDDD~QFrƒ†¦ÿÜn#pŽw(çzû¼ÄÇ'*‚"""RƒÛ]ˆÅj ©¯¶'DDD~¾Ü«¼+¢9â\Ñ—²ˆˆˆˆˆˆˆˆˆHÈTP‘© ("""?I×®i\sͯå^©'s„ Š"""""""""2EDDDDDDDD$d*(ŠˆˆˆˆˆˆˆˆHÈ"ù1ºvMÃ4ÍÀý¸¸8EDDD¹WDêÑñ Š^¯«Õª5'""R8Πy½^ÀÐö„ˆˆÈy™{•wE4GœFrƒ†¦a€ÿ7ß§cš^,†…ÈÈhMF"""‚×를¼Ô¿wÔíª*Úžùùr¯ò®ˆæˆså¬ Š¦Ï‡‰ªV)"""õ”a`Œ¹±¢í ‘Ÿ/÷*ïŠhŽ8~Ò5 ‹Cßë""""ÚžQÞ‘zC³ˆˆˆˆˆˆˆˆˆˆ„LE ™ Š"""""""""2EDDDDDDDD$d*(ŠˆˆˆˆˆˆˆˆHÈTP‘© ("""""""""!SAQDDDDDDDDDB¦‚¢ˆˆˆˆˆˆˆˆˆ„LE ™ Š"""""""""2EDDDDDDDD$d*(ŠˆˆˆˆˆˆˆˆHÈTP‘© ("""""""""!SAQDDDDDDDDDB¦‚¢ˆˆˆˆˆˆˆˆˆ„LE ™ Š"""""""""2EDDDDDDDD$d*(ŠˆˆˆˆˆˆˆˆHÈTP‘© ("""""""""!SAQDDDDDDDDDBöÿ7’¸Á8ÜYIEND®B`‚prometheus_flask_exporter-0.23.1/examples/sample-signals/docker-compose.yml000066400000000000000000000016341464362612600273100ustar00rootroot00000000000000version: '2' services: # a sample app with metrics enabled app: container_name: app build: context: app stop_signal: SIGKILL # dumb, random load generator generator: build: context: generator stop_signal: SIGKILL depends_on: - app # the Prometheus server prometheus: container_name: prometheus image: prom/prometheus:v2.2.1 volumes: - ./prometheus/config.yml:/etc/prometheus/prometheus.yml depends_on: - app # Grafana for visualization grafana: image: grafana/grafana:5.1.0 volumes: - ./grafana/config.ini:/etc/grafana/grafana.ini - ./grafana/datasource.yaml:/etc/grafana/provisioning/datasources/default.yaml - ./grafana/dashboard.yaml:/etc/grafana/provisioning/dashboards/default.yaml - ./grafana/dashboards:/var/lib/grafana/dashboards ports: - 3000:3000 depends_on: - prometheus prometheus_flask_exporter-0.23.1/examples/sample-signals/generator/000077500000000000000000000000001464362612600256355ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/sample-signals/generator/Dockerfile000066400000000000000000000002761464362612600276340ustar00rootroot00000000000000FROM python:3.11-alpine ADD requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD generate_events.py /var/app/generator.py CMD python /var/app/generator.py prometheus_flask_exporter-0.23.1/examples/sample-signals/generator/generate_events.py000066400000000000000000000011011464362612600313560ustar00rootroot00000000000000import time import random import threading import requests endpoints = ("one", "two", "three", "four", "error") HOST = "http://app:5000/" def run(): while True: try: target = random.choice(endpoints) requests.get(HOST + target, timeout=1) except requests.RequestException: print("cannot connect", HOST) time.sleep(1) if __name__ == "__main__": for _ in range(4): thread = threading.Thread(target=run) thread.daemon = True thread.start() while True: time.sleep(1) prometheus_flask_exporter-0.23.1/examples/sample-signals/generator/requirements.txt000066400000000000000000000000111464362612600311110ustar00rootroot00000000000000requests prometheus_flask_exporter-0.23.1/examples/sample-signals/grafana/000077500000000000000000000000001464362612600252465ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/sample-signals/grafana/config.ini000066400000000000000000000325231464362612600272210ustar00rootroot00000000000000##################### Grafana Configuration Example ##################### # # Everything has defaults so you only need to uncomment things you want to # change # possible values : production, development ;app_mode = production # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty ;instance_name = ${HOSTNAME} #################################### Paths #################################### [paths] # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) ;data = /var/lib/grafana # Directory where grafana can store logs ;logs = /var/log/grafana # Directory where grafana will automatically scan and look for plugins ;plugins = /var/lib/grafana/plugins # folder that contains provisioning config files that grafana will apply on startup and while running. ;provisioning = conf/provisioning #################################### Server #################################### [server] # Protocol (http, https, socket) ;protocol = http # The ip address to bind to, empty will bind to all interfaces ;http_addr = # The http port to use ;http_port = 3000 # The public facing domain name used to access grafana from a browser ;domain = localhost # Redirect to correct domain if host header does not match domain # Prevents DNS rebinding attacks ;enforce_domain = false # The full public facing url you use in browser, used for redirects and emails # If you use reverse proxy and sub path specify full url (with sub path) ;root_url = http://localhost:3000 # Log web requests ;router_logging = false # the path relative working path ;static_root_path = public # enable gzip ;enable_gzip = false # https certs & key file ;cert_file = ;cert_key = # Unix socket path ;socket = #################################### Database #################################### [database] # You can configure the database connection by specifying type, host, name, user and password # as separate properties or as on string using the url properties. # Either "mysql", "postgres" or "sqlite3", it's your choice ;type = sqlite3 ;host = 127.0.0.1:3306 ;name = grafana ;user = root # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" ;password = # Use either URL or the previous fields to configure the database # Example: mysql://user:secret@host:port/database ;url = # For "postgres" only, either "disable", "require" or "verify-full" ;ssl_mode = disable # For "sqlite3" only, path relative to data_path setting ;path = grafana.db # Max idle conn setting default is 2 ;max_idle_conn = 2 # Max conn setting default is 0 (mean not set) ;max_open_conn = # Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) ;conn_max_lifetime = 14400 # Set to true to log the sql calls and execution times. log_queries = #################################### Session #################################### [session] # Either "memory", "file", "redis", "mysql", "postgres", default is "file" ;provider = file # Provider config options # memory: not have any config yet # file: session dir path, is relative to grafana data_path # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` # mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` # postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable ;provider_config = sessions # Session cookie name ;cookie_name = grafana_sess # If you use session in https only, default is false ;cookie_secure = false # Session life time, default is 86400 ;session_life_time = 86400 #################################### Data proxy ########################### [dataproxy] # This enables data proxy logging, default is false ;logging = false #################################### Analytics #################################### [analytics] # Server reporting, sends usage counters to stats.grafana.org every 24 hours. # No ip addresses are being tracked, only simple counters to track # running instances, dashboard and error counts. It is very helpful to us. # Change this option to false to disable reporting. ;reporting_enabled = true # Set to false to disable all checks to https://grafana.net # for new vesions (grafana itself and plugins), check is used # in some UI views to notify that grafana or plugin update exists # This option does not cause any auto updates, nor send any information # only a GET request to http://grafana.com to get latest versions ;check_for_updates = true # Google Analytics universal tracking code, only enabled if you specify an id here ;google_analytics_ua_id = #################################### Security #################################### [security] # default admin user, created on startup ;admin_user = admin # default admin password, can be changed before first start of grafana, or in profile settings ;admin_password = admin # used for signing ;secret_key = SW2YcwTIb9zpOOhoPsMm # Auto-login remember days ;login_remember_days = 7 ;cookie_username = grafana_user ;cookie_remember_name = grafana_remember # disable gravatar profile images ;disable_gravatar = false # data source proxy whitelist (ip_or_domain:port separated by spaces) ;data_source_proxy_whitelist = # disable protection against brute force login attempts ;disable_brute_force_login_protection = false #################################### Snapshots ########################### [snapshots] # snapshot sharing options ;external_enabled = true ;external_snapshot_url = https://snapshots-origin.raintank.io ;external_snapshot_name = Publish to snapshot.raintank.io # remove expired snapshot ;snapshot_remove_expired = true #################################### Dashboards History ################## [dashboards] # Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1 ;versions_to_keep = 20 #################################### Users ############################### [users] # disable user signup / registration ;allow_sign_up = true # Allow non admin users to create organizations ;allow_org_create = true # Set to true to automatically assign new users to the default organization (id 1) ;auto_assign_org = true # Default role new users will be automatically assigned (if disabled above is set to true) ;auto_assign_org_role = Viewer # Background text for the user field on the login page ;login_hint = email or username # Default UI theme ("dark" or "light") ;default_theme = dark # External user management, these options affect the organization users view ;external_manage_link_url = ;external_manage_link_name = ;external_manage_info = # Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. viewers_can_edit = true [auth] # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false ;disable_login_form = false # Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false ;disable_signout_menu = false #################################### Anonymous Auth ########################## [auth.anonymous] # enable anonymous access enabled = true # specify organization name that should be used for unauthenticated users ;org_name = Main Org. # specify role for unauthenticated users ;org_role = Viewer #################################### Github Auth ########################## [auth.github] ;enabled = false ;allow_sign_up = true ;client_id = some_id ;client_secret = some_secret ;scopes = user:email,read:org ;auth_url = https://github.com/login/oauth/authorize ;token_url = https://github.com/login/oauth/access_token ;api_url = https://api.github.com/user ;team_ids = ;allowed_organizations = #################################### Google Auth ########################## [auth.google] ;enabled = false ;allow_sign_up = true ;client_id = some_client_id ;client_secret = some_client_secret ;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email ;auth_url = https://accounts.google.com/o/oauth2/auth ;token_url = https://accounts.google.com/o/oauth2/token ;api_url = https://www.googleapis.com/oauth2/v1/userinfo ;allowed_domains = #################################### Generic OAuth ########################## [auth.generic_oauth] ;enabled = false ;name = OAuth ;allow_sign_up = true ;client_id = some_id ;client_secret = some_secret ;scopes = user:email,read:org ;auth_url = https://foo.bar/login/oauth/authorize ;token_url = https://foo.bar/login/oauth/access_token ;api_url = https://foo.bar/user ;team_ids = ;allowed_organizations = #################################### Grafana.com Auth #################### [auth.grafana_com] ;enabled = false ;allow_sign_up = true ;client_id = some_id ;client_secret = some_secret ;scopes = user:email ;allowed_organizations = #################################### Auth Proxy ########################## [auth.proxy] ;enabled = false ;header_name = X-WEBAUTH-USER ;header_property = username ;auto_sign_up = true ;ldap_sync_ttl = 60 ;whitelist = 192.168.1.1, 192.168.2.1 #################################### Basic Auth ########################## [auth.basic] ;enabled = true #################################### Auth LDAP ########################## [auth.ldap] ;enabled = false ;config_file = /etc/grafana/ldap.toml ;allow_sign_up = true #################################### SMTP / Emailing ########################## [smtp] ;enabled = false ;host = localhost:25 ;user = # If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" ;password = ;cert_file = ;key_file = ;skip_verify = false ;from_address = admin@grafana.localhost ;from_name = Grafana # EHLO identity in SMTP dialog (defaults to instance_name) ;ehlo_identity = dashboard.example.com [emails] ;welcome_email_on_sign_up = false #################################### Logging ########################## [log] # Either "console", "file", "syslog". Default is console and file # Use space to separate multiple modes, e.g. "console file" ;mode = console file # Either "debug", "info", "warn", "error", "critical", default is "info" ;level = info # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug ;filters = # For "console" mode only [log.console] ;level = # log line format, valid options are text, console and json ;format = console # For "file" mode only [log.file] ;level = # log line format, valid options are text, console and json ;format = text # This enables automated log rotate(switch of following options), default is true ;log_rotate = true # Max line number of single file, default is 1000000 ;max_lines = 1000000 # Max size shift of single file, default is 28 means 1 << 28, 256MB ;max_size_shift = 28 # Segment log daily, default is true ;daily_rotate = true # Expired days of log file(delete after max days), default is 7 ;max_days = 7 [log.syslog] ;level = # log line format, valid options are text, console and json ;format = text # Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used. ;network = ;address = # Syslog facility. user, daemon and local0 through local7 are valid. ;facility = # Syslog tag. By default, the process' argv[0] is used. ;tag = #################################### Alerting ############################ [alerting] # Disable alerting engine & UI features ;enabled = true # Makes it possible to turn off alert rule execution but alerting UI is visible ;execute_alerts = true #################################### Internal Grafana Metrics ########################## # Metrics available at HTTP API Url /metrics [metrics] # Disable / Enable internal metrics ;enabled = true # Publish interval ;interval_seconds = 10 # Send internal metrics to Graphite [metrics.graphite] # Enable by setting the address setting (ex localhost:2003) ;address = ;prefix = prod.grafana.%(instance_name)s. #################################### Distributed tracing ############ [tracing.jaeger] # Enable by setting the address sending traces to jaeger (ex localhost:6831) ;address = localhost:6831 # Tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2) ;always_included_tag = tag1:value1 # Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote ;sampler_type = const # jaeger samplerconfig param # for "const" sampler, 0 or 1 for always false/true respectively # for "probabilistic" sampler, a probability between 0 and 1 # for "rateLimiting" sampler, the number of spans per second # for "remote" sampler, param is the same as for "probabilistic" # and indicates the initial sampling rate before the actual one # is received from the mothership ;sampler_param = 1 #################################### Grafana.com integration ########################## # Url used to to import dashboards directly from Grafana.com [grafana_com] ;url = https://grafana.com #################################### External image storage ########################## [external_image_storage] # Used for uploading images to public servers so they can be included in slack/email messages. # you can choose between (s3, webdav, gcs, azure_blob, local) ;provider = [external_image_storage.s3] ;bucket = ;region = ;path = ;access_key = ;secret_key = [external_image_storage.webdav] ;url = ;public_url = ;username = ;password = [external_image_storage.gcs] ;key_file = ;bucket = ;path = [external_image_storage.azure_blob] ;account_name = ;account_key = ;container_name = [external_image_storage.local] # does not require any configuration prometheus_flask_exporter-0.23.1/examples/sample-signals/grafana/dashboard.yaml000066400000000000000000000002771464362612600300670ustar00rootroot00000000000000apiVersion: 1 providers: - name: "Prometheus" orgId: 1 folder: "" type: file disableDeletion: false editable: true options: path: /var/lib/grafana/dashboards prometheus_flask_exporter-0.23.1/examples/sample-signals/grafana/dashboards/000077500000000000000000000000001464362612600273605ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/sample-signals/grafana/dashboards/example.json000066400000000000000000000460771464362612600317240ustar00rootroot00000000000000{ "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "gnetId": null, "graphTooltip": 0, "links": [], "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 4, "w": 10, "x": 0, "y": 0 }, "id": 2, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:214", "expr": "rate(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Requests per second", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:376", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:377", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 4, "w": 6, "x": 10, "y": 0 }, "id": 4, "legend": { "avg": true, "current": true, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "$$hashKey": "object:1922", "alias": "errors", "color": "#c15c17" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:766", "expr": "sum(rate(flask_http_request_duration_seconds_count{status!=\"200\"}[30s]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "errors", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Errors per second", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:890", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:891", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 4, "w": 8, "x": 16, "y": 0 }, "id": 13, "legend": { "avg": true, "current": false, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": false, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "$$hashKey": "object:255", "alias": "HTTP 500", "color": "#bf1b00" } ], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "$$hashKey": "object:140", "expr": "increase(flask_http_request_total[1m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "HTTP {{ status }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Total requests per minute", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:211", "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "$$hashKey": "object:212", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "fill": 1, "gridPos": { "h": 5, "w": 10, "x": 0, "y": 4 }, "id": 6, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:146", "expr": "rate(flask_http_request_duration_seconds_sum{status=\"200\"}[30s])\n/\nrate(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Average response time [30s]", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:1004", "decimals": null, "format": "s", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:1005", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "", "fill": 1, "gridPos": { "h": 5, "w": 9, "x": 10, "y": 4 }, "id": 15, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:426", "expr": "histogram_quantile(0.5, rate(flask_http_request_duration_seconds_bucket{status=\"200\"}[30s]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Request duration [s] - p50", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:1280", "format": "none", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:1281", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 5, "w": 5, "x": 19, "y": 4 }, "id": 8, "legend": { "avg": false, "current": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:638", "expr": "process_resident_memory_bytes{job=\"example\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "mem", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Memory usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:683", "format": "decbytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:684", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 5, "w": 10, "x": 0, "y": 9 }, "id": 11, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:1079", "expr": "increase(flask_http_request_duration_seconds_bucket{status=\"200\",le=\"0.25\"}[30s]) \n/ ignoring (le) increase(flask_http_request_duration_seconds_count{status=\"200\"}[30s])", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Requests under 250ms", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:1137", "decimals": null, "format": "percentunit", "label": null, "logBase": 1, "max": "1", "min": "0", "show": true }, { "$$hashKey": "object:1138", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 5, "w": 9, "x": 10, "y": 9 }, "id": 16, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:426", "expr": "histogram_quantile(0.9, rate(flask_http_request_duration_seconds_bucket{status=\"200\"}[30s]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ path }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Request duration [s] - p90", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 5, "w": 5, "x": 19, "y": 9 }, "id": 9, "legend": { "avg": false, "current": true, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "$$hashKey": "object:638", "expr": "rate(process_cpu_seconds_total{job=\"example\"}[30s])", "format": "time_series", "intervalFactor": 1, "legendFormat": "cpu", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "CPU usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:683", "format": "percentunit", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:684", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "refresh": "3s", "schemaVersion": 16, "style": "dark", "tags": [], "templating": { "list": [] }, "time": { "from": "now-5m", "to": "now" }, "timepicker": { "refresh_intervals": [ "3s" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "", "title": "Example dashboard", "uid": "_eX4mpl3", "version": 1 } prometheus_flask_exporter-0.23.1/examples/sample-signals/grafana/datasource.yaml000066400000000000000000000002401464362612600302600ustar00rootroot00000000000000apiVersion: 1 datasources: - name: Prometheus type: prometheus editable: true is_default: true access: proxy url: http://prometheus:9090 prometheus_flask_exporter-0.23.1/examples/sample-signals/prometheus/000077500000000000000000000000001464362612600260425ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/sample-signals/prometheus/config.yml000066400000000000000000000004121464362612600300270ustar00rootroot00000000000000global: scrape_interval: 3s external_labels: monitor: "example-app" rule_files: scrape_configs: - job_name: "prometheus" static_configs: - targets: ["prometheus:9090"] - job_name: "example" static_configs: - targets: ["app:5000"] prometheus_flask_exporter-0.23.1/examples/uwsgi-connexion/000077500000000000000000000000001464362612600240645ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/uwsgi-connexion/Dockerfile000066400000000000000000000007061464362612600260610ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache gcc musl-dev linux-headers RUN apk add --no-cache curl && pip install flask connexion uwsgi prometheus_client ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/uwsgi-connexion /var/flask WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp ENV METRICS_PORT 9200 CMD uwsgi --http 0.0.0.0:4000 --module main:app --master --processes 4 --threads 2 prometheus_flask_exporter-0.23.1/examples/uwsgi-connexion/README.md000066400000000000000000000002121464362612600253360ustar00rootroot00000000000000# uWSGI with Connexion with pydantic This is a test case for [issue #61](https://github.com/rycus86/prometheus_flask_exporter/issues/61) prometheus_flask_exporter-0.23.1/examples/uwsgi-connexion/endpoint.py000066400000000000000000000005461464362612600262630ustar00rootroot00000000000000from main import metrics @metrics.summary('test_by_status', 'Test Request latencies by status', labels={ 'code': lambda r: r.status_code }) def test() -> dict: return {'version': 'Test version'} @metrics.content_type('text/plain') @metrics.counter('test_plain', 'Counter for plain responses') def plain() -> str: return 'Test plain response' prometheus_flask_exporter-0.23.1/examples/uwsgi-connexion/main.py000066400000000000000000000004231464362612600253610ustar00rootroot00000000000000import connexion from prometheus_flask_exporter import ConnexionPrometheusMetrics app = connexion.App(__name__) metrics = ConnexionPrometheusMetrics(app) app.add_api('my_api.yaml') if __name__ == '__main__': app.app.run(host='0.0.0.0', port=4000, use_reloader=False) prometheus_flask_exporter-0.23.1/examples/uwsgi-connexion/my_api.yaml000066400000000000000000000005731464362612600262330ustar00rootroot00000000000000openapi: 3.0.0 info: version: 1.0.0 title: Test paths: /test: get: operationId: endpoint.test responses: '200': description: Test /plain: get: operationId: endpoint.plain responses: '200': description: Plain content: text/plain: schema: type: string prometheus_flask_exporter-0.23.1/examples/uwsgi-connexion/run_tests.sh000077500000000000000000000025371464362612600264600ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs uwsgi-connexion docker rm -f uwsgi-connexion > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t uwsgi-connexion ../../. > /dev/null || _fail docker run -d --name uwsgi-connexion -p 4000:4000 uwsgi-connexion > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl -fs http://localhost:4000/metrics > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s -i http://localhost:4000/test | grep 'Content-Type: application/json' -q if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done for _ in $(seq 1 7); do curl -s -i http://localhost:4000/plain | grep 'Content-Type: text/plain' -q if [ "$?" != "0" ]; then echo 'Failed to request the plain endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'test_by_status_count{code="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi curl -s http://localhost:4000/metrics \ | grep 'test_plain_total 7.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f uwsgi-connexion > /dev/null echo 'OK, all done' prometheus_flask_exporter-0.23.1/examples/uwsgi-lazy-apps/000077500000000000000000000000001464362612600240045ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/uwsgi-lazy-apps/Dockerfile000066400000000000000000000007431464362612600260020ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache gcc musl-dev linux-headers ADD examples/uwsgi-lazy-apps/requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/uwsgi-lazy-apps/server.py /var/flask/ WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp CMD uwsgi --http 0.0.0.0:4000 --module server:app --master --processes 4 --threads 2 --lazy-apps prometheus_flask_exporter-0.23.1/examples/uwsgi-lazy-apps/README.md000066400000000000000000000015431464362612600252660ustar00rootroot00000000000000# uWSGI lazy-apps example This [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) example has a [lazy-apps](https://uwsgi-docs.readthedocs.io/en/latest/articles/TheArtOfGracefulReloading.html#preforking-vs-lazy-apps-vs-lazy) sample app in [server.py](server.py) with multiprocessing enabled for metrics collection. In practise, this means the individual forks metrics will be combined, and the metrics endpoint from any of them should include the global stats. This example exposes the metrics on an individual endpoint, managed by [prometheus_client](https://github.com/prometheus/client_python#multiprocess-mode-gunicorn), started only on the master process. ## Thanks Huge thanks for [@xiecang](https://github.com/xiecang) for bringing this to my attention in [prometheus_flask_exporter#31](https://github.com/rycus86/prometheus_flask_exporter/issues/31) ! prometheus_flask_exporter-0.23.1/examples/uwsgi-lazy-apps/requirements.txt000066400000000000000000000000701464362612600272650ustar00rootroot00000000000000flask uwsgi prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/uwsgi-lazy-apps/run_tests.sh000077500000000000000000000020761464362612600263760ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs uwsgi-lazy-apps-sample docker rm -f uwsgi-lazy-apps-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t uwsgi-lazy-apps-sample ../../. > /dev/null || _fail docker run -d --name uwsgi-lazy-apps-sample -p 4000:4000 uwsgi-lazy-apps-sample > /dev/null || _fail echo 'Waiting for uwsgi [lazy apps] to start...' for _ in $(seq 1 10); do PROCESS_COUNT=$(docker exec uwsgi-lazy-apps-sample sh -c 'pgrep -a uwsgi | wc -l') if [ $PROCESS_COUNT -ge 5 ]; then break fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:4000/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f uwsgi-lazy-apps-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/uwsgi-lazy-apps/server.py000066400000000000000000000005471464362612600256720ustar00rootroot00000000000000from flask import Flask from prometheus_flask_exporter.multiprocess import UWsgiPrometheusMetrics app = Flask(__name__) metrics = UWsgiPrometheusMetrics(app) metrics.register_endpoint('/metrics') @app.route('/test') def index(): return 'Hello world' if __name__ == '__main__': metrics.start_http_server(9100) app.run(debug=False, port=5000) prometheus_flask_exporter-0.23.1/examples/uwsgi/000077500000000000000000000000001464362612600220665ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/uwsgi/Dockerfile000066400000000000000000000007311464362612600240610ustar00rootroot00000000000000FROM python:3.11-alpine RUN apk add --no-cache gcc musl-dev linux-headers ADD examples/uwsgi/requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt ADD . /tmp/latest RUN pip install -e /tmp/latest --upgrade ADD examples/uwsgi/server.py /var/flask/ WORKDIR /var/flask ENV PROMETHEUS_MULTIPROC_DIR /tmp ENV prometheus_multiproc_dir /tmp ENV METRICS_PORT 9200 CMD uwsgi --http 0.0.0.0:4000 --module server:app --master --processes 4 --threads 2 prometheus_flask_exporter-0.23.1/examples/uwsgi/README.md000066400000000000000000000013301464362612600233420ustar00rootroot00000000000000# uWSGI example This [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) example has a sample app in [server.py](server.py) with multiprocessing enabled for metrics collection. In practise, this means the individual forks metrics will be combined, and the metrics endpoint from any of them should include the global stats. This example exposes the metrics on an individual endpoint, managed by [prometheus_client](https://github.com/prometheus/client_python#multiprocess-mode-gunicorn), started only on the master process. ## Thanks Huge thanks for [@Miouge1](https://github.com/Miouge1) for bringing this to my attention in [prometheus_flask_exporter#15](https://github.com/rycus86/prometheus_flask_exporter/issues/15) ! prometheus_flask_exporter-0.23.1/examples/uwsgi/requirements.txt000066400000000000000000000000701464362612600253470ustar00rootroot00000000000000flask uwsgi prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/uwsgi/run_tests.sh000077500000000000000000000017711464362612600244610ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs uwsgi-sample docker rm -f uwsgi-sample > /dev/null 2>&1 exit 1 } docker build -f Dockerfile -t uwsgi-sample ../../. > /dev/null || _fail docker run -d --name uwsgi-sample -p 4000:4000 -p 9200:9200 uwsgi-sample > /dev/null || _fail echo 'Waiting for uwsgi to start...' for _ in $(seq 1 10); do PROCESS_COUNT=$(docker exec uwsgi-sample sh -c 'pgrep -a uwsgi | wc -l') if [ $PROCESS_COUNT -ge 5 ]; then break fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:4000/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:9200/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f uwsgi-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/uwsgi/server.py000066400000000000000000000006061464362612600237500ustar00rootroot00000000000000import os from flask import Flask from prometheus_flask_exporter.multiprocess import UWsgiPrometheusMetrics app = Flask(__name__) metrics = UWsgiPrometheusMetrics(app) metrics.start_http_server(int(os.getenv('METRICS_PORT'))) @app.route('/test') def index(): return 'Hello world' if __name__ == '__main__': metrics.start_http_server(9100) app.run(debug=False, port=5000) prometheus_flask_exporter-0.23.1/examples/wsgi/000077500000000000000000000000001464362612600217015ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/examples/wsgi/Dockerfile000066400000000000000000000011111464362612600236650ustar00rootroot00000000000000FROM httpd RUN apt-get update \ && apt-get install --no-install-recommends -y \ python3 python3-dev python3-pip python3-venv \ libapr1-dev libaprutil1-dev gcc WORKDIR /usr/local/apache2 ADD examples/wsgi/requirements.txt /tmp/requirements.txt ADD . /tmp/latest RUN python3 -m venv venv \ && . venv/bin/activate \ && pip install mod_wsgi \ && pip install -r /tmp/requirements.txt \ && pip install -e /tmp/latest --upgrade ADD examples/wsgi/httpd.conf /usr/local/apache2/conf/httpd.conf ADD examples/wsgi/app.py examples/wsgi/wsgi.py /var/flask/ prometheus_flask_exporter-0.23.1/examples/wsgi/README.md000066400000000000000000000007671464362612600231720ustar00rootroot00000000000000# WSGI example This example has a sample app in [app.py](app.py) with multiprocessing enabled for metrics collection. In practise, this means the individual forks metrics will be combined, and the metrics endpoint from any of them should include the global stats. ## Thanks Huge thanks for [@thatcher](https://github.com/thatcher) for bringing this to my attention, and for the investigation he's done in [prometheus_flask_exporter#5](https://github.com/rycus86/prometheus_flask_exporter/issues/5) ! prometheus_flask_exporter-0.23.1/examples/wsgi/app.py000066400000000000000000000010011464362612600230230ustar00rootroot00000000000000from flask import Flask from prometheus_client import multiprocess from prometheus_client.core import CollectorRegistry from prometheus_flask_exporter import PrometheusMetrics app = Flask(__name__) registry = CollectorRegistry() multiprocess.MultiProcessCollector(registry, path='/tmp') metrics = PrometheusMetrics(app, registry=registry) @app.route('/test') def test(): return 'OK' @app.route('/ping') @metrics.do_not_track() def ping(): return 'pong' if __name__ == '__main__': app.run() prometheus_flask_exporter-0.23.1/examples/wsgi/httpd.conf000066400000000000000000000514251464362612600237020ustar00rootroot00000000000000# # This is the main Apache HTTP server configuration file. It contains the # configuration directives that give the server its instructions. # See for detailed information. # In particular, see # # for a discussion of each configuration directive. # # Do NOT simply read the instructions in here without understanding # what they do. They're here only as hints or reminders. If you are unsure # consult the online docs. You have been warned. # # Configuration and logfile names: If the filenames you specify for many # of the server's control files begin with "/" (or "drive:/" for Win32), the # server will use that explicit path. If the filenames do *not* begin # with "/", the value of ServerRoot is prepended -- so "logs/access_log" # with ServerRoot set to "/usr/local/apache2" will be interpreted by the # server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log" # will be interpreted as '/logs/access_log'. # # ServerRoot: The top of the directory tree under which the server's # configuration, error, and log files are kept. # # Do not add a slash at the end of the directory path. If you point # ServerRoot at a non-local disk, be sure to specify a local disk on the # Mutex directive, if file-based mutexes are used. If you wish to share the # same ServerRoot for multiple httpd daemons, you will need to change at # least PidFile. # ServerRoot "/usr/local/apache2" # # Mutex: Allows you to set the mutex mechanism and mutex file directory # for individual mutexes, or change the global defaults # # Uncomment and change the directory if mutexes are file-based and the default # mutex file directory is not on a local disk or is not appropriate for some # other reason. # # Mutex default:logs # # Listen: Allows you to bind Apache to specific IP addresses and/or # ports, instead of the default. See also the # directive. # # Change this to Listen on specific IP addresses as shown below to # prevent Apache from glomming onto all bound IP addresses. # #Listen 12.34.56.78:80 Listen 80 # # Dynamic Shared Object (DSO) Support # # To be able to use the functionality of a module which was built as a DSO you # have to place corresponding `LoadModule' lines at this location so the # directives contained in it are actually available _before_ they are used. # Statically compiled modules (those listed by `httpd -l') do not need # to be loaded here. # # Example: # LoadModule foo_module modules/mod_foo.so # LoadModule mpm_event_module modules/mod_mpm_event.so #LoadModule mpm_prefork_module modules/mod_mpm_prefork.so #LoadModule mpm_worker_module modules/mod_mpm_worker.so LoadModule authn_file_module modules/mod_authn_file.so #LoadModule authn_dbm_module modules/mod_authn_dbm.so #LoadModule authn_anon_module modules/mod_authn_anon.so #LoadModule authn_dbd_module modules/mod_authn_dbd.so #LoadModule authn_socache_module modules/mod_authn_socache.so LoadModule authn_core_module modules/mod_authn_core.so LoadModule authz_host_module modules/mod_authz_host.so LoadModule authz_groupfile_module modules/mod_authz_groupfile.so LoadModule authz_user_module modules/mod_authz_user.so #LoadModule authz_dbm_module modules/mod_authz_dbm.so #LoadModule authz_owner_module modules/mod_authz_owner.so #LoadModule authz_dbd_module modules/mod_authz_dbd.so LoadModule authz_core_module modules/mod_authz_core.so #LoadModule authnz_ldap_module modules/mod_authnz_ldap.so #LoadModule authnz_fcgi_module modules/mod_authnz_fcgi.so LoadModule access_compat_module modules/mod_access_compat.so LoadModule auth_basic_module modules/mod_auth_basic.so #LoadModule auth_form_module modules/mod_auth_form.so #LoadModule auth_digest_module modules/mod_auth_digest.so #LoadModule allowmethods_module modules/mod_allowmethods.so #LoadModule isapi_module modules/mod_isapi.so #LoadModule file_cache_module modules/mod_file_cache.so #LoadModule cache_module modules/mod_cache.so #LoadModule cache_disk_module modules/mod_cache_disk.so #LoadModule cache_socache_module modules/mod_cache_socache.so #LoadModule socache_shmcb_module modules/mod_socache_shmcb.so #LoadModule socache_dbm_module modules/mod_socache_dbm.so #LoadModule socache_memcache_module modules/mod_socache_memcache.so #LoadModule watchdog_module modules/mod_watchdog.so #LoadModule macro_module modules/mod_macro.so #LoadModule dbd_module modules/mod_dbd.so #LoadModule bucketeer_module modules/mod_bucketeer.so #LoadModule dumpio_module modules/mod_dumpio.so #LoadModule echo_module modules/mod_echo.so #LoadModule example_hooks_module modules/mod_example_hooks.so #LoadModule case_filter_module modules/mod_case_filter.so #LoadModule case_filter_in_module modules/mod_case_filter_in.so #LoadModule example_ipc_module modules/mod_example_ipc.so #LoadModule buffer_module modules/mod_buffer.so #LoadModule data_module modules/mod_data.so #LoadModule ratelimit_module modules/mod_ratelimit.so LoadModule reqtimeout_module modules/mod_reqtimeout.so #LoadModule ext_filter_module modules/mod_ext_filter.so #LoadModule request_module modules/mod_request.so #LoadModule include_module modules/mod_include.so LoadModule filter_module modules/mod_filter.so #LoadModule reflector_module modules/mod_reflector.so #LoadModule substitute_module modules/mod_substitute.so #LoadModule sed_module modules/mod_sed.so #LoadModule charset_lite_module modules/mod_charset_lite.so #LoadModule deflate_module modules/mod_deflate.so #LoadModule xml2enc_module modules/mod_xml2enc.so #LoadModule proxy_html_module modules/mod_proxy_html.so LoadModule mime_module modules/mod_mime.so #LoadModule ldap_module modules/mod_ldap.so LoadModule log_config_module modules/mod_log_config.so #LoadModule log_debug_module modules/mod_log_debug.so #LoadModule log_forensic_module modules/mod_log_forensic.so #LoadModule logio_module modules/mod_logio.so #LoadModule lua_module modules/mod_lua.so LoadModule env_module modules/mod_env.so #LoadModule mime_magic_module modules/mod_mime_magic.so #LoadModule cern_meta_module modules/mod_cern_meta.so #LoadModule expires_module modules/mod_expires.so LoadModule headers_module modules/mod_headers.so #LoadModule ident_module modules/mod_ident.so #LoadModule usertrack_module modules/mod_usertrack.so #LoadModule unique_id_module modules/mod_unique_id.so LoadModule setenvif_module modules/mod_setenvif.so LoadModule version_module modules/mod_version.so #LoadModule remoteip_module modules/mod_remoteip.so #LoadModule proxy_module modules/mod_proxy.so #LoadModule proxy_connect_module modules/mod_proxy_connect.so #LoadModule proxy_ftp_module modules/mod_proxy_ftp.so #LoadModule proxy_http_module modules/mod_proxy_http.so #LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so #LoadModule proxy_scgi_module modules/mod_proxy_scgi.so #LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so #LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so #LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so #LoadModule proxy_ajp_module modules/mod_proxy_ajp.so #LoadModule proxy_balancer_module modules/mod_proxy_balancer.so #LoadModule proxy_express_module modules/mod_proxy_express.so #LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so #LoadModule session_module modules/mod_session.so #LoadModule session_cookie_module modules/mod_session_cookie.so #LoadModule session_crypto_module modules/mod_session_crypto.so #LoadModule session_dbd_module modules/mod_session_dbd.so #LoadModule slotmem_shm_module modules/mod_slotmem_shm.so #LoadModule slotmem_plain_module modules/mod_slotmem_plain.so #LoadModule ssl_module modules/mod_ssl.so #LoadModule optional_hook_export_module modules/mod_optional_hook_export.so #LoadModule optional_hook_import_module modules/mod_optional_hook_import.so #LoadModule optional_fn_import_module modules/mod_optional_fn_import.so #LoadModule optional_fn_export_module modules/mod_optional_fn_export.so #LoadModule dialup_module modules/mod_dialup.so #LoadModule http2_module modules/mod_http2.so #LoadModule proxy_http2_module modules/mod_proxy_http2.so #LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so #LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so #LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so #LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so LoadModule unixd_module modules/mod_unixd.so #LoadModule heartbeat_module modules/mod_heartbeat.so #LoadModule heartmonitor_module modules/mod_heartmonitor.so #LoadModule dav_module modules/mod_dav.so LoadModule status_module modules/mod_status.so LoadModule autoindex_module modules/mod_autoindex.so #LoadModule asis_module modules/mod_asis.so #LoadModule info_module modules/mod_info.so #LoadModule suexec_module modules/mod_suexec.so #LoadModule cgid_module modules/mod_cgid.so #LoadModule cgi_module modules/mod_cgi.so #LoadModule dav_fs_module modules/mod_dav_fs.so #LoadModule dav_lock_module modules/mod_dav_lock.so #LoadModule vhost_alias_module modules/mod_vhost_alias.so #LoadModule negotiation_module modules/mod_negotiation.so LoadModule dir_module modules/mod_dir.so #LoadModule imagemap_module modules/mod_imagemap.so #LoadModule actions_module modules/mod_actions.so #LoadModule speling_module modules/mod_speling.so #LoadModule userdir_module modules/mod_userdir.so LoadModule alias_module modules/mod_alias.so #LoadModule rewrite_module modules/mod_rewrite.so # # If you wish httpd to run as a different user or group, you must run # httpd as root initially and it will switch. # # User/Group: The name (or #number) of the user/group to run httpd as. # It is usually good practice to create a dedicated user and group for # running httpd, as with most system services. # User daemon Group daemon # 'Main' server configuration # # The directives in this section set up the values used by the 'main' # server, which responds to any requests that aren't handled by a # definition. These values also provide defaults for # any containers you may define later in the file. # # All of these directives may appear inside containers, # in which case these default settings will be overridden for the # virtual host being defined. # # # ServerAdmin: Your address, where problems with the server should be # e-mailed. This address appears on some server-generated pages, such # as error documents. e.g. admin@your-domain.com # ServerAdmin you@example.com # # ServerName gives the name and port that the server uses to identify itself. # This can often be determined automatically, but we recommend you specify # it explicitly to prevent problems during startup. # # If your host doesn't have a registered DNS name, enter its IP address here. # #ServerName www.example.com:80 # # Deny access to the entirety of your server's filesystem. You must # explicitly permit access to web content directories in other # blocks below. # AllowOverride none Require all denied # # Note that from this point forward you must specifically allow # particular features to be enabled - so if something's not working as # you might expect, make sure that you have specifically enabled it # below. # # # DocumentRoot: The directory out of which you will serve your # documents. By default, all requests are taken from this directory, but # symbolic links and aliases may be used to point to other locations. # DocumentRoot "/usr/local/apache2/htdocs" # # Possible values for the Options directive are "None", "All", # or any combination of: # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews # # Note that "MultiViews" must be named *explicitly* --- "Options All" # doesn't give it to you. # # The Options directive is both complicated and important. Please see # http://httpd.apache.org/docs/2.4/mod/core.html#options # for more information. # Options Indexes FollowSymLinks # # AllowOverride controls what directives may be placed in .htaccess files. # It can be "All", "None", or any combination of the keywords: # AllowOverride FileInfo AuthConfig Limit # AllowOverride None # # Controls who can get stuff from this server. # Require all granted # # DirectoryIndex: sets the file that Apache will serve if a directory # is requested. # DirectoryIndex index.html # # The following lines prevent .htaccess and .htpasswd files from being # viewed by Web clients. # Require all denied # # ErrorLog: The location of the error log file. # If you do not specify an ErrorLog directive within a # container, error messages relating to that virtual host will be # logged here. If you *do* define an error logfile for a # container, that host's errors will be logged there and not here. # ErrorLog /proc/self/fd/2 # # LogLevel: Control the number of messages logged to the error_log. # Possible values include: debug, info, notice, warn, error, crit, # alert, emerg. # LogLevel warn # # The following directives define some format nicknames for use with # a CustomLog directive (see below). # LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined LogFormat "%h %l %u %t \"%r\" %>s %b" common # You need to enable mod_logio.c to use %I and %O LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio # # The location and format of the access logfile (Common Logfile Format). # If you do not define any access logfiles within a # container, they will be logged here. Contrariwise, if you *do* # define per- access logfiles, transactions will be # logged therein and *not* in this file. # CustomLog /proc/self/fd/1 common # # If you prefer a logfile with access, agent, and referer information # (Combined Logfile Format) you can use the following directive. # #CustomLog "logs/access_log" combined # # Redirect: Allows you to tell clients about documents that used to # exist in your server's namespace, but do not anymore. The client # will make a new request for the document at its new location. # Example: # Redirect permanent /foo http://www.example.com/bar # # Alias: Maps web paths into filesystem paths and is used to # access content that does not live under the DocumentRoot. # Example: # Alias /webpath /full/filesystem/path # # If you include a trailing / on /webpath then the server will # require it to be present in the URL. You will also likely # need to provide a section to allow access to # the filesystem path. # # ScriptAlias: This controls which directories contain server scripts. # ScriptAliases are essentially the same as Aliases, except that # documents in the target directory are treated as applications and # run by the server when requested rather than as documents sent to the # client. The same rules about trailing "/" apply to ScriptAlias # directives as to Alias. # ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/" # # ScriptSock: On threaded servers, designate the path to the UNIX # socket used to communicate with the CGI daemon of mod_cgid. # #Scriptsock cgisock # # "/usr/local/apache2/cgi-bin" should be changed to whatever your ScriptAliased # CGI directory exists, if you have that configured. # AllowOverride None Options None Require all granted # # Avoid passing HTTP_PROXY environment to CGI's on this or any proxied # backend servers which have lingering "httpoxy" defects. # 'Proxy' request header is undefined by the IETF, not listed by IANA # RequestHeader unset Proxy early # # TypesConfig points to the file containing the list of mappings from # filename extension to MIME-type. # TypesConfig conf/mime.types # # AddType allows you to add to or override the MIME configuration # file specified in TypesConfig for specific file types. # #AddType application/x-gzip .tgz # # AddEncoding allows you to have certain browsers uncompress # information on the fly. Note: Not all browsers support this. # #AddEncoding x-compress .Z #AddEncoding x-gzip .gz .tgz # # If the AddEncoding directives above are commented-out, then you # probably should define those extensions to indicate media types: # AddType application/x-compress .Z AddType application/x-gzip .gz .tgz # # AddHandler allows you to map certain file extensions to "handlers": # actions unrelated to filetype. These can be either built into the server # or added with the Action directive (see below) # # To use CGI scripts outside of ScriptAliased directories: # (You will also need to add "ExecCGI" to the "Options" directive.) # #AddHandler cgi-script .cgi # For type maps (negotiated resources): #AddHandler type-map var # # Filters allow you to process content before it is sent to the client. # # To parse .shtml files for server-side includes (SSI): # (You will also need to add "Includes" to the "Options" directive.) # #AddType text/html .shtml #AddOutputFilter INCLUDES .shtml # # The mod_mime_magic module allows the server to use various hints from the # contents of the file itself to determine its type. The MIMEMagicFile # directive tells the module where the hint definitions are located. # #MIMEMagicFile conf/magic # # Customizable error responses come in three flavors: # 1) plain text 2) local redirects 3) external redirects # # Some examples: #ErrorDocument 500 "The server made a boo boo." #ErrorDocument 404 /missing.html #ErrorDocument 404 "/cgi-bin/missing_handler.pl" #ErrorDocument 402 http://www.example.com/subscription_info.html # # # MaxRanges: Maximum number of Ranges in a request before # returning the entire resource, or one of the special # values 'default', 'none' or 'unlimited'. # Default setting is to accept 200 Ranges. #MaxRanges unlimited # # EnableMMAP and EnableSendfile: On systems that support it, # memory-mapping or the sendfile syscall may be used to deliver # files. This usually improves server performance, but must # be turned off when serving from networked-mounted # filesystems or if support for these functions is otherwise # broken on your system. # Defaults: EnableMMAP On, EnableSendfile Off # #EnableMMAP off #EnableSendfile on # Supplemental configuration # # The configuration files in the conf/extra/ directory can be # included to add extra features or to modify the default configuration of # the server, or you may simply copy their contents here and change as # necessary. # Server-pool management (MPM specific) #Include conf/extra/httpd-mpm.conf # Multi-language error messages #Include conf/extra/httpd-multilang-errordoc.conf # Fancy directory listings #Include conf/extra/httpd-autoindex.conf # Language settings #Include conf/extra/httpd-languages.conf # User home directories #Include conf/extra/httpd-userdir.conf # Real-time info on requests and configuration #Include conf/extra/httpd-info.conf # Virtual hosts #Include conf/extra/httpd-vhosts.conf # Local access to the Apache HTTP Server Manual #Include conf/extra/httpd-manual.conf # Distributed authoring and versioning (WebDAV) #Include conf/extra/httpd-dav.conf # Various default settings #Include conf/extra/httpd-default.conf # Configure mod_proxy_html to understand HTML4/XHTML1 Include conf/extra/proxy-html.conf # Secure (SSL/TLS) connections #Include conf/extra/httpd-ssl.conf # # Note: The following must must be present to support # starting without SSL on platforms with no /dev/random equivalent # but a statically compiled-in mod_ssl. # SSLRandomSeed startup builtin SSLRandomSeed connect builtin LoadModule wsgi_module "/usr/local/apache2/venv/lib/python3.11/site-packages/mod_wsgi/server/mod_wsgi-py311.cpython-311-x86_64-linux-gnu.so" WSGIPythonHome "/usr/local/apache2/venv" WSGISocketPrefix /var/run/wsgi LogLevel info WSGIDaemonProcess sample-services processes=8 threads=48 maximum-requests=10000 display-name=%{GROUP} WSGIProcessGroup sample-services WSGIApplicationGroup %{GLOBAL} WSGIScriptAlias / /var/flask/wsgi.py Require all granted Order allow,deny Allow from all prometheus_flask_exporter-0.23.1/examples/wsgi/requirements.txt000066400000000000000000000000621464362612600251630ustar00rootroot00000000000000flask prometheus_client prometheus_flask_exporter prometheus_flask_exporter-0.23.1/examples/wsgi/run_tests.sh000077500000000000000000000017621464362612600242740ustar00rootroot00000000000000#!/bin/bash cd "$(dirname "$0")" _fail() { docker logs wsgi-sample docker rm -f wsgi-sample > /dev/null 2>&1 exit 1 } docker build --platform linux/amd64 -f Dockerfile -t wsgi-sample ../../. > /dev/null || _fail docker run -d --name wsgi-sample -p 8889:80 wsgi-sample > /dev/null || _fail echo 'Waiting for the server to start...' for _ in $(seq 1 10); do if curl --max-time 1 -fs http://localhost:8889/ping > /dev/null; then break else sleep 0.2 fi done echo 'Starting the tests...' for _ in $(seq 1 10); do curl -s http://localhost:8889/test > /dev/null if [ "$?" != "0" ]; then echo 'Failed to request the test endpoint' _fail fi done curl -s http://localhost:8889/metrics \ | grep 'flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 10.0' \ > /dev/null if [ "$?" != "0" ]; then echo 'The expected metrics are not found' _fail fi docker rm -f wsgi-sample > /dev/null echo 'OK, all done'prometheus_flask_exporter-0.23.1/examples/wsgi/wsgi.py000066400000000000000000000003111464362612600232170ustar00rootroot00000000000000import os import sys sys.path.insert(0, os.path.dirname(__file__)) os.environ["PROMETHEUS_MULTIPROC_DIR"] = "/tmp" os.environ["prometheus_multiproc_dir"] = "/tmp" from app import app as application prometheus_flask_exporter-0.23.1/prepare_release.sh000077500000000000000000000007541464362612600226150ustar00rootroot00000000000000#!/usr/bin/env bash if [ -z "$1" ]; then echo 'No version number given' exit 1 fi VERSION="$1" sed -i '' "s/version=.*,/version='${VERSION}',/" setup.py sed -i '' "s#download_url=.*,#download_url='https://github.com/rycus86/prometheus_flask_exporter/archive/${VERSION}.tar.gz',#" setup.py sed -i '' "s/__version__ = '.*'/__version__ = '${VERSION}'/" prometheus_flask_exporter/__init__.py sed -i '' "s/prometheus-flask-exporter==.*/prometheus-flask-exporter==${VERSION}/" README.md prometheus_flask_exporter-0.23.1/prometheus_flask_exporter/000077500000000000000000000000001464362612600244155ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/prometheus_flask_exporter/__init__.py000066400000000000000000001122111464362612600265240ustar00rootroot00000000000000import functools import inspect import os import re import sys import threading import warnings from timeit import default_timer from flask import Flask, Response from flask import request, make_response, current_app from flask.views import MethodView from prometheus_client import Counter, Histogram, Gauge, Summary from prometheus_client import multiprocess as pc_multiprocess, CollectorRegistry try: # prometheus-client >= 0.14.0 from prometheus_client.exposition import choose_encoder except ImportError: # prometheus-client < 0.14.0 from prometheus_client.exposition import choose_formatter as choose_encoder from werkzeug.serving import is_running_from_reloader if sys.version_info[0:2] >= (3, 4): # Python v3.4+ has a built-in has __wrapped__ attribute wraps = functools.wraps else: # in previous Python version we have to set the missing attribute def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES): def wrapper(f): f = functools.wraps(wrapped, assigned, updated)(f) f.__wrapped__ = wrapped return f return wrapper try: # try to convert http.HTTPStatus to int status codes from http import HTTPStatus def _to_status_code(response_status): if isinstance(response_status, HTTPStatus): return response_status.value else: return response_status except ImportError: # otherwise simply use the status as is def _to_status_code(response_status): return response_status NO_PREFIX = '#no_prefix' """ Constant indicating that default metrics should not have any prefix applied. It purposely uses invalid characters defined for metrics names as specified in Prometheus documentation (see: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels) """ class PrometheusMetrics: """ Prometheus metrics export configuration for Flask. The default metrics include a Histogram for HTTP request latencies and number of HTTP requests plus a Counter for the total number of HTTP requests. Sample usage: app = Flask(__name__) metrics = PrometheusMetrics(app) # static information as metric metrics.info('app_info', 'Application info', version='1.0.3') @app.route('/') def main(): pass # requests tracked by default @app.route('/skip') @metrics.do_not_track() def skip(): pass # default metrics are not collected @app.route('/') @metrics.do_not_track() @metrics.counter('invocation_by_type', 'Number of invocations by type', labels={'item_type': lambda: request.view_args['type']}) def by_type(item_type): pass # only the counter is collected, not the default metrics @app.route('/long-running') @metrics.gauge('in_progress', 'Long running requests in progress') def long_running(): pass @app.route('/status/') @metrics.do_not_track() @metrics.summary('requests_by_status', 'Request latencies by status', labels={'status': lambda r: r.status_code}) @metrics.histogram('requests_by_status_and_path', 'Request latencies by status and path', labels={'status': lambda r: r.status_code, 'path': lambda: request.path}) def echo_status(status): return 'Status: %s' % status, status Label values can be defined as callables: - With a single argument that will be the Flask Response object - Without an argument, possibly to use with the Flask `request` object """ def __init__(self, app, path='/metrics', export_defaults=True, defaults_prefix='flask', group_by='path', buckets=None, default_latency_as_histogram=True, default_labels=None, response_converter=None, excluded_paths=None, exclude_user_defaults=True, metrics_decorator=None, registry=None, **kwargs): """ Create a new Prometheus metrics export configuration. :param app: the Flask application :param path: the metrics path (defaults to `/metrics`) :param export_defaults: expose all HTTP request latencies and number of HTTP requests :param defaults_prefix: string to prefix the default exported metrics name with (when either `export_defaults=True` or `export_defaults(..)` is called) or in case you don't want any prefix then use `NO_PREFIX` constant :param group_by: group default HTTP metrics by this request property, like `path`, `endpoint`, `url_rule`, etc. (defaults to `path`) :param buckets: the time buckets for request latencies (will use the default when `None`) :param default_latency_as_histogram: export request latencies as a Histogram (defaults), otherwise use a Summary :param default_labels: default labels to attach to each of the metrics exposed by this `PrometheusMetrics` instance :param response_converter: a function that converts the captured the produced response object to a Flask friendly representation :param metrics_decorator: an optional decorator to apply to the metrics endpoint, takes a function and needs to return a function :param excluded_paths: regular expression(s) as a string or a list of strings for paths to exclude from tracking :param exclude_user_defaults: also apply the `excluded_paths` exclusions to user-defined defaults (not only built-in ones) :param registry: the Prometheus Registry to use """ self.app = app self.path = path self._export_defaults = export_defaults self._defaults_prefix = defaults_prefix or 'flask' self._default_labels = default_labels or {} self._default_latency_as_histogram = default_latency_as_histogram self._response_converter = response_converter or make_response self._metrics_decorator = metrics_decorator self.buckets = buckets self.version = __version__ if registry: self.registry = registry else: # load the default registry from the underlying # Prometheus library here for easier unit testing # see https://github.com/rycus86/prometheus_flask_exporter/pull/20 from prometheus_client import REGISTRY as DEFAULT_REGISTRY self.registry = DEFAULT_REGISTRY if kwargs.get('static_labels'): warnings.warn( 'The `static_labels` argument of `PrometheusMetrics` is ' 'deprecated since 0.15.0, please use the ' 'new `default_labels` argument.', DeprecationWarning ) for key, value in kwargs.get('static_labels', dict()).items(): if key not in self._default_labels: self._default_labels[key] = value if kwargs.get('group_by_endpoint') is True: warnings.warn( 'The `group_by_endpoint` argument of `PrometheusMetrics` is ' 'deprecated since 0.4.0, please use the ' 'new `group_by` argument.', DeprecationWarning ) self.group_by = 'endpoint' elif group_by: self.group_by = group_by else: self.group_by = 'path' if excluded_paths: if PrometheusMetrics._is_string(excluded_paths): excluded_paths = [excluded_paths] self.excluded_paths = [ re.compile(p) for p in excluded_paths ] else: self.excluded_paths = None self.exclude_user_defaults = exclude_user_defaults if app is not None: self.init_app(app) @classmethod def for_app_factory(cls, **kwargs): """ A convenience method to create a new instance that is suitable for Flask "app factory" configurations. Please see: http://flask.pocoo.org/docs/1.0/patterns/appfactories/ Note, that you will need to call `init_app(...)` later with the Flask application as its parameter. This method takes the same keyword arguments as the default constructor. """ return cls(app=None, **kwargs) def init_app(self, app): """ This callback can be used to initialize an application for the use with this prometheus reporter setup. This is usually used with a Flask "app factory" configuration. Please see: http://flask.pocoo.org/docs/1.0/patterns/appfactories/ Note, that you need to use `PrometheusMetrics.for_app_factory()` for this mode, otherwise it is called automatically. :param app: the Flask application """ if self.path: self.register_endpoint(self.path, app) if self._export_defaults: self.export_defaults( buckets=self.buckets, group_by=self.group_by, latency_as_histogram=self._default_latency_as_histogram, prefix=self._defaults_prefix, app=app ) def register_endpoint(self, path, app=None): """ Register the metrics endpoint on the Flask application. :param path: the path of the endpoint :param app: the Flask application to register the endpoint on (by default it is the application registered with this class) """ if app is None: app = self.app or current_app if is_running_from_reloader() and not os.environ.get('DEBUG_METRICS'): app.logger.debug( 'Metrics are disabled when run in the Flask development server' ' with reload enabled. Set the environment variable' ' DEBUG_METRICS=1 to enable them anyway.' ) return @self.do_not_track() def prometheus_metrics(): accept_header = request.headers.get("Accept") if 'name[]' in request.args: names = request.args.getlist('name[]') else: names = None generated_data, content_type = self.generate_metrics(accept_header, names) headers = {'Content-Type': content_type} return generated_data, 200, headers # apply any user supplied decorators, like authentication if self._metrics_decorator: prometheus_metrics = self._metrics_decorator(prometheus_metrics) # apply the Flask route decorator on our metrics endpoint app.route(path)(prometheus_metrics) def generate_metrics(self, accept_header=None, names=None): """ Generate the metrics output for Prometheus to consume. This can be exposed on a dedicated server, or on the Flask app, or for local development you can use the shorthand method to expose it on a new Flask app, see `PrometheusMetrics.start_http_server()`. :param accept_header: The value of the HTTP Accept request header (default `None`) :param names: Names to only return samples for, must be a list of strings if not `None` (default `None`) :return: a tuple of response content and response content type (both `str` types) """ if 'PROMETHEUS_MULTIPROC_DIR' in os.environ or 'prometheus_multiproc_dir' in os.environ: registry = CollectorRegistry() else: registry = self.registry if names: registry = registry.restricted_registry(names) if 'PROMETHEUS_MULTIPROC_DIR' in os.environ or 'prometheus_multiproc_dir' in os.environ: pc_multiprocess.MultiProcessCollector(registry) generate_latest, content_type = choose_encoder(accept_header) generated_content = generate_latest(registry).decode('utf-8') return generated_content, content_type def start_http_server(self, port, host='0.0.0.0', endpoint='/metrics', ssl=None): """ Start an HTTP server for exposing the metrics. This will be an individual Flask application, not the one registered with this class. :param port: the HTTP port to expose the metrics endpoint on :param host: the HTTP host to listen on (default: `0.0.0.0`) :param endpoint: the URL path to expose the endpoint on (default: `/metrics`) :param ssl: enable SSL to http server It expects a dict with 2 keys: `cert` and `key` with certificate and key paths. Default: `None` """ if is_running_from_reloader(): return app = Flask('prometheus-flask-exporter-%d' % port) self.register_endpoint(endpoint, app) def run_app(): if ssl is None: app.run(host=host, port=port) else: app.run(host=host, port=port, ssl_context=(ssl["cert"], ssl["key"])) thread = threading.Thread(target=run_app) thread.daemon = True thread.start() def export_defaults(self, buckets=None, group_by='path', latency_as_histogram=True, prefix='flask', app=None, **kwargs): """ Export the default metrics: - HTTP request latencies - HTTP request exceptions - Number of HTTP requests :param buckets: the time buckets for request latencies (will use the default when `None`) :param group_by: group default HTTP metrics by this request property, like `path`, `endpoint`, `rule`, etc. (defaults to `path`) :param latency_as_histogram: export request latencies as a Histogram, otherwise use a Summary instead (defaults to `True` to export as a Histogram) :param prefix: prefix to start the default metrics names with or `NO_PREFIX` (to skip prefix) :param app: the Flask application """ if app is None: app = self.app or current_app if not prefix: prefix = self._defaults_prefix or 'flask' if kwargs.get('group_by_endpoint') is True: warnings.warn( 'The `group_by_endpoint` argument of ' '`PrometheusMetrics.export_defaults` is deprecated since 0.4.0, ' 'please use the new `group_by` argument.', DeprecationWarning ) duration_group = 'endpoint' elif group_by: duration_group = group_by else: duration_group = 'path' if callable(duration_group): duration_group_name = duration_group.__name__ else: duration_group_name = duration_group if prefix == NO_PREFIX: prefix = "" else: prefix = prefix + "_" try: self.info( '%sexporter_info' % prefix, 'Information about the Prometheus Flask exporter', version=self.version ) except ValueError: return # looks like we have already exported the default metrics labels = self._get_combined_labels(None) if latency_as_histogram: # use the default buckets from prometheus_client if not given here buckets_as_kwargs = {} if buckets is not None: buckets_as_kwargs['buckets'] = buckets request_duration_metric = Histogram( '%shttp_request_duration_seconds' % prefix, 'Flask HTTP request duration in seconds', ('method', duration_group_name, 'status') + labels.keys(), registry=self.registry, **buckets_as_kwargs ) else: # export as Summary instead request_duration_metric = Summary( '%shttp_request_duration_seconds' % prefix, 'Flask HTTP request duration in seconds', ('method', duration_group_name, 'status') + labels.keys(), registry=self.registry ) counter_labels = ('method', 'status') + labels.keys() request_total_metric = Counter( '%shttp_request_total' % prefix, 'Total number of HTTP requests', counter_labels, registry=self.registry ) request_exceptions_metric = Counter( '%shttp_request_exceptions_total' % prefix, 'Total number of HTTP requests which resulted in an exception', counter_labels, registry=self.registry ) def before_request(): request.prom_start_time = default_timer() def after_request(response): if hasattr(request, 'prom_do_not_track') or hasattr(request, 'prom_exclude_all'): return response if self.excluded_paths: if any(pattern.match(request.path) for pattern in self.excluded_paths): return response if hasattr(request, 'prom_start_time') and self._not_yet_handled('duration_reported'): total_time = max(default_timer() - request.prom_start_time, 0) if callable(duration_group): group = duration_group(request) else: group = getattr(request, duration_group) request_duration_labels = { 'method': request.method, 'status': _to_status_code(response.status_code), duration_group_name: group } request_duration_labels.update(labels.values_for(response)) request_duration_metric.labels(**request_duration_labels).observe(total_time) if self._not_yet_handled('total_reported'): request_total_metric.labels( method=request.method, status=_to_status_code(response.status_code), **labels.values_for(response) ).inc() return response def teardown_request(exception=None): if not exception or hasattr(request, 'prom_do_not_track') or hasattr(request, 'prom_exclude_all'): return if self.excluded_paths: if any(pattern.match(request.path) for pattern in self.excluded_paths): return response = make_response('Exception: %s' % exception, 500) if callable(duration_group): group = duration_group(request) else: group = getattr(request, duration_group) request_exceptions_metric.labels( method=request.method, status=500, **labels.values_for(response) ).inc() if hasattr(request, 'prom_start_time') and self._not_yet_handled('duration_reported'): total_time = max(default_timer() - request.prom_start_time, 0) request_duration_labels = { 'method': request.method, 'status': 500, duration_group_name: group } request_duration_labels.update(labels.values_for(response)) request_duration_metric.labels(**request_duration_labels).observe(total_time) if self._not_yet_handled('total_reported'): request_total_metric.labels( method=request.method, status=500, **labels.values_for(response) ).inc() return app.before_request(before_request) app.after_request(after_request) app.teardown_request(teardown_request) def register_default(self, *metric_wrappers, **kwargs): """ Registers metric wrappers to track all endpoints, similar to `export_defaults` but with user defined metrics. Call this function after all routes have been set up. Use the metric wrappers as arguments: - metrics.counter(..) - metrics.gauge(..) - metrics.summary(..) - metrics.histogram(..) :param metric_wrappers: one or more metric wrappers to register for all available endpoints :param app: the Flask application to register the default metric for (by default it is the application registered with this class) """ app = kwargs.get('app') if app is None: app = self.app or current_app for endpoint, view_func in app.view_functions.items(): for wrapper in metric_wrappers: view_func = wrapper(view_func) app.view_functions[endpoint] = view_func def histogram(self, name, description, labels=None, initial_value_when_only_static_labels=True, **kwargs): """ Use a Histogram to track the execution time and invocation count of the method. :param name: the name of the metric :param description: the description of the metric :param labels: a dictionary of `{labelname: callable_or_value}` for labels :param initial_value_when_only_static_labels: whether to give metric an initial value when only static labels are present :param kwargs: additional keyword arguments for creating the Histogram """ return self._track( Histogram, lambda metric, time: metric.observe(time), kwargs, name, description, labels, initial_value_when_only_static_labels=initial_value_when_only_static_labels, registry=self.registry ) def summary(self, name, description, labels=None, initial_value_when_only_static_labels=True, **kwargs): """ Use a Summary to track the execution time and invocation count of the method. :param name: the name of the metric :param description: the description of the metric :param labels: a dictionary of `{labelname: callable_or_value}` for labels :param initial_value_when_only_static_labels: whether to give metric an initial value when only static labels are present :param kwargs: additional keyword arguments for creating the Summary """ return self._track( Summary, lambda metric, time: metric.observe(time), kwargs, name, description, labels, initial_value_when_only_static_labels=initial_value_when_only_static_labels, registry=self.registry ) def gauge(self, name, description, labels=None, initial_value_when_only_static_labels=True, **kwargs): """ Use a Gauge to track the number of invocations in progress for the method. :param name: the name of the metric :param description: the description of the metric :param labels: a dictionary of `{labelname: callable_or_value}` for labels :param initial_value_when_only_static_labels: whether to give metric an initial value when only static labels are present :param kwargs: additional keyword arguments for creating the Gauge """ return self._track( Gauge, lambda metric, time: metric.dec(), kwargs, name, description, labels, initial_value_when_only_static_labels=initial_value_when_only_static_labels, registry=self.registry, before=lambda metric: metric.inc(), revert_when_not_tracked=lambda metric: metric.dec() ) def counter(self, name, description, labels=None, initial_value_when_only_static_labels=True, **kwargs): """ Use a Counter to track the total number of invocations of the method. :param name: the name of the metric :param description: the description of the metric :param labels: a dictionary of `{labelname: callable_or_value}` for labels :param initial_value_when_only_static_labels: whether to give metric an initial value when only static labels are present :param kwargs: additional keyword arguments for creating the Counter """ return self._track( Counter, lambda metric, time: metric.inc(), kwargs, name, description, labels, initial_value_when_only_static_labels=initial_value_when_only_static_labels, registry=self.registry ) def _track(self, metric_type, metric_call, metric_kwargs, name, description, labels, initial_value_when_only_static_labels, registry, before=None, revert_when_not_tracked=None): """ Internal method decorator logic. :param metric_type: the type of the metric from the `prometheus_client` library :param metric_call: the invocation to execute as a callable with `(metric, time)` :param metric_kwargs: additional keyword arguments for creating the metric :param name: the name of the metric :param description: the description of the metric :param labels: a dictionary of `{labelname: callable_or_value}` for labels :param initial_value_when_only_static_labels: whether to give metric an initial value when only static labels are present :param registry: the Prometheus Registry to use :param before: an optional callable to invoke before executing the request handler method accepting the single `metric` argument :param revert_when_not_tracked: an optional callable to invoke when a non-tracked endpoint is being handled to undo any actions already done on it, accepts a single `metric` argument """ if labels is not None and not isinstance(labels, dict): raise TypeError('labels needs to be a dictionary of {labelname: callable}') labels = self._get_combined_labels(labels) parent_metric = metric_type( name, description, labelnames=labels.keys(), registry=registry, **metric_kwargs ) # When all labels are already known at this point, the metric can get an initial value. if initial_value_when_only_static_labels and labels.has_keys() and labels.has_only_static_values(): parent_metric.labels(*labels.get_default_values()) def get_metric(response): if labels.has_keys(): return parent_metric.labels(**labels.values_for(response)) else: return parent_metric def decorator(f): @wraps(f) def func(*args, **kwargs): if self.exclude_user_defaults and self.excluded_paths: # exclude based on default excludes if any(pattern.match(request.path) for pattern in self.excluded_paths): return f(*args, **kwargs) if before: metric = get_metric(None) before(metric) else: metric = None exception = None start_time = default_timer() try: try: # execute the handler function response = f(*args, **kwargs) except Exception as ex: # let Flask decide to wrap or reraise the Exception response = current_app.handle_user_exception(ex) except Exception as ex: # if it was re-raised, treat it as an InternalServerError exception = ex response = make_response(f'Exception: {ex}', 500) if hasattr(request, 'prom_exclude_all'): if metric and revert_when_not_tracked: # special handling for Gauge metrics revert_when_not_tracked(metric) return response total_time = max(default_timer() - start_time, 0) if not metric: if not isinstance(response, Response) and request.endpoint: view_func = current_app.view_functions[request.endpoint] # There may be decorators 'above' us, # but before the function is registered with Flask while view_func and view_func != f: try: view_func = view_func.__wrapped__ except AttributeError: break if view_func == f: # we are in a request handler method response = self._response_converter(response) elif hasattr(view_func, 'view_class') and issubclass(view_func.view_class, MethodView): # we are in a method view (for Flask-RESTful for example) response = self._response_converter(response) metric = get_metric(response) metric_call(metric, time=total_time) if exception: try: # re-raise for the Flask error handler raise exception except Exception as ex: return current_app.handle_user_exception(ex) else: return response return func return decorator def _get_combined_labels(self, labels): """ Combines the given labels with static and default labels and wraps them into an object that can efficiently return the keys and values of these combined labels. """ labels = labels.copy() if labels else dict() if self._default_labels: labels.update(self._default_labels.copy()) def argspec(func): if hasattr(inspect, 'getfullargspec'): return inspect.getfullargspec(func) else: return inspect.getargspec(func) def label_value(f): if not callable(f): return lambda x: f if argspec(f).args: return lambda x: f(x) else: return lambda x: f() class CombinedLabels: def __init__(self, _labels): self.labels = _labels.items() def keys(self): return tuple(map(lambda k: k[0], self.labels)) def has_keys(self): return len(self.labels) > 0 def has_only_static_values(self): for key, value in self.labels: if callable(value): return False return True def get_default_values(self): return list(value for key, value in self.labels) def values_for(self, response): label_generator = tuple( (key, label_value(call)) for key, call in self.labels ) if labels else tuple() return {key: value(response) for key, value in label_generator} return CombinedLabels(labels) @staticmethod def do_not_track(): """ Decorator to skip the default metrics collection for the method. *Note*: explicit metrics decorators will still collect the data """ def decorator(f): @wraps(f) def func(*args, **kwargs): request.prom_do_not_track = True return f(*args, **kwargs) return func return decorator @staticmethod def exclude_all_metrics(): """ Decorator to skip all metrics collection for the method. """ def decorator(f): @wraps(f) def func(*args, **kwargs): request.prom_exclude_all = True return f(*args, **kwargs) return func return decorator def info(self, name, description, labelnames=None, labelvalues=None, **labels): """ Report any information as a Prometheus metric. This will create a `Gauge` with the initial value of 1. The easiest way to use it is: metrics = PrometheusMetrics(app) metrics.info( 'app_info', 'Application info', version='1.0', major=1, minor=0 ) If the order of the labels matters: metrics = PrometheusMetrics(app) metrics.info( 'app_info', 'Application info', ('version', 'major', 'minor'), ('1.0', 1, 0) ) :param name: the name of the metric :param description: the description of the metric :param labelnames: the names of the labels :param labelvalues: the values of the labels :param labels: the names and values of the labels :return: the newly created `Gauge` metric """ if labels and labelnames: raise ValueError( 'Cannot have labels defined as `dict` ' 'and collections of names and values' ) if labelnames is None and labels: labelnames = labels.keys() elif labelnames and labelvalues: for idx, label_name in enumerate(labelnames): labels[label_name] = labelvalues[idx] gauge = Gauge( name, description, labelnames or tuple(), registry=self.registry, multiprocess_mode='max' ) if labels: gauge = gauge.labels(**labels) gauge.set(1) return gauge @staticmethod def _is_string(value): try: return isinstance(value, str) # python3 except NameError: return isinstance(value, basestring) # python2 @staticmethod def _not_yet_handled(tracking_key): """ Check if the request has not handled some tracking yet, and mark the request if this is the first time. This is to avoid follow-up actions. :param tracking_key: a key identifying a processing step :return: True if this is the first time the request is trying to handle this processing step """ key = f'prom_{tracking_key}' if hasattr(request, key): return False else: setattr(request, key, True) return True class ConnexionPrometheusMetrics(PrometheusMetrics): """ Specific extension for Connexion (https://connexion.readthedocs.io/) that makes sure responses are converted to Flask responses. """ def __init__(self, app, default_mimetype='application/json', **kwargs): flask_app = app.app if app else None if 'response_converter' not in kwargs: kwargs['response_converter'] = self._create_response_converter(default_mimetype) super().__init__(flask_app, **kwargs) @staticmethod def content_type(content_type): """ Force the content type of the response, which would be otherwise overwritten by the metrics conversion to application/json. :param content_type: the value to send in the Content-Type response header """ def decorator(f): @wraps(f) def func(*args, **kwargs): request.prom_connexion_content_type = content_type return f(*args, **kwargs) return func return decorator @staticmethod def _create_response_converter(default_mimetype): def _make_response(response): from connexion.apis.flask_api import FlaskApi mimetype = default_mimetype if hasattr(request, 'prom_connexion_content_type'): mimetype = request.prom_connexion_content_type return FlaskApi.get_response(response, mimetype=mimetype) return _make_response class RESTfulPrometheusMetrics(PrometheusMetrics): """ Specific extension for Flask-RESTful (https://flask-restful.readthedocs.io/) that makes sure API responses are converted to Flask responses. """ def __init__(self, app, api, **kwargs): """ Initializes a new PrometheusMetrics instance that is appropriate for a Flask-RESTful application. :param app: the Flask application :param api: the Flask-RESTful API instance """ if api and 'response_converter' not in kwargs: kwargs['response_converter'] = self._create_response_converter(api) super().__init__(app, **kwargs) @classmethod def for_app_factory(cls, api=None, **kwargs): return cls(app=None, api=api, **kwargs) def init_app(self, app, api=None): if api: self._response_converter = self._create_response_converter(api) return super().init_app(app) @staticmethod def _create_response_converter(api): def _make_response(response): if response is None: response = (None, 200) return api.make_response(*response) return _make_response __version__ = '0.23.1' prometheus_flask_exporter-0.23.1/prometheus_flask_exporter/multiprocess.py000066400000000000000000000203411464362612600275200ustar00rootroot00000000000000import os from abc import ABCMeta, abstractmethod from prometheus_client import CollectorRegistry from prometheus_client import start_http_server as pc_start_http_server from prometheus_client.multiprocess import MultiProcessCollector from prometheus_client.multiprocess import mark_process_dead as pc_mark_process_dead from . import PrometheusMetrics def _check_multiproc_env_var(): """ Checks that the `PROMETHEUS_MULTIPROC_DIR` environment variable is set, which is required for the multiprocess collector to work properly. :raises ValueError: if the environment variable is not set or if it does not point to a directory """ if 'PROMETHEUS_MULTIPROC_DIR' in os.environ: if os.path.isdir(os.environ['PROMETHEUS_MULTIPROC_DIR']): return elif 'prometheus_multiproc_dir' in os.environ: if os.path.isdir(os.environ['prometheus_multiproc_dir']): return raise ValueError('one of env PROMETHEUS_MULTIPROC_DIR or env prometheus_multiproc_dir ' + 'must be set and be a directory') class MultiprocessPrometheusMetrics(PrometheusMetrics): """ An extension of the `PrometheusMetrics` class that provides convenience functions for multiprocess applications. There are ready to use classes for uWSGI and Gunicorn. For everything else, extend this class and override the `should_start_http_server` to only return `True` from one process only - typically the main one. Note: you will need to explicitly call the `start_http_server` function. """ __metaclass__ = ABCMeta def __init__(self, app=None, **kwargs): """ Create a new multiprocess-aware Prometheus metrics export configuration. :param registry: the Prometheus Registry to use (can be `None` and it will be registered with `prometheus_client.multiprocess.MultiProcessCollector`) """ _check_multiproc_env_var() registry = kwargs.pop('registry', CollectorRegistry()) MultiProcessCollector(registry) kwargs.pop('path', None) # remove the path parameter if it was passed in super().__init__( app=app, path=None, registry=registry, **kwargs ) def start_http_server(self, port, host='0.0.0.0', endpoint=None, ssl=None): """ Start an HTTP server for exposing the metrics, if the `should_start_http_server` function says we should, otherwise just return. Uses the implementation from `prometheus_client` rather than a Flask app. :param port: the HTTP port to expose the metrics endpoint on :param host: the HTTP host to listen on (default: `0.0.0.0`) :param endpoint: **ignored**, the HTTP server will respond on any path :param ssl: **ignored**, the server will not support SSL/HTTPS """ if self.should_start_http_server(): pc_start_http_server(port, host, registry=self.registry) @abstractmethod def should_start_http_server(self): """ Whether or not to start the HTTP server. Only return `True` from one process only, typically the main one. Note: you still need to explicitly call the `start_http_server` function. :return: `True` if the server should start, `False` otherwise """ pass class MultiprocessInternalPrometheusMetrics(MultiprocessPrometheusMetrics): """ A multiprocess `PrometheusMetrics` extension with the metrics endpoint registered on the Flask app internally. This variant is expected to expose the metrics endpoint on the same server as the production endpoints are served too. Alternatively, you can use the instance functions as well. """ def __init__(self, app=None, path='/metrics', **kwargs): """ Create a new multiprocess-aware Prometheus metrics export configuration. """ super().__init__(app=app, **kwargs) if app: self.register_endpoint(path) else: self.path = path def should_start_http_server(self): return False @classmethod def start_http_server_when_ready(cls, port, host='0.0.0.0'): import warnings warnings.warn( 'The `MultiprocessInternalPrometheusMetrics` class is expected to expose the metrics endpoint ' 'on the same Flask application, so the `start_http_server_when_ready` should not be called.', UserWarning ) class UWsgiPrometheusMetrics(MultiprocessPrometheusMetrics): """ A multiprocess `PrometheusMetrics` extension targeting uWSGI deployments. This will only start the HTTP server for metrics on the main process, indicated by `uwsgi.masterpid()`. """ def should_start_http_server(self): import uwsgi return os.getpid() == uwsgi.masterpid() class GunicornPrometheusMetrics(MultiprocessPrometheusMetrics): """ A multiprocess `PrometheusMetrics` extension targeting Gunicorn deployments. This variant is expected to serve the metrics endpoint on an individual HTTP server. See `GunicornInternalPrometheusMetrics` for one that serves the metrics endpoint on the same server as the other endpoints. It should have Gunicorn configuration to start the HTTP server like this: from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics def when_ready(server): GunicornPrometheusMetrics.start_http_server_when_ready(metrics_port) def child_exit(server, worker): GunicornPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) Alternatively, you can use the instance functions as well. """ def should_start_http_server(self): return True @classmethod def start_http_server_when_ready(cls, port, host='0.0.0.0'): """ Start the HTTP server from the Gunicorn config module. Doesn't necessarily need an instance, a class is fine. Example: def when_ready(server): GunicornPrometheusMetrics.start_http_server_when_ready(metrics_port) :param port: the HTTP port to expose the metrics endpoint on :param host: the HTTP host to listen on (default: `0.0.0.0`) """ _check_multiproc_env_var() GunicornPrometheusMetrics().start_http_server(port, host) @classmethod def mark_process_dead_on_child_exit(cls, pid): """ Mark a child worker as exited from the Gunicorn config module. Example: def child_exit(server, worker): GunicornPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) :param pid: the worker pid that has exited """ pc_mark_process_dead(pid) class GunicornInternalPrometheusMetrics(GunicornPrometheusMetrics): """ A multiprocess `PrometheusMetrics` extension targeting Gunicorn deployments. This variant is expected to expose the metrics endpoint on the same server as the production endpoints are served too. See also the `GunicornPrometheusMetrics` class that will start a new HTTP server for the metrics endpoint. It should have Gunicorn configuration to start the HTTP server like this: from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics def child_exit(server, worker): GunicornInternalPrometheusMetrics.mark_process_dead_on_child_exit(worker.pid) Alternatively, you can use the instance functions as well. """ def __init__(self, app=None, path='/metrics', **kwargs): """ Create a new multiprocess-aware Prometheus metrics export configuration. """ super().__init__(app=app, **kwargs) if app: self.register_endpoint(path) else: self.path = path def should_start_http_server(self): return False @classmethod def start_http_server_when_ready(cls, port, host='0.0.0.0'): import warnings warnings.warn( 'The `GunicornInternalPrometheusMetrics` class is expected to expose the metrics endpoint ' 'on the same Flask application, so the `start_http_server_when_ready` should not be called. ' 'Maybe you are looking for the `GunicornPrometheusMetrics` class?', UserWarning ) prometheus_flask_exporter-0.23.1/requirements.txt000066400000000000000000000002671464362612600224030ustar00rootroot00000000000000flask prometheus_client werkzeug>=3.0.1 # not directly required, pinned by Snyk to avoid a vulnerability zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability prometheus_flask_exporter-0.23.1/setup.py000066400000000000000000000023721464362612600206300ustar00rootroot00000000000000from pathlib import Path from setuptools import setup long_description = Path('README.md').read_text() setup( name='prometheus_flask_exporter', packages=['prometheus_flask_exporter'], version='0.23.1', description='Prometheus metrics exporter for Flask', long_description=long_description, long_description_content_type='text/markdown', license='MIT', author='Viktor Adam', author_email='rycus86@gmail.com', url='https://github.com/rycus86/prometheus_flask_exporter', download_url='https://github.com/rycus86/prometheus_flask_exporter/archive/0.23.1.tar.gz', keywords=['prometheus', 'flask', 'monitoring', 'exporter'], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Topic :: System :: Monitoring', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', ], install_requires=['prometheus_client', 'flask'], ) prometheus_flask_exporter-0.23.1/tests/000077500000000000000000000000001464362612600202545ustar00rootroot00000000000000prometheus_flask_exporter-0.23.1/tests/test_app_factory.py000066400000000000000000000032571464362612600242030ustar00rootroot00000000000000import sys import unittest from flask import Flask from prometheus_client import CollectorRegistry from prometheus_flask_exporter import PrometheusMetrics class AppFactoryTest(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.testing = True self.client = self.app.test_client() if sys.version_info.major < 3: self.assertRegex = self.assertRegexpMatches self.assertNotRegex = self.assertNotRegexpMatches registry = CollectorRegistry(auto_describe=True) self.metrics = PrometheusMetrics.for_app_factory(registry=registry) self.metrics.init_app(self.app) def test_restricted(self): @self.app.route('/test') def test(): return 'OK' self.client.get('/test') response = self.client.get('/metrics') self.assertEqual(response.status_code, 200) response = self.client.get('/metrics?name[]=flask_exporter_info') self.assertEqual(response.status_code, 200) response = self.client.get('/metrics' '?name[]=flask_http_request_duration_seconds_bucket' '&name[]=flask_http_request_duration_seconds_count' '&name[]=flask_http_request_duration_seconds_sum') self.assertEqual(response.status_code, 200) self.assertIn('flask_http_request_duration_seconds_bucket{le="0.1",method="GET",path="/test",status="200"} 1.0', str(response.data)) self.assertIn('flask_http_request_duration_seconds_count{method="GET",path="/test",status="200"} 1.0', str(response.data)) prometheus_flask_exporter-0.23.1/tests/test_blueprint.py000066400000000000000000000102311464362612600236660ustar00rootroot00000000000000import sys import unittest from flask import Flask, Blueprint, request from prometheus_client import CollectorRegistry from prometheus_flask_exporter import PrometheusMetrics, RESTfulPrometheusMetrics class BlueprintTest(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.testing = True self.client = self.app.test_client() if sys.version_info.major < 3: self.assertRegex = self.assertRegexpMatches self.assertNotRegex = self.assertNotRegexpMatches registry = CollectorRegistry(auto_describe=True) self.metrics = PrometheusMetrics.for_app_factory(registry=registry) def test_blueprint(self): blueprint = Blueprint('test-blueprint', __name__) @blueprint.route('/test') @self.metrics.summary('requests_by_status', 'Request latencies by status', labels={'status': lambda r: r.status_code}) def test(): return 'OK' self.app.register_blueprint(blueprint) self.metrics.init_app(self.app) self.client.get('/test') response = self.client.get('/metrics') self.assertEqual(response.status_code, 200) self.assertIn('requests_by_status_count{status="200"} 1.0', str(response.data)) self.assertRegex(str(response.data), 'requests_by_status_sum{status="200"} [0-9.]+') def test_restful_with_blueprints(self): try: from flask_restful import Resource, Api except ImportError: self.skipTest('Flask-RESTful is not available') return class SampleResource(Resource): status = 200 @self.metrics.summary('requests_by_status', 'Request latencies by status', labels={'status': lambda r: r.status_code}) def get(self): if 'fail' in request.args: return 'Not OK', 400 else: return 'OK' blueprint = Blueprint('v1', __name__, url_prefix='/v1') api = Api(blueprint) api.add_resource(SampleResource, '/sample', endpoint='api_sample') self.app.register_blueprint(blueprint) self.metrics.init_app(self.app) self.client.get('/v1/sample') self.client.get('/v1/sample') self.client.get('/v1/sample?fail=1') response = self.client.get('/metrics') self.assertEqual(response.status_code, 200) self.assertIn('requests_by_status_count{status="200"} 2.0', str(response.data)) self.assertRegex(str(response.data), 'requests_by_status_sum{status="200"} [0-9.]+') self.assertIn('requests_by_status_count{status="400"} 1.0', str(response.data)) self.assertRegex(str(response.data), 'requests_by_status_sum{status="400"} [0-9.]+') def test_restful_return_none(self): try: from flask_restful import Resource, Api except ImportError: self.skipTest('Flask-RESTful is not available') return api = Api(self.app) self.metrics = RESTfulPrometheusMetrics(self.app, api=api) class SampleResource(Resource): status = 200 @self.metrics.summary('requests_by_status', 'Request latencies by status', labels={'status': lambda r: r.status_code}) def get(self): if 'fail' in request.args: return None, 400, {'X-Error': 'Test error'} else: return None api.add_resource(SampleResource, '/v1/sample', endpoint='api_sample') self.client.get('/v1/sample') self.client.get('/v1/sample') self.client.get('/v1/sample?fail=1') response = self.client.get('/metrics') self.assertEqual(response.status_code, 200) self.assertIn('requests_by_status_count{status="200"} 2.0', str(response.data)) self.assertRegex(str(response.data), 'requests_by_status_sum{status="200"} [0-9.]+') self.assertIn('requests_by_status_count{status="400"} 1.0', str(response.data)) self.assertRegex(str(response.data), 'requests_by_status_sum{status="400"} [0-9.]+') prometheus_flask_exporter-0.23.1/tests/test_defaults.py000066400000000000000000000567611464362612600235130ustar00rootroot00000000000000from unittest_helper import BaseTestCase from prometheus_flask_exporter import NO_PREFIX from flask import request, make_response from werkzeug.exceptions import Conflict class DefaultsTest(BaseTestCase): def test_simple(self): metrics = self.metrics() @self.app.route('/test') def test(): return 'OK' self.assertMetric( 'flask_exporter_info', '1.0', ('version', metrics.version) ) self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_total', '2.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('path', '/metrics'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '0.1'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '0.5'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '1.0'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '5.0'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '+Inf'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('path', '/skip/defaults'), ('status', 200) ) self.client.get('/test') self.assertMetric( 'flask_http_request_total', '3.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '3.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_response_object(self): metrics = self.metrics() @metrics.counter('success_invocation', 'Successful invocation') def success(): return 200 @self.app.route('/test') def test(): return make_response('OK', success()) self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_total', '2.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric('success_invocation_total', '2.0') def test_skip(self): metrics = self.metrics() @self.app.route('/skip') @metrics.do_not_track() def test(): return 'OK' self.client.get('/skip') self.client.get('/skip') self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('path', '/skip'), ('status', 200) ) def test_exception_counter_metric(self): self.metrics() @self.app.route('/error') def test_error(): raise AttributeError @self.app.route('/abort') def test_abort(): return Conflict() try: self.client.get('/error') except AttributeError: pass self.assertMetric( 'flask_http_request_exceptions_total', '1.0', ('method', 'GET'), ('status', 500) ) for _ in range(3): try: self.client.get('/error') except AttributeError: pass self.assertMetric( 'flask_http_request_exceptions_total', '4.0', ('method', 'GET'), ('status', 500) ) for _ in range(5): self.client.get('/abort') self.assertMetric( 'flask_http_request_exceptions_total', '4.0', ('method', 'GET'), ('status', 500) ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('status', 409) ) self.assertMetric( 'flask_http_request_total', '5.0', ('method', 'GET'), ('status', 409) ) def test_do_not_track_only_excludes_defaults(self): metrics = self.metrics() @self.app.route('/skip/defaults') @metrics.counter('cnt_before', 'Counter before') @metrics.do_not_track() @metrics.counter('cnt_after', 'Counter after') def test(): return 'OK' self.client.get('/skip/defaults') self.client.get('/skip/defaults') self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('path', '/skip/defaults'), ('status', 200) ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('path', '/skip/defaults'), ) self.assertMetric('cnt_before_total', 2.0) self.assertMetric('cnt_after_total', 2.0) def test_exclude_all_wrapping(self): metrics = self.metrics() @self.app.route('/skip') @metrics.gauge('gauge_before', 'Gauge before') @metrics.counter('cnt_before', 'Counter before') @metrics.exclude_all_metrics() @metrics.counter('cnt_after', 'Counter after') @metrics.gauge('gauge_after', 'Gauge after') def test(): return 'OK' self.client.get('/skip') self.client.get('/skip') self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertMetric('cnt_before_total', 0.0) self.assertMetric('cnt_after_total', 0.0) self.assertMetric('gauge_before', 0.0) self.assertMetric('gauge_after', 0.0) def test_custom_path(self): self.metrics(path='/my-metrics') @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.assertMetric( 'flask_http_request_total', '1.0', ('method', 'GET'), ('status', 200), endpoint='/my-metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '1.0', ('method', 'GET'), ('path', '/test'), ('status', 200), endpoint='/my-metrics' ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('path', '/test'), endpoint='/my-metrics' ) def test_custom_metrics_decorator(self): invocations = list() def decorate_metrics(f): def decorated(*args): invocations.append('metrics') return f(*args) return decorated self.metrics(metrics_decorator=decorate_metrics) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.assertEqual(len(invocations), 0) self.assertMetric( 'flask_http_request_total', '1.0', ('method', 'GET'), ('status', 200) ) self.assertEqual(len(invocations), 1) def test_no_default_export(self): self.metrics(export_defaults=False) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200), endpoint='/metrics' ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('path', '/test'), ('status', 200), endpoint='/metrics' ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('path', '/test'), ('status', 200), endpoint='/metrics' ) def test_custom_defaults_prefix(self): self.assumeBeforeFlaskVersion('2.2.0') metrics = self.metrics(defaults_prefix='www') self.assertAbsent( 'flask_exporter_info', ('version', metrics.version) ) self.assertMetric( 'www_exporter_info', '1.0', ('version', metrics.version) ) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'www_http_request_total', '2.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'www_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_custom_defaults_prefix__F220(self): self.assumeMinimumFlaskVersion('2.2.0') metrics = self.metrics(defaults_prefix='www') @self.app.route('/test') def test(): return 'OK' self.assertAbsent( 'flask_exporter_info', ('version', metrics.version) ) self.assertMetric( 'www_exporter_info', '1.0', ('version', metrics.version) ) self.client.get('/test') self.client.get('/test') self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'www_http_request_total', '2.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'www_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_no_defaults_prefix(self): metrics = self.metrics(defaults_prefix=NO_PREFIX) @self.app.route('/test') def test(): return 'OK' self.assertAbsent( 'flask_exporter_info', ('version', metrics.version) ) self.assertMetric( 'exporter_info', '1.0', ('version', metrics.version) ) self.client.get('/test') self.client.get('/test') self.client.get('/test') self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'http_request_total', '3.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'http_request_duration_seconds_count', '3.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_late_defaults_export(self): self.assumeBeforeFlaskVersion('2.2.0') metrics = self.metrics(export_defaults=False) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertAbsent( 'flask_exporter_info', ('version', metrics.version) ) self.assertAbsent( 'late_exporter_info', ('version', metrics.version) ) self.assertAbsent( 'flask_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertAbsent( 'late_http_request_total', ('method', 'GET'), ('status', 200) ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('path', '/test'), ) metrics.export_defaults(prefix='late') self.assertMetric( 'late_exporter_info', '1.0', ('version', metrics.version) ) self.client.get('/test') self.client.get('/test') self.client.get('/test') self.assertMetric( 'late_http_request_total', '3.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'late_http_request_duration_seconds_count', '3.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('path', '/test'), ) def test_late_defaults_export__F220(self): self.assumeMinimumFlaskVersion('2.2.0') metrics = self.metrics(export_defaults=False) @self.app.route('/test') def test(): return 'OK' metrics.export_defaults(prefix='late') self.assertMetric( 'late_exporter_info', '1.0', ('version', metrics.version) ) self.client.get('/test') self.client.get('/test') self.client.get('/test') self.assertMetric( 'late_http_request_total', '3.0', ('method', 'GET'), ('status', 200) ) self.assertMetric( 'late_http_request_duration_seconds_count', '3.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_exceptions_total', ('method', 'GET'), ('path', '/test'), ) def test_export_latencies_as_summary(self): self.assumeBeforeFlaskVersion('2.2.0') metrics = self.metrics(export_defaults=False) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertAbsent( 'flask_http_request_duration_seconds_sum', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('le', '+Inf'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) metrics.export_defaults(latency_as_histogram=False) self.client.get('/test') self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_duration_seconds_sum', '[0-9.e-]+', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '3.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('le', '+Inf'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_export_latencies_as_summary__F220(self): self.assumeMinimumFlaskVersion('2.2.0') metrics = self.metrics(export_defaults=False) @self.app.route('/test') def test(): return 'OK' metrics.export_defaults(latency_as_histogram=False) self.client.get('/test') self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_duration_seconds_sum', '[0-9.e-]+', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '3.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('le', '+Inf'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_non_automatic_endpoint_registration(self): metrics = self.metrics(path=None) @self.app.route('/test') def test(): return 'OK' metrics.register_endpoint('/manual/metrics') self.client.get('/test') no_metrics_response = self.client.get('/metrics') self.assertEqual(no_metrics_response.status_code, 404) self.client.get('/test') self.assertMetric( 'flask_http_request_total', '2.0', ('method', 'GET'), ('status', 200), endpoint='/manual/metrics' ) def test_latencies_as_summary(self): self.metrics(default_latency_as_histogram=False) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_duration_seconds_sum', '[0-9.e-]+', ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_custom_buckets(self): self.metrics(buckets=(0.2, 2, 4)) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '0.2'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '2.0'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '4.0'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('le', '+Inf'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('le', '0.1'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('le', '0.3'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('le', '1.2'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('le', '5.0'), ('method', 'GET'), ('path', '/test'), ('status', 200) ) def test_invalid_labels(self): metrics = self.metrics() self.assertRaises( TypeError, metrics.counter, 'invalid_counter', 'Counter with invalid labels', labels=('name', 'value') ) def test_info(self): metrics = self.metrics() metrics.info('info', 'Info', x=1, y=2) self.assertMetric('info', '1.0', ('x', 1), ('y', 2)) sample = metrics.info( 'sample', 'Sample', ('key',), ('value',) ) sample.set(5) self.assertMetric('sample', '5.0', ('key', 'value')) self.assertRaises( ValueError, metrics.info, 'invalid', 'Invalid', ('both', 'names'), ('and', 'values'), are='defined' ) metrics.info('no_labels', 'Without labels') self.assertMetric('no_labels', '1.0') def test_static_labels(self): metrics = self.metrics(static_labels={ 'app_name': 'Test-App', 'api_version': 1 }) @self.app.route('/test') @metrics.counter('test_counter', 'Test Counter', labels={'code': lambda r: r.status_code}) def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_total', '2.0', ('method', 'GET'), ('status', 200), ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200), ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'test_counter_total', '2.0', ('code', 200), ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'test_counter_created', '.', ('code', 200), ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'flask_exporter_info', '', ('version', metrics.version) # no default labels here ) def test_static_labels_without_metric_labels(self): metrics = self.metrics(static_labels={ 'app_name': 'Test-App', 'api_version': 1 }) @self.app.route('/test') @metrics.counter('test_counter', 'Test Counter') def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_total', '2.0', ('method', 'GET'), ('status', 200), ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200), ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'test_counter_total', '2.0', ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'test_counter_created', '.', ('app_name', 'Test-App'), ('api_version', 1) ) self.assertMetric( 'flask_exporter_info', '', ('version', metrics.version) # no default labels here ) def test_default_labels(self): metrics = self.metrics( static_labels={ 'static': 'testing' }, default_labels={ 'dm': lambda: request.method }) @self.app.route('/test') @metrics.counter('test_counter', 'Test Counter', labels={'code': lambda r: r.status_code}) def test(): return 'OK' self.client.get('/test') self.client.get('/test') self.assertMetric( 'flask_http_request_total', '2.0', ('method', 'GET'), ('status', 200), ('static', 'testing'), ('dm', 'GET') ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/test'), ('status', 200), ('static', 'testing'), ('dm', 'GET') ) self.assertMetric( 'test_counter_total', '2.0', ('code', 200), ('static', 'testing'), ('dm', 'GET') ) self.assertMetric( 'test_counter_created', '.', ('code', 200), ('static', 'testing'), ('dm', 'GET') ) self.assertMetric( 'flask_exporter_info', '', ('version', metrics.version) # no default labels here ) prometheus_flask_exporter-0.23.1/tests/test_endpoint.py000066400000000000000000000370321464362612600235120ustar00rootroot00000000000000import time import werkzeug.exceptions from flask import request, abort from unittest_helper import BaseTestCase try: from urllib2 import urlopen except ImportError: # Python 3 from urllib.request import urlopen class EndpointTest(BaseTestCase): def test_restricted(self): self.metrics() @self.app.route('/test') def test(): return 'OK' self.client.get('/test') response = self.client.get('/metrics') self.assertIn('flask_exporter_info', str(response.data)) self.assertIn('flask_http_request_total', str(response.data)) self.assertIn('flask_http_request_duration_seconds', str(response.data)) response = self.client.get('/metrics?name[]=flask_exporter_info') self.assertIn('flask_exporter_info', str(response.data)) self.assertNotIn('flask_http_request_total', str(response.data)) self.assertNotIn('flask_http_request_duration_seconds', str(response.data)) response = self.client.get('/metrics' '?name[]=flask_http_request_duration_seconds_bucket' '&name[]=flask_http_request_duration_seconds_count' '&name[]=flask_http_request_duration_seconds_sum') self.assertNotIn('flask_exporter_info', str(response.data)) self.assertNotIn('flask_http_request_total', str(response.data)) self.assertIn('flask_http_request_duration_seconds_bucket', str(response.data)) self.assertIn('flask_http_request_duration_seconds_count', str(response.data)) self.assertIn('flask_http_request_duration_seconds_sum', str(response.data)) def test_generate_metrics_content(self): metrics = self.metrics(path=None) @self.app.route('/test') def test(): return 'OK' self.client.get('/test') response = self.client.get('/metrics') self.assertEqual(404, response.status_code) response_data, _ = metrics.generate_metrics() self.assertIn('flask_exporter_info', response_data) self.assertIn('flask_http_request_total', response_data) self.assertIn('flask_http_request_duration_seconds', response_data) response_data, _ = metrics.generate_metrics(names=['flask_exporter_info']) self.assertIn('flask_exporter_info', response_data) self.assertNotIn('flask_http_request_total', response_data) self.assertNotIn('flask_http_request_duration_seconds', response_data) response_data, _ = metrics.generate_metrics(names=[ 'flask_http_request_duration_seconds_bucket', 'flask_http_request_duration_seconds_count', 'flask_http_request_duration_seconds_sum' ]) self.assertNotIn('flask_exporter_info', response_data) self.assertNotIn('flask_http_request_total', response_data) self.assertIn('flask_http_request_duration_seconds_bucket', response_data) self.assertIn('flask_http_request_duration_seconds_count', response_data) self.assertIn('flask_http_request_duration_seconds_sum', response_data) def test_http_server(self): metrics = self.metrics() metrics.start_http_server(32001) metrics.start_http_server(32002, endpoint='/test/metrics') metrics.start_http_server(32003, host='127.0.0.1') def wait_for_startup(): for _ in range(10): try: urlopen('http://localhost:32001/metrics') urlopen('http://localhost:32002/test/metrics') urlopen('http://localhost:32003/metrics') break except: time.sleep(0.5) wait_for_startup() response = urlopen('http://localhost:32001/metrics') self.assertEqual(response.getcode(), 200) self.assertIn('flask_exporter_info', str(response.read())) response = urlopen('http://localhost:32002/test/metrics') self.assertEqual(response.getcode(), 200) self.assertIn('flask_exporter_info', str(response.read())) response = urlopen('http://localhost:32003/metrics') self.assertEqual(response.getcode(), 200) self.assertIn('flask_exporter_info', str(response.read())) def test_http_status_enum(self): try: from http import HTTPStatus except ImportError: self.skipTest('http.HTTPStatus is not available') metrics = self.metrics() @self.app.route('/no/content') def no_content(): import http return {}, http.HTTPStatus.NO_CONTENT self.client.get('/no/content') self.client.get('/no/content') self.assertMetric( 'flask_http_request_total', '2.0', ('method', 'GET'), ('status', 204) ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('method', 'GET'), ('path', '/no/content'), ('status', 204) ) def test_abort(self): metrics = self.metrics() @self.app.route('/error') @metrics.summary('http_index_requests_by_status', 'Request latencies by status', labels={'status': lambda r: r.status_code}) @metrics.histogram('http_index_requests_by_status_and_path', 'Index requests latencies by status and path', labels={ 'status': lambda r: r.status_code, 'path': lambda: request.path }) def throw_error(): return abort(503) self.client.get('/error') self.assertMetric('http_index_requests_by_status_count', 1.0, ('status', 503)) self.assertMetric('http_index_requests_by_status_sum', '.', ('status', 503)) self.assertMetric( 'http_index_requests_by_status_and_path_count', 1.0, ('status', 503), ('path', '/error') ) self.assertMetric( 'http_index_requests_by_status_and_path_sum', '.', ('status', 503), ('path', '/error') ) self.assertMetric( 'http_index_requests_by_status_and_path_bucket', 1.0, ('status', 503), ('path', '/error'), ('le', 0.5) ) self.assertMetric( 'http_index_requests_by_status_and_path_bucket', 1.0, ('status', 503), ('path', '/error'), ('le', 10.0) ) def test_exception(self): metrics = self.metrics() @self.app.route('/exception') @metrics.summary('http_with_exception', 'Tracks the method raising an exception', labels={'status': lambda r: r.status_code}) def raise_exception(): raise NotImplementedError('On purpose') try: self.client.get('/exception') except NotImplementedError: pass self.assertMetric('http_with_exception_count', 1.0, ('status', 500)) self.assertMetric('http_with_exception_sum', '.', ('status', 500)) def test_abort_before(self): @self.app.before_request def before_request(): if request.path == '/metrics': return raise abort(400) self.metrics() self.client.get('/abort/before') self.client.get('/abort/before') self.assertMetric( 'flask_http_request_total', 2.0, ('method', 'GET'), ('status', 400) ) def test_error_handler(self): metrics = self.metrics() @self.app.errorhandler(NotImplementedError) def not_implemented_handler(e): return 'Not implemented', 400 @self.app.errorhandler(werkzeug.exceptions.Conflict) def handle_conflict(e): return 'Bad request for conflict', 400, {'X-Original': e.code} @self.app.route('/exception') @metrics.summary('http_with_exception', 'Tracks the method raising an exception', labels={'status': lambda r: r.status_code}) def raise_exception(): raise NotImplementedError('On purpose') @self.app.route('/conflict') @metrics.summary('http_with_code', 'Tracks the error with the original code', labels={'status': lambda r: r.status_code, 'code': lambda r: r.headers.get('X-Original', -1)}) def conflicts(): abort(409) for _ in range(3): self.client.get('/exception') for _ in range(7): self.client.get('/conflict') self.assertMetric('http_with_exception_count', 3.0, ('status', 400)) self.assertMetric('http_with_exception_sum', '.', ('status', 400)) self.assertMetric('http_with_code_count', 7.0, ('status', 400), ('code', 409)) self.assertMetric('http_with_code_sum', '.', ('status', 400), ('code', 409)) def test_error_no_handler(self): self.metrics() @self.app.route('/exception') def raise_exception(): raise NotImplementedError('On purpose') for _ in range(5): try: self.client.get('/exception') except NotImplementedError: pass self.assertMetric('flask_http_request_total', 5.0, ('method', 'GET'), ('status', 500)) self.assertMetric( 'flask_http_request_duration_seconds_count', 5.0, ('method', 'GET'), ('status', 500), ('path', '/exception') ) def test_named_endpoint(self): metrics = self.metrics() @self.app.route('/testing', endpoint='testing_endpoint') @metrics.summary('requests_by_status', 'Request latencies by status', labels={'status': lambda r: r.status_code}) def testing(): return 'OK' for _ in range(5): self.client.get('/testing') self.assertMetric('requests_by_status_count', 5.0, ('status', 200)) self.assertMetric('requests_by_status_sum', '.', ('status', 200)) def test_track_multiple_endpoints(self): metrics = self.metrics() test_request_counter = metrics.counter( 'test_counter', 'Request counter for tests', labels={'path': lambda: request.path} ) @self.app.route('/first') @test_request_counter def first(): return 'OK' @self.app.route('/second') @test_request_counter def second(): return 'OK' for _ in range(5): self.client.get('/first') self.client.get('/second') self.assertMetric( 'flask_http_request_total', 10.0, ('method', 'GET'), ('status', 200) ) self.assertMetric( 'test_counter_total', 5.0, ('path', '/first') ) self.assertMetric( 'test_counter_total', 5.0, ('path', '/second') ) def test_track_more_defaults(self): metrics = self.metrics() @self.app.route('/first') def first(): return 'OK' @self.app.route('/second') def second(): return 'OK' metrics.register_default( metrics.counter( 'test_counter', 'Request counter for tests', labels={'path': lambda: request.path} ), metrics.summary( 'test_summary', 'Request summary for tests', labels={'path': lambda: request.path} ) ) for _ in range(5): self.client.get('/first') self.client.get('/second') self.assertMetric( 'flask_http_request_total', 10.0, ('method', 'GET'), ('status', 200) ) self.assertMetric( 'test_counter_total', 5.0, ('path', '/first') ) self.assertMetric( 'test_counter_total', 5.0, ('path', '/second') ) # issue #157 response = self.client.get('/metrics').text self.assertNotIn('', response) def test_excluded_endpoints(self): self.metrics(excluded_paths='/exc') class RequestCounter: value = 0 @self.app.route('/included') def included(): RequestCounter.value += 1 return 'OK' @self.app.route('/excluded') def excluded(): RequestCounter.value += 1 return 'OK' for _ in range(5): self.client.get('/included') self.client.get('/excluded') self.assertEqual(10, RequestCounter.value) self.assertMetric( 'flask_http_request_total', 5.0, ('method', 'GET'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', 5.0, ('method', 'GET'), ('status', 200), ('path', '/included') ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('status', 200), ('path', '/excluded') ) def test_multiple_excluded_endpoints(self): self.metrics(excluded_paths=[ '/exc/one', '/exc/t.*' ]) class RequestCounter: value = 0 @self.app.route('/included') def included(): RequestCounter.value += 1 return 'OK' @self.app.route('/exc/one') def excluded_one(): RequestCounter.value += 1 return 'OK' @self.app.route('/exc/two') def excluded_two(): RequestCounter.value += 1 return 'OK' for _ in range(5): self.client.get('/included') self.client.get('/exc/one') self.client.get('/exc/two') self.assertEqual(15, RequestCounter.value) self.assertMetric( 'flask_http_request_total', 5.0, ('method', 'GET'), ('status', 200) ) self.assertMetric( 'flask_http_request_duration_seconds_count', 5.0, ('method', 'GET'), ('status', 200), ('path', '/included') ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('status', 200), ('path', '/exc/one') ) self.assertAbsent( 'flask_http_request_duration_seconds_count', ('method', 'GET'), ('status', 200), ('path', '/exc/two') ) def test_exclude_paths_from_user_metrics(self): metrics = self.metrics(excluded_paths='/excluded', exclude_user_defaults=True) @self.app.route('/included') def included(): return 'OK' @self.app.route('/excluded') def excluded(): return 'OK' metrics.register_default( metrics.counter( name='by_path_counter', description='Request count by path', labels={'path': lambda: request.path} ) ) for _ in range(5): self.client.get('/included') self.client.get('/excluded') self.assertMetric( 'flask_http_request_total', 5.0, ('method', 'GET'), ('status', 200) ) self.assertMetric( 'by_path_counter_total', 5.0, ('path', '/included') ) self.assertAbsent( 'by_path_counter_total', ('path', '/excluded') ) # issue #157 response = self.client.get('/metrics').text self.assertNotIn('', response) prometheus_flask_exporter-0.23.1/tests/test_extensions.py000066400000000000000000000153131464362612600240670ustar00rootroot00000000000000import os import sys from flask import Flask from prometheus_flask_exporter import ConnexionPrometheusMetrics from prometheus_flask_exporter import PrometheusMetrics from prometheus_flask_exporter import RESTfulPrometheusMetrics from prometheus_flask_exporter.multiprocess import GunicornInternalPrometheusMetrics from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics from prometheus_flask_exporter.multiprocess import MultiprocessPrometheusMetrics from prometheus_flask_exporter.multiprocess import UWsgiPrometheusMetrics from unittest_helper import BaseTestCase class ExtensionsTest(BaseTestCase): def setUp(self): super().setUp() if 'PROMETHEUS_MULTIPROC_DIR' not in os.environ: os.environ['PROMETHEUS_MULTIPROC_DIR'] = '/tmp' self._multiproc_dir_added = True elif 'prometheus_multiproc_dir' not in os.environ: os.environ['prometheus_multiproc_dir'] = '/tmp' self._multiproc_dir_added = True else: self._multiproc_dir_added = False if sys.version_info.major < 3: # some integrations don't work on Python 2 anymore: # - ConnexionPrometheusMetrics # - MultiprocessPrometheusMetrics self._all_extensions = [ PrometheusMetrics, RESTfulPrometheusMetrics, UWsgiPrometheusMetrics, GunicornPrometheusMetrics, GunicornInternalPrometheusMetrics ] else: self._all_extensions = [ PrometheusMetrics, RESTfulPrometheusMetrics, ConnexionPrometheusMetrics, MultiprocessPrometheusMetrics, UWsgiPrometheusMetrics, GunicornPrometheusMetrics, GunicornInternalPrometheusMetrics ] def tearDown(self): if self._multiproc_dir_added: if os.environ.get('PROMETHEUS_MULTIPROC_DIR'): del os.environ['PROMETHEUS_MULTIPROC_DIR'] if os.environ.get('prometheus_multiproc_dir'): del os.environ['prometheus_multiproc_dir'] def test_with_defaults(self): for extension_type in self._all_extensions: try: app = Flask(__name__) app.testing = True flask_app = app kwargs = {} if extension_type is ConnexionPrometheusMetrics: class WrappedApp: def __init__(self, app): self.app = app # ConnexionPrometheusMetrics wraps this in its own type of app app = WrappedApp(app) elif extension_type is RESTfulPrometheusMetrics: # RESTfulPrometheusMetrics has one additional positional argument kwargs = {'api': None} obj = extension_type(app=app, **kwargs) except Exception as ex: self.fail('Failed to instantiate %s: %s' % (extension_type.__name__, ex)) self.assertIs(obj.app, flask_app, 'Unexpected app object in %s' % extension_type.__name__) def test_with_registry(self): for extension_type in self._all_extensions: class MockRegistry: def register(self, *arg, **kwargs): pass registry = MockRegistry() # RESTfulPrometheusMetrics has one additional positional argument kwargs = {'api': None} if extension_type is RESTfulPrometheusMetrics else {} kwargs['registry'] = registry try: obj = extension_type(app=None, **kwargs) except Exception as ex: self.fail('Failed to instantiate %s: %s' % (extension_type.__name__, ex)) self.assertIs(obj.registry, registry, 'Unexpected registry object in %s' % extension_type.__name__) def test_with_other_parameters(self): for extension_type in self._all_extensions: # RESTfulPrometheusMetrics has one additional positional argument kwargs = {'api': None} if extension_type is RESTfulPrometheusMetrics else {} kwargs['path'] = '/testing' kwargs['export_defaults'] = False kwargs['defaults_prefix'] = 'unittest' kwargs['default_labels'] = {'testing': 1} try: obj = extension_type(app=None, **kwargs) except Exception as ex: self.fail('Failed to instantiate %s: %s' % (extension_type.__name__, ex)) for arg, value in kwargs.items(): if arg == 'api': continue # skip this argument for RESTfulPrometheusMetrics if arg == 'path': continue # path is set to None in many multiprocess implementations if hasattr(obj, '_' + arg): self.assertIs(getattr(obj, '_' + arg), value, 'Unexpected %s object in %s' % (arg, extension_type.__name__)) else: self.assertIs(getattr(obj, arg), value, 'Unexpected %s object in %s' % (arg, extension_type.__name__)) def test_prometheus_multiproc_env_var_change(self): for extension_type in self._all_extensions: if extension_type in [ MultiprocessPrometheusMetrics, UWsgiPrometheusMetrics, GunicornPrometheusMetrics, GunicornInternalPrometheusMetrics ]: # Check only lower case env var works if os.environ.get('PROMETHEUS_MULTIPROC_DIR'): del os.environ['PROMETHEUS_MULTIPROC_DIR'] os.environ['prometheus_multiproc_dir'] = '/tmp' try: app = Flask(__name__) app.testing = True flask_app = app obj = extension_type(app=app) except Exception as ex: self.fail('Failed to instantiate %s: %s' % (extension_type.__name__, ex)) self.assertIs(obj.app, flask_app, 'Unexpected app object in %s' % extension_type.__name__) # Check only upper case env var works if os.environ.get('prometheus_multiproc_dir'): del os.environ['prometheus_multiproc_dir'] os.environ['PROMETHEUS_MULTIPROC_DIR'] = '/tmp' try: app2 = Flask(__name__) app2.testing = True flask_app2 = app2 obj2 = extension_type(app=app2) except Exception as ex: self.fail('Failed to instantiate %s: %s' % (extension_type.__name__, ex)) self.assertIs(obj2.app, flask_app2, 'Unexpected app object in %s' % extension_type.__name__) prometheus_flask_exporter-0.23.1/tests/test_group_by.py000066400000000000000000000215031464362612600235140ustar00rootroot00000000000000import warnings from unittest_helper import BaseTestCase class GroupByTest(BaseTestCase): def test_group_by_path_default(self): self.metrics() @self.app.route('/') def a_test_endpoint(url): return url + ' is OK' self.client.get('/default1') self.client.get('/default2') self.client.get('/default3') for path in ('/default1', '/default2', '/default3'): self.assertMetric( 'flask_http_request_duration_seconds_bucket', '1.0', ('path', path), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '1.0', ('path', path), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) def test_group_by_path_default_with_summaries(self): self.metrics(default_latency_as_histogram=False) @self.app.route('/') def a_test_endpoint(url): return url + ' is OK' self.client.get('/default1') self.client.get('/default2') self.client.get('/default3') for path in ('/default1', '/default2', '/default3'): self.assertMetric( 'flask_http_request_duration_seconds_sum', '[0-9.e-]+', ('path', path), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '1.0', ('path', path), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) def test_group_by_path(self): self.metrics(group_by='path') @self.app.route('/') def a_test_endpoint(url): return url + ' is OK' self.client.get('/test1') self.client.get('/test2') self.client.get('/test3') for path in ('/test1', '/test2', '/test3'): self.assertMetric( 'flask_http_request_duration_seconds_bucket', '1.0', ('path', path), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '1.0', ('path', path), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) def test_group_by_rule(self): self.metrics(group_by='url_rule') @self.app.route('/test/') def first_test_endpoint(item): return item + ' is OK' @self.app.route('/get/') def second_test_endpoint(sample): return sample + ' is OK' self.client.get('/test/1') self.client.get('/test/2') self.client.get('/get/1') self.assertMetric( 'flask_http_request_duration_seconds_bucket', '2.0', ('url_rule', '/test/'), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '2.0', ('url_rule', '/test/'), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_bucket', '1.0', ('url_rule', '/get/'), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '1.0', ('url_rule', '/get/'), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) def test_group_by_endpoint(self): self.metrics(group_by='endpoint') @self.app.route('/') def a_test_endpoint(url): return url + ' is OK' self.client.get('/test') self.client.get('/test2') self.client.get('/test3') self.assertMetric( 'flask_http_request_duration_seconds_bucket', '3.0', ('endpoint', 'a_test_endpoint'), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '3.0', ('endpoint', 'a_test_endpoint'), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('path', '/test'), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) def test_group_by_endpoint_deprecated(self): warnings.filterwarnings('once', category=DeprecationWarning) with warnings.catch_warnings(record=True) as w: self.metrics(group_by_endpoint=True) # make sure we have the deprecation warning for this self.assertEqual(len(w), 1) self.assertEqual(w[0].category, DeprecationWarning) self.assertIn('group_by_endpoint', str(w[0].message)) @self.app.route('/') def a_legacy_endpoint(url): return url + ' is OK' self.client.get('/test') self.client.get('/test2') self.client.get('/test3') self.assertMetric( 'flask_http_request_duration_seconds_bucket', '3.0', ('endpoint', 'a_legacy_endpoint'), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '3.0', ('endpoint', 'a_legacy_endpoint'), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) self.assertAbsent( 'flask_http_request_duration_seconds_bucket', ('path', '/test'), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) def test_group_by_deprecated_late_warning(self): warnings.filterwarnings('once', category=DeprecationWarning) with warnings.catch_warnings(record=True) as initial: metrics = self.metrics(export_defaults=False) self.assertEqual(len(initial), 0) with warnings.catch_warnings(record=True) as w: metrics.export_defaults(group_by_endpoint=True) # make sure we have the deprecation warning for this self.assertEqual(len(w), 1) self.assertEqual(w[0].category, DeprecationWarning) self.assertIn('group_by_endpoint', str(w[0].message)) def test_group_by_full_path(self): self.metrics(group_by='full_path') @self.app.route('/') def a_test_endpoint(url): return url + ' is OK' self.client.get('/test?x=1') self.client.get('/test?x=2') self.client.get('/test?x=3') for path in ('/test?x=1', '/test?x=2', '/test?x=3'): self.assertMetric( 'flask_http_request_duration_seconds_bucket', '1.0', ('full_path', path), ('status', 200), ('le', '+Inf'), ('method', 'GET'), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '1.0', ('full_path', path), ('status', 200), ('method', 'GET'), endpoint='/metrics' ) def test_group_by_func(self): def composite(r): return '%s::%s >> %s' % ( r.method, r.path, r.args.get('type', 'none') ) self.metrics(group_by=composite) @self.app.route('/', methods=['GET', 'POST']) def a_test_endpoint(url): return url + ' is OK' self.client.get('/sample?type=A') self.client.get('/sample?type=Beta') self.client.post('/plain') for value in ('GET::/sample >> A', 'GET::/sample >> Beta', 'POST::/plain >> none'): self.assertMetric( 'flask_http_request_duration_seconds_bucket', '1.0', ('composite', value), ('status', 200), ('le', '+Inf'), ('method', value.split('::').pop(0)), endpoint='/metrics' ) self.assertMetric( 'flask_http_request_duration_seconds_count', '1.0', ('composite', value), ('status', 200), ('method', value.split('::').pop(0)), endpoint='/metrics' ) def test_group_by_lambda_is_not_supported(self): try: self.metrics(group_by=lambda r: '%s-%s' % (r.method, r.path)) self.fail('Expected to fail on grouping by lambda') except Exception as ex: self.assertIn('invalid label', str(ex).lower()) prometheus_flask_exporter-0.23.1/tests/test_metric_initialization.py000066400000000000000000000064301464362612600262620ustar00rootroot00000000000000from abc import ABC, abstractmethod from flask import request from unittest_helper import BaseTestCase # The class nesting avoids that the abstract base class will be tested (which is not possible because it is abstract..) class MetricInitializationTest: class MetricInitializationTest(BaseTestCase, ABC): metric_suffix = None @property @abstractmethod def metric_type(self): pass def get_metric_decorator(self, metrics): return getattr(metrics, self.metric_type) def _test_metric_initialization(self, labels=None, initial_value_when_only_static_labels=True): metrics = self.metrics() metric_decorator = self.get_metric_decorator(metrics) test_path = '/test/1' @self.app.route(test_path) @metric_decorator('metric_1', 'Metric 1', labels=labels, initial_value_when_only_static_labels=initial_value_when_only_static_labels) def test1(): return 'OK' if labels: # replace callable with the "expected" result if 'path' in labels: labels['path'] = test_path label_value_pairs = labels.items() else: label_value_pairs = [] prometheus_metric_name = 'metric_1' if self.metric_suffix: prometheus_metric_name += self.metric_suffix # test metric value before any incoming HTTP call self.assertMetric(prometheus_metric_name, '0.0', *label_value_pairs) self.client.get('/test/1') if self.metric_type == 'gauge': expected_metric_value = '0.0' else: expected_metric_value = '1.0' self.assertMetric(prometheus_metric_name, expected_metric_value, *label_value_pairs) def test_initial_value_no_labels(self): self._test_metric_initialization() def test_initial_value_only_static_labels(self): labels = {'label_name': 'label_value'} self._test_metric_initialization(labels) def test_initial_value_only_static_labels_no_initialization(self): labels = {'label_name': 'label_value'} self.assertRaises(AssertionError, self._test_metric_initialization, labels, initial_value_when_only_static_labels=False) def test_initial_value_callable_label(self): labels = {'path': lambda: request.path} self.assertRaises(AssertionError, self._test_metric_initialization, labels) class HistogramInitializationTest(MetricInitializationTest.MetricInitializationTest): metric_suffix = '_count' @property def metric_type(self): return 'histogram' class SummaryInitializationTest(MetricInitializationTest.MetricInitializationTest): metric_suffix = '_count' @property def metric_type(self): return 'summary' class GaugeInitializationTest(MetricInitializationTest.MetricInitializationTest): @property def metric_type(self): return 'gauge' class CounterInitializationTest(MetricInitializationTest.MetricInitializationTest): metric_suffix = '_total' @property def metric_type(self): return 'counter' prometheus_flask_exporter-0.23.1/tests/test_metrics.py000066400000000000000000000125641464362612600233430ustar00rootroot00000000000000from unittest_helper import BaseTestCase from flask import request class MetricsTest(BaseTestCase): def test_histogram(self): metrics = self.metrics() @self.app.route('/test/1') @metrics.histogram('hist_1', 'Histogram 1') def test1(): return 'OK' @self.app.route('/test/2') @metrics.histogram('hist_2', 'Histogram 2', labels={ 'uri': lambda: request.path, 'code': lambda r: r.status_code }) def test2(): return 'OK' @self.app.route('/test//') @metrics.histogram('hist_3', 'Histogram 3', labels={ 'x_value': lambda: request.view_args['x'], 'y_value': lambda: request.view_args['y'] }, buckets=(0.7, 2.9)) def test3(x, y): return 'OK: %d/%d' % (x, y) self.assertMetric('hist_1_count', '0.0') self.client.get('/test/1') self.assertMetric('hist_1_count', '1.0') self.assertMetric('hist_1_bucket', '1.0', ('le', '2.5')) self.client.get('/test/2') self.assertMetric( 'hist_2_count', '1.0', ('uri', '/test/2'), ('code', 200) ) self.assertMetric( 'hist_2_bucket', '1.0', ('le', '1.0'), ('uri', '/test/2'), ('code', 200) ) self.client.get('/test/3/4') self.assertMetric( 'hist_3_count', '1.0', ('x_value', '3'), ('y_value', '4') ) self.assertMetric( 'hist_3_bucket', '1.0', ('le', '0.7'), ('x_value', '3'), ('y_value', '4') ) def test_summary(self): metrics = self.metrics() @self.app.route('/test/1') @metrics.summary('sum_1', 'Summary 1') def test1(): return 'OK' @self.app.route('/test/2') @metrics.summary('sum_2', 'Summary 2', labels={ 'uri': lambda: request.path, 'code': lambda r: r.status_code, 'variant': 2 }) def test2(): return 'OK' self.assertMetric('sum_1_count', '0.0') self.client.get('/test/1') self.assertMetric('sum_1_count', '1.0') self.client.get('/test/2') self.assertMetric( 'sum_2_count', '1.0', ('uri', '/test/2'), ('code', 200), ('variant', 2) ) def test_gauge(self): metrics = self.metrics() @self.app.route('/test/1') @metrics.gauge('gauge_1', 'Gauge 1') def test1(): self.assertMetric('gauge_1', '1.0') return 'OK' @self.app.route('/test/') @metrics.gauge('gauge_2', 'Gauge 2', labels={ 'uri': lambda: request.path, 'a_value': lambda: request.view_args['a'] }) def test2(a): self.assertMetric( 'gauge_2', '1.0', ('uri', '/test/2'), ('a_value', 2) ) return 'OK: %d' % a self.assertMetric('gauge_1', '0.0') self.client.get('/test/1') self.assertMetric('gauge_1', '0.0') self.client.get('/test/2') self.assertMetric( 'gauge_2', '0.0', ('uri', '/test/2'), ('a_value', 2) ) def test_counter(self): metrics = self.metrics() @self.app.route('/test/1') @metrics.counter('cnt_1', 'Counter 1') def test1(): return 'OK' @self.app.route('/test/2') @metrics.counter('cnt_2', 'Counter 2', labels={ 'uri': lambda: request.path, 'code': lambda r: r.status_code }) def test2(): return 'OK' self.assertMetric('cnt_1_total', '0.0') self.client.get('/test/1') self.assertMetric('cnt_1_total', '1.0') self.client.get('/test/1') self.assertMetric('cnt_1_total', '2.0') self.client.get('/test/1') self.assertMetric('cnt_1_total', '3.0') self.client.get('/test/2') self.assertMetric( 'cnt_2_total', '1.0', ('uri', '/test/2'), ('code', 200) ) def test_default_format(self): self.metrics() @self.app.route('/example') def test(): return 'OK' for _ in range(5): self.client.get('/example') from prometheus_client.exposition import CONTENT_TYPE_LATEST response = self.client.get('/metrics') self.assertEqual(response.status_code, 200) self.assertEqual(response.content_type, CONTENT_TYPE_LATEST) self.assertNotIn('# EOF', str(response.data)) self.assertRegex( str(response.data), 'flask_http_request_duration_seconds_count\\{[^}]+\\} 5.0') def test_openmetrics_format(self): self.metrics() @self.app.route('/example') def test(): return 'OK' for _ in range(5): self.client.get('/example') from prometheus_client.openmetrics.exposition import CONTENT_TYPE_LATEST response = self.client.get('/metrics', headers={'Accept': 'application/openmetrics-text'}) self.assertEqual(response.status_code, 200) self.assertEqual(response.content_type, CONTENT_TYPE_LATEST) self.assertIn('# EOF', str(response.data)) self.assertRegex( str(response.data), 'flask_http_request_duration_seconds_count\\{[^}]+\\} 5.0') prometheus_flask_exporter-0.23.1/tests/unittest_helper.py000066400000000000000000000064361464362612600240550ustar00rootroot00000000000000import re import sys import unittest import prometheus_client from flask import Flask from prometheus_flask_exporter import PrometheusMetrics class BaseTestCase(unittest.TestCase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if sys.version_info.major < 3: self.assertRegex = self.assertRegexpMatches self.assertNotRegex = self.assertNotRegexpMatches def setUp(self): self.app = Flask(__name__) self.app.testing = True self.client = self.app.test_client() # reset the underlying Prometheus registry prometheus_client.REGISTRY = prometheus_client.CollectorRegistry(auto_describe=True) def metrics(self, **kwargs): return PrometheusMetrics(self.app, registry=kwargs.pop('registry', None), **kwargs) def assertMetric(self, name, value, *labels, **kwargs): if labels: pattern = r'(?ms).*%s\{(%s)\} %s.*' % ( name, ','.join( '(?:%s)="(?:%s)"' % ( '|'.join(str(item) for item, _ in labels), '|'.join( str(item).replace('+', r'\+').replace('?', r'\?') for _, item in labels ) ) for _ in labels ), value ) else: pattern = '(?ms).*%s %s.*' % (name, value) response = self.client.get(kwargs.get('endpoint', '/metrics')) self.assertEqual(response.status_code, 200) self.assertRegex( str(response.data), pattern, msg='Failing metric: %s%s, Regexp didn\'t match' % (name, dict(labels)) ) if not labels: return match = re.sub(pattern, r'\1', str(response.data)) for item in labels: self.assertIn(('%s="%s"' % item), match) def assertAbsent(self, name, *labels, **kwargs): if labels: pattern = r'(?ms).*%s\{(%s)\} .*' % ( name, ','.join( '(?:%s)="(?:%s)"' % ( '|'.join(str(item) for item, _ in labels), '|'.join(str(item).replace('+', r'\+') for _, item in labels) ) for _ in labels ) ) else: pattern = '.*%s [0-9.]+.*' % name response = self.client.get(kwargs.get('endpoint', '/metrics')) self.assertEqual(response.status_code, 200) self.assertNotRegex(str(response.data), pattern) def assumeMinimumFlaskVersion(self, version): from flask import __version__ as flask_version desired_version = list(map(int, version.split('.'))) actual_version = list(map(int, flask_version.split('.'))) if actual_version < desired_version: self.skipTest(reason='Flask version %s is before the desired version %s' % (flask_version, version)) def assumeBeforeFlaskVersion(self, version): from flask import __version__ as flask_version desired_version = list(map(int, version.split('.'))) actual_version = list(map(int, flask_version.split('.'))) if actual_version >= desired_version: self.skipTest(reason='Flask version %s is not before the desired version %s' % (flask_version, version))